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