@voila.dev/ui-chart 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/package.json +57 -0
  2. package/src/components/chart-area.tsx +124 -0
  3. package/src/components/chart-bars.tsx +105 -0
  4. package/src/components/chart-cursor.tsx +105 -0
  5. package/src/components/chart-data-table.tsx +55 -0
  6. package/src/components/chart-empty.tsx +22 -0
  7. package/src/components/chart-grid.tsx +81 -0
  8. package/src/components/chart-label-list.tsx +178 -0
  9. package/src/components/chart-legend.tsx +106 -0
  10. package/src/components/chart-line.tsx +100 -0
  11. package/src/components/chart-pie.tsx +130 -0
  12. package/src/components/chart-points.tsx +94 -0
  13. package/src/components/chart-polar-angle-axis.tsx +76 -0
  14. package/src/components/chart-polar-grid.tsx +64 -0
  15. package/src/components/chart-radar.tsx +103 -0
  16. package/src/components/chart-radial-bar.tsx +122 -0
  17. package/src/components/chart-reference-line.tsx +60 -0
  18. package/src/components/chart-root.tsx +188 -0
  19. package/src/components/chart-skeleton.tsx +45 -0
  20. package/src/components/chart-slice.tsx +59 -0
  21. package/src/components/chart-style.tsx +78 -0
  22. package/src/components/chart-tooltip.tsx +205 -0
  23. package/src/components/chart-x-axis.tsx +94 -0
  24. package/src/components/chart-y-axis.tsx +90 -0
  25. package/src/components/chart.tsx +146 -0
  26. package/src/context/chart-context.tsx +70 -0
  27. package/src/core/axis.ts +53 -0
  28. package/src/core/bars.ts +160 -0
  29. package/src/core/chart-model.ts +156 -0
  30. package/src/core/config.ts +68 -0
  31. package/src/core/format.ts +86 -0
  32. package/src/core/geometry.ts +271 -0
  33. package/src/core/polar.ts +138 -0
  34. package/src/core/scales.ts +167 -0
  35. package/src/core/series.ts +70 -0
  36. package/src/core/ticks.ts +118 -0
  37. package/src/core/types.ts +71 -0
  38. package/src/hooks/use-chart-dimensions.ts +50 -0
  39. package/src/hooks/use-chart-pointer.ts +152 -0
  40. package/src/styles.css +10 -0
@@ -0,0 +1,64 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+ import type * as React from "react";
3
+
4
+ import { useChartContext } from "#/context/chart-context.tsx";
5
+ import { polygonPath } from "#/core/geometry.ts";
6
+ import { polarFrame, ringPoints, ringRadii } from "#/core/polar.ts";
7
+
8
+ /**
9
+ * The web behind a radar chart: rings at even value steps plus one spoke per
10
+ * axis. The rings are polygons, not circles, so they meet the spokes exactly
11
+ * where the data will.
12
+ */
13
+
14
+ export interface ChartPolarGridProps extends React.ComponentProps<"g"> {
15
+ readonly rings?: number;
16
+ readonly spokes?: boolean;
17
+ readonly inset?: number;
18
+ }
19
+
20
+ function ChartPolarGrid({
21
+ className,
22
+ rings = 4,
23
+ spokes = true,
24
+ inset = 24,
25
+ ...props
26
+ }: ChartPolarGridProps) {
27
+ const { innerWidth, innerHeight, categories } = useChartContext();
28
+ const { cx, cy, radius } = polarFrame({ innerWidth, innerHeight, inset });
29
+ const axisCount = categories.length;
30
+
31
+ if (axisCount === 0 || radius <= 0) {
32
+ return null;
33
+ }
34
+
35
+ return (
36
+ <g
37
+ data-slot="chart-polar-grid"
38
+ className={cn("fill-none stroke-border/60", className)}
39
+ {...props}
40
+ >
41
+ {ringRadii(radius, rings).map((ringRadius) => (
42
+ <path
43
+ key={ringRadius}
44
+ data-slot="chart-polar-ring"
45
+ d={polygonPath(ringPoints(cx, cy, ringRadius, axisCount))}
46
+ />
47
+ ))}
48
+ {spokes
49
+ ? ringPoints(cx, cy, radius, axisCount).map((point, index) => (
50
+ <line
51
+ key={categories[index] ?? index}
52
+ data-slot="chart-polar-spoke"
53
+ x1={cx}
54
+ y1={cy}
55
+ x2={point.x}
56
+ y2={point.y}
57
+ />
58
+ ))
59
+ : null}
60
+ </g>
61
+ );
62
+ }
63
+
64
+ export { ChartPolarGrid };
@@ -0,0 +1,103 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+ import type * as React from "react";
3
+
4
+ import { useChartContext } from "#/context/chart-context.tsx";
5
+ import { readNumber } from "#/core/chart-model.ts";
6
+ import { seriesColor } from "#/core/config.ts";
7
+ import { polygonPath } from "#/core/geometry.ts";
8
+ import { axisAngle, polarFrame, polarToCartesian } from "#/core/polar.ts";
9
+
10
+ /**
11
+ * One closed shape per series, with a vertex on every category axis. Radii come
12
+ * from the value axis's domain, so two radar series on the same chart are
13
+ * directly comparable.
14
+ */
15
+
16
+ export interface ChartRadarProps extends React.ComponentProps<"g"> {
17
+ readonly keys?: ReadonlyArray<string>;
18
+ readonly fillOpacity?: number;
19
+ readonly strokeWidth?: number;
20
+ readonly dots?: boolean;
21
+ readonly inset?: number;
22
+ }
23
+
24
+ const DOT_RADIUS = 3;
25
+
26
+ function ChartRadar({
27
+ className,
28
+ keys,
29
+ fillOpacity = 0.35,
30
+ strokeWidth = 2,
31
+ dots = false,
32
+ inset = 24,
33
+ ...props
34
+ }: ChartRadarProps) {
35
+ const {
36
+ data,
37
+ innerWidth,
38
+ innerHeight,
39
+ categories,
40
+ valueKeys,
41
+ valueScale,
42
+ config,
43
+ } = useChartContext();
44
+
45
+ const drawnKeys = keys ?? valueKeys;
46
+ const { cx, cy, radius } = polarFrame({ innerWidth, innerHeight, inset });
47
+ const [domainStart, domainEnd] = valueScale.domain;
48
+ const span = domainEnd - domainStart;
49
+ const count = categories.length;
50
+
51
+ if (count === 0 || radius <= 0) {
52
+ return null;
53
+ }
54
+
55
+ const radiusFor = (value: number): number =>
56
+ span === 0 ? 0 : Math.max(0, ((value - domainStart) / span) * radius);
57
+
58
+ return (
59
+ <g data-slot="chart-radar" className={cn(className)} {...props}>
60
+ {drawnKeys.map((key, seriesIndex) => {
61
+ const color = seriesColor(config, key, seriesIndex);
62
+ const vertices = data.map((datum, index) =>
63
+ polarToCartesian(
64
+ cx,
65
+ cy,
66
+ radiusFor(readNumber(datum, key)),
67
+ axisAngle(index, count),
68
+ ),
69
+ );
70
+ return (
71
+ <g key={key} data-slot="chart-radar-series" data-series={key}>
72
+ <path
73
+ data-slot="chart-radar-shape"
74
+ data-chart-animate=""
75
+ d={polygonPath(vertices)}
76
+ fill={color}
77
+ fillOpacity={fillOpacity}
78
+ stroke={color}
79
+ strokeWidth={strokeWidth}
80
+ strokeLinejoin="round"
81
+ />
82
+ {dots
83
+ ? vertices.map((vertex, index) => (
84
+ <circle
85
+ key={categories[index] ?? index}
86
+ data-slot="chart-dot"
87
+ data-series={key}
88
+ data-index={index}
89
+ cx={vertex.x}
90
+ cy={vertex.y}
91
+ r={DOT_RADIUS}
92
+ fill={color}
93
+ />
94
+ ))
95
+ : null}
96
+ </g>
97
+ );
98
+ })}
99
+ </g>
100
+ );
101
+ }
102
+
103
+ export { ChartRadar };
@@ -0,0 +1,122 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+ import type * as React from "react";
3
+
4
+ import { useChartContext } from "#/context/chart-context.tsx";
5
+ import { readNumber } from "#/core/chart-model.ts";
6
+ import { configKeyFor, seriesColor } from "#/core/config.ts";
7
+ import { arcPath } from "#/core/geometry.ts";
8
+ import { polarFrame } from "#/core/polar.ts";
9
+
10
+ /**
11
+ * Bars bent around the centre — one concentric track per row. Reads as a set of
12
+ * gauges, which is what it is usually asked to be: a completion rate, a fill
13
+ * rate, a share of a target.
14
+ */
15
+
16
+ export interface ChartRadialBarProps extends React.ComponentProps<"g"> {
17
+ readonly dataKey?: string;
18
+ readonly nameKey?: string;
19
+ /** Value that fills a whole track. Defaults to the value axis maximum. */
20
+ readonly max?: number;
21
+ readonly startAngle?: number;
22
+ readonly endAngle?: number;
23
+ /** Fraction of the radius left hollow in the middle. */
24
+ readonly innerRadiusRatio?: number;
25
+ /** Draw the unfilled remainder of each track. */
26
+ readonly background?: boolean;
27
+ readonly inset?: number;
28
+ /** Pixels between two tracks. */
29
+ readonly gap?: number;
30
+ }
31
+
32
+ function ChartRadialBar({
33
+ className,
34
+ dataKey,
35
+ nameKey,
36
+ max,
37
+ startAngle = 0,
38
+ endAngle = 360,
39
+ innerRadiusRatio = 0.3,
40
+ background = true,
41
+ inset = 8,
42
+ gap = 4,
43
+ ...props
44
+ }: ChartRadialBarProps) {
45
+ const {
46
+ data,
47
+ innerWidth,
48
+ innerHeight,
49
+ valueKeys,
50
+ valueScale,
51
+ config,
52
+ category,
53
+ categories,
54
+ active,
55
+ setActive,
56
+ } = useChartContext();
57
+
58
+ const key = dataKey ?? valueKeys[0];
59
+ if (key === undefined || data.length === 0) {
60
+ return null;
61
+ }
62
+
63
+ const { cx, cy, radius } = polarFrame({ innerWidth, innerHeight, inset });
64
+ const innerRadius = radius * innerRadiusRatio;
65
+ const trackDepth = (radius - innerRadius) / data.length;
66
+ const ceiling = max ?? valueScale.domain[1];
67
+ const span = endAngle - startAngle;
68
+
69
+ return (
70
+ <g data-slot="chart-radial-bar" className={cn(className)} {...props}>
71
+ {data.map((datum, index) => {
72
+ // Track zero is the outermost, so the first row of data reads first.
73
+ const trackOuter = radius - index * trackDepth;
74
+ const trackInner = trackOuter - trackDepth + gap;
75
+ const fraction =
76
+ ceiling === 0 ? 0 : Math.min(1, readNumber(datum, key) / ceiling);
77
+ const configKey = configKeyFor(
78
+ datum,
79
+ categories[index] ?? String(index),
80
+ nameKey ?? category?.key,
81
+ );
82
+ const track = {
83
+ cx,
84
+ cy,
85
+ innerRadius: trackInner,
86
+ outerRadius: trackOuter,
87
+ };
88
+ return (
89
+ <g
90
+ key={configKey}
91
+ data-slot="chart-radial-track"
92
+ data-series={configKey}
93
+ data-index={index}
94
+ data-state={active?.index === index ? "active" : "idle"}
95
+ onPointerEnter={() => setActive({ index, x: cx, y: cy })}
96
+ onPointerLeave={() => setActive(null)}
97
+ >
98
+ {background ? (
99
+ <path
100
+ data-slot="chart-radial-background"
101
+ d={arcPath({ ...track, startAngle, endAngle })}
102
+ className="fill-muted"
103
+ />
104
+ ) : null}
105
+ <path
106
+ data-slot="chart-radial-value"
107
+ data-chart-animate=""
108
+ d={arcPath({
109
+ ...track,
110
+ startAngle,
111
+ endAngle: startAngle + span * fraction,
112
+ })}
113
+ fill={seriesColor(config, configKey, index)}
114
+ />
115
+ </g>
116
+ );
117
+ })}
118
+ </g>
119
+ );
120
+ }
121
+
122
+ export { ChartRadialBar };
@@ -0,0 +1,60 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+ import type * as React from "react";
3
+
4
+ import { useChartContext } from "#/context/chart-context.tsx";
5
+ import { formatTickValue } from "#/core/format.ts";
6
+
7
+ /**
8
+ * A marker line across the plot at one value — a target, a threshold, or the
9
+ * zero baseline a chart with negative values needs in order to be readable.
10
+ */
11
+
12
+ export interface ChartReferenceLineProps extends React.ComponentProps<"g"> {
13
+ /** Position on the value axis. */
14
+ readonly value: number;
15
+ readonly label?: string;
16
+ readonly strokeDasharray?: string;
17
+ }
18
+
19
+ function ChartReferenceLine({
20
+ className,
21
+ value,
22
+ label,
23
+ strokeDasharray = "4 4",
24
+ ...props
25
+ }: ChartReferenceLineProps) {
26
+ const { valueScale, orientation, innerWidth, innerHeight } =
27
+ useChartContext();
28
+ const offset = valueScale.scale(value);
29
+ const isVertical = orientation === "vertical";
30
+
31
+ return (
32
+ <g
33
+ data-slot="chart-reference-line"
34
+ data-value={value}
35
+ className={cn("stroke-border", className)}
36
+ {...props}
37
+ >
38
+ <line
39
+ x1={isVertical ? 0 : offset}
40
+ x2={isVertical ? innerWidth : offset}
41
+ y1={isVertical ? offset : 0}
42
+ y2={isVertical ? offset : innerHeight}
43
+ strokeDasharray={strokeDasharray}
44
+ />
45
+ <text
46
+ data-slot="chart-reference-label"
47
+ x={isVertical ? innerWidth : offset}
48
+ y={isVertical ? offset : 0}
49
+ dx={isVertical ? -4 : 4}
50
+ dy={isVertical ? -4 : 10}
51
+ textAnchor={isVertical ? "end" : "start"}
52
+ className="fill-muted-foreground stroke-none text-[10px]"
53
+ >
54
+ {label ?? formatTickValue(value)}
55
+ </text>
56
+ </g>
57
+ );
58
+ }
59
+
60
+ export { ChartReferenceLine };
@@ -0,0 +1,188 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+ import * as React from "react";
3
+
4
+ import { ChartDataTable } from "#/components/chart-data-table.tsx";
5
+ import { ChartStyle } from "#/components/chart-style.tsx";
6
+ import {
7
+ type ChartContextValue,
8
+ ChartProvider,
9
+ } from "#/context/chart-context.tsx";
10
+ import {
11
+ buildChartModel,
12
+ type ChartCategorySpec,
13
+ type ChartValueSpec,
14
+ } from "#/core/chart-model.ts";
15
+ import { type ChartConfig, seriesLabelText } from "#/core/config.ts";
16
+ import type {
17
+ ChartDatum,
18
+ ChartMargin,
19
+ ChartOrientation,
20
+ } from "#/core/types.ts";
21
+ import { useChartDimensions } from "#/hooks/use-chart-dimensions.ts";
22
+ import { useChartPointer } from "#/hooks/use-chart-pointer.ts";
23
+
24
+ /**
25
+ * The frame every other chart part draws into: it measures itself, derives the
26
+ * scales, owns the active datum and publishes all of it on context.
27
+ *
28
+ * Marks are placed inside the plotting area, which is the SVG inset by
29
+ * `margin`. Chrome that is not SVG — tooltip, legend — portals into an overlay
30
+ * layer above it, so composition stays flat at the call site.
31
+ */
32
+
33
+ const DEFAULT_MARGIN: ChartMargin = {
34
+ top: 8,
35
+ right: 8,
36
+ bottom: 24,
37
+ left: 40,
38
+ };
39
+
40
+ export interface ChartRootProps
41
+ extends Omit<React.ComponentProps<"div">, "children"> {
42
+ /** Per-series labels, colours and icons. */
43
+ readonly config: ChartConfig;
44
+ readonly data?: ReadonlyArray<ChartDatum>;
45
+ /** The category axis: which field names each row. */
46
+ readonly x?: ChartCategorySpec;
47
+ /** The value axis: which fields are plotted, and how they combine. */
48
+ readonly y?: ChartValueSpec;
49
+ /** `horizontal` puts the categories down the side. */
50
+ readonly orientation?: ChartOrientation;
51
+ readonly margin?: Partial<ChartMargin>;
52
+ /** Turns pointer scrubbing and keyboard navigation off. */
53
+ readonly interactive?: boolean;
54
+ readonly children?: React.ReactNode;
55
+ }
56
+
57
+ /**
58
+ * A one-line description of the picture, for readers who get the `role="img"`
59
+ * label but not the drawing. The hidden data table underneath carries the
60
+ * numbers themselves.
61
+ */
62
+ function describeChart(
63
+ config: ChartConfig,
64
+ valueKeys: ReadonlyArray<string>,
65
+ count: number,
66
+ ): string {
67
+ if (valueKeys.length === 0) {
68
+ return "Chart";
69
+ }
70
+ const names = valueKeys.map((key) => seriesLabelText(config, key)).join(", ");
71
+ return `Chart of ${names} over ${count} ${count === 1 ? "point" : "points"}`;
72
+ }
73
+
74
+ function ChartRoot({
75
+ id,
76
+ className,
77
+ config,
78
+ data = [],
79
+ x,
80
+ y,
81
+ orientation = "vertical",
82
+ margin: marginProp,
83
+ interactive = true,
84
+ children,
85
+ "aria-label": ariaLabel,
86
+ ...props
87
+ }: ChartRootProps) {
88
+ const generatedId = React.useId();
89
+ const chartId = `chart-${id ?? generatedId.replace(/:/g, "")}`;
90
+ const { ref, width, height } = useChartDimensions();
91
+ const [overlay, setOverlay] = React.useState<HTMLDivElement | null>(null);
92
+
93
+ const margin: ChartMargin = { ...DEFAULT_MARGIN, ...marginProp };
94
+ const innerWidth = Math.max(0, width - margin.left - margin.right);
95
+ const innerHeight = Math.max(0, height - margin.top - margin.bottom);
96
+
97
+ const model = React.useMemo(
98
+ () =>
99
+ buildChartModel({
100
+ data,
101
+ category: x,
102
+ value: y,
103
+ orientation,
104
+ innerWidth,
105
+ innerHeight,
106
+ }),
107
+ [data, x, y, orientation, innerWidth, innerHeight],
108
+ );
109
+
110
+ const { active, setActive, handlers } = useChartPointer({
111
+ enabled: interactive && x !== undefined,
112
+ count: data.length,
113
+ width,
114
+ height,
115
+ margin,
116
+ orientation,
117
+ categoryScale: model.categoryScale,
118
+ });
119
+
120
+ const description =
121
+ ariaLabel ?? describeChart(config, model.valueKeys, data.length);
122
+
123
+ const context: ChartContextValue = {
124
+ chartId,
125
+ config,
126
+ data,
127
+ width,
128
+ height,
129
+ margin,
130
+ innerWidth,
131
+ innerHeight,
132
+ orientation,
133
+ category: x,
134
+ value: y,
135
+ active,
136
+ setActive,
137
+ overlay,
138
+ ...model,
139
+ };
140
+
141
+ return (
142
+ <div
143
+ data-slot="chart-root"
144
+ data-chart={chartId}
145
+ data-orientation={orientation}
146
+ ref={ref}
147
+ className={cn(
148
+ "relative flex aspect-video w-full justify-center text-xs",
149
+ className,
150
+ )}
151
+ {...props}
152
+ >
153
+ <ChartStyle id={chartId} config={config} />
154
+ <svg
155
+ data-slot="chart-svg"
156
+ role="img"
157
+ aria-label={description}
158
+ width={width}
159
+ height={height}
160
+ viewBox={`0 0 ${width} ${height}`}
161
+ className="absolute inset-0 h-full w-full rounded-md outline-none focus-visible:ring-2 focus-visible:ring-ring"
162
+ {...handlers}
163
+ >
164
+ <g
165
+ data-slot="chart-plot"
166
+ transform={`translate(${margin.left},${margin.top})`}
167
+ >
168
+ <ChartProvider value={context}>{children}</ChartProvider>
169
+ </g>
170
+ </svg>
171
+ <div
172
+ data-slot="chart-overlay"
173
+ ref={setOverlay}
174
+ className="pointer-events-none absolute inset-0"
175
+ />
176
+ <ChartDataTable
177
+ caption={description}
178
+ categoryLabel={x?.key ?? "Category"}
179
+ categories={model.categories}
180
+ valueKeys={model.valueKeys}
181
+ data={data}
182
+ config={config}
183
+ />
184
+ </div>
185
+ );
186
+ }
187
+
188
+ export { ChartRoot };
@@ -0,0 +1,45 @@
1
+ import { Skeleton } from "@voila.dev/ui/components/skeleton";
2
+ import { cn } from "@voila.dev/ui/lib/utils";
3
+ import type * as React from "react";
4
+
5
+ /**
6
+ * Loading placeholder shaped like a bar chart. Defaults to the root's
7
+ * aspect-video box — size it like the chart it stands in for (the same
8
+ * `h-* w-full`), or the page will jump when the data lands.
9
+ *
10
+ * The English default label matches the kit's other built-in labels; pass a
11
+ * localized `label` from the app.
12
+ */
13
+
14
+ /** Fixed, not random, so the server and the client render the same bars. */
15
+ const BAR_HEIGHTS = [40, 70, 55, 90, 65, 80, 50] as const;
16
+
17
+ export interface ChartSkeletonProps extends React.ComponentProps<"div"> {
18
+ readonly label?: string;
19
+ }
20
+
21
+ function ChartSkeleton({
22
+ className,
23
+ label = "Loading",
24
+ ...props
25
+ }: ChartSkeletonProps) {
26
+ return (
27
+ <div
28
+ data-slot="chart-skeleton"
29
+ role="status"
30
+ aria-label={label}
31
+ className={cn("flex aspect-video w-full items-end gap-2", className)}
32
+ {...props}
33
+ >
34
+ {BAR_HEIGHTS.map((barHeight) => (
35
+ <Skeleton
36
+ key={barHeight}
37
+ className="w-full"
38
+ style={{ height: `${barHeight}%` }}
39
+ />
40
+ ))}
41
+ </div>
42
+ );
43
+ }
44
+
45
+ export { ChartSkeleton };
@@ -0,0 +1,59 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+ import type * as React from "react";
3
+
4
+ import { type ArcOptions, arcPath } from "#/core/geometry.ts";
5
+
6
+ /**
7
+ * One wedge of a pie or donut. Exposed on its own so a caller can build a
8
+ * bespoke round chart out of the same part the built-in ones use.
9
+ */
10
+
11
+ export interface ChartSliceProps
12
+ extends ArcOptions,
13
+ Omit<React.ComponentProps<"path">, "d" | "cx" | "cy"> {
14
+ readonly state?: "idle" | "active" | "muted";
15
+ }
16
+
17
+ /** How far the active wedge lifts out of the ring. */
18
+ const ACTIVE_LIFT = 4;
19
+
20
+ function ChartSlice({
21
+ cx,
22
+ cy,
23
+ innerRadius,
24
+ outerRadius,
25
+ startAngle,
26
+ endAngle,
27
+ state = "idle",
28
+ className,
29
+ ...props
30
+ }: ChartSliceProps) {
31
+ const path = arcPath({
32
+ cx,
33
+ cy,
34
+ innerRadius,
35
+ outerRadius: state === "active" ? outerRadius + ACTIVE_LIFT : outerRadius,
36
+ startAngle,
37
+ endAngle,
38
+ });
39
+ if (path === "") {
40
+ return null;
41
+ }
42
+
43
+ return (
44
+ <path
45
+ data-slot="chart-slice"
46
+ data-state={state}
47
+ data-chart-animate=""
48
+ d={path}
49
+ className={cn(
50
+ "stroke-background transition-opacity duration-150 data-[state=muted]:opacity-60",
51
+ className,
52
+ )}
53
+ strokeWidth={2}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ export { ChartSlice };