Chart.js: Time scale bar charts overlap

Created on 22 Apr 2016  ·  24Comments  ·  Source: chartjs/Chart.js

I'm pretty sure this is not default behaviour as when using a time axis scale with a bar chart, the bars overlap. If you compare this to a standard bar chart the bars would shrink in size. The screen shots are taken from the samples.

Resized
resized

Overlapping
overlapped

As far as I can work out I believe it could be to do with the calculation of the width based on the number of ticks. As the ticks are reduced on the time version to fit the label in the calculation should really still use the number of points. But so far I have not manage to find the exact location in the source.

John

help wanted time scale bug

Most helpful comment

A colleague and I have been trying to fix the overlapping issue and have come up with the following naive approach that temporarily fixes the issue for our needs. Since we are new to the ChartJS library and are not able to spend the time to re-work the bar chart so that our modifications are properly optimized we would like to share our changes with the community.

In Summary

We have modified the getRuler function so that it can find the shortest distance between two points in a data series and uses that distance for the bar width. As mentioned above, this approach is not fully optimized as it will require a little rework so that getRuler is not recalculating the minimum distance every single time it is called. Instead it will need to integrate with responsive and somehow cache this calculation.

Whats Fixed

  • Bar Widths on Time Scale Axes (getRuler)
  • Positioning of Stacked Bars on Time Scale Axes (getRuler)
  • Category Widths on A Bar Chart's Time Scale Axes (getRuler)
  • Alignment of Bars over Time Scale Labels (calculateBarX)

First Change made

getRuler: function(index) {
  var me = this;
  var meta = me.getMeta();
  var xScale = me.getScaleForId(meta.xAxisID);
  var datasetCount = me.getBarCount();

  var tickWidth;

  if (xScale.options.type === 'category') {
    tickWidth = xScale.getPixelForTick(index + 1) - xScale.getPixelForTick(index);
  } else {
    // Average width
    tickWidth = xScale.width / xScale.ticks.length;
  }
  var categoryWidth = tickWidth * xScale.options.categoryPercentage;
  var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2;
  var fullBarWidth = categoryWidth / datasetCount;

  /*** START MODIFICATIONS ***/

  // correct the bar width in case of time series modifying ticks
  if (xScale.ticks.length !== me.chart.data.labels.length) {
    var dataPos = [];
    for (var i = 0; i < meta.data.length; i++) {
      dataPos.push(xScale.getPixelForValue(null, meta.data[i]._index, meta.data[i]._datasetIndex, me.chart.isCombo));
    }

    dataPos.sort(function(a,b){
      return a-b;
    })

    var minDist;
    var tmpDist;
    for (var i = 1; i < dataPos.length; i++) {
      tmpDist = dataPos[i] - dataPos[i-1];
      if(!minDist || tmpDist < minDist) { minDist = tmpDist; }
    }

    if(fullBarWidth > minDist) { fullBarWidth = minDist; }

    categoryWidth = fullBarWidth;
    fullBarWidth = fullBarWidth / datasetCount;
  }

  /*** END MODIFICATIONS ***/

  var barWidth = fullBarWidth * xScale.options.barPercentage;
  var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage);

  return {
    datasetCount: datasetCount,
    tickWidth: tickWidth,
    categoryWidth: categoryWidth,
    categorySpacing: categorySpacing,
    fullBarWidth: fullBarWidth,
    barWidth: barWidth,
    barSpacing: barSpacing
  };
}

Second Change Made

calculateBarX: function(index, datasetIndex) {
  var me = this;
  var meta = me.getMeta();
  var xScale = me.getScaleForId(meta.xAxisID);
  var barIndex = me.getBarIndex(datasetIndex);

  var ruler = me.getRuler(index);
  var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo);
  leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0;

  /*** START MODIFICATIONS ***/
  if (xScale.options.type == 'time') { leftTick -= ruler.barWidth * 1.38; }
  /*** END MODIFICATIONS ***/

  if (xScale.options.stacked) {
    return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing;
  }

  return leftTick +
    (ruler.barWidth / 2) +
    ruler.categorySpacing +
    (ruler.barWidth * barIndex) +
    (ruler.barSpacing / 2) +
    (ruler.barSpacing * barIndex);
}

Before the code change: https://jsfiddle.net/a1x7p17h/2/

After the code change: https://jsfiddle.net/fmbycrLx/2/

All 24 comments

Little update, I can get the bar items to a more reasonable width by adding some extra logic into the getRuler method which compares the length of labels and the ticks as a percentage then apply that logic to the width of bar. Pretty sure you guys will have a better way though.

if (xScale.ticks.length !== this.chart.data.labels.length) { var perc = xScale.ticks.length / this.chart.data.labels.length; fullBarWidth = (fullBarWidth * perc); }

I also get the issue same as #2277 with the first or last item missing.

@johnw86 thanks for looking into this a bit more. The getRuler function causes some other problems, documented in #2216, that can probably be fixed together.

Could you post a screenshot or fiddle of your chart looks like after your fix?

Yeah no problem screen shots before and after below:

before

after

Looks nice! Could you create a PR for this?

+1 - was just having this exact problem!

Thank you!

I tried pulling this patch into my charts, still seeing the overlap - problem goes away if I don't use the time scale ..

The code from the PR is definitely being called but - still overlaps..

Its actually ok if I drag the browser super wide, but at a certain point - and way before they should overlap - the chart changes from skinny bars that are well fitting to fat bars that are overlapping.

You got a screen shot @sblackstone, any chance you can put the code up somewhere to take a look? jsfiddle etc

In my case, this fix seems to introduce the same problem it tried to fix (im using v2.1.6 where this fix is already in the code). Let me show the output before removing the fix piece of code from chart js code:
beforecomment

Then after commenting this code:
if (xScale.ticks.length !== me.chart.data.labels.length) { var perc = xScale.ticks.length / me.chart.data.labels.length; fullBarWidth = (fullBarWidth * perc); }
I get the following output, which seems to fix my issue:
aftercomment

Im not sure about the "me.chart.data.labels.length" variable, where and how is its value defined? I think this problem has something to do with the fact im using two datasets, but im not sure, it might be that im doing something wrong.

I temporarily got around this issue by setting a much lower value for categoryPercentage:

myChart = new Chart(ctx,
  type: 'bar'
  options:
    scales:
      xAxes: [
        stacked: true
        type: 'time'
        categoryPercentage: 0.07
      ]

Actually, that workaround proved to be wrong. It makes labels and bars in chart mismatch:

image

A colleague and I have been trying to fix the overlapping issue and have come up with the following naive approach that temporarily fixes the issue for our needs. Since we are new to the ChartJS library and are not able to spend the time to re-work the bar chart so that our modifications are properly optimized we would like to share our changes with the community.

In Summary

We have modified the getRuler function so that it can find the shortest distance between two points in a data series and uses that distance for the bar width. As mentioned above, this approach is not fully optimized as it will require a little rework so that getRuler is not recalculating the minimum distance every single time it is called. Instead it will need to integrate with responsive and somehow cache this calculation.

Whats Fixed

  • Bar Widths on Time Scale Axes (getRuler)
  • Positioning of Stacked Bars on Time Scale Axes (getRuler)
  • Category Widths on A Bar Chart's Time Scale Axes (getRuler)
  • Alignment of Bars over Time Scale Labels (calculateBarX)

First Change made

getRuler: function(index) {
  var me = this;
  var meta = me.getMeta();
  var xScale = me.getScaleForId(meta.xAxisID);
  var datasetCount = me.getBarCount();

  var tickWidth;

  if (xScale.options.type === 'category') {
    tickWidth = xScale.getPixelForTick(index + 1) - xScale.getPixelForTick(index);
  } else {
    // Average width
    tickWidth = xScale.width / xScale.ticks.length;
  }
  var categoryWidth = tickWidth * xScale.options.categoryPercentage;
  var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2;
  var fullBarWidth = categoryWidth / datasetCount;

  /*** START MODIFICATIONS ***/

  // correct the bar width in case of time series modifying ticks
  if (xScale.ticks.length !== me.chart.data.labels.length) {
    var dataPos = [];
    for (var i = 0; i < meta.data.length; i++) {
      dataPos.push(xScale.getPixelForValue(null, meta.data[i]._index, meta.data[i]._datasetIndex, me.chart.isCombo));
    }

    dataPos.sort(function(a,b){
      return a-b;
    })

    var minDist;
    var tmpDist;
    for (var i = 1; i < dataPos.length; i++) {
      tmpDist = dataPos[i] - dataPos[i-1];
      if(!minDist || tmpDist < minDist) { minDist = tmpDist; }
    }

    if(fullBarWidth > minDist) { fullBarWidth = minDist; }

    categoryWidth = fullBarWidth;
    fullBarWidth = fullBarWidth / datasetCount;
  }

  /*** END MODIFICATIONS ***/

  var barWidth = fullBarWidth * xScale.options.barPercentage;
  var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage);

  return {
    datasetCount: datasetCount,
    tickWidth: tickWidth,
    categoryWidth: categoryWidth,
    categorySpacing: categorySpacing,
    fullBarWidth: fullBarWidth,
    barWidth: barWidth,
    barSpacing: barSpacing
  };
}

Second Change Made

calculateBarX: function(index, datasetIndex) {
  var me = this;
  var meta = me.getMeta();
  var xScale = me.getScaleForId(meta.xAxisID);
  var barIndex = me.getBarIndex(datasetIndex);

  var ruler = me.getRuler(index);
  var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo);
  leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0;

  /*** START MODIFICATIONS ***/
  if (xScale.options.type == 'time') { leftTick -= ruler.barWidth * 1.38; }
  /*** END MODIFICATIONS ***/

  if (xScale.options.stacked) {
    return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing;
  }

  return leftTick +
    (ruler.barWidth / 2) +
    ruler.categorySpacing +
    (ruler.barWidth * barIndex) +
    (ruler.barSpacing / 2) +
    (ruler.barSpacing * barIndex);
}

Before the code change: https://jsfiddle.net/a1x7p17h/2/

After the code change: https://jsfiddle.net/fmbycrLx/2/

A colleague of mine just ran into this issue, too.

is there any solution for this?

For what it's worth, in one of my projects I fixed the improper bar positioning with time scale axis by modifying the source code.

Where Chart.controllers.bar is defined, change the code as follows:
At line 6341 (in version 2.5 of Chart.bundle.js) is the calculateBarX definition. Change the return from

return leftTick + (ruler.barWidth / 2) + ruler.categorySpacing + (ruler.barWidth * stackIndex) + (ruler.barSpacing / 2);/* +    (ruler.barSpacing * stackIndex);

to
return leftTick;

Similarly, for Chart.controllers.horizontalBar change code as follows:
At line 6669 (in version 2.5 of Chart.bundle.js) is the calculateBarY definition. Change the return from

return topTick + (ruler.barHeight / 2) + ruler.categorySpacing + (ruler.barHeight * stackIndex) +
(ruler.barSpacing / 2) + (ruler.barSpacing * stackIndex);

to
return topTick;

Obviously, the line numbers may be different for different versions of the source code.

Beware! I have not tested these changes on any charts other than the time label data in my charts, so I have no idea if this might screw something else up. But it fixed the issue for my charts.

Hi everybody,

Have you find a solution for this bug ?

Best regards,
Romain.

Hi everyone !

I also encounter this problem.
To avoid the overlapping I worked Time Scale properties unit and unitStepSize.

In my case I manipulate timestamp with a step of 30 minutes.
By setting unit to "minute" and unitStepSize to 30, the overlapping disappear.

Here a Plunker: https://plnkr.co/edit/2I0uYADSLAg8fiELEEs3?p=preview

Only @arasith's solution here worked. Beware that datasetCount has been renamed to stackCount

This is still a pretty big issue. I am still getting this problem in 2.6.0 ( along with the x-axis cutoff problem #2277 ):
chartglitch

Is anyone working on this? I think this is a pretty big issue, specially when working with combined bar and line charts. If no one is working on this, in order to work wit @arasith solution and build a more robust solution (including responsive for example) and try to push it to the project. Thanks.

I found the overlapping problem is due to the bar size calculation is base on the number of ticks in the time scale. Under the original getRule function in the controller.bar.js:
var tickSize = fullSize / scale.ticks.length;

The problem happened when the time scale is optimised with step size > 1, causing error in the bar size calculation. The bar size becomes bigger and overlaps to the neighbouring bar. The correct calculation should be:
var tickSize = fullSize / (scale.ticks.length * scale.stepSize);

I'm using the new Dataset Controller Interface and Scale Interface to derive my customer Bar chart controller and TimeScale to modify 2 lines of code each in of these interface.

The code I changed is as below:
Under the new TimeScale interface:

buildTicks: function() {
    ...
    me.stepSize = stepSize;
    ...
};

Under the new Chart.controllers.derivedBar interface:

getRuler: function() {
    ...
    var tickSize = fullSize / (scale.ticks.length * scale.stepSize);
    ...
};

Check out the complete code here https://codepen.io/allentcm/pen/KqQGoE/

@allentcm - This works really well! I don't know where to put this code, but if anyone does, could you please make a pull request with this modification?

It still cuts off the last entry, but that's a different problem. For now, I'll be happy with this fix.

I have fixed this for a project I was working on and thought I would submit a PR here to fix it as well, how do I go about this?

@darkshuffle I would wait until we get #4545 to be merged and see if it fixes your issue. If not, then you can submit a PR with the new code.

Yeah, just had a look at the latest and it seems to have been sorted with a bunch of scale updates 👍

Was this page helpful?
0 / 5 - 0 ratings