panelui-native 0.99.0 → 0.101.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 +2 -0
- package/lib/module/components/animated-badge/index.js +24 -2
- package/lib/module/components/animated-badge/index.js.map +1 -1
- package/lib/module/components/bump-chart/index.js +1236 -0
- package/lib/module/components/bump-chart/index.js.map +1 -0
- package/lib/module/components/compare/index.js +576 -0
- package/lib/module/components/compare/index.js.map +1 -0
- package/lib/module/components/mirror-area-chart/index.js +1113 -0
- package/lib/module/components/mirror-area-chart/index.js.map +1 -0
- package/lib/module/components/sankey-chart/index.js +933 -0
- package/lib/module/components/sankey-chart/index.js.map +1 -0
- package/lib/module/components/sankey-chart/sankey-layout.js +530 -0
- package/lib/module/components/sankey-chart/sankey-layout.js.map +1 -0
- package/lib/module/icons/index.js +15 -3
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +101 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/animated-badge/index.d.ts +5 -0
- package/lib/typescript/src/components/animated-badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bump-chart/index.d.ts +267 -0
- package/lib/typescript/src/components/bump-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/compare/index.d.ts +183 -0
- package/lib/typescript/src/components/compare/index.d.ts.map +1 -0
- package/lib/typescript/src/components/mirror-area-chart/index.d.ts +263 -0
- package/lib/typescript/src/components/mirror-area-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/sankey-chart/index.d.ts +307 -0
- package/lib/typescript/src/components/sankey-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/sankey-chart/sankey-layout.d.ts +107 -0
- package/lib/typescript/src/components/sankey-chart/sankey-layout.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +40 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/animated-badge/index.tsx +35 -2
- package/src/components/bump-chart/index.tsx +1455 -0
- package/src/components/compare/index.tsx +650 -0
- package/src/components/mirror-area-chart/index.tsx +1333 -0
- package/src/components/sankey-chart/index.tsx +1165 -0
- package/src/components/sankey-chart/sankey-layout.ts +645 -0
- package/src/icons/index.tsx +20 -6
- package/src/index.ts +62 -0
- package/src/utils/chart.ts +116 -0
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MirrorAreaChart — two readings of one timeline, one above a baseline and one
|
|
5
|
+
* below it.
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* <MirrorAreaChart data={load} xDataKey="time">
|
|
9
|
+
* <MirrorAreaChart.Grid />
|
|
10
|
+
* <MirrorAreaChart.Area dataKey="jobs" label="jobs" muted />
|
|
11
|
+
* <MirrorAreaChart.Area dataKey="high" label="high priority" />
|
|
12
|
+
* <MirrorAreaChart.Area dataKey="share" label="% high" side="bottom" />
|
|
13
|
+
* <MirrorAreaChart.Baseline />
|
|
14
|
+
* <MirrorAreaChart.Tooltip />
|
|
15
|
+
* </MirrorAreaChart>
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* ## Two scales, one timeline
|
|
19
|
+
*
|
|
20
|
+
* Each half has its own scale, because the two readings are usually in
|
|
21
|
+
* different units — a count above and a percentage below, or requests above
|
|
22
|
+
* and errors below. Both grow away from the baseline, so a peak in one lines
|
|
23
|
+
* up with whatever the other did at the same moment, and neither is squeezed
|
|
24
|
+
* into the other's range.
|
|
25
|
+
*
|
|
26
|
+
* Areas on the same side overlay in the order they are declared. Declare the
|
|
27
|
+
* whole first and the part of it second, so the part is drawn inside the whole.
|
|
28
|
+
*/
|
|
29
|
+
import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
30
|
+
import { StyleSheet, View } from 'react-native';
|
|
31
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
32
|
+
import Animated, { Easing, cancelAnimation, runOnJS, useAnimatedProps, useAnimatedStyle, useReducedMotion, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
|
|
33
|
+
import Svg, { Circle, Defs, G, Line as SvgLine, LinearGradient, Path, Rect, Stop } from 'react-native-svg';
|
|
34
|
+
import { useCSSVariable } from 'uniwind';
|
|
35
|
+
import { Text } from "../../primitives/text.js";
|
|
36
|
+
import { ChartAccessibilityData } from "../../primitives/chart-accessibility.js";
|
|
37
|
+
import { finiteChartDomain } from "../../primitives/finite-chart.js";
|
|
38
|
+
import { colorStop, columnValues, compactNumber, dotsPath, segment, useSeriesColor, xOf } from "../../utils/chart.js";
|
|
39
|
+
import { cn } from "../../utils/cn.js";
|
|
40
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
41
|
+
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
42
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
43
|
+
const AnimatedLine = Animated.createAnimatedComponent(SvgLine);
|
|
44
|
+
const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient);
|
|
45
|
+
const PADDING = {
|
|
46
|
+
top: 12,
|
|
47
|
+
right: 10,
|
|
48
|
+
bottom: 30,
|
|
49
|
+
left: 10
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Left gutter reserved when a `YAxis` is present. */
|
|
53
|
+
const Y_AXIS_WIDTH = 40;
|
|
54
|
+
|
|
55
|
+
/** Gap between the value labels and the plot. */
|
|
56
|
+
const Y_AXIS_GUTTER = 6;
|
|
57
|
+
|
|
58
|
+
/** Box each x label is centred in. */
|
|
59
|
+
const POINT_LABEL_WIDTH = 56;
|
|
60
|
+
|
|
61
|
+
/** Line height of an `xs` label, for centring one on the line it names. */
|
|
62
|
+
const AXIS_LABEL_HEIGHT = 16;
|
|
63
|
+
|
|
64
|
+
/** Width of the scrub readout. */
|
|
65
|
+
const READOUT_WIDTH = 168;
|
|
66
|
+
|
|
67
|
+
/** Diameter of the dot riding the top edge under the crosshair. */
|
|
68
|
+
const DOT = 9;
|
|
69
|
+
const MirrorAreaChartContext = /*#__PURE__*/createContext(null);
|
|
70
|
+
function useChart(component) {
|
|
71
|
+
const context = useContext(MirrorAreaChartContext);
|
|
72
|
+
if (!context) {
|
|
73
|
+
throw new Error(`${component} must be used within a <MirrorAreaChart>`);
|
|
74
|
+
}
|
|
75
|
+
return context;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The row under the crosshair, for a header or a readout outside the chart. */
|
|
79
|
+
export function useMirrorAreaChart() {
|
|
80
|
+
const {
|
|
81
|
+
data,
|
|
82
|
+
activeIndexJS,
|
|
83
|
+
xDataKey
|
|
84
|
+
} = useChart('useMirrorAreaChart');
|
|
85
|
+
return {
|
|
86
|
+
activeIndex: activeIndexJS,
|
|
87
|
+
activePoint: activeIndexJS >= 0 ? data[activeIndexJS] ?? null : null,
|
|
88
|
+
xDataKey
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A series' edge, `grow` of the way out from the baseline. `direction` is `-1`
|
|
94
|
+
* for the top half and `1` for the bottom. A negative value is drawn on the
|
|
95
|
+
* baseline: each half only grows away from it.
|
|
96
|
+
*/
|
|
97
|
+
function edgeRuns(values, plot, baseline, height, max, grow, direction) {
|
|
98
|
+
'worklet';
|
|
99
|
+
|
|
100
|
+
const runs = [];
|
|
101
|
+
let run = [];
|
|
102
|
+
const span = max > 0 ? max : 1;
|
|
103
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
104
|
+
const value = values[i];
|
|
105
|
+
if (value === null || value === undefined) {
|
|
106
|
+
if (run.length) runs.push(run);
|
|
107
|
+
run = [];
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const reach = Math.max(value, 0) / span * height * grow;
|
|
111
|
+
run.push({
|
|
112
|
+
x: xOf(i, values.length, plot),
|
|
113
|
+
y: baseline + direction * reach
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (run.length) runs.push(run);
|
|
117
|
+
return runs;
|
|
118
|
+
}
|
|
119
|
+
function edgePath(runs, curve) {
|
|
120
|
+
'worklet';
|
|
121
|
+
|
|
122
|
+
let d = '';
|
|
123
|
+
for (const run of runs) d += `${segment(run, curve)} `;
|
|
124
|
+
return d;
|
|
125
|
+
}
|
|
126
|
+
function fillPath(runs, curve, baseline) {
|
|
127
|
+
'worklet';
|
|
128
|
+
|
|
129
|
+
let d = '';
|
|
130
|
+
for (const run of runs) {
|
|
131
|
+
const edge = segment(run, curve);
|
|
132
|
+
if (!edge) continue;
|
|
133
|
+
const first = run[0];
|
|
134
|
+
const last = run[run.length - 1];
|
|
135
|
+
d += `${edge} L${last.x},${baseline} L${first.x},${baseline} Z `;
|
|
136
|
+
}
|
|
137
|
+
return d;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The largest value on one side, padded so a peak does not touch the edge. */
|
|
141
|
+
function sideExtent(data, keys, explicit) {
|
|
142
|
+
if (explicit) return Math.max(explicit[1], 0) || 1;
|
|
143
|
+
let max = 0;
|
|
144
|
+
for (const row of data) {
|
|
145
|
+
for (const key of keys) {
|
|
146
|
+
const value = row[key];
|
|
147
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > max) max = value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return max > 0 ? max * 1.08 : 1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Imperative handle: re-run the reveal on demand, for a "replay" control. */
|
|
154
|
+
|
|
155
|
+
function ChildSlot({
|
|
156
|
+
children
|
|
157
|
+
}) {
|
|
158
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
159
|
+
children: children
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function partition(children) {
|
|
163
|
+
const svg = [];
|
|
164
|
+
const series = [];
|
|
165
|
+
const overlay = [];
|
|
166
|
+
const header = [];
|
|
167
|
+
Children.forEach(children, (child, index) => {
|
|
168
|
+
if (! /*#__PURE__*/isValidElement(child)) return;
|
|
169
|
+
const layer = child.type.layer ?? 'svg';
|
|
170
|
+
const slot = /*#__PURE__*/_jsx(ChildSlot, {
|
|
171
|
+
children: child
|
|
172
|
+
}, index);
|
|
173
|
+
const bucket = layer === 'header' ? header : layer === 'overlay' ? overlay : layer === 'series' ? series : svg;
|
|
174
|
+
bucket.push(slot);
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
svg,
|
|
178
|
+
series,
|
|
179
|
+
overlay,
|
|
180
|
+
header
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const MirrorAreaChartRoot = /*#__PURE__*/forwardRef(function MirrorAreaChartRoot({
|
|
184
|
+
className,
|
|
185
|
+
data,
|
|
186
|
+
xDataKey = 'date',
|
|
187
|
+
split = 0.55,
|
|
188
|
+
topDomain,
|
|
189
|
+
bottomDomain,
|
|
190
|
+
curve = 'monotone',
|
|
191
|
+
status = 'ready',
|
|
192
|
+
aspectRatio = 1.6,
|
|
193
|
+
animationDuration = 700,
|
|
194
|
+
domainDuration = 500,
|
|
195
|
+
onActiveIndexChange,
|
|
196
|
+
accessible,
|
|
197
|
+
accessibilityLabel,
|
|
198
|
+
accessibilityHint,
|
|
199
|
+
accessibilityLabelForDatum,
|
|
200
|
+
onAccessibilityDatumPress,
|
|
201
|
+
compact = false,
|
|
202
|
+
children,
|
|
203
|
+
...props
|
|
204
|
+
}, ref) {
|
|
205
|
+
const [size, setSize] = useState({
|
|
206
|
+
width: 0,
|
|
207
|
+
height: 0
|
|
208
|
+
});
|
|
209
|
+
const [registered, setRegistered] = useState([]);
|
|
210
|
+
const [activeIndexJS, setActiveIndexJS] = useState(-1);
|
|
211
|
+
const grow = useSharedValue(0);
|
|
212
|
+
const topMax = useSharedValue(0);
|
|
213
|
+
const bottomMax = useSharedValue(0);
|
|
214
|
+
const activeIndex = useSharedValue(-1);
|
|
215
|
+
const reducedMotion = useReducedMotion();
|
|
216
|
+
const registerSeries = useMemo(() => entry => setRegistered(current => {
|
|
217
|
+
const existing = current.find(item => item.key === entry.key);
|
|
218
|
+
if (existing && existing.color === entry.color && existing.label === entry.label && existing.side === entry.side) {
|
|
219
|
+
return current;
|
|
220
|
+
}
|
|
221
|
+
return [...current.filter(item => item.key !== entry.key), entry];
|
|
222
|
+
}), []);
|
|
223
|
+
const unregisterSeries = useMemo(() => key => setRegistered(current => current.filter(item => item.key !== key)), []);
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* The order the areas were declared in, read from the children rather than
|
|
227
|
+
* from registration: an area re-registers when its colour resolves, and
|
|
228
|
+
* appending it again would reorder the legend and the readout.
|
|
229
|
+
*/
|
|
230
|
+
const {
|
|
231
|
+
hasYAxis,
|
|
232
|
+
order
|
|
233
|
+
} = useMemo(() => {
|
|
234
|
+
let axis = false;
|
|
235
|
+
const keys = [];
|
|
236
|
+
Children.forEach(children, child => {
|
|
237
|
+
if (! /*#__PURE__*/isValidElement(child)) return;
|
|
238
|
+
const type = child.type;
|
|
239
|
+
const childProps = child.props;
|
|
240
|
+
if (type.axis === 'y') axis = true;
|
|
241
|
+
if (type.area && typeof childProps.dataKey === 'string') keys.push(childProps.dataKey);
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
hasYAxis: axis,
|
|
245
|
+
order: keys.join('|')
|
|
246
|
+
};
|
|
247
|
+
}, [children]);
|
|
248
|
+
const series = useMemo(() => {
|
|
249
|
+
const declared = order ? order.split('|') : [];
|
|
250
|
+
const position = key => {
|
|
251
|
+
const index = declared.indexOf(key);
|
|
252
|
+
return index === -1 ? declared.length : index;
|
|
253
|
+
};
|
|
254
|
+
return [...registered].sort((a, b) => position(a.key) - position(b.key));
|
|
255
|
+
}, [registered, order]);
|
|
256
|
+
const pad = compact ? {
|
|
257
|
+
top: 2,
|
|
258
|
+
right: 1,
|
|
259
|
+
bottom: 2,
|
|
260
|
+
left: 1
|
|
261
|
+
} : {
|
|
262
|
+
...PADDING,
|
|
263
|
+
left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left
|
|
264
|
+
};
|
|
265
|
+
const plot = {
|
|
266
|
+
left: pad.left,
|
|
267
|
+
top: pad.top,
|
|
268
|
+
width: Math.max(size.width - pad.left - pad.right, 0),
|
|
269
|
+
height: Math.max(size.height - pad.top - pad.bottom, 0)
|
|
270
|
+
};
|
|
271
|
+
const share = Math.min(Math.max(Number.isFinite(split) ? split : 0.55, 0.1), 0.9);
|
|
272
|
+
const baseline = plot.top + plot.height * share;
|
|
273
|
+
const topKeys = series.filter(item => item.side === 'top').map(item => item.key).join('|');
|
|
274
|
+
const bottomKeys = series.filter(item => item.side === 'bottom').map(item => item.key).join('|');
|
|
275
|
+
const safeTop = finiteChartDomain(topDomain);
|
|
276
|
+
const safeBottom = finiteChartDomain(bottomDomain);
|
|
277
|
+
const topExtent = useMemo(() => sideExtent(data, topKeys ? topKeys.split('|') : [], safeTop),
|
|
278
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
279
|
+
[data, topKeys, safeTop?.[0], safeTop?.[1]]);
|
|
280
|
+
const bottomExtent = useMemo(() => sideExtent(data, bottomKeys ? bottomKeys.split('|') : [], safeBottom),
|
|
281
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
282
|
+
[data, bottomKeys, safeBottom?.[0], safeBottom?.[1]]);
|
|
283
|
+
const loading = status === 'loading';
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (loading) return;
|
|
286
|
+
const first = topMax.value === 0 && bottomMax.value === 0;
|
|
287
|
+
if (first || reducedMotion) {
|
|
288
|
+
topMax.value = topExtent;
|
|
289
|
+
bottomMax.value = bottomExtent;
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
topMax.value = withTiming(topExtent, {
|
|
293
|
+
duration: domainDuration
|
|
294
|
+
});
|
|
295
|
+
bottomMax.value = withTiming(bottomExtent, {
|
|
296
|
+
duration: domainDuration
|
|
297
|
+
});
|
|
298
|
+
}, [topExtent, bottomExtent, loading, reducedMotion, domainDuration, topMax, bottomMax]);
|
|
299
|
+
const revealed = useRef(false);
|
|
300
|
+
const playReveal = useMemo(() => () => {
|
|
301
|
+
if (reducedMotion) {
|
|
302
|
+
grow.value = 1;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
grow.value = 0;
|
|
306
|
+
grow.value = withTiming(1, {
|
|
307
|
+
duration: animationDuration,
|
|
308
|
+
easing: Easing.out(Easing.cubic)
|
|
309
|
+
});
|
|
310
|
+
}, [reducedMotion, animationDuration, grow]);
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
// Going back to `loading` lays the bands flat again, so a refetch grows
|
|
313
|
+
// them out from the baseline instead of appearing complete.
|
|
314
|
+
if (loading) {
|
|
315
|
+
revealed.current = false;
|
|
316
|
+
cancelAnimation(grow);
|
|
317
|
+
grow.value = 0;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (revealed.current || plot.width <= 0 || !data.length) return;
|
|
321
|
+
revealed.current = true;
|
|
322
|
+
playReveal();
|
|
323
|
+
}, [loading, plot.width, data.length, playReveal, grow]);
|
|
324
|
+
useImperativeHandle(ref, () => ({
|
|
325
|
+
replay: playReveal
|
|
326
|
+
}), [playReveal]);
|
|
327
|
+
const handleActiveIndex = useMemo(() => index => {
|
|
328
|
+
setActiveIndexJS(index);
|
|
329
|
+
onActiveIndexChange?.(index, index >= 0 ? data[index] ?? null : null);
|
|
330
|
+
}, [onActiveIndexChange, data]);
|
|
331
|
+
const onLayout = event => {
|
|
332
|
+
const {
|
|
333
|
+
width,
|
|
334
|
+
height
|
|
335
|
+
} = event.nativeEvent.layout;
|
|
336
|
+
setSize(current => Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1 ? current : {
|
|
337
|
+
width,
|
|
338
|
+
height
|
|
339
|
+
});
|
|
340
|
+
props.onLayout?.(event);
|
|
341
|
+
};
|
|
342
|
+
const context = useMemo(() => ({
|
|
343
|
+
data,
|
|
344
|
+
xDataKey,
|
|
345
|
+
plot,
|
|
346
|
+
pad,
|
|
347
|
+
baseline,
|
|
348
|
+
status,
|
|
349
|
+
curve,
|
|
350
|
+
series,
|
|
351
|
+
registerSeries,
|
|
352
|
+
unregisterSeries,
|
|
353
|
+
topMax,
|
|
354
|
+
bottomMax,
|
|
355
|
+
topExtent,
|
|
356
|
+
bottomExtent,
|
|
357
|
+
grow,
|
|
358
|
+
activeIndex,
|
|
359
|
+
activeIndexJS,
|
|
360
|
+
setActiveIndexJS: handleActiveIndex
|
|
361
|
+
}),
|
|
362
|
+
// `plot` and `pad` are rebuilt every render from `size`, so they are
|
|
363
|
+
// compared by value.
|
|
364
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
365
|
+
[data, xDataKey, plot.width, plot.height, plot.left, plot.top, pad.right, baseline, status, curve, series, registerSeries, unregisterSeries, topMax, bottomMax, topExtent, bottomExtent, grow, activeIndex, activeIndexJS, handleActiveIndex]);
|
|
366
|
+
const {
|
|
367
|
+
svg,
|
|
368
|
+
series: seriesLayer,
|
|
369
|
+
overlay,
|
|
370
|
+
header
|
|
371
|
+
} = partition(children);
|
|
372
|
+
return /*#__PURE__*/_jsx(MirrorAreaChartContext.Provider, {
|
|
373
|
+
value: context,
|
|
374
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
375
|
+
...props,
|
|
376
|
+
style: props.style,
|
|
377
|
+
className: cn('w-full', className),
|
|
378
|
+
children: [header, /*#__PURE__*/_jsx(ChartAccessibilityData, {
|
|
379
|
+
chart: "Mirror area chart",
|
|
380
|
+
data: data,
|
|
381
|
+
disabled: accessible === false || loading,
|
|
382
|
+
accessibilityLabel: accessibilityLabel,
|
|
383
|
+
accessibilityHint: accessibilityHint,
|
|
384
|
+
accessibilityLabelForDatum: accessibilityLabelForDatum,
|
|
385
|
+
onAccessibilityDatumPress: onAccessibilityDatumPress,
|
|
386
|
+
valueOf: datum => [[xDataKey, datum[xDataKey]], ...series.map(item => [item.label, datum[item.key]])]
|
|
387
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
388
|
+
onLayout: onLayout,
|
|
389
|
+
style: {
|
|
390
|
+
aspectRatio
|
|
391
|
+
},
|
|
392
|
+
className: "w-full",
|
|
393
|
+
accessibilityElementsHidden: true,
|
|
394
|
+
importantForAccessibility: "no-hide-descendants",
|
|
395
|
+
children: plot.width > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
396
|
+
children: [/*#__PURE__*/_jsxs(Svg, {
|
|
397
|
+
width: "100%",
|
|
398
|
+
height: "100%",
|
|
399
|
+
style: StyleSheet.absoluteFill,
|
|
400
|
+
children: [svg, /*#__PURE__*/_jsx(G, {
|
|
401
|
+
children: seriesLayer
|
|
402
|
+
})]
|
|
403
|
+
}), overlay]
|
|
404
|
+
}) : null
|
|
405
|
+
})]
|
|
406
|
+
})
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
MirrorAreaChartRoot.displayName = 'MirrorAreaChart';
|
|
410
|
+
|
|
411
|
+
/* -------------------------------------------------------------------------- */
|
|
412
|
+
/* SVG layer */
|
|
413
|
+
/* -------------------------------------------------------------------------- */
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* The texture behind the bands. Dots by default, which mark the plot's extent
|
|
417
|
+
* without drawing lines that compete with the baseline.
|
|
418
|
+
*/
|
|
419
|
+
function MirrorAreaChartGrid({
|
|
420
|
+
variant = 'dots',
|
|
421
|
+
spacing,
|
|
422
|
+
color,
|
|
423
|
+
opacity = 1
|
|
424
|
+
}) {
|
|
425
|
+
const {
|
|
426
|
+
plot,
|
|
427
|
+
baseline
|
|
428
|
+
} = useChart('MirrorAreaChart.Grid');
|
|
429
|
+
const token = useCSSVariable('--color-border');
|
|
430
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(0,0,0,0.1)');
|
|
431
|
+
const dots = useMemo(() => {
|
|
432
|
+
if (variant !== 'dots') return '';
|
|
433
|
+
const gap = Math.max(spacing ?? 12, 4);
|
|
434
|
+
const points = [];
|
|
435
|
+
const columns = Math.floor(plot.width / gap);
|
|
436
|
+
const offsetX = plot.left + (plot.width - columns * gap) / 2;
|
|
437
|
+
// Rows are laid out from the baseline in both directions, so one row
|
|
438
|
+
// always lies on it and the pattern is symmetrical about it.
|
|
439
|
+
for (let y = baseline; y >= plot.top - 0.5; y -= gap) {
|
|
440
|
+
for (let c = 0; c <= columns; c += 1) points.push({
|
|
441
|
+
x: offsetX + c * gap,
|
|
442
|
+
y
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
for (let y = baseline + gap; y <= plot.top + plot.height + 0.5; y += gap) {
|
|
446
|
+
for (let c = 0; c <= columns; c += 1) points.push({
|
|
447
|
+
x: offsetX + c * gap,
|
|
448
|
+
y
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return dotsPath(points, 0.9);
|
|
452
|
+
}, [variant, spacing, plot.left, plot.top, plot.width, plot.height, baseline]);
|
|
453
|
+
if (variant === 'dots') {
|
|
454
|
+
return /*#__PURE__*/_jsx(Path, {
|
|
455
|
+
d: dots,
|
|
456
|
+
fill: stroke,
|
|
457
|
+
opacity: opacity
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
const rules = Math.max(Math.round(spacing ?? 2), 1);
|
|
461
|
+
const lines = [];
|
|
462
|
+
for (let i = 1; i <= rules; i += 1) {
|
|
463
|
+
lines.push(baseline - (baseline - plot.top) * i / rules);
|
|
464
|
+
lines.push(baseline + (plot.top + plot.height - baseline) * i / rules);
|
|
465
|
+
}
|
|
466
|
+
return /*#__PURE__*/_jsx(G, {
|
|
467
|
+
opacity: opacity,
|
|
468
|
+
children: lines.map(y => /*#__PURE__*/_jsx(SvgLine, {
|
|
469
|
+
x1: plot.left,
|
|
470
|
+
x2: plot.left + plot.width,
|
|
471
|
+
y1: y,
|
|
472
|
+
y2: y,
|
|
473
|
+
stroke: stroke,
|
|
474
|
+
strokeDasharray: "4,6",
|
|
475
|
+
strokeWidth: 1
|
|
476
|
+
}, y))
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
MirrorAreaChartGrid.displayName = 'MirrorAreaChart.Grid';
|
|
480
|
+
MirrorAreaChartGrid.layer = 'svg';
|
|
481
|
+
/**
|
|
482
|
+
* The line both halves grow away from. Declare it after the areas so it is
|
|
483
|
+
* drawn over their bases.
|
|
484
|
+
*/
|
|
485
|
+
function MirrorAreaChartBaseline({
|
|
486
|
+
color,
|
|
487
|
+
strokeWidth = 1,
|
|
488
|
+
opacity = 0.5
|
|
489
|
+
}) {
|
|
490
|
+
const {
|
|
491
|
+
plot,
|
|
492
|
+
baseline
|
|
493
|
+
} = useChart('MirrorAreaChart.Baseline');
|
|
494
|
+
const token = useCSSVariable('--color-muted-foreground');
|
|
495
|
+
const stroke = color ?? (typeof token === 'string' ? token : '#737373');
|
|
496
|
+
return /*#__PURE__*/_jsx(SvgLine, {
|
|
497
|
+
x1: plot.left,
|
|
498
|
+
x2: plot.left + plot.width,
|
|
499
|
+
y1: baseline,
|
|
500
|
+
y2: baseline,
|
|
501
|
+
stroke: stroke,
|
|
502
|
+
strokeWidth: strokeWidth,
|
|
503
|
+
opacity: opacity
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
MirrorAreaChartBaseline.displayName = 'MirrorAreaChart.Baseline';
|
|
507
|
+
MirrorAreaChartBaseline.layer = 'series';
|
|
508
|
+
/**
|
|
509
|
+
* One band, growing away from the baseline on its side. Areas on one side
|
|
510
|
+
* overlay in declaration order, so a whole declared first is drawn behind a
|
|
511
|
+
* part of it declared second.
|
|
512
|
+
*/
|
|
513
|
+
function MirrorAreaChartArea({
|
|
514
|
+
dataKey,
|
|
515
|
+
side = 'top',
|
|
516
|
+
label,
|
|
517
|
+
color,
|
|
518
|
+
colorIndex = 1,
|
|
519
|
+
muted = false,
|
|
520
|
+
fillOpacity,
|
|
521
|
+
gradientToOpacity,
|
|
522
|
+
showLine = true,
|
|
523
|
+
strokeWidth = 1.5
|
|
524
|
+
}) {
|
|
525
|
+
const {
|
|
526
|
+
data,
|
|
527
|
+
plot,
|
|
528
|
+
baseline,
|
|
529
|
+
curve,
|
|
530
|
+
registerSeries,
|
|
531
|
+
unregisterSeries,
|
|
532
|
+
topMax,
|
|
533
|
+
bottomMax,
|
|
534
|
+
grow
|
|
535
|
+
} = useChart('MirrorAreaChart.Area');
|
|
536
|
+
const seriesColor = useSeriesColor(color, colorIndex);
|
|
537
|
+
const mutedToken = useCSSVariable('--color-muted-foreground');
|
|
538
|
+
const stroke = muted && !color ? typeof mutedToken === 'string' ? mutedToken : '#737373' : seriesColor;
|
|
539
|
+
const name = label ?? dataKey;
|
|
540
|
+
const gradientId = `panelui-mirror-fill-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
541
|
+
const outer = fillOpacity ?? (muted ? 0.22 : 0.3);
|
|
542
|
+
const inner = gradientToOpacity ?? outer;
|
|
543
|
+
useEffect(() => {
|
|
544
|
+
registerSeries({
|
|
545
|
+
key: dataKey,
|
|
546
|
+
color: stroke,
|
|
547
|
+
label: name,
|
|
548
|
+
side
|
|
549
|
+
});
|
|
550
|
+
}, [dataKey, stroke, name, side, registerSeries]);
|
|
551
|
+
useEffect(() => () => unregisterSeries(dataKey), [dataKey, unregisterSeries]);
|
|
552
|
+
const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
|
|
553
|
+
const top = side === 'top';
|
|
554
|
+
const height = top ? baseline - plot.top : plot.top + plot.height - baseline;
|
|
555
|
+
const fillProps = useAnimatedProps(() => {
|
|
556
|
+
const runs = edgeRuns(values, plot, baseline, height, top ? topMax.value : bottomMax.value, grow.value, top ? -1 : 1);
|
|
557
|
+
return {
|
|
558
|
+
d: fillPath(runs, curve, baseline)
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
const lineProps = useAnimatedProps(() => {
|
|
562
|
+
const runs = edgeRuns(values, plot, baseline, height, top ? topMax.value : bottomMax.value, grow.value, top ? -1 : 1);
|
|
563
|
+
// Hidden while the band is flat, or the edge lines of every area are
|
|
564
|
+
// drawn along the baseline over the skeleton while the chart waits.
|
|
565
|
+
return {
|
|
566
|
+
d: edgePath(runs, curve),
|
|
567
|
+
opacity: grow.value > 0.001 ? 1 : 0
|
|
568
|
+
};
|
|
569
|
+
});
|
|
570
|
+
return /*#__PURE__*/_jsxs(G, {
|
|
571
|
+
children: [/*#__PURE__*/_jsx(Defs, {
|
|
572
|
+
children: /*#__PURE__*/_jsxs(LinearGradient, {
|
|
573
|
+
id: gradientId,
|
|
574
|
+
x1: "0",
|
|
575
|
+
y1: "0",
|
|
576
|
+
x2: "0",
|
|
577
|
+
y2: "1",
|
|
578
|
+
children: [/*#__PURE__*/_jsx(Stop, {
|
|
579
|
+
offset: "0",
|
|
580
|
+
stopColor: stroke,
|
|
581
|
+
stopOpacity: top ? outer : inner
|
|
582
|
+
}), /*#__PURE__*/_jsx(Stop, {
|
|
583
|
+
offset: "1",
|
|
584
|
+
stopColor: stroke,
|
|
585
|
+
stopOpacity: top ? inner : outer
|
|
586
|
+
})]
|
|
587
|
+
})
|
|
588
|
+
}), /*#__PURE__*/_jsx(AnimatedPath, {
|
|
589
|
+
animatedProps: fillProps,
|
|
590
|
+
fill: `url(#${gradientId})`
|
|
591
|
+
}), showLine ? /*#__PURE__*/_jsx(AnimatedPath, {
|
|
592
|
+
animatedProps: lineProps,
|
|
593
|
+
stroke: stroke,
|
|
594
|
+
strokeWidth: strokeWidth,
|
|
595
|
+
strokeLinecap: "round",
|
|
596
|
+
strokeLinejoin: "round",
|
|
597
|
+
fill: "none"
|
|
598
|
+
}) : null]
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
MirrorAreaChartArea.displayName = 'MirrorAreaChart.Area';
|
|
602
|
+
MirrorAreaChartArea.layer = 'series';
|
|
603
|
+
// Read by the root, for the order the areas were declared in.
|
|
604
|
+
MirrorAreaChartArea.area = true;
|
|
605
|
+
/**
|
|
606
|
+
* The loading state: a low band either side of the baseline with a highlight
|
|
607
|
+
* travelling across it. Flat, so it says where the bands will be and nothing
|
|
608
|
+
* about their shape.
|
|
609
|
+
*/
|
|
610
|
+
function MirrorAreaChartSkeleton({
|
|
611
|
+
duration = 1400,
|
|
612
|
+
color
|
|
613
|
+
}) {
|
|
614
|
+
const {
|
|
615
|
+
plot,
|
|
616
|
+
baseline,
|
|
617
|
+
status
|
|
618
|
+
} = useChart('MirrorAreaChart.Skeleton');
|
|
619
|
+
const token = useCSSVariable('--color-skeleton');
|
|
620
|
+
const base = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
621
|
+
const highlight = useSeriesColor(undefined, 1);
|
|
622
|
+
const sweep = useSharedValue(0);
|
|
623
|
+
const reducedMotion = useReducedMotion();
|
|
624
|
+
const loading = status === 'loading';
|
|
625
|
+
useEffect(() => {
|
|
626
|
+
if (!loading || reducedMotion) {
|
|
627
|
+
cancelAnimation(sweep);
|
|
628
|
+
sweep.value = 0;
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
sweep.value = 0;
|
|
632
|
+
sweep.value = withRepeat(withTiming(1, {
|
|
633
|
+
duration,
|
|
634
|
+
easing: Easing.linear
|
|
635
|
+
}), -1, false);
|
|
636
|
+
return () => cancelAnimation(sweep);
|
|
637
|
+
}, [loading, reducedMotion, duration, sweep]);
|
|
638
|
+
const animatedProps = useAnimatedProps(() => ({
|
|
639
|
+
x1: `${(sweep.value * 1.4 - 0.4) * 100}%`,
|
|
640
|
+
x2: `${(sweep.value * 1.4 - 0.4 + 0.4) * 100}%`
|
|
641
|
+
}));
|
|
642
|
+
if (!loading) return null;
|
|
643
|
+
const above = (baseline - plot.top) * 0.18;
|
|
644
|
+
const below = (plot.top + plot.height - baseline) * 0.18;
|
|
645
|
+
const gradientId = 'panelui-mirror-skeleton';
|
|
646
|
+
return /*#__PURE__*/_jsxs(G, {
|
|
647
|
+
children: [/*#__PURE__*/_jsx(Defs, {
|
|
648
|
+
children: /*#__PURE__*/_jsxs(AnimatedLinearGradient, {
|
|
649
|
+
id: gradientId,
|
|
650
|
+
animatedProps: animatedProps,
|
|
651
|
+
y1: "0",
|
|
652
|
+
y2: "0",
|
|
653
|
+
children: [/*#__PURE__*/_jsx(Stop, {
|
|
654
|
+
offset: "0",
|
|
655
|
+
...colorStop(base)
|
|
656
|
+
}), /*#__PURE__*/_jsx(Stop, {
|
|
657
|
+
offset: "0.5",
|
|
658
|
+
stopColor: highlight,
|
|
659
|
+
stopOpacity: 0.3
|
|
660
|
+
}), /*#__PURE__*/_jsx(Stop, {
|
|
661
|
+
offset: "1",
|
|
662
|
+
...colorStop(base)
|
|
663
|
+
})]
|
|
664
|
+
})
|
|
665
|
+
}), /*#__PURE__*/_jsx(Rect, {
|
|
666
|
+
x: plot.left,
|
|
667
|
+
y: baseline - above,
|
|
668
|
+
width: plot.width,
|
|
669
|
+
height: above + below,
|
|
670
|
+
fill: `url(#${gradientId})`
|
|
671
|
+
})]
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
MirrorAreaChartSkeleton.displayName = 'MirrorAreaChart.Skeleton';
|
|
675
|
+
MirrorAreaChartSkeleton.layer = 'svg';
|
|
676
|
+
|
|
677
|
+
/* -------------------------------------------------------------------------- */
|
|
678
|
+
/* Overlay layer */
|
|
679
|
+
/* -------------------------------------------------------------------------- */
|
|
680
|
+
|
|
681
|
+
/** The x labels. Real text, so they follow the theme's font and text scaling. */
|
|
682
|
+
function MirrorAreaChartXAxis({
|
|
683
|
+
ticks = 4,
|
|
684
|
+
format,
|
|
685
|
+
className
|
|
686
|
+
}) {
|
|
687
|
+
const {
|
|
688
|
+
data,
|
|
689
|
+
xDataKey,
|
|
690
|
+
plot,
|
|
691
|
+
pad
|
|
692
|
+
} = useChart('MirrorAreaChart.XAxis');
|
|
693
|
+
const labels = useMemo(() => {
|
|
694
|
+
if (!data.length) return [];
|
|
695
|
+
const count = Math.min(ticks, data.length);
|
|
696
|
+
const step = count > 1 ? (data.length - 1) / (count - 1) : 0;
|
|
697
|
+
return Array.from({
|
|
698
|
+
length: count
|
|
699
|
+
}, (_unused, index) => {
|
|
700
|
+
const dataIndex = Math.round(index * step);
|
|
701
|
+
const datum = data[dataIndex];
|
|
702
|
+
if (!datum) return null;
|
|
703
|
+
return {
|
|
704
|
+
key: dataIndex,
|
|
705
|
+
text: format ? format(datum, dataIndex) : String(datum[xDataKey] ?? '')
|
|
706
|
+
};
|
|
707
|
+
}).filter(label => label !== null);
|
|
708
|
+
}, [data, ticks, format, xDataKey]);
|
|
709
|
+
return /*#__PURE__*/_jsx(View, {
|
|
710
|
+
style: {
|
|
711
|
+
position: 'absolute',
|
|
712
|
+
inset: 0,
|
|
713
|
+
pointerEvents: 'none'
|
|
714
|
+
},
|
|
715
|
+
className: cn(className),
|
|
716
|
+
children: labels.map(label => /*#__PURE__*/_jsx(Text, {
|
|
717
|
+
size: "xs",
|
|
718
|
+
muted: true,
|
|
719
|
+
numberOfLines: 1,
|
|
720
|
+
style: {
|
|
721
|
+
position: 'absolute',
|
|
722
|
+
bottom: 0,
|
|
723
|
+
left: Math.max(0, Math.min(xOf(label.key, data.length, plot) - POINT_LABEL_WIDTH / 2, plot.left + plot.width + pad.right - POINT_LABEL_WIDTH)),
|
|
724
|
+
width: POINT_LABEL_WIDTH,
|
|
725
|
+
textAlign: 'center'
|
|
726
|
+
},
|
|
727
|
+
children: label.text
|
|
728
|
+
}, label.key))
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
MirrorAreaChartXAxis.displayName = 'MirrorAreaChart.XAxis';
|
|
732
|
+
MirrorAreaChartXAxis.layer = 'overlay';
|
|
733
|
+
/**
|
|
734
|
+
* Three labels down the side: the top half's maximum, zero on the baseline,
|
|
735
|
+
* and the bottom half's maximum. Two scales need two labels, and a column of
|
|
736
|
+
* evenly spaced numbers would suggest one.
|
|
737
|
+
*/
|
|
738
|
+
function MirrorAreaChartYAxis({
|
|
739
|
+
formatTop,
|
|
740
|
+
formatBottom,
|
|
741
|
+
className
|
|
742
|
+
}) {
|
|
743
|
+
const {
|
|
744
|
+
plot,
|
|
745
|
+
baseline,
|
|
746
|
+
topExtent,
|
|
747
|
+
bottomExtent
|
|
748
|
+
} = useChart('MirrorAreaChart.YAxis');
|
|
749
|
+
const width = Math.max(plot.left - Y_AXIS_GUTTER, 0);
|
|
750
|
+
const fmtTop = formatTop ?? compactNumber;
|
|
751
|
+
const fmtBottom = formatBottom ?? compactNumber;
|
|
752
|
+
const labels = [{
|
|
753
|
+
key: 'top',
|
|
754
|
+
y: plot.top,
|
|
755
|
+
text: fmtTop(topExtent)
|
|
756
|
+
}, {
|
|
757
|
+
key: 'zero',
|
|
758
|
+
y: baseline,
|
|
759
|
+
text: '0'
|
|
760
|
+
}, {
|
|
761
|
+
key: 'bottom',
|
|
762
|
+
y: plot.top + plot.height,
|
|
763
|
+
text: fmtBottom(bottomExtent)
|
|
764
|
+
}];
|
|
765
|
+
return /*#__PURE__*/_jsx(View, {
|
|
766
|
+
style: {
|
|
767
|
+
position: 'absolute',
|
|
768
|
+
inset: 0,
|
|
769
|
+
pointerEvents: 'none'
|
|
770
|
+
},
|
|
771
|
+
className: cn(className),
|
|
772
|
+
children: labels.map(label => /*#__PURE__*/_jsx(Text, {
|
|
773
|
+
size: "xs",
|
|
774
|
+
muted: true,
|
|
775
|
+
numberOfLines: 1,
|
|
776
|
+
style: {
|
|
777
|
+
position: 'absolute',
|
|
778
|
+
left: 0,
|
|
779
|
+
top: label.y - AXIS_LABEL_HEIGHT / 2,
|
|
780
|
+
width,
|
|
781
|
+
textAlign: 'right'
|
|
782
|
+
},
|
|
783
|
+
children: label.text
|
|
784
|
+
}, label.key))
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
MirrorAreaChartYAxis.displayName = 'MirrorAreaChart.YAxis';
|
|
788
|
+
MirrorAreaChartYAxis.layer = 'overlay';
|
|
789
|
+
// Read by the root, which leaves room for the labels before laying the plot out.
|
|
790
|
+
MirrorAreaChartYAxis.axis = 'y';
|
|
791
|
+
/**
|
|
792
|
+
* A dashed crosshair, a dot on the outer edge of the top half, and a readout
|
|
793
|
+
* beside them. The hit area is the whole plot.
|
|
794
|
+
*/
|
|
795
|
+
function MirrorAreaChartTooltip({
|
|
796
|
+
color,
|
|
797
|
+
formatValue,
|
|
798
|
+
formatX,
|
|
799
|
+
formatSummary,
|
|
800
|
+
className
|
|
801
|
+
}) {
|
|
802
|
+
const {
|
|
803
|
+
data,
|
|
804
|
+
xDataKey,
|
|
805
|
+
plot,
|
|
806
|
+
baseline,
|
|
807
|
+
series,
|
|
808
|
+
topMax,
|
|
809
|
+
bottomMax,
|
|
810
|
+
grow,
|
|
811
|
+
activeIndex,
|
|
812
|
+
activeIndexJS,
|
|
813
|
+
setActiveIndexJS,
|
|
814
|
+
status
|
|
815
|
+
} = useChart('MirrorAreaChart.Tooltip');
|
|
816
|
+
const token = useCSSVariable('--color-foreground');
|
|
817
|
+
const stroke = color ?? (typeof token === 'string' ? token : '#888888');
|
|
818
|
+
const background = useCSSVariable('--color-background');
|
|
819
|
+
const ring = typeof background === 'string' ? background : 'white';
|
|
820
|
+
const total = data.length;
|
|
821
|
+
const left = plot.left;
|
|
822
|
+
const width = plot.width;
|
|
823
|
+
|
|
824
|
+
// Declared inside the memo, next to its callers: a worklet may only call
|
|
825
|
+
// another worklet.
|
|
826
|
+
const pan = useMemo(() => {
|
|
827
|
+
const resolve = x => {
|
|
828
|
+
'worklet';
|
|
829
|
+
|
|
830
|
+
if (total < 1 || width <= 0) return;
|
|
831
|
+
const ratio = total < 2 ? 0 : (x - left) / width;
|
|
832
|
+
const next = Math.round(Math.min(1, Math.max(0, ratio)) * (total - 1));
|
|
833
|
+
if (next === activeIndex.value) return;
|
|
834
|
+
activeIndex.value = next;
|
|
835
|
+
runOnJS(setActiveIndexJS)(next);
|
|
836
|
+
};
|
|
837
|
+
return Gesture.Pan().minDistance(0).onBegin(event => {
|
|
838
|
+
'worklet';
|
|
839
|
+
|
|
840
|
+
resolve(event.x);
|
|
841
|
+
}).onUpdate(event => {
|
|
842
|
+
'worklet';
|
|
843
|
+
|
|
844
|
+
resolve(event.x);
|
|
845
|
+
}).onFinalize(() => {
|
|
846
|
+
'worklet';
|
|
847
|
+
|
|
848
|
+
activeIndex.value = -1;
|
|
849
|
+
runOnJS(setActiveIndexJS)(-1);
|
|
850
|
+
});
|
|
851
|
+
}, [total, left, width, activeIndex, setActiveIndexJS]);
|
|
852
|
+
|
|
853
|
+
/*
|
|
854
|
+
* The dot sits on the outermost edge on the top half — the largest value of
|
|
855
|
+
* any top area at that point — or on the bottom half's when there is no top.
|
|
856
|
+
*/
|
|
857
|
+
const topSeries = series.filter(item => item.side === 'top');
|
|
858
|
+
const dotSide = topSeries.length ? 'top' : 'bottom';
|
|
859
|
+
const dotValues = useMemo(() => {
|
|
860
|
+
const keys = series.filter(item => item.side === dotSide).map(item => item.key);
|
|
861
|
+
return data.map(row => {
|
|
862
|
+
let most = null;
|
|
863
|
+
for (const key of keys) {
|
|
864
|
+
const value = row[key];
|
|
865
|
+
if (typeof value === 'number' && Number.isFinite(value) && (most === null || value > most)) {
|
|
866
|
+
most = value;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return most;
|
|
870
|
+
});
|
|
871
|
+
}, [data, series, dotSide]);
|
|
872
|
+
const topHeight = baseline - plot.top;
|
|
873
|
+
const bottomHeight = plot.top + plot.height - baseline;
|
|
874
|
+
const dotProps = useAnimatedProps(() => {
|
|
875
|
+
const index = activeIndex.value;
|
|
876
|
+
const value = index >= 0 ? dotValues[index] : null;
|
|
877
|
+
if (index < 0 || value === null || value === undefined) return {
|
|
878
|
+
opacity: 0,
|
|
879
|
+
cx: 0,
|
|
880
|
+
cy: 0
|
|
881
|
+
};
|
|
882
|
+
const up = dotSide === 'top';
|
|
883
|
+
const max = up ? topMax.value : bottomMax.value;
|
|
884
|
+
const reach = Math.max(value, 0) / (max > 0 ? max : 1) * (up ? topHeight : bottomHeight) * grow.value;
|
|
885
|
+
return {
|
|
886
|
+
opacity: 1,
|
|
887
|
+
cx: xOf(index, total, plot),
|
|
888
|
+
cy: baseline + (up ? -reach : reach)
|
|
889
|
+
};
|
|
890
|
+
});
|
|
891
|
+
const crosshairProps = useAnimatedProps(() => {
|
|
892
|
+
const index = activeIndex.value;
|
|
893
|
+
const x = index < 0 ? 0 : xOf(index, total, plot);
|
|
894
|
+
return {
|
|
895
|
+
x1: x,
|
|
896
|
+
x2: x,
|
|
897
|
+
opacity: index < 0 ? 0 : 0.6
|
|
898
|
+
};
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Beside the crosshair, on whichever side has room, so it never covers the
|
|
902
|
+
// point it describes.
|
|
903
|
+
const readoutStyle = useAnimatedStyle(() => {
|
|
904
|
+
const index = activeIndex.value;
|
|
905
|
+
if (index < 0) return {
|
|
906
|
+
opacity: 0
|
|
907
|
+
};
|
|
908
|
+
const x = xOf(index, total, plot);
|
|
909
|
+
// Clear of the dot's radius and its ring, not only of the crosshair.
|
|
910
|
+
const gap = DOT / 2 + 10;
|
|
911
|
+
const fitsLeft = x - gap - READOUT_WIDTH >= plot.left;
|
|
912
|
+
const at = fitsLeft ? x - gap - READOUT_WIDTH : Math.min(x + gap, plot.left + plot.width - READOUT_WIDTH);
|
|
913
|
+
return {
|
|
914
|
+
opacity: 1,
|
|
915
|
+
transform: [{
|
|
916
|
+
translateX: at
|
|
917
|
+
}]
|
|
918
|
+
};
|
|
919
|
+
});
|
|
920
|
+
const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
|
|
921
|
+
const fmtValue = formatValue ?? (value => compactNumber(value));
|
|
922
|
+
const fmtX = formatX ?? (datum => String(datum[xDataKey] ?? ''));
|
|
923
|
+
if (status === 'loading') return null;
|
|
924
|
+
return /*#__PURE__*/_jsx(GestureDetector, {
|
|
925
|
+
gesture: pan,
|
|
926
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
927
|
+
style: StyleSheet.absoluteFill,
|
|
928
|
+
children: [/*#__PURE__*/_jsxs(Svg, {
|
|
929
|
+
width: "100%",
|
|
930
|
+
height: "100%",
|
|
931
|
+
style: StyleSheet.absoluteFill,
|
|
932
|
+
pointerEvents: "none",
|
|
933
|
+
children: [/*#__PURE__*/_jsx(AnimatedLine, {
|
|
934
|
+
animatedProps: crosshairProps,
|
|
935
|
+
y1: plot.top,
|
|
936
|
+
y2: plot.top + plot.height,
|
|
937
|
+
stroke: stroke,
|
|
938
|
+
strokeWidth: 1,
|
|
939
|
+
strokeDasharray: "3,4"
|
|
940
|
+
}), /*#__PURE__*/_jsx(AnimatedCircle, {
|
|
941
|
+
animatedProps: dotProps,
|
|
942
|
+
r: DOT / 2,
|
|
943
|
+
fill: stroke,
|
|
944
|
+
stroke: ring,
|
|
945
|
+
strokeWidth: 2
|
|
946
|
+
})]
|
|
947
|
+
}), /*#__PURE__*/_jsx(Animated.View, {
|
|
948
|
+
pointerEvents: "none",
|
|
949
|
+
style: [{
|
|
950
|
+
position: 'absolute',
|
|
951
|
+
left: 0,
|
|
952
|
+
top: plot.top,
|
|
953
|
+
width: READOUT_WIDTH
|
|
954
|
+
}, readoutStyle],
|
|
955
|
+
children: active ? /*#__PURE__*/_jsxs(View, {
|
|
956
|
+
className: cn('gap-0.5 rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg', className),
|
|
957
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
958
|
+
size: "xs",
|
|
959
|
+
muted: true,
|
|
960
|
+
numberOfLines: 1,
|
|
961
|
+
children: fmtX(active)
|
|
962
|
+
}), formatSummary ? /*#__PURE__*/_jsx(Text, {
|
|
963
|
+
size: "xs",
|
|
964
|
+
weight: "semibold",
|
|
965
|
+
numberOfLines: 2,
|
|
966
|
+
children: formatSummary(active)
|
|
967
|
+
}) : series.map(item => {
|
|
968
|
+
const value = active[item.key];
|
|
969
|
+
if (typeof value !== 'number') return null;
|
|
970
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
971
|
+
className: "flex-row items-center gap-1.5",
|
|
972
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
973
|
+
style: {
|
|
974
|
+
width: 6,
|
|
975
|
+
height: 6,
|
|
976
|
+
borderRadius: 3,
|
|
977
|
+
backgroundColor: item.color
|
|
978
|
+
}
|
|
979
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
980
|
+
size: "xs",
|
|
981
|
+
weight: "medium",
|
|
982
|
+
children: fmtValue(value, item.key)
|
|
983
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
984
|
+
size: "xs",
|
|
985
|
+
muted: true,
|
|
986
|
+
numberOfLines: 1,
|
|
987
|
+
className: "shrink",
|
|
988
|
+
children: item.label
|
|
989
|
+
})]
|
|
990
|
+
}, item.key);
|
|
991
|
+
})]
|
|
992
|
+
}) : null
|
|
993
|
+
})]
|
|
994
|
+
})
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
MirrorAreaChartTooltip.displayName = 'MirrorAreaChart.Tooltip';
|
|
998
|
+
MirrorAreaChartTooltip.layer = 'overlay';
|
|
999
|
+
/** A swatch and a name per area, top half first. */
|
|
1000
|
+
function MirrorAreaChartLegend({
|
|
1001
|
+
className,
|
|
1002
|
+
...props
|
|
1003
|
+
}) {
|
|
1004
|
+
const {
|
|
1005
|
+
series
|
|
1006
|
+
} = useChart('MirrorAreaChart.Legend');
|
|
1007
|
+
if (!series.length) return null;
|
|
1008
|
+
return /*#__PURE__*/_jsx(View, {
|
|
1009
|
+
...props,
|
|
1010
|
+
style: [{
|
|
1011
|
+
pointerEvents: 'none'
|
|
1012
|
+
}, props.style],
|
|
1013
|
+
className: cn('absolute right-2 top-0 flex-row flex-wrap gap-3', className),
|
|
1014
|
+
children: sidesInOrder(series).map(item => /*#__PURE__*/_jsx(SeriesSwatch, {
|
|
1015
|
+
color: item.color,
|
|
1016
|
+
label: item.label
|
|
1017
|
+
}, item.key))
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
MirrorAreaChartLegend.displayName = 'MirrorAreaChart.Legend';
|
|
1021
|
+
MirrorAreaChartLegend.layer = 'overlay';
|
|
1022
|
+
|
|
1023
|
+
/** Top-half areas first, then the bottom half's, each in declaration order. */
|
|
1024
|
+
function sidesInOrder(series) {
|
|
1025
|
+
return [...series.filter(item => item.side === 'top'), ...series.filter(item => item.side === 'bottom')];
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
/** One series' colour and name. Shared by the legend and the header. */
|
|
1029
|
+
function SeriesSwatch({
|
|
1030
|
+
color,
|
|
1031
|
+
label
|
|
1032
|
+
}) {
|
|
1033
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
1034
|
+
className: "flex-row items-center gap-1.5",
|
|
1035
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
1036
|
+
style: {
|
|
1037
|
+
backgroundColor: color
|
|
1038
|
+
},
|
|
1039
|
+
className: "h-2 w-2 rounded-full"
|
|
1040
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
1041
|
+
size: "xs",
|
|
1042
|
+
muted: true,
|
|
1043
|
+
children: label
|
|
1044
|
+
})]
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/* -------------------------------------------------------------------------- */
|
|
1049
|
+
/* Header layer */
|
|
1050
|
+
/* -------------------------------------------------------------------------- */
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* The strip above the plot: what the chart is of and what it currently reads.
|
|
1054
|
+
* The value is not derived here — pass the formatted string, from
|
|
1055
|
+
* `onActiveIndexChange` if it follows the finger.
|
|
1056
|
+
*/
|
|
1057
|
+
function MirrorAreaChartHeader({
|
|
1058
|
+
className,
|
|
1059
|
+
title,
|
|
1060
|
+
value,
|
|
1061
|
+
caption,
|
|
1062
|
+
legend = false,
|
|
1063
|
+
children,
|
|
1064
|
+
...props
|
|
1065
|
+
}) {
|
|
1066
|
+
const {
|
|
1067
|
+
series
|
|
1068
|
+
} = useChart('MirrorAreaChart.Header');
|
|
1069
|
+
const trailing = children ?? (legend && series.length ? /*#__PURE__*/_jsx(View, {
|
|
1070
|
+
className: "flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1",
|
|
1071
|
+
children: sidesInOrder(series).map(item => /*#__PURE__*/_jsx(SeriesSwatch, {
|
|
1072
|
+
color: item.color,
|
|
1073
|
+
label: item.label
|
|
1074
|
+
}, item.key))
|
|
1075
|
+
}) : null);
|
|
1076
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
1077
|
+
...props,
|
|
1078
|
+
className: cn('flex-row items-start justify-between gap-3 pb-3', className),
|
|
1079
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
1080
|
+
className: "flex-1 gap-0.5",
|
|
1081
|
+
children: [title ? /*#__PURE__*/_jsx(Text, {
|
|
1082
|
+
size: "xs",
|
|
1083
|
+
muted: true,
|
|
1084
|
+
children: title
|
|
1085
|
+
}) : null, value ? /*#__PURE__*/_jsx(Text, {
|
|
1086
|
+
size: "xl",
|
|
1087
|
+
weight: "bold",
|
|
1088
|
+
children: value
|
|
1089
|
+
}) : null, caption ? /*#__PURE__*/_jsx(Text, {
|
|
1090
|
+
size: "xs",
|
|
1091
|
+
muted: true,
|
|
1092
|
+
children: caption
|
|
1093
|
+
}) : null]
|
|
1094
|
+
}), trailing ? /*#__PURE__*/_jsx(View, {
|
|
1095
|
+
className: "shrink pt-1",
|
|
1096
|
+
children: trailing
|
|
1097
|
+
}) : null]
|
|
1098
|
+
});
|
|
1099
|
+
}
|
|
1100
|
+
MirrorAreaChartHeader.displayName = 'MirrorAreaChart.Header';
|
|
1101
|
+
MirrorAreaChartHeader.layer = 'header';
|
|
1102
|
+
export const MirrorAreaChart = Object.assign(MirrorAreaChartRoot, {
|
|
1103
|
+
Header: MirrorAreaChartHeader,
|
|
1104
|
+
Grid: MirrorAreaChartGrid,
|
|
1105
|
+
Area: MirrorAreaChartArea,
|
|
1106
|
+
Baseline: MirrorAreaChartBaseline,
|
|
1107
|
+
Skeleton: MirrorAreaChartSkeleton,
|
|
1108
|
+
XAxis: MirrorAreaChartXAxis,
|
|
1109
|
+
YAxis: MirrorAreaChartYAxis,
|
|
1110
|
+
Tooltip: MirrorAreaChartTooltip,
|
|
1111
|
+
Legend: MirrorAreaChartLegend
|
|
1112
|
+
});
|
|
1113
|
+
//# sourceMappingURL=index.js.map
|