javascriptreactjschart.js

Chart.js: Stacked Bar Chart Layout Issue with Additional Single Bar


I'm facing an issue with the layout of my Chart.js stacked bar chart. I'm trying to display a single bar for "前年電力量" (Previous Year's Power Consumption) alongside a stacked bar for "予想電力量" (Current Year's Power Consumption).

The issue is with the ordering and stack. if I comment the dataset "Previous year's total power consumption" in the testdata and the order and stack, the layout works fine, there is an example of data with the correct layout but only for one month, trying to achieve for all 12.

here is the code example in the codesandbox: https://codesandbox.io/p/sandbox/dv2wl3

Trying to achieve the same is if you use data instead of testdata in .

const testdata = {
    labels: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"],
    datasets: [
      // Unique bar for 前年電力量 (Previous Year's Power Consumption)
      {
        label: "前年電力量", // Previous year's total power consumption
        data: [100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100],
        backgroundColor: "rgb(114, 225, 164)", // Green color for previous year
        // stack: "previousYear", // Separate stack for previous year
        barPercentage: 0.5,
        // grouped: false,
        // order: 3,
      },
  
      // Baseline bar (container) for 今年の電力消費量 (Current Year Estimate)
      {
        label: "予想電力量", // Estimated power consumption for the current year
        data: [150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150], // Baseline data for each month
        backgroundColor: "rgba(237, 239, 255, 1)", // Light color to act as background container
        borderColor: "rgba(106, 122, 242, 1)",
        borderWidth: 1,
        borderDash: [5, 5],
        stack: "currentYear", // Stack for current year estimate
        grouped: false,
        order: 1, // Render this first to act as a background layer
        barPercentage: 0.5,
      },

      // Stacked categories within 今年の電力消費量 (Current Year Power Consumption)
      {
        label: "空調 (今年)", // Air Conditioning - Current Year
        data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
        backgroundColor: "rgba(88, 181, 247, 1)", // Blue color for air conditioning
        stack: "currentYear", // Stack for current year
        grouped: false,
        order: 1,
        barPercentage: 0.5,
      },
      {
        label: "照明 (今年)", // Illumination - Current Year
        data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
        backgroundColor: "rgb(71, 68, 222)", // Color for illumination
        stack: "currentYear", // Stack for current year
        grouped: false,
        order: 1,
        barPercentage: 0.5,
      },
      {
        label: "冷ケース (今年)", // Cooling Case - Current Year
        data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
        backgroundColor: "rgb(157, 201, 210)", // Color for cooling case
        stack: "currentYear", // Stack for current year
        grouped: false,
        order: 1,
        barPercentage: 0.5,
      },
      {
        label: "その他 (今年)", // Others - Current Year
        data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
        backgroundColor: "rgb(182, 193, 200)", // Color for others
        stack: "currentYear", // Stack for current year
        grouped: true,
        order: 1,
        barPercentage: 0.5,
      },
    ],
  };

Thanks!


Solution

  • After some trial and error, it seems like showing the baseline bar "Estimated power consumption for the current year" as background and working with multiple bars: single bar "前年電力量 (Previous Year's Power Consumption)" and stacked bar "Stacked categories within 今年の電力消費量 (Current Year Power Consumption)" tricky and challenging.

    However, thinking of a trick to combine the baseline bar and stacked bar as the stacked bar. For the value of the baseline bar, it should deduct the values for categories

    150 (Estimated power consumption for the current year) - 40 (Sum of Stacked categories within 今年の電力消費量 (Current Year Power Consumption)) = 110 (Value of baseline chart stacked)

    so that the baseline bar looks like the background bar. This is achieved by manipulating the data, resulting in formattedData.

    When hovering over the baseline bar and the tooltip, the original value should be shown instead of the formatted value. This can be achieved via the callback event in the tooltip settings.

    import _ from "lodash";
    
    const testdata = {
      labels: [
        "1月",
        "2月",
        "3月",
        "4月",
        "5月",
        "6月",
        "7月",
        "8月",
        "9月",
        "10月",
        "11月",
        "12月",
      ],
      datasets: [
        // Unique bar for 前年電力量 (Previous Year's Power Consumption)
        {
          label: "前年電力量", // Previous year's total power consumption
          data: [100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100],
          backgroundColor: "rgb(114, 225, 164)", // Green color for previous year
          stack: "previousYear", // Separate stack for previous year
          barPercentage: 0.5,
          grouped: true,
          // order: 3,
        },
    
        // Baseline bar (container) for 今年の電力消費量 (Current Year Estimate)
        {
          label: "予想電力量", // Estimated power consumption for the current year
          data: [150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150], // Baseline data for each month
          backgroundColor: "rgba(237, 239, 255, 1)", // Light color to act as background container
          borderColor: "rgba(106, 122, 242, 1)",
          borderWidth: 1,
          borderDash: [5, 5],
          stack: "currentYear", // Stack for current year estimate
          grouped: true,
          order: 2, // Render this first to act as a background layer
          barPercentage: 0.5,
        },
    
        // Stacked categories within 今年の電力消費量 (Current Year Power Consumption)
        {
          label: "空調 (今年)", // Air Conditioning - Current Year
          data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
          backgroundColor: "rgba(88, 181, 247, 1)", // Blue color for air conditioning
          stack: "currentYear", // Stack for current year
          grouped: true,
          order: 1,
          barPercentage: 0.5,
        },
        {
          label: "照明 (今年)", // Illumination - Current Year
          data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
          backgroundColor: "rgb(71, 68, 222)", // Color for illumination
          stack: "currentYear", // Stack for current year
          grouped: true,
          order: 1,
          barPercentage: 0.5,
        },
        {
          label: "冷ケース (今年)", // Cooling Case - Current Year
          data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
          backgroundColor: "rgb(157, 201, 210)", // Color for cooling case
          stack: "currentYear", // Stack for current year
          grouped: true,
          order: 1,
          barPercentage: 0.5,
        },
        {
          label: "その他 (今年)", // Others - Current Year
          data: [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10],
          backgroundColor: "rgb(182, 193, 200)", // Color for others
          stack: "currentYear", // Stack for current year
          grouped: true,
          order: 1,
          barPercentage: 0.5,
        },
      ],
    };
    
    let formattedData = _.cloneDeep(testdata);
    let currentYearCategoriesDataset = testdata.datasets
      .filter((x) => x.stack === "currentYear")
      .filter((x, i) => i > 0);
    
    // Display Baseline bar with stacked bar by total minus categories
    formattedData.datasets = [
      ...formattedData.datasets.filter((x) => x.stack !== "currentYear"),
      ...formattedData.datasets
        .filter((x) => x.stack === "currentYear")
        .map((dataset, i) => {
          if (i == 0) {
            dataset.data = dataset.data.map(
              (data, j) =>
                data -
                currentYearCategoriesDataset.reduce(
                  (sum, cur) => sum + cur.data[j],
                  0
                )
            );
          }
    
         return dataset;
        }),
    ];
    
    <GraphContainer>
      <GraphWrapper>
        <Bar options={options} data={formattedData} />
      </GraphWrapper>
    </GraphContainer>
    
      const options = {
        responsive: true,
        maintainAspectRatio: false,
        scales: {
          x: {
            stacked: true,
          },
          y: {
            stacked: true,
            beginAtZero: true,
            max: 250,
            ticks: {
              stepSize: 50,
            },
          },
        },
        plugins: {
          legend: {
            display: false,
          },
          tooltip: {
            callbacks: {
              label: function (tooltipItem) {
                const dataset = tooltipItem.dataset;
                const label = dataset.label;
                const value = tooltipItem.raw;
    
                // Get the original value for 予想電力量
                if (dataset.stack === "currentYear") {
                  // Or
                  //if (label === "予想電力量") {
    
                  let totalValue = testdata.datasets
                    .filter((x) => x.stack === "currentYear")
                    .find((x, i) => i === 0).data[tooltipItem.dataIndex];
    
                  console.log(totalValue);
                  return `${label}: ${totalValue}`;
                }
    
                return `${label}: ${value}`;
              },
            },
          },
          annotation: {
            annotations: {
              predictionLine: {
                type: "line",
                yMin: 180, // prediction value
                yMax: 180,
                borderColor: "rgba(255, 99, 132, 0.8)",
                borderWidth: 2,
                borderDash: [6, 6],
                label: {
                  enabled: true,
                  content: "予想値", // Prediction label
                  position: "end",
                  backgroundColor: "rgba(255, 99, 132, 0.8)",
                  color: "white",
                  padding: 4,
                },
              },
            },
          },
        },
      };
    

    Demo @ Code Sandbox