From 3ba8a0611c58eba13f987ff5c6e40038e8af0b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20M=C3=BCller?= Date: Mon, 13 May 2024 14:33:12 +0200 Subject: [PATCH 1/3] [@mantine/charts] BarChart: added waterfall type --- .../src/pages/charts/bar-chart.mdx | 10 +++++ .../BarChart/BarChart.demo.waterfall.tsx | 44 +++++++++++++++++++ .../charts/BarChart/BarChart.demos.story.tsx | 5 +++ .../demos/src/demos/charts/BarChart/_data.ts | 25 +++++++++++ .../demos/src/demos/charts/BarChart/index.ts | 1 + .../charts/src/BarChart/BarChart.story.tsx | 27 ++++++++++++ .../@mantine/charts/src/BarChart/BarChart.tsx | 43 ++++++++++++++++-- .../src/ChartLegend/ChartLegend.module.css | 4 ++ .../charts/src/ChartLegend/ChartLegend.tsx | 5 +++ .../charts/src/ChartTooltip/ChartTooltip.tsx | 6 +++ 10 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 packages/@docs/demos/src/demos/charts/BarChart/BarChart.demo.waterfall.tsx diff --git a/apps/mantine.dev/src/pages/charts/bar-chart.mdx b/apps/mantine.dev/src/pages/charts/bar-chart.mdx index e1bf92249dc..f3e367f5fa2 100644 --- a/apps/mantine.dev/src/pages/charts/bar-chart.mdx +++ b/apps/mantine.dev/src/pages/charts/bar-chart.mdx @@ -28,6 +28,16 @@ contribution of each series in terms of percentages. +## Waterfall bar chart + +Set `type="waterfall"` to render a waterfall bar chart. This chart type illustrates how an +initial value is influenced by subsequent positive or negative values, +with each bar starting where the previous one ended. +Use the `color` prop inside data to color each bar individually. Note that the series color gets overwritten for this specific bar. +Use the `standalone` prop inside data to decouple the bar from the flow. + + + ## Legend To display chart legend, set `withLegend` prop. When one of the items in the legend diff --git a/packages/@docs/demos/src/demos/charts/BarChart/BarChart.demo.waterfall.tsx b/packages/@docs/demos/src/demos/charts/BarChart/BarChart.demo.waterfall.tsx new file mode 100644 index 00000000000..c9f0bf73961 --- /dev/null +++ b/packages/@docs/demos/src/demos/charts/BarChart/BarChart.demo.waterfall.tsx @@ -0,0 +1,44 @@ +import { BarChart } from '@mantine/charts'; +import { MantineDemo } from '@mantinex/demo'; +import { waterfallCode, waterfallData } from './_data'; + +const code = ` +import { BarChart } from '@mantine/charts'; +import { data } from './data'; + + +function Demo() { + return ( + + ); +} +`; + +function Demo() { + return ( + + ); +} + +export const waterfall: MantineDemo = { + type: 'code', + component: Demo, + code: [ + { code, language: 'tsx', fileName: 'Demo.tsx' }, + { code: waterfallCode, language: 'tsx', fileName: 'data.ts' }, + ], +}; diff --git a/packages/@docs/demos/src/demos/charts/BarChart/BarChart.demos.story.tsx b/packages/@docs/demos/src/demos/charts/BarChart/BarChart.demos.story.tsx index 6912a8fca17..8dcb0fe29d7 100644 --- a/packages/@docs/demos/src/demos/charts/BarChart/BarChart.demos.story.tsx +++ b/packages/@docs/demos/src/demos/charts/BarChart/BarChart.demos.story.tsx @@ -88,6 +88,11 @@ export const Demo_stacked = { render: renderDemo(demos.stacked), }; +export const Demo_waterfall = { + name: '⭐ Demo: waterfall', + render: renderDemo(demos.waterfall), +}; + export const Demo_percent = { name: '⭐ Demo: percent', render: renderDemo(demos.percent), diff --git a/packages/@docs/demos/src/demos/charts/BarChart/_data.ts b/packages/@docs/demos/src/demos/charts/BarChart/_data.ts index 266fde5e081..8595642bad6 100644 --- a/packages/@docs/demos/src/demos/charts/BarChart/_data.ts +++ b/packages/@docs/demos/src/demos/charts/BarChart/_data.ts @@ -17,3 +17,28 @@ export const data = [ { month: 'June', Smartphones: 750, Laptops: 600, Tablets: 1000 }, ]; `; + +export const waterfallData = [ + { item: 'TaxRate', 'Effective tax rate in %': 21, color: 'blue.3' }, + { item: 'Foreign inc.', 'Effective tax rate in %': -15.5, color: 'green' }, + { item: 'Perm. diff.', 'Effective tax rate in %': -3, color: 'green' }, + { item: 'Credits', 'Effective tax rate in %': -3, color: 'green' }, + { item: 'Loss carryf. ', 'Effective tax rate in %': -2, color: 'green' }, + { item: 'Law changes', 'Effective tax rate in %': 2, color: 'red' }, + { item: 'Reven. adj.', 'Effective tax rate in %': 4, color: 'red' }, + { item: 'ETR', 'Effective tax rate in %': 3.5, color: 'blue.3', standalone: true }, +]; + +export const waterfallCode = ` +export const data = +[ + { item: 'TaxRate', 'Effective tax rate in %': 21, color: 'blue.3' }, + { item: 'Foreign inc.', 'Effective tax rate in %': -15.5, color: 'green' }, + { item: 'Perm. diff.', 'Effective tax rate in %': -3, color: 'green' }, + { item: 'Credits', 'Effective tax rate in %': -3, color: 'green' }, + { item: 'Loss carryf. ', 'Effective tax rate in %': -2, color: 'green' }, + { item: 'Law changes', 'Effective tax rate in %': 2, color: 'red' }, + { item: 'Reven. adj.', 'Effective tax rate in %': 4, color: 'red' }, + { item: 'ETR', 'Effective tax rate in %': 3.5, color: 'blue.3', standalone: true }, +]; +`; diff --git a/packages/@docs/demos/src/demos/charts/BarChart/index.ts b/packages/@docs/demos/src/demos/charts/BarChart/index.ts index 2c753e8307e..f6974073002 100644 --- a/packages/@docs/demos/src/demos/charts/BarChart/index.ts +++ b/packages/@docs/demos/src/demos/charts/BarChart/index.ts @@ -15,6 +15,7 @@ export { unit } from './BarChart.demo.unit'; export { xAxisOffset } from './BarChart.demo.xAxisOffset'; export { yScale } from './BarChart.demo.yScale'; export { stacked } from './BarChart.demo.stacked'; +export { waterfall } from './BarChart.demo.waterfall'; export { percent } from './BarChart.demo.percent'; export { vertical } from './BarChart.demo.vertical'; export { seriesLabels } from './BarChart.demo.seriesLabels'; diff --git a/packages/@mantine/charts/src/BarChart/BarChart.story.tsx b/packages/@mantine/charts/src/BarChart/BarChart.story.tsx index 5fe89648f8e..92f536ab58e 100644 --- a/packages/@mantine/charts/src/BarChart/BarChart.story.tsx +++ b/packages/@mantine/charts/src/BarChart/BarChart.story.tsx @@ -20,6 +20,17 @@ const data = [ { month: 'June', Smartphones: 40, Laptops: 45, Tablets: 50 }, ]; +const waterfallData = [ + { item: 'TaxRate', 'Effective tax rate in %': 21, color: 'blue.3' }, + { item: 'Foreign inc.', 'Effective tax rate in %': -15.5, color: 'green' }, + { item: 'Perm. diff.', 'Effective tax rate in %': -3, color: 'green' }, + { item: 'Credits', 'Effective tax rate in %': -3, color: 'green' }, + { item: 'Loss carryf. ', 'Effective tax rate in %': -2, color: 'green' }, + { item: 'Law changes', 'Effective tax rate in %': 2, color: 'red' }, + { item: 'Reven. adj.', 'Effective tax rate in %': 4, color: 'red' }, + { item: 'ETR', 'Effective tax rate in %': 3.5, color: 'blue.3', standalone: true }, +]; + export function Usage() { return (
@@ -73,6 +84,22 @@ export function Stacked() { ); } +export function Waterfall() { + return ( +
+ +
+ ); +} + export function Vertical() { return (
diff --git a/packages/@mantine/charts/src/BarChart/BarChart.tsx b/packages/@mantine/charts/src/BarChart/BarChart.tsx index b07f9673ad8..580198bbc90 100644 --- a/packages/@mantine/charts/src/BarChart/BarChart.tsx +++ b/packages/@mantine/charts/src/BarChart/BarChart.tsx @@ -3,6 +3,7 @@ import { Bar, BarProps, CartesianGrid, + Cell, Label, Legend, BarChart as ReChartsBarChart, @@ -38,7 +39,7 @@ function valueToPercent(value: number) { export interface BarChartSeries extends ChartSeries {} -export type BarChartType = 'default' | 'stacked' | 'percent'; +export type BarChartType = 'default' | 'stacked' | 'percent' | 'waterfall'; export type BarChartStylesNames = | 'bar' @@ -55,7 +56,7 @@ export interface BarChartProps GridChartBaseProps, StylesApiProps, ElementProps<'div'> { - /** Data used to display chart */ + /** Data used to display chart. Special keys: `color`: to adjust color on per Bar level. `standalone`: Opt out of the flow if `type="waterfall"` is set. */ data: Record[]; /** An array of objects with `name` and `color` keys. Determines which data should be consumed from the `data` array. */ @@ -186,6 +187,31 @@ export const BarChart = factory((_props, ref) => { props, }); + function calculateCumulativeTotal(waterfallData: Record[]) { + let start: number = 0; + let end: number = 0; + return waterfallData.map((item) => { + if (item.standalone) { + for (const prop in item) { + if (typeof item[prop] === 'number' && prop !== dataKey) { + item[prop] = [0, item[prop]]; + } + } + } else { + for (const prop in item) { + if (typeof item[prop] === 'number' && prop !== dataKey) { + end += item[prop]; + item[prop] = [start, end]; + start = end; + } + } + } + return item; + }); + } + + const inputData = type === 'waterfall' ? calculateCumulativeTotal(data) : data; + const getStyles = useStyles({ name: 'BarChart', classes, @@ -217,7 +243,14 @@ export const BarChart = factory((_props, ref) => { stackId={stacked ? 'stack' : undefined} label={withBarValueLabel ? : undefined} {...(typeof barProps === 'function' ? barProps(item) : barProps)} - /> + > + {inputData.map((entry, index) => ( + + ))} + ); }); @@ -250,7 +283,7 @@ export const BarChart = factory((_props, ref) => { > ((_props, ref) => { classNames={resolvedClassNames} styles={resolvedStyles} series={series} + showColor={type !== 'waterfall'} /> )} {...legendProps} @@ -346,6 +380,7 @@ export const BarChart = factory((_props, ref) => { ((_props, ref) => { legendPosition, mod, series, + showColor, ...others } = props; @@ -85,6 +89,7 @@ export const ChartLegend = factory((_props, ref) => { {...getStyles('legendItem')} onMouseEnter={() => onHighlight(item.dataKey)} onMouseLeave={() => onHighlight(null)} + data-type={showColor === false ? 'no-colorSwatch' : 'colorSwatch'} > [], s function getData(item: Record, type: 'area' | 'radial' | 'scatter') { if (type === 'radial' || type === 'scatter') { + if (Array.isArray(item.value)) { + return item.value[1] - item.value[0]; + } return item.value; } + if (Array.isArray(item.payload[item.dataKey])) { + return item.payload[item.dataKey][1] - item.payload[item.dataKey][0]; + } return item.payload[item.dataKey]; } From 547a4d5b4b7c0acaaa9c3d9b3140cb651120930b Mon Sep 17 00:00:00 2001 From: Vitaly Rtishchev Date: Fri, 24 May 2024 14:50:44 +0400 Subject: [PATCH 2/3] Code review and fixes --- .../charts/src/AreaChart/AreaChart.tsx | 1 + .../@mantine/charts/src/BarChart/BarChart.tsx | 50 +++++++++---------- .../src/ChartLegend/ChartLegend.module.css | 2 +- .../charts/src/ChartLegend/ChartLegend.tsx | 4 +- packages/@mantine/charts/src/types.ts | 2 +- 5 files changed, 30 insertions(+), 29 deletions(-) diff --git a/packages/@mantine/charts/src/AreaChart/AreaChart.tsx b/packages/@mantine/charts/src/AreaChart/AreaChart.tsx index ecdf0951d31..facf94223b2 100644 --- a/packages/@mantine/charts/src/AreaChart/AreaChart.tsx +++ b/packages/@mantine/charts/src/AreaChart/AreaChart.tsx @@ -42,6 +42,7 @@ function valueToPercent(value: number) { export interface AreaChartSeries extends ChartSeries { strokeDasharray?: string | number; + color: MantineColor; } export type AreaChartType = 'default' | 'stacked' | 'percent' | 'split'; diff --git a/packages/@mantine/charts/src/BarChart/BarChart.tsx b/packages/@mantine/charts/src/BarChart/BarChart.tsx index 580198bbc90..20f3c76bded 100644 --- a/packages/@mantine/charts/src/BarChart/BarChart.tsx +++ b/packages/@mantine/charts/src/BarChart/BarChart.tsx @@ -56,7 +56,7 @@ export interface BarChartProps GridChartBaseProps, StylesApiProps, ElementProps<'div'> { - /** Data used to display chart. Special keys: `color`: to adjust color on per Bar level. `standalone`: Opt out of the flow if `type="waterfall"` is set. */ + /** Data used to display chart. */ data: Record[]; /** An array of objects with `name` and `color` keys. Determines which data should be consumed from the `data` array. */ @@ -129,6 +129,29 @@ function BarLabel({ value, valueFormatter, ...others }: Record) { ); } +function calculateCumulativeTotal(waterfallData: Record[], dataKey: string) { + let start: number = 0; + let end: number = 0; + return waterfallData.map((item) => { + if (item.standalone) { + for (const prop in item) { + if (typeof item[prop] === 'number' && prop !== dataKey) { + item[prop] = [0, item[prop]]; + } + } + } else { + for (const prop in item) { + if (typeof item[prop] === 'number' && prop !== dataKey) { + end += item[prop]; + item[prop] = [start, end]; + start = end; + } + } + } + return item; + }); +} + export const BarChart = factory((_props, ref) => { const props = useProps('BarChart', defaultProps, _props); const { @@ -187,30 +210,7 @@ export const BarChart = factory((_props, ref) => { props, }); - function calculateCumulativeTotal(waterfallData: Record[]) { - let start: number = 0; - let end: number = 0; - return waterfallData.map((item) => { - if (item.standalone) { - for (const prop in item) { - if (typeof item[prop] === 'number' && prop !== dataKey) { - item[prop] = [0, item[prop]]; - } - } - } else { - for (const prop in item) { - if (typeof item[prop] === 'number' && prop !== dataKey) { - end += item[prop]; - item[prop] = [start, end]; - start = end; - } - } - } - return item; - }); - } - - const inputData = type === 'waterfall' ? calculateCumulativeTotal(data) : data; + const inputData = type === 'waterfall' ? calculateCumulativeTotal(data, dataKey) : data; const getStyles = useStyles({ name: 'BarChart', diff --git a/packages/@mantine/charts/src/ChartLegend/ChartLegend.module.css b/packages/@mantine/charts/src/ChartLegend/ChartLegend.module.css index bd8206f351a..5a56e3c8148 100644 --- a/packages/@mantine/charts/src/ChartLegend/ChartLegend.module.css +++ b/packages/@mantine/charts/src/ChartLegend/ChartLegend.module.css @@ -32,7 +32,7 @@ } } - &[data-type='no-colorSwatch'] .legendItemColor { + &[data-without-color] .legendItemColor { display: none; } } diff --git a/packages/@mantine/charts/src/ChartLegend/ChartLegend.tsx b/packages/@mantine/charts/src/ChartLegend/ChartLegend.tsx index 51fdf5dc7f0..16b59770ea9 100644 --- a/packages/@mantine/charts/src/ChartLegend/ChartLegend.tsx +++ b/packages/@mantine/charts/src/ChartLegend/ChartLegend.tsx @@ -35,7 +35,7 @@ export interface ChartLegendProps /** Data used for labels, only applicable for area charts: AreaChart, LineChart, BarChart */ series?: ChartSeries[]; - /** Show color swatch next to the legend item */ + /** Determines whether color swatch should be shown next to the label, `true` by default */ showColor?: boolean; } @@ -89,7 +89,7 @@ export const ChartLegend = factory((_props, ref) => { {...getStyles('legendItem')} onMouseEnter={() => onHighlight(item.dataKey)} onMouseLeave={() => onHighlight(null)} - data-type={showColor === false ? 'no-colorSwatch' : 'colorSwatch'} + data-without-color={showColor === false || undefined} > Date: Fri, 24 May 2024 14:53:38 +0400 Subject: [PATCH 3/3] Update demo code --- .../demos/src/demos/charts/BarChart/_data.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/@docs/demos/src/demos/charts/BarChart/_data.ts b/packages/@docs/demos/src/demos/charts/BarChart/_data.ts index 8595642bad6..9080e5387a2 100644 --- a/packages/@docs/demos/src/demos/charts/BarChart/_data.ts +++ b/packages/@docs/demos/src/demos/charts/BarChart/_data.ts @@ -19,26 +19,26 @@ export const data = [ `; export const waterfallData = [ - { item: 'TaxRate', 'Effective tax rate in %': 21, color: 'blue.3' }, - { item: 'Foreign inc.', 'Effective tax rate in %': -15.5, color: 'green' }, - { item: 'Perm. diff.', 'Effective tax rate in %': -3, color: 'green' }, - { item: 'Credits', 'Effective tax rate in %': -3, color: 'green' }, - { item: 'Loss carryf. ', 'Effective tax rate in %': -2, color: 'green' }, + { item: 'TaxRate', 'Effective tax rate in %': 21, color: 'blue' }, + { item: 'Foreign inc.', 'Effective tax rate in %': -15.5, color: 'teal' }, + { item: 'Perm. diff.', 'Effective tax rate in %': -3, color: 'teal' }, + { item: 'Credits', 'Effective tax rate in %': -3, color: 'teal' }, + { item: 'Loss carryf. ', 'Effective tax rate in %': -2, color: 'teal' }, { item: 'Law changes', 'Effective tax rate in %': 2, color: 'red' }, { item: 'Reven. adj.', 'Effective tax rate in %': 4, color: 'red' }, - { item: 'ETR', 'Effective tax rate in %': 3.5, color: 'blue.3', standalone: true }, + { item: 'ETR', 'Effective tax rate in %': 3.5, color: 'blue', standalone: true }, ]; export const waterfallCode = ` export const data = [ - { item: 'TaxRate', 'Effective tax rate in %': 21, color: 'blue.3' }, - { item: 'Foreign inc.', 'Effective tax rate in %': -15.5, color: 'green' }, - { item: 'Perm. diff.', 'Effective tax rate in %': -3, color: 'green' }, - { item: 'Credits', 'Effective tax rate in %': -3, color: 'green' }, - { item: 'Loss carryf. ', 'Effective tax rate in %': -2, color: 'green' }, + { item: 'TaxRate', 'Effective tax rate in %': 21, color: 'blue' }, + { item: 'Foreign inc.', 'Effective tax rate in %': -15.5, color: 'teal' }, + { item: 'Perm. diff.', 'Effective tax rate in %': -3, color: 'teal' }, + { item: 'Credits', 'Effective tax rate in %': -3, color: 'teal' }, + { item: 'Loss carryf. ', 'Effective tax rate in %': -2, color: 'teal' }, { item: 'Law changes', 'Effective tax rate in %': 2, color: 'red' }, { item: 'Reven. adj.', 'Effective tax rate in %': 4, color: 'red' }, - { item: 'ETR', 'Effective tax rate in %': 3.5, color: 'blue.3', standalone: true }, + { item: 'ETR', 'Effective tax rate in %': 3.5, color: 'blue', standalone: true }, ]; `;