From d243cb3b6529060ae42bbad294cc03f695da5e6a Mon Sep 17 00:00:00 2001 From: Lukas Harbarth Date: Mon, 9 Mar 2026 13:17:46 +0100 Subject: [PATCH 1/2] feat(Bar/Column/ComposedChart): add stack aggregate total labels and tooltip support --- .../src/components/BarChart/BarChart.mdx | 47 ++++++++++ .../components/BarChart/BarChart.stories.tsx | 74 ++++++++++++++++ .../charts/src/components/BarChart/index.tsx | 28 +++++- .../components/ColumnChart/ColumnChart.mdx | 47 ++++++++++ .../ColumnChart/ColumnChart.stories.tsx | 76 ++++++++++++++++ .../src/components/ColumnChart/index.tsx | 28 +++++- .../ComposedChart/ComposedChart.mdx | 47 ++++++++++ .../ComposedChart/ComposedChart.stories.tsx | 86 +++++++++++++++++++ .../src/components/ComposedChart/index.tsx | 30 ++++++- .../hooks/usePrepareDimensionsAndMeasures.ts | 49 ++++++++--- .../src/interfaces/ICartesianChartConfig.ts | 11 +++ .../charts/src/interfaces/IChartDimension.ts | 2 + .../charts/src/interfaces/IChartMeasure.ts | 2 + .../src/internal/StackAggregateLabel.tsx | 38 ++++++++ .../src/internal/StackedTooltipContent.tsx | 48 +++++++++++ 15 files changed, 598 insertions(+), 15 deletions(-) create mode 100644 packages/charts/src/internal/StackAggregateLabel.tsx create mode 100644 packages/charts/src/internal/StackedTooltipContent.tsx 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 ( ) { 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 ( +