diff --git a/packages/charts/src/components/BarChart/BarChart.mdx b/packages/charts/src/components/BarChart/BarChart.mdx
index 85333181ac9..5c60a1e252a 100644
--- a/packages/charts/src/components/BarChart/BarChart.mdx
+++ b/packages/charts/src/components/BarChart/BarChart.mdx
@@ -67,6 +67,53 @@ You can set a reference line to any value by using the `referenceLine` `chartCon
+### With Stack Aggregate Totals
+
+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.
+
+
+
+### With Custom Tooltip Total
+
+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.
+
+```jsx
+import { ThemingParameters } from '@ui5/webcomponents-react-base';
+import { DefaultTooltipContent } from 'recharts';
+
+const stackedAccessors = new Set(['users', 'sessions']);
+
+const CustomTooltipContent = (props) => {
+ const { payload, ...rest } = props;
+ if (!payload?.length) {
+ return ;
+ }
+ const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
+ if (!stackedEntries.length) {
+ return ;
+ }
+ const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
+ const augmentedPayload = [
+ ...payload,
+ {
+ name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
+ value: total,
+ color: ThemingParameters.sapTextColor,
+ },
+ ];
+ return ;
+};
+
+
+ }}
+/>
+```
+
+
+
diff --git a/packages/charts/src/components/BarChart/BarChart.stories.tsx b/packages/charts/src/components/BarChart/BarChart.stories.tsx
index ea200bef050..dfe6d34c038 100644
--- a/packages/charts/src/components/BarChart/BarChart.stories.tsx
+++ b/packages/charts/src/components/BarChart/BarChart.stories.tsx
@@ -1,4 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
+import { ThemingParameters } from '@ui5/webcomponents-react-base';
+import { DefaultTooltipContent } from 'recharts';
import {
complexDataSet,
legendConfig,
@@ -165,6 +167,78 @@ export const WithNormalizedStacks: Story = {
args: stackedNormalizedConfig,
};
+export const WithStackAggregateTotalsAndTooltip: Story = {
+ args: {
+ dataset: complexDataSet.slice(0, 3),
+ measures: [
+ {
+ accessor: 'users',
+ stackId: 'A',
+ label: 'Users',
+ },
+ {
+ accessor: 'sessions',
+ stackId: 'A',
+ label: 'Active Sessions',
+ },
+ ],
+ chartConfig: {
+ showStackAggregateTotals: true,
+ },
+ },
+};
+
+const stackedAccessors = new Set(['users', 'sessions']);
+
+const CustomTooltipContent = (props) => {
+ const { payload, ...rest } = props;
+ if (!payload?.length) {
+ return ;
+ }
+ const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
+ if (!stackedEntries.length) {
+ return ;
+ }
+ const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
+ const augmentedPayload = [
+ ...payload,
+ {
+ name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
+ value: total,
+ color: ThemingParameters.sapTextColor,
+ },
+ ];
+ return ;
+};
+
+export const WithCustomTooltipTotal: Story = {
+ args: {
+ dataset: complexDataSet.slice(0, 5),
+ measures: [
+ {
+ accessor: 'users',
+ stackId: 'A',
+ label: 'Users',
+ },
+ {
+ accessor: 'sessions',
+ stackId: 'A',
+ label: 'Active Sessions',
+ },
+ {
+ accessor: 'volume',
+ label: 'Vol.',
+ },
+ ],
+ chartConfig: {
+ showStackAggregateTotals: true,
+ },
+ tooltipConfig: {
+ content: ,
+ },
+ },
+};
+
export const WithCustomTooltipConfig: Story = {
args: tooltipConfig,
};
diff --git a/packages/charts/src/components/BarChart/index.tsx b/packages/charts/src/components/BarChart/index.tsx
index 4bcc5daf426..903ec73d4d8 100644
--- a/packages/charts/src/components/BarChart/index.tsx
+++ b/packages/charts/src/components/BarChart/index.tsx
@@ -33,6 +33,8 @@ import type { IChartMeasure } from '../../interfaces/IChartMeasure.js';
import { ChartContainer } from '../../internal/ChartContainer.js';
import { ChartDataLabel } from '../../internal/ChartDataLabel.js';
import { defaultFormatter } from '../../internal/defaults.js';
+import { StackAggregateLabel } from '../../internal/StackAggregateLabel.js';
+import { StackedTooltipContent } from '../../internal/StackedTooltipContent.js';
import { brushProps, tickLineConfig, tooltipContentStyle, tooltipFillOpacity } from '../../internal/staticProps.js';
import { getCellColors, resolvePrimaryAndSecondaryMeasures } from '../../internal/Utils.js';
import { XAxisTicks } from '../../internal/XAxisTicks.js';
@@ -168,11 +170,12 @@ const BarChart = forwardRef((props, ref) => {
};
const referenceLine = chartConfig.referenceLine;
- const { dimensions, measures } = usePrepareDimensionsAndMeasures(
+ const { dimensions, measures, stackGroups, lastInStack } = usePrepareDimensionsAndMeasures(
props.dimensions,
props.measures,
dimensionDefaults,
measureDefaults,
+ chartConfig.showStackAggregateTotals,
);
const tooltipValueFormatter = useTooltipFormatter(measures);
@@ -224,6 +227,10 @@ const BarChart = forwardRef((props, ref) => {
const { isMounted, handleBarAnimationStart, handleBarAnimationEnd } = useCancelAnimationFallback(noAnimation);
+ const stackGroupKeys = Object.keys(stackGroups);
+ const showStackTotalInTooltip =
+ chartConfig.showStackAggregateTotals && stackGroupKeys.length === 1 && measures.every((m) => m.stackId != null);
+
const { chartConfig: _0, dimensions: _1, measures: _2, ...propsWithoutOmitted } = rest;
return (
((props, ref) => {
valueAccessor={valueAccessor(element.accessor)}
content={}
/>
+ {chartConfig.showStackAggregateTotals &&
+ element.stackId &&
+ typeof element.accessor === 'string' &&
+ lastInStack.has(element.accessor) && (
+ }
+ />
+ )}
{dataset.map((data, i) => {
return (
((props, ref) => {
contentStyle={tooltipContentStyle}
labelFormatter={tooltipLabelFormatter}
{...tooltipConfig}
+ {...(showStackTotalInTooltip && {
+ content: (
+
+ ),
+ })}
/>
)}
{!!chartConfig.zoomingTool && (
diff --git a/packages/charts/src/components/ColumnChart/ColumnChart.mdx b/packages/charts/src/components/ColumnChart/ColumnChart.mdx
index f526044a839..782565958af 100644
--- a/packages/charts/src/components/ColumnChart/ColumnChart.mdx
+++ b/packages/charts/src/components/ColumnChart/ColumnChart.mdx
@@ -66,6 +66,53 @@ You can set a reference line to any value by using the `referenceLine` `chartCon
+### With Stack Aggregate Totals
+
+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.
+
+
+
+### With Custom Tooltip Total
+
+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.
+
+```jsx
+import { ThemingParameters } from '@ui5/webcomponents-react-base';
+import { DefaultTooltipContent } from 'recharts';
+
+const stackedAccessors = new Set(['users', 'sessions']);
+
+const CustomTooltipContent = (props) => {
+ const { payload, ...rest } = props;
+ if (!payload?.length) {
+ return ;
+ }
+ const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
+ if (!stackedEntries.length) {
+ return ;
+ }
+ const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
+ const augmentedPayload = [
+ ...payload,
+ {
+ name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
+ value: total,
+ color: ThemingParameters.sapTextColor,
+ },
+ ];
+ return ;
+};
+
+
+ }}
+/>
+```
+
+
+
;
diff --git a/packages/charts/src/components/ColumnChart/ColumnChart.stories.tsx b/packages/charts/src/components/ColumnChart/ColumnChart.stories.tsx
index ec42f12829e..0959ec783ba 100644
--- a/packages/charts/src/components/ColumnChart/ColumnChart.stories.tsx
+++ b/packages/charts/src/components/ColumnChart/ColumnChart.stories.tsx
@@ -1,4 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
+import { ThemingParameters } from '@ui5/webcomponents-react-base';
+import { DefaultTooltipContent } from 'recharts';
import {
complexDataSet,
legendConfig,
@@ -156,6 +158,80 @@ export const WithHighlightedMeasure: Story = {
},
};
+export const WithStackAggregateTotals: Story = {
+ args: {
+ dataset: complexDataSet.slice(0, 3),
+ dimensions: [{ accessor: 'name' }],
+ measures: [
+ {
+ accessor: 'users',
+ stackId: 'A',
+ label: 'Users',
+ },
+ {
+ accessor: 'sessions',
+ stackId: 'A',
+ label: 'Active Sessions',
+ },
+ ],
+ chartConfig: {
+ showStackAggregateTotals: true,
+ },
+ },
+};
+
+const stackedAccessors = new Set(['users', 'sessions']);
+
+const CustomTooltipContent = (props) => {
+ const { payload, ...rest } = props;
+ if (!payload?.length) {
+ return ;
+ }
+ const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
+ if (!stackedEntries.length) {
+ return ;
+ }
+ const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
+ const augmentedPayload = [
+ ...payload,
+ {
+ name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
+ value: total,
+ color: ThemingParameters.sapTextColor,
+ },
+ ];
+ return ;
+};
+
+export const WithCustomTooltipTotal: Story = {
+ args: {
+ dataset: complexDataSet.slice(0, 5),
+ dimensions: [{ accessor: 'name' }],
+ measures: [
+ {
+ accessor: 'users',
+ stackId: 'A',
+ label: 'Users',
+ },
+ {
+ accessor: 'sessions',
+ stackId: 'A',
+ label: 'Active Sessions',
+ },
+ {
+ accessor: 'volume',
+ label: 'Vol.',
+ },
+ ],
+ chartConfig: {
+ showStackAggregateTotals: true,
+ },
+ tooltipConfig: {
+ content: ,
+ },
+ },
+};
+
export const WithCustomTooltipConfig: Story = {
args: tooltipConfig,
};
diff --git a/packages/charts/src/components/ColumnChart/index.tsx b/packages/charts/src/components/ColumnChart/index.tsx
index 578d128d5a7..4317379f0be 100644
--- a/packages/charts/src/components/ColumnChart/index.tsx
+++ b/packages/charts/src/components/ColumnChart/index.tsx
@@ -33,6 +33,8 @@ import type { IChartMeasure } from '../../interfaces/IChartMeasure.js';
import { ChartContainer } from '../../internal/ChartContainer.js';
import { ChartDataLabel } from '../../internal/ChartDataLabel.js';
import { defaultFormatter } from '../../internal/defaults.js';
+import { StackAggregateLabel } from '../../internal/StackAggregateLabel.js';
+import { StackedTooltipContent } from '../../internal/StackedTooltipContent.js';
import { brushProps, tickLineConfig, tooltipContentStyle, tooltipFillOpacity } from '../../internal/staticProps.js';
import { getCellColors, resolvePrimaryAndSecondaryMeasures } from '../../internal/Utils.js';
import { XAxisTicks } from '../../internal/XAxisTicks.js';
@@ -165,11 +167,12 @@ const ColumnChart = forwardRef((props, ref) =>
};
const { referenceLine } = chartConfig;
- const { dimensions, measures } = usePrepareDimensionsAndMeasures(
+ const { dimensions, measures, stackGroups, lastInStack } = usePrepareDimensionsAndMeasures(
props.dimensions,
props.measures,
dimensionDefaults,
measureDefaults,
+ chartConfig.showStackAggregateTotals,
);
const tooltipValueFormatter = useTooltipFormatter(measures);
@@ -225,6 +228,10 @@ const ColumnChart = forwardRef((props, ref) =>
const { isMounted, handleBarAnimationStart, handleBarAnimationEnd } = useCancelAnimationFallback(noAnimation);
+ const stackGroupKeys = Object.keys(stackGroups);
+ const showStackTotalInTooltip =
+ chartConfig.showStackAggregateTotals && stackGroupKeys.length === 1 && measures.every((m) => m.stackId != null);
+
return (
((props, ref) =>
valueAccessor={valueAccessor(element.accessor)}
content={}
/>
+ {chartConfig.showStackAggregateTotals &&
+ element.stackId &&
+ typeof element.accessor === 'string' &&
+ lastInStack.has(element.accessor) && (
+ }
+ />
+ )}
{dataset.map((data, i) => {
return (
((props, ref) =>
contentStyle={tooltipContentStyle}
labelFormatter={tooltipLabelFormatter}
{...tooltipConfig}
+ {...(showStackTotalInTooltip && {
+ content: (
+
+ ),
+ })}
/>
)}
{!!chartConfig.zoomingTool && (
diff --git a/packages/charts/src/components/ComposedChart/ComposedChart.mdx b/packages/charts/src/components/ComposedChart/ComposedChart.mdx
index bfc2f5e8c36..a1ecfceaf63 100644
--- a/packages/charts/src/components/ComposedChart/ComposedChart.mdx
+++ b/packages/charts/src/components/ComposedChart/ComposedChart.mdx
@@ -64,6 +64,53 @@ You can set a reference line to any value by using the `referenceLine` `chartCon
+### With Stack Aggregate Totals
+
+You can display a total label at the top of each stacked bar group by setting `chartConfig.showStackAggregateTotals` to `true`. The tooltip includes the total automatically when all measures are stacked bars within a single stack group. If non-bar measures (e.g. lines or areas) are present, only the bar labels are shown.
+
+
+
+### With Custom Tooltip Total
+
+When non-bar measures (e.g. lines or areas) are present alongside stacked bars, the built-in tooltip total is not available. You can provide a custom tooltip via the `tooltipConfig.content` prop to display a total for the stacked bar measures.
+
+```jsx
+import { ThemingParameters } from '@ui5/webcomponents-react-base';
+import { DefaultTooltipContent } from 'recharts';
+
+const stackedAccessors = new Set(['users', 'sessions']);
+
+const CustomTooltipContent = (props) => {
+ const { payload, ...rest } = props;
+ if (!payload?.length) {
+ return ;
+ }
+ const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
+ if (!stackedEntries.length) {
+ return ;
+ }
+ const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
+ const augmentedPayload = [
+ ...payload,
+ {
+ name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
+ value: total,
+ color: ThemingParameters.sapTextColor,
+ },
+ ];
+ return ;
+};
+
+
+ }}
+/>
+```
+
+
+
diff --git a/packages/charts/src/components/ComposedChart/ComposedChart.stories.tsx b/packages/charts/src/components/ComposedChart/ComposedChart.stories.tsx
index 5a4050ea1ff..229fe23e92f 100644
--- a/packages/charts/src/components/ComposedChart/ComposedChart.stories.tsx
+++ b/packages/charts/src/components/ComposedChart/ComposedChart.stories.tsx
@@ -1,4 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
+import { ThemingParameters } from '@ui5/webcomponents-react-base';
+import { DefaultTooltipContent } from 'recharts';
import { bigDataSet, complexDataSet, legendConfig, simpleDataSet, tooltipConfig } from '../../resources/DemoProps.js';
import { ComposedChart } from './index.js';
@@ -205,6 +207,90 @@ export const LoadingPlaceholder: Story = {
},
};
+export const WithStackAggregateTotals: Story = {
+ args: {
+ dataset: complexDataSet.slice(0, 7),
+ dimensions: [{ accessor: 'name' }],
+ measures: [
+ {
+ accessor: 'users',
+ stackId: 'A',
+ label: 'Users',
+ type: 'bar',
+ },
+ {
+ accessor: 'sessions',
+ stackId: 'A',
+ label: 'Active Sessions',
+ type: 'bar',
+ },
+ {
+ accessor: 'volume',
+ label: 'Vol.',
+ type: 'line',
+ },
+ ],
+ chartConfig: {
+ showStackAggregateTotals: true,
+ },
+ },
+};
+
+const stackedAccessors = new Set(['users', 'sessions']);
+
+const CustomTooltipContent = (props) => {
+ const { payload, ...rest } = props;
+ if (!payload?.length) {
+ return ;
+ }
+ const stackedEntries = payload.filter((entry) => stackedAccessors.has(entry.dataKey));
+ if (!stackedEntries.length) {
+ return ;
+ }
+ const total = stackedEntries.reduce((sum, entry) => sum + (Number(entry.value) || 0), 0);
+ const augmentedPayload = [
+ ...payload,
+ {
+ name: `Total (${stackedEntries.map((entry) => entry.name).join(' + ')})`,
+ value: total,
+ color: ThemingParameters.sapTextColor,
+ },
+ ];
+ return ;
+};
+
+export const WithCustomTooltipTotal: Story = {
+ args: {
+ dataset: complexDataSet.slice(0, 7),
+ dimensions: [{ accessor: 'name' }],
+ measures: [
+ {
+ accessor: 'users',
+ stackId: 'A',
+ label: 'Users',
+ type: 'bar',
+ },
+ {
+ accessor: 'sessions',
+ stackId: 'A',
+ label: 'Active Sessions',
+ type: 'bar',
+ },
+ {
+ accessor: 'volume',
+ label: 'Vol.',
+ type: 'line',
+ },
+ ],
+ chartConfig: {
+ showStackAggregateTotals: true,
+ },
+ tooltipConfig: {
+ content: ,
+ },
+ },
+};
+
export const WithCustomTooltipConfig: Story = {
args: tooltipConfig,
};
diff --git a/packages/charts/src/components/ComposedChart/index.tsx b/packages/charts/src/components/ComposedChart/index.tsx
index 34c4576cd72..9b771bffce2 100644
--- a/packages/charts/src/components/ComposedChart/index.tsx
+++ b/packages/charts/src/components/ComposedChart/index.tsx
@@ -34,6 +34,8 @@ import type { IChartMeasure } from '../../interfaces/IChartMeasure.js';
import { ChartContainer } from '../../internal/ChartContainer.js';
import { ChartDataLabel } from '../../internal/ChartDataLabel.js';
import { defaultFormatter } from '../../internal/defaults.js';
+import { StackAggregateLabel } from '../../internal/StackAggregateLabel.js';
+import { StackedTooltipContent } from '../../internal/StackedTooltipContent.js';
import { brushProps, tickLineConfig, tooltipContentStyle, tooltipFillOpacity } from '../../internal/staticProps.js';
import { getCellColors, resolvePrimaryAndSecondaryMeasures } from '../../internal/Utils.js';
import { XAxisTicks } from '../../internal/XAxisTicks.js';
@@ -184,11 +186,12 @@ const ComposedChart = forwardRef((props, ref
};
const { referenceLine } = chartConfig;
- const { dimensions, measures } = usePrepareDimensionsAndMeasures(
+ const { dimensions, measures, stackGroups, lastInStack } = usePrepareDimensionsAndMeasures(
props.dimensions,
props.measures,
dimensionDefaults,
measureDefaults,
+ chartConfig.showStackAggregateTotals,
);
const tooltipValueFormatter = useTooltipFormatter(measures);
@@ -274,6 +277,10 @@ const ComposedChart = forwardRef((props, ref
const { chartConfig: _0, dimensions: _1, measures: _2, ...propsWithoutOmitted } = rest;
const isRTL = useIsRTL(chartRef);
+ const stackGroupKeys = Object.keys(stackGroups);
+ const showStackTotalInTooltip =
+ chartConfig.showStackAggregateTotals && stackGroupKeys.length === 1 && measures.every((m) => m.stackId != null);
+
return (
((props, ref
contentStyle={tooltipContentStyle}
labelFormatter={tooltipLabelFormatter}
{...tooltipConfig}
+ {...(showStackTotalInTooltip && {
+ content: (
+
+ ),
+ })}
/>
)}
{!noLegend && (
@@ -514,6 +529,19 @@ const ComposedChart = forwardRef((props, ref
valueAccessor={valueAccessor(element.accessor)}
content={}
/>
+ {chartConfig.showStackAggregateTotals &&
+ element.stackId &&
+ typeof element.accessor === 'string' &&
+ lastInStack.has(element.accessor) && (
+
+ }
+ />
+ )}
{dataset.map((data, i) => {
return (
| {
reactKey: 'b',
},
],
+ stackGroups: {},
+ lastInStack: new Set(),
});
});
@@ -65,6 +67,8 @@ describe('useLabelFormatter', () => {
reactKey: 'b',
},
],
+ stackGroups: {},
+ lastInStack: new Set(),
});
});
@@ -96,6 +100,8 @@ describe('useLabelFormatter', () => {
reactKey: 'b',
},
],
+ stackGroups: {},
+ lastInStack: new Set(),
});
});
});
diff --git a/packages/charts/src/hooks/usePrepareDimensionsAndMeasures.ts b/packages/charts/src/hooks/usePrepareDimensionsAndMeasures.ts
index 32127f53b95..06d5828ff1c 100644
--- a/packages/charts/src/hooks/usePrepareDimensionsAndMeasures.ts
+++ b/packages/charts/src/hooks/usePrepareDimensionsAndMeasures.ts
@@ -8,11 +8,15 @@ function getAccessorReactKey(accessorObj: Record) {
return reactKey;
}
+const emptyStackGroups: Record = {};
+const emptyLastInStack = new Set();
+
export const usePrepareDimensionsAndMeasures = (
rawDimensions,
rawMeasures,
dimensionDefaults = {},
measureDefaults = {},
+ showStackAggregateTotals = false,
) => {
const dimensions: DimensionConfig = useMemo(
() =>
@@ -26,17 +30,38 @@ export const usePrepareDimensionsAndMeasures =
- rawMeasures.map((measure) => {
- return {
- ...measureDefaults,
- ...measure,
- reactKey: getAccessorReactKey(measure),
- };
- }),
- [rawMeasures, measureDefaults],
- );
+ const { measures, stackGroups, lastInStack } = useMemo(() => {
+ const groups: Record = {};
+ const preparedMeasures = rawMeasures.map((measure) => {
+ const prepared = {
+ ...measureDefaults,
+ ...measure,
+ reactKey: getAccessorReactKey(measure),
+ };
+ if (showStackAggregateTotals && prepared.stackId && typeof prepared.accessor === 'string') {
+ if (!groups[prepared.stackId]) {
+ groups[prepared.stackId] = [];
+ }
+ groups[prepared.stackId].push(prepared.accessor);
+ }
+ return prepared;
+ });
+
+ if (!showStackAggregateTotals) {
+ return {
+ measures: preparedMeasures as MeasureConfig,
+ stackGroups: emptyStackGroups,
+ lastInStack: emptyLastInStack,
+ };
+ }
+
+ const last = new Set();
+ Object.values(groups).forEach((accessors) => {
+ last.add(accessors[accessors.length - 1]);
+ });
+
+ return { measures: preparedMeasures as MeasureConfig, stackGroups: groups, lastInStack: last };
+ }, [rawMeasures, measureDefaults, showStackAggregateTotals]);
- return { dimensions, measures };
+ return { dimensions, measures, stackGroups, lastInStack };
};
diff --git a/packages/charts/src/interfaces/ICartesianChartConfig.ts b/packages/charts/src/interfaces/ICartesianChartConfig.ts
index c9633cefe69..279f297841f 100644
--- a/packages/charts/src/interfaces/ICartesianChartConfig.ts
+++ b/packages/charts/src/interfaces/ICartesianChartConfig.ts
@@ -80,4 +80,15 @@ export interface ICartesianChartConfig {
* __Note:__ It is possible to overwrite internally used props. Please use with caution!
*/
secondXAxisConfig?: Omit;
+ /**
+ * Defines whether an aggregate total label should be displayed at the end of each stacked bar/column group.
+ *
+ * Only applies when measures use `stackId`. Non-stacked measures are not affected.
+ */
+ showStackAggregateTotals?: boolean;
+ /**
+ * Defines a custom formatter for the stack aggregate total label.
+ * If not set, the raw numeric total is displayed.
+ */
+ stackAggregateTotalFormatter?: (value: any) => string | number;
}
diff --git a/packages/charts/src/interfaces/IChartDimension.ts b/packages/charts/src/interfaces/IChartDimension.ts
index de23a60f551..de800be3b62 100644
--- a/packages/charts/src/interfaces/IChartDimension.ts
+++ b/packages/charts/src/interfaces/IChartDimension.ts
@@ -2,6 +2,8 @@ export interface IChartDimension {
/**
* A string containing the path to the dataset key this line should display.
* Supports object structures by using `'parent.child'`. Can also be a getter.
+ *
+ * __Note:__ Function accessors are not fully supported by all chart features (e.g. tooltips, event handlers, secondary axis, stack aggregate totals). For full feature support, use string accessors.
*/
accessor: string | ((dataset: Record) => string | number);
/**
diff --git a/packages/charts/src/interfaces/IChartMeasure.ts b/packages/charts/src/interfaces/IChartMeasure.ts
index 8153f8ee07a..81ffd501c09 100644
--- a/packages/charts/src/interfaces/IChartMeasure.ts
+++ b/packages/charts/src/interfaces/IChartMeasure.ts
@@ -4,6 +4,8 @@ export interface IChartMeasure {
/**
* A string containing the path to the dataset key this line should display.
* Supports object structures by using '`parent.child'`. Can also be a getter.
+ *
+ * __Note:__ Function accessors are not fully supported by all chart features (e.g. tooltips, event handlers, secondary axis, stack aggregate totals). For full feature support, use string accessors.
*/
accessor: string | ((data: any) => any);
/**
diff --git a/packages/charts/src/internal/StackAggregateLabel.tsx b/packages/charts/src/internal/StackAggregateLabel.tsx
new file mode 100644
index 00000000000..06c920231ca
--- /dev/null
+++ b/packages/charts/src/internal/StackAggregateLabel.tsx
@@ -0,0 +1,38 @@
+import { ThemingParameters } from '@ui5/webcomponents-react-base';
+import type { ReactElement } from 'react';
+import { Label } from 'recharts';
+import type { LabelProps } from 'recharts';
+
+interface StackAggregateLabelProps {
+ stackAccessors: string[];
+ dataset: Record[];
+ // recharts LabelList props
+ viewBox?: { x: number; y: number; width: number; height: number };
+ index?: number;
+ offset?: number;
+ position?: LabelProps['position'];
+}
+
+export const StackAggregateLabel = (props: StackAggregateLabelProps): ReactElement | null => {
+ const { stackAccessors, dataset, viewBox, index, offset, position } = props;
+
+ if (index == null || !viewBox || !dataset?.[index]) {
+ return null;
+ }
+
+ const dataEntry = dataset[index];
+ const total = stackAccessors.reduce((sum, accessor) => {
+ return sum + (Number(dataEntry[accessor]) || 0);
+ }, 0);
+
+ return (
+
+ );
+};
diff --git a/packages/charts/src/internal/StackedTooltipContent.tsx b/packages/charts/src/internal/StackedTooltipContent.tsx
new file mode 100644
index 00000000000..a219900e2d2
--- /dev/null
+++ b/packages/charts/src/internal/StackedTooltipContent.tsx
@@ -0,0 +1,48 @@
+import { ThemingParameters } from '@ui5/webcomponents-react-base';
+import type { ReactElement } from 'react';
+import { DefaultTooltipContent } from 'recharts';
+
+interface StackedTooltipContentProps {
+ stackAccessors: string[];
+ totalFormatter?: (value: number) => string | number;
+ // recharts tooltip
+ payload?: Array<{
+ dataKey?: string;
+ value?: number;
+ name?: string;
+ color?: string;
+ payload?: Record;
+ }>;
+ [key: string]: unknown;
+}
+
+export const StackedTooltipContent = (props: StackedTooltipContentProps): ReactElement => {
+ const { stackAccessors, totalFormatter, payload, ...tooltipProps } = props;
+
+ if (!payload?.length) {
+ return ;
+ }
+
+ const firstEntry = payload[0]?.payload;
+ if (!firstEntry) {
+ return ;
+ }
+
+ const total = stackAccessors.reduce((sum, accessor) => {
+ return sum + (Number(firstEntry[accessor]) || 0);
+ }, 0);
+
+ const formattedTotal = totalFormatter ? totalFormatter(total) : total;
+
+ const augmentedPayload = [
+ ...payload,
+ {
+ name: 'Total',
+ value: formattedTotal,
+ color: ThemingParameters.sapTextColor,
+ dataKey: '__stackTotal__',
+ },
+ ];
+
+ return ;
+};
| | |