Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display one label of the sum of stacked bars #16

Closed
LeoDupont opened this issue Nov 30, 2017 · 17 comments
Closed

Display one label of the sum of stacked bars #16

LeoDupont opened this issue Nov 30, 2017 · 17 comments

Comments

@LeoDupont
Copy link

LeoDupont commented Nov 30, 2017

Hi,

I have a horizontal stacked bar chart which looks like this:

chrome_2017-11-30_14-51-19

And I would like to display the sum of each stacked bar, like so:

chrome_2017-11-30_14-50-04

I managed to get this result by returning an empty string for datasets that are not the last one (datasetIndex), and computing the sum for each bar (dataIndex):

plugins: {
	datalabels: {
		formatter: (value, ctx) => {
			let datasets = ctx.chart.data.datasets; // Tried `.filter(ds => !ds._meta.hidden);` without success
			if (ctx.datasetIndex === datasets.length - 1) {
				let sum = 0;
				datasets.map(dataset => {
					sum += dataset.data[ctx.dataIndex];
				});
				return sum.toLocaleString(/* ... */);
			}
			else {
				return '';
			}

		},
		anchor: 'end',
		align: 'end'
	}
}

It works great, but if I toggle off one dataset (via the chart's legend), the result is less great:

  • if I toggle off the first dataset, I still have the sum of the two datasets as a label:

    chrome_2017-11-30_15-57-51

  • if I toggle off the last dataset, I don't have a label anymore:

    chrome_2017-11-30_15-59-59

As I commented in my code snippet, I tried to filter out the datasets with the _meta.hidden metadata, but it seems that the formatter function is not called again when toggleing datasets via the chart's legend.

Is there a better way to use datalabels with stacked bar charts? Or does anyone have an idea to make it work?

@simonbrunel
Copy link
Member

Hi @LeoDupont. Can you build a jsfiddle (or similar) with your current chart config and data? It would help to investigate a solution.

formatter should be called when toggling dataset visiblity but _meta.hidden is private and actually doesn't describe the actual dataset visibility, you should use chart.isDatasetVisible(index) instead.

@LeoDupont
Copy link
Author

Hello @simonbrunel, thank you for your answer.

I can confirm formatter is called when toggling dataset visibility (my bad).

Regarding _meta.hidden, I was also mistaken, the correct syntax being _meta[0].hidden (my bad, too).
I tried to use chart.isDatasetVisible(index) but it turned out ctx.dataset has no datasetIndex property, so I could not filter this way. Unless I missed that property?
So I chose to stick to _meta[0].hidden.

Finally, my condition if (ctx.datasetIndex === datasets.length - 1) could not work correctly because if I hide the first dataset, the last one has the index 1 whereas my datasets array is 1-item long, so its last index is 0...

Here is my final snippet, which works as intended:

plugins: {
	datalabels: {
		formatter: (value, ctx) => {
			
			// Array of visible datasets :
			let datasets = ctx.chart.data.datasets.filter(
				ds => !ds._meta[0].hidden
				// ds => ctx.chart.isDatasetVisible(ds.datasetIndex) // <-- does not work
			);
			
			// If this is the last visible dataset of the bar :
			if (datasets.indexOf(ctx.dataset) === datasets.length - 1) {
				let sum = 0;
				datasets.map(dataset => {
					sum += dataset.data[ctx.dataIndex];
				});
				return sum.toLocaleString(/* ... */);
			}
			else {
				return '';
			}
		},
		anchor: 'end',
		align: 'end'
	}
}

Its JSFiddle : https://jsfiddle.net/78ndvf2n/

I'm concerned about the _meta[0] though. Is _meta likely to have other sub-object than 0?

@simonbrunel
Copy link
Member

You should really not access _meta directly, that's very internal and have a special structure. If you want to access dataset metadata, you can use chart.getDatasetMeta(datasetIndex). I'm quite sure there is ctx.datasetIndex, if not that's a bug.

@simonbrunel
Copy link
Member

simonbrunel commented Nov 30, 2017

I think I would move the summation logic into a separated plugin responsible to compute the total for each "stack" and the utmost dataset index, the one for which you want to display the label. It would happen only one time, before the chart update (better for performances):

const totalizer = {
  id: 'totalizer',

  beforeUpdate: chart => {
    let totals = {}
    let utmost = 0

    chart.data.datasets.forEach((dataset, datasetIndex) => {
      if (chart.isDatasetVisible(datasetIndex)) {
        utmost = datasetIndex
        dataset.data.forEach((value, index) => {
          totals[index] = (totals[index] || 0) + value
        })
      }
    })

    chart.$totalizer = {
      totals: totals,
      utmost: utmost
    }
  }
}

Then you need to register this plugin to the charts you want to display the total:

new Chart('chart', {
  plugins: [totalizer],
  // ...
}

At this point, you can access computed information from chart.$totalizer and thus configure the datalabels plugin easily:

new Chart('chart', {
  plugins: [totalizer],
  options: {
    plugins: {
      datalabels: {
        formatter: (value, ctx) => {
          const total = ctx.chart.$totalizer.totals[ctx.dataIndex]
          return total.toLocaleString('fr-FR', {
            style: 'currency',
            currency: 'EUR'
          })
        },
        display: function(ctx) {
           return ctx.datasetIndex === ctx.chart.$totalizer.utmost
        }
      }
    }
  }
  // ...
}

Note that it's better to hide the other labels using the display option because nothing else will be computed for this label (e.g. the formatter will not be called for hidden labels).

Fiddle: https://jsfiddle.net/simonbrunel/9ezggxx5/

This example doesn't take in account all use cases, such as grouped stacks or negative values.

Edit: I didn't realize that you got a working solution before writing this comment.

@LeoDupont
Copy link
Author

I think your solution is much cleaner, so I opted for it. Thank you!
I won't use negative values nor grouped stacks for this project, but it may be a good plugin idea.

PS: I indeed have ctx.datasetIndex in the formatter, but I could not use it in my previous implementation (with a datasets.filter(/* ... */)).

@simonbrunel
Copy link
Member

Sounds good!

FYI, the array.filter callback takes extra arguments such as the index and the array itself. In your case you would have used index as the dataset index:

let datasets = ctx.chart.data.datasets.filter(
  (ds, datasetIndex) => ctx.chart.isDatasetVisible(datasetIndex)
)

@simonbrunel simonbrunel changed the title One label for one stacked bar Display one label of the sum of stacked bars Dec 1, 2017
@ShafighBahamin
Copy link

Hi. I'm new to using Chart js and I wanted to ask how to put the total count of stacked bar chart at the top of each bar.

@shgithu6
Copy link

Try this for toggle dataset

plugins: {
datalabels: {
formatter: (value, ctx) => {
if (ctx.datasetIndex == 0)
return value + "%";
else
return "";
},
anchor: 'end',
align: 'end'
}
}

@ShafighBahamin
Copy link

I already figured it out but thanks still!

@michvllni
Copy link

I know this is an old topic, but I modified Simon's plugin to be usable for grouped stacks. Hope this helps anyone.

const totalizer = {
            id: 'totalizer',
            beforeUpdate: function (chart) {
                var totals = [];
                var utmost = {};
                chart.data.datasets.forEach(function (dataset, datasetIndex) {
                    if (chart.isDatasetVisible(datasetIndex)) {
                        var stack = dataset.stack;
                        utmost[stack] = datasetIndex;
                        dataset.data.forEach(function (value, index) {
                            if (totals[index] === undefined) {
                                totals[index] = {};
                            }
                            totals[index][stack] = (totals[index][stack] || 0) + value;
                        })
                    }
                });
                chart.$totalizer = {
                    totals: totals,
                    utmost: utmost
                }
            }
        }

The totals are then callable by using chart.$totalizer.totals[dataIndex][stack] and the utmost items are callable by using chart.$totalizer.utmost[stack]

@midhun-qburst
Copy link

Thanks a lot @michvllni , This helped a lot. Don't forget to use dataIndex and stack as ctx.dataIndex and ctx.stack if u end up in errors.

@Synchro
Copy link

Synchro commented Oct 8, 2021

I'm trying to add this, but I get this error:

TypeError: dataset.data.forEach is not a function. (In 'dataset.data.forEach((value, index) => {
                        totals[index] = (totals[index] || 0) + value
                    })', 'dataset.data.forEach' is undefined)

I'm guessing that this is because my dataset.data is an object, not an array, because the index values (object property names) are date strings, so you can't iterate over it using forEach. How would you make this work for any kind of index?

@codeofsumit
Copy link

The provided solutions are for same-length datasets only, right?
Because datasets with data like this: {x: '2022-01-02', y: 123} might not be in the correct order, stacked by the time unit (e.g. in months or years), and will have gaps in data.

I'm trying to make it work for that use case but wasn't successful so far.

@mgug01
Copy link

mgug01 commented Feb 15, 2022

I am having the same issue as Synchro- and am also trying to do this in react-chartjs-2. Are there any work-arounds?

@Voldemorten
Copy link

I am just contributing to this incase any one is working with object structured data ([{x: 'label', y: 10},...]) I solved the problem like this:

const totalizer = {
  id: 'totalizer',
  
  beforeUpdate: chart => {
      let totals = {};
      let utmost = {};
  
      chart.data.datasets.forEach((dataset, datasetIndex) => {
          if (chart.isDatasetVisible(datasetIndex)) {
              dataset.data.forEach((value, index) => {
                  utmost[value.x] = datasetIndex;
                  totals[value.x] = (totals[value.x] || 0) + value.y;
              })
          }
      })
  
      chart.$totalizer = {
          totals: totals,
          utmost: utmost
      }
  }
}

and in the display method:

display: function (ctx) {
    const x = ctx.dataset.data[ctx.dataIndex].x;
    if (ctx.chart.$totalizer.utmost[x] === ctx.datasetIndex) {
        return true;
    };
    return false;
},

Hope this helps someone.

@stef-van-looveren
Copy link

I know this is an old topic, but I modified Simon's plugin to be usable for grouped stacks. Hope this helps anyone.

const totalizer = {
            id: 'totalizer',
            beforeUpdate: function (chart) {
                var totals = [];
                var utmost = {};
                chart.data.datasets.forEach(function (dataset, datasetIndex) {
                    if (chart.isDatasetVisible(datasetIndex)) {
                        var stack = dataset.stack;
                        utmost[stack] = datasetIndex;
                        dataset.data.forEach(function (value, index) {
                            if (totals[index] === undefined) {
                                totals[index] = {};
                            }
                            totals[index][stack] = (totals[index][stack] || 0) + value;
                        })
                    }
                });
                chart.$totalizer = {
                    totals: totals,
                    utmost: utmost
                }
            }
        }

The totals are then callable by using chart.$totalizer.totals[dataIndex][stack] and the utmost items are callable by using chart.$totalizer.utmost[stack]

This is great!! thanks a lot.

@alundstroem
Copy link

Mb a total thread-gravedigger but I've updated this for the version I happened to use, if anybody else is using ChartJS 3.9.x it might help? See https://jsfiddle.net/v3z5umt0/ https://github.com/chartjs/chartjs-plugin-datalabels/issues/91

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests