Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,11 @@
align-items: flex-start;
}

/* Stacked chart tooltip total row */
.stackedTooltipWithTotal .recharts-tooltip-item:last-child {
font-weight: bold;
}

/* TODO remove this workaround as soon as https://github.com/storybookjs/storybook/issues/20497 is fixed */
.docs-story > div > div[scale] {
min-height: 20px;
Expand Down
23 changes: 23 additions & 0 deletions cypress/support/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,26 @@ export function testChartLegendConfig(Component, props) {
cy.findAllByTestId('catval').should('be.visible');
});
}

export function testStackAggregateTotals(Component, props) {
it('showStackAggregateTotals', () => {
const { dataset, measures } = props;
const stackAccessors = measures.filter((measure) => measure.stackId != null).map((measure) => measure.accessor);
const expectedTotals: number[] = dataset.map((entry) =>
stackAccessors.reduce((sum, accessor) => sum + (Number(entry[accessor]) || 0), 0),
);

cy.mount(<Component {...props} noAnimation chartConfig={{ showStackAggregateTotals: true }} />);

expectedTotals.forEach((total) => {
cy.get('.recharts-label').contains(total).closest('text').should('have.attr', 'font-weight', 'bold');
});

// tooltip
cy.get('.recharts-wrapper').trigger('mousemove', 'center', { force: true });
cy.get('.recharts-tooltip-item').last().should('contain.text', 'Total : 560').and('have.css', 'font-weight', '700');

cy.mount(<Component {...props} noAnimation chartConfig={{ showStackAggregateTotals: false }} />);
cy.get('.recharts-label').should('not.exist');
});
}
16 changes: 15 additions & 1 deletion packages/charts/src/components/BarChart/BarChart.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { complexDataSet } from '../../resources/DemoProps.js';
import { BarChart } from './index.js';
import { cypressPassThroughTestsFactory, testChartLegendConfig, testChartZoomingTool } from '@/cypress/support/utils';
import {
cypressPassThroughTestsFactory,
testChartLegendConfig,
testChartZoomingTool,
testStackAggregateTotals,
} from '@/cypress/support/utils';

const dimensions = [
{
Expand Down Expand Up @@ -93,4 +98,13 @@ describe('BarChart', () => {
testChartZoomingTool(BarChart, { dataset: complexDataSet, dimensions, measures });

cypressPassThroughTestsFactory(BarChart, { dimensions: [], measures: [] });

testStackAggregateTotals(BarChart, {
dataset: complexDataSet.slice(0, 3),
dimensions,
measures: [
{ accessor: 'users', stackId: 'A', label: 'Users' },
{ accessor: 'sessions', stackId: 'A', label: 'Active Sessions' },
],
});
});
12 changes: 12 additions & 0 deletions packages/charts/src/components/BarChart/BarChart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import TooltipStory from '../../resources/TooltipConfig.mdx';
import LegendStory from '../../resources/LegendConfig.mdx';
import NormalizedStackedChartStory from '../../resources/NormalizedStackedChart.mdx';
import StackAggregateTotalsStory from '../../resources/StackAggregateTotals.mdx';
import CustomTooltipTotalStory from '../../resources/CustomTooltipTotal.mdx';
import * as ComponentStories from './BarChart.stories';

<Meta of={ComponentStories} />
Expand Down Expand Up @@ -67,6 +69,16 @@ You can set a reference line to any value by using the `referenceLine` `chartCon

<Canvas of={ComponentStories.WithHighlightedMeasure} />

<StackAggregateTotalsStory
of={ComponentStories.WithStackAggregateTotalsAndTooltip}
description={<>You can display a total label at the end of each stacked bar group by setting <code>chartConfig.showStackAggregateTotals</code> to <code>true</code>. The tooltip includes the total automatically when only a single bar per dimension is present.</>}
/>

<CustomTooltipTotalStory
of={ComponentStories.WithCustomTooltipTotal}
description={<>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 <code>tooltipConfig.content</code> prop to display a total for specific measures.</>}
/>

<TooltipStory of={ComponentStories.WithCustomTooltipConfig} />

<LegendStory of={ComponentStories.WithCustomLegendConfig} />
Expand Down
51 changes: 51 additions & 0 deletions packages/charts/src/components/BarChart/BarChart.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import {
complexDataSet,
CustomTooltipContent,
legendConfig,
secondaryDimensionDataSet,
simpleDataSet,
Expand Down Expand Up @@ -165,6 +166,56 @@ export const WithNormalizedStacks: Story = {
args: stackedNormalizedConfig,
};

export const WithStackAggregateTotalsAndTooltip: Story = {
name: 'With Stack Aggregate Totals',
args: {
dataset: complexDataSet.slice(0, 3),
measures: [
{
accessor: 'users',
stackId: 'A',
label: 'Users',
},
{
accessor: 'sessions',
stackId: 'A',
label: 'Active Sessions',
},
],
chartConfig: {
showStackAggregateTotals: true,
},
},
};

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: <CustomTooltipContent />,
},
},
};

export const WithCustomTooltipConfig: Story = {
args: tooltipConfig,
};
Expand Down
28 changes: 27 additions & 1 deletion packages/charts/src/components/BarChart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -168,11 +170,12 @@ const BarChart = forwardRef<HTMLDivElement, BarChartProps>((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);
Expand Down Expand Up @@ -224,6 +227,10 @@ const BarChart = forwardRef<HTMLDivElement, BarChartProps>((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 (
<ChartContainer
Expand Down Expand Up @@ -337,6 +344,17 @@ const BarChart = forwardRef<HTMLDivElement, BarChartProps>((props, ref) => {
valueAccessor={valueAccessor(element.accessor)}
content={<ChartDataLabel config={element} chartType="bar" position={'insideRight'} />}
/>
{chartConfig.showStackAggregateTotals &&
element.stackId &&
typeof element.accessor === 'string' &&
lastInStack.has(element.accessor) && (
<LabelList
data={dataset}
valueAccessor={valueAccessor(element.accessor)}
position="right"
content={<StackAggregateLabel stackAccessors={stackGroups[element.stackId]} dataset={dataset} />}
/>
)}
{dataset.map((data, i) => {
return (
<Cell
Expand Down Expand Up @@ -374,6 +392,14 @@ const BarChart = forwardRef<HTMLDivElement, BarChartProps>((props, ref) => {
contentStyle={tooltipContentStyle}
labelFormatter={tooltipLabelFormatter}
{...tooltipConfig}
{...(showStackTotalInTooltip && {
content: (
<StackedTooltipContent
stackAccessors={stackGroups[stackGroupKeys[0]]}
totalFormatter={chartConfig.stackAggregateTotalFormatter}
/>
),
})}
/>
)}
{!!chartConfig.zoomingTool && (
Expand Down
16 changes: 15 additions & 1 deletion packages/charts/src/components/ColumnChart/ColumnChart.cy.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { complexDataSet } from '../../resources/DemoProps.js';
import { ColumnChart } from './index.js';
import { cypressPassThroughTestsFactory, testChartLegendConfig, testChartZoomingTool } from '@/cypress/support/utils';
import {
cypressPassThroughTestsFactory,
testChartLegendConfig,
testChartZoomingTool,
testStackAggregateTotals,
} from '@/cypress/support/utils';

const dimensions = [
{
Expand Down Expand Up @@ -83,4 +88,13 @@ describe('ColumnChart', () => {
testChartLegendConfig(ColumnChart, { dataset: complexDataSet, dimensions, measures });

cypressPassThroughTestsFactory(ColumnChart, { dimensions: [], measures: [] });

testStackAggregateTotals(ColumnChart, {
dataset: complexDataSet.slice(0, 3),
dimensions,
measures: [
{ accessor: 'users', stackId: 'A', label: 'Users' },
{ accessor: 'sessions', stackId: 'A', label: 'Active Sessions' },
],
});
});
14 changes: 13 additions & 1 deletion packages/charts/src/components/ColumnChart/ColumnChart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import TooltipStory from '../../resources/TooltipConfig.mdx';
import LegendStory from '../../resources/LegendConfig.mdx';
import NormalizedStackedChartStory from '../../resources/NormalizedStackedChart.mdx';
import StackAggregateTotalsStory from '../../resources/StackAggregateTotals.mdx';
import CustomTooltipTotalStory from '../../resources/CustomTooltipTotal.mdx';
import * as ComponentStories from './ColumnChart.stories';

<Meta of={ComponentStories} />
Expand Down Expand Up @@ -62,10 +64,20 @@ You can set a reference line to any value by using the `referenceLine` `chartCon

<Canvas of={ComponentStories.WithReferenceLine} />

## With Highlighted Measures
### With Highlighted Measures

<Canvas of={ComponentStories.WithHighlightedMeasure} />

<StackAggregateTotalsStory
of={ComponentStories.WithStackAggregateTotals}
description={<>You can display a total label at the top of each stacked column group by setting <code>chartConfig.showStackAggregateTotals</code> to <code>true</code>. The tooltip includes the total automatically when only a single column per dimension is present.</>}
/>

<CustomTooltipTotalStory
of={ComponentStories.WithCustomTooltipTotal}
description={<>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 <code>tooltipConfig.content</code> prop to display a total for specific measures.</>}
/>

<TooltipStory of={ComponentStories.WithCustomTooltipConfig} />;

<LegendStory of={ComponentStories.WithCustomLegendConfig} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import {
complexDataSet,
CustomTooltipContent,
legendConfig,
secondaryDimensionDataSet,
simpleDataSet,
Expand Down Expand Up @@ -156,6 +157,57 @@ 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,
},
},
};

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: <CustomTooltipContent />,
},
},
};

export const WithCustomTooltipConfig: Story = {
args: tooltipConfig,
};
Expand Down
Loading
Loading