panelui-native 0.22.6 → 0.28.1
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 +1 -0
- package/lib/module/components/accordion/index.js +8 -8
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/alert/index.js +2 -2
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/area-chart/index.js +958 -0
- package/lib/module/components/area-chart/index.js.map +1 -0
- package/lib/module/components/attachment/index.js +2 -1
- package/lib/module/components/attachment/index.js.map +1 -1
- package/lib/module/components/avatar/index.js +4 -4
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/badge/index.js +4 -4
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/bar-chart/index.js +994 -0
- package/lib/module/components/bar-chart/index.js.map +1 -0
- package/lib/module/components/bottom-sheet/index.js +213 -24
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/breadcrumb/index.js +275 -0
- package/lib/module/components/breadcrumb/index.js.map +1 -0
- package/lib/module/components/button/index.js +11 -6
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/calendar/index.js +557 -0
- package/lib/module/components/calendar/index.js.map +1 -0
- package/lib/module/components/carousel/index.js +680 -0
- package/lib/module/components/carousel/index.js.map +1 -0
- package/lib/module/components/chip/index.js +6 -6
- package/lib/module/components/chip/index.js.map +1 -1
- package/lib/module/components/date-picker/index.js +158 -0
- package/lib/module/components/date-picker/index.js.map +1 -0
- package/lib/module/components/dialog/index.js +7 -2
- package/lib/module/components/dialog/index.js.map +1 -1
- package/lib/module/components/direction/index.js +15 -20
- package/lib/module/components/direction/index.js.map +1 -1
- package/lib/module/components/empty-state/index.js +2 -2
- package/lib/module/components/empty-state/index.js.map +1 -1
- package/lib/module/components/field/index.js +5 -5
- package/lib/module/components/field/index.js.map +1 -1
- package/lib/module/components/flow/index.js +3 -3
- package/lib/module/components/flow/index.js.map +1 -1
- package/lib/module/components/frame/index.js +22 -5
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/heatmap-chart/index.js +28 -8
- package/lib/module/components/heatmap-chart/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +5 -4
- package/lib/module/components/input-group/index.js.map +1 -1
- package/lib/module/components/item/index.js +9 -9
- package/lib/module/components/item/index.js.map +1 -1
- package/lib/module/components/label/index.js +4 -4
- package/lib/module/components/label/index.js.map +1 -1
- package/lib/module/components/line-chart/index.js +165 -184
- package/lib/module/components/line-chart/index.js.map +1 -1
- package/lib/module/components/loader/index.js +758 -0
- package/lib/module/components/loader/index.js.map +1 -0
- package/lib/module/components/map/basemap.js +265 -0
- package/lib/module/components/map/basemap.js.map +1 -0
- package/lib/module/components/map/index.js +850 -0
- package/lib/module/components/map/index.js.map +1 -0
- package/lib/module/components/map/maplibre.js +87 -0
- package/lib/module/components/map/maplibre.js.map +1 -0
- package/lib/module/components/marker/index.js +3 -3
- package/lib/module/components/marker/index.js.map +1 -1
- package/lib/module/components/message/index.js +15 -14
- package/lib/module/components/message/index.js.map +1 -1
- package/lib/module/components/message-scroller/index.js +5 -4
- package/lib/module/components/message-scroller/index.js.map +1 -1
- package/lib/module/components/otp-input/index.js +2 -2
- package/lib/module/components/popover/index.js +39 -7
- package/lib/module/components/popover/index.js.map +1 -1
- package/lib/module/components/progress/index.js +8 -2
- package/lib/module/components/progress/index.js.map +1 -1
- package/lib/module/components/radio-group/index.js +2 -2
- package/lib/module/components/radio-group/index.js.map +1 -1
- package/lib/module/components/ring-chart/index.js +423 -0
- package/lib/module/components/ring-chart/index.js.map +1 -0
- package/lib/module/components/section-rail/index.js +6 -6
- package/lib/module/components/section-rail/index.js.map +1 -1
- package/lib/module/components/select/index.js +10 -4
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/separator/index.js +76 -22
- package/lib/module/components/separator/index.js.map +1 -1
- package/lib/module/components/shimmer/index.js +10 -1
- package/lib/module/components/shimmer/index.js.map +1 -1
- package/lib/module/components/slider/index.js +19 -5
- package/lib/module/components/slider/index.js.map +1 -1
- package/lib/module/components/steps/index.js +5 -5
- package/lib/module/components/steps/index.js.map +1 -1
- package/lib/module/components/switch/index.js +6 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/tabs/index.js +7 -7
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +4 -4
- package/lib/module/components/timeline/index.js.map +1 -1
- package/lib/module/components/toast/index.js +2 -2
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/toggle-button/index.js +5 -5
- package/lib/module/components/toggle-button/index.js.map +1 -1
- package/lib/module/components/tooltip/index.js +492 -0
- package/lib/module/components/tooltip/index.js.map +1 -0
- package/lib/module/components/typography/index.js +7 -2
- package/lib/module/components/typography/index.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-back-handler.js +37 -0
- package/lib/module/hooks/use-back-handler.js.map +1 -0
- package/lib/module/hooks/use-direction.js +43 -0
- package/lib/module/hooks/use-direction.js.map +1 -0
- package/lib/module/icons/index.js +180 -2
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +12 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/primitives/keyboard-avoider.js +2 -1
- package/lib/module/primitives/keyboard-avoider.js.map +1 -1
- package/lib/module/primitives/text.js +55 -11
- package/lib/module/primitives/text.js.map +1 -1
- package/lib/module/utils/chart.js +279 -0
- package/lib/module/utils/chart.js.map +1 -0
- package/lib/module/utils/date.js +386 -0
- package/lib/module/utils/date.js.map +1 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
- package/lib/typescript/src/components/area-chart/index.d.ts +237 -0
- package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/attachment/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bar-chart/index.d.ts +270 -0
- package/lib/typescript/src/components/bar-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +75 -4
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/breadcrumb/index.d.ts +100 -0
- package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/calendar/index.d.ts +108 -0
- package/lib/typescript/src/components/calendar/index.d.ts.map +1 -0
- package/lib/typescript/src/components/carousel/index.d.ts +179 -0
- package/lib/typescript/src/components/carousel/index.d.ts.map +1 -0
- package/lib/typescript/src/components/chip/index.d.ts.map +1 -1
- package/lib/typescript/src/components/date-picker/index.d.ts +91 -0
- package/lib/typescript/src/components/date-picker/index.d.ts.map +1 -0
- package/lib/typescript/src/components/dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/components/direction/index.d.ts +11 -10
- package/lib/typescript/src/components/direction/index.d.ts.map +1 -1
- package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
- package/lib/typescript/src/components/field/index.d.ts.map +1 -1
- package/lib/typescript/src/components/frame/index.d.ts +17 -0
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/heatmap-chart/index.d.ts +22 -3
- package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/item/index.d.ts.map +1 -1
- package/lib/typescript/src/components/label/index.d.ts.map +1 -1
- package/lib/typescript/src/components/line-chart/index.d.ts +42 -2
- package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
- package/lib/typescript/src/components/loader/index.d.ts +38 -0
- package/lib/typescript/src/components/loader/index.d.ts.map +1 -0
- package/lib/typescript/src/components/map/basemap.d.ts +76 -0
- package/lib/typescript/src/components/map/basemap.d.ts.map +1 -0
- package/lib/typescript/src/components/map/index.d.ts +367 -0
- package/lib/typescript/src/components/map/index.d.ts.map +1 -0
- package/lib/typescript/src/components/map/maplibre.d.ts +193 -0
- package/lib/typescript/src/components/map/maplibre.d.ts.map +1 -0
- package/lib/typescript/src/components/marker/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message-scroller/index.d.ts.map +1 -1
- package/lib/typescript/src/components/popover/index.d.ts.map +1 -1
- package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
- package/lib/typescript/src/components/ring-chart/index.d.ts +156 -0
- package/lib/typescript/src/components/ring-chart/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/separator/index.d.ts +36 -1
- package/lib/typescript/src/components/separator/index.d.ts.map +1 -1
- package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
- package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
- package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/tooltip/index.d.ts +125 -0
- package/lib/typescript/src/components/tooltip/index.d.ts.map +1 -0
- package/lib/typescript/src/components/typography/index.d.ts +6 -0
- package/lib/typescript/src/components/typography/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +1 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/use-back-handler.d.ts +19 -0
- package/lib/typescript/src/hooks/use-back-handler.d.ts.map +1 -0
- package/lib/typescript/src/hooks/use-direction.d.ts +20 -0
- package/lib/typescript/src/hooks/use-direction.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +8 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +14 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
- package/lib/typescript/src/primitives/text.d.ts +20 -0
- package/lib/typescript/src/primitives/text.d.ts.map +1 -1
- package/lib/typescript/src/utils/chart.d.ts +96 -0
- package/lib/typescript/src/utils/chart.d.ts.map +1 -0
- package/lib/typescript/src/utils/date.d.ts +116 -0
- package/lib/typescript/src/utils/date.d.ts.map +1 -0
- package/package.json +9 -1
- package/src/components/accordion/index.tsx +8 -12
- package/src/components/alert/index.tsx +2 -2
- package/src/components/area-chart/index.tsx +1147 -0
- package/src/components/attachment/index.tsx +2 -2
- package/src/components/avatar/index.tsx +4 -4
- package/src/components/badge/index.tsx +6 -6
- package/src/components/bar-chart/index.tsx +1208 -0
- package/src/components/bottom-sheet/index.tsx +282 -21
- package/src/components/breadcrumb/index.tsx +363 -0
- package/src/components/button/index.tsx +12 -8
- package/src/components/calendar/index.tsx +703 -0
- package/src/components/carousel/index.tsx +788 -0
- package/src/components/chip/index.tsx +6 -8
- package/src/components/date-picker/index.tsx +237 -0
- package/src/components/dialog/index.tsx +7 -2
- package/src/components/direction/index.tsx +22 -22
- package/src/components/empty-state/index.tsx +2 -2
- package/src/components/field/index.tsx +3 -5
- package/src/components/flow/index.tsx +3 -3
- package/src/components/frame/index.tsx +22 -7
- package/src/components/heatmap-chart/index.tsx +58 -11
- package/src/components/input-group/index.tsx +5 -4
- package/src/components/item/index.tsx +9 -9
- package/src/components/label/index.tsx +2 -2
- package/src/components/line-chart/index.tsx +194 -223
- package/src/components/loader/index.tsx +812 -0
- package/src/components/map/basemap.ts +304 -0
- package/src/components/map/index.tsx +1116 -0
- package/src/components/map/maplibre.ts +201 -0
- package/src/components/marker/index.tsx +3 -3
- package/src/components/message/index.tsx +15 -14
- package/src/components/message-scroller/index.tsx +5 -4
- package/src/components/otp-input/index.tsx +2 -2
- package/src/components/popover/index.tsx +43 -7
- package/src/components/progress/index.tsx +10 -2
- package/src/components/radio-group/index.tsx +2 -2
- package/src/components/ring-chart/index.tsx +543 -0
- package/src/components/section-rail/index.tsx +6 -6
- package/src/components/select/index.tsx +10 -4
- package/src/components/separator/index.tsx +77 -19
- package/src/components/shimmer/index.tsx +10 -1
- package/src/components/slider/index.tsx +20 -5
- package/src/components/steps/index.tsx +5 -5
- package/src/components/switch/index.tsx +6 -1
- package/src/components/tabs/index.tsx +7 -9
- package/src/components/timeline/index.tsx +4 -4
- package/src/components/toast/index.tsx +2 -2
- package/src/components/toggle-button/index.tsx +5 -7
- package/src/components/tooltip/index.tsx +610 -0
- package/src/components/typography/index.tsx +7 -2
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-back-handler.ts +37 -0
- package/src/hooks/use-direction.ts +45 -0
- package/src/icons/index.tsx +111 -0
- package/src/index.ts +129 -0
- package/src/primitives/keyboard-avoider.tsx +2 -1
- package/src/primitives/text.tsx +53 -8
- package/src/utils/chart.ts +355 -0
- package/src/utils/date.ts +446 -0
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AreaChart — filled bands over time, stacked or overlaid.
|
|
3
|
+
*
|
|
4
|
+
* ```tsx
|
|
5
|
+
* <AreaChart data={traffic} xDataKey="date" stacked>
|
|
6
|
+
* <AreaChart.Grid />
|
|
7
|
+
* <AreaChart.Area dataKey="direct" />
|
|
8
|
+
* <AreaChart.Area dataKey="search" colorIndex={2} />
|
|
9
|
+
* <AreaChart.XAxis />
|
|
10
|
+
* <AreaChart.Tooltip />
|
|
11
|
+
* </AreaChart>
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* ## Why this is not `LineChart.Area`
|
|
15
|
+
*
|
|
16
|
+
* `LineChart.Area` shades under a line, which is one series saying "this is
|
|
17
|
+
* the shape of the thing". This chart answers a different question — *what is
|
|
18
|
+
* it made of* — and that needs stacking: each band sits on the running total
|
|
19
|
+
* of the ones below it, so the top edge is the whole and the thickness of each
|
|
20
|
+
* band is its share.
|
|
21
|
+
*
|
|
22
|
+
* Stacking is not a flag that could be bolted on. It changes the y-domain from
|
|
23
|
+
* the largest single series to the largest *sum*, it makes each area's
|
|
24
|
+
* baseline a curve rather than the axis, and it makes the order the series are
|
|
25
|
+
* declared in load-bearing. A chart where all of that is true of some children
|
|
26
|
+
* and not others is a chart nobody can read, so it is a chart of its own.
|
|
27
|
+
*
|
|
28
|
+
* Unstacked, the bands overlay each other and the fills are drawn translucent,
|
|
29
|
+
* which is the right reading when the series are alternatives rather than
|
|
30
|
+
* parts — two plans compared, not two slices of one total.
|
|
31
|
+
*/
|
|
32
|
+
import {
|
|
33
|
+
Children,
|
|
34
|
+
createContext,
|
|
35
|
+
forwardRef,
|
|
36
|
+
isValidElement,
|
|
37
|
+
useContext,
|
|
38
|
+
useEffect,
|
|
39
|
+
useId,
|
|
40
|
+
useImperativeHandle,
|
|
41
|
+
useMemo,
|
|
42
|
+
useRef,
|
|
43
|
+
useState,
|
|
44
|
+
type ReactNode,
|
|
45
|
+
} from 'react';
|
|
46
|
+
import { StyleSheet, View, type LayoutChangeEvent, type ViewProps } from 'react-native';
|
|
47
|
+
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
|
|
48
|
+
import Animated, {
|
|
49
|
+
Easing,
|
|
50
|
+
runOnJS,
|
|
51
|
+
useAnimatedProps,
|
|
52
|
+
useAnimatedStyle,
|
|
53
|
+
useReducedMotion,
|
|
54
|
+
useSharedValue,
|
|
55
|
+
withTiming,
|
|
56
|
+
type SharedValue,
|
|
57
|
+
} from 'react-native-reanimated';
|
|
58
|
+
import Svg, {
|
|
59
|
+
Circle,
|
|
60
|
+
ClipPath,
|
|
61
|
+
Defs,
|
|
62
|
+
G,
|
|
63
|
+
Line as SvgLine,
|
|
64
|
+
LinearGradient,
|
|
65
|
+
Path,
|
|
66
|
+
Rect,
|
|
67
|
+
Stop,
|
|
68
|
+
} from 'react-native-svg';
|
|
69
|
+
import { useCSSVariable } from 'uniwind';
|
|
70
|
+
import { Text } from '../../primitives/text';
|
|
71
|
+
import {
|
|
72
|
+
areaPath,
|
|
73
|
+
columnValues,
|
|
74
|
+
compactNumber,
|
|
75
|
+
linePath,
|
|
76
|
+
useSeriesColor,
|
|
77
|
+
xOf,
|
|
78
|
+
yOf,
|
|
79
|
+
type ChartCurve,
|
|
80
|
+
type Plot,
|
|
81
|
+
type SeriesColorIndex,
|
|
82
|
+
} from '../../utils/chart';
|
|
83
|
+
import { cn } from '../../utils/cn';
|
|
84
|
+
|
|
85
|
+
const AnimatedPath = Animated.createAnimatedComponent(Path);
|
|
86
|
+
const AnimatedRect = Animated.createAnimatedComponent(Rect);
|
|
87
|
+
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
|
|
88
|
+
|
|
89
|
+
const PADDING = { top: 12, right: 10, bottom: 22, left: 10 };
|
|
90
|
+
const LABEL_WIDTH = 132;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Box each x label is centred in. Wide enough for a short label to sit over
|
|
94
|
+
* its point without the neighbours colliding; a longer one is ellipsised
|
|
95
|
+
* rather than allowed to push the others out of place.
|
|
96
|
+
*/
|
|
97
|
+
const POINT_LABEL_WIDTH = 56;
|
|
98
|
+
|
|
99
|
+
/** Line height of an `xs` label, for centring one on the grid line it names. */
|
|
100
|
+
const AXIS_LABEL_HEIGHT = 16;
|
|
101
|
+
const DOT = 9;
|
|
102
|
+
|
|
103
|
+
/** Left gutter reserved when a `YAxis` is present, for its labels to sit in. */
|
|
104
|
+
const Y_AXIS_WIDTH = 44;
|
|
105
|
+
|
|
106
|
+
/** Gap between the value labels and the plot they sit beside. */
|
|
107
|
+
const Y_AXIS_GUTTER = 6;
|
|
108
|
+
|
|
109
|
+
type Layer = 'svg' | 'overlay' | 'header';
|
|
110
|
+
|
|
111
|
+
export type AreaChartStatus = 'loading' | 'ready';
|
|
112
|
+
export type AreaChartDatum = Record<string, string | number | null | undefined>;
|
|
113
|
+
|
|
114
|
+
interface AreaChartContextValue {
|
|
115
|
+
data: AreaChartDatum[];
|
|
116
|
+
xDataKey: string;
|
|
117
|
+
plot: Plot;
|
|
118
|
+
status: AreaChartStatus;
|
|
119
|
+
curve: ChartCurve;
|
|
120
|
+
stacked: boolean;
|
|
121
|
+
series: [string, string][];
|
|
122
|
+
registerSeries: (key: string, color: string) => void;
|
|
123
|
+
unregisterSeries: (key: string) => void;
|
|
124
|
+
domainMin: SharedValue<number>;
|
|
125
|
+
domainMax: SharedValue<number>;
|
|
126
|
+
extent: [number, number];
|
|
127
|
+
activeIndex: SharedValue<number>;
|
|
128
|
+
activeIndexJS: number;
|
|
129
|
+
setActiveIndexJS: (index: number) => void;
|
|
130
|
+
clipId: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const AreaChartContext = createContext<AreaChartContextValue | null>(null);
|
|
134
|
+
|
|
135
|
+
function useChart(component: string): AreaChartContextValue {
|
|
136
|
+
const context = useContext(AreaChartContext);
|
|
137
|
+
if (!context) {
|
|
138
|
+
throw new Error(`${component} must be used within an <AreaChart>`);
|
|
139
|
+
}
|
|
140
|
+
return context;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The point under the crosshair, for something rendered *inside* the chart.
|
|
145
|
+
* A readout in the card's header is outside this provider — use
|
|
146
|
+
* `onActiveIndexChange` for that.
|
|
147
|
+
*/
|
|
148
|
+
export function useAreaChart() {
|
|
149
|
+
const { data, activeIndexJS, xDataKey } = useChart('useAreaChart');
|
|
150
|
+
return {
|
|
151
|
+
activeIndex: activeIndexJS,
|
|
152
|
+
activePoint: activeIndexJS >= 0 ? (data[activeIndexJS] ?? null) : null,
|
|
153
|
+
xDataKey,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface AreaChartProps extends ViewProps {
|
|
158
|
+
className?: string;
|
|
159
|
+
/** The rows. Each one is a point along the x-axis. */
|
|
160
|
+
data: AreaChartDatum[];
|
|
161
|
+
/** Key holding the x label. Used by the axis and the crosshair readout. */
|
|
162
|
+
xDataKey?: string;
|
|
163
|
+
/** `loading` draws a flat skeleton and grows into the real bands on `ready`. */
|
|
164
|
+
status?: AreaChartStatus;
|
|
165
|
+
/** Width ÷ height. `2` is the wide card shape. */
|
|
166
|
+
aspectRatio?: number;
|
|
167
|
+
/** Milliseconds for the reveal on mount. */
|
|
168
|
+
animationDuration?: number;
|
|
169
|
+
/** Milliseconds for the y-axis to settle after the data changes. */
|
|
170
|
+
domainDuration?: number;
|
|
171
|
+
/** Fix the y-axis instead of deriving it from the data. */
|
|
172
|
+
yDomain?: [number, number];
|
|
173
|
+
/**
|
|
174
|
+
* Sit each band on the running total of the ones below it, so the top edge
|
|
175
|
+
* is the whole and each thickness is a share. The order the `Area` children
|
|
176
|
+
* are declared in is the stacking order, bottom first.
|
|
177
|
+
*
|
|
178
|
+
* Unstacked, the bands overlay and their fills are translucent — the right
|
|
179
|
+
* reading when the series are alternatives rather than parts of a total.
|
|
180
|
+
*/
|
|
181
|
+
stacked?: boolean;
|
|
182
|
+
/** `monotone` never overshoots between points; `linear` joins them straight. */
|
|
183
|
+
curve?: ChartCurve;
|
|
184
|
+
/**
|
|
185
|
+
* The point under the crosshair as it moves, and `-1`/`null` when it lifts.
|
|
186
|
+
* Fires when the index changes, not per frame.
|
|
187
|
+
*/
|
|
188
|
+
onActiveIndexChange?: (index: number, datum: AreaChartDatum | null) => void;
|
|
189
|
+
/** Drop the axis padding so the bands reach the edges, for a sparkline. */
|
|
190
|
+
compact?: boolean;
|
|
191
|
+
children?: ReactNode;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Imperative handle: re-run the reveal on demand, for a "replay" control. */
|
|
195
|
+
export interface AreaChartHandle {
|
|
196
|
+
replay: () => void;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ChildSlot({ children }: { children: ReactNode }) {
|
|
200
|
+
return <>{children}</>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function partition(children: ReactNode) {
|
|
204
|
+
const svg: ReactNode[] = [];
|
|
205
|
+
const overlay: ReactNode[] = [];
|
|
206
|
+
const header: ReactNode[] = [];
|
|
207
|
+
Children.forEach(children, (child, index) => {
|
|
208
|
+
if (!isValidElement(child)) return;
|
|
209
|
+
const layer = (child.type as { layer?: Layer }).layer ?? 'svg';
|
|
210
|
+
const slot = <ChildSlot key={index}>{child}</ChildSlot>;
|
|
211
|
+
(layer === 'header' ? header : layer === 'overlay' ? overlay : svg).push(slot);
|
|
212
|
+
});
|
|
213
|
+
return { svg, overlay, header };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const AreaChartRoot = forwardRef<AreaChartHandle, AreaChartProps>(function AreaChartRoot(
|
|
217
|
+
{
|
|
218
|
+
className,
|
|
219
|
+
data,
|
|
220
|
+
xDataKey = 'date',
|
|
221
|
+
status = 'ready',
|
|
222
|
+
aspectRatio = 2,
|
|
223
|
+
animationDuration = 1100,
|
|
224
|
+
domainDuration = 500,
|
|
225
|
+
yDomain,
|
|
226
|
+
stacked = false,
|
|
227
|
+
curve = 'monotone',
|
|
228
|
+
onActiveIndexChange,
|
|
229
|
+
compact = false,
|
|
230
|
+
children,
|
|
231
|
+
...props
|
|
232
|
+
},
|
|
233
|
+
ref
|
|
234
|
+
) {
|
|
235
|
+
const [size, setSize] = useState({ width: 0, height: 0 });
|
|
236
|
+
const [series, setSeries] = useState<[string, string][]>([]);
|
|
237
|
+
const [activeIndexJS, setActiveIndexJS] = useState(-1);
|
|
238
|
+
const clipId = `panelui-area-clip-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
239
|
+
|
|
240
|
+
const reveal = useSharedValue(0);
|
|
241
|
+
const domainMin = useSharedValue(0);
|
|
242
|
+
const domainMax = useSharedValue(0);
|
|
243
|
+
const activeIndex = useSharedValue(-1);
|
|
244
|
+
const reducedMotion = useReducedMotion();
|
|
245
|
+
|
|
246
|
+
const registerSeries = useMemo(
|
|
247
|
+
() => (key: string, color: string) =>
|
|
248
|
+
setSeries((current) => {
|
|
249
|
+
const existing = current.find(([k]) => k === key);
|
|
250
|
+
if (existing?.[1] === color) return current;
|
|
251
|
+
return [...current.filter(([k]) => k !== key), [key, color]];
|
|
252
|
+
}),
|
|
253
|
+
[]
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const unregisterSeries = useMemo(
|
|
257
|
+
() => (key: string) => setSeries((current) => current.filter(([k]) => k !== key)),
|
|
258
|
+
[]
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
/*
|
|
262
|
+
* Whether an axis is asking for room. It has to be known before the plot is
|
|
263
|
+
* laid out, and only the root sees the children early enough to ask — the
|
|
264
|
+
* axis itself renders into a box that has already been decided.
|
|
265
|
+
*/
|
|
266
|
+
const hasYAxis = useMemo(() => {
|
|
267
|
+
let found = false;
|
|
268
|
+
Children.forEach(children, (child) => {
|
|
269
|
+
if (isValidElement(child) && (child.type as { axis?: string }).axis === 'y') {
|
|
270
|
+
found = true;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return found;
|
|
274
|
+
}, [children]);
|
|
275
|
+
|
|
276
|
+
const pad = compact
|
|
277
|
+
? { top: 2, right: 1, bottom: 2, left: 1 }
|
|
278
|
+
: { ...PADDING, left: hasYAxis ? Y_AXIS_WIDTH : PADDING.left };
|
|
279
|
+
const plot: Plot = {
|
|
280
|
+
left: pad.left,
|
|
281
|
+
top: pad.top,
|
|
282
|
+
width: Math.max(size.width - pad.left - pad.right, 0),
|
|
283
|
+
height: Math.max(size.height - pad.top - pad.bottom, 0),
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const seriesKeys = series.map(([key]) => key).join('|');
|
|
287
|
+
const extent = useMemo<[number, number]>(() => {
|
|
288
|
+
if (yDomain) return yDomain;
|
|
289
|
+
const keys = seriesKeys ? seriesKeys.split('|') : [];
|
|
290
|
+
let min = Infinity;
|
|
291
|
+
let max = -Infinity;
|
|
292
|
+
|
|
293
|
+
for (const row of data) {
|
|
294
|
+
/*
|
|
295
|
+
* Stacked, the tall thing is the sum of the row; overlaid, it is the
|
|
296
|
+
* largest single value in it. Reading the same number for both crops a
|
|
297
|
+
* stack at its largest band and clips everything above it.
|
|
298
|
+
*/
|
|
299
|
+
let rowTotal = 0;
|
|
300
|
+
for (const key of keys) {
|
|
301
|
+
const value = row[key];
|
|
302
|
+
if (typeof value !== 'number' || Number.isNaN(value)) continue;
|
|
303
|
+
if (stacked) {
|
|
304
|
+
rowTotal += value;
|
|
305
|
+
} else {
|
|
306
|
+
if (value < min) min = value;
|
|
307
|
+
if (value > max) max = value;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (stacked) {
|
|
311
|
+
if (rowTotal < min) min = rowTotal;
|
|
312
|
+
if (rowTotal > max) max = rowTotal;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (min === Infinity) return [0, 1];
|
|
317
|
+
if (min === max) return [min - 1, max + 1];
|
|
318
|
+
/*
|
|
319
|
+
* An area is a filled region, and a region floating above the bottom of
|
|
320
|
+
* the plot reads as a shape rather than as a quantity — so a series that
|
|
321
|
+
* never goes below zero is drawn from zero. Padding only the top.
|
|
322
|
+
*/
|
|
323
|
+
const floor = min >= 0 ? 0 : min - (max - min) * 0.1;
|
|
324
|
+
return [floor, max + (max - min) * 0.1];
|
|
325
|
+
}, [data, yDomain, seriesKeys, stacked]);
|
|
326
|
+
|
|
327
|
+
const loading = status === 'loading';
|
|
328
|
+
|
|
329
|
+
useEffect(() => {
|
|
330
|
+
if (loading) return;
|
|
331
|
+
const [min, max] = extent;
|
|
332
|
+
const first = domainMin.value === 0 && domainMax.value === 0;
|
|
333
|
+
if (first || reducedMotion) {
|
|
334
|
+
domainMin.value = min;
|
|
335
|
+
domainMax.value = max;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
domainMin.value = withTiming(min, { duration: domainDuration });
|
|
339
|
+
domainMax.value = withTiming(max, { duration: domainDuration });
|
|
340
|
+
}, [extent, loading, reducedMotion, domainDuration, domainMin, domainMax]);
|
|
341
|
+
|
|
342
|
+
const revealed = useRef(false);
|
|
343
|
+
const playReveal = useMemo(
|
|
344
|
+
() => () => {
|
|
345
|
+
if (reducedMotion) {
|
|
346
|
+
reveal.value = 1;
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
reveal.value = 0;
|
|
350
|
+
reveal.value = withTiming(1, {
|
|
351
|
+
duration: animationDuration,
|
|
352
|
+
easing: Easing.bezier(0.85, 0, 0.15, 1),
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
[reducedMotion, animationDuration, reveal]
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
if (revealed.current || loading || plot.width <= 0 || !data.length) return;
|
|
360
|
+
revealed.current = true;
|
|
361
|
+
playReveal();
|
|
362
|
+
}, [loading, plot.width, data.length, playReveal]);
|
|
363
|
+
|
|
364
|
+
useImperativeHandle(ref, () => ({ replay: playReveal }), [playReveal]);
|
|
365
|
+
|
|
366
|
+
const clipProps = useAnimatedProps(() => ({ width: plot.width * reveal.value }));
|
|
367
|
+
|
|
368
|
+
const handleActiveIndex = useMemo(
|
|
369
|
+
() => (index: number) => {
|
|
370
|
+
setActiveIndexJS(index);
|
|
371
|
+
onActiveIndexChange?.(index, index >= 0 ? (data[index] ?? null) : null);
|
|
372
|
+
},
|
|
373
|
+
[onActiveIndexChange, data]
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
const onLayout = (event: LayoutChangeEvent) => {
|
|
377
|
+
const { width, height } = event.nativeEvent.layout;
|
|
378
|
+
setSize((current) =>
|
|
379
|
+
Math.abs(current.width - width) < 1 && Math.abs(current.height - height) < 1
|
|
380
|
+
? current
|
|
381
|
+
: { width, height }
|
|
382
|
+
);
|
|
383
|
+
props.onLayout?.(event);
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const context = useMemo<AreaChartContextValue>(
|
|
387
|
+
() => ({
|
|
388
|
+
data,
|
|
389
|
+
xDataKey,
|
|
390
|
+
plot,
|
|
391
|
+
status,
|
|
392
|
+
curve,
|
|
393
|
+
stacked,
|
|
394
|
+
series,
|
|
395
|
+
registerSeries,
|
|
396
|
+
unregisterSeries,
|
|
397
|
+
domainMin,
|
|
398
|
+
domainMax,
|
|
399
|
+
extent,
|
|
400
|
+
activeIndex,
|
|
401
|
+
activeIndexJS,
|
|
402
|
+
setActiveIndexJS: handleActiveIndex,
|
|
403
|
+
clipId,
|
|
404
|
+
}),
|
|
405
|
+
// `plot` is rebuilt every render from `size`, so it is compared by value.
|
|
406
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
407
|
+
[
|
|
408
|
+
data,
|
|
409
|
+
xDataKey,
|
|
410
|
+
plot.width,
|
|
411
|
+
plot.height,
|
|
412
|
+
plot.left,
|
|
413
|
+
plot.top,
|
|
414
|
+
status,
|
|
415
|
+
curve,
|
|
416
|
+
stacked,
|
|
417
|
+
series,
|
|
418
|
+
registerSeries,
|
|
419
|
+
unregisterSeries,
|
|
420
|
+
domainMin,
|
|
421
|
+
domainMax,
|
|
422
|
+
extent,
|
|
423
|
+
activeIndex,
|
|
424
|
+
activeIndexJS,
|
|
425
|
+
handleActiveIndex,
|
|
426
|
+
clipId,
|
|
427
|
+
]
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const { svg, overlay, header } = partition(children);
|
|
431
|
+
|
|
432
|
+
/*
|
|
433
|
+
* Two views, because the header is not part of the plot. `aspectRatio` and
|
|
434
|
+
* the layout measurement belong to the drawing area alone — measured on the
|
|
435
|
+
* outer view they would take in the header too, and the plot would lose as
|
|
436
|
+
* much height as the readout took while still claiming the shape asked for.
|
|
437
|
+
*/
|
|
438
|
+
return (
|
|
439
|
+
<AreaChartContext.Provider value={context}>
|
|
440
|
+
<View {...props} style={props.style} className={cn('w-full', className)}>
|
|
441
|
+
{header}
|
|
442
|
+
<View onLayout={onLayout} style={{ aspectRatio }} className="w-full">
|
|
443
|
+
{plot.width > 0 ? (
|
|
444
|
+
<>
|
|
445
|
+
<Svg width="100%" height="100%" style={StyleSheet.absoluteFill}>
|
|
446
|
+
<Defs>
|
|
447
|
+
{/*
|
|
448
|
+
* One clip for everything in the plot. Sharing it is what
|
|
449
|
+
* makes the reveal read as the chart arriving rather than as
|
|
450
|
+
* several bands animating in at once.
|
|
451
|
+
*/}
|
|
452
|
+
<ClipPath id={clipId}>
|
|
453
|
+
<AnimatedRect
|
|
454
|
+
x={plot.left}
|
|
455
|
+
y={0}
|
|
456
|
+
height={size.height}
|
|
457
|
+
animatedProps={clipProps}
|
|
458
|
+
/>
|
|
459
|
+
</ClipPath>
|
|
460
|
+
</Defs>
|
|
461
|
+
{svg}
|
|
462
|
+
</Svg>
|
|
463
|
+
{overlay}
|
|
464
|
+
</>
|
|
465
|
+
) : null}
|
|
466
|
+
</View>
|
|
467
|
+
</View>
|
|
468
|
+
</AreaChartContext.Provider>
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
AreaChartRoot.displayName = 'AreaChart';
|
|
472
|
+
|
|
473
|
+
/* -------------------------------------------------------------------------- */
|
|
474
|
+
/* SVG layer */
|
|
475
|
+
/* -------------------------------------------------------------------------- */
|
|
476
|
+
|
|
477
|
+
export interface AreaChartGridProps {
|
|
478
|
+
rows?: number;
|
|
479
|
+
color?: string;
|
|
480
|
+
dashArray?: string;
|
|
481
|
+
opacity?: number;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Horizontal rules, so a band can be read against a number. */
|
|
485
|
+
function AreaChartGrid({ rows = 4, color, dashArray = '4,6', opacity = 1 }: AreaChartGridProps) {
|
|
486
|
+
const { plot } = useChart('AreaChart.Grid');
|
|
487
|
+
const token = useCSSVariable('--color-border');
|
|
488
|
+
const stroke = color ?? (typeof token === 'string' ? token : 'rgba(0,0,0,0.1)');
|
|
489
|
+
|
|
490
|
+
return (
|
|
491
|
+
<G opacity={opacity}>
|
|
492
|
+
{Array.from({ length: rows + 1 }, (_unused, index) => index / rows).map((fraction) => (
|
|
493
|
+
<SvgLine
|
|
494
|
+
key={fraction}
|
|
495
|
+
x1={plot.left}
|
|
496
|
+
x2={plot.left + plot.width}
|
|
497
|
+
y1={plot.top + plot.height * fraction}
|
|
498
|
+
y2={plot.top + plot.height * fraction}
|
|
499
|
+
stroke={stroke}
|
|
500
|
+
strokeDasharray={dashArray}
|
|
501
|
+
strokeWidth={1}
|
|
502
|
+
/>
|
|
503
|
+
))}
|
|
504
|
+
</G>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
AreaChartGrid.displayName = 'AreaChart.Grid';
|
|
508
|
+
AreaChartGrid.layer = 'svg' as Layer;
|
|
509
|
+
|
|
510
|
+
export interface AreaChartAreaProps {
|
|
511
|
+
/** Column in the data holding this series' values. */
|
|
512
|
+
dataKey: string;
|
|
513
|
+
/** Explicit colour. Defaults to the `--color-chart-*` token for `colorIndex`. */
|
|
514
|
+
color?: string;
|
|
515
|
+
/** Which of the five chart tokens to take. */
|
|
516
|
+
colorIndex?: SeriesColorIndex;
|
|
517
|
+
/** Opacity of the fill at the top of the band. */
|
|
518
|
+
fillOpacity?: number;
|
|
519
|
+
/** Opacity at the bottom. `0` fades the band out; match `fillOpacity` for a flat fill. */
|
|
520
|
+
gradientToOpacity?: number;
|
|
521
|
+
/** Draw the line along the top edge of the band. */
|
|
522
|
+
showLine?: boolean;
|
|
523
|
+
/** Thickness of that line. */
|
|
524
|
+
strokeWidth?: number;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* One filled band.
|
|
529
|
+
*
|
|
530
|
+
* The fill is a gradient by default rather than a flat wash, because a flat
|
|
531
|
+
* fill of any weight competes with the line on top of it — fading it downward
|
|
532
|
+
* keeps the top edge, which is the part carrying the numbers, the darkest
|
|
533
|
+
* thing in the band.
|
|
534
|
+
*/
|
|
535
|
+
function AreaChartArea({
|
|
536
|
+
dataKey,
|
|
537
|
+
color,
|
|
538
|
+
colorIndex = 1,
|
|
539
|
+
fillOpacity,
|
|
540
|
+
gradientToOpacity = 0,
|
|
541
|
+
showLine = true,
|
|
542
|
+
strokeWidth = 2,
|
|
543
|
+
}: AreaChartAreaProps) {
|
|
544
|
+
const {
|
|
545
|
+
data,
|
|
546
|
+
plot,
|
|
547
|
+
status,
|
|
548
|
+
curve,
|
|
549
|
+
stacked,
|
|
550
|
+
series,
|
|
551
|
+
registerSeries,
|
|
552
|
+
unregisterSeries,
|
|
553
|
+
domainMin,
|
|
554
|
+
domainMax,
|
|
555
|
+
clipId,
|
|
556
|
+
} = useChart('AreaChart.Area');
|
|
557
|
+
|
|
558
|
+
const stroke = useSeriesColor(color, colorIndex);
|
|
559
|
+
const gradientId = `panelui-area-fill-${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
|
|
560
|
+
|
|
561
|
+
// Stacked bands touch, so a translucent one shows the band beneath through
|
|
562
|
+
// it and the two read as a third colour. Overlaid they must be translucent,
|
|
563
|
+
// because the one in front would otherwise hide the ones behind entirely.
|
|
564
|
+
const topOpacity = fillOpacity ?? (stacked ? 0.85 : 0.35);
|
|
565
|
+
|
|
566
|
+
useEffect(() => {
|
|
567
|
+
registerSeries(dataKey, stroke);
|
|
568
|
+
return () => unregisterSeries(dataKey);
|
|
569
|
+
}, [dataKey, stroke, registerSeries, unregisterSeries]);
|
|
570
|
+
|
|
571
|
+
const values = useMemo(() => columnValues(data, dataKey), [data, dataKey]);
|
|
572
|
+
|
|
573
|
+
/*
|
|
574
|
+
* The running total of the series declared below this one, which is what
|
|
575
|
+
* this band sits on. Registration order is declaration order, so the first
|
|
576
|
+
* `Area` child is the bottom of the stack.
|
|
577
|
+
*/
|
|
578
|
+
const baselines = useMemo(() => {
|
|
579
|
+
if (!stacked) return undefined;
|
|
580
|
+
const below = series.slice(0, series.findIndex(([key]) => key === dataKey));
|
|
581
|
+
if (!below.length) return undefined;
|
|
582
|
+
return data.map((row) => {
|
|
583
|
+
let total = 0;
|
|
584
|
+
for (const [key] of below) {
|
|
585
|
+
const value = row[key];
|
|
586
|
+
if (typeof value === 'number' && !Number.isNaN(value)) total += value;
|
|
587
|
+
}
|
|
588
|
+
return total;
|
|
589
|
+
});
|
|
590
|
+
}, [stacked, series, dataKey, data]);
|
|
591
|
+
|
|
592
|
+
const loading = status === 'loading';
|
|
593
|
+
|
|
594
|
+
const fillProps = useAnimatedProps(() => ({
|
|
595
|
+
d: areaPath(values, plot, domainMin.value, domainMax.value, curve, loading, baselines),
|
|
596
|
+
}));
|
|
597
|
+
|
|
598
|
+
const lineProps = useAnimatedProps(() => ({
|
|
599
|
+
d: linePath(values, plot, domainMin.value, domainMax.value, curve, loading, baselines),
|
|
600
|
+
}));
|
|
601
|
+
|
|
602
|
+
return (
|
|
603
|
+
<G clipPath={`url(#${clipId})`}>
|
|
604
|
+
<Defs>
|
|
605
|
+
<LinearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
|
606
|
+
<Stop offset="0" stopColor={stroke} stopOpacity={topOpacity} />
|
|
607
|
+
<Stop offset="1" stopColor={stroke} stopOpacity={gradientToOpacity} />
|
|
608
|
+
</LinearGradient>
|
|
609
|
+
</Defs>
|
|
610
|
+
<AnimatedPath animatedProps={fillProps} fill={`url(#${gradientId})`} />
|
|
611
|
+
{showLine ? (
|
|
612
|
+
<AnimatedPath
|
|
613
|
+
animatedProps={lineProps}
|
|
614
|
+
stroke={stroke}
|
|
615
|
+
strokeWidth={strokeWidth}
|
|
616
|
+
strokeLinecap="round"
|
|
617
|
+
strokeLinejoin="round"
|
|
618
|
+
fill="none"
|
|
619
|
+
/>
|
|
620
|
+
) : null}
|
|
621
|
+
</G>
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
AreaChartArea.displayName = 'AreaChart.Area';
|
|
625
|
+
AreaChartArea.layer = 'svg' as Layer;
|
|
626
|
+
|
|
627
|
+
/* -------------------------------------------------------------------------- */
|
|
628
|
+
/* Overlay layer */
|
|
629
|
+
/* -------------------------------------------------------------------------- */
|
|
630
|
+
|
|
631
|
+
export interface AreaChartXAxisProps {
|
|
632
|
+
ticks?: number;
|
|
633
|
+
format?: (datum: AreaChartDatum, index: number) => string;
|
|
634
|
+
className?: string;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/** The x labels. Real text, so they follow the theme's font and text scaling. */
|
|
638
|
+
function AreaChartXAxis({ ticks = 4, format, className }: AreaChartXAxisProps) {
|
|
639
|
+
const { data, xDataKey, plot } = useChart('AreaChart.XAxis');
|
|
640
|
+
|
|
641
|
+
const labels = useMemo(() => {
|
|
642
|
+
if (!data.length) return [];
|
|
643
|
+
const count = Math.min(ticks, data.length);
|
|
644
|
+
const step = count > 1 ? (data.length - 1) / (count - 1) : 0;
|
|
645
|
+
return Array.from({ length: count }, (_unused, index) => {
|
|
646
|
+
const dataIndex = Math.round(index * step);
|
|
647
|
+
const datum = data[dataIndex];
|
|
648
|
+
if (!datum) return null;
|
|
649
|
+
return {
|
|
650
|
+
key: dataIndex,
|
|
651
|
+
text: format ? format(datum, dataIndex) : String(datum[xDataKey] ?? ''),
|
|
652
|
+
};
|
|
653
|
+
}).filter((label): label is { key: number; text: string } => label !== null);
|
|
654
|
+
}, [data, ticks, format, xDataKey]);
|
|
655
|
+
|
|
656
|
+
/*
|
|
657
|
+
* Each label sits on its own point rather than being spread along the axis.
|
|
658
|
+
* The indices `ticks` picks are not evenly spaced — twelve months shown five
|
|
659
|
+
* at a time gives 0,3,6,8,11 — so spreading them evenly put the ones in
|
|
660
|
+
* between over the wrong part of the line.
|
|
661
|
+
*
|
|
662
|
+
* The box is backed off by half its own width rather than translated by
|
|
663
|
+
* `-50%`, which is not reliable across React Native versions.
|
|
664
|
+
*/
|
|
665
|
+
return (
|
|
666
|
+
<View
|
|
667
|
+
style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
|
|
668
|
+
className={cn(className)}
|
|
669
|
+
>
|
|
670
|
+
{labels.map((label) => (
|
|
671
|
+
<Text
|
|
672
|
+
key={label.key}
|
|
673
|
+
size="xs"
|
|
674
|
+
muted
|
|
675
|
+
numberOfLines={1}
|
|
676
|
+
style={{
|
|
677
|
+
position: 'absolute',
|
|
678
|
+
bottom: 0,
|
|
679
|
+
// Centred on its point, then held inside the chart. The first and
|
|
680
|
+
// last points sit on the plot's own edges, so a box centred on
|
|
681
|
+
// them hangs half its width off the side — the clamp slides those
|
|
682
|
+
// two back in rather than letting the numbers leave the frame.
|
|
683
|
+
left: Math.max(
|
|
684
|
+
0,
|
|
685
|
+
Math.min(
|
|
686
|
+
xOf(label.key, data.length, plot) - POINT_LABEL_WIDTH / 2,
|
|
687
|
+
plot.left + plot.width + PADDING.right - POINT_LABEL_WIDTH
|
|
688
|
+
)
|
|
689
|
+
),
|
|
690
|
+
width: POINT_LABEL_WIDTH,
|
|
691
|
+
textAlign: 'center',
|
|
692
|
+
}}
|
|
693
|
+
>
|
|
694
|
+
{label.text}
|
|
695
|
+
</Text>
|
|
696
|
+
))}
|
|
697
|
+
</View>
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
AreaChartXAxis.displayName = 'AreaChart.XAxis';
|
|
701
|
+
AreaChartXAxis.layer = 'overlay' as Layer;
|
|
702
|
+
|
|
703
|
+
export interface AreaChartYAxisProps {
|
|
704
|
+
ticks?: number;
|
|
705
|
+
format?: (value: number) => string;
|
|
706
|
+
className?: string;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Value labels down the side, aligned to the grid lines. */
|
|
710
|
+
function AreaChartYAxis({ ticks = 4, format, className }: AreaChartYAxisProps) {
|
|
711
|
+
const { plot, extent } = useChart('AreaChart.YAxis');
|
|
712
|
+
|
|
713
|
+
const labels = useMemo(() => {
|
|
714
|
+
const [min, max] = extent;
|
|
715
|
+
if (min === 0 && max === 0) return [];
|
|
716
|
+
return Array.from({ length: ticks + 1 }, (_unused, index) => {
|
|
717
|
+
const value = max - ((max - min) * index) / ticks;
|
|
718
|
+
return { key: index, text: format ? format(value) : compactNumber(value) };
|
|
719
|
+
});
|
|
720
|
+
}, [extent, ticks, format]);
|
|
721
|
+
|
|
722
|
+
return (
|
|
723
|
+
<View
|
|
724
|
+
pointerEvents="none"
|
|
725
|
+
style={{
|
|
726
|
+
position: 'absolute',
|
|
727
|
+
left: 0,
|
|
728
|
+
// Centred on the grid line each label names: the strip is lifted half
|
|
729
|
+
// a label and grown by a whole one, so `justify-between` lands the
|
|
730
|
+
// text's middle on the line rather than its top edge on the first line
|
|
731
|
+
// and its bottom edge on the last.
|
|
732
|
+
top: plot.top - AXIS_LABEL_HEIGHT / 2,
|
|
733
|
+
height: plot.height + AXIS_LABEL_HEIGHT,
|
|
734
|
+
// The gutter the root reserved, less a little breathing room, so the
|
|
735
|
+
// numbers sit clear of the plot instead of against it.
|
|
736
|
+
width: Math.max(plot.left - Y_AXIS_GUTTER, 0),
|
|
737
|
+
}}
|
|
738
|
+
className={cn('items-end justify-between', className)}
|
|
739
|
+
>
|
|
740
|
+
{labels.map((label) => (
|
|
741
|
+
<Text key={label.key} size="xs" muted numberOfLines={1}>
|
|
742
|
+
{label.text}
|
|
743
|
+
</Text>
|
|
744
|
+
))}
|
|
745
|
+
</View>
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
AreaChartYAxis.displayName = 'AreaChart.YAxis';
|
|
749
|
+
AreaChartYAxis.layer = 'overlay' as Layer;
|
|
750
|
+
// Read by the root, which has to leave room for the labels before it
|
|
751
|
+
// lays the plot out — an axis drawn over the plot is unreadable, and
|
|
752
|
+
// makes what it is drawn over unreadable too.
|
|
753
|
+
AreaChartYAxis.axis = 'y' as const;
|
|
754
|
+
|
|
755
|
+
/** A dot riding the top edge of one band as the crosshair moves. */
|
|
756
|
+
function CrosshairDot({
|
|
757
|
+
values,
|
|
758
|
+
baselines,
|
|
759
|
+
plot,
|
|
760
|
+
domainMin,
|
|
761
|
+
domainMax,
|
|
762
|
+
activeIndex,
|
|
763
|
+
total,
|
|
764
|
+
color,
|
|
765
|
+
}: {
|
|
766
|
+
values: (number | null)[];
|
|
767
|
+
baselines: number[] | undefined;
|
|
768
|
+
plot: Plot;
|
|
769
|
+
domainMin: SharedValue<number>;
|
|
770
|
+
domainMax: SharedValue<number>;
|
|
771
|
+
activeIndex: SharedValue<number>;
|
|
772
|
+
total: number;
|
|
773
|
+
color: string;
|
|
774
|
+
}) {
|
|
775
|
+
const props = useAnimatedProps(() => {
|
|
776
|
+
const index = activeIndex.value;
|
|
777
|
+
const value = index >= 0 ? values[index] : null;
|
|
778
|
+
if (index < 0 || value === null || value === undefined) {
|
|
779
|
+
return { opacity: 0, cx: 0, cy: 0 };
|
|
780
|
+
}
|
|
781
|
+
const stacked = value + (baselines?.[index] ?? 0);
|
|
782
|
+
return {
|
|
783
|
+
opacity: 1,
|
|
784
|
+
cx: xOf(index, total, plot),
|
|
785
|
+
cy: yOf(stacked, plot, domainMin.value, domainMax.value),
|
|
786
|
+
};
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
return (
|
|
790
|
+
<AnimatedCircle
|
|
791
|
+
animatedProps={props}
|
|
792
|
+
r={DOT / 2}
|
|
793
|
+
fill={color}
|
|
794
|
+
stroke="white"
|
|
795
|
+
strokeWidth={2}
|
|
796
|
+
/>
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export interface AreaChartTooltipProps {
|
|
801
|
+
color?: string;
|
|
802
|
+
/** Format one series' value. Defaults to a compact number. */
|
|
803
|
+
formatValue?: (value: number, key: string) => string;
|
|
804
|
+
/** Format the readout's heading from the row. Defaults to the value at xDataKey. */
|
|
805
|
+
formatX?: (datum: AreaChartDatum) => string;
|
|
806
|
+
className?: string;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* The crosshair, the gesture that drives it, and the readout that rides it.
|
|
811
|
+
*
|
|
812
|
+
* The hit area is the whole plot. A crosshair you have to land on a band to
|
|
813
|
+
* summon is a crosshair nobody finds.
|
|
814
|
+
*/
|
|
815
|
+
function AreaChartTooltip({ color, formatValue, formatX, className }: AreaChartTooltipProps) {
|
|
816
|
+
const {
|
|
817
|
+
data,
|
|
818
|
+
xDataKey,
|
|
819
|
+
plot,
|
|
820
|
+
stacked,
|
|
821
|
+
series,
|
|
822
|
+
domainMin,
|
|
823
|
+
domainMax,
|
|
824
|
+
activeIndex,
|
|
825
|
+
activeIndexJS,
|
|
826
|
+
setActiveIndexJS,
|
|
827
|
+
status,
|
|
828
|
+
} = useChart('AreaChart.Tooltip');
|
|
829
|
+
|
|
830
|
+
const token = useCSSVariable('--color-foreground');
|
|
831
|
+
const stroke = color ?? (typeof token === 'string' ? token : '#888888');
|
|
832
|
+
|
|
833
|
+
const total = data.length;
|
|
834
|
+
const left = plot.left;
|
|
835
|
+
const width = plot.width;
|
|
836
|
+
|
|
837
|
+
/*
|
|
838
|
+
* Declared inside the memo, next to its callers: a worklet may only call
|
|
839
|
+
* another worklet, and the rule is enforced by crashing rather than warning.
|
|
840
|
+
*/
|
|
841
|
+
const pan = useMemo(() => {
|
|
842
|
+
const resolve = (x: number) => {
|
|
843
|
+
'worklet';
|
|
844
|
+
if (total < 2 || width <= 0) return;
|
|
845
|
+
const ratio = (x - left) / width;
|
|
846
|
+
const next = Math.round(Math.min(1, Math.max(0, ratio)) * (total - 1));
|
|
847
|
+
if (next === activeIndex.value) return;
|
|
848
|
+
activeIndex.value = next;
|
|
849
|
+
runOnJS(setActiveIndexJS)(next);
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
return Gesture.Pan()
|
|
853
|
+
.minDistance(0)
|
|
854
|
+
.onBegin((event) => {
|
|
855
|
+
'worklet';
|
|
856
|
+
resolve(event.x);
|
|
857
|
+
})
|
|
858
|
+
.onUpdate((event) => {
|
|
859
|
+
'worklet';
|
|
860
|
+
resolve(event.x);
|
|
861
|
+
})
|
|
862
|
+
.onFinalize(() => {
|
|
863
|
+
'worklet';
|
|
864
|
+
activeIndex.value = -1;
|
|
865
|
+
runOnJS(setActiveIndexJS)(-1);
|
|
866
|
+
});
|
|
867
|
+
}, [total, left, width, activeIndex, setActiveIndexJS]);
|
|
868
|
+
|
|
869
|
+
const crosshairStyle = useAnimatedStyle(() => {
|
|
870
|
+
const index = activeIndex.value;
|
|
871
|
+
return {
|
|
872
|
+
opacity: index < 0 ? 0 : 0.45,
|
|
873
|
+
transform: [{ translateX: index < 0 ? 0 : xOf(index, total, plot) }],
|
|
874
|
+
};
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
const labelStyle = useAnimatedStyle(() => {
|
|
878
|
+
const index = activeIndex.value;
|
|
879
|
+
if (index < 0) return { opacity: 0 };
|
|
880
|
+
const x = xOf(index, total, plot);
|
|
881
|
+
const half = LABEL_WIDTH / 2;
|
|
882
|
+
const clamped = Math.min(
|
|
883
|
+
plot.left + plot.width - half,
|
|
884
|
+
Math.max(plot.left + half, x)
|
|
885
|
+
);
|
|
886
|
+
return { opacity: 1, transform: [{ translateX: clamped - half }] };
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
/*
|
|
890
|
+
* The dots ride the *top edge* of each band, which for a stack is the
|
|
891
|
+
* running total rather than the value — so the baselines are recomputed here
|
|
892
|
+
* the same way the bands compute them, and for the same reason.
|
|
893
|
+
*/
|
|
894
|
+
const stacks = useMemo(() => {
|
|
895
|
+
return series.map(([key, seriesColor], index) => {
|
|
896
|
+
const below = series.slice(0, index);
|
|
897
|
+
const baselines =
|
|
898
|
+
stacked && below.length
|
|
899
|
+
? data.map((row) => {
|
|
900
|
+
let sum = 0;
|
|
901
|
+
for (const [k] of below) {
|
|
902
|
+
const value = row[k];
|
|
903
|
+
if (typeof value === 'number' && !Number.isNaN(value)) sum += value;
|
|
904
|
+
}
|
|
905
|
+
return sum;
|
|
906
|
+
})
|
|
907
|
+
: undefined;
|
|
908
|
+
return { key, color: seriesColor, values: columnValues(data, key), baselines };
|
|
909
|
+
});
|
|
910
|
+
}, [series, data, stacked]);
|
|
911
|
+
|
|
912
|
+
const active = activeIndexJS >= 0 ? data[activeIndexJS] : null;
|
|
913
|
+
const fmtValue = formatValue ?? ((value: number) => compactNumber(value));
|
|
914
|
+
const fmtX = formatX ?? ((datum: AreaChartDatum) => String(datum[xDataKey] ?? ''));
|
|
915
|
+
|
|
916
|
+
if (status === 'loading') return null;
|
|
917
|
+
|
|
918
|
+
return (
|
|
919
|
+
<GestureDetector gesture={pan}>
|
|
920
|
+
<View style={StyleSheet.absoluteFill}>
|
|
921
|
+
<Animated.View
|
|
922
|
+
pointerEvents="none"
|
|
923
|
+
style={[
|
|
924
|
+
{
|
|
925
|
+
position: 'absolute',
|
|
926
|
+
left: 0,
|
|
927
|
+
top: plot.top,
|
|
928
|
+
width: 1,
|
|
929
|
+
height: plot.height,
|
|
930
|
+
backgroundColor: stroke,
|
|
931
|
+
},
|
|
932
|
+
crosshairStyle,
|
|
933
|
+
]}
|
|
934
|
+
/>
|
|
935
|
+
<Svg
|
|
936
|
+
width="100%"
|
|
937
|
+
height="100%"
|
|
938
|
+
style={StyleSheet.absoluteFill}
|
|
939
|
+
pointerEvents="none"
|
|
940
|
+
>
|
|
941
|
+
{stacks.map((stack) => (
|
|
942
|
+
<CrosshairDot
|
|
943
|
+
key={stack.key}
|
|
944
|
+
values={stack.values}
|
|
945
|
+
baselines={stack.baselines}
|
|
946
|
+
plot={plot}
|
|
947
|
+
domainMin={domainMin}
|
|
948
|
+
domainMax={domainMax}
|
|
949
|
+
activeIndex={activeIndex}
|
|
950
|
+
total={total}
|
|
951
|
+
color={stack.color}
|
|
952
|
+
/>
|
|
953
|
+
))}
|
|
954
|
+
</Svg>
|
|
955
|
+
<Animated.View
|
|
956
|
+
pointerEvents="none"
|
|
957
|
+
style={[
|
|
958
|
+
{ position: 'absolute', left: 0, top: 0, width: LABEL_WIDTH },
|
|
959
|
+
labelStyle,
|
|
960
|
+
]}
|
|
961
|
+
>
|
|
962
|
+
{active ? (
|
|
963
|
+
<View
|
|
964
|
+
className={cn(
|
|
965
|
+
'rounded-xl border border-border bg-popover px-2.5 py-1.5 shadow-lg',
|
|
966
|
+
className
|
|
967
|
+
)}
|
|
968
|
+
>
|
|
969
|
+
<Text size="xs" muted numberOfLines={1}>
|
|
970
|
+
{fmtX(active)}
|
|
971
|
+
</Text>
|
|
972
|
+
{series.map(([key, seriesColor]) => {
|
|
973
|
+
const value = active[key];
|
|
974
|
+
if (typeof value !== 'number') return null;
|
|
975
|
+
return (
|
|
976
|
+
<View key={key} className="flex-row items-center gap-1.5">
|
|
977
|
+
<View
|
|
978
|
+
style={{
|
|
979
|
+
width: 6,
|
|
980
|
+
height: 6,
|
|
981
|
+
borderRadius: 3,
|
|
982
|
+
backgroundColor: seriesColor,
|
|
983
|
+
}}
|
|
984
|
+
/>
|
|
985
|
+
<Text size="xs" weight="medium">
|
|
986
|
+
{fmtValue(value, key)}
|
|
987
|
+
</Text>
|
|
988
|
+
</View>
|
|
989
|
+
);
|
|
990
|
+
})}
|
|
991
|
+
</View>
|
|
992
|
+
) : null}
|
|
993
|
+
</Animated.View>
|
|
994
|
+
</View>
|
|
995
|
+
</GestureDetector>
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
AreaChartTooltip.displayName = 'AreaChart.Tooltip';
|
|
999
|
+
AreaChartTooltip.layer = 'overlay' as Layer;
|
|
1000
|
+
|
|
1001
|
+
export interface AreaChartLegendProps extends ViewProps {
|
|
1002
|
+
className?: string;
|
|
1003
|
+
/** Prettier names for the series keys. */
|
|
1004
|
+
labels?: Record<string, string>;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* A swatch and a name per series.
|
|
1009
|
+
*
|
|
1010
|
+
* Reversed for a stack, so the key reads in the order the bands appear on the
|
|
1011
|
+
* chart: the last one declared is the top band, and a legend listing it last
|
|
1012
|
+
* points at the bottom one.
|
|
1013
|
+
*/
|
|
1014
|
+
function AreaChartLegend({ className, labels, ...props }: AreaChartLegendProps) {
|
|
1015
|
+
const { series, stacked } = useChart('AreaChart.Legend');
|
|
1016
|
+
if (!series.length) return null;
|
|
1017
|
+
const ordered = stacked ? [...series].reverse() : series;
|
|
1018
|
+
|
|
1019
|
+
return (
|
|
1020
|
+
<View
|
|
1021
|
+
{...props}
|
|
1022
|
+
style={[{ pointerEvents: 'none' }, props.style]}
|
|
1023
|
+
className={cn('absolute right-2 top-1 flex-row gap-3', className)}
|
|
1024
|
+
>
|
|
1025
|
+
{ordered.map(([key, color]) => (
|
|
1026
|
+
<SeriesSwatch key={key} color={color} label={labels?.[key] ?? key} />
|
|
1027
|
+
))}
|
|
1028
|
+
</View>
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
AreaChartLegend.displayName = 'AreaChart.Legend';
|
|
1032
|
+
AreaChartLegend.layer = 'overlay' as Layer;
|
|
1033
|
+
|
|
1034
|
+
/** One series' colour and name. Shared by the legend and the header. */
|
|
1035
|
+
function SeriesSwatch({ color, label }: { color: string; label: string }) {
|
|
1036
|
+
return (
|
|
1037
|
+
<View className="flex-row items-center gap-1.5">
|
|
1038
|
+
<View style={{ backgroundColor: color }} className="h-2 w-2 rounded-full" />
|
|
1039
|
+
<Text size="xs" muted>
|
|
1040
|
+
{label}
|
|
1041
|
+
</Text>
|
|
1042
|
+
</View>
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/* -------------------------------------------------------------------------- */
|
|
1047
|
+
/* Header layer */
|
|
1048
|
+
/* -------------------------------------------------------------------------- */
|
|
1049
|
+
|
|
1050
|
+
export interface AreaChartHeaderProps extends ViewProps {
|
|
1051
|
+
className?: string;
|
|
1052
|
+
/** Small line above the value — what the chart is of. */
|
|
1053
|
+
title?: string;
|
|
1054
|
+
/** The readout. The largest thing on the card, and the first thing read. */
|
|
1055
|
+
value?: string;
|
|
1056
|
+
/** One muted line under the value — a period, a comparison, a total. */
|
|
1057
|
+
caption?: string;
|
|
1058
|
+
/** Prettier names for the series keys, as the legend takes. */
|
|
1059
|
+
labels?: Record<string, string>;
|
|
1060
|
+
/**
|
|
1061
|
+
* Draw a swatch and a name per series along the trailing edge, in the order
|
|
1062
|
+
* the bands appear on a stack. Prefer this to `AreaChart.Legend` on a chart
|
|
1063
|
+
* that has a header: the legend floats over the plot, where a tall band and a
|
|
1064
|
+
* key end up in the same corner.
|
|
1065
|
+
*/
|
|
1066
|
+
legend?: boolean;
|
|
1067
|
+
/** Trailing slot — a control, a badge, a range picker. Wins over `legend`. */
|
|
1068
|
+
children?: ReactNode;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* The strip above the plot: what the chart is of, what it currently reads, and
|
|
1073
|
+
* what the colours mean.
|
|
1074
|
+
*
|
|
1075
|
+
* It belongs to the chart rather than to the card around it because it is about
|
|
1076
|
+
* the *plot* — the number changes as a finger moves along the bands, and the
|
|
1077
|
+
* legend is the series list the chart itself is holding. The card's header is a
|
|
1078
|
+
* caption on the tray the chart sits in; this is the chart introducing itself.
|
|
1079
|
+
*
|
|
1080
|
+
* The value is not derived here. A readout that follows the finger belongs to
|
|
1081
|
+
* whoever owns the data — take it from `onActiveIndexChange` and pass the
|
|
1082
|
+
* formatted string down, so one header can show a total when nothing is pressed
|
|
1083
|
+
* and a point's value when something is.
|
|
1084
|
+
*/
|
|
1085
|
+
function AreaChartHeader({
|
|
1086
|
+
className,
|
|
1087
|
+
title,
|
|
1088
|
+
value,
|
|
1089
|
+
caption,
|
|
1090
|
+
labels,
|
|
1091
|
+
legend = false,
|
|
1092
|
+
children,
|
|
1093
|
+
...props
|
|
1094
|
+
}: AreaChartHeaderProps) {
|
|
1095
|
+
const { series, stacked } = useChart('AreaChart.Header');
|
|
1096
|
+
const ordered = stacked ? [...series].reverse() : series;
|
|
1097
|
+
const trailing =
|
|
1098
|
+
children ??
|
|
1099
|
+
(legend && ordered.length ? (
|
|
1100
|
+
<View className="flex-row flex-wrap items-center justify-end gap-x-3 gap-y-1">
|
|
1101
|
+
{ordered.map(([key, color]) => (
|
|
1102
|
+
<SeriesSwatch key={key} color={color} label={labels?.[key] ?? key} />
|
|
1103
|
+
))}
|
|
1104
|
+
</View>
|
|
1105
|
+
) : null);
|
|
1106
|
+
|
|
1107
|
+
return (
|
|
1108
|
+
<View
|
|
1109
|
+
{...props}
|
|
1110
|
+
className={cn('flex-row items-start justify-between gap-3 pb-3', className)}
|
|
1111
|
+
>
|
|
1112
|
+
<View className="flex-1 gap-0.5">
|
|
1113
|
+
{title ? (
|
|
1114
|
+
<Text size="xs" muted>
|
|
1115
|
+
{title}
|
|
1116
|
+
</Text>
|
|
1117
|
+
) : null}
|
|
1118
|
+
{value ? (
|
|
1119
|
+
<Text size="xl" weight="bold">
|
|
1120
|
+
{value}
|
|
1121
|
+
</Text>
|
|
1122
|
+
) : null}
|
|
1123
|
+
{caption ? (
|
|
1124
|
+
<Text size="xs" muted>
|
|
1125
|
+
{caption}
|
|
1126
|
+
</Text>
|
|
1127
|
+
) : null}
|
|
1128
|
+
</View>
|
|
1129
|
+
{/* Shrinkable, unlike a view's default in React Native. Held rigid, a
|
|
1130
|
+
three-series key takes the width it wants and the caption underneath
|
|
1131
|
+
the value wraps to two lines to make room for it. */}
|
|
1132
|
+
{trailing ? <View className="shrink pt-1">{trailing}</View> : null}
|
|
1133
|
+
</View>
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
AreaChartHeader.displayName = 'AreaChart.Header';
|
|
1137
|
+
AreaChartHeader.layer = 'header' as Layer;
|
|
1138
|
+
|
|
1139
|
+
export const AreaChart = Object.assign(AreaChartRoot, {
|
|
1140
|
+
Header: AreaChartHeader,
|
|
1141
|
+
Grid: AreaChartGrid,
|
|
1142
|
+
Area: AreaChartArea,
|
|
1143
|
+
XAxis: AreaChartXAxis,
|
|
1144
|
+
YAxis: AreaChartYAxis,
|
|
1145
|
+
Tooltip: AreaChartTooltip,
|
|
1146
|
+
Legend: AreaChartLegend,
|
|
1147
|
+
});
|