向ChartJS堆叠条形图添加数据维度

yfjy0ee7  于 11个月前  发布在  Chart.js
关注(0)|答案(1)|浏览(134)

我正在尝试创建一个x轴上有一个小时时段的堆叠条形图(上午9点,上午10点,上午11点,...)和计数的y轴。我想按天分组的数据(星期一,星期二,...)和人(Alice,Bob)看起来像这样:x1c 0d1x星期一,星期二,鲍勃,爱丽丝应该是可选选项(隐藏/显示),但没有标签的酒吧-(这只是为了澄清数据)。
理想情况下,我想指定我的数据如下:

const labels = ['9am','10am','11am','12pm'];
const data = {
  labels: labels,
  datasets: [
    {
      label: 'Monday',
      data: [{x:'9am',person:'Bob',y:7},{x:'10am',person:'Bob',y:6},{x:'11am',person:'Bob',y:5},{x:'12pm',y:4},{x:'9am',person:'Alice',y:7},{x:'10am',person:'Alice',y:6},{x:'11am',person:'Alice',y:5},{x:'12pm',y:4}],
      stack: data.x,
    },
    {
      label: 'Tuesday',
      data: [{x:'9am',person:'Bob',y:7},{x:'10am',person:'Bob',y:6},{x:'11am',person:'Bob',y:5},{x:'12pm',y:4},{x:'9am',person:'Alice',y:7},{x:'10am',person:'Alice',y:6},{x:'11am',person:'Alice',y:5},{x:'12pm',y:4}],
    }
  ]
};

字符串
或(编辑1)

const labels = ['9am','10am','11am','12pm'];
const data = {
  labels: labels,
  datasets: [
    {
      label: 'Bob',
      data: [{x:'9am',day:'Monday',y:7},{x:'10am',day:'Monday',y:6},{x:'11am',day:'Monday',y:5},{x:'12pm', day:'Monday', y:4},{x:'9am',day:'Tuesday',y:7},{x:'10am',day:'Tuesday',y:6},{x:'11am',day:'Tuesday',y:5},{x:'12pm',day:'Tuesday',y:4}],
      backgroundColor: 'blue'
    },
    {
      label: 'Alice',
      data: [{x:'9am',day:'Monday',y:7},{x:'10am',day:'Monday',y:6},{x:'11am',day:'Monday',y:5},{x:'12pm',day:'Monday',y:4},{x:'9am',day:'Tuesday',y:7},{x:'10am',day:'Tuesday',y:6},{x:'11am',day:'Tuesday',y:5},{x:'12pm',day:'Tuesday',y:4}],
      backgroundColor:'amber';
    }
  ]
};


并由一些数据项指定堆栈,如'data.day'。
我试过的配置:

const config = {
  type: 'bar',
  data: data,
  options: {
    plugins: {
      title: {
        display: true,
        text: 'Chart.js Bar Chart - Stacked'
      },
    },
    responsive: true,
    scales: {
      x: {
        stacked: true,
      },
      y: {
        stacked: true
      }
    }
  }
};

const labels = ['9am','10am','11am','12pm'];
const data = {
  labels: labels,
  datasets: [
    {
      label: 'Bob-Monday',
      data: [{x:'9am',y:7},{x:'10am',y:6},{x:'11am',y:5},{x:'12pm',y:4}],
      stack: 'Monday',
    },
    {
      label: 'Alice-Monday',
      data: [{x:'9am',y:7},{x:'10am',y:8},{x:'11am',y:10},{x:'12pm',y:9}],
      stack: 'Monday',
    },
    {
      label: 'Bob-Tue',
      data: [{x:'9am',y:7},{x:'10am',y:6},{x:'11am',y:4},{x:'12pm',y:3}],
      stack: 'Tue',
    },
    {
      label: 'Alice-Tue',
      data: [{x:'9am',y:7},{x:'10am',y:8},{x:'11am',y:10},{x:'12pm',y:9}],
      stack: 'Tue',
    }
  ]
};

const config = {
  type: 'bar',
  data: data,
  options: {
    plugins: {
      title: {
        display: true,
        text: 'Chart.js Bar Chart - Stacked'
      },
    },
    responsive: true,
    scales: {
      x: {
        stacked: true,
      },
      y: {
        stacked: true
      }
    }
  }
};
const ctx = document.getElementById('myChart');
const chart = new Chart(ctx, config);
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<canvas id="myChart"></canvas>

的字符串

g52tjvyc

g52tjvyc1#

由于问题的主要特征似乎是数据由两个变量personday索引的事实,(除了标准的x轴变体hour之外),由于您已经建议以非chart.js格式重新构建数据,但对数据有意义,我建议您的数据结构上的两个层次的格式(以下示例中的dataset2d)并使用一个custom plugin,它将在其beforeInit处理程序中预处理数据。这将允许在datasets2d项的级别设置选项,注意:形式上,数据是三维的,但由于hour是x轴上表示的变量,因此我们将关注其余两个变量。
这个问题的主要难点在于对自定义图例项的要求。幸运的是,chart.js在图例标签配置方面非常灵活,允许以pointStyle的形式提供自定义图例或画布,而选择和取消选择图例项的效果可以通过可自定义的图例单击处理程序来完美控制。
下面的代码片段中的插件datasets2d首先将2d数据简单Map到一维标准数据集,并将堆栈对应于内部label的值。然后,它使用上面提到的图例自定义功能来实现所需的图例项形式和功能。

const labels = ['9am', '10am', '11am', '12pm'];
const data = {
    labels: labels,
    datasets2d: [
        {
            label: 'Bob',
            datasets: [
                {
                    label: 'Monday',
                    data: [{x: '9am', y: 7}, {x: '10am', y: 6}, {x: '11am', y: 5}, {x: '12pm', y: 4}],
                    // or just data: [7, 6, 5, 4],
                    backgroundColor: 'rgba(54, 162, 235, 0.7)',
                },
                {
                    label: 'Tuesday',
                    data: [{x: '9am', y: 7}, {x: '10am', y: 6}, {x: '11am', y: 4}, {x: '12pm', y: 3}],
                    // or just data: [7, 6, 4, 3]
                    backgroundColor: 'rgba(54, 162, 235, 0.5)'
                }
            ]
        },
        {
            label: 'Alice',
            datasets: [
                {
                    label: 'Monday',
                    data: [{x: '9am', y: 7}, {x: '10am', y: 8}, {x: '11am', y: 10}, {x: '12pm', y: 9}],
                    // or just data: [7, 8, 10, 9],
                    backgroundColor: 'rgba(255, 99, 132, 0.7)'
                },
                {
                    label: 'Tuesday',
                    data: [{x: '9am', y: 7}, {x: '10am', y: 8}, {x: '11am', y: 10}, {x: '12pm', y: 9}],
                    // or just data: [7, 8, 10, 9]
                    backgroundColor: 'rgba(255, 99, 132, 0.5)'
                }
            ]
        }
    ]
};

const pluginDatasets2d = {
    id: 'datasets2d',

    _objMinusKeys(obj, minusKeys = []){
        return Object.fromEntries(Object.entries(obj).filter(([key]) => !minusKeys.includes(key)));
    },

    _canvasMultiColor(width, height, colors, horizontal){
        const canvas = document.createElement('canvas');
        canvas.width = width;
        canvas.height = height;
        const ctx = canvas.getContext("2d");
        const nColors = colors.length, frac = 1 / nColors;
        const grad = horizontal ? ctx.createLinearGradient(0, 0, width, 0) : ctx.createLinearGradient(0, height, 0, 0);
        grad.addColorStop(0, colors[0]);
        for(let iColor = 1; iColor < nColors; iColor++){
            grad.addColorStop(iColor * frac, colors[iColor - 1]);
            grad.addColorStop(iColor * frac, colors[iColor]);
        }
        grad.addColorStop(1, colors[nColors - 1]);
        ctx.fillStyle = grad;
        ctx.fillRect(0, 0, width, height);
        return canvas;
    },

    _generateLegendLabel(text, idx, colors, legendPointWidth, hidden, horizontal = true){
        const legendPointHeight = legendPointWidth / 2;
        return {
            text: text,
            datasetIndex: idx,
            fillStyle: '#fff', // not used
            fontColor: '#000',
            hidden: hidden,
            lineCap: 0, // not used
            lineDash: [], // not used
            lineDashOffset: 0, // not used
            lineJoin: 0, // not used
            lineWidth: 0, // not used
            strokeStyle: '#fff', // not used
            pointStyle: this._canvasMultiColor(
                legendPointWidth, legendPointHeight, colors, horizontal),
            rotation: 0
        }
    },

    onLegendClick(e, legendItem, legend){
        const _computeDatasetsToBeHidden = function(legendIndicesDisabled){
                const datasetsHidden = [];
                for(const legendIndex of legendIndicesDisabled){
                    for(const index of legend.legendItems[legendIndex].datasetIndex){
                        if(!datasetsHidden.includes(index)){
                            datasetsHidden.push(index);
                        }
                    }
                }
                datasetsHidden.sort((a,b)=>a-b);
                return datasetsHidden;
            },

            _computeLegendIndicesToBeDisabled = function(datasetsHidden){
                const legendIndicesDisabled = [];
                legend.legendItems.forEach((legendItem, legendIndex) => {
                    const datasetIndices = legendItem.datasetIndex;
                    if(datasetIndices.reduce((s, index) => s && datasetsHidden.includes(index), true)){
                        legendIndicesDisabled.push(legendIndex);
                    }
                });
                return legendIndicesDisabled;
            };

        const legendIndex = legend.legendItems.indexOf(legendItem);
        let legendIndicesDisabled1 = legend.legendItems.map(
            (legendItem, legendIndex) => !legendItem.hidden ? null : legendIndex).filter(x=>x!==null);

        let datasetsHidden1 = _computeDatasetsToBeHidden(legendIndicesDisabled1);
        const clickToEnable = legendIndicesDisabled1.includes(legendIndex);
        let legendIndicesDisabled =  clickToEnable ?
            legendIndicesDisabled1.filter(legendIndex1 => legendIndex1 !== legendIndex) :
            legendIndicesDisabled1.concat(legendIndex).sort((a,b)=>a-b);
        let datasetsHidden = _computeDatasetsToBeHidden(legendIndicesDisabled);

        if(datasetsHidden.length === datasetsHidden1.length && clickToEnable){
            // if the click makes no difference in datasets status (maintaining all other legend items status)
            datasetsHidden1 = datasetsHidden.filter(datasetIdx => !legendItem.datasetIndex.includes(datasetIdx));
            // then show/hide clicked item datasets and update the other legend items
            let kSafe = 0;
            while(datasetsHidden1.length !== datasetsHidden.length && kSafe++<10){
                datasetsHidden = datasetsHidden1;
                legendIndicesDisabled = _computeLegendIndicesToBeDisabled(datasetsHidden);
                datasetsHidden1 = _computeDatasetsToBeHidden(legendIndicesDisabled);
            }
        }
        else{
            legendIndicesDisabled = _computeLegendIndicesToBeDisabled(datasetsHidden);
        }

        const chart = legend.chart;
        for(let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++){
            chart.setDatasetVisibility(datasetIndex, !datasetsHidden.includes(datasetIndex));
        }
        chart.update();
        legend.legendItems.forEach((legendItem, legendIndex) => {
            legendItem.hidden = legendIndicesDisabled.includes(legendIndex);
        })
    },

    // make closure with plugin, to be able to call other functions of the plugin
    generateLabelsGen(plugin){
        return function(chart){
            const height = chart.legend.height;
            if(!height) return;

            const {stacks, labels, orderedStacks, orderedLabels} = chart._metasets.reduce(
                ({stacks, labels, orderedLabels, orderedStacks}, _meta, i) => {
                    const stack = _meta.stack,
                        label = _meta.label.replace(new RegExp(' \- ' + stack + '$'), '');
                    if(!stacks[stack]){
                        stacks[stack] = [i];
                        orderedStacks.push(stack);
                    }
                    else{
                        stacks[stack].push(i);
                    }
                    if(!labels[label]){
                        labels[label] = [i];
                        orderedLabels.push(label);
                    }
                    else{
                        labels[label].push(i);
                    }
                    return ({stacks, labels, orderedLabels, orderedStacks});
                }, {labels: {}, stacks: {}, orderedLabels: [], orderedStacks: []});
            const colorForMetaset =
                    idx => chart.getDatasetMeta(idx)._dataset.backgroundColor
                        || 'rgba(192, 192, 192, 0.5)',
                colorsForMetasets = a => a.map(colorForMetaset),
                pointStyleWidth = chart.legend.options.labels.pointStyleWidth;

            if(chart.legend.legendItems){
                return orderedLabels.map(
                    label => plugin._generateLegendLabel(label, labels[label],
                        colorsForMetasets(labels[label]),
                        pointStyleWidth || 40, chart.legend.legendItems[0]?.hidden)
                ).concat(orderedStacks.map(
                    stack => plugin._generateLegendLabel(stack, stacks[stack],
                        colorsForMetasets(stacks[stack]),
                        pointStyleWidth || 40, chart.legend.legendItems[0]?.hidden, false)
                ));
            }
        }
    },

    beforeInit(chart){
        const datasets2d = chart.config.data.datasets2d;
        const datasetsNewObj = {}, optionsNewObj = {};
        for(const dataset2d of datasets2d){
            const label = dataset2d.label || '',
                // take options from dataset2d
                optionsTop = this._objMinusKeys(dataset2d, ['label', 'datasets']);
            for(const dataset of dataset2d.datasets){
                const innerLabel = dataset.label || '',
                    newLabel = label + ' - ' + innerLabel;
                optionsNewObj[newLabel] = {
                    ...optionsTop,
                    // combine with options from dataset
                    ...this._objMinusKeys(dataset, ['label', 'data']),
                    stack: innerLabel
                };
                datasetsNewObj[newLabel] = [];
                for(const dataItem of dataset.data){
                    datasetsNewObj[newLabel].push(dataItem);
                }
            }
        }
        const datasetsNew = Object.entries(datasetsNewObj).map(
            ([label, data]) => ({data, label, ...optionsNewObj[label]})
        );
        chart.config.data.datasets = datasetsNew;

        chart.legend.options.labels.usePointStyle = true;
        chart.legend.options.labels.pointStyleWidth = chart.legend.options.labels.boxWidth;

        chart.legend.options.labels.generateLabels = this.generateLabelsGen(this);
        chart.legend.options.onClick = this.onLegendClick;
    }
};

const config = {
    type: 'bar',
    data: data,
    plugins: [
        pluginDatasets2d
    ],
    options: {
        plugins: {
            title: {
                display: true,
                text: 'Chart.js Bar Chart - Stacked'
            },
            legend: {
                //position: "right"
            }
        },
        responsive: true,
        scales: {
            x: {
                stacked: true,
            },
            y: {
                stacked: true
            }
        }
    }
};
new Chart('myChart', config);

个字符
Here's a version与多个人和天;添加或删除一天或一个人就像更改前两个数组的内容一样简单。

相关问题