@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,199 +1,193 @@
1
- /**
2
- * SVG path-string builders.
3
- *
4
- * This is where the "never bridge a gap" guarantee (FR-012) becomes
5
- * structural rather than a rule someone has to remember. A missing value does
6
- * not produce a coordinate — it ends the current subpath. The next known value
7
- * starts a fresh one with an `M` command. There is physically no line segment
8
- * spanning the gap, so no amount of downstream carelessness can draw one.
9
- *
10
- * Batching into path data is also what keeps element counts flat: one `Path`
11
- * per series regardless of point count (research.md R2).
12
- *
13
- * PURE. No React, no SVG imports — this module emits strings.
14
- */
15
-
16
- export interface Pt {
17
- readonly x: number;
18
- readonly y: number;
19
- }
20
-
21
- /** A point that may be absent, mirroring the value contract. */
22
- export type MaybePt = Pt | null;
23
-
24
- function n(value: number): string {
25
- // Two decimals is well below sub-pixel visibility and keeps `d` strings
26
- // short, which matters when they cross the bridge on every data change.
27
- return Number.isFinite(value) ? value.toFixed(2) : '0';
28
- }
29
-
30
- /**
31
- * Polyline through the known points, broken at every gap.
32
- *
33
- * `null` entries are missing values. They terminate the current subpath; they
34
- * are never interpolated, carried forward, or skipped over.
35
- */
36
- export function linePath(points: readonly MaybePt[]): string {
37
- let d = '';
38
- let penDown = false;
39
-
40
- for (const p of points) {
41
- if (p === null) {
42
- // Lift the pen. The gap stays a gap.
43
- penDown = false;
44
- continue;
45
- }
46
- if (!penDown) {
47
- d += `M${n(p.x)} ${n(p.y)}`;
48
- penDown = true;
49
- } else {
50
- d += `L${n(p.x)} ${n(p.y)}`;
51
- }
52
- }
53
-
54
- return d;
55
- }
56
-
57
- /**
58
- * Filled area between the polyline and a baseline, broken at gaps in the same
59
- * way: each contiguous run of known points becomes its own closed shape.
60
- */
61
- export function areaPath(
62
- points: readonly MaybePt[],
63
- baselineY: number,
64
- ): string {
65
- let d = '';
66
- let run: Pt[] = [];
67
-
68
- const flush = () => {
69
- if (run.length === 0) {
70
- return;
71
- }
72
- d += `M${n(run[0].x)} ${n(baselineY)}`;
73
- for (const p of run) {
74
- d += `L${n(p.x)} ${n(p.y)}`;
75
- }
76
- d += `L${n(run[run.length - 1].x)} ${n(baselineY)}Z`;
77
- run = [];
78
- };
79
-
80
- for (const p of points) {
81
- if (p === null) {
82
- flush();
83
- } else {
84
- run.push(p);
85
- }
86
- }
87
- flush();
88
-
89
- return d;
90
- }
91
-
92
- export interface RectSpec {
93
- readonly x: number;
94
- readonly y: number;
95
- readonly width: number;
96
- readonly height: number;
97
- readonly radius?: number;
98
- }
99
-
100
- /** One rectangle as a subpath, with optional corner rounding. */
101
- export function rectSubpath(r: RectSpec): string {
102
- const radius = Math.min(r.radius ?? 0, r.width / 2, Math.abs(r.height) / 2);
103
-
104
- if (radius <= 0) {
105
- return (
106
- `M${n(r.x)} ${n(r.y)}` +
107
- `h${n(r.width)}v${n(r.height)}h${n(-r.width)}Z`
108
- );
109
- }
110
-
111
- const { x, y, width: w, height: h } = r;
112
- return (
113
- `M${n(x + radius)} ${n(y)}` +
114
- `h${n(w - radius * 2)}` +
115
- `a${n(radius)} ${n(radius)} 0 0 1 ${n(radius)} ${n(radius)}` +
116
- `v${n(h - radius * 2)}` +
117
- `a${n(radius)} ${n(radius)} 0 0 1 ${n(-radius)} ${n(radius)}` +
118
- `h${n(-(w - radius * 2))}` +
119
- `a${n(radius)} ${n(radius)} 0 0 1 ${n(-radius)} ${n(-radius)}` +
120
- `v${n(-(h - radius * 2))}` +
121
- `a${n(radius)} ${n(radius)} 0 0 1 ${n(radius)} ${n(-radius)}Z`
122
- );
123
- }
124
-
125
- /**
126
- * Many rectangles batched into one path — how bars and heatmap cells stay at
127
- * one element per series or per intensity bucket.
128
- */
129
- export function rectBatchPath(rects: readonly RectSpec[]): string {
130
- let d = '';
131
- for (const r of rects) {
132
- d += rectSubpath(r);
133
- }
134
- return d;
135
- }
136
-
137
- export interface ArcSpec {
138
- readonly cx: number;
139
- readonly cy: number;
140
- readonly outerRadius: number;
141
- readonly innerRadius: number;
142
- /** Radians, clockwise from 12 o'clock. */
143
- readonly startAngle: number;
144
- readonly endAngle: number;
145
- }
146
-
147
- function polar(cx: number, cy: number, r: number, angle: number): Pt {
148
- // Offset by -90 degrees so 0 radians is 12 o'clock, which is where users
149
- // expect a donut to start.
150
- const a = angle - Math.PI / 2;
151
- return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) };
152
- }
153
-
154
- /**
155
- * Donut or pie segment. An inner radius of 0 yields a pie wedge.
156
- *
157
- * A full-circle segment is drawn as two half-arcs, because SVG cannot express
158
- * a 360-degree arc in a single command — start and end would coincide and
159
- * nothing would be drawn.
160
- */
161
- export function arcPath(spec: ArcSpec): string {
162
- const { cx, cy, outerRadius: ro, innerRadius: ri } = spec;
163
- const sweep = spec.endAngle - spec.startAngle;
164
- const full = Math.abs(sweep) >= Math.PI * 2 - 1e-9;
165
-
166
- if (full) {
167
- const mid = spec.startAngle + Math.PI;
168
- return (
169
- arcPath({ ...spec, endAngle: mid }) +
170
- arcPath({ ...spec, startAngle: mid, endAngle: spec.startAngle + Math.PI * 2 })
171
- );
172
- }
173
-
174
- const largeArc = Math.abs(sweep) > Math.PI ? 1 : 0;
175
- const o0 = polar(cx, cy, ro, spec.startAngle);
176
- const o1 = polar(cx, cy, ro, spec.endAngle);
177
-
178
- if (ri <= 0) {
179
- return (
180
- `M${n(cx)} ${n(cy)}` +
181
- `L${n(o0.x)} ${n(o0.y)}` +
182
- `A${n(ro)} ${n(ro)} 0 ${largeArc} 1 ${n(o1.x)} ${n(o1.y)}Z`
183
- );
184
- }
185
-
186
- const i1 = polar(cx, cy, ri, spec.endAngle);
187
- const i0 = polar(cx, cy, ri, spec.startAngle);
188
- return (
189
- `M${n(o0.x)} ${n(o0.y)}` +
190
- `A${n(ro)} ${n(ro)} 0 ${largeArc} 1 ${n(o1.x)} ${n(o1.y)}` +
191
- `L${n(i1.x)} ${n(i1.y)}` +
192
- `A${n(ri)} ${n(ri)} 0 ${largeArc} 0 ${n(i0.x)} ${n(i0.y)}Z`
193
- );
194
- }
195
-
196
- /** Straight line, for axes, baselines, and threshold markers. */
197
- export function linearSegment(from: Pt, to: Pt): string {
198
- return `M${n(from.x)} ${n(from.y)}L${n(to.x)} ${n(to.y)}`;
199
- }
1
+ /**
2
+ * SVG path-string builders.
3
+ *
4
+ * This is where the "never bridge a gap" guarantee (FR-012) becomes
5
+ * structural rather than a rule someone has to remember. A missing value does
6
+ * not produce a coordinate — it ends the current subpath. The next known value
7
+ * starts a fresh one with an `M` command. There is physically no line segment
8
+ * spanning the gap, so no amount of downstream carelessness can draw one.
9
+ *
10
+ * Batching into path data is also what keeps element counts flat: one `Path`
11
+ * per series regardless of point count (research.md R2).
12
+ *
13
+ * PURE. No React, no SVG imports — this module emits strings.
14
+ */
15
+
16
+ export interface Pt {
17
+ readonly x: number;
18
+ readonly y: number;
19
+ }
20
+
21
+ /** A point that may be absent, mirroring the value contract. */
22
+ export type MaybePt = Pt | null;
23
+
24
+ function n(value: number): string {
25
+ // Two decimals is well below sub-pixel visibility and keeps `d` strings
26
+ // short, which matters when they cross the bridge on every data change.
27
+ return Number.isFinite(value) ? value.toFixed(2) : '0';
28
+ }
29
+
30
+ /**
31
+ * Polyline through the known points, broken at every gap.
32
+ *
33
+ * `null` entries are missing values. They terminate the current subpath; they
34
+ * are never interpolated, carried forward, or skipped over.
35
+ */
36
+ export function linePath(points: readonly MaybePt[]): string {
37
+ let d = '';
38
+ let penDown = false;
39
+
40
+ for (const p of points) {
41
+ if (p === null) {
42
+ // Lift the pen. The gap stays a gap.
43
+ penDown = false;
44
+ continue;
45
+ }
46
+ if (!penDown) {
47
+ d += `M${n(p.x)} ${n(p.y)}`;
48
+ penDown = true;
49
+ } else {
50
+ d += `L${n(p.x)} ${n(p.y)}`;
51
+ }
52
+ }
53
+
54
+ return d;
55
+ }
56
+
57
+ /**
58
+ * Filled area between the polyline and a baseline, broken at gaps in the same
59
+ * way: each contiguous run of known points becomes its own closed shape.
60
+ */
61
+ export function areaPath(points: readonly MaybePt[], baselineY: number): string {
62
+ let d = '';
63
+ let run: Pt[] = [];
64
+
65
+ const flush = () => {
66
+ if (run.length === 0) {
67
+ return;
68
+ }
69
+ d += `M${n(run[0].x)} ${n(baselineY)}`;
70
+ for (const p of run) {
71
+ d += `L${n(p.x)} ${n(p.y)}`;
72
+ }
73
+ d += `L${n(run[run.length - 1].x)} ${n(baselineY)}Z`;
74
+ run = [];
75
+ };
76
+
77
+ for (const p of points) {
78
+ if (p === null) {
79
+ flush();
80
+ } else {
81
+ run.push(p);
82
+ }
83
+ }
84
+ flush();
85
+
86
+ return d;
87
+ }
88
+
89
+ export interface RectSpec {
90
+ readonly x: number;
91
+ readonly y: number;
92
+ readonly width: number;
93
+ readonly height: number;
94
+ readonly radius?: number;
95
+ }
96
+
97
+ /** One rectangle as a subpath, with optional corner rounding. */
98
+ export function rectSubpath(r: RectSpec): string {
99
+ const radius = Math.min(r.radius ?? 0, r.width / 2, Math.abs(r.height) / 2);
100
+
101
+ if (radius <= 0) {
102
+ return `M${n(r.x)} ${n(r.y)}` + `h${n(r.width)}v${n(r.height)}h${n(-r.width)}Z`;
103
+ }
104
+
105
+ const { x, y, width: w, height: h } = r;
106
+ return (
107
+ `M${n(x + radius)} ${n(y)}` +
108
+ `h${n(w - radius * 2)}` +
109
+ `a${n(radius)} ${n(radius)} 0 0 1 ${n(radius)} ${n(radius)}` +
110
+ `v${n(h - radius * 2)}` +
111
+ `a${n(radius)} ${n(radius)} 0 0 1 ${n(-radius)} ${n(radius)}` +
112
+ `h${n(-(w - radius * 2))}` +
113
+ `a${n(radius)} ${n(radius)} 0 0 1 ${n(-radius)} ${n(-radius)}` +
114
+ `v${n(-(h - radius * 2))}` +
115
+ `a${n(radius)} ${n(radius)} 0 0 1 ${n(radius)} ${n(-radius)}Z`
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Many rectangles batched into one path — how bars and heatmap cells stay at
121
+ * one element per series or per intensity bucket.
122
+ */
123
+ export function rectBatchPath(rects: readonly RectSpec[]): string {
124
+ let d = '';
125
+ for (const r of rects) {
126
+ d += rectSubpath(r);
127
+ }
128
+ return d;
129
+ }
130
+
131
+ export interface ArcSpec {
132
+ readonly cx: number;
133
+ readonly cy: number;
134
+ readonly outerRadius: number;
135
+ readonly innerRadius: number;
136
+ /** Radians, clockwise from 12 o'clock. */
137
+ readonly startAngle: number;
138
+ readonly endAngle: number;
139
+ }
140
+
141
+ function polar(cx: number, cy: number, r: number, angle: number): Pt {
142
+ // Offset by -90 degrees so 0 radians is 12 o'clock, which is where users
143
+ // expect a donut to start.
144
+ const a = angle - Math.PI / 2;
145
+ return { x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) };
146
+ }
147
+
148
+ /**
149
+ * Donut or pie segment. An inner radius of 0 yields a pie wedge.
150
+ *
151
+ * A full-circle segment is drawn as two half-arcs, because SVG cannot express
152
+ * a 360-degree arc in a single command — start and end would coincide and
153
+ * nothing would be drawn.
154
+ */
155
+ export function arcPath(spec: ArcSpec): string {
156
+ const { cx, cy, outerRadius: ro, innerRadius: ri } = spec;
157
+ const sweep = spec.endAngle - spec.startAngle;
158
+ const full = Math.abs(sweep) >= Math.PI * 2 - 1e-9;
159
+
160
+ if (full) {
161
+ const mid = spec.startAngle + Math.PI;
162
+ return (
163
+ arcPath({ ...spec, endAngle: mid }) +
164
+ arcPath({ ...spec, startAngle: mid, endAngle: spec.startAngle + Math.PI * 2 })
165
+ );
166
+ }
167
+
168
+ const largeArc = Math.abs(sweep) > Math.PI ? 1 : 0;
169
+ const o0 = polar(cx, cy, ro, spec.startAngle);
170
+ const o1 = polar(cx, cy, ro, spec.endAngle);
171
+
172
+ if (ri <= 0) {
173
+ return (
174
+ `M${n(cx)} ${n(cy)}` +
175
+ `L${n(o0.x)} ${n(o0.y)}` +
176
+ `A${n(ro)} ${n(ro)} 0 ${largeArc} 1 ${n(o1.x)} ${n(o1.y)}Z`
177
+ );
178
+ }
179
+
180
+ const i1 = polar(cx, cy, ri, spec.endAngle);
181
+ const i0 = polar(cx, cy, ri, spec.startAngle);
182
+ return (
183
+ `M${n(o0.x)} ${n(o0.y)}` +
184
+ `A${n(ro)} ${n(ro)} 0 ${largeArc} 1 ${n(o1.x)} ${n(o1.y)}` +
185
+ `L${n(i1.x)} ${n(i1.y)}` +
186
+ `A${n(ri)} ${n(ri)} 0 ${largeArc} 0 ${n(i0.x)} ${n(i0.y)}Z`
187
+ );
188
+ }
189
+
190
+ /** Straight line, for axes, baselines, and threshold markers. */
191
+ export function linearSegment(from: Pt, to: Pt): string {
192
+ return `M${n(from.x)} ${n(from.y)}L${n(to.x)} ${n(to.y)}`;
193
+ }