@chipmobilesdk/rn-chart 0.2.0 → 0.3.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/CHANGELOG.md +164 -115
- package/README.md +391 -381
- package/package.json +49 -49
- package/skills/sdk-rn-chart/SKILL.md +68 -69
- package/src/a11y/summary.ts +190 -177
- package/src/contract/datasource.ts +126 -136
- package/src/contract/errors.ts +129 -133
- package/src/contract/format.ts +168 -168
- package/src/contract/interaction.ts +70 -71
- package/src/contract/marker.ts +75 -75
- package/src/contract/props.ts +64 -60
- package/src/contract/state.ts +88 -94
- package/src/contract/strings.ts +48 -0
- package/src/contract/theme.ts +169 -177
- package/src/contract/value.ts +121 -125
- package/src/geometry/bar.ts +271 -273
- package/src/geometry/distribution.ts +197 -203
- package/src/geometry/heatmap.ts +189 -195
- package/src/geometry/hitTest.ts +117 -120
- package/src/geometry/layout.ts +162 -173
- package/src/geometry/markers.ts +55 -57
- package/src/geometry/path.ts +193 -199
- package/src/geometry/progress.ts +223 -230
- package/src/geometry/scale.ts +140 -146
- package/src/geometry/ticks.ts +145 -153
- package/src/geometry/timeSeries.ts +168 -168
- package/src/geometry/types.ts +94 -94
- package/src/index.ts +162 -176
- package/src/react/ChartBoundary.tsx +96 -96
- package/src/react/ChartFrame.tsx +596 -622
- package/src/react/ChartLegend.tsx +216 -215
- package/src/react/ChartThemeProvider.tsx +44 -48
- package/src/react/charts.tsx +154 -161
- package/src/react/useChartLayout.ts +124 -124
- package/src/react/useChartSelection.ts +152 -170
- package/src/render/Axis.tsx +120 -122
- package/src/render/Marks.tsx +173 -205
- package/src/validation/budgets.ts +113 -110
- package/src/validation/validateDatasource.ts +174 -174
|
@@ -1,215 +1,216 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Legend (FR-066).
|
|
3
|
-
*
|
|
4
|
-
* On by default for multi-series and multi-segment charts, disableable, and its
|
|
5
|
-
* model is exported so an app can render its own layout instead. Every entry
|
|
6
|
-
* carries a dash pattern as well as a colour, so series stay distinguishable
|
|
7
|
-
* without colour perception.
|
|
8
|
-
*/
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
case '
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
readonly
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
readonly
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Legend (FR-066).
|
|
3
|
+
*
|
|
4
|
+
* On by default for multi-series and multi-segment charts, disableable, and its
|
|
5
|
+
* model is exported so an app can render its own layout instead. Every entry
|
|
6
|
+
* carries a dash pattern as well as a colour, so series stay distinguishable
|
|
7
|
+
* without colour perception.
|
|
8
|
+
*/
|
|
9
|
+
import { chartStrings, type ChartStrings } from '../contract/strings';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
12
|
+
|
|
13
|
+
import type { ChartDatasource, ChartFamily } from '../contract/datasource';
|
|
14
|
+
import {
|
|
15
|
+
resolveSegmentColors,
|
|
16
|
+
resolveSeriesColor,
|
|
17
|
+
segmentKey,
|
|
18
|
+
type ChartTheme,
|
|
19
|
+
type LegendEntry,
|
|
20
|
+
} from '../contract/theme';
|
|
21
|
+
|
|
22
|
+
/** Minimum tap target, per platform accessibility guidance (UX-003). */
|
|
23
|
+
const MIN_TAP_TARGET = 44;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Legend model. A distribution chart gets one entry per segment, in the colour
|
|
27
|
+
* its arc is drawn in; it used to get one per series, so a one-series donut of
|
|
28
|
+
* four segments showed a single entry (CHT-02).
|
|
29
|
+
*/
|
|
30
|
+
export function buildLegendEntries<M>(
|
|
31
|
+
datasource: ChartDatasource<M>,
|
|
32
|
+
theme: ChartTheme,
|
|
33
|
+
hidden: ReadonlySet<string>,
|
|
34
|
+
toggleable: boolean,
|
|
35
|
+
family?: ChartFamily,
|
|
36
|
+
): LegendEntry[] {
|
|
37
|
+
if (family === 'distribution') {
|
|
38
|
+
const colors = resolveSegmentColors(datasource, theme);
|
|
39
|
+
return datasource.series.flatMap(series =>
|
|
40
|
+
series.points.map(point => {
|
|
41
|
+
const single = series.points.length === 1;
|
|
42
|
+
let label = series.label;
|
|
43
|
+
if (!single) {
|
|
44
|
+
label = point.position.kind === 'category' ? point.position.category : point.id;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
seriesId: series.id,
|
|
48
|
+
pointId: point.id,
|
|
49
|
+
label,
|
|
50
|
+
color: colors[segmentKey(series.id, point.id)],
|
|
51
|
+
pattern: 'solid' as const,
|
|
52
|
+
visible: !hidden.has(series.id),
|
|
53
|
+
// Visibility is per series, so only a one-segment series toggles.
|
|
54
|
+
toggleable: toggleable && single,
|
|
55
|
+
};
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return datasource.series.map((series, index) => ({
|
|
60
|
+
seriesId: series.id,
|
|
61
|
+
label: series.label,
|
|
62
|
+
color: resolveSeriesColor(theme, index, series.color),
|
|
63
|
+
pattern: series.pattern ?? 'solid',
|
|
64
|
+
visible: !hidden.has(series.id),
|
|
65
|
+
toggleable,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function keyOf(entry: LegendEntry): string {
|
|
70
|
+
return entry.pointId === undefined ? entry.seriesId : segmentKey(entry.seriesId, entry.pointId);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function segmentCountFor(pattern: LegendEntry['pattern']): number {
|
|
74
|
+
switch (pattern) {
|
|
75
|
+
case 'dotted':
|
|
76
|
+
return 4;
|
|
77
|
+
case 'dashed':
|
|
78
|
+
case 'dashDot':
|
|
79
|
+
return 2;
|
|
80
|
+
default:
|
|
81
|
+
return 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function PatternSwatch({
|
|
86
|
+
entry,
|
|
87
|
+
theme,
|
|
88
|
+
}: {
|
|
89
|
+
readonly entry: LegendEntry;
|
|
90
|
+
readonly theme: ChartTheme;
|
|
91
|
+
}) {
|
|
92
|
+
// Dashes are drawn as gapped segments so the pattern reads at swatch size —
|
|
93
|
+
// this is what gives a series an identity beyond its colour (FR-064).
|
|
94
|
+
const segments = segmentCountFor(entry.pattern);
|
|
95
|
+
const gap = segments > 1 ? 1 : 0;
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<View style={styles.swatchRow}>
|
|
99
|
+
{Array.from({ length: segments }, (_, i) => (
|
|
100
|
+
<View
|
|
101
|
+
key={i}
|
|
102
|
+
style={[
|
|
103
|
+
styles.swatchSegment,
|
|
104
|
+
entry.visible ? styles.swatchVisible : styles.swatchHidden,
|
|
105
|
+
{
|
|
106
|
+
width: 12 / segments - gap,
|
|
107
|
+
marginRight: gap,
|
|
108
|
+
backgroundColor: entry.visible ? entry.color : theme.muted,
|
|
109
|
+
},
|
|
110
|
+
]}
|
|
111
|
+
/>
|
|
112
|
+
))}
|
|
113
|
+
</View>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function ChartLegend({
|
|
118
|
+
entries,
|
|
119
|
+
theme,
|
|
120
|
+
onToggle,
|
|
121
|
+
strings = chartStrings.defaults,
|
|
122
|
+
}: {
|
|
123
|
+
readonly entries: readonly LegendEntry[];
|
|
124
|
+
readonly theme: ChartTheme;
|
|
125
|
+
readonly onToggle?: (seriesId: string) => void;
|
|
126
|
+
readonly strings?: ChartStrings;
|
|
127
|
+
}) {
|
|
128
|
+
if (entries.length === 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<View style={styles.container} accessibilityRole="list">
|
|
134
|
+
{entries.map(entry => {
|
|
135
|
+
const content = (
|
|
136
|
+
<>
|
|
137
|
+
<PatternSwatch entry={entry} theme={theme} />
|
|
138
|
+
<Text
|
|
139
|
+
style={[
|
|
140
|
+
styles.label,
|
|
141
|
+
{
|
|
142
|
+
color: entry.visible ? theme.typography.legendLabel.color : theme.muted,
|
|
143
|
+
fontSize: theme.typography.legendLabel.fontSize,
|
|
144
|
+
},
|
|
145
|
+
]}
|
|
146
|
+
numberOfLines={1}
|
|
147
|
+
>
|
|
148
|
+
{entry.label}
|
|
149
|
+
</Text>
|
|
150
|
+
</>
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
if (!entry.toggleable || !onToggle) {
|
|
154
|
+
return (
|
|
155
|
+
<View key={keyOf(entry)} style={styles.entry} accessibilityRole="text">
|
|
156
|
+
{content}
|
|
157
|
+
</View>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Pressable
|
|
163
|
+
key={keyOf(entry)}
|
|
164
|
+
onPress={() => onToggle(entry.seriesId)}
|
|
165
|
+
accessibilityRole="switch"
|
|
166
|
+
accessibilityState={{ checked: entry.visible }}
|
|
167
|
+
accessibilityLabel={entry.label}
|
|
168
|
+
accessibilityHint={
|
|
169
|
+
entry.visible ? strings['chart.legend.hide'] : strings['chart.legend.show']
|
|
170
|
+
}
|
|
171
|
+
style={[styles.entry, styles.tappable]}
|
|
172
|
+
>
|
|
173
|
+
{content}
|
|
174
|
+
</Pressable>
|
|
175
|
+
);
|
|
176
|
+
})}
|
|
177
|
+
</View>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const styles = StyleSheet.create({
|
|
182
|
+
container: {
|
|
183
|
+
flexDirection: 'row',
|
|
184
|
+
flexWrap: 'wrap',
|
|
185
|
+
alignItems: 'center',
|
|
186
|
+
},
|
|
187
|
+
entry: {
|
|
188
|
+
flexDirection: 'row',
|
|
189
|
+
alignItems: 'center',
|
|
190
|
+
marginRight: 12,
|
|
191
|
+
marginTop: 4,
|
|
192
|
+
},
|
|
193
|
+
tappable: {
|
|
194
|
+
minHeight: MIN_TAP_TARGET,
|
|
195
|
+
paddingVertical: 4,
|
|
196
|
+
},
|
|
197
|
+
swatchRow: {
|
|
198
|
+
flexDirection: 'row',
|
|
199
|
+
alignItems: 'center',
|
|
200
|
+
width: 12,
|
|
201
|
+
marginRight: 6,
|
|
202
|
+
},
|
|
203
|
+
swatchSegment: {
|
|
204
|
+
height: 3,
|
|
205
|
+
borderRadius: 2,
|
|
206
|
+
},
|
|
207
|
+
swatchVisible: {
|
|
208
|
+
opacity: 1,
|
|
209
|
+
},
|
|
210
|
+
swatchHidden: {
|
|
211
|
+
opacity: 0.4,
|
|
212
|
+
},
|
|
213
|
+
label: {
|
|
214
|
+
flexShrink: 1,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
@@ -1,48 +1,44 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Theme resolution: prop -> context -> built-in default keyed on the OS scheme.
|
|
3
|
-
*
|
|
4
|
-
* The dependency is inverted on purpose. The package declares a `ChartTheme`
|
|
5
|
-
* value type and any design system can satisfy it, which is what keeps
|
|
6
|
-
* `@chipmobilesdk/rn-theme` and `react-native-unistyles` out of this package
|
|
7
|
-
* entirely (FR-065, LIB-004).
|
|
8
|
-
*/
|
|
9
|
-
import React, { createContext, useContext, useMemo } from 'react';
|
|
10
|
-
import { useColorScheme } from 'react-native';
|
|
11
|
-
|
|
12
|
-
import { defaultChartTheme, type ChartTheme } from '../contract/theme';
|
|
13
|
-
|
|
14
|
-
const ChartThemeContext = createContext<ChartTheme | undefined>(undefined);
|
|
15
|
-
|
|
16
|
-
export function ChartThemeProvider({
|
|
17
|
-
theme,
|
|
18
|
-
children,
|
|
19
|
-
}: {
|
|
20
|
-
readonly theme: ChartTheme;
|
|
21
|
-
readonly children: React.ReactNode;
|
|
22
|
-
}) {
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
return defaultChartTheme(scheme === 'dark' ? 'dark' : 'light');
|
|
47
|
-
}, [explicit, fromContext, scheme]);
|
|
48
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Theme resolution: prop -> context -> built-in default keyed on the OS scheme.
|
|
3
|
+
*
|
|
4
|
+
* The dependency is inverted on purpose. The package declares a `ChartTheme`
|
|
5
|
+
* value type and any design system can satisfy it, which is what keeps
|
|
6
|
+
* `@chipmobilesdk/rn-theme` and `react-native-unistyles` out of this package
|
|
7
|
+
* entirely (FR-065, LIB-004).
|
|
8
|
+
*/
|
|
9
|
+
import React, { createContext, useContext, useMemo } from 'react';
|
|
10
|
+
import { useColorScheme } from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { defaultChartTheme, type ChartTheme } from '../contract/theme';
|
|
13
|
+
|
|
14
|
+
const ChartThemeContext = createContext<ChartTheme | undefined>(undefined);
|
|
15
|
+
|
|
16
|
+
export function ChartThemeProvider({
|
|
17
|
+
theme,
|
|
18
|
+
children,
|
|
19
|
+
}: {
|
|
20
|
+
readonly theme: ChartTheme;
|
|
21
|
+
readonly children: React.ReactNode;
|
|
22
|
+
}) {
|
|
23
|
+
return <ChartThemeContext.Provider value={theme}>{children}</ChartThemeContext.Provider>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* An explicit prop wins, then context, then the built-in default for the
|
|
28
|
+
* current OS colour scheme — so a chart renders correctly with no theme
|
|
29
|
+
* configuration at all.
|
|
30
|
+
*/
|
|
31
|
+
export function useChartTheme(explicit?: ChartTheme): ChartTheme {
|
|
32
|
+
const fromContext = useContext(ChartThemeContext);
|
|
33
|
+
const scheme = useColorScheme();
|
|
34
|
+
|
|
35
|
+
return useMemo(() => {
|
|
36
|
+
if (explicit) {
|
|
37
|
+
return explicit;
|
|
38
|
+
}
|
|
39
|
+
if (fromContext) {
|
|
40
|
+
return fromContext;
|
|
41
|
+
}
|
|
42
|
+
return defaultChartTheme(scheme === 'dark' ? 'dark' : 'light');
|
|
43
|
+
}, [explicit, fromContext, scheme]);
|
|
44
|
+
}
|