Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Chart.js dynamic bar width

I have a requirement to render a set of time series data of contiguous blocks.

I need to describe a series of bars which could span many hours, or just minutes, with their own Y value.

I'm not sure if ChartJS is what I should be using for this, but I have looked at extending the Bar type, but it seems very hard coded for each bar to be the same width. The Scale Class internally is used for labels, chart width etc, not just the bars themselves.

I am trying to achieve something like this that works in Excel: http://peltiertech.com/variable-width-column-charts/

Has anyone else had to come up with something similar?

enter image description here

like image 224
James Avatar asked Oct 20 '25 01:10

James


2 Answers

I found I needed to do this and the answer by @potatopeelings was great, but out of date for version 2 of Chartjs. I did something similar by creating my own controller/chart type via extending bar:

//controller.barw.js

module.exports = function(Chart) {

    var helpers = Chart.helpers;

    Chart.defaults.barw = {
        hover: {
            mode: 'label'
        },

        scales: {
            xAxes: [{
                type: 'category',

                // Specific to Bar Controller
                categoryPercentage: 0.8,
                barPercentage: 0.9,

                // grid line settings
                gridLines: {
                    offsetGridLines: true
                }
            }],
            yAxes: [{
                type: 'linear'
            }]
        }
    };

    Chart.controllers.barw = Chart.controllers.bar.extend({
        
        /**
         * @private
         */
        getRuler: function() {
            var me = this;
            var scale = me.getIndexScale();
            var options = scale.options;
            var stackCount = me.getStackCount();
            var fullSize = scale.isHorizontal()? scale.width : scale.height;
            var tickSize = fullSize / scale.ticks.length;
            var categorySize = tickSize * options.categoryPercentage;
            var fullBarSize = categorySize / stackCount;
            var barSize = fullBarSize * options.barPercentage;

            barSize = Math.min(
                helpers.getValueOrDefault(options.barThickness, barSize),
                helpers.getValueOrDefault(options.maxBarThickness, Infinity));

            return {
                fullSize: fullSize,
                stackCount: stackCount,
                tickSize: tickSize,
                categorySize: categorySize,
                categorySpacing: tickSize - categorySize,
                fullBarSize: fullBarSize,
                barSize: barSize,
                barSpacing: fullBarSize - barSize,
                scale: scale
            };
        },


        /**
         * @private
         */
        calculateBarIndexPixels: function(datasetIndex, index, ruler) {
            var me = this;
            var scale = ruler.scale;
            var options = scale.options;
            var isCombo = me.chart.isCombo;
            var stackIndex = me.getStackIndex(datasetIndex);
            var base = scale.getPixelForValue(null, index, datasetIndex, isCombo);
            var size = ruler.barSize;
            
            var dataset = me.chart.data.datasets[datasetIndex];
            if(dataset.weights) {
                var total = dataset.weights.reduce((m, x) => m + x, 0);
                var perc = dataset.weights[index] / total;
                var offset = 0;
                for(var i = 0; i < index; i++) {
                    offset += dataset.weights[i] / total;
                }
                var pixelOffset = Math.round(ruler.fullSize * offset);
                var base = scale.isHorizontal() ? scale.left : scale.top;
                base += pixelOffset;

                size = Math.round(ruler.fullSize * perc);
                size -= ruler.categorySpacing;
                size -= ruler.barSpacing;
            }            

            base -= isCombo? ruler.tickSize / 2 : 0;
            base += ruler.fullBarSize * stackIndex;
            base += ruler.categorySpacing / 2;
            base += ruler.barSpacing / 2;

            return {
                size: size,
                base: base,
                head: base + size,
                center: base + size / 2
            };
        },
    });
};

Then you need to add it to your chartjs instance like this:

import Chart from 'chart.js'
import barw from 'controller.barw'

barw(Chart); //add plugin to chartjs

and finally, similar to the other answer, the weights of the bar widths need to be added to the data set:

var data = {
    labels: ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
    datasets: [
        {
            label: "My First dataset",
            fillColor: "rgba(220,220,220,0.5)",
            strokeColor: "rgba(220,220,220,0.8)",
            highlightFill: "rgba(220,220,220,0.7)",
            highlightStroke: "rgba(220,220,220,1)",
            data: [65, 59, 80, 30, 56, 65, 40],
            weights: [1, 0.9, 1, 2, 1, 4, 0.3]
        },
    ]
};

This will hopefully get someone onto the right track. What I have certainly isn't perfect, but if you make sure you have the right number of weight to data points, you should be right.

Best of luck.

like image 139
Shane Avatar answered Oct 22 '25 00:10

Shane


This is based on the @Shane's code, I just posted to help, since is a common question.enter image description here

calculateBarIndexPixels: function (datasetIndex, index, ruler) {
  const options = ruler.scale.options;

  const range = options.barThickness === 'flex' ? computeFlexCategoryTraits(index, ruler, options) : computeFitCategoryTraits(index, ruler, options);
  const barSize = range.chunk;

  const stackIndex = this.getStackIndex(datasetIndex, this.getMeta().stack);

  let center = range.start + range.chunk * stackIndex + range.chunk / 2;
  let size = range.chunk * range.ratio;

  let start = range.start;

  const dataset = this.chart.data.datasets[datasetIndex];
  if (dataset.weights) {
    //the max weight should be one
    size = barSize * dataset.weights[index];
    const meta = this.chart.controller.getDatasetMeta(0);
    const lastModel = index > 0 ? meta.data[index - 1]._model : null;
    //last column takes the full bar
    if (lastModel) {
      //start could be last center plus half of last column width
      start = lastModel.x + lastModel.width / 2;
    }
    center = start + size * stackIndex + size / 2;
  }

  return {
    size: size,
    base: center - size / 2,
    head: center + size / 2,
    center: center
  };
}
like image 20
Bryan Acuña Núñez Avatar answered Oct 22 '25 01:10

Bryan Acuña Núñez