Skip to content

Commit a1b1921

Browse files
authored
Merge pull request #442 from buildo/basic-charts
Basic charts
2 parents 13123d1 + c104de0 commit a1b1921

26 files changed

+959
-3
lines changed

Diff for: packages/bento-design-system/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"react-select": "^5.4.0",
9292
"react-table": "^7.8.0",
9393
"react-use": "^17.4.0",
94+
"recharts": "^2.1.16",
9495
"ts-deepmerge": "^2.0.3",
9596
"ts-pattern": "^3.3.5"
9697
},

Diff for: packages/bento-design-system/src/BentoConfig.ts

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { BannerConfig } from "./Banner/Config";
55
import { BreadcrumbConfig } from "./Breadcrumb/Config";
66
import { ButtonConfig } from "./Button/Config";
77
import { CardConfig } from "./Card/Config";
8+
import { ChartConfig } from "./Charts/Config";
89
import { ChipConfig } from "./Chip/Config";
910
import { DateFieldConfig } from "./DateField/Config";
1011
import { DisclosureConfig } from "./Disclosure/Config";
@@ -45,6 +46,7 @@ export type BentoConfig = {
4546
breadcrumb: BreadcrumbConfig;
4647
button: ButtonConfig;
4748
card: CardConfig;
49+
chart: ChartConfig;
4850
chip: ChipConfig;
4951
dateField: DateFieldConfig;
5052
decorativeDivider: DecorativeDividerConfig;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {
2+
Bar,
3+
BarChart as RechartBarChart,
4+
Legend,
5+
ResponsiveContainer,
6+
Tooltip,
7+
XAxis,
8+
YAxis,
9+
} from "recharts";
10+
import { useBentoConfig } from "../../BentoConfigContext";
11+
import { bodyRecipe } from "../../Typography/Body/Body.css";
12+
import { allColors } from "../../util/atoms";
13+
import { vars } from "../../vars.css";
14+
import { ChartProps } from "../ChartProps";
15+
import { legendContent } from "../Legend/Legend";
16+
import { tooltipContent } from "../Tooltip/Tooltip";
17+
18+
type Props<D extends string, C extends string> = ChartProps & {
19+
data: Record<D | C, unknown>[];
20+
categories: C[];
21+
dataKey: D;
22+
hideXAxis?: boolean;
23+
hideYAxis?: boolean;
24+
stacked?: boolean;
25+
};
26+
27+
export type { Props as BarChartProps };
28+
29+
export function BarChart<D extends string, C extends string>({
30+
data,
31+
dataKey,
32+
categories,
33+
hideXAxis = false,
34+
hideYAxis = false,
35+
hideLegend = false,
36+
disableAnimation = false,
37+
hideTooltip = false,
38+
width = "100%",
39+
height,
40+
minWidth,
41+
minHeight,
42+
maxHeight,
43+
aspect,
44+
debounce,
45+
stacked = false,
46+
dataColors,
47+
children,
48+
}: Props<D, C>) {
49+
const config = useBentoConfig();
50+
const colors = (dataColors ?? config.chart.defaultDataColors).map(
51+
(colorName) => allColors[colorName]
52+
);
53+
54+
return (
55+
<ResponsiveContainer
56+
className={bodyRecipe({ size: "medium", weight: "default", color: "default" })}
57+
width={width}
58+
height={height}
59+
minWidth={minWidth}
60+
minHeight={minHeight}
61+
maxHeight={maxHeight}
62+
aspect={aspect}
63+
debounce={debounce}
64+
>
65+
<RechartBarChart data={data}>
66+
{!hideXAxis && <XAxis dataKey={dataKey} />}
67+
{!hideYAxis && <YAxis />}
68+
{!hideTooltip && (
69+
<Tooltip
70+
content={tooltipContent}
71+
cursor={{ fill: vars.backgroundColor.backgroundSecondary }}
72+
/>
73+
)}
74+
{!hideLegend && <Legend content={legendContent} />}
75+
{categories.map((category, i) => (
76+
<Bar
77+
key={category}
78+
dataKey={category}
79+
fill={colors[i % colors.length]}
80+
isAnimationActive={!disableAnimation}
81+
stackId={stacked ? "stack" : undefined}
82+
/>
83+
))}
84+
{children}
85+
</RechartBarChart>
86+
</ResponsiveContainer>
87+
);
88+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { ResponsiveContainerProps } from "recharts";
2+
import { Children } from "../util/Children";
3+
import { ChartDataColor } from "./Config";
4+
5+
export type ChartProps = {
6+
dataColors?: ChartDataColor[];
7+
disableAnimation?: boolean;
8+
hideLegend?: boolean;
9+
hideTooltip?: boolean;
10+
children?: Children;
11+
} & Pick<
12+
ResponsiveContainerProps,
13+
"width" | "height" | "minWidth" | "minHeight" | "maxHeight" | "aspect" | "debounce"
14+
>;

Diff for: packages/bento-design-system/src/Charts/Config.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { allColors } from "../util/atoms";
2+
3+
export type ChartDataColor = keyof typeof allColors;
4+
5+
export type ChartConfig = {
6+
defaultDataColors: ChartDataColor[];
7+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {
2+
PieChart as RechartPieChart,
3+
Pie,
4+
Cell,
5+
Tooltip,
6+
Legend,
7+
ResponsiveContainer,
8+
} from "recharts";
9+
import { useBentoConfig } from "../../BentoConfigContext";
10+
import { bodyRecipe } from "../../Typography/Body/Body.css";
11+
import { allColors } from "../../util/atoms";
12+
import { ChartProps } from "../ChartProps";
13+
import { legendContent } from "../Legend/Legend";
14+
import { tooltipContent } from "../Tooltip/Tooltip";
15+
16+
type Props<D extends string, C extends string> = ChartProps & {
17+
data: Record<C | D, unknown>[];
18+
category: C;
19+
dataKey: D;
20+
};
21+
22+
export type { Props as DonutChartProps };
23+
24+
export function DonutChart<D extends string, C extends string>({
25+
data,
26+
dataKey,
27+
category,
28+
hideLegend = false,
29+
disableAnimation = false,
30+
hideTooltip = false,
31+
width = "100%",
32+
height,
33+
minWidth,
34+
minHeight,
35+
maxHeight,
36+
aspect,
37+
debounce,
38+
dataColors,
39+
children,
40+
}: Props<D, C>) {
41+
const config = useBentoConfig();
42+
const colors = (dataColors ?? config.chart.defaultDataColors).map(
43+
(colorName) => allColors[colorName]
44+
);
45+
46+
return (
47+
<ResponsiveContainer
48+
className={bodyRecipe({ size: "medium", weight: "default", color: "default" })}
49+
width={width}
50+
height={height}
51+
minWidth={minWidth}
52+
minHeight={minHeight}
53+
maxHeight={maxHeight}
54+
aspect={aspect}
55+
debounce={debounce}
56+
>
57+
<RechartPieChart>
58+
<Pie
59+
data={data}
60+
dataKey={category}
61+
nameKey={dataKey}
62+
isAnimationActive={!disableAnimation}
63+
cx="50%"
64+
cy="50%"
65+
startAngle={90}
66+
endAngle={-270}
67+
innerRadius="75%"
68+
outerRadius="100%"
69+
paddingAngle={0}
70+
// Remove 1px gap between slices
71+
stroke=""
72+
>
73+
{data.map((_entry, i) => (
74+
<Cell key={`cell-${i}`} fill={colors[i % colors.length]} />
75+
))}
76+
</Pie>
77+
{!hideTooltip && <Tooltip content={tooltipContent} />}
78+
{!hideLegend && <Legend content={legendContent} />}
79+
{children}
80+
</RechartPieChart>
81+
</ResponsiveContainer>
82+
);
83+
}
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { LegendProps as RechartsLegendProps } from "recharts";
2+
import type { Payload } from "recharts/types/component/DefaultLegendContent";
3+
import { match } from "ts-pattern";
4+
import { Box } from "../../Box/Box";
5+
import { Column, Columns } from "../../Layout/Columns";
6+
import { Inline } from "../../Layout/Inline";
7+
import { Body } from "../../Typography/Body/Body";
8+
9+
export const legendContent: Required<RechartsLegendProps>["content"] = ({ payload = [] }) => {
10+
return (
11+
<Box paddingTop={8} width="full">
12+
<Inline space={16} alignY="center" align="center">
13+
{payload.map((entry) =>
14+
match(entry.type ?? "square")
15+
.with("plainline", "line", () => <LegendItemLine key={entry.value} {...entry} />)
16+
.with(
17+
"rect",
18+
"square",
19+
"circle",
20+
"cross",
21+
"diamond",
22+
"star",
23+
"triangle",
24+
"wye",
25+
"none",
26+
() => <LegendItemArea key={entry.value} {...entry} />
27+
)
28+
.exhaustive()
29+
)}
30+
</Inline>
31+
</Box>
32+
);
33+
};
34+
35+
function LegendItemArea({ color, value }: Payload) {
36+
return (
37+
<Columns space={4} alignY="center">
38+
<Column width="content">
39+
<Box height={16} width={16} borderRadius={4} style={{ backgroundColor: color }} />
40+
</Column>
41+
<Body size="small">{value}</Body>
42+
</Columns>
43+
);
44+
}
45+
46+
function LegendItemLine({ color, value }: Payload) {
47+
return (
48+
<Columns space={4} alignY="center">
49+
<Column width="content">
50+
<Box width={16} borderRadius="circledX" style={{ height: 3, backgroundColor: color }} />
51+
</Column>
52+
<Body size="small">{value}</Body>
53+
</Columns>
54+
);
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import {
2+
LineChart as RechartLineChart,
3+
Line,
4+
XAxis,
5+
YAxis,
6+
Tooltip,
7+
Legend,
8+
ResponsiveContainer,
9+
} from "recharts";
10+
import { useBentoConfig } from "../../BentoConfigContext";
11+
import { bodyRecipe } from "../../Typography/Body/Body.css";
12+
import { allColors } from "../../util/atoms";
13+
import { ChartProps } from "../ChartProps";
14+
import { legendContent } from "../Legend/Legend";
15+
import { tooltipContent } from "../Tooltip/Tooltip";
16+
17+
type Props<D extends string, C extends string> = ChartProps & {
18+
data: Record<D | C, unknown>[];
19+
categories: C[];
20+
dataKey: D;
21+
hideXAxis?: boolean;
22+
hideYAxis?: boolean;
23+
lineType?:
24+
| "linear"
25+
| "natural"
26+
| "monotoneX"
27+
| "monotoneY"
28+
| "monotone"
29+
| "step"
30+
| "stepBefore"
31+
| "stepAfter";
32+
};
33+
34+
export type { Props as LineChartProps };
35+
36+
export function LineChart<D extends string, C extends string>({
37+
data,
38+
dataKey,
39+
categories,
40+
hideXAxis = false,
41+
hideYAxis = false,
42+
hideLegend = false,
43+
disableAnimation = false,
44+
hideTooltip = false,
45+
width = "100%",
46+
height,
47+
minWidth,
48+
minHeight,
49+
maxHeight,
50+
aspect,
51+
debounce,
52+
lineType = "monotone",
53+
dataColors,
54+
children,
55+
}: Props<D, C>) {
56+
const config = useBentoConfig();
57+
const colors = (dataColors ?? config.chart.defaultDataColors).map(
58+
(colorName) => allColors[colorName]
59+
);
60+
61+
return (
62+
<ResponsiveContainer
63+
className={bodyRecipe({ size: "medium", weight: "default", color: "default" })}
64+
width={width}
65+
height={height}
66+
minWidth={minWidth}
67+
minHeight={minHeight}
68+
maxHeight={maxHeight}
69+
aspect={aspect}
70+
debounce={debounce}
71+
>
72+
<RechartLineChart data={data}>
73+
{categories.map((category, i) => (
74+
<Line
75+
type={lineType}
76+
key={category}
77+
dataKey={category}
78+
isAnimationActive={!disableAnimation}
79+
stroke={colors[i % colors.length]}
80+
strokeWidth={2}
81+
dot={false}
82+
/>
83+
))}
84+
{!hideXAxis && <XAxis dataKey={dataKey} />}
85+
{!hideYAxis && <YAxis />}
86+
{!hideTooltip && <Tooltip content={tooltipContent} />}
87+
{!hideLegend && <Legend content={legendContent} />}
88+
{children}
89+
</RechartLineChart>
90+
</ResponsiveContainer>
91+
);
92+
}

0 commit comments

Comments
 (0)