3

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

James
  • 2,458
  • 3
  • 26
  • 50
  • http://stackoverflow.com/questions/13058195? It's highcharts btw (needs a license if used commercially) and not chart.js – potatopeelings Aug 21 '15 at 11:37
  • Thank you, that is a good option if I cannot find a version built on D3 (I want to draw other items on the bar). – James Aug 21 '15 at 11:41
  • D3 is a really good option. And incase you haven't already seen it - checkout http://stackoverflow.com/questions/21610828 – potatopeelings Aug 21 '15 at 11:44

3 Answers3

2

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.

Shane
  • 401
  • 4
  • 16
1

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
  };
}
  • 1
    Can you maybe say in your answer where this code goes? – talz Jan 19 '21 at 11:18
  • yes, you can extend the Chart.controllers like this and inside you paste the code, as I metioned this was based on @Shane code, `Chart.controllers.customBar = Chart.controllers.bar.extend({ calculateBarIndexPixels(datasetIndex, index, ruler) {` – Bryan Acuña Núñez Jan 18 '22 at 23:20
0

For Chart.js you can create a new extension based on the bar class to do this. It's a bit involved though - however most of it is a copy paste of the bar type library code

Chart.types.Bar.extend({
    name: "BarAlt",
    // all blocks that don't have a comment are a direct copy paste of the Chart.js library code
    initialize: function (data) {

        // the sum of all widths
        var widthSum = data.datasets[0].data2.reduce(function (a, b) { return a + b }, 0);
        // cumulative sum of all preceding widths
        var cumulativeSum = [ 0 ];
        data.datasets[0].data2.forEach(function (e, i, arr) {
            cumulativeSum.push(cumulativeSum[i] + e);
        })


        var options = this.options;

        // completely rewrite this class to calculate the x position and bar width's based on data2
        this.ScaleClass = Chart.Scale.extend({
            offsetGridLines: true,
            calculateBarX: function (barIndex) {
                var xSpan = this.width - this.xScalePaddingLeft;
                var x = this.xScalePaddingLeft + (cumulativeSum[barIndex] / widthSum * xSpan) - this.calculateBarWidth(barIndex) / 2;
                return x + this.calculateBarWidth(barIndex);
            },
            calculateBarWidth: function (index) {
                var xSpan = this.width - this.xScalePaddingLeft;
                return (xSpan * data.datasets[0].data2[index] / widthSum);
            }
        });

        this.datasets = [];

        if (this.options.showTooltips) {
            Chart.helpers.bindEvents(this, this.options.tooltipEvents, function (evt) {
                var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : [];

                this.eachBars(function (bar) {
                    bar.restore(['fillColor', 'strokeColor']);
                });
                Chart.helpers.each(activeBars, function (activeBar) {
                    activeBar.fillColor = activeBar.highlightFill;
                    activeBar.strokeColor = activeBar.highlightStroke;
                });
                this.showTooltip(activeBars);
            });
        }

        this.BarClass = Chart.Rectangle.extend({
            strokeWidth: this.options.barStrokeWidth,
            showStroke: this.options.barShowStroke,
            ctx: this.chart.ctx
        });

        Chart.helpers.each(data.datasets, function (dataset, datasetIndex) {

            var datasetObject = {
                label: dataset.label || null,
                fillColor: dataset.fillColor,
                strokeColor: dataset.strokeColor,
                bars: []
            };

            this.datasets.push(datasetObject);

            Chart.helpers.each(dataset.data, function (dataPoint, index) {
                datasetObject.bars.push(new this.BarClass({
                    value: dataPoint,
                    label: data.labels[index],
                    datasetLabel: dataset.label,
                    strokeColor: dataset.strokeColor,
                    fillColor: dataset.fillColor,
                    highlightFill: dataset.highlightFill || dataset.fillColor,
                    highlightStroke: dataset.highlightStroke || dataset.strokeColor
                }));
            }, this);

        }, this);

        this.buildScale(data.labels);
        // remove the labels - they won't be positioned correctly anyway
        this.scale.xLabels.forEach(function (e, i, arr) {
            arr[i] = '';
        })

        this.BarClass.prototype.base = this.scale.endPoint;

        this.eachBars(function (bar, index, datasetIndex) {
            // change the way the x and width functions are called
            Chart.helpers.extend(bar, {
                width: this.scale.calculateBarWidth(index),
                x: this.scale.calculateBarX(index),
                y: this.scale.endPoint
            });

            bar.save();
        }, this);

        this.render();
    },
    draw: function (ease) {
        var easingDecimal = ease || 1;
        this.clear();

        var ctx = this.chart.ctx;

        this.scale.draw(1);

        Chart.helpers.each(this.datasets, function (dataset, datasetIndex) {
            Chart.helpers.each(dataset.bars, function (bar, index) {
                if (bar.hasValue()) {
                    bar.base = this.scale.endPoint;
                    // change the way the x and width functions are called
                    bar.transition({
                        x: this.scale.calculateBarX(index),
                        y: this.scale.calculateY(bar.value),
                        width: this.scale.calculateBarWidth(index)
                    }, easingDecimal).draw();

                }
            }, this);

        }, this);
    }
});

You pass in the widths like below

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],
            data2: [10, 20, 30, 20, 10, 40, 10]
        },
    ]
};

and you call it like so

var ctx = document.getElementById('canvas').getContext('2d');
var myLineChart = new Chart(ctx).BarAlt(data);

Fiddle - http://jsfiddle.net/moye0cp4/


enter image description here

potatopeelings
  • 40,709
  • 7
  • 95
  • 119