@chipmobilesdk/rn-chart 0.2.1 → 0.4.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 +56 -0
- package/README.md +54 -2
- package/package.json +5 -2
- package/skills/sdk-rn-chart/SKILL.md +49 -14
- package/src/a11y/summary.ts +85 -46
- package/src/contract/distribution.ts +44 -0
- package/src/contract/errors.ts +6 -0
- package/src/contract/format.ts +6 -0
- package/src/contract/interaction.ts +2 -2
- package/src/contract/props.ts +7 -0
- package/src/contract/strings.ts +50 -0
- package/src/geometry/bar.ts +27 -16
- package/src/geometry/distribution.ts +11 -6
- package/src/geometry/heatmap.ts +34 -17
- package/src/geometry/hitTest.ts +35 -0
- package/src/geometry/progress.ts +0 -1
- package/src/geometry/ticks.ts +0 -13
- package/src/geometry/timeSeries.ts +7 -2
- package/src/geometry/types.ts +11 -0
- package/src/index.ts +8 -1
- package/src/react/ChartBoundary.tsx +2 -2
- package/src/react/ChartFrame.tsx +128 -362
- package/src/react/ChartLegend.tsx +65 -3
- package/src/react/ChartPlot.tsx +57 -0
- package/src/react/ChartStateOverlay.tsx +94 -0
- package/src/react/charts.tsx +27 -12
- package/src/react/pointEvent.ts +40 -0
- package/src/react/useChartA11y.ts +113 -0
- package/src/react/useChartFormatters.ts +105 -0
- package/src/react/useChartLayout.ts +5 -2
- package/src/react/useChartSelection.ts +77 -40
- package/src/react/useChartStatus.ts +85 -0
- package/src/render/Axis.tsx +26 -22
- package/src/render/Marks.tsx +2 -9
- package/src/validation/budgets.ts +16 -4
- package/src/validation/validateDatasource.ts +38 -14
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every text the package itself speaks or shows (CHT-05): screen-reader
|
|
3
|
+
* summaries and announcements, adjustable-action labels, built-in state text and
|
|
4
|
+
* legend hints. English defaults; an app translates them by passing `strings`
|
|
5
|
+
* (or by registering `chartStrings.user` with rn-i18n and passing the translated
|
|
6
|
+
* texts). `{name}` marks a variable.
|
|
7
|
+
*/
|
|
8
|
+
import { defineMessages } from '@chipmobilesdk/rn-core';
|
|
9
|
+
|
|
10
|
+
export const chartStrings = defineMessages({
|
|
11
|
+
user: {
|
|
12
|
+
'chart.family.progress': 'progress chart',
|
|
13
|
+
'chart.family.distribution': 'distribution chart',
|
|
14
|
+
'chart.family.timeSeries': 'time series chart',
|
|
15
|
+
'chart.family.bar': 'bar chart',
|
|
16
|
+
'chart.family.heatmap': 'heatmap',
|
|
17
|
+
'chart.status.loading': 'Loading data.',
|
|
18
|
+
'chart.status.empty': 'No data yet.',
|
|
19
|
+
'chart.status.error': 'Data could not be shown.',
|
|
20
|
+
'chart.status.stale': 'Showing data that may be out of date.',
|
|
21
|
+
'chart.status.noPermission': 'This data is not available to you.',
|
|
22
|
+
'chart.status.partial': 'Some data is missing.',
|
|
23
|
+
'chart.summary.heading': '{title}, {noun}.',
|
|
24
|
+
'chart.summary.headingUntitled': '{noun}.',
|
|
25
|
+
'chart.summary.scale': '{series} series, {points} points.',
|
|
26
|
+
'chart.summary.scaleSingle': '{points} points.',
|
|
27
|
+
'chart.summary.single': 'Value {value}.',
|
|
28
|
+
'chart.summary.extremes': 'Highest {maxLabel} at {max}. Lowest {minLabel} at {min}.',
|
|
29
|
+
'chart.summary.marker': '{name} {value}',
|
|
30
|
+
'chart.summary.incomplete': 'Some values are missing, so totals are incomplete.',
|
|
31
|
+
'chart.point.withheld': '{position}, {series}, not available.',
|
|
32
|
+
'chart.point.noData': '{position}, {series}, no data.',
|
|
33
|
+
'chart.point.value': '{position}, {series}, {value}.',
|
|
34
|
+
'chart.total': 'Total {value}.',
|
|
35
|
+
'chart.total.incomplete': 'Total {value}, excluding {count} values that are not available.',
|
|
36
|
+
'chart.action.next': 'Next data point',
|
|
37
|
+
'chart.action.previous': 'Previous data point',
|
|
38
|
+
'chart.state.loading': 'Loading…',
|
|
39
|
+
'chart.state.empty': 'No data yet',
|
|
40
|
+
'chart.state.error': 'Unable to show this chart',
|
|
41
|
+
'chart.state.noPermission': 'Not available',
|
|
42
|
+
'chart.legend.hide': 'Hides this series',
|
|
43
|
+
'chart.legend.show': 'Shows this series',
|
|
44
|
+
'chart.legend.scale': 'Colour scale from {min} to {max}',
|
|
45
|
+
'chart.note.incomplete': 'Some values are unavailable, so totals are incomplete.',
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type ChartStrings = typeof chartStrings.defaults;
|
|
50
|
+
export type ChartStringKey = keyof ChartStrings;
|
package/src/geometry/bar.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { insetForAxes, plotBottom, plotRight } from './layout';
|
|
|
14
14
|
import { horizontalMarkers, markerDomainValues } from './markers';
|
|
15
15
|
import { rectBatchPath, type RectSpec } from './path';
|
|
16
16
|
import { bandScale, domainFrom, linearScale, widenDegenerateDomain } from './scale';
|
|
17
|
-
import { thinTicks, type Tick } from './ticks';
|
|
17
|
+
import { niceTickValues, thinTicks, type Tick } from './ticks';
|
|
18
18
|
import type { GeometryInput, MarkPath, ResolvedGeometry } from './types';
|
|
19
19
|
|
|
20
20
|
export type BarGrouping = 'grouped' | 'stacked';
|
|
@@ -75,21 +75,24 @@ function stackedDomainValues<M>(
|
|
|
75
75
|
): number[] {
|
|
76
76
|
const values: number[] = [];
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
negative += point.value.value;
|
|
89
|
-
}
|
|
78
|
+
// One pass over the points instead of a search per category and series.
|
|
79
|
+
const positive = new Map<string, number>();
|
|
80
|
+
const negative = new Map<string, number>();
|
|
81
|
+
for (const series of visible) {
|
|
82
|
+
const counted = new Set<string>();
|
|
83
|
+
for (const point of series.points) {
|
|
84
|
+
const category = categoryKey(point.position);
|
|
85
|
+
// The first point of a series in a category is the one drawn there.
|
|
86
|
+
if (counted.has(category) || !isKnown(point.value)) {
|
|
87
|
+
continue;
|
|
90
88
|
}
|
|
89
|
+
counted.add(category);
|
|
90
|
+
const sums = point.value.value >= 0 ? positive : negative;
|
|
91
|
+
sums.set(category, (sums.get(category) ?? 0) + point.value.value);
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
}
|
|
94
|
+
for (const category of categories) {
|
|
95
|
+
values.push(positive.get(category) ?? 0, negative.get(category) ?? 0);
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
return values;
|
|
@@ -149,6 +152,13 @@ export function buildBarGeometry<M>(
|
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
154
|
|
|
155
|
+
// Instants have an order of their own. First-seen order put Tuesday before
|
|
156
|
+
// Monday when one series started a day late, and disagreed with the
|
|
157
|
+
// time-series chart drawn from the same datasource (CHT-20).
|
|
158
|
+
if (datasource.positionKind === 'instant') {
|
|
159
|
+
categories.sort((a, b) => Number(a) - Number(b));
|
|
160
|
+
}
|
|
161
|
+
|
|
152
162
|
const visible = datasource.series.filter(s => !s.hidden);
|
|
153
163
|
const excluded = countExcluded(datasource);
|
|
154
164
|
const values = [
|
|
@@ -168,7 +178,8 @@ export function buildBarGeometry<M>(
|
|
|
168
178
|
min: values.some(v => v < 0) ? padded.min : Math.max(padded.min, 0),
|
|
169
179
|
max: values.some(v => v > 0) ? padded.max : Math.min(padded.max, 0),
|
|
170
180
|
});
|
|
171
|
-
|
|
181
|
+
// Round steps a reader can count in, not the padded extremes (105.3).
|
|
182
|
+
const yTickValues = niceTickValues(domain, 4);
|
|
172
183
|
const xLabel = (c: string) =>
|
|
173
184
|
datasource.positionKind === 'instant'
|
|
174
185
|
? input.formatInstant(Number(c))
|
|
@@ -219,7 +230,7 @@ export function buildBarGeometry<M>(
|
|
|
219
230
|
y: top,
|
|
220
231
|
width: barWidth,
|
|
221
232
|
height,
|
|
222
|
-
radius: options.radius,
|
|
233
|
+
radius: options.radius ?? input.radius?.bar,
|
|
223
234
|
});
|
|
224
235
|
|
|
225
236
|
hitRegions.push({
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* PURE. No React, no SVG.
|
|
10
10
|
*/
|
|
11
11
|
import type { ChartDatasource } from '../contract/datasource';
|
|
12
|
+
import { resolveDistributionValues, type DistributionValues } from '../contract/distribution';
|
|
12
13
|
import { segmentKey } from '../contract/theme';
|
|
13
14
|
import { isKnown, isWithheld, sumKnown } from '../contract/value';
|
|
14
15
|
import type { HitRegion } from './hitTest';
|
|
@@ -21,6 +22,8 @@ export interface DistributionOptions {
|
|
|
21
22
|
readonly shape?: DistributionShape;
|
|
22
23
|
/** Donut hole as a fraction of the outer radius. */
|
|
23
24
|
readonly innerRadiusRatio?: number;
|
|
25
|
+
/** What the values mean; inferred from the data when omitted. */
|
|
26
|
+
readonly valuesAre?: DistributionValues;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
const TAU = Math.PI * 2;
|
|
@@ -69,10 +72,11 @@ export function buildDistributionGeometry<M>(
|
|
|
69
72
|
// Proportions supplied directly (each in 0..1, summing to ~1) are honoured
|
|
70
73
|
// as given; absolute values are normalised by their own total. Either way we
|
|
71
74
|
// never stretch a short sum to fill the circle.
|
|
72
|
-
const suppliedAsProportions =
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
const suppliedAsProportions =
|
|
76
|
+
resolveDistributionValues(datasource, options.valuesAre) === 'proportions';
|
|
77
|
+
// Shares declared as proportions that add up past the whole are normalised
|
|
78
|
+
// rather than drawn over one another.
|
|
79
|
+
const denominator = suppliedAsProportions ? Math.max(1, total.total) : total.total;
|
|
76
80
|
const coverage = total.total / denominator;
|
|
77
81
|
const incomplete = total.incomplete || coverage < 0.999;
|
|
78
82
|
|
|
@@ -88,7 +92,7 @@ export function buildDistributionGeometry<M>(
|
|
|
88
92
|
let cursor = plot.x;
|
|
89
93
|
|
|
90
94
|
for (const { series, point } of segments) {
|
|
91
|
-
if (!isKnown(point.value)) {
|
|
95
|
+
if (!isKnown(point.value) || point.value.value < 0) {
|
|
92
96
|
continue;
|
|
93
97
|
}
|
|
94
98
|
const fraction = point.value.value / denominator;
|
|
@@ -135,7 +139,7 @@ export function buildDistributionGeometry<M>(
|
|
|
135
139
|
let angle = 0;
|
|
136
140
|
|
|
137
141
|
for (const { series, point } of segments) {
|
|
138
|
-
if (!isKnown(point.value)) {
|
|
142
|
+
if (!isKnown(point.value) || point.value.value < 0) {
|
|
139
143
|
continue;
|
|
140
144
|
}
|
|
141
145
|
|
|
@@ -180,6 +184,7 @@ export function buildDistributionGeometry<M>(
|
|
|
180
184
|
pointId: point.id,
|
|
181
185
|
x: cx + labelRadius * Math.cos(mid),
|
|
182
186
|
y: cy + labelRadius * Math.sin(mid),
|
|
187
|
+
arc: { cx, cy, innerRadius, outerRadius, startAngle: start, endAngle: end },
|
|
183
188
|
selectable: !isWithheld(point.value),
|
|
184
189
|
});
|
|
185
190
|
}
|
package/src/geometry/heatmap.ts
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
import type { ChartDatasource } from '../contract/datasource';
|
|
16
16
|
import { isKnown, isWithheld } from '../contract/value';
|
|
17
17
|
import type { HitRegion } from './hitTest';
|
|
18
|
+
import { insetForAxes } from './layout';
|
|
18
19
|
import { rectBatchPath, type RectSpec } from './path';
|
|
20
|
+
import { thinTicks } from './ticks';
|
|
19
21
|
import type { GeometryInput, MarkPath, ResolvedGeometry } from './types';
|
|
20
22
|
|
|
21
23
|
export interface HeatmapOptions {
|
|
@@ -30,8 +32,6 @@ export function buildHeatmapGeometry<M>(
|
|
|
30
32
|
input: GeometryInput,
|
|
31
33
|
options: HeatmapOptions = {},
|
|
32
34
|
): ResolvedGeometry {
|
|
33
|
-
const { plot } = input;
|
|
34
|
-
|
|
35
35
|
const rows: string[] = [];
|
|
36
36
|
const columns: string[] = [];
|
|
37
37
|
const rowSeen = new Set<string>();
|
|
@@ -53,9 +53,9 @@ export function buildHeatmapGeometry<M>(
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
if (rows.length === 0 || columns.length === 0 || plot.width <= 0) {
|
|
56
|
+
if (rows.length === 0 || columns.length === 0 || input.plot.width <= 0) {
|
|
57
57
|
return {
|
|
58
|
-
plot,
|
|
58
|
+
plot: input.plot,
|
|
59
59
|
paths: [],
|
|
60
60
|
markers: [],
|
|
61
61
|
labels: [],
|
|
@@ -66,6 +66,18 @@ export function buildHeatmapGeometry<M>(
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Row and column names are the heatmap's axes. The plot makes room for
|
|
70
|
+
// them; they were computed before and never drawn (CHT-15).
|
|
71
|
+
const columnLabels = columns.map(input.formatCategory);
|
|
72
|
+
const rowLabels = rows.map(input.formatCategory);
|
|
73
|
+
const plot = insetForAxes(input.plot, {
|
|
74
|
+
xLabels: columnLabels,
|
|
75
|
+
yLabels: rowLabels,
|
|
76
|
+
fontSize: input.fontSize,
|
|
77
|
+
});
|
|
78
|
+
const columnIndex = new Map(columns.map((column, index) => [column, index]));
|
|
79
|
+
const rowIndex = new Map(rows.map((row, index) => [row, index]));
|
|
80
|
+
|
|
69
81
|
const gap = options.cellGap ?? 2;
|
|
70
82
|
const cellWidth = plot.width / columns.length;
|
|
71
83
|
const cellHeight = plot.height / rows.length;
|
|
@@ -102,18 +114,18 @@ export function buildHeatmapGeometry<M>(
|
|
|
102
114
|
continue;
|
|
103
115
|
}
|
|
104
116
|
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
if (
|
|
117
|
+
const col = columnIndex.get(point.position.column);
|
|
118
|
+
const row = rowIndex.get(point.position.row);
|
|
119
|
+
if (col === undefined || row === undefined) {
|
|
108
120
|
continue;
|
|
109
121
|
}
|
|
110
122
|
|
|
111
123
|
const rect: RectSpec = {
|
|
112
|
-
x: plot.x +
|
|
113
|
-
y: plot.y +
|
|
124
|
+
x: plot.x + col * cellWidth + gap / 2,
|
|
125
|
+
y: plot.y + row * cellHeight + gap / 2,
|
|
114
126
|
width: Math.max(0, cellWidth - gap),
|
|
115
127
|
height: Math.max(0, cellHeight - gap),
|
|
116
|
-
radius: options.cellRadius,
|
|
128
|
+
radius: options.cellRadius ?? input.radius?.cell,
|
|
117
129
|
};
|
|
118
130
|
|
|
119
131
|
if (isKnown(point.value)) {
|
|
@@ -173,17 +185,22 @@ export function buildHeatmapGeometry<M>(
|
|
|
173
185
|
paths,
|
|
174
186
|
markers: [],
|
|
175
187
|
labels: [],
|
|
176
|
-
xTicks:
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
xTicks: thinTicks(
|
|
189
|
+
columnLabels.map((label, i) => ({
|
|
190
|
+
value: i,
|
|
191
|
+
position: plot.x + i * cellWidth + cellWidth / 2,
|
|
192
|
+
label,
|
|
193
|
+
})),
|
|
194
|
+
input.fontSize,
|
|
195
|
+
plot.width,
|
|
196
|
+
),
|
|
197
|
+
yTicks: rowLabels.map((label, i) => ({
|
|
182
198
|
value: i,
|
|
183
199
|
position: plot.y + i * cellHeight + cellHeight / 2,
|
|
184
|
-
label
|
|
200
|
+
label,
|
|
185
201
|
})),
|
|
186
202
|
hitRegions,
|
|
203
|
+
...(hasKnown ? { colorScale: { min, max, colors: ramp.slice(0, bucketCount) } } : {}),
|
|
187
204
|
incomplete: excluded > 0,
|
|
188
205
|
};
|
|
189
206
|
}
|
package/src/geometry/hitTest.ts
CHANGED
|
@@ -23,6 +23,19 @@ export interface HitRegion {
|
|
|
23
23
|
readonly width: number;
|
|
24
24
|
readonly height: number;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* An arc segment: containment by radius and angle. Angles are radians,
|
|
28
|
+
* clockwise from twelve o'clock, as the distribution geometry draws them.
|
|
29
|
+
* A region with an arc is hit only inside it, never by proximity.
|
|
30
|
+
*/
|
|
31
|
+
readonly arc?: {
|
|
32
|
+
readonly cx: number;
|
|
33
|
+
readonly cy: number;
|
|
34
|
+
readonly innerRadius: number;
|
|
35
|
+
readonly outerRadius: number;
|
|
36
|
+
readonly startAngle: number;
|
|
37
|
+
readonly endAngle: number;
|
|
38
|
+
};
|
|
26
39
|
/**
|
|
27
40
|
* False for points that must never be selectable: withheld values, and
|
|
28
41
|
* anything the chart is not currently drawing (FR-053).
|
|
@@ -47,6 +60,20 @@ function inBounds(region: HitRegion, x: number, y: number): boolean {
|
|
|
47
60
|
return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
|
|
48
61
|
}
|
|
49
62
|
|
|
63
|
+
const TAU = Math.PI * 2;
|
|
64
|
+
|
|
65
|
+
function inArc(arc: NonNullable<HitRegion['arc']>, x: number, y: number): boolean {
|
|
66
|
+
const dx = x - arc.cx;
|
|
67
|
+
const dy = y - arc.cy;
|
|
68
|
+
const radius = Math.sqrt(dx * dx + dy * dy);
|
|
69
|
+
if (radius < arc.innerRadius || radius > arc.outerRadius) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Clockwise from the top, the convention the arcs are drawn in.
|
|
73
|
+
const angle = (Math.atan2(dy, dx) + Math.PI / 2 + TAU) % TAU;
|
|
74
|
+
return angle >= arc.startAngle && angle < arc.endAngle;
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
/**
|
|
51
78
|
* Resolves a touch to a mark.
|
|
52
79
|
*
|
|
@@ -70,6 +97,14 @@ export function hitTest(
|
|
|
70
97
|
if (inBounds(region, x, y)) {
|
|
71
98
|
return { seriesId: region.seriesId, pointId: region.pointId, distance: 0 };
|
|
72
99
|
}
|
|
100
|
+
if (region.arc) {
|
|
101
|
+
// A large segment's edge is far from its midpoint, and a neighbour's
|
|
102
|
+
// midpoint can be nearer: only the segment under the finger counts.
|
|
103
|
+
if (inArc(region.arc, x, y)) {
|
|
104
|
+
return { seriesId: region.seriesId, pointId: region.pointId, distance: 0 };
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
73
108
|
|
|
74
109
|
const dx = region.x - x;
|
|
75
110
|
const dy = region.y - y;
|
package/src/geometry/progress.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { rectSubpath } from './path';
|
|
|
17
17
|
import type { GeometryInput, GeometryLabel, MarkerLine, MarkPath, ResolvedGeometry } from './types';
|
|
18
18
|
|
|
19
19
|
export interface ProgressOptions {
|
|
20
|
-
readonly orientation?: 'horizontal' | 'vertical';
|
|
21
20
|
readonly trackRadius?: number;
|
|
22
21
|
}
|
|
23
22
|
|
package/src/geometry/ticks.ts
CHANGED
|
@@ -130,16 +130,3 @@ export function reserveAxisSpace(
|
|
|
130
130
|
}
|
|
131
131
|
return widest + LABEL_GUTTER;
|
|
132
132
|
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Shortens a label that cannot fit, signalling the truncation with an ellipsis.
|
|
136
|
-
* Callers must expose the full value through an accessible label (FR-070).
|
|
137
|
-
*/
|
|
138
|
-
export function truncateToWidth(text: string, fontSize: number, maxWidthPx: number): string {
|
|
139
|
-
if (estimateTextWidth(text, fontSize) <= maxWidthPx) {
|
|
140
|
-
return text;
|
|
141
|
-
}
|
|
142
|
-
const perChar = fontSize * AVERAGE_ADVANCE_RATIO;
|
|
143
|
-
const fits = Math.max(1, Math.floor(maxWidthPx / perChar) - 1);
|
|
144
|
-
return `${text.slice(0, fits)}…`;
|
|
145
|
-
}
|
|
@@ -50,8 +50,13 @@ export function buildTimeSeriesGeometry<M>(
|
|
|
50
50
|
const values: number[] = [];
|
|
51
51
|
let excluded = 0;
|
|
52
52
|
|
|
53
|
+
// Sorted and windowed once per series; both passes below read the result.
|
|
54
|
+
const ordered = new Map(
|
|
55
|
+
datasource.series.map(series => [series, visiblePoints(series, options.range)] as const),
|
|
56
|
+
);
|
|
57
|
+
|
|
53
58
|
for (const series of datasource.series) {
|
|
54
|
-
for (const point of
|
|
59
|
+
for (const point of ordered.get(series) ?? []) {
|
|
55
60
|
if (point.position.kind === 'instant') {
|
|
56
61
|
instants.push(point.position.at);
|
|
57
62
|
}
|
|
@@ -104,7 +109,7 @@ export function buildTimeSeriesGeometry<M>(
|
|
|
104
109
|
|
|
105
110
|
const pts: MaybePt[] = [];
|
|
106
111
|
|
|
107
|
-
for (const point of
|
|
112
|
+
for (const point of ordered.get(series) ?? []) {
|
|
108
113
|
const at = point.position.kind === 'instant' ? point.position.at : 0;
|
|
109
114
|
const px = x.project(at);
|
|
110
115
|
|
package/src/geometry/types.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface GeometryLabel {
|
|
|
46
46
|
readonly muted?: boolean;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** What a colour ramp runs from and to, for the legend that explains it. */
|
|
50
|
+
export interface ColorScale {
|
|
51
|
+
readonly min: number;
|
|
52
|
+
readonly max: number;
|
|
53
|
+
readonly colors: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
export interface ResolvedGeometry {
|
|
50
57
|
readonly plot: PlotRect;
|
|
51
58
|
readonly paths: readonly MarkPath[];
|
|
@@ -54,6 +61,8 @@ export interface ResolvedGeometry {
|
|
|
54
61
|
readonly xTicks: readonly Tick[];
|
|
55
62
|
readonly yTicks: readonly Tick[];
|
|
56
63
|
readonly hitRegions: readonly HitRegion[];
|
|
64
|
+
/** Present when colour encodes a value, as in a heatmap. */
|
|
65
|
+
readonly colorScale?: ColorScale;
|
|
57
66
|
/**
|
|
58
67
|
* True when something was excluded from a total or proportion, so the chart
|
|
59
68
|
* can say so rather than presenting a partial figure as whole (FR-013).
|
|
@@ -88,6 +97,8 @@ export interface GeometryInput {
|
|
|
88
97
|
Record<'unknown' | 'notApplicable' | 'withheld', NonValueTexture>
|
|
89
98
|
>;
|
|
90
99
|
readonly heatmapRamp: readonly string[];
|
|
100
|
+
/** Corner radii from the theme, for bars and heatmap cells. */
|
|
101
|
+
readonly radius?: { readonly bar: number; readonly cell: number };
|
|
91
102
|
readonly formatValue: (value: number) => string;
|
|
92
103
|
readonly formatCategory: (raw: string) => string;
|
|
93
104
|
readonly formatInstant: (at: number) => string;
|
package/src/index.ts
CHANGED
|
@@ -62,6 +62,8 @@ export {
|
|
|
62
62
|
resolveUnit,
|
|
63
63
|
} from './contract/datasource';
|
|
64
64
|
|
|
65
|
+
export type { DistributionValues } from './contract/distribution';
|
|
66
|
+
|
|
65
67
|
export type { MarkerKind, ProgressReading, ReferenceMarker } from './contract/marker';
|
|
66
68
|
export { findTarget, isUsableTarget, readProgress } from './contract/marker';
|
|
67
69
|
|
|
@@ -149,10 +151,15 @@ export {
|
|
|
149
151
|
TimeSeriesChart,
|
|
150
152
|
} from './react/charts';
|
|
151
153
|
|
|
152
|
-
export { ChartLegend, buildLegendEntries } from './react/ChartLegend';
|
|
154
|
+
export { ChartLegend, ColorScaleLegend, buildLegendEntries } from './react/ChartLegend';
|
|
155
|
+
export type { ColorScale } from './geometry/types';
|
|
153
156
|
export { ChartThemeProvider, useChartTheme } from './react/ChartThemeProvider';
|
|
154
157
|
|
|
155
158
|
// ---------------------------------------------------------------------------
|
|
156
159
|
// Accessibility helpers
|
|
157
160
|
// ---------------------------------------------------------------------------
|
|
158
161
|
export { buildChartSummary, buildPointAnnouncement, buildTotalAnnouncement } from './a11y/summary';
|
|
162
|
+
|
|
163
|
+
// The package's own texts, for translation (CHT-05).
|
|
164
|
+
export { chartStrings } from './contract/strings';
|
|
165
|
+
export type { ChartStringKey, ChartStrings } from './contract/strings';
|
|
@@ -69,7 +69,7 @@ export class ChartBoundary extends React.Component<Props, State> {
|
|
|
69
69
|
return { error: chartError('render', 'renderException') };
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
componentDidCatch(error: unknown, info: { componentStack?: string }): void {
|
|
72
|
+
override componentDidCatch(error: unknown, info: { componentStack?: string }): void {
|
|
73
73
|
const report = toFailureReport(
|
|
74
74
|
this.props.family,
|
|
75
75
|
error,
|
|
@@ -87,7 +87,7 @@ export class ChartBoundary extends React.Component<Props, State> {
|
|
|
87
87
|
this.props.onFailure?.(report);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
render(): React.ReactNode {
|
|
90
|
+
override render(): React.ReactNode {
|
|
91
91
|
if (this.state.error) {
|
|
92
92
|
return this.props.renderFallback(this.state.error);
|
|
93
93
|
}
|