@chipmobilesdk/rn-chart 0.2.0 → 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 -115
- package/README.md +381 -381
- package/package.json +46 -49
- package/skills/sdk-rn-chart/SKILL.md +68 -69
- package/src/a11y/summary.ts +170 -177
- package/src/contract/datasource.ts +126 -136
- 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 -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 +158 -176
- package/src/react/ChartBoundary.tsx +96 -96
- package/src/react/ChartFrame.tsx +583 -622
- package/src/react/ChartLegend.tsx +211 -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 +103 -110
- package/src/validation/validateDatasource.ts +174 -174
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Time-series geometry: one stroked path per series over an instant domain.
|
|
3
|
-
*
|
|
4
|
-
* Gaps are the interesting part. A non-known value contributes `null` to the
|
|
5
|
-
* point list, `linePath` lifts the pen there, and the line simply does not
|
|
6
|
-
* exist across the gap (FR-012).
|
|
7
|
-
*
|
|
8
|
-
* PURE. No React, no SVG.
|
|
9
|
-
*/
|
|
10
|
-
import type { ChartDatasource, Series } from '../contract/datasource';
|
|
11
|
-
import { isKnown, isWithheld } from '../contract/value';
|
|
12
|
-
import type { HitRegion } from './hitTest';
|
|
13
|
-
import { insetForAxes, plotBottom, plotRight } from './layout';
|
|
14
|
-
import { horizontalMarkers, markerDomainValues } from './markers';
|
|
15
|
-
import { linePath, type MaybePt } from './path';
|
|
16
|
-
import { domainFrom, linearScale, timeScale, widenDegenerateDomain } from './scale';
|
|
17
|
-
import { niceTickValues, thinTicks, type Tick } from './ticks';
|
|
18
|
-
import type { GeometryInput, MarkPath, ResolvedGeometry } from './types';
|
|
19
|
-
|
|
20
|
-
export interface TimeSeriesOptions {
|
|
21
|
-
/** Window supplied by the app. Users cannot draw one (pan/zoom out of scope). */
|
|
22
|
-
readonly range?: { readonly from: number; readonly to: number };
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function visiblePoints<M>(series: Series<M>, range?: TimeSeriesOptions['range']) {
|
|
26
|
-
const points = [...series.points]
|
|
27
|
-
.filter(p => p.position.kind === 'instant')
|
|
28
|
-
// Out-of-order input must still produce a correct axis.
|
|
29
|
-
.sort((a, b) => {
|
|
30
|
-
const at = a.position.kind === 'instant' ? a.position.at : 0;
|
|
31
|
-
const bt = b.position.kind === 'instant' ? b.position.at : 0;
|
|
32
|
-
return at - bt;
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
if (!range) {
|
|
36
|
-
return points;
|
|
37
|
-
}
|
|
38
|
-
return points.filter(p => {
|
|
39
|
-
const at = p.position.kind === 'instant' ? p.position.at : 0;
|
|
40
|
-
return at >= range.from && at <= range.to;
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function buildTimeSeriesGeometry<M>(
|
|
45
|
-
datasource: ChartDatasource<M>,
|
|
46
|
-
input: GeometryInput,
|
|
47
|
-
options: TimeSeriesOptions = {},
|
|
48
|
-
): ResolvedGeometry {
|
|
49
|
-
const instants: number[] = [];
|
|
50
|
-
const values: number[] = [];
|
|
51
|
-
let excluded = 0;
|
|
52
|
-
|
|
53
|
-
for (const series of datasource.series) {
|
|
54
|
-
for (const point of visiblePoints(series, options.range)) {
|
|
55
|
-
if (point.position.kind === 'instant') {
|
|
56
|
-
instants.push(point.position.at);
|
|
57
|
-
}
|
|
58
|
-
if (isKnown(point.value)) {
|
|
59
|
-
values.push(point.value.value);
|
|
60
|
-
} else {
|
|
61
|
-
excluded += 1;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (instants.length === 0) {
|
|
67
|
-
return {
|
|
68
|
-
plot: input.plot,
|
|
69
|
-
paths: [],
|
|
70
|
-
markers: [],
|
|
71
|
-
labels: [],
|
|
72
|
-
xTicks: [],
|
|
73
|
-
yTicks: [],
|
|
74
|
-
hitRegions: [],
|
|
75
|
-
incomplete: excluded > 0,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Tick labels depend only on the domains, so the plot can make room for
|
|
80
|
-
// them before anything is projected.
|
|
81
|
-
// Widened here, as the scales would, so the ticks label the axes actually drawn.
|
|
82
|
-
const xDomain = widenDegenerateDomain(domainFrom(instants));
|
|
83
|
-
const yDomain = widenDegenerateDomain(
|
|
84
|
-
domainFrom([...values, ...markerDomainValues(datasource.markers)], { padFraction: 0.1 }),
|
|
85
|
-
);
|
|
86
|
-
const xTickValues = niceTickValues(xDomain, 4);
|
|
87
|
-
const yTickValues = niceTickValues(yDomain, 4);
|
|
88
|
-
const plot = insetForAxes(input.plot, {
|
|
89
|
-
xLabels: xTickValues.map(input.formatInstant),
|
|
90
|
-
yLabels: yTickValues.map(input.formatValue),
|
|
91
|
-
fontSize: input.fontSize,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const x = timeScale(xDomain, { min: plot.x, max: plotRight(plot) });
|
|
95
|
-
const y = linearScale(yDomain, { min: plotBottom(plot), max: plot.y });
|
|
96
|
-
|
|
97
|
-
const paths: MarkPath[] = [];
|
|
98
|
-
const hitRegions: HitRegion[] = [];
|
|
99
|
-
|
|
100
|
-
for (const series of datasource.series) {
|
|
101
|
-
if (series.hidden) {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const pts: MaybePt[] = [];
|
|
106
|
-
|
|
107
|
-
for (const point of visiblePoints(series, options.range)) {
|
|
108
|
-
const at = point.position.kind === 'instant' ? point.position.at : 0;
|
|
109
|
-
const px = x.project(at);
|
|
110
|
-
|
|
111
|
-
if (!isKnown(point.value)) {
|
|
112
|
-
// The gap. No coordinate is produced, so no segment can span it.
|
|
113
|
-
pts.push(null);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const py = y.project(point.value.value);
|
|
118
|
-
pts.push({ x: px, y: py });
|
|
119
|
-
|
|
120
|
-
hitRegions.push({
|
|
121
|
-
seriesId: series.id,
|
|
122
|
-
pointId: point.id,
|
|
123
|
-
x: px,
|
|
124
|
-
y: py,
|
|
125
|
-
// Withheld values are never selectable, so a callback can never
|
|
126
|
-
// carry one (FR-053).
|
|
127
|
-
selectable: !isWithheld(point.value),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const d = linePath(pts);
|
|
132
|
-
if (d.length > 0) {
|
|
133
|
-
paths.push({
|
|
134
|
-
key: `line-${series.id}`,
|
|
135
|
-
seriesId: series.id,
|
|
136
|
-
d,
|
|
137
|
-
color: input.seriesColors[series.id],
|
|
138
|
-
pattern: series.pattern ?? 'solid',
|
|
139
|
-
filled: false,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const xTicks: Tick[] = xTickValues.map(v => ({
|
|
145
|
-
value: v,
|
|
146
|
-
position: x.project(v),
|
|
147
|
-
label: input.formatInstant(v),
|
|
148
|
-
}));
|
|
149
|
-
|
|
150
|
-
const yTicks: Tick[] = yTickValues.map(v => ({
|
|
151
|
-
value: v,
|
|
152
|
-
position: y.project(v),
|
|
153
|
-
label: input.formatValue(v),
|
|
154
|
-
}));
|
|
155
|
-
|
|
156
|
-
const markers = horizontalMarkers(datasource.markers, plot, y.project);
|
|
157
|
-
|
|
158
|
-
return {
|
|
159
|
-
plot,
|
|
160
|
-
paths,
|
|
161
|
-
markers: markers.lines,
|
|
162
|
-
labels: markers.labels,
|
|
163
|
-
xTicks: thinTicks(xTicks, input.fontSize, plot.width),
|
|
164
|
-
yTicks,
|
|
165
|
-
hitRegions,
|
|
166
|
-
incomplete: excluded > 0,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Time-series geometry: one stroked path per series over an instant domain.
|
|
3
|
+
*
|
|
4
|
+
* Gaps are the interesting part. A non-known value contributes `null` to the
|
|
5
|
+
* point list, `linePath` lifts the pen there, and the line simply does not
|
|
6
|
+
* exist across the gap (FR-012).
|
|
7
|
+
*
|
|
8
|
+
* PURE. No React, no SVG.
|
|
9
|
+
*/
|
|
10
|
+
import type { ChartDatasource, Series } from '../contract/datasource';
|
|
11
|
+
import { isKnown, isWithheld } from '../contract/value';
|
|
12
|
+
import type { HitRegion } from './hitTest';
|
|
13
|
+
import { insetForAxes, plotBottom, plotRight } from './layout';
|
|
14
|
+
import { horizontalMarkers, markerDomainValues } from './markers';
|
|
15
|
+
import { linePath, type MaybePt } from './path';
|
|
16
|
+
import { domainFrom, linearScale, timeScale, widenDegenerateDomain } from './scale';
|
|
17
|
+
import { niceTickValues, thinTicks, type Tick } from './ticks';
|
|
18
|
+
import type { GeometryInput, MarkPath, ResolvedGeometry } from './types';
|
|
19
|
+
|
|
20
|
+
export interface TimeSeriesOptions {
|
|
21
|
+
/** Window supplied by the app. Users cannot draw one (pan/zoom out of scope). */
|
|
22
|
+
readonly range?: { readonly from: number; readonly to: number };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function visiblePoints<M>(series: Series<M>, range?: TimeSeriesOptions['range']) {
|
|
26
|
+
const points = [...series.points]
|
|
27
|
+
.filter(p => p.position.kind === 'instant')
|
|
28
|
+
// Out-of-order input must still produce a correct axis.
|
|
29
|
+
.sort((a, b) => {
|
|
30
|
+
const at = a.position.kind === 'instant' ? a.position.at : 0;
|
|
31
|
+
const bt = b.position.kind === 'instant' ? b.position.at : 0;
|
|
32
|
+
return at - bt;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!range) {
|
|
36
|
+
return points;
|
|
37
|
+
}
|
|
38
|
+
return points.filter(p => {
|
|
39
|
+
const at = p.position.kind === 'instant' ? p.position.at : 0;
|
|
40
|
+
return at >= range.from && at <= range.to;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildTimeSeriesGeometry<M>(
|
|
45
|
+
datasource: ChartDatasource<M>,
|
|
46
|
+
input: GeometryInput,
|
|
47
|
+
options: TimeSeriesOptions = {},
|
|
48
|
+
): ResolvedGeometry {
|
|
49
|
+
const instants: number[] = [];
|
|
50
|
+
const values: number[] = [];
|
|
51
|
+
let excluded = 0;
|
|
52
|
+
|
|
53
|
+
for (const series of datasource.series) {
|
|
54
|
+
for (const point of visiblePoints(series, options.range)) {
|
|
55
|
+
if (point.position.kind === 'instant') {
|
|
56
|
+
instants.push(point.position.at);
|
|
57
|
+
}
|
|
58
|
+
if (isKnown(point.value)) {
|
|
59
|
+
values.push(point.value.value);
|
|
60
|
+
} else {
|
|
61
|
+
excluded += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (instants.length === 0) {
|
|
67
|
+
return {
|
|
68
|
+
plot: input.plot,
|
|
69
|
+
paths: [],
|
|
70
|
+
markers: [],
|
|
71
|
+
labels: [],
|
|
72
|
+
xTicks: [],
|
|
73
|
+
yTicks: [],
|
|
74
|
+
hitRegions: [],
|
|
75
|
+
incomplete: excluded > 0,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Tick labels depend only on the domains, so the plot can make room for
|
|
80
|
+
// them before anything is projected.
|
|
81
|
+
// Widened here, as the scales would, so the ticks label the axes actually drawn.
|
|
82
|
+
const xDomain = widenDegenerateDomain(domainFrom(instants));
|
|
83
|
+
const yDomain = widenDegenerateDomain(
|
|
84
|
+
domainFrom([...values, ...markerDomainValues(datasource.markers)], { padFraction: 0.1 }),
|
|
85
|
+
);
|
|
86
|
+
const xTickValues = niceTickValues(xDomain, 4);
|
|
87
|
+
const yTickValues = niceTickValues(yDomain, 4);
|
|
88
|
+
const plot = insetForAxes(input.plot, {
|
|
89
|
+
xLabels: xTickValues.map(input.formatInstant),
|
|
90
|
+
yLabels: yTickValues.map(input.formatValue),
|
|
91
|
+
fontSize: input.fontSize,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const x = timeScale(xDomain, { min: plot.x, max: plotRight(plot) });
|
|
95
|
+
const y = linearScale(yDomain, { min: plotBottom(plot), max: plot.y });
|
|
96
|
+
|
|
97
|
+
const paths: MarkPath[] = [];
|
|
98
|
+
const hitRegions: HitRegion[] = [];
|
|
99
|
+
|
|
100
|
+
for (const series of datasource.series) {
|
|
101
|
+
if (series.hidden) {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pts: MaybePt[] = [];
|
|
106
|
+
|
|
107
|
+
for (const point of visiblePoints(series, options.range)) {
|
|
108
|
+
const at = point.position.kind === 'instant' ? point.position.at : 0;
|
|
109
|
+
const px = x.project(at);
|
|
110
|
+
|
|
111
|
+
if (!isKnown(point.value)) {
|
|
112
|
+
// The gap. No coordinate is produced, so no segment can span it.
|
|
113
|
+
pts.push(null);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const py = y.project(point.value.value);
|
|
118
|
+
pts.push({ x: px, y: py });
|
|
119
|
+
|
|
120
|
+
hitRegions.push({
|
|
121
|
+
seriesId: series.id,
|
|
122
|
+
pointId: point.id,
|
|
123
|
+
x: px,
|
|
124
|
+
y: py,
|
|
125
|
+
// Withheld values are never selectable, so a callback can never
|
|
126
|
+
// carry one (FR-053).
|
|
127
|
+
selectable: !isWithheld(point.value),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const d = linePath(pts);
|
|
132
|
+
if (d.length > 0) {
|
|
133
|
+
paths.push({
|
|
134
|
+
key: `line-${series.id}`,
|
|
135
|
+
seriesId: series.id,
|
|
136
|
+
d,
|
|
137
|
+
color: input.seriesColors[series.id],
|
|
138
|
+
pattern: series.pattern ?? 'solid',
|
|
139
|
+
filled: false,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const xTicks: Tick[] = xTickValues.map(v => ({
|
|
145
|
+
value: v,
|
|
146
|
+
position: x.project(v),
|
|
147
|
+
label: input.formatInstant(v),
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
const yTicks: Tick[] = yTickValues.map(v => ({
|
|
151
|
+
value: v,
|
|
152
|
+
position: y.project(v),
|
|
153
|
+
label: input.formatValue(v),
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
const markers = horizontalMarkers(datasource.markers, plot, y.project);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
plot,
|
|
160
|
+
paths,
|
|
161
|
+
markers: markers.lines,
|
|
162
|
+
labels: markers.labels,
|
|
163
|
+
xTicks: thinTicks(xTicks, input.fontSize, plot.width),
|
|
164
|
+
yTicks,
|
|
165
|
+
hitRegions,
|
|
166
|
+
incomplete: excluded > 0,
|
|
167
|
+
};
|
|
168
|
+
}
|
package/src/geometry/types.ts
CHANGED
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The single memoized artefact each chart produces.
|
|
3
|
-
*
|
|
4
|
-
* `ResolvedGeometry` is keyed on datasource identity, plot rectangle, theme
|
|
5
|
-
* identity, and formatter identity — and deliberately **not** on selection.
|
|
6
|
-
* Highlighting a point must repaint the highlight, not recompute every scale
|
|
7
|
-
* and path string (research.md R3, FR-073).
|
|
8
|
-
*
|
|
9
|
-
* PURE. No React, no SVG.
|
|
10
|
-
*/
|
|
11
|
-
import type { SeriesPattern } from '../contract/datasource';
|
|
12
|
-
import type { NonValueTexture } from '../contract/theme';
|
|
13
|
-
import type { HitRegion } from './hitTest';
|
|
14
|
-
import type { PlotRect } from './layout';
|
|
15
|
-
import type { Tick } from './ticks';
|
|
16
|
-
|
|
17
|
-
/** One drawable group. Element count scales with these, never with points. */
|
|
18
|
-
export interface MarkPath {
|
|
19
|
-
readonly key: string;
|
|
20
|
-
readonly seriesId: string;
|
|
21
|
-
readonly d: string;
|
|
22
|
-
readonly color: string;
|
|
23
|
-
readonly pattern: SeriesPattern;
|
|
24
|
-
/** Filled shapes (bars, arcs, cells) versus stroked ones (lines). */
|
|
25
|
-
readonly filled: boolean;
|
|
26
|
-
/** Set on marks standing in for a non-value class: texture, not hue. */
|
|
27
|
-
readonly texture?: NonValueTexture;
|
|
28
|
-
readonly opacity?: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/** A target, baseline, or threshold drawn across the plot. */
|
|
32
|
-
export interface MarkerLine {
|
|
33
|
-
readonly id: string;
|
|
34
|
-
readonly d: string;
|
|
35
|
-
readonly label?: string;
|
|
36
|
-
readonly kind: 'target' | 'baseline' | 'threshold';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Text the chart draws itself, such as progress values or segment labels. */
|
|
40
|
-
export interface GeometryLabel {
|
|
41
|
-
readonly key: string;
|
|
42
|
-
readonly text: string;
|
|
43
|
-
readonly x: number;
|
|
44
|
-
readonly y: number;
|
|
45
|
-
readonly anchor: 'start' | 'middle' | 'end';
|
|
46
|
-
readonly muted?: boolean;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface ResolvedGeometry {
|
|
50
|
-
readonly plot: PlotRect;
|
|
51
|
-
readonly paths: readonly MarkPath[];
|
|
52
|
-
readonly markers: readonly MarkerLine[];
|
|
53
|
-
readonly labels: readonly GeometryLabel[];
|
|
54
|
-
readonly xTicks: readonly Tick[];
|
|
55
|
-
readonly yTicks: readonly Tick[];
|
|
56
|
-
readonly hitRegions: readonly HitRegion[];
|
|
57
|
-
/**
|
|
58
|
-
* True when something was excluded from a total or proportion, so the chart
|
|
59
|
-
* can say so rather than presenting a partial figure as whole (FR-013).
|
|
60
|
-
*/
|
|
61
|
-
readonly incomplete: boolean;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export const EMPTY_GEOMETRY: ResolvedGeometry = {
|
|
65
|
-
plot: { x: 0, y: 0, width: 0, height: 0 },
|
|
66
|
-
paths: [],
|
|
67
|
-
markers: [],
|
|
68
|
-
labels: [],
|
|
69
|
-
xTicks: [],
|
|
70
|
-
yTicks: [],
|
|
71
|
-
hitRegions: [],
|
|
72
|
-
incomplete: false,
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/** Inputs every family geometry builder needs. */
|
|
76
|
-
export interface GeometryInput {
|
|
77
|
-
readonly plot: PlotRect;
|
|
78
|
-
readonly fontSize: number;
|
|
79
|
-
/** Resolved per series, so geometry never reaches into the theme itself. */
|
|
80
|
-
readonly seriesColors: Readonly<Record<string, string>>;
|
|
81
|
-
/**
|
|
82
|
-
* Distribution segment colours keyed by `segmentKey(seriesId, pointId)`.
|
|
83
|
-
* Only resolved for the distribution family. Point colours used to share
|
|
84
|
-
* `seriesColors`, so a point id equal to a series id overwrote that series.
|
|
85
|
-
*/
|
|
86
|
-
readonly segmentColors?: Readonly<Record<string, string>>;
|
|
87
|
-
readonly nonValueTextures: Readonly<
|
|
88
|
-
Record<'unknown' | 'notApplicable' | 'withheld', NonValueTexture>
|
|
89
|
-
>;
|
|
90
|
-
readonly heatmapRamp: readonly string[];
|
|
91
|
-
readonly formatValue: (value: number) => string;
|
|
92
|
-
readonly formatCategory: (raw: string) => string;
|
|
93
|
-
readonly formatInstant: (at: number) => string;
|
|
94
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The single memoized artefact each chart produces.
|
|
3
|
+
*
|
|
4
|
+
* `ResolvedGeometry` is keyed on datasource identity, plot rectangle, theme
|
|
5
|
+
* identity, and formatter identity — and deliberately **not** on selection.
|
|
6
|
+
* Highlighting a point must repaint the highlight, not recompute every scale
|
|
7
|
+
* and path string (research.md R3, FR-073).
|
|
8
|
+
*
|
|
9
|
+
* PURE. No React, no SVG.
|
|
10
|
+
*/
|
|
11
|
+
import type { SeriesPattern } from '../contract/datasource';
|
|
12
|
+
import type { NonValueTexture } from '../contract/theme';
|
|
13
|
+
import type { HitRegion } from './hitTest';
|
|
14
|
+
import type { PlotRect } from './layout';
|
|
15
|
+
import type { Tick } from './ticks';
|
|
16
|
+
|
|
17
|
+
/** One drawable group. Element count scales with these, never with points. */
|
|
18
|
+
export interface MarkPath {
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly seriesId: string;
|
|
21
|
+
readonly d: string;
|
|
22
|
+
readonly color: string;
|
|
23
|
+
readonly pattern: SeriesPattern;
|
|
24
|
+
/** Filled shapes (bars, arcs, cells) versus stroked ones (lines). */
|
|
25
|
+
readonly filled: boolean;
|
|
26
|
+
/** Set on marks standing in for a non-value class: texture, not hue. */
|
|
27
|
+
readonly texture?: NonValueTexture;
|
|
28
|
+
readonly opacity?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A target, baseline, or threshold drawn across the plot. */
|
|
32
|
+
export interface MarkerLine {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly d: string;
|
|
35
|
+
readonly label?: string;
|
|
36
|
+
readonly kind: 'target' | 'baseline' | 'threshold';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Text the chart draws itself, such as progress values or segment labels. */
|
|
40
|
+
export interface GeometryLabel {
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly text: string;
|
|
43
|
+
readonly x: number;
|
|
44
|
+
readonly y: number;
|
|
45
|
+
readonly anchor: 'start' | 'middle' | 'end';
|
|
46
|
+
readonly muted?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ResolvedGeometry {
|
|
50
|
+
readonly plot: PlotRect;
|
|
51
|
+
readonly paths: readonly MarkPath[];
|
|
52
|
+
readonly markers: readonly MarkerLine[];
|
|
53
|
+
readonly labels: readonly GeometryLabel[];
|
|
54
|
+
readonly xTicks: readonly Tick[];
|
|
55
|
+
readonly yTicks: readonly Tick[];
|
|
56
|
+
readonly hitRegions: readonly HitRegion[];
|
|
57
|
+
/**
|
|
58
|
+
* True when something was excluded from a total or proportion, so the chart
|
|
59
|
+
* can say so rather than presenting a partial figure as whole (FR-013).
|
|
60
|
+
*/
|
|
61
|
+
readonly incomplete: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const EMPTY_GEOMETRY: ResolvedGeometry = {
|
|
65
|
+
plot: { x: 0, y: 0, width: 0, height: 0 },
|
|
66
|
+
paths: [],
|
|
67
|
+
markers: [],
|
|
68
|
+
labels: [],
|
|
69
|
+
xTicks: [],
|
|
70
|
+
yTicks: [],
|
|
71
|
+
hitRegions: [],
|
|
72
|
+
incomplete: false,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Inputs every family geometry builder needs. */
|
|
76
|
+
export interface GeometryInput {
|
|
77
|
+
readonly plot: PlotRect;
|
|
78
|
+
readonly fontSize: number;
|
|
79
|
+
/** Resolved per series, so geometry never reaches into the theme itself. */
|
|
80
|
+
readonly seriesColors: Readonly<Record<string, string>>;
|
|
81
|
+
/**
|
|
82
|
+
* Distribution segment colours keyed by `segmentKey(seriesId, pointId)`.
|
|
83
|
+
* Only resolved for the distribution family. Point colours used to share
|
|
84
|
+
* `seriesColors`, so a point id equal to a series id overwrote that series.
|
|
85
|
+
*/
|
|
86
|
+
readonly segmentColors?: Readonly<Record<string, string>>;
|
|
87
|
+
readonly nonValueTextures: Readonly<
|
|
88
|
+
Record<'unknown' | 'notApplicable' | 'withheld', NonValueTexture>
|
|
89
|
+
>;
|
|
90
|
+
readonly heatmapRamp: readonly string[];
|
|
91
|
+
readonly formatValue: (value: number) => string;
|
|
92
|
+
readonly formatCategory: (raw: string) => string;
|
|
93
|
+
readonly formatInstant: (at: number) => string;
|
|
94
|
+
}
|