2

I'd like each stacked bar to have their "total" label position just above the entire bar.

I've managed to get the sum, but I'm having trouble positioning the label at the top.

cocky-mayer-hjiblr

If you notice, the labels are positioned at the bottom:

img1

I've tried playing around with the anchor and offset properties, but I was unable to achieve the desired result.

Code:

import * as React from "react";
import { Bar as RCBar } from "react-chartjs-2";
import { CategoryScale } from "chart.js";
import Chart from "chart.js/auto";
import events from "./events";
import chartOptions from "./chartOptions";
import ChartDataLabels from "chartjs-plugin-datalabels";

Chart.register(CategoryScale, ChartDataLabels);

const Graph = React.memo(() => {
  const canvasRef = React.useRef(null);
  const chartContainerRef = React.useRef(null);

  const formatter = (value, ctx) => {
    const datasets = ctx.chart.data.datasets.filter(
      (ds) => !ds._meta?.[0].hidden
    );

    const foundDatasetIndex = datasets.indexOf(ctx.dataset);

    if (foundDatasetIndex === datasets.length - 1) {
      let sum = 0;
      datasets.forEach((dataset) => {
        sum += dataset.data[ctx.dataIndex];
      });

      return sum;
    } else {
      return "";
    }
  };

  const generateChartData = React.useCallback(() => {
    const labels = [];
    const dataNew = [];
    const dataSuccess = [];
    const dataOnHold = [];
    const dataCanceled = [];

    events.forEach((event) => {
      labels.push(event.trigger);
      event.detailStatus.forEach((detailStatus) => {
        if (detailStatus.code === 0) {
          dataNew.push({
            label: event.trigger,
            sum: detailStatus.sum
          });
        }
        if (detailStatus.code === 1) {
          dataSuccess.push({
            label: event.trigger,
            sum: detailStatus.sum
          });
        }
        if (detailStatus.code === 10) {
          dataOnHold.push({
            label: event.trigger,
            sum: detailStatus.sum
          });
        }
        if (detailStatus.code === 11) {
          dataCanceled.push({
            label: event.trigger,
            sum: detailStatus.sum
          });
        }
      });
    });

    return {
      labels,
      datasets: [
        {
          label: "New",
          data: labels.map(
            (label) =>
              dataNew.find(({ label: labelInData }) => labelInData === label)
                ?.sum ?? 0
          ),
          backgroundColor: "rgba(0, 120, 153, 1)",
          borderColor: "rgba(255, 255, 255, 1)",
          borderWidth: 2,
          borderRadius: 5,
          borderSkipped: "bottom",
          barThickness: 15
        },
        {
          label: "Finished",
          data: labels.map(
            (label) =>
              dataSuccess.find(
                ({ label: labelInData }) => labelInData === label
              )?.sum ?? 0
          ),
          backgroundColor: "rgba(4, 198, 201, 1)",
          borderColor: "rgba(255, 255, 255, 1)",
          borderWidth: 2,
          borderRadius: 5,
          borderSkipped: "bottom",
          barThickness: 15
        },
        {
          label: "On hold",
          data: labels.map(
            (label) =>
              dataOnHold.find(({ label: labelInData }) => labelInData === label)
                ?.sum ?? 0
          ),
          backgroundColor: "rgba(211, 156, 247, 1)",
          borderColor: "rgba(255, 255, 255, 1)",
          borderWidth: 2,
          borderRadius: 5,
          borderSkipped: "bottom",
          barThickness: 15
        },
        {
          label: "Cancelled",
          data: labels.map(
            (label) =>
              dataCanceled.find(
                ({ label: labelInData }) => labelInData === label
              )?.sum ?? 0
          ),
          backgroundColor: "rgba(168, 180, 189, 1)",
          borderColor: "rgba(255, 255, 255, 1)",
          borderWidth: 2,
          borderRadius: 5,
          borderSkipped: "bottom",
          barThickness: 15
        }
      ]
    };
  }, []);

  return (
    <div ref={chartContainerRef}>
      <RCBar
        options={{
          ...chartOptions,
          plugins: {
            ...chartOptions.plugins,
            datalabels: {
              ...chartOptions.plugins.datalabels,
              formatter
            }
          }
        }}
        data={generateChartData()}
        ref={canvasRef}
      />
    </div>
  );
});

export default Graph;
Mike K
  • 7,621
  • 14
  • 60
  • 120

1 Answers1

2

You need to change the formatter function to make it work.

The total value shall appear on the top most value of the stacked bars only. The stacked bars however don't contain zero values nor values from hidden datasets. I ended up with the following solution that works but can probably be improved/simplified.

const formatter = (value, ctx) => {
  const stackedValues = ctx.chart.data.datasets
    .map((ds) => ds.data[ctx.dataIndex]);
  const dsIdxLastVisibleNonZeroValue = stackedValues
    .reduce((prev, curr, i) => !!curr && !ctx.chart.getDatasetMeta(i).hidden ? Math.max(prev, i) : prev, 0);
  if (!!value && ctx.datasetIndex === dsIdxLastVisibleNonZeroValue) {
    return stackedValues
      .filter((ds, i) => !ctx.chart.getDatasetMeta(i).hidden)
      .reduce((sum, v) => sum + v, 0);
  } else {
    return "";
  }
};

Please take a look at your amended Code Sandbox and see how it works.

uminder
  • 23,831
  • 5
  • 37
  • 72