@chipmobilesdk/rn-chart 0.1.1 → 0.2.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +126 -74
  2. package/README.md +337 -314
  3. package/package.json +46 -49
  4. package/skills/sdk-rn-chart/SKILL.md +1 -2
  5. package/src/a11y/summary.ts +170 -177
  6. package/src/contract/datasource.ts +126 -134
  7. package/src/contract/errors.ts +129 -133
  8. package/src/contract/format.ts +162 -168
  9. package/src/contract/interaction.ts +70 -71
  10. package/src/contract/marker.ts +75 -75
  11. package/src/contract/props.ts +57 -60
  12. package/src/contract/state.ts +88 -94
  13. package/src/contract/theme.ts +169 -139
  14. package/src/contract/value.ts +121 -125
  15. package/src/geometry/bar.ts +271 -256
  16. package/src/geometry/distribution.ts +197 -200
  17. package/src/geometry/heatmap.ts +189 -195
  18. package/src/geometry/hitTest.ts +117 -120
  19. package/src/geometry/layout.ts +162 -121
  20. package/src/geometry/markers.ts +55 -0
  21. package/src/geometry/path.ts +193 -199
  22. package/src/geometry/progress.ts +223 -204
  23. package/src/geometry/scale.ts +140 -146
  24. package/src/geometry/ticks.ts +145 -153
  25. package/src/geometry/timeSeries.ts +168 -158
  26. package/src/geometry/types.ts +94 -88
  27. package/src/index.ts +158 -176
  28. package/src/react/ChartBoundary.tsx +96 -96
  29. package/src/react/ChartFrame.tsx +583 -620
  30. package/src/react/ChartLegend.tsx +211 -177
  31. package/src/react/ChartThemeProvider.tsx +44 -48
  32. package/src/react/charts.tsx +154 -161
  33. package/src/react/useChartLayout.ts +124 -124
  34. package/src/react/useChartSelection.ts +152 -170
  35. package/src/render/Axis.tsx +120 -122
  36. package/src/render/Marks.tsx +173 -205
  37. package/src/validation/budgets.ts +103 -110
  38. 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
+ }