@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.
- package/CHANGELOG.md +126 -74
- package/README.md +337 -314
- package/package.json +46 -49
- package/skills/sdk-rn-chart/SKILL.md +1 -2
- package/src/a11y/summary.ts +170 -177
- package/src/contract/datasource.ts +126 -134
- package/src/contract/errors.ts +129 -133
- package/src/contract/format.ts +162 -168
- package/src/contract/interaction.ts +70 -71
- package/src/contract/marker.ts +75 -75
- package/src/contract/props.ts +57 -60
- package/src/contract/state.ts +88 -94
- package/src/contract/theme.ts +169 -139
- package/src/contract/value.ts +121 -125
- package/src/geometry/bar.ts +271 -256
- package/src/geometry/distribution.ts +197 -200
- package/src/geometry/heatmap.ts +189 -195
- package/src/geometry/hitTest.ts +117 -120
- package/src/geometry/layout.ts +162 -121
- package/src/geometry/markers.ts +55 -0
- package/src/geometry/path.ts +193 -199
- package/src/geometry/progress.ts +223 -204
- package/src/geometry/scale.ts +140 -146
- package/src/geometry/ticks.ts +145 -153
- package/src/geometry/timeSeries.ts +168 -158
- package/src/geometry/types.ts +94 -88
- package/src/index.ts +158 -176
- package/src/react/ChartBoundary.tsx +96 -96
- package/src/react/ChartFrame.tsx +583 -620
- package/src/react/ChartLegend.tsx +211 -177
- 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 +103 -110
- package/src/validation/validateDatasource.ts +174 -174
package/src/geometry/path.ts
CHANGED
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
readonly
|
|
94
|
-
readonly
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`
|
|
114
|
-
`
|
|
115
|
-
`a${n(radius)} ${n(radius)} 0 0 1 ${n(radius)} ${n(radius)}`
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
readonly
|
|
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
|
-
/** 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
|
+
}
|