@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
package/package.json
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@chipmobilesdk/rn-chart",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"private": false,
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"main": "src/index.ts",
|
|
7
|
-
"types": "src/index.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./src/index.ts",
|
|
11
|
-
"react-native": "./src/index.ts",
|
|
12
|
-
"default": "./src/index.ts"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"src/index.ts",
|
|
17
|
-
"src/a11y",
|
|
18
|
-
"src/contract",
|
|
19
|
-
"src/geometry",
|
|
20
|
-
"src/react",
|
|
21
|
-
"src/render",
|
|
22
|
-
"src/validation",
|
|
23
|
-
"skills",
|
|
24
|
-
"README.md",
|
|
25
|
-
"CHANGELOG.md"
|
|
26
|
-
],
|
|
27
|
-
"sideEffects": false,
|
|
28
|
-
"scripts": {
|
|
29
|
-
"typecheck": "tsc -p tsconfig.json
|
|
30
|
-
"pack:
|
|
31
|
-
},
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"react-native-svg":
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
"publishConfig": {
|
|
46
|
-
"access": "public",
|
|
47
|
-
"registry": "https://registry.npmjs.org/"
|
|
48
|
-
}
|
|
49
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@chipmobilesdk/rn-chart",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"react-native": "./src/index.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src/index.ts",
|
|
17
|
+
"src/a11y",
|
|
18
|
+
"src/contract",
|
|
19
|
+
"src/geometry",
|
|
20
|
+
"src/react",
|
|
21
|
+
"src/render",
|
|
22
|
+
"src/validation",
|
|
23
|
+
"skills",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"scripts": {
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
|
|
30
|
+
"pack:check": "npm pack --dry-run"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@chipmobilesdk/rn-core": "^0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"react": ">=19",
|
|
37
|
+
"react-native": ">=0.85",
|
|
38
|
+
"react-native-svg": ">=15.15"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"react-native-svg": {
|
|
42
|
+
"optional": false
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"registry": "https://registry.npmjs.org/"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,69 +1,68 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: sdk-rn-chart
|
|
3
|
-
description: "Charts via @chipmobilesdk/rn-chart: datasource contract, series theming, legends, selection, dataset budgets, chart accessibility. Use when rendering a chart, wiring chart data, theming series, or hitting a dataset size limit."
|
|
4
|
-
metadata:
|
|
5
|
-
package: "@chipmobilesdk/rn-chart"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
| Size limits | `## Dataset budgets` |
|
|
1
|
+
---
|
|
2
|
+
name: sdk-rn-chart
|
|
3
|
+
description: "Charts via @chipmobilesdk/rn-chart: datasource contract, series theming, legends, selection, dataset budgets, chart accessibility. Use when rendering a chart, wiring chart data, theming series, or hitting a dataset size limit."
|
|
4
|
+
metadata:
|
|
5
|
+
package: "@chipmobilesdk/rn-chart"
|
|
6
|
+
user-invocable: true
|
|
7
|
+
disable-model-invocation: false
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# @chipmobilesdk/rn-chart
|
|
11
|
+
|
|
12
|
+
Chart rendering with an explicit datasource contract, its own theme provider,
|
|
13
|
+
accessibility support and enforced dataset budgets.
|
|
14
|
+
|
|
15
|
+
## Use this when
|
|
16
|
+
|
|
17
|
+
- Rendering any chart or adding a series
|
|
18
|
+
- Shaping data to the datasource contract
|
|
19
|
+
- Theming series colors or building a legend
|
|
20
|
+
- Handling selection / interaction on a chart
|
|
21
|
+
- A chart refuses to render a large dataset
|
|
22
|
+
|
|
23
|
+
## Check what it deliberately does not do
|
|
24
|
+
|
|
25
|
+
The README opens with `## What it does not do`. Read that first — it prevents building
|
|
26
|
+
a feature this package intentionally excludes.
|
|
27
|
+
|
|
28
|
+
## Datasets are budgeted
|
|
29
|
+
|
|
30
|
+
`DATASET_BUDGETS` and `checkBudget` enforce size limits so charts stay at 60 FPS on
|
|
31
|
+
low-end devices. When a budget is exceeded, aggregate or window the data — do not raise
|
|
32
|
+
the budget. See `## Dataset budgets`.
|
|
33
|
+
|
|
34
|
+
## Chart theming is separate from app theming
|
|
35
|
+
|
|
36
|
+
Charts use `ChartThemeProvider` / `useChartTheme`, not the app-wide theme provider from
|
|
37
|
+
`sdk-rn-theme`. Series colors come from `resolveSeriesColor`, never from hardcoded values.
|
|
38
|
+
|
|
39
|
+
## Public API
|
|
40
|
+
|
|
41
|
+
Runtime:
|
|
42
|
+
|
|
43
|
+
- `ChartThemeProvider`, `useChartTheme`, `defaultChartTheme`, `resolveSeriesColor`
|
|
44
|
+
- `ChartLegend`, `buildLegendEntries`
|
|
45
|
+
- `findTarget`, `isUsableTarget`, `readProgress`, `sameSelection`
|
|
46
|
+
- `DATASET_BUDGETS`, `checkBudget`
|
|
47
|
+
- `isChartError`
|
|
48
|
+
|
|
49
|
+
Types: `MarkerKind`, `ProgressReading`, `ReferenceMarker`, `BudgetCheck`, `DatasetBudget`
|
|
50
|
+
|
|
51
|
+
Use `sameSelection` for selection equality — comparing selection objects by reference
|
|
52
|
+
causes redundant re-renders.
|
|
53
|
+
|
|
54
|
+
## Where the detail lives
|
|
55
|
+
|
|
56
|
+
Read one section of `node_modules/@chipmobilesdk/rn-chart/README.md` (~360 lines):
|
|
57
|
+
|
|
58
|
+
| Need | Section |
|
|
59
|
+
|---|---|
|
|
60
|
+
| Scope check | `## What it does not do` |
|
|
61
|
+
| First integration | `## Install`, `## Quick start` |
|
|
62
|
+
| Shaping data | `## The datasource contract`, `## Value semantics` |
|
|
63
|
+
| Loading/empty/error rendering | `## States` |
|
|
64
|
+
| Touch and selection | `## Interaction` |
|
|
65
|
+
| Colors and legends | `## Theming` |
|
|
66
|
+
| Axis/label formatting | `## Formatting` |
|
|
67
|
+
| Screen reader support | `## Accessibility` |
|
|
68
|
+
| Size limits | `## Dataset budgets` |
|
package/src/a11y/summary.ts
CHANGED
|
@@ -1,177 +1,190 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Composed accessible summaries (FR-067, FR-068, FR-069, FR-072).
|
|
3
|
-
*
|
|
4
|
-
* Exposing 2,500 individual points to a screen reader would be unusable even
|
|
5
|
-
* if it were cheap. A composed summary covers the same need: what the chart is,
|
|
6
|
-
* what it shows, where the extremes are, and what any target says.
|
|
7
|
-
*
|
|
8
|
-
* Withheld values never enter a summary — that would leak exactly what the
|
|
9
|
-
* withheld class exists to protect (FR-033).
|
|
10
|
-
*/
|
|
11
|
-
import type { ChartDatasource, ChartFamily } from '../contract/datasource';
|
|
12
|
-
import type { ReferenceMarker } from '../contract/marker';
|
|
13
|
-
import type { ChartStatus } from '../contract/state';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
readonly
|
|
43
|
-
readonly
|
|
44
|
-
readonly
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
statusPhrase
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Composed accessible summaries (FR-067, FR-068, FR-069, FR-072).
|
|
3
|
+
*
|
|
4
|
+
* Exposing 2,500 individual points to a screen reader would be unusable even
|
|
5
|
+
* if it were cheap. A composed summary covers the same need: what the chart is,
|
|
6
|
+
* what it shows, where the extremes are, and what any target says.
|
|
7
|
+
*
|
|
8
|
+
* Withheld values never enter a summary — that would leak exactly what the
|
|
9
|
+
* withheld class exists to protect (FR-033).
|
|
10
|
+
*/
|
|
11
|
+
import type { ChartDatasource, ChartFamily } from '../contract/datasource';
|
|
12
|
+
import type { ReferenceMarker } from '../contract/marker';
|
|
13
|
+
import type { ChartStatus } from '../contract/state';
|
|
14
|
+
import { formatMessage } from '@chipmobilesdk/rn-core';
|
|
15
|
+
|
|
16
|
+
import { chartStrings, type ChartStrings } from '../contract/strings';
|
|
17
|
+
import { isKnown, sumKnown, type ValueState } from '../contract/value';
|
|
18
|
+
|
|
19
|
+
const FAMILY_NOUN: Record<ChartFamily, keyof ChartStrings> = {
|
|
20
|
+
progress: 'chart.family.progress',
|
|
21
|
+
distribution: 'chart.family.distribution',
|
|
22
|
+
timeSeries: 'chart.family.timeSeries',
|
|
23
|
+
bar: 'chart.family.bar',
|
|
24
|
+
heatmap: 'chart.family.heatmap',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* What a screen reader hears when the chart is not showing data. Announcing
|
|
29
|
+
* the state beats silence, and beats announcing a stale value as if current.
|
|
30
|
+
*/
|
|
31
|
+
const STATUS_PHRASE: Record<ChartStatus, keyof ChartStrings | undefined> = {
|
|
32
|
+
loading: 'chart.status.loading',
|
|
33
|
+
empty: 'chart.status.empty',
|
|
34
|
+
error: 'chart.status.error',
|
|
35
|
+
stale: 'chart.status.stale',
|
|
36
|
+
noPermission: 'chart.status.noPermission',
|
|
37
|
+
partial: 'chart.status.partial',
|
|
38
|
+
ready: undefined,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface SummaryInput<M = unknown> {
|
|
42
|
+
readonly datasource: ChartDatasource<M>;
|
|
43
|
+
readonly family: ChartFamily;
|
|
44
|
+
readonly status: ChartStatus;
|
|
45
|
+
readonly title?: string;
|
|
46
|
+
readonly formatValue: (value: ValueState) => string;
|
|
47
|
+
readonly incomplete?: boolean;
|
|
48
|
+
/** The texts to speak; the English defaults when omitted. */
|
|
49
|
+
readonly strings?: ChartStrings;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function describeExtremes<M>(
|
|
53
|
+
datasource: ChartDatasource<M>,
|
|
54
|
+
formatValue: (v: ValueState) => string,
|
|
55
|
+
strings: ChartStrings,
|
|
56
|
+
): string {
|
|
57
|
+
let min: { label: string; value: number } | undefined;
|
|
58
|
+
let max: { label: string; value: number } | undefined;
|
|
59
|
+
|
|
60
|
+
for (const series of datasource.series) {
|
|
61
|
+
for (const point of series.points) {
|
|
62
|
+
// Withheld and unknown values are skipped entirely.
|
|
63
|
+
if (!isKnown(point.value)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const label =
|
|
67
|
+
point.position.kind === 'category'
|
|
68
|
+
? point.position.category
|
|
69
|
+
: point.position.kind === 'matrix'
|
|
70
|
+
? `${point.position.row} ${point.position.column}`
|
|
71
|
+
: series.label;
|
|
72
|
+
|
|
73
|
+
if (!min || point.value.value < min.value) {
|
|
74
|
+
min = { label, value: point.value.value };
|
|
75
|
+
}
|
|
76
|
+
if (!max || point.value.value > max.value) {
|
|
77
|
+
max = { label, value: point.value.value };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!min || !max) {
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
const maxText = formatValue({ kind: 'known', value: max.value });
|
|
86
|
+
if (min.label === max.label) {
|
|
87
|
+
return formatMessage(strings['chart.summary.single'], { value: maxText });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return formatMessage(strings['chart.summary.extremes'], {
|
|
91
|
+
maxLabel: max.label,
|
|
92
|
+
max: maxText,
|
|
93
|
+
minLabel: min.label,
|
|
94
|
+
min: formatValue({ kind: 'known', value: min.value }),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function describeMarkers(
|
|
99
|
+
markers: readonly ReferenceMarker[] | undefined,
|
|
100
|
+
formatValue: (v: ValueState) => string,
|
|
101
|
+
strings: ChartStrings,
|
|
102
|
+
): string {
|
|
103
|
+
if (!markers || markers.length === 0) {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const parts: string[] = [];
|
|
108
|
+
for (const marker of markers) {
|
|
109
|
+
if (!isKnown(marker.value)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const name = marker.label ?? marker.kind;
|
|
113
|
+
parts.push(
|
|
114
|
+
formatMessage(strings['chart.summary.marker'], { name, value: formatValue(marker.value) }),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return parts.length > 0 ? `${parts.join('. ')}.` : '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Builds the label read aloud for the chart as a whole.
|
|
123
|
+
*
|
|
124
|
+
* Non-ready statuses replace the data description rather than appending to it,
|
|
125
|
+
* so a loading chart never announces figures it is not showing.
|
|
126
|
+
*/
|
|
127
|
+
export function buildChartSummary<M>(input: SummaryInput<M>): string {
|
|
128
|
+
const strings = input.strings ?? chartStrings.defaults;
|
|
129
|
+
const noun = strings[FAMILY_NOUN[input.family]];
|
|
130
|
+
const heading = input.title
|
|
131
|
+
? formatMessage(strings['chart.summary.heading'], { title: input.title, noun })
|
|
132
|
+
: formatMessage(strings['chart.summary.headingUntitled'], { noun });
|
|
133
|
+
|
|
134
|
+
const statusKey = STATUS_PHRASE[input.status];
|
|
135
|
+
const statusPhrase = statusKey ? strings[statusKey] : undefined;
|
|
136
|
+
if (input.status !== 'ready' && input.status !== 'partial' && input.status !== 'stale') {
|
|
137
|
+
return `${heading} ${statusPhrase ?? ''}`.trim();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const seriesCount = input.datasource.series.length;
|
|
141
|
+
const pointCount = input.datasource.series.reduce((acc, s) => acc + s.points.length, 0);
|
|
142
|
+
|
|
143
|
+
const scale =
|
|
144
|
+
seriesCount > 1
|
|
145
|
+
? formatMessage(strings['chart.summary.scale'], { series: seriesCount, points: pointCount })
|
|
146
|
+
: formatMessage(strings['chart.summary.scaleSingle'], { points: pointCount });
|
|
147
|
+
|
|
148
|
+
const parts = [
|
|
149
|
+
heading,
|
|
150
|
+
statusPhrase,
|
|
151
|
+
scale,
|
|
152
|
+
describeExtremes(input.datasource, input.formatValue, strings),
|
|
153
|
+
describeMarkers(input.datasource.markers, input.formatValue, strings),
|
|
154
|
+
input.incomplete ? strings['chart.summary.incomplete'] : '',
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
return parts.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Announced for the currently selected point on an adjustable chart. */
|
|
161
|
+
export function buildPointAnnouncement(
|
|
162
|
+
seriesLabel: string,
|
|
163
|
+
positionLabel: string,
|
|
164
|
+
value: ValueState,
|
|
165
|
+
formatValue: (v: ValueState) => string,
|
|
166
|
+
strings: ChartStrings = chartStrings.defaults,
|
|
167
|
+
): string {
|
|
168
|
+
const vars = { position: positionLabel, series: seriesLabel };
|
|
169
|
+
if (value.kind === 'withheld') {
|
|
170
|
+
return formatMessage(strings['chart.point.withheld'], vars);
|
|
171
|
+
}
|
|
172
|
+
if (!isKnown(value)) {
|
|
173
|
+
return formatMessage(strings['chart.point.noData'], vars);
|
|
174
|
+
}
|
|
175
|
+
return formatMessage(strings['chart.point.value'], { ...vars, value: formatValue(value) });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Total plus an explicit note when it is not the whole picture (FR-013). */
|
|
179
|
+
export function buildTotalAnnouncement<M>(
|
|
180
|
+
datasource: ChartDatasource<M>,
|
|
181
|
+
formatValue: (v: ValueState) => string,
|
|
182
|
+
strings: ChartStrings = chartStrings.defaults,
|
|
183
|
+
): string {
|
|
184
|
+
const values = datasource.series.flatMap(s => s.points.map(p => p.value));
|
|
185
|
+
const total = sumKnown(values);
|
|
186
|
+
const value = formatValue({ kind: 'known', value: total.total });
|
|
187
|
+
return total.incomplete
|
|
188
|
+
? formatMessage(strings['chart.total.incomplete'], { value, count: total.excludedPoints })
|
|
189
|
+
: formatMessage(strings['chart.total'], { value });
|
|
190
|
+
}
|