@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,78 @@
1
+ import type { ChartConfig } from "#/core/config.ts";
2
+
3
+ /**
4
+ * Per-chart CSS: the `--color-<key>` variable each configured series is drawn
5
+ * with, and the entrance animation its marks fade in on.
6
+ *
7
+ * The variables live in a stylesheet rather than in an inline `style` so a
8
+ * `theme` pair can serve light and dark from the same markup — no re-render on
9
+ * theme change, no reading the document during render.
10
+ */
11
+
12
+ /** Where each theme's variables apply. Light is the unprefixed default. */
13
+ const THEME_SELECTORS = { light: "", dark: ".dark" } as const;
14
+
15
+ /** Guards interpolated colours: variables, functions, hex and keywords only. */
16
+ const CSS_VALUE_PATTERN = /^[\w(),.%#\s-]+$/;
17
+
18
+ function colorDeclarations(
19
+ config: ChartConfig,
20
+ theme: "light" | "dark",
21
+ ): string {
22
+ return Object.entries(config)
23
+ .map(([key, item]) => {
24
+ const color = item.theme?.[theme] ?? item.color;
25
+ return color !== undefined && CSS_VALUE_PATTERN.test(color)
26
+ ? ` --color-${key}: ${color};`
27
+ : null;
28
+ })
29
+ .filter((declaration) => declaration !== null)
30
+ .join("\n");
31
+ }
32
+
33
+ /**
34
+ * Marks opt in with `data-chart-animate`. Gated behind
35
+ * `prefers-reduced-motion: no-preference`, so a reader who has asked for less
36
+ * motion gets the finished chart immediately instead of a faster animation.
37
+ */
38
+ const ENTER_ANIMATION = `
39
+ @keyframes voila-chart-enter {
40
+ from { opacity: 0; }
41
+ to { opacity: 1; }
42
+ }
43
+ @media (prefers-reduced-motion: no-preference) {
44
+ [data-slot="chart-root"] [data-chart-animate] {
45
+ animation: voila-chart-enter 320ms cubic-bezier(0.4, 0, 0.2, 1) both;
46
+ animation-delay: var(--chart-enter-delay, 0ms);
47
+ }
48
+ }`;
49
+
50
+ export function ChartStyle({
51
+ id,
52
+ config,
53
+ }: {
54
+ readonly id: string;
55
+ readonly config: ChartConfig;
56
+ }) {
57
+ const scoped = Object.entries(THEME_SELECTORS)
58
+ .map(([theme, prefix]) => {
59
+ const declarations = colorDeclarations(
60
+ config,
61
+ theme as keyof typeof THEME_SELECTORS,
62
+ );
63
+ return declarations === ""
64
+ ? ""
65
+ : `${prefix} [data-chart=${id}] {\n${declarations}\n}`;
66
+ })
67
+ .filter((block) => block !== "")
68
+ .join("\n");
69
+
70
+ // A <style> element has no other API. Everything interpolated is either a
71
+ // generated id or a colour that passed CSS_VALUE_PATTERN.
72
+ return (
73
+ <style
74
+ data-slot="chart-style"
75
+ dangerouslySetInnerHTML={{ __html: `${scoped}\n${ENTER_ANIMATION}` }}
76
+ />
77
+ );
78
+ }
@@ -0,0 +1,205 @@
1
+ import { cn } from "@voila.dev/ui/lib/utils";
2
+ import type * as React from "react";
3
+ import { createPortal } from "react-dom";
4
+
5
+ import { useChartContext } from "#/context/chart-context.tsx";
6
+ import { readNumber } from "#/core/chart-model.ts";
7
+ import { configKeyFor, seriesColor } from "#/core/config.ts";
8
+ import { formatTickValue } from "#/core/format.ts";
9
+
10
+ /**
11
+ * The readout for the active datum. `ChartTooltip` places it; the content is a
12
+ * separate component so a caller can replace the body without reimplementing
13
+ * the positioning, which is the fiddly half.
14
+ *
15
+ * It is announced politely rather than assertively: scrubbing across a chart
16
+ * fires a lot of updates, and interrupting the reader on every one of them
17
+ * would be worse than silence.
18
+ */
19
+
20
+ export type ChartTooltipIndicator = "dot" | "line" | "dashed";
21
+
22
+ export interface ChartTooltipProps {
23
+ /** Replaces the default body. */
24
+ readonly content?: React.ReactNode;
25
+ /** Pixels between the pointer and the panel. */
26
+ readonly offset?: number;
27
+ readonly className?: string;
28
+ }
29
+
30
+ /** Room the panel needs above the pointer before it stops flipping upwards. */
31
+ const PANEL_HEIGHT = 88;
32
+ /** Nominal half-width, used to keep the panel's edges on screen. */
33
+ const HALF_PANEL = 72;
34
+
35
+ /** Keeps the panel inside the given bounds rather than letting it hang off. */
36
+ function clamp(value: number, low: number, high: number): number {
37
+ return Math.min(Math.max(value, low), high);
38
+ }
39
+
40
+ function ChartTooltip({ content, offset = 12, className }: ChartTooltipProps) {
41
+ const { active, chartId, margin, overlay } = useChartContext();
42
+
43
+ if (active === null || overlay === null) {
44
+ return null;
45
+ }
46
+
47
+ // Positioned against the viewport and portalled to the document, not to the
48
+ // chart's own overlay: a sparkline inside a card is both tiny and clipped by
49
+ // `overflow-hidden`, and a readout the card cuts in half is worse than none.
50
+ const box = overlay.getBoundingClientRect();
51
+ const view = overlay.ownerDocument.defaultView;
52
+ const viewportWidth = view?.innerWidth ?? box.right;
53
+ const viewportHeight = view?.innerHeight ?? box.bottom;
54
+
55
+ const anchorX = box.left + margin.left + active.x;
56
+ const anchorY = box.top + margin.top + active.y;
57
+
58
+ // The panel sits above the reader's finger, where it does not hide what is
59
+ // being pointed at — unless the top of the window is in the way.
60
+ const above = anchorY - PANEL_HEIGHT > 0;
61
+ const left = clamp(anchorX, HALF_PANEL, viewportWidth - HALF_PANEL);
62
+ const top = clamp(anchorY + (above ? -offset : offset), 0, viewportHeight);
63
+
64
+ return createPortal(
65
+ <div
66
+ data-slot="chart-tooltip"
67
+ // The `--color-<key>` variables are scoped to `[data-chart=<id>]`, and
68
+ // the panel no longer lives inside the chart. Carrying the id onto it
69
+ // keeps the series colours resolvable from the document body.
70
+ data-chart={chartId}
71
+ data-placement={above ? "above" : "below"}
72
+ className={cn(
73
+ "pointer-events-none fixed z-50 -translate-x-1/2",
74
+ above && "-translate-y-full",
75
+ className,
76
+ )}
77
+ style={{ left, top }}
78
+ >
79
+ {content ?? <ChartTooltipContent />}
80
+ </div>,
81
+ overlay.ownerDocument.body,
82
+ );
83
+ }
84
+
85
+ export interface ChartTooltipContentProps extends React.ComponentProps<"div"> {
86
+ readonly hideLabel?: boolean;
87
+ readonly hideIndicator?: boolean;
88
+ readonly indicator?: ChartTooltipIndicator;
89
+ /** Field naming the config entry, for charts coloured per row. */
90
+ readonly nameKey?: string;
91
+ /** Field holding the panel's heading. Defaults to the category. */
92
+ readonly labelKey?: string;
93
+ readonly labelClassName?: string;
94
+ readonly formatter?: (
95
+ value: number,
96
+ name: React.ReactNode,
97
+ configKey: string,
98
+ ) => React.ReactNode;
99
+ readonly labelFormatter?: (label: string) => React.ReactNode;
100
+ }
101
+
102
+ const indicatorClassNames: Record<ChartTooltipIndicator, string> = {
103
+ dot: "h-2.5 w-2.5 rounded-[2px]",
104
+ line: "h-2.5 w-1 rounded-[2px]",
105
+ dashed: "h-2.5 w-0 border-[1.5px] border-dashed bg-transparent",
106
+ };
107
+
108
+ function ChartTooltipMarker({
109
+ indicator,
110
+ color,
111
+ }: {
112
+ readonly indicator: ChartTooltipIndicator;
113
+ readonly color: string;
114
+ }) {
115
+ return (
116
+ <div
117
+ data-slot="chart-tooltip-marker"
118
+ className={cn("shrink-0", indicatorClassNames[indicator])}
119
+ style={{ backgroundColor: color, borderColor: color }}
120
+ />
121
+ );
122
+ }
123
+
124
+ function ChartTooltipContent({
125
+ className,
126
+ hideLabel = false,
127
+ hideIndicator = false,
128
+ indicator = "dot",
129
+ nameKey,
130
+ labelKey,
131
+ labelClassName,
132
+ formatter,
133
+ labelFormatter,
134
+ ...props
135
+ }: ChartTooltipContentProps) {
136
+ const { active, data, categories, valueKeys, config } = useChartContext();
137
+
138
+ if (active === null) {
139
+ return null;
140
+ }
141
+ const datum = data[active.index];
142
+ if (datum === undefined) {
143
+ return null;
144
+ }
145
+
146
+ const rawLabel =
147
+ labelKey === undefined
148
+ ? (categories[active.index] ?? "")
149
+ : String(datum[labelKey] ?? "");
150
+ const label = labelFormatter ? labelFormatter(rawLabel) : rawLabel;
151
+
152
+ return (
153
+ <div
154
+ data-slot="chart-tooltip-content"
155
+ role="status"
156
+ aria-live="polite"
157
+ className={cn(
158
+ "grid min-w-32 items-start gap-1.5 rounded-lg bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-md ring-1 ring-foreground/10",
159
+ className,
160
+ )}
161
+ {...props}
162
+ >
163
+ {hideLabel ? null : (
164
+ <div className={cn("font-medium", labelClassName)}>{label}</div>
165
+ )}
166
+ <div className="grid gap-1.5">
167
+ {valueKeys.map((key, index) => {
168
+ const configKey = configKeyFor(datum, key, nameKey);
169
+ const name = config[configKey]?.label ?? configKey;
170
+ const value = readNumber(datum, key);
171
+ const Icon = config[configKey]?.icon;
172
+ return (
173
+ <div
174
+ key={key}
175
+ data-slot="chart-tooltip-row"
176
+ data-series={key}
177
+ className="flex w-full items-center gap-2 leading-none"
178
+ >
179
+ {hideIndicator ? null : Icon ? (
180
+ <Icon />
181
+ ) : (
182
+ <ChartTooltipMarker
183
+ indicator={indicator}
184
+ color={seriesColor(config, configKey, index)}
185
+ />
186
+ )}
187
+ {formatter ? (
188
+ formatter(value, name, configKey)
189
+ ) : (
190
+ <div className="flex flex-1 items-center justify-between gap-2">
191
+ <span className="text-muted-foreground">{name}</span>
192
+ <span className="font-medium font-mono text-foreground tabular-nums">
193
+ {formatTickValue(value)}
194
+ </span>
195
+ </div>
196
+ )}
197
+ </div>
198
+ );
199
+ })}
200
+ </div>
201
+ </div>
202
+ );
203
+ }
204
+
205
+ export { ChartTooltip, ChartTooltipContent };
@@ -0,0 +1,94 @@
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 { axisTicks } from "#/core/axis.ts";
6
+ import { formatLabel } from "#/core/format.ts";
7
+
8
+ /** The bottom axis: categories on a vertical chart, values on a horizontal one. */
9
+
10
+ export interface ChartXAxisProps extends React.ComponentProps<"g"> {
11
+ readonly tickLine?: boolean;
12
+ readonly axisLine?: boolean;
13
+ readonly tickCount?: number;
14
+ /** Minimum pixels between two labels before ticks start being dropped. */
15
+ readonly minTickGap?: number;
16
+ /** Gap between the axis and its labels. */
17
+ readonly tickMargin?: number;
18
+ readonly tickFormatter?: (value: number | string) => string;
19
+ /** Keeps the scale but draws nothing — useful behind a bar's own labels. */
20
+ readonly hide?: boolean;
21
+ }
22
+
23
+ const TICK_LINE_LENGTH = 4;
24
+
25
+ function ChartXAxis({
26
+ className,
27
+ tickLine = false,
28
+ axisLine = false,
29
+ tickCount = 5,
30
+ minTickGap = 8,
31
+ tickMargin = 8,
32
+ tickFormatter,
33
+ hide = false,
34
+ ...props
35
+ }: ChartXAxisProps) {
36
+ const { xScale, innerWidth, innerHeight } = useChartContext();
37
+ if (hide) {
38
+ return null;
39
+ }
40
+
41
+ const ticks = axisTicks(xScale, {
42
+ count: tickCount,
43
+ minTickGap,
44
+ available: innerWidth,
45
+ });
46
+ const format = tickFormatter ?? formatLabel;
47
+
48
+ return (
49
+ <g
50
+ data-slot="chart-x-axis"
51
+ transform={`translate(0,${innerHeight})`}
52
+ className={cn("stroke-border text-muted-foreground", className)}
53
+ {...props}
54
+ >
55
+ {axisLine ? (
56
+ <line
57
+ data-slot="chart-axis-line"
58
+ x1={0}
59
+ x2={innerWidth}
60
+ y1={0}
61
+ y2={0}
62
+ />
63
+ ) : null}
64
+ {ticks.map((tick) => (
65
+ <g
66
+ key={`${tick.value}`}
67
+ data-slot="chart-x-axis-tick"
68
+ transform={`translate(${tick.offset},0)`}
69
+ >
70
+ {tickLine ? (
71
+ <line
72
+ data-slot="chart-tick-line"
73
+ x1={0}
74
+ x2={0}
75
+ y1={0}
76
+ y2={TICK_LINE_LENGTH}
77
+ />
78
+ ) : null}
79
+ <text
80
+ data-slot="chart-tick-label"
81
+ y={tickMargin}
82
+ dy="0.71em"
83
+ textAnchor="middle"
84
+ className="fill-muted-foreground stroke-none text-[10px]"
85
+ >
86
+ {format(tick.value)}
87
+ </text>
88
+ </g>
89
+ ))}
90
+ </g>
91
+ );
92
+ }
93
+
94
+ export { ChartXAxis };
@@ -0,0 +1,90 @@
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 { axisTicks } from "#/core/axis.ts";
6
+ import { formatLabel } from "#/core/format.ts";
7
+
8
+ /** The left axis: values on a vertical chart, categories on a horizontal one. */
9
+
10
+ export interface ChartYAxisProps extends React.ComponentProps<"g"> {
11
+ readonly tickLine?: boolean;
12
+ readonly axisLine?: boolean;
13
+ readonly tickCount?: number;
14
+ readonly minTickGap?: number;
15
+ readonly tickMargin?: number;
16
+ readonly tickFormatter?: (value: number | string) => string;
17
+ readonly hide?: boolean;
18
+ }
19
+
20
+ const TICK_LINE_LENGTH = 4;
21
+
22
+ function ChartYAxis({
23
+ className,
24
+ tickLine = false,
25
+ axisLine = false,
26
+ tickCount = 5,
27
+ minTickGap = 4,
28
+ tickMargin = 8,
29
+ tickFormatter,
30
+ hide = false,
31
+ ...props
32
+ }: ChartYAxisProps) {
33
+ const { yScale, innerHeight } = useChartContext();
34
+ if (hide) {
35
+ return null;
36
+ }
37
+
38
+ const ticks = axisTicks(yScale, {
39
+ count: tickCount,
40
+ minTickGap,
41
+ available: innerHeight,
42
+ });
43
+ const format = tickFormatter ?? formatLabel;
44
+
45
+ return (
46
+ <g
47
+ data-slot="chart-y-axis"
48
+ className={cn("stroke-border text-muted-foreground", className)}
49
+ {...props}
50
+ >
51
+ {axisLine ? (
52
+ <line
53
+ data-slot="chart-axis-line"
54
+ x1={0}
55
+ x2={0}
56
+ y1={0}
57
+ y2={innerHeight}
58
+ />
59
+ ) : null}
60
+ {ticks.map((tick) => (
61
+ <g
62
+ key={`${tick.value}`}
63
+ data-slot="chart-y-axis-tick"
64
+ transform={`translate(0,${tick.offset})`}
65
+ >
66
+ {tickLine ? (
67
+ <line
68
+ data-slot="chart-tick-line"
69
+ x1={0}
70
+ x2={-TICK_LINE_LENGTH}
71
+ y1={0}
72
+ y2={0}
73
+ />
74
+ ) : null}
75
+ <text
76
+ data-slot="chart-tick-label"
77
+ x={-tickMargin}
78
+ dy="0.32em"
79
+ textAnchor="end"
80
+ className="fill-muted-foreground stroke-none text-[10px]"
81
+ >
82
+ {format(tick.value)}
83
+ </text>
84
+ </g>
85
+ ))}
86
+ </g>
87
+ );
88
+ }
89
+
90
+ export { ChartYAxis };
@@ -0,0 +1,146 @@
1
+ import { ChartArea } from "#/components/chart-area.tsx";
2
+ import { ChartBars } from "#/components/chart-bars.tsx";
3
+ import { ChartCursor } from "#/components/chart-cursor.tsx";
4
+ import { ChartEmpty } from "#/components/chart-empty.tsx";
5
+ import { ChartGrid } from "#/components/chart-grid.tsx";
6
+ import { ChartLabelList } from "#/components/chart-label-list.tsx";
7
+ import { ChartLegend, ChartLegendContent } from "#/components/chart-legend.tsx";
8
+ import { ChartLine } from "#/components/chart-line.tsx";
9
+ import { ChartDonut, ChartPie } from "#/components/chart-pie.tsx";
10
+ import { ChartPoints } from "#/components/chart-points.tsx";
11
+ import { ChartPolarAngleAxis } from "#/components/chart-polar-angle-axis.tsx";
12
+ import { ChartPolarGrid } from "#/components/chart-polar-grid.tsx";
13
+ import { ChartRadar } from "#/components/chart-radar.tsx";
14
+ import { ChartRadialBar } from "#/components/chart-radial-bar.tsx";
15
+ import { ChartReferenceLine } from "#/components/chart-reference-line.tsx";
16
+ import { ChartRoot } from "#/components/chart-root.tsx";
17
+ import { ChartSkeleton } from "#/components/chart-skeleton.tsx";
18
+ import { ChartSlice } from "#/components/chart-slice.tsx";
19
+ import { ChartStyle } from "#/components/chart-style.tsx";
20
+ import {
21
+ ChartTooltip,
22
+ ChartTooltipContent,
23
+ } from "#/components/chart-tooltip.tsx";
24
+ import { ChartXAxis } from "#/components/chart-x-axis.tsx";
25
+ import { ChartYAxis } from "#/components/chart-y-axis.tsx";
26
+ import { useChartContext } from "#/context/chart-context.tsx";
27
+
28
+ /**
29
+ * The chart kit's public surface.
30
+ *
31
+ * `Chart.Root` frames the picture and `Chart.*` draws into it:
32
+ *
33
+ * ```tsx
34
+ * <Chart.Root config={config} data={data} x={{ key: "month" }} y={{ keys: ["missions"] }}>
35
+ * <Chart.Grid />
36
+ * <Chart.XAxis />
37
+ * <Chart.Cursor />
38
+ * <Chart.Bars />
39
+ * <Chart.Tooltip />
40
+ * </Chart.Root>
41
+ * ```
42
+ *
43
+ * Every part is also a flat named export, for callers who would rather import
44
+ * `ChartBars` than reach through the namespace.
45
+ */
46
+ const Chart = {
47
+ Root: ChartRoot,
48
+ Style: ChartStyle,
49
+ Grid: ChartGrid,
50
+ XAxis: ChartXAxis,
51
+ YAxis: ChartYAxis,
52
+ Bars: ChartBars,
53
+ Line: ChartLine,
54
+ Area: ChartArea,
55
+ Points: ChartPoints,
56
+ ReferenceLine: ChartReferenceLine,
57
+ LabelList: ChartLabelList,
58
+ Pie: ChartPie,
59
+ Donut: ChartDonut,
60
+ Slice: ChartSlice,
61
+ Radar: ChartRadar,
62
+ PolarGrid: ChartPolarGrid,
63
+ PolarAngleAxis: ChartPolarAngleAxis,
64
+ RadialBar: ChartRadialBar,
65
+ Cursor: ChartCursor,
66
+ Tooltip: ChartTooltip,
67
+ TooltipContent: ChartTooltipContent,
68
+ Legend: ChartLegend,
69
+ LegendContent: ChartLegendContent,
70
+ Skeleton: ChartSkeleton,
71
+ Empty: ChartEmpty,
72
+ } as const;
73
+
74
+ /** The chart the current part is drawing into. Throws outside a `Chart.Root`. */
75
+ const useChart = useChartContext;
76
+
77
+ export type { ChartAreaProps } from "#/components/chart-area.tsx";
78
+ export type { ChartBarsProps } from "#/components/chart-bars.tsx";
79
+ export type { ChartCursorProps } from "#/components/chart-cursor.tsx";
80
+ export type { ChartGridProps } from "#/components/chart-grid.tsx";
81
+ export type { ChartLabelListProps } from "#/components/chart-label-list.tsx";
82
+ export type {
83
+ ChartLegendContentProps,
84
+ ChartLegendProps,
85
+ } from "#/components/chart-legend.tsx";
86
+ export type { ChartLineProps } from "#/components/chart-line.tsx";
87
+ export type { ChartPieProps } from "#/components/chart-pie.tsx";
88
+ export type { ChartPointsProps } from "#/components/chart-points.tsx";
89
+ export type { ChartPolarAngleAxisProps } from "#/components/chart-polar-angle-axis.tsx";
90
+ export type { ChartPolarGridProps } from "#/components/chart-polar-grid.tsx";
91
+ export type { ChartRadarProps } from "#/components/chart-radar.tsx";
92
+ export type { ChartRadialBarProps } from "#/components/chart-radial-bar.tsx";
93
+ export type { ChartReferenceLineProps } from "#/components/chart-reference-line.tsx";
94
+ export type { ChartRootProps } from "#/components/chart-root.tsx";
95
+ export type { ChartSkeletonProps } from "#/components/chart-skeleton.tsx";
96
+ export type { ChartSliceProps } from "#/components/chart-slice.tsx";
97
+ export type {
98
+ ChartTooltipContentProps,
99
+ ChartTooltipIndicator,
100
+ ChartTooltipProps,
101
+ } from "#/components/chart-tooltip.tsx";
102
+ export type { ChartXAxisProps } from "#/components/chart-x-axis.tsx";
103
+ export type { ChartYAxisProps } from "#/components/chart-y-axis.tsx";
104
+ export type { ChartContextValue } from "#/context/chart-context.tsx";
105
+ export type {
106
+ ChartCategorySpec,
107
+ ChartValueSpec,
108
+ } from "#/core/chart-model.ts";
109
+ export type { ChartConfig, ChartTheme } from "#/core/config.ts";
110
+ export type {
111
+ ChartCurve,
112
+ ChartDatum,
113
+ ChartMargin,
114
+ ChartOrientation,
115
+ } from "#/core/types.ts";
116
+ export type { ChartActive } from "#/hooks/use-chart-pointer.ts";
117
+ export {
118
+ Chart,
119
+ ChartArea,
120
+ ChartBars,
121
+ ChartCursor,
122
+ ChartDonut,
123
+ ChartEmpty,
124
+ ChartGrid,
125
+ ChartLabelList,
126
+ ChartLegend,
127
+ ChartLegendContent,
128
+ ChartLine,
129
+ ChartPie,
130
+ ChartPoints,
131
+ ChartPolarAngleAxis,
132
+ ChartPolarGrid,
133
+ ChartRadar,
134
+ ChartRadialBar,
135
+ ChartReferenceLine,
136
+ ChartRoot,
137
+ ChartSkeleton,
138
+ ChartSlice,
139
+ ChartStyle,
140
+ ChartTooltip,
141
+ ChartTooltipContent,
142
+ ChartXAxis,
143
+ ChartYAxis,
144
+ useChart,
145
+ useChartContext,
146
+ };
@@ -0,0 +1,70 @@
1
+ import * as React from "react";
2
+
3
+ import type { ChartCategorySpec, ChartValueSpec } from "#/core/chart-model.ts";
4
+ import type { ChartConfig } from "#/core/config.ts";
5
+ import type {
6
+ ChartDatum,
7
+ ChartDiscreteScale,
8
+ ChartLinearScale,
9
+ ChartMargin,
10
+ ChartOrientation,
11
+ ChartScale,
12
+ } from "#/core/types.ts";
13
+ import type { ChartActive } from "#/hooks/use-chart-pointer.ts";
14
+
15
+ export type { ChartConfig } from "#/core/config.ts";
16
+
17
+ export interface ChartContextValue {
18
+ /** Stable per-chart id. Scopes the injected `--color-<key>` variables. */
19
+ readonly chartId: string;
20
+ readonly config: ChartConfig;
21
+ readonly data: ReadonlyArray<ChartDatum>;
22
+ readonly width: number;
23
+ readonly height: number;
24
+ readonly margin: ChartMargin;
25
+ readonly innerWidth: number;
26
+ readonly innerHeight: number;
27
+ readonly orientation: ChartOrientation;
28
+ readonly category: ChartCategorySpec | undefined;
29
+ readonly value: ChartValueSpec | undefined;
30
+ readonly categories: ReadonlyArray<string>;
31
+ readonly valueKeys: ReadonlyArray<string>;
32
+ readonly categoryScale: ChartDiscreteScale;
33
+ readonly valueScale: ChartLinearScale;
34
+ readonly xScale: ChartScale;
35
+ readonly yScale: ChartScale;
36
+ readonly active: ChartActive | null;
37
+ readonly setActive: React.Dispatch<React.SetStateAction<ChartActive | null>>;
38
+ /**
39
+ * Where overlay parts (tooltip, legend) portal to. `null` until the root has
40
+ * mounted, which is exactly what keeps the server render free of them.
41
+ */
42
+ readonly overlay: HTMLDivElement | null;
43
+ }
44
+
45
+ const ChartContext = React.createContext<ChartContextValue | null>(null);
46
+
47
+ export function ChartProvider({
48
+ value,
49
+ children,
50
+ }: {
51
+ readonly value: ChartContextValue;
52
+ readonly children: React.ReactNode;
53
+ }) {
54
+ return (
55
+ <ChartContext.Provider value={value}>{children}</ChartContext.Provider>
56
+ );
57
+ }
58
+
59
+ /**
60
+ * The chart every mark is drawing into. Throws rather than returning `null`:
61
+ * a `Chart.Bars` outside a `Chart.Root` has no scales, so there is nothing
62
+ * sensible to render and a clear error beats an empty SVG.
63
+ */
64
+ export function useChartContext(): ChartContextValue {
65
+ const context = React.useContext(ChartContext);
66
+ if (context === null) {
67
+ throw new Error("Chart parts must be used within a <Chart.Root />");
68
+ }
69
+ return context;
70
+ }