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

问题描述 投票:0回答:1

我正在尝试创建一个堆叠条形图,其中 x 轴上有一个小时段(上午 9 点、上午 10 点、上午 11 点……),y 轴上有计数。我希望按天(周一、周二……)和人(爱丽丝、鲍勃)分组的数据如下所示: 星期一、星期二、鲍勃、爱丽丝应该是可选择的选项(隐藏/显示),但条形上没有标签 - (这只是为了澄清数据)。

理想情况下,我想指定我的数据,例如:

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>

chart.js
1个回答
0
投票

在我看来,您的数据的索引是 两个变量,

person
day
并且因为 你还需要数据预处理,我会 建议相应地构建您的数据 并使用自定义插件 这将对数据进行预处理 它
beforeInit
处理程序。 (形式上,数据是三维的, 因为还有小时变量,但因为 这是 x 轴上表示的变量, 我们将重点关注两个剩余变量)

问题的主要难点在于 自定义图例项目的要求。幸运的是, Chart.js 在以下方面很灵活 图例标签配置 允许自定义图例或画布 提供为

pointStyle
, 而选择和取消选择的效果 传奇项目可以通过完美控制 可定制的图例点击处理程序

下面代码片段中的

插件

datasets2d
, 首先将二维数据简单映射到 带堆栈的一维标准数据集 对应于内部
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 indices = legendItem.datasetIndex; // multiple indices (array)
      legendItem.hidden = !legendItem.hidden;
      const chart = legend.chart;
      for(const index of indices){
         if(legendItem.hidden){
            chart.setDatasetVisibility(index, false);
         }
         else{
            chart.setDatasetVisibility(index, true);
         }
      }
      chart.update();
      // now update all legend items, since it is possible for a legend item
      // to change hidden status as a result of a click on another item
      for(const legendItem1 of legend.legendItems){
         const indices1 = legendItem1.datasetIndex;
         legendItem1.hidden = !indices1.map(index => chart.isDatasetVisible(index))
            .reduce((s, b) => s || b);
      }
   },

   // 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', 'data']);
         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);
<div style="min-height: 60vh">
   <canvas id="myChart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>

这是一个版本 多人、多日;添加或删除 一天或一个人就像改变一样简单 前两个数组的内容。

© www.soinside.com 2019 - 2024. All rights reserved.