Skip to content

Commit d243cb3

Browse files
committed
feat(Bar/Column/ComposedChart): add stack aggregate total labels and tooltip support
1 parent 9e5fa48 commit d243cb3

15 files changed

Lines changed: 598 additions & 15 deletions

packages/charts/src/components/BarChart/BarChart.mdx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,53 @@ You can set a reference line to any value by using the `referenceLine` `chartCon
6767

6868
<Canvas of={ComponentStories.WithHighlightedMeasure} />
6969

70+
### With Stack Aggregate Totals
71+
72+
You can display a total label at the end of each stacked bar group by setting `chartConfig.showStackAggregateTotals` to `true`. The tooltip includes the total automatically when only a single bar per dimension is present.
73+
74+
<Canvas of={ComponentStories.WithStackAggregateTotalsAndTooltip} />
75+
76+
### With Custom Tooltip Total
77+
78+
When multiple bars per dimension are present (e.g. stacked + standalone), the built-in tooltip total is not available. You can provide a custom tooltip via the `tooltipConfig.content` prop to display a total for specific measures.
79+
80+
```jsx
81+
import { ThemingParameters } from '@ui5/webcomponents-react-base';
82+
import { DefaultTooltipContent } from 'recharts';
83+
84+
const stackedAccessors = new Set(['users', 'sessions']);
85+
86+
const CustomTooltipContent = (props) => {
87+
const { payload, ...rest } = props;
88+
if (!payload?.length) {
89+
return <DefaultTooltipContent {...rest} payload={payload} />;
90+
}
91+
const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
92+
if (!stackedEntries.length) {
93+
return <DefaultTooltipContent {...rest} payload={payload} />;
94+
}
95+
const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
96+
const augmentedPayload = [
97+
...payload,
98+
{
99+
name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
100+
value: total,
101+
color: ThemingParameters.sapTextColor,
102+
},
103+
];
104+
return <DefaultTooltipContent {...rest} payload={augmentedPayload} />;
105+
};
106+
107+
<BarChart
108+
{...props}
109+
tooltipConfig={{
110+
content: <CustomTooltipContent />
111+
}}
112+
/>
113+
```
114+
115+
<Canvas of={ComponentStories.WithCustomTooltipTotal} />
116+
70117
<TooltipStory of={ComponentStories.WithCustomTooltipConfig} />
71118
72119
<LegendStory of={ComponentStories.WithCustomLegendConfig} />

packages/charts/src/components/BarChart/BarChart.stories.tsx

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
import { ThemingParameters } from '@ui5/webcomponents-react-base';
3+
import { DefaultTooltipContent } from 'recharts';
24
import {
35
complexDataSet,
46
legendConfig,
@@ -165,6 +167,78 @@ export const WithNormalizedStacks: Story = {
165167
args: stackedNormalizedConfig,
166168
};
167169

170+
export const WithStackAggregateTotalsAndTooltip: Story = {
171+
args: {
172+
dataset: complexDataSet.slice(0, 3),
173+
measures: [
174+
{
175+
accessor: 'users',
176+
stackId: 'A',
177+
label: 'Users',
178+
},
179+
{
180+
accessor: 'sessions',
181+
stackId: 'A',
182+
label: 'Active Sessions',
183+
},
184+
],
185+
chartConfig: {
186+
showStackAggregateTotals: true,
187+
},
188+
},
189+
};
190+
191+
const stackedAccessors = new Set(['users', 'sessions']);
192+
193+
const CustomTooltipContent = (props) => {
194+
const { payload, ...rest } = props;
195+
if (!payload?.length) {
196+
return <DefaultTooltipContent {...rest} payload={payload} />;
197+
}
198+
const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
199+
if (!stackedEntries.length) {
200+
return <DefaultTooltipContent {...rest} payload={payload} />;
201+
}
202+
const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
203+
const augmentedPayload = [
204+
...payload,
205+
{
206+
name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
207+
value: total,
208+
color: ThemingParameters.sapTextColor,
209+
},
210+
];
211+
return <DefaultTooltipContent {...rest} payload={augmentedPayload} />;
212+
};
213+
214+
export const WithCustomTooltipTotal: Story = {
215+
args: {
216+
dataset: complexDataSet.slice(0, 5),
217+
measures: [
218+
{
219+
accessor: 'users',
220+
stackId: 'A',
221+
label: 'Users',
222+
},
223+
{
224+
accessor: 'sessions',
225+
stackId: 'A',
226+
label: 'Active Sessions',
227+
},
228+
{
229+
accessor: 'volume',
230+
label: 'Vol.',
231+
},
232+
],
233+
chartConfig: {
234+
showStackAggregateTotals: true,
235+
},
236+
tooltipConfig: {
237+
content: <CustomTooltipContent />,
238+
},
239+
},
240+
};
241+
168242
export const WithCustomTooltipConfig: Story = {
169243
args: tooltipConfig,
170244
};

packages/charts/src/components/BarChart/index.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import type { IChartMeasure } from '../../interfaces/IChartMeasure.js';
3333
import { ChartContainer } from '../../internal/ChartContainer.js';
3434
import { ChartDataLabel } from '../../internal/ChartDataLabel.js';
3535
import { defaultFormatter } from '../../internal/defaults.js';
36+
import { StackAggregateLabel } from '../../internal/StackAggregateLabel.js';
37+
import { StackedTooltipContent } from '../../internal/StackedTooltipContent.js';
3638
import { brushProps, tickLineConfig, tooltipContentStyle, tooltipFillOpacity } from '../../internal/staticProps.js';
3739
import { getCellColors, resolvePrimaryAndSecondaryMeasures } from '../../internal/Utils.js';
3840
import { XAxisTicks } from '../../internal/XAxisTicks.js';
@@ -168,11 +170,12 @@ const BarChart = forwardRef<HTMLDivElement, BarChartProps>((props, ref) => {
168170
};
169171
const referenceLine = chartConfig.referenceLine;
170172

171-
const { dimensions, measures } = usePrepareDimensionsAndMeasures(
173+
const { dimensions, measures, stackGroups, lastInStack } = usePrepareDimensionsAndMeasures(
172174
props.dimensions,
173175
props.measures,
174176
dimensionDefaults,
175177
measureDefaults,
178+
chartConfig.showStackAggregateTotals,
176179
);
177180

178181
const tooltipValueFormatter = useTooltipFormatter(measures);
@@ -224,6 +227,10 @@ const BarChart = forwardRef<HTMLDivElement, BarChartProps>((props, ref) => {
224227

225228
const { isMounted, handleBarAnimationStart, handleBarAnimationEnd } = useCancelAnimationFallback(noAnimation);
226229

230+
const stackGroupKeys = Object.keys(stackGroups);
231+
const showStackTotalInTooltip =
232+
chartConfig.showStackAggregateTotals && stackGroupKeys.length === 1 && measures.every((m) => m.stackId != null);
233+
227234
const { chartConfig: _0, dimensions: _1, measures: _2, ...propsWithoutOmitted } = rest;
228235
return (
229236
<ChartContainer
@@ -337,6 +344,17 @@ const BarChart = forwardRef<HTMLDivElement, BarChartProps>((props, ref) => {
337344
valueAccessor={valueAccessor(element.accessor)}
338345
content={<ChartDataLabel config={element} chartType="bar" position={'insideRight'} />}
339346
/>
347+
{chartConfig.showStackAggregateTotals &&
348+
element.stackId &&
349+
typeof element.accessor === 'string' &&
350+
lastInStack.has(element.accessor) && (
351+
<LabelList
352+
data={dataset}
353+
valueAccessor={valueAccessor(element.accessor)}
354+
position="right"
355+
content={<StackAggregateLabel stackAccessors={stackGroups[element.stackId]} dataset={dataset} />}
356+
/>
357+
)}
340358
{dataset.map((data, i) => {
341359
return (
342360
<Cell
@@ -374,6 +392,14 @@ const BarChart = forwardRef<HTMLDivElement, BarChartProps>((props, ref) => {
374392
contentStyle={tooltipContentStyle}
375393
labelFormatter={tooltipLabelFormatter}
376394
{...tooltipConfig}
395+
{...(showStackTotalInTooltip && {
396+
content: (
397+
<StackedTooltipContent
398+
stackAccessors={stackGroups[stackGroupKeys[0]]}
399+
totalFormatter={chartConfig.stackAggregateTotalFormatter}
400+
/>
401+
),
402+
})}
377403
/>
378404
)}
379405
{!!chartConfig.zoomingTool && (

packages/charts/src/components/ColumnChart/ColumnChart.mdx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,53 @@ You can set a reference line to any value by using the `referenceLine` `chartCon
6666

6767
<Canvas of={ComponentStories.WithHighlightedMeasure} />
6868

69+
### With Stack Aggregate Totals
70+
71+
You can display a total label at the top of each stacked column group by setting `chartConfig.showStackAggregateTotals` to `true`. The tooltip includes the total automatically when only a single column per dimension is present.
72+
73+
<Canvas of={ComponentStories.WithStackAggregateTotals} />
74+
75+
### With Custom Tooltip Total
76+
77+
When multiple columns per dimension are present (e.g. stacked + standalone), the built-in tooltip total is not available. You can provide a custom tooltip via the `tooltipConfig.content` prop to display a total for specific measures.
78+
79+
```jsx
80+
import { ThemingParameters } from '@ui5/webcomponents-react-base';
81+
import { DefaultTooltipContent } from 'recharts';
82+
83+
const stackedAccessors = new Set(['users', 'sessions']);
84+
85+
const CustomTooltipContent = (props) => {
86+
const { payload, ...rest } = props;
87+
if (!payload?.length) {
88+
return <DefaultTooltipContent {...rest} payload={payload} />;
89+
}
90+
const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
91+
if (!stackedEntries.length) {
92+
return <DefaultTooltipContent {...rest} payload={payload} />;
93+
}
94+
const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
95+
const augmentedPayload = [
96+
...payload,
97+
{
98+
name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
99+
value: total,
100+
color: ThemingParameters.sapTextColor,
101+
},
102+
];
103+
return <DefaultTooltipContent {...rest} payload={augmentedPayload} />;
104+
};
105+
106+
<ColumnChart
107+
{...props}
108+
tooltipConfig={{
109+
content: <CustomTooltipContent />
110+
}}
111+
/>
112+
```
113+
114+
<Canvas of={ComponentStories.WithCustomTooltipTotal} />
115+
69116
<TooltipStory of={ComponentStories.WithCustomTooltipConfig} />;
70117
71118
<LegendStory of={ComponentStories.WithCustomLegendConfig} />

packages/charts/src/components/ColumnChart/ColumnChart.stories.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
import { ThemingParameters } from '@ui5/webcomponents-react-base';
3+
import { DefaultTooltipContent } from 'recharts';
24
import {
35
complexDataSet,
46
legendConfig,
@@ -156,6 +158,80 @@ export const WithHighlightedMeasure: Story = {
156158
},
157159
};
158160

161+
export const WithStackAggregateTotals: Story = {
162+
args: {
163+
dataset: complexDataSet.slice(0, 3),
164+
dimensions: [{ accessor: 'name' }],
165+
measures: [
166+
{
167+
accessor: 'users',
168+
stackId: 'A',
169+
label: 'Users',
170+
},
171+
{
172+
accessor: 'sessions',
173+
stackId: 'A',
174+
label: 'Active Sessions',
175+
},
176+
],
177+
chartConfig: {
178+
showStackAggregateTotals: true,
179+
},
180+
},
181+
};
182+
183+
const stackedAccessors = new Set(['users', 'sessions']);
184+
185+
const CustomTooltipContent = (props) => {
186+
const { payload, ...rest } = props;
187+
if (!payload?.length) {
188+
return <DefaultTooltipContent {...rest} payload={payload} />;
189+
}
190+
const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
191+
if (!stackedEntries.length) {
192+
return <DefaultTooltipContent {...rest} payload={payload} />;
193+
}
194+
const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
195+
const augmentedPayload = [
196+
...payload,
197+
{
198+
name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
199+
value: total,
200+
color: ThemingParameters.sapTextColor,
201+
},
202+
];
203+
return <DefaultTooltipContent {...rest} payload={augmentedPayload} />;
204+
};
205+
206+
export const WithCustomTooltipTotal: Story = {
207+
args: {
208+
dataset: complexDataSet.slice(0, 5),
209+
dimensions: [{ accessor: 'name' }],
210+
measures: [
211+
{
212+
accessor: 'users',
213+
stackId: 'A',
214+
label: 'Users',
215+
},
216+
{
217+
accessor: 'sessions',
218+
stackId: 'A',
219+
label: 'Active Sessions',
220+
},
221+
{
222+
accessor: 'volume',
223+
label: 'Vol.',
224+
},
225+
],
226+
chartConfig: {
227+
showStackAggregateTotals: true,
228+
},
229+
tooltipConfig: {
230+
content: <CustomTooltipContent />,
231+
},
232+
},
233+
};
234+
159235
export const WithCustomTooltipConfig: Story = {
160236
args: tooltipConfig,
161237
};

0 commit comments

Comments
 (0)