@delacour/react-native-ui 0.1.0-alpha.3
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 +192 -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 +435 -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 +232 -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 +245 -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 +148 -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 +95 -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 +276 -0
- package/src/components/bottom-sheet/bottom-sheet.variants.ts +199 -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 +100 -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 +33 -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 +78 -0
- package/src/components/field/field.tsx +144 -0
- package/src/components/field/field.types.ts +15 -0
- package/src/components/field/field.variants.test.ts +216 -0
- package/src/components/field/field.variants.ts +136 -0
- package/src/components/field/index.ts +21 -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 +64 -0
- package/src/components/pressable/index.ts +9 -0
- package/src/components/pressable/pressable.tsx +256 -0
- package/src/components/pressable/pressable.variants.test.ts +99 -0
- package/src/components/pressable/pressable.variants.ts +63 -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 +247 -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 +581 -0
- package/src/components/radio/radio.variants.ts +272 -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 +48 -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 +161 -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 +804 -0
- package/src/components/slider/slider.variants.ts +607 -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 +286 -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 +672 -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,210 @@
|
|
|
1
|
+
# Chart
|
|
2
|
+
|
|
3
|
+
Skia charts wearing the theme's five-colour series ramp — the skin over
|
|
4
|
+
[`@delacour/react-native-charts`](../../../../charts/AGENTS.md), which does the drawing and
|
|
5
|
+
knows nothing about tokens.
|
|
6
|
+
|
|
7
|
+
`import { Chart, PieChart } from "@delacour/react-native-ui/chart";`
|
|
8
|
+
|
|
9
|
+
## Files
|
|
10
|
+
|
|
11
|
+
| File | What it holds |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `index.ts` | → `@delacour/react-native-ui/chart` |
|
|
14
|
+
| `chart.tsx` | The root, the child partition, and the single `Object.assign` |
|
|
15
|
+
| `chart.context.tsx` | The context, mounted twice — see below |
|
|
16
|
+
| `chart.types.ts` | `ChartConfig`, `ChartSeriesConfig`, `ChartResolvedSeries` |
|
|
17
|
+
| `chart.variants.ts` | The slotted `tv()`, the ramp, and every pure resolver |
|
|
18
|
+
| `chart.variants.test.ts` | Including the assertion that the ramp tokens exist |
|
|
19
|
+
| `use-chart-font.ts` | `--font-sans` into an `SkFont` |
|
|
20
|
+
| `use-chart-palette.ts` | The eight fixed `useThemeColor` slots a series list resolves through |
|
|
21
|
+
| `chart-grid.tsx`, `chart-line.tsx`, `chart-area.tsx` | Skia marks; `Chart.Area` takes a `stackId` |
|
|
22
|
+
| `chart-bar.tsx` | `Chart.Bar` — a spec the root collects, and a lone engine bar when reached unrouted |
|
|
23
|
+
| `chart-bars.tsx` | Internal `ChartBars`: every `Chart.Bar` drawn as one bar, group or stack, spliced in by the root |
|
|
24
|
+
| `chart-scatter.tsx`, `chart-candlestick.tsx` | Skia marks — dots per datum, and O/H/L/C candles in the sentiment tokens |
|
|
25
|
+
| `chart-x-axis.tsx`, `chart-y-axis.tsx` | Skia axis labels |
|
|
26
|
+
| `chart-legend.tsx`, `chart-tooltip.tsx` | React Native, around and over the canvas |
|
|
27
|
+
| `chart-tooltip-dot.tsx`, `chart-tooltip-x.tsx`, `chart-tooltip-y.tsx` | Skia cursor marks, named under the tooltip; `.X` is a band over bars |
|
|
28
|
+
| `pie-chart.tsx` | The second root: `PieChart`, its child partition, and its `Object.assign` |
|
|
29
|
+
| `pie-chart.context.tsx` | The pie's context, mounted twice the same way; `useChartSeries` in `chart.context.tsx` reads either |
|
|
30
|
+
| `pie-chart-slice.tsx`, `pie-chart-label.tsx` | Skia marks — the wedges with a hairline between them, and a label per wedge |
|
|
31
|
+
| `pie-chart-center.tsx`, `pie-chart-tooltip.tsx` | React Native, over the canvas — the donut hole's content and the tap readout |
|
|
32
|
+
|
|
33
|
+
## Design
|
|
34
|
+
|
|
35
|
+
- **Skia's `<Canvas>` is a second React reconciler, so every theme lookup
|
|
36
|
+
happens in the root, above it.** A hook called inside the canvas has no
|
|
37
|
+
Uniwind provider above it and resolves nothing. `ChartRoot` calls
|
|
38
|
+
`useThemeColor` for the series ramp, the grid and the axis labels, and the
|
|
39
|
+
marks receive resolved strings as plain props. This is the single most
|
|
40
|
+
important thing to know before editing anything in this folder.
|
|
41
|
+
|
|
42
|
+
- **The context is mounted twice, from one value.** Once around the React
|
|
43
|
+
Native subtree — legend and tooltip — and once *inside* the canvas around the
|
|
44
|
+
marks. Context resolves by which reconciler renders the provider node, so a
|
|
45
|
+
provider placed among the canvas' children lands in the Skia tree and marks
|
|
46
|
+
below it read it normally. That is why no context bridge and no `its-fine`
|
|
47
|
+
dependency is needed.
|
|
48
|
+
|
|
49
|
+
- **A Skia mark takes values, never a `className`.** There is nothing for
|
|
50
|
+
`cn()` to merge on a canvas node and nothing Uniwind could compile. Only the
|
|
51
|
+
root, the legend and the tooltip accept one. Rule 7's `withUniwind` carve-out
|
|
52
|
+
is already spent on `icon.tsx`, and this component does not need a second: it
|
|
53
|
+
sizes the canvas by sizing the `View` around it.
|
|
54
|
+
|
|
55
|
+
- **The `config` is the chart.** It names each series, sets the draw order, and
|
|
56
|
+
assigns the ramp by position, so a call site writes `yKey="desktop"` and
|
|
57
|
+
never a colour. The shape is shadcn's deliberately — rule 11's principle,
|
|
58
|
+
that a theme written for a web app moves across as a copy rather than a
|
|
59
|
+
translation, extends past token names to the component's own API.
|
|
60
|
+
|
|
61
|
+
- **The ramp stops at five, and cycles.** `--chart-1` … `--chart-5` are what
|
|
62
|
+
shadcn's palette declares. A sixth token would not survive a pasted theme: a
|
|
63
|
+
designer copying a tweakcn palette supplies five, so a six-series chart would
|
|
64
|
+
draw five themed lines and one stranger — and one wrong-coloured line reads
|
|
65
|
+
as a bug in a way a slightly-off surface does not.
|
|
66
|
+
|
|
67
|
+
- **At most eight *distinct* tokens per render, and it throws past that.**
|
|
68
|
+
`useThemeColor` is a hook and cannot run in a loop over a list whose length
|
|
69
|
+
changes, so `useChartPalette` resolves a fixed eight slots and
|
|
70
|
+
`partitionChartColors` dedupes the tokens and pads to reach them. The ramp
|
|
71
|
+
dedupes, so a pie of twenty ramp slices needs five. Literal colours bypass
|
|
72
|
+
the hook entirely, so a chart of twenty literal-coloured series is fine.
|
|
73
|
+
Eight is an opinion — a categorical legend stops being readable well before
|
|
74
|
+
it — not a limit of the technique.
|
|
75
|
+
|
|
76
|
+
- **No new colour token.** The grid is `border`, which is what `Separator`
|
|
77
|
+
already paints a rule between regions with; axis labels are
|
|
78
|
+
`muted-foreground`, the colour every secondary label here uses; the tooltip
|
|
79
|
+
is `popover` on `border`. Rule 11 would require a `--chart-grid` to be
|
|
80
|
+
derived from a shadcn token anyway, and every borrowed palette would leave it
|
|
81
|
+
on this package's default.
|
|
82
|
+
|
|
83
|
+
- **One new size token trio**, `--spacing-chart-sm/md/lg`. A canvas has no
|
|
84
|
+
intrinsic height, and without a token every chart on a dashboard would be a
|
|
85
|
+
different one. A height rather than `aspect-video` because two charts at
|
|
86
|
+
different widths must still line up — and registering the names with
|
|
87
|
+
tailwind-merge is what lets a caller's `className="aspect-video h-auto"`
|
|
88
|
+
cleanly win when they want that instead.
|
|
89
|
+
|
|
90
|
+
- **The tooltip is a React Native view even though it floats over the canvas.**
|
|
91
|
+
It wants `popover`, `border`, the radius scale and the type scale, none of
|
|
92
|
+
which exist in Skia, and it is the part a caller most wants to restyle —
|
|
93
|
+
which a className cannot do to a Skia rounded rect. Axis labels are the
|
|
94
|
+
opposite call and stay in Skia: they move with the plot under a scrub, and an
|
|
95
|
+
overlaid React Native label would lag them by a frame.
|
|
96
|
+
|
|
97
|
+
- **The tooltip's position rides the UI thread; its text does not.**
|
|
98
|
+
`useAnimatedStyle` follows the finger with no bridge hop, while the contents
|
|
99
|
+
update through `scheduleOnRN` only when the nearest datum index changes —
|
|
100
|
+
which is a few times per drag, not once per frame. Text on a shared value
|
|
101
|
+
would mean an animated `TextInput` with a `value` prop pretending to be a
|
|
102
|
+
label.
|
|
103
|
+
|
|
104
|
+
- **The cursor marks are named under `Chart.Tooltip` but placed beside it.**
|
|
105
|
+
`Chart.Tooltip.Dot`, `.X` and `.Y` are one feature with the readout, so they
|
|
106
|
+
share its namespace — but the readout is a React Native view over the canvas
|
|
107
|
+
and the marks are drawn inside it. Nesting them
|
|
108
|
+
(`<Chart.Tooltip><Chart.Tooltip.Dot /></Chart.Tooltip>`) would put a canvas
|
|
109
|
+
node in a view's tree and draw nothing, with no error. They are siblings, and
|
|
110
|
+
`partitionChildren` routes them to opposite sides of the boundary.
|
|
111
|
+
|
|
112
|
+
- **The marks snap to the datum, because the readout names one.** A rule
|
|
113
|
+
tracking the raw touch while the label beside it snaps says two different
|
|
114
|
+
things about one gesture. `snappedX` exists on the scrub for exactly this —
|
|
115
|
+
the crosshair, the dot and the number all read the same row. `glide` opts
|
|
116
|
+
into the continuous curve position, and the doc says what it costs.
|
|
117
|
+
|
|
118
|
+
- **A dot rings itself in the chart's background.** A dot in the series colour
|
|
119
|
+
sitting on a line of that same colour is a slight thickening and nothing
|
|
120
|
+
more; the ring is what makes it a knob. That is why `surfaceColor` is
|
|
121
|
+
resolved in the root and carried on the context.
|
|
122
|
+
|
|
123
|
+
- **Bars group by being siblings, and stack by sharing a `stackId`.** That
|
|
124
|
+
is shadcn's surface, and it is the reason `Chart.Bar` does not draw itself:
|
|
125
|
+
a bar's width depends on how many share its step, and its base on what is
|
|
126
|
+
stacked under it, so the root collects every `Chart.Bar`, resolves them with
|
|
127
|
+
`resolveBarLayout`, and mounts one internal `ChartBars` where the first bar
|
|
128
|
+
stood. A `Chart.Bar` wrapped in a caller's own component never reaches that
|
|
129
|
+
pass — it renders a single engine bar in its own colour, so it degrades to
|
|
130
|
+
"not grouped" rather than to nothing, but it cannot group or stack.
|
|
131
|
+
|
|
132
|
+
- **One stack per chart.** Two `stackId`s, a stack beside a loose bar, or an
|
|
133
|
+
area stack beside a bar stack all throw by name. The engine's `stackKeys` is
|
|
134
|
+
one ordered list because the y domain has to cover one set of running
|
|
135
|
+
totals; a second stack would need a group of stacks, which nothing here
|
|
136
|
+
draws yet. Throwing beats drawing something that looks stacked and is not.
|
|
137
|
+
|
|
138
|
+
- **Bar corners follow `--radius`.** `--radius` × a per-size multiplier, the
|
|
139
|
+
way `Checkbox`'s fill does, so a theme that squares its buttons squares its
|
|
140
|
+
bars. Only `--radius` survives to runtime — the scale is `@theme inline` —
|
|
141
|
+
so `BAR_RADIUS_MULTIPLIER` restates the steps and a test pins it against
|
|
142
|
+
`tokens.css`. A stack rounds only its outermost segment, so a column reads
|
|
143
|
+
as one bar rather than a pile of pills. `rounded={false}` is square.
|
|
144
|
+
|
|
145
|
+
- **Candle sentiment is the top of the ramp.** A rising candle is `chart-1`
|
|
146
|
+
and a falling one `chart-2`, so a candlestick is painted from the same five
|
|
147
|
+
colours as every other chart and a pasted palette recolours it with them.
|
|
148
|
+
`success` and `destructive` were tried first and made the one chart on a
|
|
149
|
+
dashboard that ignored the ramp; a flat candle is `muted-foreground`. The root
|
|
150
|
+
resolves the three with three fixed hook calls, which is why the part takes
|
|
151
|
+
only literal overrides — a token given to a mark inside the canvas has
|
|
152
|
+
nothing to resolve it. The config names the close field alone, so the
|
|
153
|
+
legend says one price; the tooltip prints O/H/L/C, swatched by sentiment.
|
|
154
|
+
|
|
155
|
+
- **The band is `Chart.Tooltip.X`, not a new part.** Over bars or candles the
|
|
156
|
+
rule becomes a wash one `xStep` wide, snapped to the datum, at a twelfth of
|
|
157
|
+
the axis colour. It is the same question — where along x are you — with an
|
|
158
|
+
answer sized to the mark: a hairline through the middle of a bar says less
|
|
159
|
+
than the whole column lit. `band` flips it either way. A cursor dot skips a
|
|
160
|
+
series that is only a bar, because a bar has no curve to sit on and the band
|
|
161
|
+
already picks out its column.
|
|
162
|
+
|
|
163
|
+
- **Parts must be direct children of `<Chart>`.** The root partitions them by
|
|
164
|
+
component identity to decide what goes inside the canvas, what floats over
|
|
165
|
+
it and what sits under it — the technique `Spinner` uses on its own children.
|
|
166
|
+
An array from `.map()` is fine, since `Children.toArray` flattens it; a mark
|
|
167
|
+
wrapped in a caller's own component is not, because there is nothing to match
|
|
168
|
+
until it renders.
|
|
169
|
+
|
|
170
|
+
- **Pie is a second root, not a part.** A pie shares nothing with a cartesian
|
|
171
|
+
plot on screen — no axes, no grid, no scrub, no x — and only the palette,
|
|
172
|
+
the legend and the font underneath, so `<Chart><Chart.Pie /></Chart>` would
|
|
173
|
+
be a root whose every other part throws under it. `PieChart` reuses
|
|
174
|
+
`chartVariants` for its frame heights, `useChartPalette` for its colours and
|
|
175
|
+
`ChartLegend` verbatim, through `useChartSeries`, which reads whichever
|
|
176
|
+
context is above it. Its categories are its **rows**: the ramp walks the
|
|
177
|
+
data, and the `config` is an optional override keyed by a row's name.
|
|
178
|
+
|
|
179
|
+
- **A pie's colours dedupe before the hook.** Twenty slices walk the
|
|
180
|
+
five-token ramp, and `partitionChartColors` collects *distinct* tokens, so
|
|
181
|
+
that costs five of the eight slots rather than throwing at the ninth slice.
|
|
182
|
+
Rows are dropped for a negative or unreadable value **before** the engine
|
|
183
|
+
sees them, so the series, the values, the engine's slices and the legend all
|
|
184
|
+
agree on an index — a selection reports a position in the drawn order.
|
|
185
|
+
|
|
186
|
+
- **`PieChart.Center` is a React Native view because it wants the type
|
|
187
|
+
scale.** What sits in a donut's hole is a headline number and a caption,
|
|
188
|
+
which want `text-2xl`, `foreground`, `muted-foreground` and a className —
|
|
189
|
+
none of which a Skia `<Text>` has. It centres over the frame, which is the
|
|
190
|
+
circle's centre because the root gives the engine no padding. It is named
|
|
191
|
+
`Center` rather than `Inset` because the engine's `PieInset` is the hairline
|
|
192
|
+
between slices, and the two would otherwise share a name for two things.
|
|
193
|
+
|
|
194
|
+
- **The pie tooltip is selection-driven, not scrub-driven.** There is no x to
|
|
195
|
+
scrub along, so a tap selects a slice, the others dim, and the readout sits
|
|
196
|
+
just outside that slice's outer edge on its bisector — the position a
|
|
197
|
+
callout would point at. It needs no shared values: the selection changes a
|
|
198
|
+
few times a session, not once a frame. The slice geometry it reads is
|
|
199
|
+
resolved a second time in the root, with the engine's own pure functions on
|
|
200
|
+
the same frame, because the engine's context stops at the canvas boundary
|
|
201
|
+
and the tooltip lives outside it. A tap selects only when something would
|
|
202
|
+
show it — a `PieChart.Tooltip`, an `onSelect`, or a controlled
|
|
203
|
+
`selectedIndex` — so a pie with none of those takes no gesture at all.
|
|
204
|
+
|
|
205
|
+
- **`@delacour/react-native-charts` is an optional peer, not a dependency.** A dependency
|
|
206
|
+
may be nested, and two copies of the engine mean two chart contexts — so a
|
|
207
|
+
correctly-nested `<Chart.Line>` would throw "must be used inside a `<Chart>`"
|
|
208
|
+
from inside a `<Chart>`. It also peer-depends on Skia, so a dependency would
|
|
209
|
+
install a native module for someone who only ever imported
|
|
210
|
+
`@delacour/react-native-ui/button`.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ChartArea as EngineArea, useChartContext as useEngineChart } from "@delacour/react-native-charts";
|
|
2
|
+
import type { CurveType } from "@delacour/react-native-charts/core";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
import { useSeriesColor } from "./chart.context";
|
|
5
|
+
import { resolveAreaFill } from "./chart.variants";
|
|
6
|
+
|
|
7
|
+
export type ChartAreaProps = {
|
|
8
|
+
yKey: string;
|
|
9
|
+
/**
|
|
10
|
+
* Stack this area on every sibling area that names the same id.
|
|
11
|
+
*
|
|
12
|
+
* A stacked area is a band from the top of the series below it to its
|
|
13
|
+
* own running total, so the topmost edge reads as the sum. One stack per
|
|
14
|
+
* chart, shared with the bars: a second id, or an area stack beside a bar
|
|
15
|
+
* stack, throws by name.
|
|
16
|
+
*/
|
|
17
|
+
stackId?: string;
|
|
18
|
+
color?: string;
|
|
19
|
+
curve?: CurveType;
|
|
20
|
+
connectMissingData?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Fade the fill out towards the baseline instead of painting it flat.
|
|
23
|
+
*
|
|
24
|
+
* On by default for a lone area: a flat fill at any readable opacity
|
|
25
|
+
* competes with the line it sits under. Off by default for a stacked band,
|
|
26
|
+
* which has to read as a distinct region — see `resolveAreaFill`.
|
|
27
|
+
*/
|
|
28
|
+
gradient?: boolean;
|
|
29
|
+
/** Opacity of the fill — at its top when it fades, throughout when flat. */
|
|
30
|
+
opacity?: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The region under a series, in that series' colour.
|
|
35
|
+
*
|
|
36
|
+
* The fade is built from the resolved colour rather than from an opacity prop
|
|
37
|
+
* on the whole mark, so the top of the fill keeps the series' hue at reduced
|
|
38
|
+
* strength and the bottom reaches genuine transparency. Fading the node instead
|
|
39
|
+
* would leave a visible flat edge along the baseline.
|
|
40
|
+
*
|
|
41
|
+
* A stacked area reads its segments from the engine's context — the root
|
|
42
|
+
* stacked them in data space so the y domain covers the totals — and draws
|
|
43
|
+
* the band between each segment's base and its top. It is placed inside the
|
|
44
|
+
* canvas, which is why the engine's own context is in reach here. A band is
|
|
45
|
+
* painted flat: three fades over the full plot height blur into one wash,
|
|
46
|
+
* and nothing then says where one series ends and the next begins.
|
|
47
|
+
*/
|
|
48
|
+
export function ChartArea({
|
|
49
|
+
yKey,
|
|
50
|
+
stackId,
|
|
51
|
+
color,
|
|
52
|
+
curve,
|
|
53
|
+
connectMissingData,
|
|
54
|
+
gradient: gradientProp,
|
|
55
|
+
opacity: opacityProp,
|
|
56
|
+
}: ChartAreaProps): ReactElement | null {
|
|
57
|
+
const resolved = useSeriesColor(yKey);
|
|
58
|
+
const { stacked } = useEngineChart();
|
|
59
|
+
const paint = color ?? resolved;
|
|
60
|
+
if (paint === undefined) return null;
|
|
61
|
+
const segments = stackId === undefined ? undefined : stacked[yKey];
|
|
62
|
+
const { gradient, opacity } = resolveAreaFill({
|
|
63
|
+
stacked: stackId !== undefined,
|
|
64
|
+
gradient: gradientProp,
|
|
65
|
+
opacity: opacityProp,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!gradient) {
|
|
69
|
+
return (
|
|
70
|
+
<EngineArea
|
|
71
|
+
color={paint}
|
|
72
|
+
connectMissingData={connectMissingData}
|
|
73
|
+
curve={curve}
|
|
74
|
+
opacity={opacity}
|
|
75
|
+
segments={segments}
|
|
76
|
+
yKey={yKey}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<EngineArea
|
|
83
|
+
connectMissingData={connectMissingData}
|
|
84
|
+
curve={curve}
|
|
85
|
+
gradient={[paint, "transparent"]}
|
|
86
|
+
opacity={opacity}
|
|
87
|
+
segments={segments}
|
|
88
|
+
yKey={yKey}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ChartArea.displayName = "DelacourUI.Chart.Area";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type ChartBarLabels, ChartBar as EngineBar } from "@delacour/react-native-charts";
|
|
2
|
+
import type { ChartPoint, CornerRadii } from "@delacour/react-native-charts/core";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
import { useChart, useSeriesColor } from "./chart.context";
|
|
5
|
+
import type { ChartDatum } from "./chart.types";
|
|
6
|
+
|
|
7
|
+
export type ChartBarProps = {
|
|
8
|
+
/** Which series to draw. Names a key of the chart's `config`. */
|
|
9
|
+
yKey: string;
|
|
10
|
+
/**
|
|
11
|
+
* Stack this bar on every sibling that names the same id.
|
|
12
|
+
*
|
|
13
|
+
* Sibling bars without one sit side by side within each step. One stack
|
|
14
|
+
* per chart: a second id, or a stack beside a loose bar, throws by name.
|
|
15
|
+
*/
|
|
16
|
+
stackId?: string;
|
|
17
|
+
/** Overrides the series' colour from the config. A literal — the canvas can resolve no token. */
|
|
18
|
+
color?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Round the value end. On by default, following `--radius` at the chart's
|
|
21
|
+
* size; `false` is square and a number is an exact radius in points.
|
|
22
|
+
*/
|
|
23
|
+
rounded?: boolean | number;
|
|
24
|
+
/**
|
|
25
|
+
* Print each value against its bar.
|
|
26
|
+
*
|
|
27
|
+
* `true` prints the value as written; a function receives the value and
|
|
28
|
+
* its row and returns the text, `""` to skip that bar. Drawn in the axis
|
|
29
|
+
* colour with the axis font. A grouped or stacked chart draws no labels —
|
|
30
|
+
* there is no room between the bars for them to read.
|
|
31
|
+
*/
|
|
32
|
+
labels?: boolean | ((value: number, row: ChartDatum) => string);
|
|
33
|
+
opacity?: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One bar per datum for one series, standing on zero.
|
|
38
|
+
*
|
|
39
|
+
* Placed as a direct child of `<Chart>`, it never renders itself: the root
|
|
40
|
+
* collects every `Chart.Bar`, lays them out together — grouped by being
|
|
41
|
+
* siblings, stacked by `stackId` — and draws them through one internal
|
|
42
|
+
* `Chart.Bars` in the first bar's place. That is what lets two bars know
|
|
43
|
+
* they share a step before either has rendered.
|
|
44
|
+
*
|
|
45
|
+
* Reached any other way — wrapped in a caller's own component, say — it
|
|
46
|
+
* draws a single engine bar in its series' colour. It degrades to "not
|
|
47
|
+
* grouped" rather than to nothing, but it cannot know its siblings, so a
|
|
48
|
+
* wrapped bar never groups or stacks. See AGENTS.md.
|
|
49
|
+
*/
|
|
50
|
+
export function ChartBar({ yKey, color, rounded, labels, opacity }: ChartBarProps): ReactElement | null {
|
|
51
|
+
const { barRadius, axisColor, data } = useChart();
|
|
52
|
+
const resolved = useSeriesColor(yKey);
|
|
53
|
+
const paint = color ?? resolved;
|
|
54
|
+
if (paint === undefined) return null;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<EngineBar
|
|
58
|
+
color={paint}
|
|
59
|
+
labels={resolveBarLabels(labels, axisColor, data)}
|
|
60
|
+
opacity={opacity}
|
|
61
|
+
roundedCorners={barCorners(barRadiusFor(rounded, barRadius))}
|
|
62
|
+
yKey={yKey}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ChartBar.displayName = "DelacourUI.Chart.Bar";
|
|
68
|
+
|
|
69
|
+
/** The value end's two corners at one radius. The base stays square. */
|
|
70
|
+
export function barCorners(radius: number): CornerRadii {
|
|
71
|
+
return { topLeft: radius, topRight: radius };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A bar's own `rounded` against the chart's resolved radius.
|
|
76
|
+
*
|
|
77
|
+
* The context already holds `--radius` × the size multiplier, so this only
|
|
78
|
+
* has to honour `false` and a number. `resolveBarRadius` does the same in
|
|
79
|
+
* the root, where `--radius` is readable; this runs where a part is not.
|
|
80
|
+
*/
|
|
81
|
+
export function barRadiusFor(rounded: boolean | number | undefined, barRadius: number): number {
|
|
82
|
+
if (rounded === false) return 0;
|
|
83
|
+
if (typeof rounded === "number") return Number.isFinite(rounded) ? Math.max(0, rounded) : 0;
|
|
84
|
+
return barRadius;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The engine's label options for a `labels` prop, or nothing. */
|
|
88
|
+
export function resolveBarLabels(
|
|
89
|
+
labels: ChartBarProps["labels"],
|
|
90
|
+
color: string | undefined,
|
|
91
|
+
data: readonly ChartDatum[]
|
|
92
|
+
): ChartBarLabels | undefined {
|
|
93
|
+
if (labels === undefined || labels === false || color === undefined) return undefined;
|
|
94
|
+
if (labels === true) return { color };
|
|
95
|
+
const format = labels;
|
|
96
|
+
return {
|
|
97
|
+
color,
|
|
98
|
+
formatLabel: (point: ChartPoint, index: number): string => {
|
|
99
|
+
const row = data[index];
|
|
100
|
+
return point.yValue === null || row === undefined ? "" : format(point.yValue, row);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ChartBarStackSegmentInfo,
|
|
3
|
+
type ChartBarStackSegmentOptions,
|
|
4
|
+
ChartBar as EngineBar,
|
|
5
|
+
ChartBarGroup as EngineBarGroup,
|
|
6
|
+
ChartBarStack as EngineBarStack,
|
|
7
|
+
} from "@delacour/react-native-charts";
|
|
8
|
+
import { type ReactElement, useCallback, useMemo } from "react";
|
|
9
|
+
import { useChart } from "./chart.context";
|
|
10
|
+
import { barCorners, barRadiusFor, type ChartBarProps, resolveBarLabels } from "./chart-bar";
|
|
11
|
+
|
|
12
|
+
export type ChartBarsProps = {
|
|
13
|
+
/** Every `Chart.Bar`'s props, in placement order. */
|
|
14
|
+
readonly bars: readonly ChartBarProps[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Every `Chart.Bar` of the chart, drawn as one arrangement.
|
|
19
|
+
*
|
|
20
|
+
* Internal. The root collects the `Chart.Bar` children, resolves them with
|
|
21
|
+
* `resolveBarLayout`, and mounts one of these where the first bar stood —
|
|
22
|
+
* a single engine bar, a group or a stack, depending on what it found. A
|
|
23
|
+
* bar cannot draw itself because it cannot see its siblings, and the width
|
|
24
|
+
* of every bar depends on how many share a step.
|
|
25
|
+
*
|
|
26
|
+
* Colours come from each bar's series; `rounded` and `opacity` are read
|
|
27
|
+
* from the first bar that sets them, because a group is one path per series
|
|
28
|
+
* but one radius for all — corners that differed across a group would read
|
|
29
|
+
* as a mistake. A stack rounds only its outermost segment, so the column
|
|
30
|
+
* reads as one bar; every inner segment is square where it meets the next.
|
|
31
|
+
*/
|
|
32
|
+
export function ChartBars({ bars }: ChartBarsProps): ReactElement | null {
|
|
33
|
+
const { bars: layout, series, barRadius, axisColor, data } = useChart();
|
|
34
|
+
|
|
35
|
+
const radius = barRadiusFor(bars.find((bar) => bar.rounded !== undefined)?.rounded, barRadius);
|
|
36
|
+
const corners = useMemo(() => barCorners(radius), [radius]);
|
|
37
|
+
const opacity = bars.find((bar) => bar.opacity !== undefined)?.opacity;
|
|
38
|
+
|
|
39
|
+
const colors = useMemo(
|
|
40
|
+
() =>
|
|
41
|
+
layout.keys.map((key) => {
|
|
42
|
+
const bar = bars.find((entry) => entry.yKey === key);
|
|
43
|
+
return bar?.color ?? series.find((entry) => entry.key === key)?.color ?? "transparent";
|
|
44
|
+
}),
|
|
45
|
+
[layout.keys, bars, series]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const barOptions = useCallback(
|
|
49
|
+
(info: ChartBarStackSegmentInfo): ChartBarStackSegmentOptions => (info.isTop ? { roundedCorners: corners } : {}),
|
|
50
|
+
[corners]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const first = bars[0];
|
|
54
|
+
if (first === undefined || layout.mode === "none") return null;
|
|
55
|
+
|
|
56
|
+
if (layout.mode === "single") {
|
|
57
|
+
return (
|
|
58
|
+
<EngineBar
|
|
59
|
+
color={colors[0]}
|
|
60
|
+
labels={resolveBarLabels(first.labels, axisColor, data)}
|
|
61
|
+
opacity={opacity}
|
|
62
|
+
roundedCorners={corners}
|
|
63
|
+
yKey={first.yKey}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (layout.mode === "grouped") {
|
|
69
|
+
return <EngineBarGroup colors={colors} opacity={opacity} roundedCorners={corners} yKeys={layout.keys} />;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return <EngineBarStack barOptions={barOptions} colors={colors} opacity={opacity} yKeys={layout.keys} />;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ChartBars.displayName = "DelacourUI.Chart.Bars";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ChartCandlestick as EngineCandlestick } from "@delacour/react-native-charts";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
import { useChart } from "./chart.context";
|
|
4
|
+
import type { ChartCandleColors, ChartCandlestickKeys } from "./chart.types";
|
|
5
|
+
|
|
6
|
+
export type ChartCandlestickProps = {
|
|
7
|
+
/** The row field holding each price. Default to the conventional names. */
|
|
8
|
+
open?: string;
|
|
9
|
+
high?: string;
|
|
10
|
+
low?: string;
|
|
11
|
+
close?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Overrides the sentiment colours. Literals only.
|
|
14
|
+
*
|
|
15
|
+
* The defaults are `chart-1`, `chart-2` and `muted-foreground`,
|
|
16
|
+
* resolved in the root; a token here would reach the canvas unresolved.
|
|
17
|
+
*/
|
|
18
|
+
colors?: Partial<ChartCandleColors>;
|
|
19
|
+
/** The fraction of each step a body fills. Defaults to 0.6. */
|
|
20
|
+
bodyWidth?: number;
|
|
21
|
+
/** The wick's stroke width in points. Defaults to 1. */
|
|
22
|
+
wickWidth?: number;
|
|
23
|
+
opacity?: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Open-high-low-close candles, coloured by whether each closed up or down.
|
|
28
|
+
*
|
|
29
|
+
* The chart's `config` names the close field alone, so the legend and the
|
|
30
|
+
* tooltip name one price rather than four; the root reads the other three
|
|
31
|
+
* fields off this part and plots them for the engine. A rising candle is
|
|
32
|
+
* `chart-1` and a falling one `chart-2` — the top of the same ramp every
|
|
33
|
+
* other chart wears — and a flat candle is `muted-foreground`.
|
|
34
|
+
*/
|
|
35
|
+
export function ChartCandlestick(props: ChartCandlestickProps): ReactElement {
|
|
36
|
+
const { candleColors } = useChart();
|
|
37
|
+
const { colors, bodyWidth, wickWidth, opacity } = props;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<EngineCandlestick
|
|
41
|
+
candleColors={{ ...candleColors, ...colors }}
|
|
42
|
+
candleRatio={bodyWidth}
|
|
43
|
+
keys={candlestickKeysOf(props)}
|
|
44
|
+
opacity={opacity}
|
|
45
|
+
wickStrokeWidth={wickWidth}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ChartCandlestick.displayName = "DelacourUI.Chart.Candlestick";
|
|
51
|
+
|
|
52
|
+
/** The four field names, with the conventional defaults filled in. */
|
|
53
|
+
export function candlestickKeysOf(props: ChartCandlestickProps): ChartCandlestickKeys {
|
|
54
|
+
return {
|
|
55
|
+
open: props.open ?? "open",
|
|
56
|
+
high: props.high ?? "high",
|
|
57
|
+
low: props.low ?? "low",
|
|
58
|
+
close: props.close ?? "close",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ChartGrid as EngineGrid } from "@delacour/react-native-charts";
|
|
2
|
+
import type { GridAxis } from "@delacour/react-native-charts/core";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
import { useChart } from "./chart.context";
|
|
5
|
+
|
|
6
|
+
export type ChartGridProps = {
|
|
7
|
+
/** Which ticks get a rule. Defaults to the y ticks alone. */
|
|
8
|
+
axis?: GridAxis;
|
|
9
|
+
/** Overrides the `border` token this paints with. */
|
|
10
|
+
color?: string;
|
|
11
|
+
lineWidth?: number;
|
|
12
|
+
opacity?: number;
|
|
13
|
+
/** Dash on/off lengths, in points. */
|
|
14
|
+
dash?: readonly [number, number];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A hairline rule at each tick.
|
|
19
|
+
*
|
|
20
|
+
* Painted with `border`, not a chart token of its own. A gridline is a rule
|
|
21
|
+
* between regions, which is what `Separator` already uses `border` for — and
|
|
22
|
+
* inventing a `--chart-grid` would be a token no pasted shadcn theme supplies,
|
|
23
|
+
* so every borrowed palette would leave the grid on this package's default.
|
|
24
|
+
*/
|
|
25
|
+
export function ChartGrid({ axis, color, lineWidth, opacity, dash }: ChartGridProps): ReactElement | null {
|
|
26
|
+
const { gridColor } = useChart();
|
|
27
|
+
const paint = color ?? gridColor;
|
|
28
|
+
if (paint === undefined) return null;
|
|
29
|
+
|
|
30
|
+
return <EngineGrid axis={axis} color={paint} dash={dash} lineWidth={lineWidth} opacity={opacity} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
ChartGrid.displayName = "DelacourUI.Chart.Grid";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Text } from "../text";
|
|
5
|
+
import { useChartSeries } from "./chart.context";
|
|
6
|
+
|
|
7
|
+
export type ChartLegendProps = {
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Show only these series, in this order. Defaults to all of them. */
|
|
10
|
+
keys?: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A swatch and a label per series, under the chart.
|
|
15
|
+
*
|
|
16
|
+
* Shared by `Chart` and `PieChart`: it reads whichever context is above it,
|
|
17
|
+
* since a legend row is a colour and a name under either root.
|
|
18
|
+
*
|
|
19
|
+
* A React Native view, not a Skia node, and the distinction is whether it
|
|
20
|
+
* moves *with* the canvas. A legend sits beside the plot and is static
|
|
21
|
+
* relative to it, so it can have the type scale, a colour token and a
|
|
22
|
+
* className — none of which exist on a canvas. Axis labels are the opposite
|
|
23
|
+
* case: they move under a pan and belong in Skia.
|
|
24
|
+
*/
|
|
25
|
+
export function ChartLegend({ className, keys }: ChartLegendProps): ReactElement {
|
|
26
|
+
const { series, slots } = useChartSeries();
|
|
27
|
+
const shown = keys === undefined ? series : keys.flatMap((key) => series.filter((entry) => entry.key === key));
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<View className={cn(slots.legend(), className)}>
|
|
31
|
+
{shown.map((entry) => (
|
|
32
|
+
<View className={slots.legendItem()} key={entry.key}>
|
|
33
|
+
<View className={slots.legendSwatch()} style={{ backgroundColor: entry.color }} />
|
|
34
|
+
<Text className={slots.legendLabel()}>{entry.label}</Text>
|
|
35
|
+
</View>
|
|
36
|
+
))}
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ChartLegend.displayName = "DelacourUI.Chart.Legend";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ChartLine as EngineLine } from "@delacour/react-native-charts";
|
|
2
|
+
import type { CurveType } from "@delacour/react-native-charts/core";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
import { useSeriesColor } from "./chart.context";
|
|
5
|
+
|
|
6
|
+
export type ChartLineProps = {
|
|
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 token or a literal. */
|
|
10
|
+
color?: string;
|
|
11
|
+
strokeWidth?: number;
|
|
12
|
+
curve?: CurveType;
|
|
13
|
+
/** Draw straight through a gap instead of breaking the line. */
|
|
14
|
+
connectMissingData?: boolean;
|
|
15
|
+
opacity?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A stroked line through one series, in that series' colour.
|
|
20
|
+
*
|
|
21
|
+
* A Skia mark, so it takes values and not classNames — there is nothing for
|
|
22
|
+
* `cn()` to merge on a canvas node and nothing Uniwind could compile. The
|
|
23
|
+
* colour comes from the chart's `config` by key, so a call site names the data
|
|
24
|
+
* and never the paint.
|
|
25
|
+
*/
|
|
26
|
+
export function ChartLine({
|
|
27
|
+
yKey,
|
|
28
|
+
color,
|
|
29
|
+
strokeWidth = 2,
|
|
30
|
+
curve,
|
|
31
|
+
connectMissingData,
|
|
32
|
+
opacity,
|
|
33
|
+
}: ChartLineProps): ReactElement | null {
|
|
34
|
+
const resolved = useSeriesColor(yKey);
|
|
35
|
+
const paint = color ?? resolved;
|
|
36
|
+
if (paint === undefined) return null;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<EngineLine
|
|
40
|
+
color={paint}
|
|
41
|
+
connectMissingData={connectMissingData}
|
|
42
|
+
curve={curve}
|
|
43
|
+
opacity={opacity}
|
|
44
|
+
strokeWidth={strokeWidth}
|
|
45
|
+
yKey={yKey}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
ChartLine.displayName = "DelacourUI.Chart.Line";
|