【问题标题】:Chart.js dynamic bar widthChart.js 动态条形宽度
【发布时间】:2015-11-15 06:59:43
【问题描述】:

我需要渲染一组连续块的时间序列数据。

我需要用自己的 Y 值描述一系列可能跨越数小时或数分钟的条形图。

我不确定 ChartJS 是否是我应该使用的,但我已经研究过扩展 Bar 类型,但似乎很难将每个条形编码为相同的宽度。 Scale 类在内部用于标签、图表宽度等,而不仅仅是条形本身。

我正在尝试在 Excel 中实现类似的功能:http://peltiertech.com/variable-width-column-charts/

有没有其他人不得不想出类似的东西?

【问题讨论】:

  • stackoverflow.com/questions/13058195?顺便说一句,它是 highcharts(如果在商业上使用需要许可证)而不是 chart.js
  • 谢谢,如果我找不到基于 D3 构建的版本(我想在栏上绘制其他项目),这是一个不错的选择。
  • D3 是一个非常好的选择。如果你还没有看过它 - 结帐stackoverflow.com/questions/21610828

标签: bar-chart chart.js


【解决方案1】:

我发现我需要这样做,@potatopeelings 的回答很棒,但对于 Chartjs 的第 2 版来说已经过时了。我通过扩展栏创建自己的控制器/图表类型做了类似的事情:

//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
            };
        },
    });
};

然后你需要像这样将它添加到你的 chartjs 实例中:

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

barw(Chart); //add plugin to chartjs

最后,与其他答案类似,需要将条形宽度的权重添加到数据集中:

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]
        },
    ]
};

这有望使某人走上正确的道路。我所拥有的当然并不完美,但如果您确保数据点的权重数量正确,那么您应该是对的。

祝你好运。

【讨论】:

    【解决方案2】:

    对于 Chart.js,您可以创建一个基于 bar 类的新扩展来执行此操作。虽然有点复杂 - 但是大部分是条形库代码的复制粘贴

    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);
        }
    });
    

    你传入下面的宽度

    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]
            },
        ]
    };
    

    你这样称呼它

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

    小提琴 - http://jsfiddle.net/moye0cp4/


    【讨论】:

    • 我怎么能用最新版本做类似的事情?
    【解决方案3】:

    这是基于@Shane的代码,我只是发布帮助,因为这是一个常见问题。

    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
      };
    }
    

    【讨论】:

    • 您能否在回答中说出这段代码的去向?