Skip to content

Commit dd1196d

Browse files
author
Roman Snapko
committed
Add ComboChart component with Storybook documentation
1 parent da06fb8 commit dd1196d

3 files changed

Lines changed: 331 additions & 0 deletions

File tree

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import type { Meta, StoryObj } from '@storybook/react';
2+
3+
import { ComboChart } from '@/ui/chart';
4+
import { ThemeAwareDecorator } from '../../../.storybook/decorators';
5+
6+
/**
7+
* A combination bar and line chart component built on top of Recharts primitives.
8+
* Bars are plotted against the left Y-axis, lines against the right Y-axis,
9+
* and both share a unified bottom X-axis.
10+
*/
11+
const meta: Meta<typeof ComboChart> = {
12+
title: 'ui/Charts/ComboChart',
13+
component: ComboChart,
14+
tags: ['autodocs'],
15+
parameters: {
16+
layout: 'centered',
17+
},
18+
args: {
19+
className: 'h-[227px] min-w-[770px]',
20+
xAxisKey: 'week',
21+
showXAxis: true,
22+
showLeftYAxis: false,
23+
showRightYAxis: false,
24+
showTooltip: true,
25+
showLegend: false,
26+
barSize: 12,
27+
leftYAxisTickFormatter: (value: number) => `${value}`,
28+
rightYAxisTickFormatter: (value: number) =>
29+
value === 0 ? '0' : `$${value / 1000}K`,
30+
bars: [
31+
{
32+
dataKey: 'savings',
33+
radius: 4,
34+
},
35+
],
36+
lines: [
37+
{ dataKey: 'idealBurndown', strokeWidth: 4 },
38+
{ dataKey: 'remainingSavings', strokeWidth: 4, type: 'linear' },
39+
{ dataKey: 'remainingRecommendations', strokeWidth: 4, type: 'linear' },
40+
],
41+
},
42+
decorators: [ThemeAwareDecorator],
43+
} satisfies Meta<typeof ComboChart>;
44+
45+
export default meta;
46+
47+
type Story = StoryObj<typeof meta>;
48+
49+
const defaultData = [
50+
{
51+
week: 'Week 1',
52+
savings: 40,
53+
idealBurndown: 250000,
54+
remainingSavings: 220000,
55+
remainingRecommendations: 200000,
56+
},
57+
{
58+
week: 'Week 2',
59+
savings: 80,
60+
idealBurndown: 214000,
61+
remainingSavings: 205000,
62+
remainingRecommendations: 185000,
63+
},
64+
{
65+
week: 'Week 3',
66+
savings: 60,
67+
idealBurndown: 178000,
68+
remainingSavings: 175000,
69+
remainingRecommendations: 145000,
70+
},
71+
{
72+
week: 'Week 4',
73+
savings: 120,
74+
idealBurndown: 143000,
75+
remainingSavings: 145000,
76+
remainingRecommendations: 160000,
77+
},
78+
{
79+
week: 'Week 5',
80+
savings: 90,
81+
idealBurndown: 107000,
82+
remainingSavings: 120000,
83+
remainingRecommendations: 105000,
84+
},
85+
{
86+
week: 'Week 6',
87+
savings: 50,
88+
idealBurndown: 71000,
89+
remainingSavings: 95000,
90+
remainingRecommendations: 75000,
91+
},
92+
{
93+
week: 'Week 7',
94+
savings: 140,
95+
idealBurndown: 36000,
96+
remainingSavings: 55000,
97+
remainingRecommendations: 50000,
98+
},
99+
{
100+
week: 'Week 8',
101+
savings: 100,
102+
idealBurndown: 0,
103+
remainingSavings: 30000,
104+
remainingRecommendations: 15000,
105+
},
106+
];
107+
108+
const defaultConfig = {
109+
savings: {
110+
label: 'Savings',
111+
theme: { light: '#42e08c', dark: '#359763' },
112+
},
113+
idealBurndown: {
114+
label: 'Ideal Burndown',
115+
theme: { light: '#3b82f6', dark: '#60a5fa' },
116+
},
117+
remainingSavings: {
118+
label: 'Remaining Savings',
119+
theme: { light: '#eab308', dark: '#facc15' },
120+
},
121+
remainingRecommendations: {
122+
label: 'Remaining Recommendations',
123+
theme: { light: '#ec4899', dark: '#f472b6' },
124+
},
125+
};
126+
127+
/**
128+
* A simple combo chart with one bar series (Savings) and three line series.
129+
*/
130+
export const Default: Story = {
131+
args: {
132+
data: defaultData,
133+
config: defaultConfig,
134+
},
135+
};
136+
137+
/**
138+
* Combo chart with both left (bar) and right (line) Y-axes visible.
139+
* Axis ranges are derived from the data values.
140+
*/
141+
export const WithAxes: Story = {
142+
args: {
143+
data: defaultData,
144+
config: defaultConfig,
145+
showLeftYAxis: true,
146+
showRightYAxis: true,
147+
},
148+
};
149+
150+
/**
151+
* Combo chart with the legend displayed above the chart.
152+
*/
153+
export const WithLegend: Story = {
154+
args: {
155+
data: defaultData,
156+
config: defaultConfig,
157+
showLegend: true,
158+
},
159+
};
160+
161+
/**
162+
* Combo chart combining bars with line series, both axes, legend, and custom bar spacing.
163+
*/
164+
export const AllFeatures: Story = {
165+
args: {
166+
data: defaultData,
167+
config: defaultConfig,
168+
showLeftYAxis: true,
169+
showRightYAxis: true,
170+
showLegend: true,
171+
barCategoryGap: '55%',
172+
legendClassName: 'mb-[26px] justify-end mr-[50px]',
173+
},
174+
};
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import * as React from 'react';
2+
import { Bar, ComposedChart, Customized, Line, XAxis, YAxis } from 'recharts';
3+
import {
4+
ChartConfig,
5+
ChartContainer,
6+
ChartLegend,
7+
ChartLegendContent,
8+
ChartTooltip,
9+
ChartTooltipContent,
10+
PlotAreaBackground,
11+
} from './chart';
12+
13+
export type ComboChartBarDefinition = {
14+
dataKey: string;
15+
radius?: number | [number, number, number, number];
16+
stackId?: string;
17+
};
18+
19+
export type ComboChartLineDefinition = {
20+
dataKey: string;
21+
dot?: boolean;
22+
strokeWidth?: number;
23+
type?: 'monotone' | 'natural' | 'linear' | 'step';
24+
};
25+
26+
export type ComboChartProps = {
27+
data: Record<string, unknown>[];
28+
config: ChartConfig;
29+
bars: ComboChartBarDefinition[];
30+
lines: ComboChartLineDefinition[];
31+
leftYAxisTickFormatter?: (value: any) => string;
32+
rightYAxisTickFormatter?: (value: any) => string;
33+
xAxisTickFormatter?: (value: any) => string;
34+
xAxisKey?: string;
35+
showXAxis?: boolean;
36+
showLeftYAxis?: boolean;
37+
showRightYAxis?: boolean;
38+
leftYAxisTicks?: number[];
39+
rightYAxisTicks?: number[];
40+
leftYAxisDomain?: [number | string, number | string];
41+
rightYAxisDomain?: [number | string, number | string];
42+
showTooltip?: boolean;
43+
showLegend?: boolean;
44+
barSize?: number;
45+
barCategoryGap?: number | string;
46+
legendClassName?: string;
47+
className?: string;
48+
};
49+
50+
export function ComboChart({
51+
data,
52+
config,
53+
bars,
54+
lines,
55+
leftYAxisTickFormatter,
56+
rightYAxisTickFormatter,
57+
xAxisTickFormatter,
58+
xAxisKey = 'name',
59+
showXAxis = true,
60+
showLeftYAxis = false,
61+
showRightYAxis = false,
62+
leftYAxisTicks,
63+
rightYAxisTicks,
64+
leftYAxisDomain,
65+
rightYAxisDomain,
66+
showTooltip = true,
67+
showLegend = false,
68+
barSize,
69+
barCategoryGap,
70+
legendClassName,
71+
className,
72+
}: ComboChartProps): React.JSX.Element {
73+
return (
74+
<ChartContainer config={config} className={className}>
75+
<ComposedChart
76+
data={data}
77+
accessibilityLayer
78+
barSize={barSize}
79+
barCategoryGap={barCategoryGap}
80+
>
81+
<Customized component={PlotAreaBackground} />
82+
{showXAxis && (
83+
<XAxis
84+
dataKey={xAxisKey}
85+
tickLine={false}
86+
tickMargin={20}
87+
axisLine={false}
88+
tickFormatter={xAxisTickFormatter}
89+
tick={{
90+
fill: 'hsl(var(--foreground))',
91+
}}
92+
/>
93+
)}
94+
{showLeftYAxis && (
95+
<YAxis
96+
yAxisId="left"
97+
orientation="left"
98+
tickFormatter={leftYAxisTickFormatter}
99+
tickLine={false}
100+
axisLine={false}
101+
ticks={leftYAxisTicks}
102+
domain={leftYAxisDomain}
103+
tick={{
104+
fill: 'hsl(var(--foreground))',
105+
}}
106+
/>
107+
)}
108+
{showRightYAxis && (
109+
<YAxis
110+
yAxisId="right"
111+
orientation="right"
112+
tickFormatter={rightYAxisTickFormatter}
113+
tickLine={false}
114+
axisLine={false}
115+
ticks={rightYAxisTicks}
116+
domain={rightYAxisDomain}
117+
tick={{
118+
fill: 'hsl(var(--foreground))',
119+
}}
120+
/>
121+
)}
122+
{showTooltip && (
123+
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
124+
)}
125+
{showLegend && (
126+
<ChartLegend
127+
verticalAlign="top"
128+
className={legendClassName}
129+
content={<ChartLegendContent />}
130+
/>
131+
)}
132+
{bars.map((bar) => (
133+
<Bar
134+
key={bar.dataKey}
135+
yAxisId="left"
136+
dataKey={bar.dataKey}
137+
fill={`var(--color-${bar.dataKey})`}
138+
radius={bar.radius ?? 4}
139+
stackId={bar.stackId}
140+
/>
141+
))}
142+
{lines.map((line) => (
143+
<Line
144+
key={line.dataKey}
145+
yAxisId="right"
146+
dataKey={line.dataKey}
147+
stroke={`var(--color-${line.dataKey})`}
148+
dot={line.dot ?? false}
149+
strokeWidth={line.strokeWidth ?? 2}
150+
type={line.type ?? 'monotone'}
151+
/>
152+
))}
153+
</ComposedChart>
154+
</ChartContainer>
155+
);
156+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './bar-chart';
22
export * from './chart';
3+
export * from './combo-chart';

0 commit comments

Comments
 (0)