@delacour/react-native-ui 0.1.0-alpha.20260925053522
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.
- package/README.md +113 -0
- package/package.json +125 -0
- package/src/components/accordion/AGENTS.md +193 -0
- package/src/components/accordion/accordion-content.tsx +125 -0
- package/src/components/accordion/accordion-description.tsx +17 -0
- package/src/components/accordion/accordion-indicator.tsx +104 -0
- package/src/components/accordion/accordion-item.tsx +78 -0
- package/src/components/accordion/accordion-title.tsx +18 -0
- package/src/components/accordion/accordion-trigger.tsx +148 -0
- package/src/components/accordion/accordion.context.tsx +153 -0
- package/src/components/accordion/accordion.tsx +253 -0
- package/src/components/accordion/accordion.types.ts +11 -0
- package/src/components/accordion/accordion.variants.test.ts +434 -0
- package/src/components/accordion/accordion.variants.ts +358 -0
- package/src/components/accordion/index.ts +45 -0
- package/src/components/badge/AGENTS.md +83 -0
- package/src/components/badge/badge-close-button.tsx +48 -0
- package/src/components/badge/badge-end-content.tsx +15 -0
- package/src/components/badge/badge-label.tsx +24 -0
- package/src/components/badge/badge-start-content.tsx +16 -0
- package/src/components/badge/badge.context.tsx +64 -0
- package/src/components/badge/badge.tsx +192 -0
- package/src/components/badge/badge.types.ts +10 -0
- package/src/components/badge/badge.variants.test.ts +269 -0
- package/src/components/badge/badge.variants.ts +238 -0
- package/src/components/badge/index.ts +17 -0
- package/src/components/bottom-sheet/AGENTS.md +242 -0
- package/src/components/bottom-sheet/bottom-sheet-background.tsx +35 -0
- package/src/components/bottom-sheet/bottom-sheet-close.tsx +60 -0
- package/src/components/bottom-sheet/bottom-sheet-container.tsx +246 -0
- package/src/components/bottom-sheet/bottom-sheet-content.tsx +66 -0
- package/src/components/bottom-sheet/bottom-sheet-description.tsx +29 -0
- package/src/components/bottom-sheet/bottom-sheet-footer.tsx +150 -0
- package/src/components/bottom-sheet/bottom-sheet-handle.tsx +47 -0
- package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +180 -0
- package/src/components/bottom-sheet/bottom-sheet-portal.tsx +72 -0
- package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +105 -0
- package/src/components/bottom-sheet/bottom-sheet-title.tsx +24 -0
- package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +73 -0
- package/src/components/bottom-sheet/bottom-sheet.context.tsx +135 -0
- package/src/components/bottom-sheet/bottom-sheet.tsx +125 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +293 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +220 -0
- package/src/components/bottom-sheet/index.ts +40 -0
- package/src/components/bottom-sheet/use-bottom-sheet-input.ts +125 -0
- package/src/components/button/AGENTS.md +217 -0
- package/src/components/button/button-end-content.tsx +15 -0
- package/src/components/button/button-group-separator.tsx +37 -0
- package/src/components/button/button-group-text.tsx +101 -0
- package/src/components/button/button-group.tsx +158 -0
- package/src/components/button/button-label.tsx +24 -0
- package/src/components/button/button-start-content.tsx +15 -0
- package/src/components/button/button.context.tsx +187 -0
- package/src/components/button/button.tsx +273 -0
- package/src/components/button/button.types.ts +10 -0
- package/src/components/button/button.variants.test.ts +811 -0
- package/src/components/button/button.variants.ts +480 -0
- package/src/components/button/index.ts +50 -0
- package/src/components/chart/AGENTS.md +210 -0
- package/src/components/chart/chart-area.tsx +93 -0
- package/src/components/chart/chart-bar.tsx +103 -0
- package/src/components/chart/chart-bars.tsx +75 -0
- package/src/components/chart/chart-candlestick.tsx +60 -0
- package/src/components/chart/chart-grid.tsx +33 -0
- package/src/components/chart/chart-legend.tsx +41 -0
- package/src/components/chart/chart-line.tsx +50 -0
- package/src/components/chart/chart-scatter.tsx +35 -0
- package/src/components/chart/chart-tooltip-dot.tsx +70 -0
- package/src/components/chart/chart-tooltip-x.tsx +121 -0
- package/src/components/chart/chart-tooltip-y.tsx +55 -0
- package/src/components/chart/chart-tooltip.tsx +133 -0
- package/src/components/chart/chart-x-axis.tsx +27 -0
- package/src/components/chart/chart-y-axis.tsx +21 -0
- package/src/components/chart/chart.context.tsx +127 -0
- package/src/components/chart/chart.tsx +448 -0
- package/src/components/chart/chart.types.ts +108 -0
- package/src/components/chart/chart.variants.test.ts +893 -0
- package/src/components/chart/chart.variants.ts +726 -0
- package/src/components/chart/index.ts +103 -0
- package/src/components/chart/pie-chart-center.tsx +40 -0
- package/src/components/chart/pie-chart-label.tsx +56 -0
- package/src/components/chart/pie-chart-slice.tsx +45 -0
- package/src/components/chart/pie-chart-tooltip.tsx +85 -0
- package/src/components/chart/pie-chart.context.tsx +77 -0
- package/src/components/chart/pie-chart.tsx +247 -0
- package/src/components/chart/use-chart-font.ts +25 -0
- package/src/components/chart/use-chart-palette.ts +38 -0
- package/src/components/checkbox/AGENTS.md +185 -0
- package/src/components/checkbox/checkbox-box.tsx +152 -0
- package/src/components/checkbox/checkbox-group.tsx +107 -0
- package/src/components/checkbox/checkbox-label.tsx +34 -0
- package/src/components/checkbox/checkbox.context.tsx +136 -0
- package/src/components/checkbox/checkbox.tsx +258 -0
- package/src/components/checkbox/checkbox.types.ts +14 -0
- package/src/components/checkbox/checkbox.variants.test.ts +634 -0
- package/src/components/checkbox/checkbox.variants.ts +484 -0
- package/src/components/checkbox/index.ts +42 -0
- package/src/components/field/AGENTS.md +112 -0
- package/src/components/field/field-content.tsx +24 -0
- package/src/components/field/field-description.tsx +29 -0
- package/src/components/field/field-error.tsx +61 -0
- package/src/components/field/field-group.tsx +22 -0
- package/src/components/field/field-label.tsx +47 -0
- package/src/components/field/field-legend.tsx +34 -0
- package/src/components/field/field-separator.tsx +81 -0
- package/src/components/field/field-set.tsx +22 -0
- package/src/components/field/field.context.tsx +95 -0
- package/src/components/field/field.tsx +157 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +240 -0
- package/src/components/field/field.variants.ts +156 -0
- package/src/components/field/index.ts +22 -0
- package/src/components/icon/AGENTS.md +104 -0
- package/src/components/icon/icon.context.tsx +29 -0
- package/src/components/icon/icon.tsx +92 -0
- package/src/components/icon/icon.variants.test.ts +111 -0
- package/src/components/icon/icon.variants.ts +79 -0
- package/src/components/icon/index.ts +11 -0
- package/src/components/input/AGENTS.md +126 -0
- package/src/components/input/index.ts +27 -0
- package/src/components/input/input-group-decorator.tsx +84 -0
- package/src/components/input/input-group-prefix.tsx +16 -0
- package/src/components/input/input-group-suffix.tsx +15 -0
- package/src/components/input/input-group.tsx +140 -0
- package/src/components/input/input.context.tsx +87 -0
- package/src/components/input/input.tsx +184 -0
- package/src/components/input/input.types.ts +10 -0
- package/src/components/input/input.variants.test.ts +369 -0
- package/src/components/input/input.variants.ts +329 -0
- package/src/components/list-group/AGENTS.md +67 -0
- package/src/components/list-group/index.ts +20 -0
- package/src/components/list-group/list-group-item-content.tsx +10 -0
- package/src/components/list-group/list-group-item-description.tsx +12 -0
- package/src/components/list-group/list-group-item-prefix.tsx +30 -0
- package/src/components/list-group/list-group-item-suffix.tsx +55 -0
- package/src/components/list-group/list-group-item-title.tsx +17 -0
- package/src/components/list-group/list-group-item.tsx +87 -0
- package/src/components/list-group/list-group.context.tsx +65 -0
- package/src/components/list-group/list-group.tsx +116 -0
- package/src/components/list-group/list-group.types.ts +14 -0
- package/src/components/list-group/list-group.variants.test.ts +223 -0
- package/src/components/list-group/list-group.variants.ts +107 -0
- package/src/components/pressable/AGENTS.md +74 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +261 -0
- package/src/components/pressable/pressable.variants.test.ts +128 -0
- package/src/components/pressable/pressable.variants.ts +80 -0
- package/src/components/provider/AGENTS.md +90 -0
- package/src/components/provider/index.ts +1 -0
- package/src/components/provider/provider.tsx +105 -0
- package/src/components/radio/AGENTS.md +245 -0
- package/src/components/radio/index.ts +35 -0
- package/src/components/radio/radio-group.tsx +126 -0
- package/src/components/radio/radio-indicator.tsx +91 -0
- package/src/components/radio/radio-label.tsx +39 -0
- package/src/components/radio/radio.context.tsx +132 -0
- package/src/components/radio/radio.tsx +215 -0
- package/src/components/radio/radio.variants.test.ts +580 -0
- package/src/components/radio/radio.variants.ts +271 -0
- package/src/components/screen/AGENTS.md +287 -0
- package/src/components/screen/index.ts +85 -0
- package/src/components/screen/screen-chat-list.tsx +485 -0
- package/src/components/screen/screen-content.tsx +69 -0
- package/src/components/screen/screen-debug.ts +43 -0
- package/src/components/screen/screen-error.tsx +61 -0
- package/src/components/screen/screen-flat-list.tsx +88 -0
- package/src/components/screen/screen-footer-background.tsx +60 -0
- package/src/components/screen/screen-footer.tsx +170 -0
- package/src/components/screen/screen-header.tsx +36 -0
- package/src/components/screen/screen-legend-list.tsx +105 -0
- package/src/components/screen/screen-list-component.tsx +23 -0
- package/src/components/screen/screen-loading.tsx +50 -0
- package/src/components/screen/screen-navbar-back-button.tsx +79 -0
- package/src/components/screen/screen-navbar-background.tsx +45 -0
- package/src/components/screen/screen-navbar-subtitle.tsx +26 -0
- package/src/components/screen/screen-navbar-title.tsx +31 -0
- package/src/components/screen/screen-navbar.tsx +154 -0
- package/src/components/screen/screen-root.tsx +38 -0
- package/src/components/screen/screen-scroll-area.tsx +116 -0
- package/src/components/screen/screen-scroll-shadow.tsx +186 -0
- package/src/components/screen/screen-section-list.tsx +81 -0
- package/src/components/screen/screen-view.tsx +59 -0
- package/src/components/screen/screen.context.tsx +167 -0
- package/src/components/screen/screen.tsx +114 -0
- package/src/components/screen/screen.types.ts +66 -0
- package/src/components/screen/screen.variants.test.ts +602 -0
- package/src/components/screen/screen.variants.ts +475 -0
- package/src/components/screen/use-screen-scroll-insets.ts +218 -0
- package/src/components/separator/AGENTS.md +30 -0
- package/src/components/separator/index.ts +8 -0
- package/src/components/separator/separator.tsx +85 -0
- package/src/components/slider/AGENTS.md +274 -0
- package/src/components/slider/index.ts +50 -0
- package/src/components/slider/slider-fill.tsx +68 -0
- package/src/components/slider/slider-output.tsx +55 -0
- package/src/components/slider/slider-thumb.tsx +193 -0
- package/src/components/slider/slider-track.tsx +233 -0
- package/src/components/slider/slider.context.tsx +118 -0
- package/src/components/slider/slider.tsx +321 -0
- package/src/components/slider/slider.types.ts +26 -0
- package/src/components/slider/slider.variants.test.ts +856 -0
- package/src/components/slider/slider.variants.ts +661 -0
- package/src/components/spinner/AGENTS.md +73 -0
- package/src/components/spinner/index.ts +17 -0
- package/src/components/spinner/spinner-arc.tsx +90 -0
- package/src/components/spinner/spinner-content.tsx +63 -0
- package/src/components/spinner/spinner.context.tsx +52 -0
- package/src/components/spinner/spinner.tsx +128 -0
- package/src/components/spinner/spinner.variants.test.ts +273 -0
- package/src/components/spinner/spinner.variants.ts +187 -0
- package/src/components/switch/AGENTS.md +213 -0
- package/src/components/switch/index.ts +42 -0
- package/src/components/switch/switch-content.tsx +111 -0
- package/src/components/switch/switch-end-content.tsx +18 -0
- package/src/components/switch/switch-start-content.tsx +20 -0
- package/src/components/switch/switch-thumb.tsx +102 -0
- package/src/components/switch/switch.context.tsx +79 -0
- package/src/components/switch/switch.tsx +423 -0
- package/src/components/switch/switch.types.ts +14 -0
- package/src/components/switch/switch.variants.test.ts +570 -0
- package/src/components/switch/switch.variants.ts +511 -0
- package/src/components/tabs/AGENTS.md +287 -0
- package/src/components/tabs/index.ts +69 -0
- package/src/components/tabs/tabs-content.tsx +65 -0
- package/src/components/tabs/tabs-indicator.tsx +97 -0
- package/src/components/tabs/tabs-label.tsx +62 -0
- package/src/components/tabs/tabs-list.tsx +139 -0
- package/src/components/tabs/tabs-pager.tsx +59 -0
- package/src/components/tabs/tabs-scroll-view.tsx +171 -0
- package/src/components/tabs/tabs-separator.tsx +73 -0
- package/src/components/tabs/tabs-trigger.tsx +210 -0
- package/src/components/tabs/tabs.context.tsx +294 -0
- package/src/components/tabs/tabs.tsx +435 -0
- package/src/components/tabs/tabs.types.ts +13 -0
- package/src/components/tabs/tabs.variants.test.ts +1020 -0
- package/src/components/tabs/tabs.variants.ts +670 -0
- package/src/components/text/AGENTS.md +95 -0
- package/src/components/text/index.ts +25 -0
- package/src/components/text/text.context.tsx +60 -0
- package/src/components/text/text.tsx +251 -0
- package/src/components/text/text.variants.test.ts +422 -0
- package/src/components/text/text.variants.ts +282 -0
- package/src/display-name.test.ts +145 -0
- package/src/docs.test.ts +98 -0
- package/src/expo/navigation-theme.tsx +58 -0
- package/src/hooks/use-controllable-state.ts +45 -0
- package/src/hooks/use-keyboard-state-sync.tsx +147 -0
- package/src/hooks/use-navigation-theme.ts +78 -0
- package/src/hooks/use-theme-color.ts +44 -0
- package/src/icons/central.ts +1 -0
- package/src/lib/cn.test.ts +136 -0
- package/src/lib/cn.ts +29 -0
- package/src/lib/color.test.ts +80 -0
- package/src/lib/color.ts +79 -0
- package/src/lib/compose-refs.test.ts +64 -0
- package/src/lib/compose-refs.ts +37 -0
- package/src/lib/keyboard-animation.test.ts +33 -0
- package/src/lib/keyboard-animation.ts +28 -0
- package/src/lib/merge-props.test.ts +82 -0
- package/src/lib/merge-props.ts +47 -0
- package/src/lib/navigation-theme.test.ts +49 -0
- package/src/lib/navigation-theme.ts +53 -0
- package/src/lib/slot.tsx +45 -0
- package/src/lib/tv.ts +20 -0
- package/src/styles/base.css +2 -0
- package/src/styles/geometry.test.ts +117 -0
- package/src/styles/index.css +3 -0
- package/src/styles/theme-tokens.test.ts +252 -0
- package/src/styles/theme.css +447 -0
- package/src/styles/tokens.css +132 -0
- package/src/styles/tokens.test.ts +181 -0
- package/src/styles/tokens.ts +86 -0
- package/src/uniwind-env.d.ts +1 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { ChartScatter as EngineScatter } from "@delacour/react-native-charts";
|
|
2
|
+
import type { ScatterShape } from "@delacour/react-native-charts/core";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
import { useSeriesColor } from "./chart.context";
|
|
5
|
+
|
|
6
|
+
export type ChartScatterProps = {
|
|
7
|
+
/** Which series to draw. Names a key of the chart's `config`. */
|
|
8
|
+
yKey: string;
|
|
9
|
+
/** Overrides the series' colour from the config. A literal — the canvas can resolve no token. */
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Every point's radius in points. Defaults to 4. */
|
|
12
|
+
radius?: number;
|
|
13
|
+
shape?: ScatterShape;
|
|
14
|
+
opacity?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* One filled shape per datum, in the series' colour.
|
|
19
|
+
*
|
|
20
|
+
* The engine draws a whole series as one path, so a hundred points is one
|
|
21
|
+
* Skia node; a gap is the same shape at radius zero, which is what lets a
|
|
22
|
+
* datum come and go without the series snapping. Rings, per-point radii and
|
|
23
|
+
* the engine's other options are deliberately not surfaced — a scatter on a
|
|
24
|
+
* themed chart is a set of dots, and a chart that needs more reaches for the
|
|
25
|
+
* engine's own mark.
|
|
26
|
+
*/
|
|
27
|
+
export function ChartScatter({ yKey, color, radius, shape, opacity }: ChartScatterProps): ReactElement | null {
|
|
28
|
+
const resolved = useSeriesColor(yKey);
|
|
29
|
+
const paint = color ?? resolved;
|
|
30
|
+
if (paint === undefined) return null;
|
|
31
|
+
|
|
32
|
+
return <EngineScatter color={paint} opacity={opacity} radius={radius} shape={shape} yKey={yKey} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ChartScatter.displayName = "DelacourUI.Chart.Scatter";
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ChartCursorDot } from "@delacour/react-native-charts";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import { useChart } from "./chart.context";
|
|
4
|
+
|
|
5
|
+
export type ChartTooltipDotProps = {
|
|
6
|
+
/** Which series to mark. Omit to mark every one. */
|
|
7
|
+
yKey?: string;
|
|
8
|
+
/** Overrides the series' colour. */
|
|
9
|
+
color?: string;
|
|
10
|
+
radius?: number;
|
|
11
|
+
/** The ring's width. Zero draws no ring. */
|
|
12
|
+
borderWidth?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Ride the curve continuously instead of sitting on the nearest datum.
|
|
15
|
+
*
|
|
16
|
+
* Off by default: the readout names a datum, so a dot that glides between
|
|
17
|
+
* them describes a different position from the number beside it.
|
|
18
|
+
*/
|
|
19
|
+
glide?: boolean;
|
|
20
|
+
opacity?: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A dot on each series at the scrubbed position.
|
|
25
|
+
*
|
|
26
|
+
* A Skia mark, so it moves with the plot on the UI thread. It is named under
|
|
27
|
+
* `Chart.Tooltip` because it is part of the same readout, but it is placed as
|
|
28
|
+
* a **sibling** of it rather than a child — the tooltip is a React Native view
|
|
29
|
+
* over the canvas and this is drawn inside it, so one cannot contain the other.
|
|
30
|
+
*
|
|
31
|
+
* The ring takes the chart's background, which is what makes a dot in the
|
|
32
|
+
* series colour legible on a line of that same colour.
|
|
33
|
+
*
|
|
34
|
+
* A series drawn only as a bar gets no dot. A bar has no curve for a dot to
|
|
35
|
+
* sit on — it would float at the bar's top, over a column the tooltip's band
|
|
36
|
+
* already picks out — so the dot follows only the keys some line, area or
|
|
37
|
+
* scatter draws a point for.
|
|
38
|
+
*/
|
|
39
|
+
export function ChartTooltipDot({
|
|
40
|
+
yKey,
|
|
41
|
+
color,
|
|
42
|
+
radius,
|
|
43
|
+
borderWidth,
|
|
44
|
+
glide,
|
|
45
|
+
opacity,
|
|
46
|
+
}: ChartTooltipDotProps): ReactElement {
|
|
47
|
+
const { series, surfaceColor, bars, pointKeys } = useChart();
|
|
48
|
+
const shown = (yKey === undefined ? series : series.filter((entry) => entry.key === yKey)).filter(
|
|
49
|
+
(entry) => !bars.keys.includes(entry.key) || pointKeys.includes(entry.key)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<>
|
|
54
|
+
{shown.map((entry) => (
|
|
55
|
+
<ChartCursorDot
|
|
56
|
+
borderColor={borderWidth === 0 ? undefined : surfaceColor}
|
|
57
|
+
borderWidth={borderWidth}
|
|
58
|
+
color={color ?? entry.color}
|
|
59
|
+
key={entry.key}
|
|
60
|
+
opacity={opacity}
|
|
61
|
+
radius={radius}
|
|
62
|
+
snap={glide !== true}
|
|
63
|
+
yKey={entry.key}
|
|
64
|
+
/>
|
|
65
|
+
))}
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ChartTooltipDot.displayName = "DelacourUI.Chart.Tooltip.Dot";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { ChartBounds, ChartOrientation, ChartScrubState, ChartStep } from "@delacour/react-native-charts";
|
|
2
|
+
import { ChartCursorLine, useChartContext as useEngineChart } from "@delacour/react-native-charts";
|
|
3
|
+
import { Rect } from "@shopify/react-native-skia";
|
|
4
|
+
import type { ReactElement } from "react";
|
|
5
|
+
import { useDerivedValue } from "react-native-reanimated";
|
|
6
|
+
import { useChart } from "./chart.context";
|
|
7
|
+
|
|
8
|
+
export type ChartTooltipXProps = {
|
|
9
|
+
/** Overrides the `muted-foreground` token this paints with. */
|
|
10
|
+
color?: string;
|
|
11
|
+
width?: number;
|
|
12
|
+
/** Dash on/off lengths. Pass `null` for a solid rule. */
|
|
13
|
+
dash?: readonly [number, number] | null;
|
|
14
|
+
glide?: boolean;
|
|
15
|
+
opacity?: number;
|
|
16
|
+
/**
|
|
17
|
+
* A translucent band one step wide instead of a rule.
|
|
18
|
+
*
|
|
19
|
+
* On by default when the chart holds bars or candles: a hairline through
|
|
20
|
+
* the middle of a bar says less than a wash over the whole column. Set it
|
|
21
|
+
* explicitly to get the band on a line chart, or the rule on a bar chart.
|
|
22
|
+
*/
|
|
23
|
+
band?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Dashed by default, so the crosshair reads as a guide beside solid gridlines. */
|
|
27
|
+
const DEFAULT_DASH = [4, 4] as const;
|
|
28
|
+
|
|
29
|
+
/** A wash, not a fill — the bars under it have to stay legible. */
|
|
30
|
+
const BAND_OPACITY = 0.12;
|
|
31
|
+
|
|
32
|
+
/** Parked off-canvas, so a band with nothing to point at is not at the origin. */
|
|
33
|
+
const OFFSCREEN = -10_000;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A vertical rule through the scrubbed position — or, over bars, a band.
|
|
37
|
+
*
|
|
38
|
+
* Named for the axis it reads against — it tells you *where along x* you are —
|
|
39
|
+
* rather than for the direction it is drawn in. The band is the same mark in
|
|
40
|
+
* a different width: one `xStep`, centred on the snapped datum, so it picks
|
|
41
|
+
* out the column the readout names. On a horizontal chart the step runs up
|
|
42
|
+
* the category axis, so the band lies across the plot instead.
|
|
43
|
+
*/
|
|
44
|
+
export function ChartTooltipX({ color, width, dash, glide, opacity, band }: ChartTooltipXProps): ReactElement | null {
|
|
45
|
+
const { axisColor, bars, candlestick } = useChart();
|
|
46
|
+
const paint = color ?? axisColor;
|
|
47
|
+
if (paint === undefined) return null;
|
|
48
|
+
|
|
49
|
+
if (band ?? (bars.mode !== "none" || candlestick !== null)) {
|
|
50
|
+
return <ChartTooltipBand color={paint} opacity={opacity ?? BAND_OPACITY} />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ChartCursorLine
|
|
55
|
+
axis="x"
|
|
56
|
+
color={paint}
|
|
57
|
+
dash={dash === null ? undefined : (dash ?? DEFAULT_DASH)}
|
|
58
|
+
opacity={opacity ?? 0.6}
|
|
59
|
+
snap={glide !== true}
|
|
60
|
+
width={width}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ChartTooltipX.displayName = "DelacourUI.Chart.Tooltip.X";
|
|
66
|
+
|
|
67
|
+
type ChartTooltipBandProps = {
|
|
68
|
+
readonly color: string;
|
|
69
|
+
readonly opacity: number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The band. Reads the engine's context directly, because the scrub's shared
|
|
74
|
+
* values and the step live there; the outer part only decides whether to
|
|
75
|
+
* draw one. Split so a chart with no scrub never runs the derived values.
|
|
76
|
+
*/
|
|
77
|
+
function ChartTooltipBand(props: ChartTooltipBandProps): ReactElement | null {
|
|
78
|
+
const { scrub, bounds, xStep, orientation } = useEngineChart();
|
|
79
|
+
if (scrub === null) return null;
|
|
80
|
+
return <TooltipBand {...props} bounds={bounds} orientation={orientation} scrub={scrub} step={xStep} />;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
ChartTooltipBand.displayName = "DelacourUI.Chart.Tooltip.X.Band";
|
|
84
|
+
|
|
85
|
+
type TooltipBandProps = ChartTooltipBandProps & {
|
|
86
|
+
readonly scrub: ChartScrubState;
|
|
87
|
+
readonly bounds: ChartBounds;
|
|
88
|
+
readonly step: ChartStep;
|
|
89
|
+
readonly orientation: ChartOrientation;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* One rect, `step.px` wide, riding the snapped position on the UI thread the
|
|
94
|
+
* way the engine's cursor line does. Always snapped: a band that glided
|
|
95
|
+
* between columns would highlight half of two bars. The engine reports the
|
|
96
|
+
* category's position on `snappedX` when vertical and `snappedY` when
|
|
97
|
+
* horizontal, and the other is `NaN` — which is why each axis reads its own.
|
|
98
|
+
*/
|
|
99
|
+
function TooltipBand({ color, opacity, scrub, bounds, step, orientation }: TooltipBandProps): ReactElement {
|
|
100
|
+
const horizontal = orientation === "horizontal";
|
|
101
|
+
const width = horizontal ? bounds.right - bounds.left : step.px;
|
|
102
|
+
const height = horizontal ? step.px : bounds.bottom - bounds.top;
|
|
103
|
+
|
|
104
|
+
const x = useDerivedValue(() => {
|
|
105
|
+
if (horizontal) return bounds.left;
|
|
106
|
+
const centre = scrub.snappedX.value;
|
|
107
|
+
return Number.isFinite(centre) ? centre - step.px / 2 : OFFSCREEN;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const y = useDerivedValue(() => {
|
|
111
|
+
if (!horizontal) return bounds.top;
|
|
112
|
+
const centre = scrub.snappedY.value;
|
|
113
|
+
return Number.isFinite(centre) ? centre - step.px / 2 : OFFSCREEN;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const alpha = useDerivedValue(() => (scrub.isActive.value ? opacity : 0));
|
|
117
|
+
|
|
118
|
+
return <Rect color={color} height={height} opacity={alpha} width={width} x={x} y={y} />;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
TooltipBand.displayName = "DelacourUI.Chart.Tooltip.X.Band.Inner";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { ChartCursorLine } from "@delacour/react-native-charts";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import { useChart } from "./chart.context";
|
|
4
|
+
|
|
5
|
+
export type ChartTooltipYProps = {
|
|
6
|
+
/** Which series' value the rule sits at. Defaults to the first. */
|
|
7
|
+
yKey?: string;
|
|
8
|
+
/** Overrides the series' colour. */
|
|
9
|
+
color?: string;
|
|
10
|
+
width?: number;
|
|
11
|
+
/** Dash on/off lengths. Pass `null` for a solid rule. */
|
|
12
|
+
dash?: readonly [number, number] | null;
|
|
13
|
+
glide?: boolean;
|
|
14
|
+
opacity?: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const DEFAULT_DASH = [4, 4] as const;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A horizontal rule at one series' scrubbed value.
|
|
21
|
+
*
|
|
22
|
+
* It carries the value across to the y axis, which is the same job a gridline
|
|
23
|
+
* does — so it takes the series' own colour rather than the axis', to say
|
|
24
|
+
* *which* series it is reading.
|
|
25
|
+
*
|
|
26
|
+
* One series only. Drawing a rule per series would put four horizontal lines
|
|
27
|
+
* across a four-series chart, and the reader would have to work out which
|
|
28
|
+
* belonged to which.
|
|
29
|
+
*/
|
|
30
|
+
export function ChartTooltipY({
|
|
31
|
+
yKey,
|
|
32
|
+
color,
|
|
33
|
+
width,
|
|
34
|
+
dash,
|
|
35
|
+
glide,
|
|
36
|
+
opacity = 0.6,
|
|
37
|
+
}: ChartTooltipYProps): ReactElement | null {
|
|
38
|
+
const { series } = useChart();
|
|
39
|
+
const target = yKey === undefined ? series[0] : series.find((entry) => entry.key === yKey);
|
|
40
|
+
if (target === undefined) return null;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<ChartCursorLine
|
|
44
|
+
axis="y"
|
|
45
|
+
color={color ?? target.color}
|
|
46
|
+
dash={dash === null ? undefined : (dash ?? DEFAULT_DASH)}
|
|
47
|
+
opacity={opacity}
|
|
48
|
+
snap={glide !== true}
|
|
49
|
+
width={width}
|
|
50
|
+
yKey={target.key}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ChartTooltipY.displayName = "DelacourUI.Chart.Tooltip.Y";
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { type ReactElement, useState } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import Animated, { useAnimatedReaction, useAnimatedStyle } from "react-native-reanimated";
|
|
4
|
+
import { scheduleOnRN } from "react-native-worklets";
|
|
5
|
+
import { cn } from "../../lib/cn";
|
|
6
|
+
import { Text } from "../text";
|
|
7
|
+
import { useChart } from "./chart.context";
|
|
8
|
+
import { chartTooltipOffset, resolveTooltipRows } from "./chart.variants";
|
|
9
|
+
import { ChartTooltipDot } from "./chart-tooltip-dot";
|
|
10
|
+
import { ChartTooltipX } from "./chart-tooltip-x";
|
|
11
|
+
import { ChartTooltipY } from "./chart-tooltip-y";
|
|
12
|
+
|
|
13
|
+
export type ChartTooltipProps = {
|
|
14
|
+
className?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Prints the heading from the row under the cursor.
|
|
17
|
+
*
|
|
18
|
+
* Defaults to the x field, formatted the way the axis formats it — a `Date`
|
|
19
|
+
* comes out as `20 Jan`, not as `Tue Jan 20 2026 00:00:00 GMT+0000`.
|
|
20
|
+
*/
|
|
21
|
+
formatHeading?: (row: Record<string, unknown>, index: number) => string;
|
|
22
|
+
/** Prints one series' value. Defaults to the number as written. */
|
|
23
|
+
formatValue?: (value: unknown, key: string) => string;
|
|
24
|
+
/** Overrides the measured size used to keep the tooltip inside the frame. */
|
|
25
|
+
size?: { width: number; height: number };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Assumed size before layout, so the first frame is not placed off-screen. */
|
|
29
|
+
const DEFAULT_SIZE = { width: 120, height: 56 };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A floating readout that follows the scrub.
|
|
33
|
+
*
|
|
34
|
+
* **A React Native view, deliberately, even though it sits over the canvas.**
|
|
35
|
+
* It wants `popover`, `border`, the radius scale and the type scale — none of
|
|
36
|
+
* which exist in Skia — and it is the part a caller most wants to restyle,
|
|
37
|
+
* which a className cannot do to a Skia rounded rect. Drawing it in Skia would
|
|
38
|
+
* mean re-implementing text layout and line breaking for nothing.
|
|
39
|
+
*
|
|
40
|
+
* ## Two threads, on purpose
|
|
41
|
+
*
|
|
42
|
+
* Its **position** rides the scrub's shared values through `useAnimatedStyle`,
|
|
43
|
+
* so it tracks the finger on the UI thread with no bridge hop.
|
|
44
|
+
*
|
|
45
|
+
* Its **contents** are ordinary React state, updated through `scheduleOnRN`
|
|
46
|
+
* only when the nearest datum index actually changes. That is a handful of
|
|
47
|
+
* updates per drag rather than one per frame, because the text only changes
|
|
48
|
+
* when the selected row does — and text on a shared value would mean an
|
|
49
|
+
* `AnimatedTextInput` and a `value` prop pretending to be a label.
|
|
50
|
+
*
|
|
51
|
+
* With a candlestick present the rows are open, high, low and close, each
|
|
52
|
+
* swatched in the candle's own sentiment colour — see `resolveTooltipRows`.
|
|
53
|
+
*/
|
|
54
|
+
function ChartTooltipRoot({ className, formatHeading, formatValue, size }: ChartTooltipProps): ReactElement {
|
|
55
|
+
const { scrub, slots, series, data, formatXValue, frame, candlestick, candleColors } = useChart();
|
|
56
|
+
const [index, setIndex] = useState(-1);
|
|
57
|
+
const measured = size ?? DEFAULT_SIZE;
|
|
58
|
+
|
|
59
|
+
useAnimatedReaction(
|
|
60
|
+
() => scrub.index.value,
|
|
61
|
+
(next, previous) => {
|
|
62
|
+
if (next === previous) return;
|
|
63
|
+
scheduleOnRN(setIndex, next);
|
|
64
|
+
}
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const style = useAnimatedStyle(() => {
|
|
68
|
+
const offset = chartTooltipOffset({
|
|
69
|
+
x: scrub.x.value,
|
|
70
|
+
y: scrub.series[series[0]?.key ?? ""]?.y.value ?? frame.height / 2,
|
|
71
|
+
width: measured.width,
|
|
72
|
+
height: measured.height,
|
|
73
|
+
frameWidth: frame.width,
|
|
74
|
+
frameHeight: frame.height,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
opacity: scrub.isActive.value ? 1 : 0,
|
|
79
|
+
transform: [{ translateX: offset.x }, { translateY: offset.y }],
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const row = data[index];
|
|
84
|
+
const heading = row === undefined ? "" : (formatHeading?.(row, index) ?? formatXValue(row));
|
|
85
|
+
const rows = resolveTooltipRows(
|
|
86
|
+
series,
|
|
87
|
+
candlestick === null ? null : { keys: candlestick, colors: candleColors },
|
|
88
|
+
row
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Animated.View className={cn(slots.tooltip(), className)} pointerEvents="none" style={style}>
|
|
93
|
+
{heading === "" ? null : <Text className={slots.tooltipHeading()}>{heading}</Text>}
|
|
94
|
+
{rows.map((entry) => (
|
|
95
|
+
<View className={slots.tooltipRow()} key={entry.key}>
|
|
96
|
+
<View className={slots.tooltipSwatch()} style={{ backgroundColor: entry.color }} />
|
|
97
|
+
<Text className={slots.tooltipName()}>{entry.label}</Text>
|
|
98
|
+
<Text className={slots.tooltipValue()}>
|
|
99
|
+
{row === undefined ? "" : (formatValue?.(entry.value, entry.key) ?? String(entry.value ?? ""))}
|
|
100
|
+
</Text>
|
|
101
|
+
</View>
|
|
102
|
+
))}
|
|
103
|
+
</Animated.View>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A floating readout that follows the scrub, and the marks that point at it.
|
|
109
|
+
*
|
|
110
|
+
* The readout is a React Native view over the canvas; `Dot`, `X` and `Y` are
|
|
111
|
+
* Skia marks drawn inside it. They are named here because they are one
|
|
112
|
+
* feature, but they are **placed as siblings** — a view cannot contain a
|
|
113
|
+
* canvas node, so `<Chart.Tooltip><Chart.Tooltip.Dot /></Chart.Tooltip>` would
|
|
114
|
+
* put the dot in the wrong tree and draw nothing.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* <Chart config={config} data={rows} xKey="at">
|
|
118
|
+
* <Chart.Line yKey="price" />
|
|
119
|
+
* <Chart.Tooltip.X />
|
|
120
|
+
* <Chart.Tooltip.Y />
|
|
121
|
+
* <Chart.Tooltip.Dot />
|
|
122
|
+
* <Chart.Tooltip />
|
|
123
|
+
* </Chart>
|
|
124
|
+
*/
|
|
125
|
+
export const ChartTooltip = Object.assign(ChartTooltipRoot, {
|
|
126
|
+
/** A dot on each series at the scrubbed point, ringed in the chart's background. */
|
|
127
|
+
Dot: ChartTooltipDot,
|
|
128
|
+
/** A vertical rule through the scrubbed position — where along x you are. */
|
|
129
|
+
X: ChartTooltipX,
|
|
130
|
+
/** A horizontal rule at one series' value, carrying it across to the y axis. */
|
|
131
|
+
Y: ChartTooltipY,
|
|
132
|
+
displayName: "DelacourUI.Chart.Tooltip",
|
|
133
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ChartXAxis as EngineXAxis } from "@delacour/react-native-charts";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import { useChart } from "./chart.context";
|
|
4
|
+
|
|
5
|
+
export type ChartXAxisProps = {
|
|
6
|
+
/** Overrides the `muted-foreground` token this paints with. */
|
|
7
|
+
color?: string;
|
|
8
|
+
/** Space between the plot rect and the labels. */
|
|
9
|
+
tickPadding?: number;
|
|
10
|
+
opacity?: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tick labels below the plot.
|
|
15
|
+
*
|
|
16
|
+
* `muted-foreground`, the colour every secondary label in this package already
|
|
17
|
+
* uses — an axis names the scale, it does not compete with the data.
|
|
18
|
+
*/
|
|
19
|
+
export function ChartXAxis({ color, tickPadding, opacity }: ChartXAxisProps): ReactElement | null {
|
|
20
|
+
const { axisColor } = useChart();
|
|
21
|
+
const paint = color ?? axisColor;
|
|
22
|
+
if (paint === undefined) return null;
|
|
23
|
+
|
|
24
|
+
return <EngineXAxis color={paint} opacity={opacity} tickPadding={tickPadding} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ChartXAxis.displayName = "DelacourUI.Chart.XAxis";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ChartYAxis as EngineYAxis } from "@delacour/react-native-charts";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import { useChart } from "./chart.context";
|
|
4
|
+
|
|
5
|
+
export type ChartYAxisProps = {
|
|
6
|
+
color?: string;
|
|
7
|
+
tickPadding?: number;
|
|
8
|
+
side?: "left" | "right";
|
|
9
|
+
opacity?: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Tick labels beside the plot, right-aligned so they form a clean column. */
|
|
13
|
+
export function ChartYAxis({ color, tickPadding, side, opacity }: ChartYAxisProps): ReactElement | null {
|
|
14
|
+
const { axisColor } = useChart();
|
|
15
|
+
const paint = color ?? axisColor;
|
|
16
|
+
if (paint === undefined) return null;
|
|
17
|
+
|
|
18
|
+
return <EngineYAxis color={paint} opacity={opacity} side={side} tickPadding={tickPadding} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
ChartYAxis.displayName = "DelacourUI.Chart.YAxis";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { ChartOrientation, ChartScrubState } from "@delacour/react-native-charts";
|
|
2
|
+
import { createContext, type ReactNode, useContext } from "react";
|
|
3
|
+
import type {
|
|
4
|
+
ChartBarLayout,
|
|
5
|
+
ChartCandleColors,
|
|
6
|
+
ChartCandlestickKeys,
|
|
7
|
+
ChartDatum,
|
|
8
|
+
ChartResolvedSeries,
|
|
9
|
+
} from "./chart.types";
|
|
10
|
+
import type { ChartSize, chartVariants } from "./chart.variants";
|
|
11
|
+
import { PieChartContext } from "./pie-chart.context";
|
|
12
|
+
|
|
13
|
+
export type ChartSlots = ReturnType<typeof chartVariants>;
|
|
14
|
+
|
|
15
|
+
export type ChartContextValue = {
|
|
16
|
+
/** Series in draw order, colours already resolved to values. */
|
|
17
|
+
readonly series: readonly ChartResolvedSeries[];
|
|
18
|
+
readonly data: readonly ChartDatum[];
|
|
19
|
+
readonly xKey: string;
|
|
20
|
+
/** Prints a row's x field — a Date as a date, not as an RFC timestamp. */
|
|
21
|
+
readonly formatXValue: (row: ChartDatum) => string;
|
|
22
|
+
readonly size: ChartSize;
|
|
23
|
+
readonly slots: ChartSlots;
|
|
24
|
+
/** Resolved gridline colour, or `undefined` if the theme emits none. */
|
|
25
|
+
readonly gridColor: string | undefined;
|
|
26
|
+
/** Resolved axis-label colour. */
|
|
27
|
+
readonly axisColor: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The surface the chart is drawn on.
|
|
30
|
+
*
|
|
31
|
+
* A cursor dot rings itself in this so it reads as a knob sitting on the
|
|
32
|
+
* line rather than as a slight thickening of it.
|
|
33
|
+
*/
|
|
34
|
+
readonly surfaceColor: string | undefined;
|
|
35
|
+
readonly scrub: ChartScrubState;
|
|
36
|
+
/** The frame's measured size, for placing an overlay inside it. */
|
|
37
|
+
readonly frame: { readonly width: number; readonly height: number };
|
|
38
|
+
/** Every `Chart.Bar`, resolved into one arrangement. `mode: "none"` without any. */
|
|
39
|
+
readonly bars: ChartBarLayout;
|
|
40
|
+
/** A bar's corner radius in points — `--radius` × the size's multiplier. */
|
|
41
|
+
readonly barRadius: number;
|
|
42
|
+
/** The candlestick's four fields, or `null` without one. */
|
|
43
|
+
readonly candlestick: ChartCandlestickKeys | null;
|
|
44
|
+
/** The three sentiment tokens, resolved. */
|
|
45
|
+
readonly candleColors: ChartCandleColors;
|
|
46
|
+
/**
|
|
47
|
+
* Keys some mark draws a point for — a line, an area or a scatter.
|
|
48
|
+
*
|
|
49
|
+
* A cursor dot skips a series that is only a bar: a bar has no curve for a
|
|
50
|
+
* dot to sit on, and the band under the tooltip already says which column.
|
|
51
|
+
*/
|
|
52
|
+
readonly pointKeys: readonly string[];
|
|
53
|
+
readonly orientation: ChartOrientation;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The resolved chart, published twice from one value.
|
|
58
|
+
*
|
|
59
|
+
* Twice because the chart straddles a reconciler boundary. Skia's `<Canvas>`
|
|
60
|
+
* runs its own React renderer and context does not cross into it, so the root
|
|
61
|
+
* renders one provider around the React Native subtree — the legend and the
|
|
62
|
+
* tooltip — and a second one *inside* the canvas around the marks. Same object,
|
|
63
|
+
* two provider elements, no bridge and no extra dependency.
|
|
64
|
+
*
|
|
65
|
+
* The context is exported rather than hidden behind its hook because that
|
|
66
|
+
* second mounting is unusual enough to want something to copy.
|
|
67
|
+
*/
|
|
68
|
+
export const ChartContext = createContext<ChartContextValue | null>(null);
|
|
69
|
+
|
|
70
|
+
ChartContext.displayName = "DelacourUI.ChartContext";
|
|
71
|
+
|
|
72
|
+
export function ChartProvider({
|
|
73
|
+
value,
|
|
74
|
+
children,
|
|
75
|
+
}: {
|
|
76
|
+
readonly value: ChartContextValue;
|
|
77
|
+
readonly children: ReactNode;
|
|
78
|
+
}): ReactNode {
|
|
79
|
+
return <ChartContext.Provider value={value}>{children}</ChartContext.Provider>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
ChartProvider.displayName = "DelacourUI.ChartProvider";
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The chart a part belongs to.
|
|
86
|
+
*
|
|
87
|
+
* Throws outside one. A Skia mark that silently rendered nothing would leave a
|
|
88
|
+
* blank canvas with no error and no element inspector to look at, which is
|
|
89
|
+
* among the hardest things to diagnose in this package.
|
|
90
|
+
*/
|
|
91
|
+
export function useChart(): ChartContextValue {
|
|
92
|
+
const value = useContext(ChartContext);
|
|
93
|
+
if (value === null) {
|
|
94
|
+
throw new Error("[DelacourUI] a Chart part was used outside a <Chart>. Every part reads the chart from context.");
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The chart, or `null` outside one. */
|
|
100
|
+
export function useChartContext(): ChartContextValue | null {
|
|
101
|
+
return useContext(ChartContext);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** One series' resolved colour, or `undefined` when the key names no series. */
|
|
105
|
+
export function useSeriesColor(key: string | undefined): string | undefined {
|
|
106
|
+
const { series } = useChart();
|
|
107
|
+
if (key === undefined) return undefined;
|
|
108
|
+
return series.find((entry) => entry.key === key)?.color;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The series and slots of whichever chart a part is under.
|
|
113
|
+
*
|
|
114
|
+
* For a part both roots share — the legend — which reads the same two fields
|
|
115
|
+
* off a `Chart` or a `PieChart` and does not care which. Throws outside both.
|
|
116
|
+
*/
|
|
117
|
+
export function useChartSeries(): { series: readonly ChartResolvedSeries[]; slots: ChartSlots } {
|
|
118
|
+
const cartesian = useContext(ChartContext);
|
|
119
|
+
const pie = useContext(PieChartContext);
|
|
120
|
+
const value = cartesian ?? pie;
|
|
121
|
+
if (value === null) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"[DelacourUI] a chart part was used outside a <Chart> or a <PieChart>. Every part reads the chart from context."
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return { series: value.series, slots: value.slots };
|
|
127
|
+
}
|