panelui-native 0.62.0 → 0.65.0
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 +5 -0
- package/lib/module/components/area-chart/index.js +14 -4
- package/lib/module/components/area-chart/index.js.map +1 -1
- package/lib/module/components/badge/index.js +1 -1
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +53 -6
- package/lib/module/components/bar-chart/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +2 -2
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +19 -9
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/candlestick-chart/index.js +14 -4
- package/lib/module/components/candlestick-chart/index.js.map +1 -1
- package/lib/module/components/card/index.js +26 -1
- package/lib/module/components/card/index.js.map +1 -1
- package/lib/module/components/carousel/index.js +28 -14
- package/lib/module/components/carousel/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +2 -3
- package/lib/module/components/checkbox/index.js.map +1 -1
- package/lib/module/components/dialog/index.js +51 -5
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/drawer/index.js +2 -2
- package/lib/module/components/drawer/index.js.map +1 -1
- package/lib/module/components/fab/index.js +4 -3
- package/lib/module/components/fab/index.js.map +1 -1
- package/lib/module/components/field/index.js +41 -6
- package/lib/module/components/field/index.js.map +1 -1
- package/lib/module/components/flow/flow-accessibility.js +84 -0
- package/lib/module/components/flow/flow-accessibility.js.map +1 -0
- package/lib/module/components/flow/index.js +100 -39
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/form/use-form.js +93 -66
- package/lib/module/components/form/use-form.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +2 -2
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/hex-chart/index.js +4 -4
- package/lib/module/components/hex-chart/index.js.map +1 -1
- package/lib/module/components/kpi/index.js +3 -1
- package/lib/module/components/kpi/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +17 -7
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/map/index.js +71 -8
- package/lib/module/components/map/index.js.map +1 -1
- package/lib/module/components/map/map-accessibility.js +20 -0
- package/lib/module/components/map/map-accessibility.js.map +1 -0
- package/lib/module/components/map/maplibre.js.map +1 -1
- package/lib/module/components/menu/index.js +4 -1
- package/lib/module/components/menu/index.js.map +1 -1
- package/lib/module/components/number-input/index.js +1 -23
- package/lib/module/components/number-input/index.js.map +1 -1
- package/lib/module/components/number-input/number-input-math.js +44 -0
- package/lib/module/components/number-input/number-input-math.js.map +1 -0
- package/lib/module/components/pie-chart/index.js +2 -2
- package/lib/module/components/pie-chart/index.js.map +1 -1
- package/lib/module/components/plot/index.js +1329 -0
- package/lib/module/components/plot/index.js.map +1 -0
- package/lib/module/components/polar-area-chart/index.js +2 -2
- package/lib/module/components/polar-area-chart/index.js.map +1 -1
- package/lib/module/components/radar-chart/index.js +14 -4
- package/lib/module/components/radar-chart/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +1 -2
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/ring-chart/index.js +2 -2
- package/lib/module/components/ring-chart/index.js.map +1 -1
- package/lib/module/components/section-rail/index.js +120 -15
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/selection-mode/index.js +129 -18
- package/lib/module/components/selection-mode/index.js.map +1 -1
- package/lib/module/components/sortable/index.js +20 -60
- package/lib/module/components/sortable/index.js.map +1 -1
- package/lib/module/components/sortable/reorder.js +63 -0
- package/lib/module/components/sortable/reorder.js.map +1 -0
- package/lib/module/components/swipe/index.js +4 -4
- package/lib/module/components/swipe/index.js.map +1 -1
- package/lib/module/components/switch/index.js +36 -3
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/time-picker/accessibility.js +21 -0
- package/lib/module/components/time-picker/accessibility.js.map +1 -0
- package/lib/module/components/time-picker/index.js +34 -8
- package/lib/module/components/time-picker/index.js.map +1 -1
- package/lib/module/components/waterfall-chart/index.js +1325 -0
- package/lib/module/components/waterfall-chart/index.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/modal-isolation-store.js +29 -0
- package/lib/module/primitives/modal-isolation-store.js.map +1 -0
- package/lib/module/primitives/portal.js +36 -4
- package/lib/module/primitives/portal.js.map +1 -1
- package/lib/module/providers/panel-ui-provider.js +27 -6
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/candlestick-chart/index.d.ts +6 -3
- package/lib/typescript/src/components/candlestick-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/card/index.d.ts +48 -1
- package/lib/typescript/src/components/card/index.d.ts.map +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts +1 -1
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/dialog/index.d.ts +47 -2
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
- package/lib/typescript/src/components/field/index.d.ts +8 -1
- package/lib/typescript/src/components/field/index.d.ts.map +1 -1
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts +49 -0
- package/lib/typescript/src/components/flow/flow-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/flow/index.d.ts +14 -2
- package/lib/typescript/src/components/flow/index.d.ts.map +1 -1
- package/lib/typescript/src/components/form/use-form.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/kpi/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/index.d.ts +13 -5
- package/lib/typescript/src/components/map/index.d.ts.map +1 -1
- package/lib/typescript/src/components/map/map-accessibility.d.ts +19 -0
- package/lib/typescript/src/components/map/map-accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +2 -0
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -1
- package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/number-input/number-input-math.d.ts +9 -0
- package/lib/typescript/src/components/number-input/number-input-math.d.ts.map +1 -0
- package/lib/typescript/src/components/plot/index.d.ts +471 -0
- package/lib/typescript/src/components/plot/index.d.ts.map +1 -0
- package/lib/typescript/src/components/radar-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/section-rail/index.d.ts +8 -1
- package/lib/typescript/src/components/section-rail/index.d.ts.map +1 -1
- package/lib/typescript/src/components/selection-mode/index.d.ts +82 -6
- package/lib/typescript/src/components/selection-mode/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
- package/lib/typescript/src/components/sortable/reorder.d.ts +14 -0
- package/lib/typescript/src/components/sortable/reorder.d.ts.map +1 -0
- package/lib/typescript/src/components/swipe/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts +3 -3
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/time-picker/accessibility.d.ts +10 -0
- package/lib/typescript/src/components/time-picker/accessibility.d.ts.map +1 -0
- package/lib/typescript/src/components/time-picker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/waterfall-chart/index.d.ts +379 -0
- package/lib/typescript/src/components/waterfall-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts +10 -0
- package/lib/typescript/src/primitives/modal-isolation-store.d.ts.map +1 -0
- package/lib/typescript/src/primitives/portal.d.ts +6 -0
- package/lib/typescript/src/primitives/portal.d.ts.map +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/components/area-chart/index.tsx +14 -4
- package/src/components/badge/index.tsx +4 -1
- package/src/components/bar-chart/index.tsx +54 -7
- package/src/components/bottom-sheet/index.tsx +3 -3
- package/src/components/button/index.tsx +15 -21
- package/src/components/candlestick-chart/index.tsx +20 -7
- package/src/components/card/index.tsx +32 -7
- package/src/components/carousel/index.tsx +42 -20
- package/src/components/checkbox/index.tsx +1 -2
- package/src/components/dialog/index.tsx +58 -5
- package/src/components/drawer/index.tsx +3 -3
- package/src/components/fab/index.tsx +8 -12
- package/src/components/field/index.tsx +52 -10
- package/src/components/flow/flow-accessibility.ts +109 -0
- package/src/components/flow/index.tsx +162 -50
- package/src/components/form/use-form.ts +101 -65
- package/src/components/heatmap-chart/index.tsx +2 -3
- package/src/components/hex-chart/index.tsx +4 -5
- package/src/components/kpi/index.tsx +3 -1
- package/src/components/line-chart/index.tsx +17 -6
- package/src/components/map/index.tsx +110 -7
- package/src/components/map/map-accessibility.ts +41 -0
- package/src/components/map/maplibre.ts +2 -0
- package/src/components/menu/index.tsx +4 -1
- package/src/components/number-input/index.tsx +1 -26
- package/src/components/number-input/number-input-math.ts +50 -0
- package/src/components/pie-chart/index.tsx +2 -2
- package/src/components/plot/index.tsx +1565 -0
- package/src/components/polar-area-chart/index.tsx +2 -2
- package/src/components/radar-chart/index.tsx +14 -4
- package/src/components/radio-group/index.tsx +1 -2
- package/src/components/ring-chart/index.tsx +2 -2
- package/src/components/section-rail/index.tsx +133 -14
- package/src/components/selection-mode/index.tsx +163 -26
- package/src/components/sortable/index.tsx +27 -69
- package/src/components/sortable/reorder.ts +88 -0
- package/src/components/swipe/index.tsx +7 -4
- package/src/components/switch/index.tsx +42 -6
- package/src/components/time-picker/accessibility.ts +24 -0
- package/src/components/time-picker/index.tsx +48 -2
- package/src/components/waterfall-chart/index.tsx +1589 -0
- package/src/index.ts +69 -0
- package/src/primitives/modal-isolation-store.ts +31 -0
- package/src/primitives/portal.tsx +34 -4
- package/src/providers/panel-ui-provider.tsx +41 -9
- package/theme.css +54 -3
|
@@ -0,0 +1,1329 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plot — a chart you assemble, for the chart that is not in the box.
|
|
5
|
+
*
|
|
6
|
+
* The other charts in this library each answer one question and answer it
|
|
7
|
+
* completely: a line chart knows it is drawing a series over time, and every
|
|
8
|
+
* decision inside it follows from that. This one knows nothing. It measures a
|
|
9
|
+
* box, resolves a scale, and hands both to whatever marks you put in it — so a
|
|
10
|
+
* combination nothing here ships as its own component is still a chart you can
|
|
11
|
+
* build rather than a chart you have to go without.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* <Plot data={months} xDataKey="month">
|
|
15
|
+
* <Plot.Grid />
|
|
16
|
+
* <Plot.Bars dataKey="orders" colorIndex={2} />
|
|
17
|
+
* <Plot.Line dataKey="revenue" />
|
|
18
|
+
* <Plot.YAxis />
|
|
19
|
+
* <Plot.XAxis />
|
|
20
|
+
* <Plot.Cursor />
|
|
21
|
+
* <Plot.Tooltip />
|
|
22
|
+
* </Plot>
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* ## One scale, however many marks
|
|
26
|
+
*
|
|
27
|
+
* Every mark reads the same plot box and the same y-domain, and the domain is
|
|
28
|
+
* derived from all of them together. That is the whole reason to compose rather
|
|
29
|
+
* than to stack two charts on top of each other: two scales drawn over each
|
|
30
|
+
* other look like a comparison and are not one.
|
|
31
|
+
*
|
|
32
|
+
* ## Where your own marks go
|
|
33
|
+
*
|
|
34
|
+
* `Plot.Layer` drops its children into the SVG tree, and `usePlot()` gives them
|
|
35
|
+
* the resolved geometry — the box, the tweening domain, the reveal, the palette.
|
|
36
|
+
* The scale functions are worklets exported alongside this component, so a mark
|
|
37
|
+
* of your own is rebuilt on the UI thread on the same frames these are:
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* function Threshold({ value }: { value: number }) {
|
|
41
|
+
* const { plot, domainMin, domainMax } = usePlot();
|
|
42
|
+
* const props = useAnimatedProps(() => {
|
|
43
|
+
* const y = yOf(value, plot, domainMin.value, domainMax.value);
|
|
44
|
+
* return { d: `M${plot.left},${y}H${plot.left + plot.width}` };
|
|
45
|
+
* });
|
|
46
|
+
* return <AnimatedPath animatedProps={props} stroke="red" />;
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* <Plot data={rows}>
|
|
50
|
+
* <Plot.Layer><Threshold value={80} /></Plot.Layer>
|
|
51
|
+
* </Plot>
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* Anything that is text or takes a touch goes in `Plot.Overlay` instead, which
|
|
55
|
+
* is a React Native view over the drawing. That split is not a preference: SVG
|
|
56
|
+
* text ignores the platform's text scaling and the theme's font, and a gesture
|
|
57
|
+
* handler cannot be attached to an SVG node at all.
|
|
58
|
+
*
|
|
59
|
+
* ## What it will not do for you
|
|
60
|
+
*
|
|
61
|
+
* There is no `type` prop and no set of defaults that guess at one. A chart
|
|
62
|
+
* assembled here is exactly the marks you wrote, in the order you wrote them —
|
|
63
|
+
* which is also the order they are drawn in, so a line over bars is a line
|
|
64
|
+
* written after them.
|
|
65
|
+
*/
|
|
66
|
+
import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
67
|
+
import { StyleSheet, View } from 'react-native';
|
|
68
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
69
|
+
import Animated, { Easing, runOnJS, useAnimatedProps, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
70
|
+
import Svg, { Circle, ClipPath, Defs, G, Line as SvgLine, Path, Rect } from 'react-native-svg';
|
|
71
|
+
import { useCSSVariable } from 'uniwind';
|
|
72
|
+
import { Text } from "../../primitives/text.js";
|
|
73
|
+
import { areaPath, bandOf, barPath, columnValues, compactNumber, linePath, useSeriesColor, xOf, yOf } from "../../utils/chart.js";
|
|
74
|
+
import { cn } from "../../utils/cn.js";
|
|
75
|
+
|
|
76
|
+
/*
|
|
77
|
+
* The scale functions, re-exported so a mark written outside this file can sit
|
|
78
|
+
* on the same geometry the built-in ones do. Every one of them is a worklet, so
|
|
79
|
+
* a custom mark is rebuilt on the UI thread on the frames these are — which is
|
|
80
|
+
* the difference between an escape hatch and a second, slower chart drawn on
|
|
81
|
+
* top of the first.
|
|
82
|
+
*/
|
|
83
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
84
|
+
export { areaPath, bandOf, barPath, compactNumber, linePath, segment, xAt, xOf, yOf } from "../../utils/chart.js";
|
|
85
|
+
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
86
|
+
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
87
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
88
|
+
|
|
89
|
+
/** Room left around the plot for the axis labels and the markers' rings. */
|
|
90
|
+
const PADDING = {
|
|
91
|
+
top: 12,
|
|
92
|
+
right: 10,
|
|
93
|
+
bottom: 22,
|
|
94
|
+
left: 10
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Left gutter reserved when a `YAxis` is present, for its labels to sit in. */
|
|
98
|
+
const Y_AXIS_WIDTH = 44;
|
|
99
|
+
|
|
100
|
+
/** Gap between the value labels and the plot they sit beside. */
|
|
101
|
+
const Y_AXIS_GUTTER = 6;
|
|
102
|
+
|
|
103
|
+
/** Line height of an `xs` label, for centring one on the rule it names. */
|
|
104
|
+
const AXIS_LABEL_HEIGHT = 16;
|
|
105
|
+
|
|
106
|
+
/** Box each x label is centred in; a longer one is ellipsised rather than shoving. */
|
|
107
|
+
const POINT_LABEL_WIDTH = 56;
|
|
108
|
+
|
|
109
|
+
/** Width of the readout that rides the cursor. */
|
|
110
|
+
const LABEL_WIDTH = 120;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Which layer a part belongs to. Read off the component itself, so composition
|
|
114
|
+
* stays a flat list of children instead of three nested slots whose order the
|
|
115
|
+
* caller has to remember.
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* How the index of a row becomes an x.
|
|
120
|
+
*
|
|
121
|
+
* `point` puts the first and last rows on the plot's own edges, which is what a
|
|
122
|
+
* series wants — the line should reach the frame. `band` gives every row an
|
|
123
|
+
* equal slice and centres it in the middle of that, which is what anything with
|
|
124
|
+
* width wants: a bar sitting on the edge of the plot is a bar half of which is
|
|
125
|
+
* outside it.
|
|
126
|
+
*
|
|
127
|
+
* Resolved from the marks by default, so a chart with bars in it is banded
|
|
128
|
+
* without being told.
|
|
129
|
+
*/
|
|
130
|
+
|
|
131
|
+
/** One end of the y-domain: a number to pin it at, or `auto` to derive it. */
|
|
132
|
+
|
|
133
|
+
/** Everything a mark needs to draw itself. Read it with `usePlot()`. */
|
|
134
|
+
|
|
135
|
+
const PlotContext = /*#__PURE__*/createContext(null);
|
|
136
|
+
function useChart(component) {
|
|
137
|
+
const context = useContext(PlotContext);
|
|
138
|
+
if (!context) {
|
|
139
|
+
throw new Error(`${component} must be used within a <Plot>`);
|
|
140
|
+
}
|
|
141
|
+
return context;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The resolved geometry, for a mark of your own.
|
|
146
|
+
*
|
|
147
|
+
* Everything in it is either a plain number that changes on layout or a shared
|
|
148
|
+
* value that changes every frame, so a mark built from it animates with the
|
|
149
|
+
* rest of the chart rather than beside it.
|
|
150
|
+
*/
|
|
151
|
+
export function usePlot() {
|
|
152
|
+
return useChart('usePlot');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The row under the cursor, for a readout drawn inside the plot. */
|
|
156
|
+
export function usePlotCursor() {
|
|
157
|
+
const {
|
|
158
|
+
data,
|
|
159
|
+
activeIndexJS
|
|
160
|
+
} = useChart('usePlotCursor');
|
|
161
|
+
return {
|
|
162
|
+
activeIndex: activeIndexJS,
|
|
163
|
+
activePoint: activeIndexJS >= 0 ? data[activeIndexJS] ?? null : null
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Imperative handle: re-run the reveal, for a "replay" control. */
|
|
168
|
+
|
|
169
|
+
const PlotRoot = /*#__PURE__*/forwardRef(function PlotRoot({
|
|
170
|
+
className,
|
|
171
|
+
data,
|
|
172
|
+
xDataKey = 'label',
|
|
173
|
+
status = 'ready',
|
|
174
|
+
aspectRatio = 2,
|
|
175
|
+
animationDuration = 700,
|
|
176
|
+
domainDuration = 500,
|
|
177
|
+
yDomain,
|
|
178
|
+
xScale: xScaleProp,
|
|
179
|
+
curve = 'monotone',
|
|
180
|
+
onActiveIndexChange,
|
|
181
|
+
compact = false,
|
|
182
|
+
children,
|
|
183
|
+
...props
|
|
184
|
+
}, ref) {
|
|
185
|
+
const [size, setSize] = useState({
|
|
186
|
+
width: 0,
|
|
187
|
+
height: 0
|
|
188
|
+
});
|
|
189
|
+
const [series, setSeries] = useState([]);
|
|
190
|
+
const [activeIndexJS, setActiveIndexJS] = useState(-1);
|
|
191
|
+
const clipId = `panelui-plot-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
192
|
+
const reveal = useSharedValue(0);
|
|
193
|
+
const domainMin = useSharedValue(0);
|
|
194
|
+
const domainMax = useSharedValue(0);
|
|
195
|
+
const activeIndex = useSharedValue(-1);
|
|
196
|
+
const reducedMotion = useReducedMotion();
|
|
197
|
+
const registerSeries = useMemo(() => (key, color) => setSeries(current => {
|
|
198
|
+
const existing = current.find(([k]) => k === key);
|
|
199
|
+
if (existing?.[1] === color) return current;
|
|
200
|
+
return [...current.filter(([k]) => k !== key), [key, color]];
|
|
201
|
+
}), []);
|
|
202
|
+
const unregisterSeries = useMemo(() => key => setSeries(current => current.filter(([k]) => k !== key)), []);
|
|
203
|
+
|
|
204
|
+
/*
|
|
205
|
+
* What the children need decided before anything is laid out.
|
|
206
|
+
*
|
|
207
|
+
* Both have to be known *before* the plot box exists, and neither can be
|
|
208
|
+
* asked of the parts themselves — a part renders into a box that has already
|
|
209
|
+
* been decided. An axis given no gutter is drawn over the marks; a bar placed
|
|
210
|
+
* on a point scale hangs half of itself off the frame, and a bar on an axis
|
|
211
|
+
* that skips zero is drawn at a length the data does not support.
|
|
212
|
+
*/
|
|
213
|
+
const {
|
|
214
|
+
hasYAxis,
|
|
215
|
+
hasBars
|
|
216
|
+
} = useMemo(() => {
|
|
217
|
+
let axis = false;
|
|
218
|
+
let bars = false;
|
|
219
|
+
Children.forEach(children, child => {
|
|
220
|
+
if (! /*#__PURE__*/isValidElement(child)) return;
|
|
221
|
+
const type = child.type;
|
|
222
|
+
if (type.axis === 'y') axis = true;
|
|
223
|
+
if (type.mark === 'band') bars = true;
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
hasYAxis: axis,
|
|
227
|
+
hasBars: bars
|
|
228
|
+
};
|
|
229
|
+
}, [children]);
|
|
230
|
+
const xScale = xScaleProp ?? (hasBars ? 'band' : 'point');
|
|
231
|
+
const pad = compact ? {
|
|
232
|
+
top: 2,
|
|
233
|
+
right: 1,
|
|
234
|
+
bottom: 2,
|
|
235
|
+
left: 1
|
|
236
|
+
} : {
|
|
237
|
+
...PADDING,
|
|
238
|
+
left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left
|
|
239
|
+
};
|
|
240
|
+
const plot = {
|
|
241
|
+
left: pad.left,
|
|
242
|
+
top: pad.top,
|
|
243
|
+
width: Math.max(size.width - pad.left - pad.right, 0),
|
|
244
|
+
height: Math.max(size.height - pad.top - pad.bottom, 0)
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/*
|
|
248
|
+
* One extent across every registered mark, so two series share one axis and
|
|
249
|
+
* stay comparable. A scale per series makes two quantities orders of
|
|
250
|
+
* magnitude apart look alike, which is the chart lying rather than the chart
|
|
251
|
+
* being convenient.
|
|
252
|
+
*/
|
|
253
|
+
const seriesKeys = series.map(([key]) => key).join('|');
|
|
254
|
+
const extent = useMemo(() => {
|
|
255
|
+
const lowPin = yDomain?.[0];
|
|
256
|
+
const highPin = yDomain?.[1];
|
|
257
|
+
if (typeof lowPin === 'number' && typeof highPin === 'number') {
|
|
258
|
+
return [lowPin, highPin];
|
|
259
|
+
}
|
|
260
|
+
const keys = seriesKeys ? seriesKeys.split('|') : [];
|
|
261
|
+
let min = Infinity;
|
|
262
|
+
let max = -Infinity;
|
|
263
|
+
for (const row of data) {
|
|
264
|
+
for (const key of keys) {
|
|
265
|
+
const value = row[key];
|
|
266
|
+
if (typeof value !== 'number' || Number.isNaN(value)) continue;
|
|
267
|
+
if (value < min) min = value;
|
|
268
|
+
if (value > max) max = value;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (min === Infinity) {
|
|
272
|
+
// Nothing to measure. Fall back to whatever was pinned, and to a unit
|
|
273
|
+
// domain when nothing was — dividing by a span of zero is how a mark
|
|
274
|
+
// ends up drawn at infinity.
|
|
275
|
+
const low = typeof lowPin === 'number' ? lowPin : 0;
|
|
276
|
+
const high = typeof highPin === 'number' ? highPin : low + 1;
|
|
277
|
+
return [low, high];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/*
|
|
281
|
+
* A bar's length is measured from zero, so an axis that does not contain
|
|
282
|
+
* zero makes every bar on it a lie about its own size — six columns of
|
|
283
|
+
* near-identical height standing for numbers that differ by half. So a plot
|
|
284
|
+
* with bars in it gets zero whether the data reaches it or not, and that end
|
|
285
|
+
* is then left exactly where it is: headroom under a baseline lifts the bars
|
|
286
|
+
* off the thing they are measured from.
|
|
287
|
+
*/
|
|
288
|
+
let lowRoom = 1;
|
|
289
|
+
let highRoom = 1;
|
|
290
|
+
if (hasBars) {
|
|
291
|
+
if (min > 0) min = 0;
|
|
292
|
+
if (max < 0) max = 0;
|
|
293
|
+
if (min === 0) lowRoom = 0;
|
|
294
|
+
if (max === 0) highRoom = 0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// A flat series has no extent of its own; give it one so it lands on the
|
|
298
|
+
// middle of the plot instead of dividing by zero.
|
|
299
|
+
if (min === max) {
|
|
300
|
+
min -= 1;
|
|
301
|
+
max += 1;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/*
|
|
305
|
+
* Headroom goes on the derived ends only. A pinned zero that got a tenth of
|
|
306
|
+
* the span subtracted from it is not a pinned zero, and the baseline
|
|
307
|
+
* drifting off the axis is exactly what pinning it was for.
|
|
308
|
+
*/
|
|
309
|
+
const headroom = (max - min) * 0.1;
|
|
310
|
+
return [typeof lowPin === 'number' ? lowPin : min - headroom * lowRoom, typeof highPin === 'number' ? highPin : max + headroom * highRoom];
|
|
311
|
+
}, [data, yDomain, seriesKeys, hasBars]);
|
|
312
|
+
const loading = status === 'loading';
|
|
313
|
+
// Nothing has said what the axis is yet — no mark has registered and no
|
|
314
|
+
// domain was given. Assigning one now would tween the real domain up out of a
|
|
315
|
+
// placeholder on the frame the first mark mounts.
|
|
316
|
+
const hasDomain = seriesKeys.length > 0 || yDomain !== undefined;
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (loading || !hasDomain) return;
|
|
319
|
+
const [min, max] = extent;
|
|
320
|
+
// The first domain lands without a tween: there is no previous scale to
|
|
321
|
+
// move from, and animating up from zero reads as the numbers changing.
|
|
322
|
+
const first = domainMin.value === 0 && domainMax.value === 0;
|
|
323
|
+
if (first || reducedMotion) {
|
|
324
|
+
domainMin.value = min;
|
|
325
|
+
domainMax.value = max;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
domainMin.value = withTiming(min, {
|
|
329
|
+
duration: domainDuration
|
|
330
|
+
});
|
|
331
|
+
domainMax.value = withTiming(max, {
|
|
332
|
+
duration: domainDuration
|
|
333
|
+
});
|
|
334
|
+
}, [extent, loading, hasDomain, reducedMotion, domainDuration, domainMin, domainMax]);
|
|
335
|
+
const revealed = useRef(false);
|
|
336
|
+
const playReveal = useMemo(() => () => {
|
|
337
|
+
if (reducedMotion) {
|
|
338
|
+
reveal.value = 1;
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
reveal.value = 0;
|
|
342
|
+
reveal.value = withTiming(1, {
|
|
343
|
+
duration: animationDuration,
|
|
344
|
+
easing: Easing.out(Easing.cubic)
|
|
345
|
+
});
|
|
346
|
+
}, [reducedMotion, animationDuration, reveal]);
|
|
347
|
+
useEffect(() => {
|
|
348
|
+
// Going back to `loading` arms the reveal again, so a refetch is uncovered
|
|
349
|
+
// rather than appearing whole on the frame the data lands.
|
|
350
|
+
if (loading) {
|
|
351
|
+
revealed.current = false;
|
|
352
|
+
reveal.value = 0;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (revealed.current || plot.width <= 0 || !data.length) return;
|
|
356
|
+
revealed.current = true;
|
|
357
|
+
playReveal();
|
|
358
|
+
}, [loading, plot.width, data.length, playReveal, reveal]);
|
|
359
|
+
useImperativeHandle(ref, () => ({
|
|
360
|
+
replay: playReveal
|
|
361
|
+
}), [playReveal]);
|
|
362
|
+
const c1 = useSeriesColor(undefined, 1);
|
|
363
|
+
const c2 = useSeriesColor(undefined, 2);
|
|
364
|
+
const c3 = useSeriesColor(undefined, 3);
|
|
365
|
+
const c4 = useSeriesColor(undefined, 4);
|
|
366
|
+
const c5 = useSeriesColor(undefined, 5);
|
|
367
|
+
const palette = useMemo(() => [c1, c2, c3, c4, c5], [c1, c2, c3, c4, c5]);
|
|
368
|
+
const handleActiveIndex = useMemo(() => index => {
|
|
369
|
+
setActiveIndexJS(index);
|
|
370
|
+
onActiveIndexChange?.(index, index >= 0 ? data[index] ?? null : null);
|
|
371
|
+
}, [onActiveIndexChange, data]);
|
|
372
|
+
const onLayout = event => {
|
|
373
|
+
const {
|
|
374
|
+
width,
|
|
375
|
+
height
|
|
376
|
+
} = event.nativeEvent.layout;
|
|
377
|
+
setSize(current => Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : {
|
|
378
|
+
width,
|
|
379
|
+
height
|
|
380
|
+
});
|
|
381
|
+
props.onLayout?.(event);
|
|
382
|
+
};
|
|
383
|
+
const context = useMemo(() => ({
|
|
384
|
+
data,
|
|
385
|
+
xDataKey,
|
|
386
|
+
plot,
|
|
387
|
+
xScale,
|
|
388
|
+
status,
|
|
389
|
+
domainMin,
|
|
390
|
+
domainMax,
|
|
391
|
+
extent,
|
|
392
|
+
reveal,
|
|
393
|
+
palette,
|
|
394
|
+
series,
|
|
395
|
+
registerSeries,
|
|
396
|
+
unregisterSeries,
|
|
397
|
+
activeIndex,
|
|
398
|
+
activeIndexJS,
|
|
399
|
+
setActiveIndexJS: handleActiveIndex,
|
|
400
|
+
clipId
|
|
401
|
+
}),
|
|
402
|
+
// `plot` is rebuilt every render from `size`, so it is compared by value.
|
|
403
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
404
|
+
[data, xDataKey, plot.width, plot.height, plot.left, plot.top, xScale, status, domainMin, domainMax, extent, reveal, palette, series, registerSeries, unregisterSeries, activeIndex, activeIndexJS, handleActiveIndex, clipId]);
|
|
405
|
+
const clipProps = useAnimatedProps(() => ({
|
|
406
|
+
width: plot.width * reveal.value
|
|
407
|
+
}));
|
|
408
|
+
const {
|
|
409
|
+
svg,
|
|
410
|
+
overlay,
|
|
411
|
+
header
|
|
412
|
+
} = partition(children);
|
|
413
|
+
|
|
414
|
+
/*
|
|
415
|
+
* Two views, because the header is not part of the plot. `aspectRatio` and
|
|
416
|
+
* the layout measurement belong to the drawing area alone — measured on the
|
|
417
|
+
* outer view they would take the header in too, and the plot would lose as
|
|
418
|
+
* much height as the readout took while still claiming the shape asked for.
|
|
419
|
+
*/
|
|
420
|
+
return /*#__PURE__*/_jsx(PlotContext.Provider, {
|
|
421
|
+
value: context,
|
|
422
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
423
|
+
...props,
|
|
424
|
+
style: props.style,
|
|
425
|
+
className: cn('w-full', className),
|
|
426
|
+
children: [header, /*#__PURE__*/_jsx(View, {
|
|
427
|
+
onLayout: onLayout,
|
|
428
|
+
style: {
|
|
429
|
+
aspectRatio
|
|
430
|
+
},
|
|
431
|
+
className: "w-full",
|
|
432
|
+
children: plot.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
433
|
+
children: [/*#__PURE__*/_jsxs(Svg, {
|
|
434
|
+
width: "100%",
|
|
435
|
+
height: "100%",
|
|
436
|
+
style: StyleSheet.absoluteFill,
|
|
437
|
+
children: [/*#__PURE__*/_jsx(Defs, {
|
|
438
|
+
children: /*#__PURE__*/_jsx(ClipPath, {
|
|
439
|
+
id: clipId,
|
|
440
|
+
children: /*#__PURE__*/_jsx(AnimatedRect, {
|
|
441
|
+
x: plot.left,
|
|
442
|
+
y: 0,
|
|
443
|
+
height: size.height,
|
|
444
|
+
animatedProps: clipProps
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
}), svg]
|
|
448
|
+
}), overlay]
|
|
449
|
+
}) : null
|
|
450
|
+
})]
|
|
451
|
+
})
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
PlotRoot.displayName = 'Plot';
|
|
455
|
+
|
|
456
|
+
/** Sorts the children into the SVG tree, the view layer over it, and the header. */
|
|
457
|
+
function partition(children) {
|
|
458
|
+
const svg = [];
|
|
459
|
+
const overlay = [];
|
|
460
|
+
const header = [];
|
|
461
|
+
Children.forEach(children, (child, index) => {
|
|
462
|
+
if (! /*#__PURE__*/isValidElement(child)) return;
|
|
463
|
+
const layer = child.type.layer ?? 'svg';
|
|
464
|
+
(layer === 'header' ? header : layer === 'overlay' ? overlay : svg).push(
|
|
465
|
+
/*#__PURE__*/
|
|
466
|
+
// Children of a `Children.forEach` need keys of their own once they are
|
|
467
|
+
// put into a new array.
|
|
468
|
+
_jsx(ChildSlot, {
|
|
469
|
+
children: child
|
|
470
|
+
}, index));
|
|
471
|
+
});
|
|
472
|
+
return {
|
|
473
|
+
svg,
|
|
474
|
+
overlay,
|
|
475
|
+
header
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Identity wrapper, purely so the partitioned arrays can carry keys. */
|
|
480
|
+
function ChildSlot({
|
|
481
|
+
children
|
|
482
|
+
}) {
|
|
483
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
484
|
+
children: children
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* A mark's colour: the one it was given, else the theme token at its index.
|
|
490
|
+
*
|
|
491
|
+
* Registration happens here rather than in each mark, because every mark that
|
|
492
|
+
* registers does it for the same two reasons — the domain has to take it in,
|
|
493
|
+
* and the legend has to name it.
|
|
494
|
+
*/
|
|
495
|
+
function useMark(dataKey, color, colorIndex) {
|
|
496
|
+
const {
|
|
497
|
+
palette,
|
|
498
|
+
registerSeries,
|
|
499
|
+
unregisterSeries
|
|
500
|
+
} = useChart('Plot mark');
|
|
501
|
+
const resolved = color ?? palette[(colorIndex - 1) % palette.length] ?? palette[0];
|
|
502
|
+
useEffect(() => {
|
|
503
|
+
registerSeries(dataKey, resolved);
|
|
504
|
+
return () => unregisterSeries(dataKey);
|
|
505
|
+
}, [dataKey, resolved, registerSeries, unregisterSeries]);
|
|
506
|
+
return resolved;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/* -------------------------------------------------------------------------- */
|
|
510
|
+
/* SVG layer */
|
|
511
|
+
/* -------------------------------------------------------------------------- */
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Horizontal reference lines.
|
|
515
|
+
*
|
|
516
|
+
* Outside the reveal clip on purpose: the grid is the frame the chart arrives
|
|
517
|
+
* into, so it is already there when the marks start being uncovered.
|
|
518
|
+
*/
|
|
519
|
+
function PlotGrid({
|
|
520
|
+
rows = 4,
|
|
521
|
+
color,
|
|
522
|
+
dashArray = '4,6',
|
|
523
|
+
opacity = 1
|
|
524
|
+
}) {
|
|
525
|
+
const {
|
|
526
|
+
plot
|
|
527
|
+
} = useChart('Plot.Grid');
|
|
528
|
+
const token = useCSSVariable('--color-border');
|
|
529
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
530
|
+
return /*#__PURE__*/_jsx(G, {
|
|
531
|
+
opacity: opacity,
|
|
532
|
+
children: Array.from({
|
|
533
|
+
length: rows + 1
|
|
534
|
+
}, (_, index) => {
|
|
535
|
+
const y = plot.top + plot.height / rows * index;
|
|
536
|
+
return /*#__PURE__*/_jsx(SvgLine, {
|
|
537
|
+
x1: plot.left,
|
|
538
|
+
x2: plot.left + plot.width,
|
|
539
|
+
y1: y,
|
|
540
|
+
y2: y,
|
|
541
|
+
stroke: stroke,
|
|
542
|
+
strokeWidth: 1,
|
|
543
|
+
strokeDasharray: dashArray
|
|
544
|
+
}, index);
|
|
545
|
+
})
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
PlotGrid.displayName = 'Plot.Grid';
|
|
549
|
+
PlotGrid.layer = 'svg';
|
|
550
|
+
/** A series as a stroked line. */
|
|
551
|
+
function PlotLine({
|
|
552
|
+
dataKey,
|
|
553
|
+
color,
|
|
554
|
+
colorIndex = 1,
|
|
555
|
+
strokeWidth = 2.5,
|
|
556
|
+
curve = 'monotone',
|
|
557
|
+
dashArray
|
|
558
|
+
}) {
|
|
559
|
+
const {
|
|
560
|
+
data,
|
|
561
|
+
plot,
|
|
562
|
+
xScale,
|
|
563
|
+
domainMin,
|
|
564
|
+
domainMax,
|
|
565
|
+
status,
|
|
566
|
+
clipId
|
|
567
|
+
} = useChart('Plot.Line');
|
|
568
|
+
const stroke = useMark(dataKey, color, colorIndex);
|
|
569
|
+
|
|
570
|
+
/*
|
|
571
|
+
* Pulled out of the worklet as a plain array. A worklet may close over numbers
|
|
572
|
+
* and arrays of them freely, but reading `data`'s rows — objects of mixed
|
|
573
|
+
* types, keyed by strings the caller chose — inside one on every frame is work
|
|
574
|
+
* that belongs on the JS side once.
|
|
575
|
+
*/
|
|
576
|
+
const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
|
|
577
|
+
const banded = xScale === 'band';
|
|
578
|
+
const animatedProps = useAnimatedProps(() => {
|
|
579
|
+
const min = domainMin.value;
|
|
580
|
+
const max = domainMax.value;
|
|
581
|
+
if (max === min || plot.width <= 0) return {
|
|
582
|
+
d: ''
|
|
583
|
+
};
|
|
584
|
+
if (!banded) return {
|
|
585
|
+
d: linePath(values, plot, min, max, curve, false)
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
/*
|
|
589
|
+
* A banded line is built here rather than through `linePath`, which spreads
|
|
590
|
+
* its points across the full width. On a band scale the points belong in
|
|
591
|
+
* the middle of their own slice — a line that ignored that would start half
|
|
592
|
+
* a slice left of the bar it is describing.
|
|
593
|
+
*/
|
|
594
|
+
let d = '';
|
|
595
|
+
let drawn = 0;
|
|
596
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
597
|
+
const value = values[index];
|
|
598
|
+
if (value === null || value === undefined) {
|
|
599
|
+
drawn = 0;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
const x = bandOf(index, values.length, plot);
|
|
603
|
+
const y = yOf(value, plot, min, max);
|
|
604
|
+
d += `${drawn === 0 ? 'M' : 'L'}${x},${y}`;
|
|
605
|
+
drawn += 1;
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
d
|
|
609
|
+
};
|
|
610
|
+
});
|
|
611
|
+
if (status === 'loading') return null;
|
|
612
|
+
return /*#__PURE__*/_jsx(G, {
|
|
613
|
+
clipPath: `url(#${clipId})`,
|
|
614
|
+
children: /*#__PURE__*/_jsx(AnimatedPath, {
|
|
615
|
+
animatedProps: animatedProps,
|
|
616
|
+
fill: "none",
|
|
617
|
+
stroke: stroke,
|
|
618
|
+
strokeWidth: strokeWidth,
|
|
619
|
+
strokeDasharray: dashArray,
|
|
620
|
+
strokeLinecap: "round",
|
|
621
|
+
strokeLinejoin: "round"
|
|
622
|
+
})
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
PlotLine.displayName = 'Plot.Line';
|
|
626
|
+
PlotLine.layer = 'svg';
|
|
627
|
+
/**
|
|
628
|
+
* A series as a fill down to the baseline.
|
|
629
|
+
*
|
|
630
|
+
* Written before the line it belongs under, since the order the marks are
|
|
631
|
+
* written is the order they are drawn.
|
|
632
|
+
*/
|
|
633
|
+
function PlotArea({
|
|
634
|
+
dataKey,
|
|
635
|
+
color,
|
|
636
|
+
colorIndex = 1,
|
|
637
|
+
opacity = 0.18,
|
|
638
|
+
curve = 'monotone'
|
|
639
|
+
}) {
|
|
640
|
+
const {
|
|
641
|
+
data,
|
|
642
|
+
plot,
|
|
643
|
+
domainMin,
|
|
644
|
+
domainMax,
|
|
645
|
+
status,
|
|
646
|
+
clipId
|
|
647
|
+
} = useChart('Plot.Area');
|
|
648
|
+
const fill = useMark(dataKey, color, colorIndex);
|
|
649
|
+
const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
|
|
650
|
+
const animatedProps = useAnimatedProps(() => {
|
|
651
|
+
const min = domainMin.value;
|
|
652
|
+
const max = domainMax.value;
|
|
653
|
+
if (max === min || plot.width <= 0) return {
|
|
654
|
+
d: ''
|
|
655
|
+
};
|
|
656
|
+
return {
|
|
657
|
+
d: areaPath(values, plot, min, max, curve, false)
|
|
658
|
+
};
|
|
659
|
+
});
|
|
660
|
+
if (status === 'loading') return null;
|
|
661
|
+
return /*#__PURE__*/_jsx(G, {
|
|
662
|
+
clipPath: `url(#${clipId})`,
|
|
663
|
+
children: /*#__PURE__*/_jsx(AnimatedPath, {
|
|
664
|
+
animatedProps: animatedProps,
|
|
665
|
+
fill: fill,
|
|
666
|
+
fillOpacity: opacity
|
|
667
|
+
})
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
PlotArea.displayName = 'Plot.Area';
|
|
671
|
+
PlotArea.layer = 'svg';
|
|
672
|
+
/**
|
|
673
|
+
* A series as columns.
|
|
674
|
+
*
|
|
675
|
+
* One path for all of them rather than one node per bar: a plot of two hundred
|
|
676
|
+
* periods is the same single animated prop a frame as a plot of twenty.
|
|
677
|
+
*
|
|
678
|
+
* A bar has width, so its presence puts the whole plot on a band scale unless
|
|
679
|
+
* the root was told otherwise — see `xScale`.
|
|
680
|
+
*/
|
|
681
|
+
function PlotBars({
|
|
682
|
+
dataKey,
|
|
683
|
+
color,
|
|
684
|
+
colorIndex = 1,
|
|
685
|
+
gap = 0.35,
|
|
686
|
+
radius = 4,
|
|
687
|
+
opacity = 1
|
|
688
|
+
}) {
|
|
689
|
+
const {
|
|
690
|
+
data,
|
|
691
|
+
plot,
|
|
692
|
+
domainMin,
|
|
693
|
+
domainMax,
|
|
694
|
+
status,
|
|
695
|
+
clipId
|
|
696
|
+
} = useChart('Plot.Bars');
|
|
697
|
+
const fill = useMark(dataKey, color, colorIndex);
|
|
698
|
+
const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
|
|
699
|
+
const total = values.length;
|
|
700
|
+
const slice = total > 0 ? plot.width / total : 0;
|
|
701
|
+
const width = Math.max(1, slice * (1 - Math.min(Math.max(gap, 0), 0.95)));
|
|
702
|
+
const animatedProps = useAnimatedProps(() => {
|
|
703
|
+
const min = domainMin.value;
|
|
704
|
+
const max = domainMax.value;
|
|
705
|
+
if (max === min || plot.width <= 0 || total === 0) return {
|
|
706
|
+
d: ''
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
/*
|
|
710
|
+
* Bars grow from the baseline, which is the domain's zero where the domain
|
|
711
|
+
* covers it and the nearer edge where it does not. A bar drawn from the
|
|
712
|
+
* bottom of a plot whose axis starts at 40 is a length that is not in the
|
|
713
|
+
* data.
|
|
714
|
+
*/
|
|
715
|
+
const base = yOf(Math.min(Math.max(0, min), max), plot, min, max);
|
|
716
|
+
let path = '';
|
|
717
|
+
for (let index = 0; index < total; index += 1) {
|
|
718
|
+
const value = values[index];
|
|
719
|
+
if (value === null || value === undefined) continue;
|
|
720
|
+
const y = yOf(value, plot, min, max);
|
|
721
|
+
const top = Math.min(y, base);
|
|
722
|
+
const height = Math.abs(base - y);
|
|
723
|
+
if (height <= 0) continue;
|
|
724
|
+
path += barPath(bandOf(index, total, plot) - width / 2, top, width, height, radius, y <= base ? 'up' : 'down');
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
d: path
|
|
728
|
+
};
|
|
729
|
+
});
|
|
730
|
+
if (status === 'loading') return null;
|
|
731
|
+
return /*#__PURE__*/_jsx(G, {
|
|
732
|
+
clipPath: `url(#${clipId})`,
|
|
733
|
+
children: /*#__PURE__*/_jsx(AnimatedPath, {
|
|
734
|
+
animatedProps: animatedProps,
|
|
735
|
+
fill: fill,
|
|
736
|
+
fillOpacity: opacity
|
|
737
|
+
})
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
PlotBars.displayName = 'Plot.Bars';
|
|
741
|
+
PlotBars.layer = 'svg';
|
|
742
|
+
// Read by the root before it resolves the scale: a mark with width cannot sit
|
|
743
|
+
// on a point scale without half of the first and last one leaving the plot.
|
|
744
|
+
PlotBars.mark = 'band';
|
|
745
|
+
/** A dot per row — for a series short enough that its points are worth marking. */
|
|
746
|
+
function PlotDots({
|
|
747
|
+
dataKey,
|
|
748
|
+
color,
|
|
749
|
+
colorIndex = 1,
|
|
750
|
+
size = 3.5,
|
|
751
|
+
ringWidth = 2
|
|
752
|
+
}) {
|
|
753
|
+
const {
|
|
754
|
+
data,
|
|
755
|
+
plot,
|
|
756
|
+
xScale,
|
|
757
|
+
domainMin,
|
|
758
|
+
domainMax,
|
|
759
|
+
status,
|
|
760
|
+
clipId
|
|
761
|
+
} = useChart('Plot.Dots');
|
|
762
|
+
const fill = useMark(dataKey, color, colorIndex);
|
|
763
|
+
const ringToken = useCSSVariable('--color-background');
|
|
764
|
+
const ring = typeof ringToken === 'string' ? ringToken : '#000000';
|
|
765
|
+
const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
|
|
766
|
+
const banded = xScale === 'band';
|
|
767
|
+
return /*#__PURE__*/_jsx(G, {
|
|
768
|
+
clipPath: `url(#${clipId})`,
|
|
769
|
+
children: status === 'loading' ? null : values.map((value, index) => value === null || value === undefined ? null : /*#__PURE__*/_jsx(Dot, {
|
|
770
|
+
value: value,
|
|
771
|
+
x: banded ? bandOf(index, values.length, plot) : xOf(index, values.length, plot),
|
|
772
|
+
plot: plot,
|
|
773
|
+
domainMin: domainMin,
|
|
774
|
+
domainMax: domainMax,
|
|
775
|
+
fill: fill,
|
|
776
|
+
ring: ring,
|
|
777
|
+
size: size,
|
|
778
|
+
ringWidth: ringWidth
|
|
779
|
+
}, index))
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
PlotDots.displayName = 'Plot.Dots';
|
|
783
|
+
PlotDots.layer = 'svg';
|
|
784
|
+
|
|
785
|
+
/** One dot, whose y follows the domain tween. */
|
|
786
|
+
function Dot({
|
|
787
|
+
value,
|
|
788
|
+
x,
|
|
789
|
+
plot,
|
|
790
|
+
domainMin,
|
|
791
|
+
domainMax,
|
|
792
|
+
fill,
|
|
793
|
+
ring,
|
|
794
|
+
size,
|
|
795
|
+
ringWidth
|
|
796
|
+
}) {
|
|
797
|
+
const animatedProps = useAnimatedProps(() => ({
|
|
798
|
+
cy: yOf(value, plot, domainMin.value, domainMax.value)
|
|
799
|
+
}));
|
|
800
|
+
return /*#__PURE__*/_jsx(AnimatedCircle, {
|
|
801
|
+
animatedProps: animatedProps,
|
|
802
|
+
cx: x,
|
|
803
|
+
r: size,
|
|
804
|
+
fill: fill,
|
|
805
|
+
stroke: ring,
|
|
806
|
+
strokeWidth: ringWidth
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Marks of your own, in the SVG tree.
|
|
811
|
+
*
|
|
812
|
+
* Whatever is inside is drawn where it is written — before the marks written
|
|
813
|
+
* after it, after the ones before it — and reaches the geometry through
|
|
814
|
+
* `usePlot()`. It is not given the geometry as an argument, because a mark that
|
|
815
|
+
* animates has to hold hooks of its own and a render prop is not a component.
|
|
816
|
+
*/
|
|
817
|
+
function PlotLayer({
|
|
818
|
+
children
|
|
819
|
+
}) {
|
|
820
|
+
const {
|
|
821
|
+
clipId
|
|
822
|
+
} = useChart('Plot.Layer');
|
|
823
|
+
return /*#__PURE__*/_jsx(G, {
|
|
824
|
+
clipPath: `url(#${clipId})`,
|
|
825
|
+
children: children
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
PlotLayer.displayName = 'Plot.Layer';
|
|
829
|
+
PlotLayer.layer = 'svg';
|
|
830
|
+
|
|
831
|
+
/* -------------------------------------------------------------------------- */
|
|
832
|
+
/* Overlay layer */
|
|
833
|
+
/* -------------------------------------------------------------------------- */
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Anything of your own that is text or takes a touch, laid over the drawing.
|
|
837
|
+
*
|
|
838
|
+
* SVG text ignores the platform's text scaling and the theme's font, and a
|
|
839
|
+
* gesture handler cannot be attached to an SVG node at all — so those two go
|
|
840
|
+
* here rather than in `Plot.Layer`.
|
|
841
|
+
*/
|
|
842
|
+
function PlotOverlay({
|
|
843
|
+
className,
|
|
844
|
+
children
|
|
845
|
+
}) {
|
|
846
|
+
return /*#__PURE__*/_jsx(View, {
|
|
847
|
+
pointerEvents: "box-none",
|
|
848
|
+
style: StyleSheet.absoluteFill,
|
|
849
|
+
className: className,
|
|
850
|
+
children: children
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
PlotOverlay.displayName = 'Plot.Overlay';
|
|
854
|
+
PlotOverlay.layer = 'overlay';
|
|
855
|
+
/**
|
|
856
|
+
* A reference line across the plot — a target, a limit, an average.
|
|
857
|
+
*
|
|
858
|
+
* A view rather than an SVG line, so its label is real text and follows the
|
|
859
|
+
* theme. Its height is one point and its position is a transform, so it costs
|
|
860
|
+
* no more than the SVG line would while gaining a legible caption.
|
|
861
|
+
*/
|
|
862
|
+
function PlotRule({
|
|
863
|
+
y,
|
|
864
|
+
label,
|
|
865
|
+
color,
|
|
866
|
+
className
|
|
867
|
+
}) {
|
|
868
|
+
const {
|
|
869
|
+
plot,
|
|
870
|
+
domainMin,
|
|
871
|
+
domainMax,
|
|
872
|
+
status
|
|
873
|
+
} = useChart('Plot.Rule');
|
|
874
|
+
const token = useCSSVariable('--color-muted-foreground');
|
|
875
|
+
const stroke = color ?? (typeof token === 'string' ? token : '#888888');
|
|
876
|
+
const style = useAnimatedStyle(() => {
|
|
877
|
+
const min = domainMin.value;
|
|
878
|
+
const max = domainMax.value;
|
|
879
|
+
if (max === min) return {
|
|
880
|
+
opacity: 0
|
|
881
|
+
};
|
|
882
|
+
const at = yOf(y, plot, min, max);
|
|
883
|
+
// Off the top or bottom of the plot is out of the chart's range, and a rule
|
|
884
|
+
// pinned to the edge would claim a value the axis does not cover.
|
|
885
|
+
const inside = at >= plot.top - 0.5 && at <= plot.top + plot.height + 0.5;
|
|
886
|
+
return {
|
|
887
|
+
opacity: inside ? 1 : 0,
|
|
888
|
+
transform: [{
|
|
889
|
+
translateY: at
|
|
890
|
+
}]
|
|
891
|
+
};
|
|
892
|
+
});
|
|
893
|
+
if (status === 'loading') return null;
|
|
894
|
+
return /*#__PURE__*/_jsxs(Animated.View, {
|
|
895
|
+
pointerEvents: "none",
|
|
896
|
+
style: [{
|
|
897
|
+
position: 'absolute',
|
|
898
|
+
left: plot.left,
|
|
899
|
+
width: plot.width,
|
|
900
|
+
top: 0
|
|
901
|
+
}, style],
|
|
902
|
+
className: className,
|
|
903
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
904
|
+
style: {
|
|
905
|
+
height: 1,
|
|
906
|
+
backgroundColor: stroke,
|
|
907
|
+
opacity: 0.5
|
|
908
|
+
}
|
|
909
|
+
}), label ? /*#__PURE__*/_jsx(Text, {
|
|
910
|
+
size: "xs",
|
|
911
|
+
muted: true,
|
|
912
|
+
numberOfLines: 1,
|
|
913
|
+
className: "self-end pt-0.5",
|
|
914
|
+
children: label
|
|
915
|
+
}) : null]
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
PlotRule.displayName = 'Plot.Rule';
|
|
919
|
+
PlotRule.layer = 'overlay';
|
|
920
|
+
/**
|
|
921
|
+
* The x labels. Real text rather than SVG text, so they follow the theme's font
|
|
922
|
+
* and the platform's text scaling — SVG text does neither.
|
|
923
|
+
*/
|
|
924
|
+
function PlotXAxis({
|
|
925
|
+
ticks = 4,
|
|
926
|
+
format,
|
|
927
|
+
className
|
|
928
|
+
}) {
|
|
929
|
+
const {
|
|
930
|
+
data,
|
|
931
|
+
xDataKey,
|
|
932
|
+
plot,
|
|
933
|
+
xScale
|
|
934
|
+
} = useChart('Plot.XAxis');
|
|
935
|
+
const labels = useMemo(() => {
|
|
936
|
+
if (!data.length) return [];
|
|
937
|
+
const count = Math.min(ticks, data.length);
|
|
938
|
+
const step = count > 1 ? (data.length - 1) / (count - 1) : 0;
|
|
939
|
+
return Array.from({
|
|
940
|
+
length: count
|
|
941
|
+
}, (_, index) => {
|
|
942
|
+
const dataIndex = Math.round(index * step);
|
|
943
|
+
const datum = data[dataIndex];
|
|
944
|
+
if (!datum) return null;
|
|
945
|
+
return {
|
|
946
|
+
key: dataIndex,
|
|
947
|
+
text: format ? format(datum, dataIndex) : String(datum[xDataKey] ?? '')
|
|
948
|
+
};
|
|
949
|
+
}).filter(label => label !== null);
|
|
950
|
+
}, [data, ticks, format, xDataKey]);
|
|
951
|
+
|
|
952
|
+
/*
|
|
953
|
+
* Each label sits on its own point rather than being spread along the axis.
|
|
954
|
+
* The indices `ticks` picks are not evenly spaced — twelve months shown five
|
|
955
|
+
* at a time gives 0, 3, 6, 8, 11 — so spreading them evenly put the ones in
|
|
956
|
+
* between over the wrong part of the plot.
|
|
957
|
+
*/
|
|
958
|
+
return /*#__PURE__*/_jsx(View, {
|
|
959
|
+
style: {
|
|
960
|
+
position: 'absolute',
|
|
961
|
+
inset: 0,
|
|
962
|
+
pointerEvents: 'none'
|
|
963
|
+
},
|
|
964
|
+
className: cn(className),
|
|
965
|
+
children: labels.map(label => {
|
|
966
|
+
const x = xScale === 'band' ? bandOf(label.key, data.length, plot) : xOf(label.key, data.length, plot);
|
|
967
|
+
return /*#__PURE__*/_jsx(Text, {
|
|
968
|
+
size: "xs",
|
|
969
|
+
muted: true,
|
|
970
|
+
numberOfLines: 1,
|
|
971
|
+
style: {
|
|
972
|
+
position: 'absolute',
|
|
973
|
+
bottom: 0,
|
|
974
|
+
// Centred on its point, then held inside the chart. On a point
|
|
975
|
+
// scale the first and last sit on the plot's own edges, so a box
|
|
976
|
+
// centred on them hangs half its width off the side.
|
|
977
|
+
left: Math.max(0, Math.min(x - POINT_LABEL_WIDTH / 2, plot.left + plot.width + PADDING.right - POINT_LABEL_WIDTH)),
|
|
978
|
+
width: POINT_LABEL_WIDTH,
|
|
979
|
+
textAlign: 'center'
|
|
980
|
+
},
|
|
981
|
+
children: label.text
|
|
982
|
+
}, label.key);
|
|
983
|
+
})
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
PlotXAxis.displayName = 'Plot.XAxis';
|
|
987
|
+
PlotXAxis.layer = 'overlay';
|
|
988
|
+
/**
|
|
989
|
+
* Value labels down the side, one per grid line.
|
|
990
|
+
*
|
|
991
|
+
* Give it the same `ticks` as the grid, or the numbers name lines that are not
|
|
992
|
+
* there. Four is the default on both for that reason.
|
|
993
|
+
*
|
|
994
|
+
* The labels are the domain the data settles at, not the tweening one — a
|
|
995
|
+
* number counting through every intermediate value while the axis animates is
|
|
996
|
+
* noise, and the axis is the part of a chart that has to hold still enough to
|
|
997
|
+
* be read.
|
|
998
|
+
*/
|
|
999
|
+
function PlotYAxis({
|
|
1000
|
+
ticks = 4,
|
|
1001
|
+
format,
|
|
1002
|
+
className
|
|
1003
|
+
}) {
|
|
1004
|
+
const {
|
|
1005
|
+
plot,
|
|
1006
|
+
extent
|
|
1007
|
+
} = useChart('Plot.YAxis');
|
|
1008
|
+
const labels = useMemo(() => {
|
|
1009
|
+
const [min, max] = extent;
|
|
1010
|
+
if (min === 0 && max === 0) return [];
|
|
1011
|
+
return Array.from({
|
|
1012
|
+
length: ticks + 1
|
|
1013
|
+
}, (_unused, index) => {
|
|
1014
|
+
const value = max - (max - min) * index / ticks;
|
|
1015
|
+
return {
|
|
1016
|
+
key: index,
|
|
1017
|
+
text: format ? format(value) : compactNumber(value)
|
|
1018
|
+
};
|
|
1019
|
+
});
|
|
1020
|
+
}, [extent, ticks, format]);
|
|
1021
|
+
return /*#__PURE__*/_jsx(View, {
|
|
1022
|
+
pointerEvents: "none",
|
|
1023
|
+
style: {
|
|
1024
|
+
position: 'absolute',
|
|
1025
|
+
left: 0,
|
|
1026
|
+
// Centred on the grid line each label names: the strip is lifted half a
|
|
1027
|
+
// label and grown by a whole one, so `justify-between` lands the text's
|
|
1028
|
+
// middle on the line rather than its top edge on the first.
|
|
1029
|
+
top: plot.top - AXIS_LABEL_HEIGHT / 2,
|
|
1030
|
+
height: plot.height + AXIS_LABEL_HEIGHT,
|
|
1031
|
+
width: Math.max(plot.left - Y_AXIS_GUTTER, 0)
|
|
1032
|
+
},
|
|
1033
|
+
className: cn('items-end justify-between', className),
|
|
1034
|
+
children: labels.map(label => /*#__PURE__*/_jsx(Text, {
|
|
1035
|
+
size: "xs",
|
|
1036
|
+
muted: true,
|
|
1037
|
+
numberOfLines: 1,
|
|
1038
|
+
children: label.text
|
|
1039
|
+
}, label.key))
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
PlotYAxis.displayName = 'Plot.YAxis';
|
|
1043
|
+
PlotYAxis.layer = 'overlay';
|
|
1044
|
+
// Read by the root, which has to leave room for the labels before it lays the
|
|
1045
|
+
// plot out.
|
|
1046
|
+
PlotYAxis.axis = 'y';
|
|
1047
|
+
/**
|
|
1048
|
+
* The touch handling, and the line that follows it.
|
|
1049
|
+
*
|
|
1050
|
+
* Split from the readout next door because they are separately useful: a plot
|
|
1051
|
+
* whose value is shown in its own header wants this and no label, and a plot
|
|
1052
|
+
* that highlights a bar wants this and nothing else at all. Both read the same
|
|
1053
|
+
* index.
|
|
1054
|
+
*
|
|
1055
|
+
* The hit area is the whole plot. A cursor you have to land on the line to
|
|
1056
|
+
* summon is a cursor nobody finds.
|
|
1057
|
+
*/
|
|
1058
|
+
function PlotCursor({
|
|
1059
|
+
color,
|
|
1060
|
+
showLine = true
|
|
1061
|
+
}) {
|
|
1062
|
+
const {
|
|
1063
|
+
data,
|
|
1064
|
+
plot,
|
|
1065
|
+
xScale,
|
|
1066
|
+
activeIndex,
|
|
1067
|
+
setActiveIndexJS,
|
|
1068
|
+
status
|
|
1069
|
+
} = useChart('Plot.Cursor');
|
|
1070
|
+
const token = useCSSVariable('--color-foreground');
|
|
1071
|
+
const stroke = color ?? (typeof token === 'string' ? token : '#888888');
|
|
1072
|
+
const total = data.length;
|
|
1073
|
+
const left = plot.left;
|
|
1074
|
+
const width = plot.width;
|
|
1075
|
+
const banded = xScale === 'band';
|
|
1076
|
+
|
|
1077
|
+
/*
|
|
1078
|
+
* Built in one closure, and everything it captures is a number or a shared
|
|
1079
|
+
* value. A worklet may only call another worklet, and the rule is enforced by
|
|
1080
|
+
* crashing rather than by warning — so the resolver is declared next to its
|
|
1081
|
+
* callers instead of as a helper further down the file where it would be easy
|
|
1082
|
+
* to leave un-workletised.
|
|
1083
|
+
*/
|
|
1084
|
+
const pan = useMemo(() => {
|
|
1085
|
+
const resolve = x => {
|
|
1086
|
+
'worklet';
|
|
1087
|
+
|
|
1088
|
+
if (total < 1 || width <= 0) return;
|
|
1089
|
+
const ratio = (x - left) / width;
|
|
1090
|
+
const clamped = Math.min(1, Math.max(0, ratio));
|
|
1091
|
+
// A band scale divides the plot into `total` slices and the finger is in
|
|
1092
|
+
// one of them; a point scale has `total - 1` gaps between marks and the
|
|
1093
|
+
// finger rounds to the nearest.
|
|
1094
|
+
const next = banded ? Math.min(total - 1, Math.floor(clamped * total)) : Math.round(clamped * Math.max(total - 1, 0));
|
|
1095
|
+
if (next === activeIndex.value) return;
|
|
1096
|
+
activeIndex.value = next;
|
|
1097
|
+
// Only the index needs JS, and only when it changes — a drag across a
|
|
1098
|
+
// hundred rows costs a hundred re-renders at most, not one per frame for
|
|
1099
|
+
// the length of the gesture.
|
|
1100
|
+
runOnJS(setActiveIndexJS)(next);
|
|
1101
|
+
};
|
|
1102
|
+
return Gesture.Pan().minDistance(0).onBegin(event => {
|
|
1103
|
+
'worklet';
|
|
1104
|
+
|
|
1105
|
+
resolve(event.x);
|
|
1106
|
+
}).onUpdate(event => {
|
|
1107
|
+
'worklet';
|
|
1108
|
+
|
|
1109
|
+
resolve(event.x);
|
|
1110
|
+
}).onFinalize(() => {
|
|
1111
|
+
'worklet';
|
|
1112
|
+
|
|
1113
|
+
activeIndex.value = -1;
|
|
1114
|
+
runOnJS(setActiveIndexJS)(-1);
|
|
1115
|
+
});
|
|
1116
|
+
}, [total, left, width, banded, activeIndex, setActiveIndexJS]);
|
|
1117
|
+
const lineStyle = useAnimatedStyle(() => {
|
|
1118
|
+
const index = activeIndex.value;
|
|
1119
|
+
if (index < 0) return {
|
|
1120
|
+
opacity: 0
|
|
1121
|
+
};
|
|
1122
|
+
const x = banded ? bandOf(index, total, plot) : xOf(index, total, plot);
|
|
1123
|
+
return {
|
|
1124
|
+
opacity: 0.45,
|
|
1125
|
+
transform: [{
|
|
1126
|
+
translateX: x
|
|
1127
|
+
}]
|
|
1128
|
+
};
|
|
1129
|
+
});
|
|
1130
|
+
if (status === 'loading') return null;
|
|
1131
|
+
return /*#__PURE__*/_jsx(GestureDetector, {
|
|
1132
|
+
gesture: pan,
|
|
1133
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
1134
|
+
style: StyleSheet.absoluteFill,
|
|
1135
|
+
children: showLine ? /*#__PURE__*/_jsx(Animated.View, {
|
|
1136
|
+
pointerEvents: "none",
|
|
1137
|
+
style: [{
|
|
1138
|
+
position: 'absolute',
|
|
1139
|
+
left: 0,
|
|
1140
|
+
top: plot.top,
|
|
1141
|
+
width: 1,
|
|
1142
|
+
height: plot.height,
|
|
1143
|
+
backgroundColor: stroke
|
|
1144
|
+
}, lineStyle]
|
|
1145
|
+
}) : null
|
|
1146
|
+
})
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
PlotCursor.displayName = 'Plot.Cursor';
|
|
1150
|
+
PlotCursor.layer = 'overlay';
|
|
1151
|
+
/**
|
|
1152
|
+
* The readout that rides the cursor.
|
|
1153
|
+
*
|
|
1154
|
+
* Needs a `Plot.Cursor` beside it — the cursor owns the gesture and this only
|
|
1155
|
+
* reads the index it resolves. On its own it never appears, which is the right
|
|
1156
|
+
* failure: a label with no way to move is worse than no label.
|
|
1157
|
+
*/
|
|
1158
|
+
function PlotTooltip({
|
|
1159
|
+
formatValue,
|
|
1160
|
+
formatX,
|
|
1161
|
+
children,
|
|
1162
|
+
className
|
|
1163
|
+
}) {
|
|
1164
|
+
const {
|
|
1165
|
+
data,
|
|
1166
|
+
xDataKey,
|
|
1167
|
+
plot,
|
|
1168
|
+
xScale,
|
|
1169
|
+
series,
|
|
1170
|
+
activeIndex,
|
|
1171
|
+
activeIndexJS,
|
|
1172
|
+
status
|
|
1173
|
+
} = useChart('Plot.Tooltip');
|
|
1174
|
+
const total = data.length;
|
|
1175
|
+
const banded = xScale === 'band';
|
|
1176
|
+
|
|
1177
|
+
// Centred over the cursor but clamped inside the plot, so it never runs off
|
|
1178
|
+
// the edge at the first or last row.
|
|
1179
|
+
const style = useAnimatedStyle(() => {
|
|
1180
|
+
const index = activeIndex.value;
|
|
1181
|
+
if (index < 0) return {
|
|
1182
|
+
opacity: 0
|
|
1183
|
+
};
|
|
1184
|
+
const x = banded ? bandOf(index, total, plot) : xOf(index, total, plot);
|
|
1185
|
+
const half = LABEL_WIDTH / 2;
|
|
1186
|
+
const clamped = Math.min(plot.left + plot.width - half, Math.max(plot.left + half, x));
|
|
1187
|
+
return {
|
|
1188
|
+
opacity: 1,
|
|
1189
|
+
transform: [{
|
|
1190
|
+
translateX: clamped - half
|
|
1191
|
+
}]
|
|
1192
|
+
};
|
|
1193
|
+
});
|
|
1194
|
+
const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
|
|
1195
|
+
const fmtValue = formatValue ?? (value => compactNumber(value));
|
|
1196
|
+
const fmtX = formatX ?? (datum => String(datum[xDataKey] ?? ''));
|
|
1197
|
+
if (status === 'loading') return null;
|
|
1198
|
+
return /*#__PURE__*/_jsx(Animated.View, {
|
|
1199
|
+
pointerEvents: "none",
|
|
1200
|
+
style: [{
|
|
1201
|
+
position: 'absolute',
|
|
1202
|
+
left: 0,
|
|
1203
|
+
top: Math.max(plot.top - 4, 0),
|
|
1204
|
+
width: LABEL_WIDTH
|
|
1205
|
+
}, style],
|
|
1206
|
+
children: /*#__PURE__*/_jsx(View, {
|
|
1207
|
+
className: cn('items-center rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg', className),
|
|
1208
|
+
children: active ? children ? children(active, activeIndexJS) : /*#__PURE__*/_jsxs(_Fragment, {
|
|
1209
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
1210
|
+
size: "xs",
|
|
1211
|
+
muted: true,
|
|
1212
|
+
numberOfLines: 1,
|
|
1213
|
+
children: fmtX(active)
|
|
1214
|
+
}), series.map(([key, color]) => {
|
|
1215
|
+
const value = active[key];
|
|
1216
|
+
if (typeof value !== 'number') return null;
|
|
1217
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
1218
|
+
className: "flex-row items-center gap-1.5",
|
|
1219
|
+
children: [series.length > 1 ? /*#__PURE__*/_jsx(View, {
|
|
1220
|
+
style: {
|
|
1221
|
+
backgroundColor: color
|
|
1222
|
+
},
|
|
1223
|
+
className: "h-1.5 w-1.5 rounded-full"
|
|
1224
|
+
}) : null, /*#__PURE__*/_jsx(Text, {
|
|
1225
|
+
size: "sm",
|
|
1226
|
+
weight: "semibold",
|
|
1227
|
+
numberOfLines: 1,
|
|
1228
|
+
children: fmtValue(value, key)
|
|
1229
|
+
})]
|
|
1230
|
+
}, key);
|
|
1231
|
+
})]
|
|
1232
|
+
}) : null
|
|
1233
|
+
})
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
PlotTooltip.displayName = 'Plot.Tooltip';
|
|
1237
|
+
PlotTooltip.layer = 'overlay';
|
|
1238
|
+
|
|
1239
|
+
/* -------------------------------------------------------------------------- */
|
|
1240
|
+
/* Header layer */
|
|
1241
|
+
/* -------------------------------------------------------------------------- */
|
|
1242
|
+
|
|
1243
|
+
/** The row above the drawing: what it is, and the one number worth reading. */
|
|
1244
|
+
function PlotHeader({
|
|
1245
|
+
className,
|
|
1246
|
+
title,
|
|
1247
|
+
value,
|
|
1248
|
+
caption,
|
|
1249
|
+
children,
|
|
1250
|
+
...props
|
|
1251
|
+
}) {
|
|
1252
|
+
return /*#__PURE__*/_jsx(View, {
|
|
1253
|
+
className: cn('gap-0.5', className),
|
|
1254
|
+
...props,
|
|
1255
|
+
children: children ?? /*#__PURE__*/_jsxs(_Fragment, {
|
|
1256
|
+
children: [title ? /*#__PURE__*/_jsx(Text, {
|
|
1257
|
+
size: "sm",
|
|
1258
|
+
muted: true,
|
|
1259
|
+
numberOfLines: 1,
|
|
1260
|
+
children: title
|
|
1261
|
+
}) : null, value ? /*#__PURE__*/_jsx(Text, {
|
|
1262
|
+
size: "2xl",
|
|
1263
|
+
weight: "semibold",
|
|
1264
|
+
numberOfLines: 1,
|
|
1265
|
+
children: value
|
|
1266
|
+
}) : null, caption ? /*#__PURE__*/_jsx(Text, {
|
|
1267
|
+
size: "xs",
|
|
1268
|
+
muted: true,
|
|
1269
|
+
numberOfLines: 1,
|
|
1270
|
+
children: caption
|
|
1271
|
+
}) : null]
|
|
1272
|
+
})
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
PlotHeader.displayName = 'Plot.Header';
|
|
1276
|
+
PlotHeader.layer = 'header';
|
|
1277
|
+
/**
|
|
1278
|
+
* A swatch and a name per mark, taken from the marks that registered.
|
|
1279
|
+
*
|
|
1280
|
+
* In the header rather than over the drawing, because a key laid inside the
|
|
1281
|
+
* plot either covers a mark or is squeezed to one word a line.
|
|
1282
|
+
*/
|
|
1283
|
+
function PlotLegend({
|
|
1284
|
+
className,
|
|
1285
|
+
labels,
|
|
1286
|
+
...props
|
|
1287
|
+
}) {
|
|
1288
|
+
const {
|
|
1289
|
+
series
|
|
1290
|
+
} = useChart('Plot.Legend');
|
|
1291
|
+
if (!series.length) return null;
|
|
1292
|
+
return /*#__PURE__*/_jsx(View, {
|
|
1293
|
+
className: cn('flex-row flex-wrap items-center gap-x-3 gap-y-1 pt-1', className),
|
|
1294
|
+
...props,
|
|
1295
|
+
children: series.map(([key, color]) => /*#__PURE__*/_jsxs(View, {
|
|
1296
|
+
className: "flex-row items-center gap-1.5",
|
|
1297
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
1298
|
+
style: {
|
|
1299
|
+
backgroundColor: color
|
|
1300
|
+
},
|
|
1301
|
+
className: "h-2 w-2 rounded-full"
|
|
1302
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
1303
|
+
size: "xs",
|
|
1304
|
+
muted: true,
|
|
1305
|
+
numberOfLines: 1,
|
|
1306
|
+
children: labels?.[key] ?? key
|
|
1307
|
+
})]
|
|
1308
|
+
}, key))
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
PlotLegend.displayName = 'Plot.Legend';
|
|
1312
|
+
PlotLegend.layer = 'header';
|
|
1313
|
+
export const Plot = Object.assign(PlotRoot, {
|
|
1314
|
+
Header: PlotHeader,
|
|
1315
|
+
Legend: PlotLegend,
|
|
1316
|
+
Grid: PlotGrid,
|
|
1317
|
+
Area: PlotArea,
|
|
1318
|
+
Bars: PlotBars,
|
|
1319
|
+
Line: PlotLine,
|
|
1320
|
+
Dots: PlotDots,
|
|
1321
|
+
Rule: PlotRule,
|
|
1322
|
+
Layer: PlotLayer,
|
|
1323
|
+
Overlay: PlotOverlay,
|
|
1324
|
+
XAxis: PlotXAxis,
|
|
1325
|
+
YAxis: PlotYAxis,
|
|
1326
|
+
Cursor: PlotCursor,
|
|
1327
|
+
Tooltip: PlotTooltip
|
|
1328
|
+
});
|
|
1329
|
+
//# sourceMappingURL=index.js.map
|