panelui-native 0.61.1 → 0.62.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/lib/module/components/live-line-chart/index.js +1063 -0
- package/lib/module/components/live-line-chart/index.js.map +1 -0
- package/lib/module/components/polar-area-chart/index.js +719 -0
- package/lib/module/components/polar-area-chart/index.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/chart.js +40 -0
- package/lib/module/utils/chart.js.map +1 -1
- package/lib/typescript/src/components/live-line-chart/index.d.ts +293 -0
- package/lib/typescript/src/components/live-line-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/polar-area-chart/index.d.ts +259 -0
- package/lib/typescript/src/components/polar-area-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +2 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +15 -14
- package/lib/typescript/src/utils/chart.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/live-line-chart/index.tsx +1294 -0
- package/src/components/polar-area-chart/index.tsx +928 -0
- package/src/index.ts +35 -0
- package/src/utils/chart.ts +44 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PolarAreaChart — several parts, compared on one measure.
|
|
5
|
+
*
|
|
6
|
+
* ```tsx
|
|
7
|
+
* <PolarAreaChart data={scores}>
|
|
8
|
+
* <PolarAreaChart.Header title="Scores" />
|
|
9
|
+
* <PolarAreaChart.Grid />
|
|
10
|
+
* <PolarAreaChart.Wedges />
|
|
11
|
+
* <PolarAreaChart.Labels />
|
|
12
|
+
* <PolarAreaChart.Legend />
|
|
13
|
+
* </PolarAreaChart>
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* ## What it is, against the pie beside it
|
|
17
|
+
*
|
|
18
|
+
* A pie divides one total: the angles are the quantity and they must come to a
|
|
19
|
+
* full turn, so a slice only means anything next to the others. Here every
|
|
20
|
+
* wedge takes the same angle and the *radius* is the quantity, which means the
|
|
21
|
+
* values do not have to add up to anything. Six unrelated readings on one scale
|
|
22
|
+
* are a polar area chart; six parts of one budget are a pie.
|
|
23
|
+
*
|
|
24
|
+
* What it buys over a bar chart is the shape. Bars are easier to read one at a
|
|
25
|
+
* time, and a reader who has to rank the middle three should be given bars. A
|
|
26
|
+
* dial is for the silhouette — which direction the weight sits in, whether one
|
|
27
|
+
* reading runs away from the rest — read at a glance and without a legend walk.
|
|
28
|
+
*
|
|
29
|
+
* ## Radius, and what it overstates
|
|
30
|
+
*
|
|
31
|
+
* `scale` decides what the radius means, and the two answers are not the same
|
|
32
|
+
* chart:
|
|
33
|
+
*
|
|
34
|
+
* - `radius` (the default) puts the value straight on the radius, so a reading
|
|
35
|
+
* can be counted off the rings. The cost is that a wedge worth twice another
|
|
36
|
+
* covers four times the area, and area is what the eye adds up first.
|
|
37
|
+
* - `area` puts it on the square root instead, so the ink is proportional to
|
|
38
|
+
* the value and nothing is overstated. The cost is the rings: they still mark
|
|
39
|
+
* equal steps, but they are no longer equally spaced.
|
|
40
|
+
*
|
|
41
|
+
* The grid is drawn through the same conversion either way, so a ring is always
|
|
42
|
+
* where its value falls rather than where an even split would put it.
|
|
43
|
+
*
|
|
44
|
+
* Touch, not hover: a wedge is selected by pressing it, and pressing the same
|
|
45
|
+
* one again clears the selection.
|
|
46
|
+
*/
|
|
47
|
+
import { Children, createContext, forwardRef, isValidElement, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
|
48
|
+
import { Pressable, View } from 'react-native';
|
|
49
|
+
import Animated, { Easing, useAnimatedProps, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
50
|
+
import Svg, { Circle, G, Line, Path } from 'react-native-svg';
|
|
51
|
+
import { useCSSVariable } from 'uniwind';
|
|
52
|
+
import { Text } from "../../primitives/text.js";
|
|
53
|
+
import { compactNumber, inkOn, polarPoint, seriesColorAt, useSeriesColor, wedgePath } from "../../utils/chart.js";
|
|
54
|
+
import { cn } from "../../utils/cn.js";
|
|
55
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
56
|
+
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
57
|
+
|
|
58
|
+
/** How far along its own radius a wedge's label sits. */
|
|
59
|
+
const LABEL_AT = 0.62;
|
|
60
|
+
|
|
61
|
+
/** The side of the invisible box a label is centred in. */
|
|
62
|
+
const LABEL_BOX = 64;
|
|
63
|
+
|
|
64
|
+
/** Below this radius in points a wedge is left unlabelled. */
|
|
65
|
+
const DEFAULT_MIN_LABEL_RADIUS = 34;
|
|
66
|
+
const TOOLTIP_WIDTH = 132;
|
|
67
|
+
const TOOLTIP_HEIGHT = 30;
|
|
68
|
+
|
|
69
|
+
/** Where a child is drawn: inside the SVG, over it, above it, or under it. */
|
|
70
|
+
|
|
71
|
+
/** Whether the chart is showing data or waiting for it. */
|
|
72
|
+
|
|
73
|
+
/** What a wedge's radius stands for. */
|
|
74
|
+
|
|
75
|
+
/** One wedge. Its angle is fixed, so only the value decides how far it reaches. */
|
|
76
|
+
|
|
77
|
+
/** A wedge's place on the dial, in turns clockwise from twelve o'clock. */
|
|
78
|
+
|
|
79
|
+
const PolarAreaChartContext = /*#__PURE__*/createContext(null);
|
|
80
|
+
function useChart(component) {
|
|
81
|
+
const context = useContext(PolarAreaChartContext);
|
|
82
|
+
if (!context) {
|
|
83
|
+
throw new Error(`${component} must be used within a <PolarAreaChart>`);
|
|
84
|
+
}
|
|
85
|
+
return context;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** The selected wedge and its reading, for something rendered inside the chart. */
|
|
89
|
+
export function usePolarAreaChart() {
|
|
90
|
+
const {
|
|
91
|
+
data,
|
|
92
|
+
wedges,
|
|
93
|
+
activeIndex
|
|
94
|
+
} = useChart('usePolarAreaChart');
|
|
95
|
+
return {
|
|
96
|
+
activeIndex,
|
|
97
|
+
activeWedge: activeIndex >= 0 ? data[activeIndex] ?? null : null,
|
|
98
|
+
/** The selected wedge's value against the maximum, 0 to 1. */
|
|
99
|
+
activeFraction: activeIndex >= 0 ? wedges[activeIndex]?.fraction ?? 0 : 0
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The steps a scale is read in, as multiples of a power of ten.
|
|
105
|
+
*
|
|
106
|
+
* Finer than the usual 1/2/5 ladder on purpose. Here the maximum is a *radius*
|
|
107
|
+
* as well as a number: rounding 140 up to 200 leaves the largest wedge stopping
|
|
108
|
+
* at seven tenths of the dial with the outer third of it empty on every chart
|
|
109
|
+
* whose readings happen to start with a one.
|
|
110
|
+
*/
|
|
111
|
+
const NICE_STEPS = [1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10];
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The first round number at or above `value`.
|
|
115
|
+
*
|
|
116
|
+
* The outermost ring is the number every other reading is judged against, so it
|
|
117
|
+
* is worth it landing on 150 rather than on 147.
|
|
118
|
+
*/
|
|
119
|
+
function niceMax(value) {
|
|
120
|
+
if (!Number.isFinite(value) || value <= 0) return 1;
|
|
121
|
+
const magnitude = 10 ** Math.floor(Math.log10(value));
|
|
122
|
+
const scaled = value / magnitude;
|
|
123
|
+
return (NICE_STEPS.find(step => scaled <= step) ?? 10) * magnitude;
|
|
124
|
+
}
|
|
125
|
+
const PolarAreaChartRoot = /*#__PURE__*/forwardRef(function PolarAreaChartRoot({
|
|
126
|
+
className,
|
|
127
|
+
data,
|
|
128
|
+
size,
|
|
129
|
+
maxValue: maxValueProp,
|
|
130
|
+
scale = 'radius',
|
|
131
|
+
startAngle = 0,
|
|
132
|
+
padAngle = 0,
|
|
133
|
+
status = 'ready',
|
|
134
|
+
animationDuration = 800,
|
|
135
|
+
activeIndex: activeIndexProp,
|
|
136
|
+
onActiveIndexChange,
|
|
137
|
+
children,
|
|
138
|
+
...props
|
|
139
|
+
}, ref) {
|
|
140
|
+
const [measured, setMeasured] = useState(0);
|
|
141
|
+
const [internalActive, setInternalActive] = useState(-1);
|
|
142
|
+
const reveal = useSharedValue(0);
|
|
143
|
+
const reducedMotion = useReducedMotion();
|
|
144
|
+
const controlled = activeIndexProp !== undefined;
|
|
145
|
+
const activeIndex = controlled ? activeIndexProp : internalActive;
|
|
146
|
+
const setActiveIndex = useMemo(() => index => {
|
|
147
|
+
if (!controlled) setInternalActive(index);
|
|
148
|
+
onActiveIndexChange?.(index);
|
|
149
|
+
}, [controlled, onActiveIndexChange]);
|
|
150
|
+
const box = size ?? measured;
|
|
151
|
+
const radius = Math.max(box / 2, 0);
|
|
152
|
+
const maxValue = useMemo(() => {
|
|
153
|
+
if (maxValueProp !== undefined) return Math.max(maxValueProp, 0) || 1;
|
|
154
|
+
return niceMax(data.reduce((top, datum) => Math.max(top, datum.value), 0));
|
|
155
|
+
}, [data, maxValueProp]);
|
|
156
|
+
|
|
157
|
+
/*
|
|
158
|
+
* One conversion, shared by the wedges, the rings and the readout. Two
|
|
159
|
+
* copies of it would be two chances for a ring to sit somewhere its own
|
|
160
|
+
* value does not.
|
|
161
|
+
*/
|
|
162
|
+
const radiusOf = useMemo(() => value => {
|
|
163
|
+
const fraction = Math.min(Math.max(value, 0) / maxValue, 1);
|
|
164
|
+
return radius * (scale === 'area' ? Math.sqrt(fraction) : fraction);
|
|
165
|
+
}, [maxValue, radius, scale]);
|
|
166
|
+
const origin = startAngle / 360;
|
|
167
|
+
const wedges = useMemo(() => {
|
|
168
|
+
const count = data.length;
|
|
169
|
+
if (!count || radius <= 0) return [];
|
|
170
|
+
const each = 1 / count;
|
|
171
|
+
// Never more than half a wedge from each side, or the two gaps meet in
|
|
172
|
+
// the middle and the wedge inverts.
|
|
173
|
+
const pad = Math.min(padAngle / 360, each / 2);
|
|
174
|
+
return data.map((datum, index) => {
|
|
175
|
+
const from = origin + each * index + pad / 2;
|
|
176
|
+
const to = origin + each * (index + 1) - pad / 2;
|
|
177
|
+
return {
|
|
178
|
+
from,
|
|
179
|
+
to,
|
|
180
|
+
mid: (from + to) / 2,
|
|
181
|
+
radius: radiusOf(datum.value),
|
|
182
|
+
fraction: Math.min(Math.max(datum.value, 0) / maxValue, 1)
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}, [data, radius, origin, padAngle, radiusOf, maxValue]);
|
|
186
|
+
const playReveal = useMemo(() => () => {
|
|
187
|
+
if (reducedMotion) {
|
|
188
|
+
reveal.value = 1;
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
reveal.value = 0;
|
|
192
|
+
reveal.value = withTiming(1, {
|
|
193
|
+
duration: animationDuration,
|
|
194
|
+
easing: Easing.bezier(0.85, 0, 0.15, 1)
|
|
195
|
+
});
|
|
196
|
+
}, [reducedMotion, animationDuration, reveal]);
|
|
197
|
+
const loading = status === 'loading';
|
|
198
|
+
const revealed = useRef(false);
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (loading) {
|
|
201
|
+
revealed.current = false;
|
|
202
|
+
reveal.value = 0;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (revealed.current || box <= 0 || !wedges.length) return;
|
|
206
|
+
revealed.current = true;
|
|
207
|
+
playReveal();
|
|
208
|
+
}, [loading, box, wedges.length, playReveal, reveal]);
|
|
209
|
+
useImperativeHandle(ref, () => ({
|
|
210
|
+
replay: playReveal
|
|
211
|
+
}), [playReveal]);
|
|
212
|
+
|
|
213
|
+
// Resolved here rather than inside the wedges, so the legend, the header
|
|
214
|
+
// and the readout can name a wedge's colour without drawing one.
|
|
215
|
+
const c1 = useSeriesColor(undefined, 1);
|
|
216
|
+
const c2 = useSeriesColor(undefined, 2);
|
|
217
|
+
const c3 = useSeriesColor(undefined, 3);
|
|
218
|
+
const c4 = useSeriesColor(undefined, 4);
|
|
219
|
+
const c5 = useSeriesColor(undefined, 5);
|
|
220
|
+
const palette = useMemo(() => [c1, c2, c3, c4, c5], [c1, c2, c3, c4, c5]);
|
|
221
|
+
const colors = useMemo(() => data.map((datum, index) => datum.color ?? seriesColorAt(palette, index)), [data, palette]);
|
|
222
|
+
const onLayout = event => {
|
|
223
|
+
const {
|
|
224
|
+
width,
|
|
225
|
+
height
|
|
226
|
+
} = event.nativeEvent.layout;
|
|
227
|
+
const next = Math.round(Math.min(width, height));
|
|
228
|
+
if (next !== measured) setMeasured(next);
|
|
229
|
+
props.onLayout?.(event);
|
|
230
|
+
};
|
|
231
|
+
const context = useMemo(() => ({
|
|
232
|
+
data,
|
|
233
|
+
size: box,
|
|
234
|
+
radius,
|
|
235
|
+
maxValue,
|
|
236
|
+
scale,
|
|
237
|
+
colors,
|
|
238
|
+
wedges,
|
|
239
|
+
radiusOf,
|
|
240
|
+
reveal,
|
|
241
|
+
status,
|
|
242
|
+
activeIndex,
|
|
243
|
+
setActiveIndex
|
|
244
|
+
}), [data, box, radius, maxValue, scale, colors, wedges, radiusOf, reveal, status, activeIndex, setActiveIndex]);
|
|
245
|
+
const slots = {
|
|
246
|
+
svg: [],
|
|
247
|
+
overlay: [],
|
|
248
|
+
header: [],
|
|
249
|
+
footer: []
|
|
250
|
+
};
|
|
251
|
+
Children.forEach(children, (child, index) => {
|
|
252
|
+
if (! /*#__PURE__*/isValidElement(child)) return;
|
|
253
|
+
const slot = child.type.slot ?? 'overlay';
|
|
254
|
+
slots[slot in slots ? slot : 'overlay'].push(/*#__PURE__*/_jsx(ChildSlot, {
|
|
255
|
+
children: child
|
|
256
|
+
}, index));
|
|
257
|
+
});
|
|
258
|
+
return /*#__PURE__*/_jsx(PolarAreaChartContext.Provider, {
|
|
259
|
+
value: context,
|
|
260
|
+
children: /*#__PURE__*/_jsxs(View, {
|
|
261
|
+
...props,
|
|
262
|
+
style: props.style,
|
|
263
|
+
className: cn('w-full', className),
|
|
264
|
+
children: [slots.header, /*#__PURE__*/_jsx(View, {
|
|
265
|
+
onLayout: onLayout,
|
|
266
|
+
style: size ? {
|
|
267
|
+
width: size,
|
|
268
|
+
height: size
|
|
269
|
+
} : {
|
|
270
|
+
aspectRatio: 1
|
|
271
|
+
},
|
|
272
|
+
className: cn('items-center justify-center', size ? 'self-center' : 'w-full'),
|
|
273
|
+
children: box > 0 ? /*#__PURE__*/_jsxs(_Fragment, {
|
|
274
|
+
children: [/*#__PURE__*/_jsx(Svg, {
|
|
275
|
+
width: box,
|
|
276
|
+
height: box,
|
|
277
|
+
children: slots.svg
|
|
278
|
+
}), /*#__PURE__*/_jsx(View, {
|
|
279
|
+
pointerEvents: "box-none",
|
|
280
|
+
style: {
|
|
281
|
+
position: 'absolute',
|
|
282
|
+
width: box,
|
|
283
|
+
height: box
|
|
284
|
+
},
|
|
285
|
+
children: slots.overlay
|
|
286
|
+
})]
|
|
287
|
+
}) : null
|
|
288
|
+
}), slots.footer]
|
|
289
|
+
})
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
PolarAreaChartRoot.displayName = 'PolarAreaChart';
|
|
293
|
+
function ChildSlot({
|
|
294
|
+
children
|
|
295
|
+
}) {
|
|
296
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
297
|
+
children: children
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Every wedge, drawn in the order the data lists them.
|
|
302
|
+
*
|
|
303
|
+
* One part rather than one per datum: the wedges share a dial, a maximum and a
|
|
304
|
+
* scale by definition, and a chart where one of them could be given a different
|
|
305
|
+
* maximum would be a chart drawing a lie.
|
|
306
|
+
*/
|
|
307
|
+
function PolarAreaChartWedges({
|
|
308
|
+
cornerRadius = 0,
|
|
309
|
+
dimOpacity = 0.35
|
|
310
|
+
}) {
|
|
311
|
+
const {
|
|
312
|
+
data,
|
|
313
|
+
wedges,
|
|
314
|
+
colors,
|
|
315
|
+
size,
|
|
316
|
+
reveal,
|
|
317
|
+
status,
|
|
318
|
+
activeIndex,
|
|
319
|
+
setActiveIndex
|
|
320
|
+
} = useChart('PolarAreaChart.Wedges');
|
|
321
|
+
if (status === 'loading' || !wedges.length) return null;
|
|
322
|
+
return /*#__PURE__*/_jsx(G, {
|
|
323
|
+
children: wedges.map((wedge, index) => {
|
|
324
|
+
const datum = data[index];
|
|
325
|
+
if (!datum || wedge.radius <= 0 || wedge.to <= wedge.from) return null;
|
|
326
|
+
return /*#__PURE__*/_jsx(Wedge, {
|
|
327
|
+
wedge: wedge,
|
|
328
|
+
fill: colors[index] ?? colors[0],
|
|
329
|
+
centre: size / 2,
|
|
330
|
+
cornerRadius: cornerRadius,
|
|
331
|
+
reveal: reveal,
|
|
332
|
+
dimmed: activeIndex >= 0 && activeIndex !== index,
|
|
333
|
+
dimOpacity: dimOpacity,
|
|
334
|
+
label: datum.label,
|
|
335
|
+
value: datum.value,
|
|
336
|
+
onPress: () => setActiveIndex(activeIndex === index ? -1 : index)
|
|
337
|
+
}, `${datum.label}-${index}`);
|
|
338
|
+
})
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
PolarAreaChartWedges.displayName = 'PolarAreaChart.Wedges';
|
|
342
|
+
PolarAreaChartWedges.slot = 'svg';
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* One wedge, rebuilt on the UI thread every frame it is growing.
|
|
346
|
+
*
|
|
347
|
+
* It grows outward rather than sweeping round, because the radius is the
|
|
348
|
+
* quantity here: a wedge half drawn is a smaller reading, which is the thing
|
|
349
|
+
* the chart is animating towards. A sweep would animate the one dimension that
|
|
350
|
+
* carries nothing.
|
|
351
|
+
*/
|
|
352
|
+
function Wedge({
|
|
353
|
+
wedge,
|
|
354
|
+
fill,
|
|
355
|
+
centre,
|
|
356
|
+
cornerRadius,
|
|
357
|
+
reveal,
|
|
358
|
+
dimmed,
|
|
359
|
+
dimOpacity,
|
|
360
|
+
label,
|
|
361
|
+
value,
|
|
362
|
+
onPress
|
|
363
|
+
}) {
|
|
364
|
+
const {
|
|
365
|
+
from,
|
|
366
|
+
to,
|
|
367
|
+
radius
|
|
368
|
+
} = wedge;
|
|
369
|
+
const animatedProps = useAnimatedProps(() => ({
|
|
370
|
+
d: wedgePath(centre, centre, radius * reveal.value, 0, from, to, cornerRadius)
|
|
371
|
+
}));
|
|
372
|
+
return /*#__PURE__*/_jsx(AnimatedPath, {
|
|
373
|
+
animatedProps: animatedProps,
|
|
374
|
+
fill: fill,
|
|
375
|
+
fillOpacity: dimmed ? dimOpacity : 1,
|
|
376
|
+
onPress: onPress
|
|
377
|
+
// An SVG node takes a label but not a role, so the wedges are reachable
|
|
378
|
+
// and named without being announced as buttons. `PolarAreaChart.Legend`
|
|
379
|
+
// is the properly wired way through the same selection.
|
|
380
|
+
,
|
|
381
|
+
accessibilityLabel: `${label}, ${compactNumber(value)}`
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* The scale, drawn as rings.
|
|
386
|
+
*
|
|
387
|
+
* Each ring stands for an even step of the value and is placed where that value
|
|
388
|
+
* falls, which under `scale="area"` is not an even step of the radius. Spacing
|
|
389
|
+
* them evenly instead would be quicker and would put every ring in the wrong
|
|
390
|
+
* place on half the charts.
|
|
391
|
+
*/
|
|
392
|
+
function PolarAreaChartGrid({
|
|
393
|
+
rings = 4,
|
|
394
|
+
color,
|
|
395
|
+
spokes = false
|
|
396
|
+
}) {
|
|
397
|
+
const {
|
|
398
|
+
size,
|
|
399
|
+
radius,
|
|
400
|
+
maxValue,
|
|
401
|
+
radiusOf,
|
|
402
|
+
wedges
|
|
403
|
+
} = useChart('PolarAreaChart.Grid');
|
|
404
|
+
const token = useCSSVariable('--color-border');
|
|
405
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
406
|
+
if (radius <= 0 || rings < 1) return null;
|
|
407
|
+
const centre = size / 2;
|
|
408
|
+
return /*#__PURE__*/_jsxs(G, {
|
|
409
|
+
children: [Array.from({
|
|
410
|
+
length: rings
|
|
411
|
+
}, (_, index) => {
|
|
412
|
+
const r = radiusOf((index + 1) / rings * maxValue);
|
|
413
|
+
if (r <= 0) return null;
|
|
414
|
+
return /*#__PURE__*/_jsx(Circle, {
|
|
415
|
+
cx: centre,
|
|
416
|
+
cy: centre,
|
|
417
|
+
r: r,
|
|
418
|
+
fill: "none",
|
|
419
|
+
stroke: stroke,
|
|
420
|
+
strokeWidth: 1
|
|
421
|
+
}, index);
|
|
422
|
+
}), spokes ? wedges.map((wedge, index) => {
|
|
423
|
+
const point = polarPoint(centre, centre, radius, wedge.from);
|
|
424
|
+
return /*#__PURE__*/_jsx(Line, {
|
|
425
|
+
x1: centre,
|
|
426
|
+
y1: centre,
|
|
427
|
+
x2: point.x,
|
|
428
|
+
y2: point.y,
|
|
429
|
+
stroke: stroke,
|
|
430
|
+
strokeWidth: 1
|
|
431
|
+
}, index);
|
|
432
|
+
}) : null]
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
PolarAreaChartGrid.displayName = 'PolarAreaChart.Grid';
|
|
436
|
+
PolarAreaChartGrid.slot = 'svg';
|
|
437
|
+
/**
|
|
438
|
+
* The reading on each wedge that has room for it.
|
|
439
|
+
*
|
|
440
|
+
* A wedge shorter than `minRadius` is left blank — the label would sit outside
|
|
441
|
+
* the wedge it belongs to, next to a neighbour it does not describe. Those are
|
|
442
|
+
* read through `Tooltip` and the legend instead.
|
|
443
|
+
*
|
|
444
|
+
* Each label takes its colour from the wedge under it. A wedge is a theme
|
|
445
|
+
* colour and a theme is free to set that anywhere on the scale, so a fixed
|
|
446
|
+
* white label disappears on the pale ones.
|
|
447
|
+
*/
|
|
448
|
+
function PolarAreaChartLabels({
|
|
449
|
+
formatValue,
|
|
450
|
+
minRadius = DEFAULT_MIN_LABEL_RADIUS,
|
|
451
|
+
className
|
|
452
|
+
}) {
|
|
453
|
+
const {
|
|
454
|
+
data,
|
|
455
|
+
wedges,
|
|
456
|
+
colors,
|
|
457
|
+
size,
|
|
458
|
+
status
|
|
459
|
+
} = useChart('PolarAreaChart.Labels');
|
|
460
|
+
const backdrop = useCSSVariable('--color-background');
|
|
461
|
+
if (status === 'loading' || !wedges.length) return null;
|
|
462
|
+
const format = formatValue ?? (value => compactNumber(value));
|
|
463
|
+
const behind = typeof backdrop === 'string' ? backdrop : undefined;
|
|
464
|
+
const centre = size / 2;
|
|
465
|
+
return /*#__PURE__*/_jsx(_Fragment, {
|
|
466
|
+
children: wedges.map((wedge, index) => {
|
|
467
|
+
const datum = data[index];
|
|
468
|
+
if (!datum || wedge.radius < minRadius) return null;
|
|
469
|
+
const point = polarPoint(centre, centre, wedge.radius * LABEL_AT, wedge.mid);
|
|
470
|
+
const ink = inkOn(colors[index] ?? colors[0], behind, 1);
|
|
471
|
+
return /*#__PURE__*/_jsx(View, {
|
|
472
|
+
// Not a target: the wedge underneath is one, and a box this size
|
|
473
|
+
// over the middle of a dial would take the presses meant for its
|
|
474
|
+
// neighbours as well as its own.
|
|
475
|
+
pointerEvents: "none",
|
|
476
|
+
style: {
|
|
477
|
+
position: 'absolute',
|
|
478
|
+
left: point.x - LABEL_BOX / 2,
|
|
479
|
+
top: point.y - LABEL_BOX / 2,
|
|
480
|
+
width: LABEL_BOX,
|
|
481
|
+
height: LABEL_BOX
|
|
482
|
+
},
|
|
483
|
+
className: cn('items-center justify-center', className),
|
|
484
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
485
|
+
size: "xs",
|
|
486
|
+
weight: "semibold",
|
|
487
|
+
numberOfLines: 1,
|
|
488
|
+
style: {
|
|
489
|
+
color: ink.color
|
|
490
|
+
},
|
|
491
|
+
children: format(datum.value, datum)
|
|
492
|
+
})
|
|
493
|
+
}, `${datum.label}-${index}`);
|
|
494
|
+
})
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
PolarAreaChartLabels.displayName = 'PolarAreaChart.Labels';
|
|
498
|
+
PolarAreaChartLabels.slot = 'overlay';
|
|
499
|
+
/**
|
|
500
|
+
* The readout for the selected wedge, floating over the dial.
|
|
501
|
+
*
|
|
502
|
+
* This is how the short wedges are named. They are the ones with no room for a
|
|
503
|
+
* label, so without it the chart answers questions about its largest readings
|
|
504
|
+
* only — which is the half the reader could already see.
|
|
505
|
+
*/
|
|
506
|
+
function PolarAreaChartTooltip({
|
|
507
|
+
formatValue,
|
|
508
|
+
className
|
|
509
|
+
}) {
|
|
510
|
+
const {
|
|
511
|
+
data,
|
|
512
|
+
wedges,
|
|
513
|
+
size,
|
|
514
|
+
activeIndex,
|
|
515
|
+
status
|
|
516
|
+
} = useChart('PolarAreaChart.Tooltip');
|
|
517
|
+
if (status === 'loading' || activeIndex < 0) return null;
|
|
518
|
+
const datum = data[activeIndex];
|
|
519
|
+
const wedge = wedges[activeIndex];
|
|
520
|
+
if (!datum || !wedge) return null;
|
|
521
|
+
const format = formatValue ?? (value => compactNumber(value));
|
|
522
|
+
const centre = size / 2;
|
|
523
|
+
const point = polarPoint(centre, centre, wedge.radius * LABEL_AT, wedge.mid);
|
|
524
|
+
|
|
525
|
+
// Over the wedge, then pushed back inside the square — a readout half off the
|
|
526
|
+
// edge is one the reader has to guess the rest of.
|
|
527
|
+
const left = Math.max(0, Math.min(size - TOOLTIP_WIDTH, point.x - TOOLTIP_WIDTH / 2));
|
|
528
|
+
const top = Math.max(0, Math.min(size - TOOLTIP_HEIGHT, point.y - TOOLTIP_HEIGHT / 2));
|
|
529
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
530
|
+
pointerEvents: "none",
|
|
531
|
+
style: {
|
|
532
|
+
position: 'absolute',
|
|
533
|
+
left,
|
|
534
|
+
top,
|
|
535
|
+
width: TOOLTIP_WIDTH
|
|
536
|
+
},
|
|
537
|
+
className: cn('items-center rounded-lg border border-border bg-popover px-2 py-1 shadow-sm', className),
|
|
538
|
+
children: [/*#__PURE__*/_jsx(Text, {
|
|
539
|
+
size: "xs",
|
|
540
|
+
weight: "medium",
|
|
541
|
+
numberOfLines: 1,
|
|
542
|
+
children: datum.label
|
|
543
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
544
|
+
size: "xs",
|
|
545
|
+
muted: true,
|
|
546
|
+
numberOfLines: 1,
|
|
547
|
+
children: format(datum.value, datum)
|
|
548
|
+
})]
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
PolarAreaChartTooltip.displayName = 'PolarAreaChart.Tooltip';
|
|
552
|
+
PolarAreaChartTooltip.slot = 'overlay';
|
|
553
|
+
/**
|
|
554
|
+
* A swatch, a name and a reading per wedge, under the dial and across the width
|
|
555
|
+
* of it. Pressable in the same way the wedges are, and the easier target of the
|
|
556
|
+
* two for anything short.
|
|
557
|
+
*
|
|
558
|
+
* The reading rather than a share, unlike the pie next door: these values need
|
|
559
|
+
* not add up to anything, so a percentage of their sum would be a number about
|
|
560
|
+
* nothing.
|
|
561
|
+
*/
|
|
562
|
+
function PolarAreaChartLegend({
|
|
563
|
+
className,
|
|
564
|
+
showValue = true,
|
|
565
|
+
formatValue,
|
|
566
|
+
...props
|
|
567
|
+
}) {
|
|
568
|
+
const {
|
|
569
|
+
data,
|
|
570
|
+
colors,
|
|
571
|
+
activeIndex,
|
|
572
|
+
setActiveIndex
|
|
573
|
+
} = useChart('PolarAreaChart.Legend');
|
|
574
|
+
if (!data.length) return null;
|
|
575
|
+
const format = formatValue ?? (value => compactNumber(value));
|
|
576
|
+
return /*#__PURE__*/_jsx(View, {
|
|
577
|
+
...props,
|
|
578
|
+
className: cn('w-full flex-row flex-wrap items-center justify-center gap-x-3 gap-y-1.5 pt-3', className),
|
|
579
|
+
children: data.map((datum, index) => {
|
|
580
|
+
const dimmed = activeIndex >= 0 && activeIndex !== index;
|
|
581
|
+
return /*#__PURE__*/_jsxs(Pressable, {
|
|
582
|
+
accessibilityRole: "button",
|
|
583
|
+
accessibilityState: {
|
|
584
|
+
selected: activeIndex === index
|
|
585
|
+
},
|
|
586
|
+
accessibilityLabel: `${datum.label}, ${format(datum.value, datum)}`,
|
|
587
|
+
onPress: () => setActiveIndex(activeIndex === index ? -1 : index),
|
|
588
|
+
style: {
|
|
589
|
+
opacity: dimmed ? 0.4 : 1
|
|
590
|
+
},
|
|
591
|
+
className: "max-w-full flex-row items-center gap-1.5",
|
|
592
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
593
|
+
style: {
|
|
594
|
+
width: 8,
|
|
595
|
+
height: 8,
|
|
596
|
+
borderRadius: 4,
|
|
597
|
+
backgroundColor: colors[index]
|
|
598
|
+
}
|
|
599
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
600
|
+
size: "xs",
|
|
601
|
+
muted: true,
|
|
602
|
+
numberOfLines: 1,
|
|
603
|
+
className: "shrink",
|
|
604
|
+
children: datum.label
|
|
605
|
+
}), showValue ? /*#__PURE__*/_jsx(Text, {
|
|
606
|
+
size: "xs",
|
|
607
|
+
weight: "medium",
|
|
608
|
+
children: format(datum.value, datum)
|
|
609
|
+
}) : null]
|
|
610
|
+
}, `${datum.label}-${index}`);
|
|
611
|
+
})
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
PolarAreaChartLegend.displayName = 'PolarAreaChart.Legend';
|
|
615
|
+
PolarAreaChartLegend.slot = 'footer';
|
|
616
|
+
/**
|
|
617
|
+
* The loading state: the dial as one plain disc, with nothing divided up yet.
|
|
618
|
+
*
|
|
619
|
+
* Deliberately undivided. Placeholder wedges would be a made-up set of
|
|
620
|
+
* readings, and a reader has no way to tell an invented one from a real one
|
|
621
|
+
* until it changes under them.
|
|
622
|
+
*/
|
|
623
|
+
function PolarAreaChartSkeleton({
|
|
624
|
+
color
|
|
625
|
+
}) {
|
|
626
|
+
const {
|
|
627
|
+
size,
|
|
628
|
+
radius,
|
|
629
|
+
status
|
|
630
|
+
} = useChart('PolarAreaChart.Skeleton');
|
|
631
|
+
const token = useCSSVariable('--color-skeleton');
|
|
632
|
+
const fill = color ?? (typeof token === 'string' ? token : 'rgba(128,128,128,0.2)');
|
|
633
|
+
if (status !== 'loading' || radius <= 0) return null;
|
|
634
|
+
return /*#__PURE__*/_jsx(Circle, {
|
|
635
|
+
cx: size / 2,
|
|
636
|
+
cy: size / 2,
|
|
637
|
+
r: radius * 0.62,
|
|
638
|
+
fill: fill
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
PolarAreaChartSkeleton.displayName = 'PolarAreaChart.Skeleton';
|
|
642
|
+
PolarAreaChartSkeleton.slot = 'svg';
|
|
643
|
+
/**
|
|
644
|
+
* The strip above the dial: what the chart is of, what it reads, and what the
|
|
645
|
+
* colours mean.
|
|
646
|
+
*
|
|
647
|
+
* The value is not derived here even though there are values to derive one
|
|
648
|
+
* from, because the formatting is not the chart's to guess: 18420 is a count, a
|
|
649
|
+
* currency or a duration depending on what was measured.
|
|
650
|
+
*/
|
|
651
|
+
function PolarAreaChartHeader({
|
|
652
|
+
className,
|
|
653
|
+
title,
|
|
654
|
+
value,
|
|
655
|
+
caption,
|
|
656
|
+
labels,
|
|
657
|
+
legend = false,
|
|
658
|
+
children,
|
|
659
|
+
...props
|
|
660
|
+
}) {
|
|
661
|
+
const {
|
|
662
|
+
data,
|
|
663
|
+
colors
|
|
664
|
+
} = useChart('PolarAreaChart.Header');
|
|
665
|
+
const trailing = children ?? (legend && data.length ? /*#__PURE__*/_jsx(View, {
|
|
666
|
+
className: "flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1",
|
|
667
|
+
children: data.map((datum, index) => /*#__PURE__*/_jsxs(View, {
|
|
668
|
+
className: "flex-row items-center gap-1.5",
|
|
669
|
+
children: [/*#__PURE__*/_jsx(View, {
|
|
670
|
+
style: {
|
|
671
|
+
width: 8,
|
|
672
|
+
height: 8,
|
|
673
|
+
borderRadius: 4,
|
|
674
|
+
backgroundColor: colors[index]
|
|
675
|
+
}
|
|
676
|
+
}), /*#__PURE__*/_jsx(Text, {
|
|
677
|
+
size: "xs",
|
|
678
|
+
muted: true,
|
|
679
|
+
numberOfLines: 1,
|
|
680
|
+
children: labels?.[datum.label] ?? datum.label
|
|
681
|
+
})]
|
|
682
|
+
}, `${datum.label}-${index}`))
|
|
683
|
+
}) : null);
|
|
684
|
+
return /*#__PURE__*/_jsxs(View, {
|
|
685
|
+
...props,
|
|
686
|
+
className: cn('flex-row items-start justify-between gap-3 pb-3', className),
|
|
687
|
+
children: [/*#__PURE__*/_jsxs(View, {
|
|
688
|
+
className: "flex-1 gap-0.5",
|
|
689
|
+
children: [title ? /*#__PURE__*/_jsx(Text, {
|
|
690
|
+
size: "xs",
|
|
691
|
+
muted: true,
|
|
692
|
+
children: title
|
|
693
|
+
}) : null, value ? /*#__PURE__*/_jsx(Text, {
|
|
694
|
+
size: "xl",
|
|
695
|
+
weight: "bold",
|
|
696
|
+
children: value
|
|
697
|
+
}) : null, caption ? /*#__PURE__*/_jsx(Text, {
|
|
698
|
+
size: "xs",
|
|
699
|
+
muted: true,
|
|
700
|
+
children: caption
|
|
701
|
+
}) : null]
|
|
702
|
+
}), trailing ? /*#__PURE__*/_jsx(View, {
|
|
703
|
+
className: "max-w-[55%] shrink pt-1",
|
|
704
|
+
children: trailing
|
|
705
|
+
}) : null]
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
PolarAreaChartHeader.displayName = 'PolarAreaChart.Header';
|
|
709
|
+
PolarAreaChartHeader.slot = 'header';
|
|
710
|
+
export const PolarAreaChart = Object.assign(PolarAreaChartRoot, {
|
|
711
|
+
Header: PolarAreaChartHeader,
|
|
712
|
+
Grid: PolarAreaChartGrid,
|
|
713
|
+
Wedges: PolarAreaChartWedges,
|
|
714
|
+
Labels: PolarAreaChartLabels,
|
|
715
|
+
Tooltip: PolarAreaChartTooltip,
|
|
716
|
+
Legend: PolarAreaChartLegend,
|
|
717
|
+
Skeleton: PolarAreaChartSkeleton
|
|
718
|
+
});
|
|
719
|
+
//# sourceMappingURL=index.js.map
|