@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,203 +1,197 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Distribution geometry: proportions between groups.
|
|
3
|
-
*
|
|
4
|
-
* Segments that do not sum to the whole are **not** rescaled to force a full
|
|
5
|
-
* circle (FR-024). The arc stops short and the geometry reports `incomplete`,
|
|
6
|
-
* so the chart can say the data is partial rather than quietly implying it is
|
|
7
|
-
* complete.
|
|
8
|
-
*
|
|
9
|
-
* PURE. No React, no SVG.
|
|
10
|
-
*/
|
|
11
|
-
import type { ChartDatasource } from '../contract/datasource';
|
|
12
|
-
import { segmentKey } from '../contract/theme';
|
|
13
|
-
import { isKnown, isWithheld, sumKnown } from '../contract/value';
|
|
14
|
-
import type { HitRegion } from './hitTest';
|
|
15
|
-
import { arcPath, rectSubpath } from './path';
|
|
16
|
-
import type {
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
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
|
-
xTicks: [],
|
|
199
|
-
yTicks: [],
|
|
200
|
-
hitRegions,
|
|
201
|
-
incomplete,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Distribution geometry: proportions between groups.
|
|
3
|
+
*
|
|
4
|
+
* Segments that do not sum to the whole are **not** rescaled to force a full
|
|
5
|
+
* circle (FR-024). The arc stops short and the geometry reports `incomplete`,
|
|
6
|
+
* so the chart can say the data is partial rather than quietly implying it is
|
|
7
|
+
* complete.
|
|
8
|
+
*
|
|
9
|
+
* PURE. No React, no SVG.
|
|
10
|
+
*/
|
|
11
|
+
import type { ChartDatasource } from '../contract/datasource';
|
|
12
|
+
import { segmentKey } from '../contract/theme';
|
|
13
|
+
import { isKnown, isWithheld, sumKnown } from '../contract/value';
|
|
14
|
+
import type { HitRegion } from './hitTest';
|
|
15
|
+
import { arcPath, rectSubpath } from './path';
|
|
16
|
+
import type { GeometryInput, GeometryLabel, MarkPath, ResolvedGeometry } from './types';
|
|
17
|
+
|
|
18
|
+
export type DistributionShape = 'donut' | 'pie' | 'segmentedBar';
|
|
19
|
+
|
|
20
|
+
export interface DistributionOptions {
|
|
21
|
+
readonly shape?: DistributionShape;
|
|
22
|
+
/** Donut hole as a fraction of the outer radius. */
|
|
23
|
+
readonly innerRadiusRatio?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const TAU = Math.PI * 2;
|
|
27
|
+
|
|
28
|
+
export function buildDistributionGeometry<M>(
|
|
29
|
+
datasource: ChartDatasource<M>,
|
|
30
|
+
input: GeometryInput,
|
|
31
|
+
options: DistributionOptions = {},
|
|
32
|
+
): ResolvedGeometry {
|
|
33
|
+
const { plot } = input;
|
|
34
|
+
const shape = options.shape ?? 'donut';
|
|
35
|
+
|
|
36
|
+
const segments = datasource.series.flatMap(series =>
|
|
37
|
+
series.points.map(point => ({ series, point })),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (segments.length === 0 || plot.width <= 0 || plot.height <= 0) {
|
|
41
|
+
return {
|
|
42
|
+
plot,
|
|
43
|
+
paths: [],
|
|
44
|
+
markers: [],
|
|
45
|
+
labels: [],
|
|
46
|
+
xTicks: [],
|
|
47
|
+
yTicks: [],
|
|
48
|
+
hitRegions: [],
|
|
49
|
+
incomplete: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const total = sumKnown(segments.map(s => s.point.value));
|
|
54
|
+
// Nothing known: there is no distribution to draw, and drawing a full grey
|
|
55
|
+
// circle would imply a shape the data does not support.
|
|
56
|
+
if (total.countedPoints === 0 || total.total <= 0) {
|
|
57
|
+
return {
|
|
58
|
+
plot,
|
|
59
|
+
paths: [],
|
|
60
|
+
markers: [],
|
|
61
|
+
labels: [],
|
|
62
|
+
xTicks: [],
|
|
63
|
+
yTicks: [],
|
|
64
|
+
hitRegions: [],
|
|
65
|
+
incomplete: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Proportions supplied directly (each in 0..1, summing to ~1) are honoured
|
|
70
|
+
// as given; absolute values are normalised by their own total. Either way we
|
|
71
|
+
// never stretch a short sum to fill the circle.
|
|
72
|
+
const suppliedAsProportions = segments.every(
|
|
73
|
+
s => !isKnown(s.point.value) || (s.point.value.value >= 0 && s.point.value.value <= 1),
|
|
74
|
+
);
|
|
75
|
+
const denominator = suppliedAsProportions ? 1 : total.total;
|
|
76
|
+
const coverage = total.total / denominator;
|
|
77
|
+
const incomplete = total.incomplete || coverage < 0.999;
|
|
78
|
+
|
|
79
|
+
const paths: MarkPath[] = [];
|
|
80
|
+
const labels: GeometryLabel[] = [];
|
|
81
|
+
const hitRegions: HitRegion[] = [];
|
|
82
|
+
const colorOf = (seriesId: string, pointId: string) =>
|
|
83
|
+
input.segmentColors?.[segmentKey(seriesId, pointId)] ?? input.seriesColors[seriesId];
|
|
84
|
+
|
|
85
|
+
if (shape === 'segmentedBar') {
|
|
86
|
+
const barHeight = Math.min(plot.height, 28);
|
|
87
|
+
const barY = plot.y + (plot.height - barHeight) / 2;
|
|
88
|
+
let cursor = plot.x;
|
|
89
|
+
|
|
90
|
+
for (const { series, point } of segments) {
|
|
91
|
+
if (!isKnown(point.value)) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const fraction = point.value.value / denominator;
|
|
95
|
+
const width = plot.width * fraction;
|
|
96
|
+
|
|
97
|
+
paths.push({
|
|
98
|
+
key: `seg-${series.id}-${point.id}`,
|
|
99
|
+
seriesId: series.id,
|
|
100
|
+
d: rectSubpath({ x: cursor, y: barY, width, height: barHeight }),
|
|
101
|
+
color: colorOf(series.id, point.id),
|
|
102
|
+
pattern: 'solid',
|
|
103
|
+
filled: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
hitRegions.push({
|
|
107
|
+
seriesId: series.id,
|
|
108
|
+
pointId: point.id,
|
|
109
|
+
x: cursor + width / 2,
|
|
110
|
+
y: barY + barHeight / 2,
|
|
111
|
+
bounds: { x: cursor, y: barY, width, height: barHeight },
|
|
112
|
+
selectable: !isWithheld(point.value),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
cursor += width;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
plot,
|
|
120
|
+
paths,
|
|
121
|
+
markers: [],
|
|
122
|
+
labels,
|
|
123
|
+
xTicks: [],
|
|
124
|
+
yTicks: [],
|
|
125
|
+
hitRegions,
|
|
126
|
+
incomplete,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const cx = plot.x + plot.width / 2;
|
|
131
|
+
const cy = plot.y + plot.height / 2;
|
|
132
|
+
const outerRadius = Math.min(plot.width, plot.height) / 2;
|
|
133
|
+
const innerRadius = shape === 'pie' ? 0 : outerRadius * (options.innerRadiusRatio ?? 0.6);
|
|
134
|
+
|
|
135
|
+
let angle = 0;
|
|
136
|
+
|
|
137
|
+
for (const { series, point } of segments) {
|
|
138
|
+
if (!isKnown(point.value)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const fraction = point.value.value / denominator;
|
|
143
|
+
const sweep = fraction * TAU;
|
|
144
|
+
const start = angle;
|
|
145
|
+
const end = angle + sweep;
|
|
146
|
+
angle = end;
|
|
147
|
+
|
|
148
|
+
paths.push({
|
|
149
|
+
key: `arc-${series.id}-${point.id}`,
|
|
150
|
+
seriesId: series.id,
|
|
151
|
+
d: arcPath({
|
|
152
|
+
cx,
|
|
153
|
+
cy,
|
|
154
|
+
outerRadius,
|
|
155
|
+
innerRadius,
|
|
156
|
+
startAngle: start,
|
|
157
|
+
endAngle: end,
|
|
158
|
+
}),
|
|
159
|
+
color: colorOf(series.id, point.id),
|
|
160
|
+
pattern: 'solid',
|
|
161
|
+
filled: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Label at the segment's midpoint, on the ring between the two radii.
|
|
165
|
+
const mid = start + sweep / 2 - Math.PI / 2;
|
|
166
|
+
const labelRadius = (outerRadius + innerRadius) / 2;
|
|
167
|
+
|
|
168
|
+
labels.push({
|
|
169
|
+
key: `arc-label-${series.id}-${point.id}`,
|
|
170
|
+
text: `${Math.round(fraction * 100)}%`,
|
|
171
|
+
x: cx + labelRadius * Math.cos(mid),
|
|
172
|
+
y: cy + labelRadius * Math.sin(mid),
|
|
173
|
+
anchor: 'middle',
|
|
174
|
+
// Hide labels on slivers where the text would not fit anyway.
|
|
175
|
+
muted: fraction < 0.05,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
hitRegions.push({
|
|
179
|
+
seriesId: series.id,
|
|
180
|
+
pointId: point.id,
|
|
181
|
+
x: cx + labelRadius * Math.cos(mid),
|
|
182
|
+
y: cy + labelRadius * Math.sin(mid),
|
|
183
|
+
selectable: !isWithheld(point.value),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
plot,
|
|
189
|
+
paths,
|
|
190
|
+
markers: [],
|
|
191
|
+
labels,
|
|
192
|
+
xTicks: [],
|
|
193
|
+
yTicks: [],
|
|
194
|
+
hitRegions,
|
|
195
|
+
incomplete,
|
|
196
|
+
};
|
|
197
|
+
}
|