@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +164 -115
  2. package/README.md +391 -381
  3. package/package.json +49 -49
  4. package/skills/sdk-rn-chart/SKILL.md +68 -69
  5. package/src/a11y/summary.ts +190 -177
  6. package/src/contract/datasource.ts +126 -136
  7. package/src/contract/errors.ts +129 -133
  8. package/src/contract/format.ts +168 -168
  9. package/src/contract/interaction.ts +70 -71
  10. package/src/contract/marker.ts +75 -75
  11. package/src/contract/props.ts +64 -60
  12. package/src/contract/state.ts +88 -94
  13. package/src/contract/strings.ts +48 -0
  14. package/src/contract/theme.ts +169 -177
  15. package/src/contract/value.ts +121 -125
  16. package/src/geometry/bar.ts +271 -273
  17. package/src/geometry/distribution.ts +197 -203
  18. package/src/geometry/heatmap.ts +189 -195
  19. package/src/geometry/hitTest.ts +117 -120
  20. package/src/geometry/layout.ts +162 -173
  21. package/src/geometry/markers.ts +55 -57
  22. package/src/geometry/path.ts +193 -199
  23. package/src/geometry/progress.ts +223 -230
  24. package/src/geometry/scale.ts +140 -146
  25. package/src/geometry/ticks.ts +145 -153
  26. package/src/geometry/timeSeries.ts +168 -168
  27. package/src/geometry/types.ts +94 -94
  28. package/src/index.ts +162 -176
  29. package/src/react/ChartBoundary.tsx +96 -96
  30. package/src/react/ChartFrame.tsx +596 -622
  31. package/src/react/ChartLegend.tsx +216 -215
  32. package/src/react/ChartThemeProvider.tsx +44 -48
  33. package/src/react/charts.tsx +154 -161
  34. package/src/react/useChartLayout.ts +124 -124
  35. package/src/react/useChartSelection.ts +152 -170
  36. package/src/render/Axis.tsx +120 -122
  37. package/src/render/Marks.tsx +173 -205
  38. package/src/validation/budgets.ts +113 -110
  39. package/src/validation/validateDatasource.ts +174 -174
@@ -1,170 +1,152 @@
1
- /**
2
- * Selection ownership: uncontrolled by default, controlled when the app
3
- * supplies a value (FR-054a).
4
- *
5
- * Two properties matter and both are load-bearing:
6
- *
7
- * - The selection is **resolved against the current datasource on every
8
- * render** rather than stored resolved. A reference to a point that no
9
- * longer exists therefore draws nothing, in either mode (FR-054).
10
- * - Selection state lives outside the geometry memo, so highlighting repaints
11
- * the highlight without recomputing scales or path strings (FR-073).
12
- */
13
- import { useCallback, useEffect, useRef, useState } from 'react';
14
-
15
- import type {
16
- PointInteractionEvent,
17
- SelectionRef,
18
- SeriesInteractionEvent,
19
- } from '../contract/interaction';
20
- import { sameSelection } from '../contract/interaction';
21
- import { findRegion, type HitRegion } from '../geometry/hitTest';
22
-
23
- export interface UseChartSelectionArgs<M> {
24
- /** Present (including `null`) means the app owns the selection. */
25
- readonly controlled?: SelectionRef | null;
26
- readonly onSelectionChange?: (
27
- next: SelectionRef | null,
28
- event: PointInteractionEvent<M> | null,
29
- ) => void;
30
- readonly onSelectionUnresolvable?: (ref: SelectionRef) => void;
31
- readonly hitRegions: readonly HitRegion[];
32
- /** Non-ready statuses suppress selection entirely (FR-053). */
33
- readonly interactionSuppressed: boolean;
34
- /**
35
- * False until the container has been measured and geometry exists.
36
- *
37
- * Without this, a chart that has not laid out yet has no hit regions, and a
38
- * perfectly valid app-controlled selection would be reported as
39
- * unresolvable on first paint — telling the app its state is wrong when it
40
- * is not.
41
- */
42
- readonly geometryReady: boolean;
43
- }
44
-
45
- export interface ChartSelectionApi<M> {
46
- /** What to draw. Never a reference the datasource cannot resolve. */
47
- readonly resolved: HitRegion | undefined;
48
- readonly selection: SelectionRef | null;
49
- readonly select: (
50
- ref: SelectionRef | null,
51
- event: PointInteractionEvent<M> | null,
52
- ) => void;
53
- }
54
-
55
- export function useChartSelection<M>(
56
- args: UseChartSelectionArgs<M>,
57
- ): ChartSelectionApi<M> {
58
- const isControlled = args.controlled !== undefined;
59
- const [internal, setInternal] = useState<SelectionRef | null>(null);
60
-
61
- const selection = isControlled ? args.controlled ?? null : internal;
62
-
63
- // Resolve fresh every render. A stale reference resolves to `undefined` and
64
- // simply is not drawn.
65
- const resolved =
66
- selection && !args.interactionSuppressed
67
- ? findRegion(args.hitRegions, selection.seriesId, selection.pointId)
68
- : undefined;
69
-
70
- // Tell a controlling app when its selection stopped resolving, rather than
71
- // silently ignoring it. Uncontrolled selections are cleared instead.
72
- const reportedRef = useRef<string | null>(null);
73
- useEffect(() => {
74
- if (
75
- !selection ||
76
- resolved ||
77
- args.interactionSuppressed ||
78
- !args.geometryReady
79
- ) {
80
- reportedRef.current = null;
81
- return;
82
- }
83
-
84
- const key = `${selection.seriesId}:${selection.pointId ?? ''}`;
85
- if (reportedRef.current === key) {
86
- return;
87
- }
88
- reportedRef.current = key;
89
-
90
- if (isControlled) {
91
- args.onSelectionUnresolvable?.(selection);
92
- } else {
93
- setInternal(null);
94
- }
95
- }, [selection, resolved, isControlled, args, args.geometryReady]);
96
-
97
- const select = useCallback(
98
- (ref: SelectionRef | null, event: PointInteractionEvent<M> | null) => {
99
- if (args.interactionSuppressed) {
100
- return;
101
- }
102
- if (!isControlled) {
103
- setInternal(prev => (sameSelection(prev, ref) ? null : ref));
104
- }
105
- args.onSelectionChange?.(ref, event);
106
- },
107
- [args, isControlled],
108
- );
109
-
110
- return { resolved, selection, select };
111
- }
112
-
113
- export interface UseSeriesVisibilityArgs<M> {
114
- /** Present means the app owns visibility. */
115
- readonly controlled?: readonly string[];
116
- readonly onVisibilityChange?: (event: SeriesInteractionEvent<M>) => void;
117
- /** Toggling stays off unless the app opts in (FR-049). */
118
- readonly enabled: boolean;
119
- /** Series marked hidden in the datasource itself. */
120
- readonly initiallyHidden: readonly string[];
121
- }
122
-
123
- export interface SeriesVisibilityApi {
124
- readonly hidden: ReadonlySet<string>;
125
- readonly toggle: (seriesId: string, metadata?: unknown) => void;
126
- readonly enabled: boolean;
127
- }
128
-
129
- export function useSeriesVisibility<M>(
130
- args: UseSeriesVisibilityArgs<M>,
131
- ): SeriesVisibilityApi {
132
- const isControlled = args.controlled !== undefined;
133
- const [internal, setInternal] = useState<readonly string[]>(
134
- args.initiallyHidden,
135
- );
136
-
137
- const hiddenList = isControlled ? args.controlled ?? [] : internal;
138
- const hidden = new Set(hiddenList);
139
-
140
- const toggle = useCallback(
141
- (seriesId: string, metadata?: unknown) => {
142
- if (!args.enabled) {
143
- return;
144
- }
145
-
146
- const currentlyHidden = new Set(
147
- isControlled ? args.controlled ?? [] : internal,
148
- );
149
- const willBeVisible = currentlyHidden.has(seriesId);
150
-
151
- if (!isControlled) {
152
- setInternal(prev =>
153
- prev.includes(seriesId)
154
- ? prev.filter(id => id !== seriesId)
155
- : [...prev, seriesId],
156
- );
157
- }
158
-
159
- args.onVisibilityChange?.({
160
- type: 'visibilityChange',
161
- seriesId,
162
- visible: willBeVisible,
163
- metadata: metadata as M,
164
- });
165
- },
166
- [args, internal, isControlled],
167
- );
168
-
169
- return { hidden, toggle, enabled: args.enabled };
170
- }
1
+ /**
2
+ * Selection ownership: uncontrolled by default, controlled when the app
3
+ * supplies a value (FR-054a).
4
+ *
5
+ * Two properties matter and both are load-bearing:
6
+ *
7
+ * - The selection is **resolved against the current datasource on every
8
+ * render** rather than stored resolved. A reference to a point that no
9
+ * longer exists therefore draws nothing, in either mode (FR-054).
10
+ * - Selection state lives outside the geometry memo, so highlighting repaints
11
+ * the highlight without recomputing scales or path strings (FR-073).
12
+ */
13
+ import { useCallback, useEffect, useRef, useState } from 'react';
14
+
15
+ import type {
16
+ PointInteractionEvent,
17
+ SelectionRef,
18
+ SeriesInteractionEvent,
19
+ } from '../contract/interaction';
20
+ import { sameSelection } from '../contract/interaction';
21
+ import { findRegion, type HitRegion } from '../geometry/hitTest';
22
+
23
+ export interface UseChartSelectionArgs<M> {
24
+ /** Present (including `null`) means the app owns the selection. */
25
+ readonly controlled?: SelectionRef | null;
26
+ readonly onSelectionChange?: (
27
+ next: SelectionRef | null,
28
+ event: PointInteractionEvent<M> | null,
29
+ ) => void;
30
+ readonly onSelectionUnresolvable?: (ref: SelectionRef) => void;
31
+ readonly hitRegions: readonly HitRegion[];
32
+ /** Non-ready statuses suppress selection entirely (FR-053). */
33
+ readonly interactionSuppressed: boolean;
34
+ /**
35
+ * False until the container has been measured and geometry exists.
36
+ *
37
+ * Without this, a chart that has not laid out yet has no hit regions, and a
38
+ * perfectly valid app-controlled selection would be reported as
39
+ * unresolvable on first paint — telling the app its state is wrong when it
40
+ * is not.
41
+ */
42
+ readonly geometryReady: boolean;
43
+ }
44
+
45
+ export interface ChartSelectionApi<M> {
46
+ /** What to draw. Never a reference the datasource cannot resolve. */
47
+ readonly resolved: HitRegion | undefined;
48
+ readonly selection: SelectionRef | null;
49
+ readonly select: (ref: SelectionRef | null, event: PointInteractionEvent<M> | null) => void;
50
+ }
51
+
52
+ export function useChartSelection<M>(args: UseChartSelectionArgs<M>): ChartSelectionApi<M> {
53
+ const isControlled = args.controlled !== undefined;
54
+ const [internal, setInternal] = useState<SelectionRef | null>(null);
55
+
56
+ const selection = isControlled ? (args.controlled ?? null) : internal;
57
+
58
+ // Resolve fresh every render. A stale reference resolves to `undefined` and
59
+ // simply is not drawn.
60
+ const resolved =
61
+ selection && !args.interactionSuppressed
62
+ ? findRegion(args.hitRegions, selection.seriesId, selection.pointId)
63
+ : undefined;
64
+
65
+ // Tell a controlling app when its selection stopped resolving, rather than
66
+ // silently ignoring it. Uncontrolled selections are cleared instead.
67
+ const reportedRef = useRef<string | null>(null);
68
+ useEffect(() => {
69
+ if (!selection || resolved || args.interactionSuppressed || !args.geometryReady) {
70
+ reportedRef.current = null;
71
+ return;
72
+ }
73
+
74
+ const key = `${selection.seriesId}:${selection.pointId ?? ''}`;
75
+ if (reportedRef.current === key) {
76
+ return;
77
+ }
78
+ reportedRef.current = key;
79
+
80
+ if (isControlled) {
81
+ args.onSelectionUnresolvable?.(selection);
82
+ } else {
83
+ setInternal(null);
84
+ }
85
+ }, [selection, resolved, isControlled, args, args.geometryReady]);
86
+
87
+ const select = useCallback(
88
+ (ref: SelectionRef | null, event: PointInteractionEvent<M> | null) => {
89
+ if (args.interactionSuppressed) {
90
+ return;
91
+ }
92
+ if (!isControlled) {
93
+ setInternal(prev => (sameSelection(prev, ref) ? null : ref));
94
+ }
95
+ args.onSelectionChange?.(ref, event);
96
+ },
97
+ [args, isControlled],
98
+ );
99
+
100
+ return { resolved, selection, select };
101
+ }
102
+
103
+ export interface UseSeriesVisibilityArgs<M> {
104
+ /** Present means the app owns visibility. */
105
+ readonly controlled?: readonly string[];
106
+ readonly onVisibilityChange?: (event: SeriesInteractionEvent<M>) => void;
107
+ /** Toggling stays off unless the app opts in (FR-049). */
108
+ readonly enabled: boolean;
109
+ /** Series marked hidden in the datasource itself. */
110
+ readonly initiallyHidden: readonly string[];
111
+ }
112
+
113
+ export interface SeriesVisibilityApi {
114
+ readonly hidden: ReadonlySet<string>;
115
+ readonly toggle: (seriesId: string, metadata?: unknown) => void;
116
+ readonly enabled: boolean;
117
+ }
118
+
119
+ export function useSeriesVisibility<M>(args: UseSeriesVisibilityArgs<M>): SeriesVisibilityApi {
120
+ const isControlled = args.controlled !== undefined;
121
+ const [internal, setInternal] = useState<readonly string[]>(args.initiallyHidden);
122
+
123
+ const hiddenList = isControlled ? (args.controlled ?? []) : internal;
124
+ const hidden = new Set(hiddenList);
125
+
126
+ const toggle = useCallback(
127
+ (seriesId: string, metadata?: unknown) => {
128
+ if (!args.enabled) {
129
+ return;
130
+ }
131
+
132
+ const currentlyHidden = new Set(isControlled ? (args.controlled ?? []) : internal);
133
+ const willBeVisible = currentlyHidden.has(seriesId);
134
+
135
+ if (!isControlled) {
136
+ setInternal(prev =>
137
+ prev.includes(seriesId) ? prev.filter(id => id !== seriesId) : [...prev, seriesId],
138
+ );
139
+ }
140
+
141
+ args.onVisibilityChange?.({
142
+ type: 'visibilityChange',
143
+ seriesId,
144
+ visible: willBeVisible,
145
+ metadata: metadata as M,
146
+ });
147
+ },
148
+ [args, internal, isControlled],
149
+ );
150
+
151
+ return { hidden, toggle, enabled: args.enabled };
152
+ }
@@ -1,122 +1,120 @@
1
- /**
2
- * Axis and grid rendering. Private SVG adapter.
3
- *
4
- * Ticks arrive already thinned by `geometry/ticks.ts`, so nothing here decides
5
- * what fits — this module only draws what the geometry chose.
6
- */
7
- import React from 'react';
8
- import { G, Line, Text as SvgText } from 'react-native-svg';
9
-
10
- import type { ChartTheme } from '../contract/theme';
11
- import type { PlotRect } from '../geometry/layout';
12
- import { plotBottom, plotRight } from '../geometry/layout';
13
- import type { Tick } from '../geometry/ticks';
14
-
15
- export function Grid({
16
- plot,
17
- yTicks,
18
- theme,
19
- }: {
20
- readonly plot: PlotRect;
21
- readonly yTicks: readonly Tick[];
22
- readonly theme: ChartTheme;
23
- }) {
24
- return (
25
- <G>
26
- {yTicks.map(tick => (
27
- <Line
28
- key={`grid-${tick.value}`}
29
- x1={plot.x}
30
- y1={tick.position}
31
- x2={plotRight(plot)}
32
- y2={tick.position}
33
- stroke={theme.grid}
34
- strokeWidth={1}
35
- />
36
- ))}
37
- </G>
38
- );
39
- }
40
-
41
- export function XAxis({
42
- plot,
43
- ticks,
44
- theme,
45
- fontSize,
46
- rotateLabels,
47
- }: {
48
- readonly plot: PlotRect;
49
- readonly ticks: readonly Tick[];
50
- readonly theme: ChartTheme;
51
- readonly fontSize: number;
52
- readonly rotateLabels?: boolean;
53
- }) {
54
- const baseline = plotBottom(plot);
55
-
56
- return (
57
- <G>
58
- <Line
59
- x1={plot.x}
60
- y1={baseline}
61
- x2={plotRight(plot)}
62
- y2={baseline}
63
- stroke={theme.axis}
64
- strokeWidth={1}
65
- />
66
- {ticks.map(tick => (
67
- <SvgText
68
- key={`x-${tick.position}-${tick.label}`}
69
- x={tick.position}
70
- y={baseline + fontSize + 4}
71
- fill={theme.typography.axisLabel.color}
72
- fontSize={fontSize}
73
- textAnchor={rotateLabels ? 'end' : 'middle'}
74
- transform={
75
- rotateLabels
76
- ? `rotate(-45, ${tick.position}, ${baseline + fontSize + 4})`
77
- : undefined
78
- }
79
- >
80
- {tick.label}
81
- </SvgText>
82
- ))}
83
- </G>
84
- );
85
- }
86
-
87
- export function YAxis({
88
- plot,
89
- ticks,
90
- theme,
91
- fontSize,
92
- }: {
93
- readonly plot: PlotRect;
94
- readonly ticks: readonly Tick[];
95
- readonly theme: ChartTheme;
96
- readonly fontSize: number;
97
- }) {
98
- return (
99
- <G>
100
- <Line
101
- x1={plot.x}
102
- y1={plot.y}
103
- x2={plot.x}
104
- y2={plotBottom(plot)}
105
- stroke={theme.axis}
106
- strokeWidth={1}
107
- />
108
- {ticks.map(tick => (
109
- <SvgText
110
- key={`y-${tick.position}-${tick.label}`}
111
- x={plot.x - 6}
112
- y={tick.position + fontSize / 3}
113
- fill={theme.typography.axisLabel.color}
114
- fontSize={fontSize}
115
- textAnchor="end"
116
- >
117
- {tick.label}
118
- </SvgText>
119
- ))}
120
- </G>
121
- );
122
- }
1
+ /**
2
+ * Axis and grid rendering. Private SVG adapter.
3
+ *
4
+ * Ticks arrive already thinned by `geometry/ticks.ts`, so nothing here decides
5
+ * what fits — this module only draws what the geometry chose.
6
+ */
7
+ import React from 'react';
8
+ import { G, Line, Text as SvgText } from 'react-native-svg';
9
+
10
+ import type { ChartTheme } from '../contract/theme';
11
+ import type { PlotRect } from '../geometry/layout';
12
+ import { plotBottom, plotRight } from '../geometry/layout';
13
+ import type { Tick } from '../geometry/ticks';
14
+
15
+ export function Grid({
16
+ plot,
17
+ yTicks,
18
+ theme,
19
+ }: {
20
+ readonly plot: PlotRect;
21
+ readonly yTicks: readonly Tick[];
22
+ readonly theme: ChartTheme;
23
+ }) {
24
+ return (
25
+ <G>
26
+ {yTicks.map(tick => (
27
+ <Line
28
+ key={`grid-${tick.value}`}
29
+ x1={plot.x}
30
+ y1={tick.position}
31
+ x2={plotRight(plot)}
32
+ y2={tick.position}
33
+ stroke={theme.grid}
34
+ strokeWidth={1}
35
+ />
36
+ ))}
37
+ </G>
38
+ );
39
+ }
40
+
41
+ export function XAxis({
42
+ plot,
43
+ ticks,
44
+ theme,
45
+ fontSize,
46
+ rotateLabels,
47
+ }: {
48
+ readonly plot: PlotRect;
49
+ readonly ticks: readonly Tick[];
50
+ readonly theme: ChartTheme;
51
+ readonly fontSize: number;
52
+ readonly rotateLabels?: boolean;
53
+ }) {
54
+ const baseline = plotBottom(plot);
55
+
56
+ return (
57
+ <G>
58
+ <Line
59
+ x1={plot.x}
60
+ y1={baseline}
61
+ x2={plotRight(plot)}
62
+ y2={baseline}
63
+ stroke={theme.axis}
64
+ strokeWidth={1}
65
+ />
66
+ {ticks.map(tick => (
67
+ <SvgText
68
+ key={`x-${tick.position}-${tick.label}`}
69
+ x={tick.position}
70
+ y={baseline + fontSize + 4}
71
+ fill={theme.typography.axisLabel.color}
72
+ fontSize={fontSize}
73
+ textAnchor={rotateLabels ? 'end' : 'middle'}
74
+ transform={
75
+ rotateLabels ? `rotate(-45, ${tick.position}, ${baseline + fontSize + 4})` : undefined
76
+ }
77
+ >
78
+ {tick.label}
79
+ </SvgText>
80
+ ))}
81
+ </G>
82
+ );
83
+ }
84
+
85
+ export function YAxis({
86
+ plot,
87
+ ticks,
88
+ theme,
89
+ fontSize,
90
+ }: {
91
+ readonly plot: PlotRect;
92
+ readonly ticks: readonly Tick[];
93
+ readonly theme: ChartTheme;
94
+ readonly fontSize: number;
95
+ }) {
96
+ return (
97
+ <G>
98
+ <Line
99
+ x1={plot.x}
100
+ y1={plot.y}
101
+ x2={plot.x}
102
+ y2={plotBottom(plot)}
103
+ stroke={theme.axis}
104
+ strokeWidth={1}
105
+ />
106
+ {ticks.map(tick => (
107
+ <SvgText
108
+ key={`y-${tick.position}-${tick.label}`}
109
+ x={plot.x - 6}
110
+ y={tick.position + fontSize / 3}
111
+ fill={theme.typography.axisLabel.color}
112
+ fontSize={fontSize}
113
+ textAnchor="end"
114
+ >
115
+ {tick.label}
116
+ </SvgText>
117
+ ))}
118
+ </G>
119
+ );
120
+ }