Skip to content

Commit

Permalink
[@mantine/charts] BarChart: Add waterfall type (#6231)
Browse files Browse the repository at this point in the history
* [@mantine/charts] BarChart: added waterfall type

* Code review and fixes

* Update demo code

---------

Co-authored-by: Vitaly Rtishchev <rtivital@gmail.com>
  • Loading branch information
Maetes and rtivital committed May 24, 2024
1 parent 7f003a5 commit 8bdabaf
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 5 deletions.
10 changes: 10 additions & 0 deletions apps/mantine.dev/src/pages/charts/bar-chart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ contribution of each series in terms of percentages.

<Demo data={BarChartDemos.percent} />

## 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.

<Demo data={BarChartDemos.waterfall} />

## Legend

To display chart legend, set `withLegend` prop. When one of the items in the legend
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<BarChart
h={300}
data={data}
dataKey="item"
type="waterfall"
series={[{ name: 'Effective tax rate in %', color: 'blue' }]}
withLegend
/>
);
}
`;

function Demo() {
return (
<BarChart
h={300}
data={waterfallData}
dataKey="item"
type="waterfall"
series={[{ name: 'Effective tax rate in %', color: 'blue' }]}
withLegend
/>
);
}

export const waterfall: MantineDemo = {
type: 'code',
component: Demo,
code: [
{ code, language: 'tsx', fileName: 'Demo.tsx' },
{ code: waterfallCode, language: 'tsx', fileName: 'data.ts' },
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
25 changes: 25 additions & 0 deletions packages/@docs/demos/src/demos/charts/BarChart/_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
{ 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', standalone: true },
];

export const waterfallCode = `
export const data =
[
{ 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', standalone: true },
];
`;
1 change: 1 addition & 0 deletions packages/@docs/demos/src/demos/charts/BarChart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions packages/@mantine/charts/src/AreaChart/AreaChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
27 changes: 27 additions & 0 deletions packages/@mantine/charts/src/BarChart/BarChart.story.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div style={{ padding: 40 }}>
Expand Down Expand Up @@ -73,6 +84,22 @@ export function Stacked() {
);
}

export function Waterfall() {
return (
<div style={{ padding: 40 }}>
<BarChart
h={300}
data={waterfallData}
dataKey="item"
type="waterfall"
fillOpacity={0.6}
withLegend
series={[{ name: 'Effective tax rate in %', color: 'blue' }]}
/>
</div>
);
}

export function Vertical() {
return (
<div style={{ padding: 40 }}>
Expand Down
43 changes: 39 additions & 4 deletions packages/@mantine/charts/src/BarChart/BarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Bar,
BarProps,
CartesianGrid,
Cell,
Label,
Legend,
BarChart as ReChartsBarChart,
Expand Down Expand Up @@ -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'
Expand All @@ -55,7 +56,7 @@ export interface BarChartProps
GridChartBaseProps,
StylesApiProps<BarChartFactory>,
ElementProps<'div'> {
/** Data used to display chart */
/** Data used to display chart. */
data: Record<string, any>[];

/** An array of objects with `name` and `color` keys. Determines which data should be consumed from the `data` array. */
Expand Down Expand Up @@ -128,6 +129,29 @@ function BarLabel({ value, valueFormatter, ...others }: Record<string, any>) {
);
}

function calculateCumulativeTotal(waterfallData: Record<string, any>[], 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<BarChartFactory>((_props, ref) => {
const props = useProps('BarChart', defaultProps, _props);
const {
Expand Down Expand Up @@ -186,6 +210,8 @@ export const BarChart = factory<BarChartFactory>((_props, ref) => {
props,
});

const inputData = type === 'waterfall' ? calculateCumulativeTotal(data, dataKey) : data;

const getStyles = useStyles<BarChartFactory>({
name: 'BarChart',
classes,
Expand Down Expand Up @@ -217,7 +243,14 @@ export const BarChart = factory<BarChartFactory>((_props, ref) => {
stackId={stacked ? 'stack' : undefined}
label={withBarValueLabel ? <BarLabel valueFormatter={valueFormatter} /> : undefined}
{...(typeof barProps === 'function' ? barProps(item) : barProps)}
/>
>
{inputData.map((entry, index) => (
<Cell
key={`cell-${index}`}
fill={entry.color ? getThemeColor(entry.color, theme) : color}
/>
))}
</Bar>
);
});

Expand Down Expand Up @@ -250,7 +283,7 @@ export const BarChart = factory<BarChartFactory>((_props, ref) => {
>
<ResponsiveContainer {...getStyles('container')}>
<ReChartsBarChart
data={data}
data={inputData}
stackOffset={type === 'percent' ? 'expand' : undefined}
layout={orientation}
margin={{
Expand All @@ -271,6 +304,7 @@ export const BarChart = factory<BarChartFactory>((_props, ref) => {
classNames={resolvedClassNames}
styles={resolvedStyles}
series={series}
showColor={type !== 'waterfall'}
/>
)}
{...legendProps}
Expand Down Expand Up @@ -346,6 +380,7 @@ export const BarChart = factory<BarChartFactory>((_props, ref) => {
<ChartTooltip
label={label}
payload={payload}
type={type === 'waterfall' ? 'scatter' : undefined}
unit={unit}
classNames={resolvedClassNames}
styles={resolvedStyles}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@
background-color: var(--mantine-color-dark-5);
}
}

&[data-without-color] .legendItemColor {
display: none;
}
}

.legendItemName {
Expand Down
5 changes: 5 additions & 0 deletions packages/@mantine/charts/src/ChartLegend/ChartLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export interface ChartLegendProps

/** Data used for labels, only applicable for area charts: AreaChart, LineChart, BarChart */
series?: ChartSeries[];

/** Determines whether color swatch should be shown next to the label, `true` by default */
showColor?: boolean;
}

export type ChartLegendFactory = Factory<{
Expand All @@ -58,6 +61,7 @@ export const ChartLegend = factory<ChartLegendFactory>((_props, ref) => {
legendPosition,
mod,
series,
showColor,
...others
} = props;

Expand Down Expand Up @@ -85,6 +89,7 @@ export const ChartLegend = factory<ChartLegendFactory>((_props, ref) => {
{...getStyles('legendItem')}
onMouseEnter={() => onHighlight(item.dataKey)}
onMouseLeave={() => onHighlight(null)}
data-without-color={showColor === false || undefined}
>
<ColorSwatch
color={item.color}
Expand Down
6 changes: 6 additions & 0 deletions packages/@mantine/charts/src/ChartTooltip/ChartTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,15 @@ export function getFilteredChartTooltipPayload(payload: Record<string, any>[], s

function getData(item: Record<string, any>, 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];
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@mantine/charts/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface ChartReferenceLineProps extends Omit<ReferenceLineProps, 'ref'

export interface ChartSeries {
name: string;
color: MantineColor;
color?: MantineColor;
label?: string;
}

Expand Down

0 comments on commit 8bdabaf

Please sign in to comment.