@chipmobilesdk/rn-chart 0.2.1 → 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 +38 -0
- package/README.md +10 -0
- package/package.json +5 -2
- package/src/a11y/summary.ts +54 -34
- package/src/contract/format.ts +6 -0
- package/src/contract/props.ts +7 -0
- package/src/contract/strings.ts +48 -0
- package/src/index.ts +4 -0
- package/src/react/ChartBoundary.tsx +2 -2
- package/src/react/ChartFrame.tsx +20 -7
- package/src/react/ChartLegend.tsx +6 -1
- package/src/validation/budgets.ts +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @chipmobilesdk/rn-chart
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- One diagnostic stream: install a sink with `installSdkSink` from
|
|
8
|
+
`@chipmobilesdk/rn-core` and it receives what every package observes (events,
|
|
9
|
+
failures, warnings) as `SdkDiagnostic`s, redacted. Listeners are notified through
|
|
10
|
+
rn-core, so a listener that throws no longer stops the others nor fails the call that
|
|
11
|
+
notified, and its error is reported rather than swallowed. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
|
|
12
|
+
|
|
13
|
+
- rn-ads: a banner's auto-refresh is a new cycle, so its `loaded` and `impression`
|
|
14
|
+
events are no longer deduplicated away; the dedupe memory is bounded. A throwing
|
|
15
|
+
status, consent or placement listener is reported as an `internal_error` event.
|
|
16
|
+
- rn-i18n: new `onDiagnostic` config receives every diagnostic (`I18nDiagnostic`,
|
|
17
|
+
with a stable `code`); without it, warnings go to `console.warn` in development
|
|
18
|
+
builds only. The warn-once memory is bounded.
|
|
19
|
+
- rn-chart: now depends on rn-core for its diagnostics.
|
|
20
|
+
- rn-payment: status, entitlement and pending listeners report a throw to the logger
|
|
21
|
+
instead of swallowing it.
|
|
22
|
+
|
|
23
|
+
- Every package's own copy is an rn-core message catalog that an app can translate. ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17))
|
|
24
|
+
|
|
25
|
+
- rn-i18n: new `runtimeEntriesFromCatalog(catalog)` registers any SDK catalog in one
|
|
26
|
+
line, converting `{name}` variables to `{{name}}`.
|
|
27
|
+
- rn-ai: new `messageKeyForError(error)` gives the copy for every error code, and
|
|
28
|
+
`aiMessageSet`. The input placeholder default is neutral ("Type a message…"), and
|
|
29
|
+
two keys were added for errors that had none (`ai.error.signIn`, `ai.error.generic`).
|
|
30
|
+
- rn-auth: new `authMessages`, the catalog split into user and developer copy.
|
|
31
|
+
- rn-payment: new `paymentMessages`.
|
|
32
|
+
- rn-chart: summaries, announcements, adjustable-action labels, built-in state text
|
|
33
|
+
and legend hints come from `chartStrings`; pass `strings` to translate them. The
|
|
34
|
+
`accessibility.summaryFormatter` prop, declared before, now replaces the summary.
|
|
35
|
+
|
|
36
|
+
### Patch Changes
|
|
37
|
+
|
|
38
|
+
- Updated dependencies ([#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17), [#17](https://github.com/operationd8979/chip-mobile-sdk/pull/17)):
|
|
39
|
+
- @chipmobilesdk/rn-core@0.1.0
|
|
40
|
+
|
|
3
41
|
## 0.2.1
|
|
4
42
|
|
|
5
43
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -285,6 +285,16 @@ points — the same access touch gives.
|
|
|
285
285
|
Non-ready charts announce their state rather than reading out figures they are
|
|
286
286
|
not showing. Accessibility props you supply are never overridden.
|
|
287
287
|
|
|
288
|
+
Everything the package says or shows itself (summaries, point announcements, the
|
|
289
|
+
adjustable actions, built-in state text, legend hints) comes from `chartStrings`,
|
|
290
|
+
with English defaults. Pass `strings` to speak the app's language; any key you
|
|
291
|
+
leave out keeps its default. `accessibility.summaryFormatter` replaces the composed
|
|
292
|
+
summary entirely, and `accessibility.label` replaces everything.
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
<BarChart datasource={steps} title="Bước" strings={{ 'chart.family.bar': 'biểu đồ cột' }} />
|
|
296
|
+
```
|
|
297
|
+
|
|
288
298
|
Axis labels are thinned to fit rather than overlapped, and scale with the system
|
|
289
299
|
text size up to **1.6×**; beyond that the growth is capped so the plot area
|
|
290
300
|
survives.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chipmobilesdk/rn-chart",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -26,9 +26,12 @@
|
|
|
26
26
|
],
|
|
27
27
|
"sideEffects": false,
|
|
28
28
|
"scripts": {
|
|
29
|
-
"typecheck": "tsc -p tsconfig.json
|
|
29
|
+
"typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.test.json",
|
|
30
30
|
"pack:check": "npm pack --dry-run"
|
|
31
31
|
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@chipmobilesdk/rn-core": "^0.1.0"
|
|
34
|
+
},
|
|
32
35
|
"peerDependencies": {
|
|
33
36
|
"react": ">=19",
|
|
34
37
|
"react-native": ">=0.85",
|
package/src/a11y/summary.ts
CHANGED
|
@@ -11,27 +11,30 @@
|
|
|
11
11
|
import type { ChartDatasource, ChartFamily } from '../contract/datasource';
|
|
12
12
|
import type { ReferenceMarker } from '../contract/marker';
|
|
13
13
|
import type { ChartStatus } from '../contract/state';
|
|
14
|
+
import { formatMessage } from '@chipmobilesdk/rn-core';
|
|
15
|
+
|
|
16
|
+
import { chartStrings, type ChartStrings } from '../contract/strings';
|
|
14
17
|
import { isKnown, sumKnown, type ValueState } from '../contract/value';
|
|
15
18
|
|
|
16
|
-
const FAMILY_NOUN: Record<ChartFamily,
|
|
17
|
-
progress: 'progress
|
|
18
|
-
distribution: 'distribution
|
|
19
|
-
timeSeries: '
|
|
20
|
-
bar: 'bar
|
|
21
|
-
heatmap: 'heatmap',
|
|
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',
|
|
22
25
|
};
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
28
|
* What a screen reader hears when the chart is not showing data. Announcing
|
|
26
29
|
* the state beats silence, and beats announcing a stale value as if current.
|
|
27
30
|
*/
|
|
28
|
-
const STATUS_PHRASE: Record<ChartStatus,
|
|
29
|
-
loading: '
|
|
30
|
-
empty: '
|
|
31
|
-
error: '
|
|
32
|
-
stale: '
|
|
33
|
-
noPermission: '
|
|
34
|
-
partial: '
|
|
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',
|
|
35
38
|
ready: undefined,
|
|
36
39
|
};
|
|
37
40
|
|
|
@@ -42,11 +45,14 @@ export interface SummaryInput<M = unknown> {
|
|
|
42
45
|
readonly title?: string;
|
|
43
46
|
readonly formatValue: (value: ValueState) => string;
|
|
44
47
|
readonly incomplete?: boolean;
|
|
48
|
+
/** The texts to speak; the English defaults when omitted. */
|
|
49
|
+
readonly strings?: ChartStrings;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
function describeExtremes<M>(
|
|
48
53
|
datasource: ChartDatasource<M>,
|
|
49
54
|
formatValue: (v: ValueState) => string,
|
|
55
|
+
strings: ChartStrings,
|
|
50
56
|
): string {
|
|
51
57
|
let min: { label: string; value: number } | undefined;
|
|
52
58
|
let max: { label: string; value: number } | undefined;
|
|
@@ -76,19 +82,23 @@ function describeExtremes<M>(
|
|
|
76
82
|
if (!min || !max) {
|
|
77
83
|
return '';
|
|
78
84
|
}
|
|
85
|
+
const maxText = formatValue({ kind: 'known', value: max.value });
|
|
79
86
|
if (min.label === max.label) {
|
|
80
|
-
return
|
|
87
|
+
return formatMessage(strings['chart.summary.single'], { value: maxText });
|
|
81
88
|
}
|
|
82
89
|
|
|
83
|
-
return (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
});
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
function describeMarkers(
|
|
90
99
|
markers: readonly ReferenceMarker[] | undefined,
|
|
91
100
|
formatValue: (v: ValueState) => string,
|
|
101
|
+
strings: ChartStrings,
|
|
92
102
|
): string {
|
|
93
103
|
if (!markers || markers.length === 0) {
|
|
94
104
|
return '';
|
|
@@ -100,7 +110,9 @@ function describeMarkers(
|
|
|
100
110
|
continue;
|
|
101
111
|
}
|
|
102
112
|
const name = marker.label ?? marker.kind;
|
|
103
|
-
parts.push(
|
|
113
|
+
parts.push(
|
|
114
|
+
formatMessage(strings['chart.summary.marker'], { name, value: formatValue(marker.value) }),
|
|
115
|
+
);
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
return parts.length > 0 ? `${parts.join('. ')}.` : '';
|
|
@@ -113,10 +125,14 @@ function describeMarkers(
|
|
|
113
125
|
* so a loading chart never announces figures it is not showing.
|
|
114
126
|
*/
|
|
115
127
|
export function buildChartSummary<M>(input: SummaryInput<M>): string {
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
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;
|
|
120
136
|
if (input.status !== 'ready' && input.status !== 'partial' && input.status !== 'stale') {
|
|
121
137
|
return `${heading} ${statusPhrase ?? ''}`.trim();
|
|
122
138
|
}
|
|
@@ -125,15 +141,17 @@ export function buildChartSummary<M>(input: SummaryInput<M>): string {
|
|
|
125
141
|
const pointCount = input.datasource.series.reduce((acc, s) => acc + s.points.length, 0);
|
|
126
142
|
|
|
127
143
|
const scale =
|
|
128
|
-
seriesCount > 1
|
|
144
|
+
seriesCount > 1
|
|
145
|
+
? formatMessage(strings['chart.summary.scale'], { series: seriesCount, points: pointCount })
|
|
146
|
+
: formatMessage(strings['chart.summary.scaleSingle'], { points: pointCount });
|
|
129
147
|
|
|
130
148
|
const parts = [
|
|
131
149
|
heading,
|
|
132
150
|
statusPhrase,
|
|
133
151
|
scale,
|
|
134
|
-
describeExtremes(input.datasource, input.formatValue),
|
|
135
|
-
describeMarkers(input.datasource.markers, input.formatValue),
|
|
136
|
-
input.incomplete ? '
|
|
152
|
+
describeExtremes(input.datasource, input.formatValue, strings),
|
|
153
|
+
describeMarkers(input.datasource.markers, input.formatValue, strings),
|
|
154
|
+
input.incomplete ? strings['chart.summary.incomplete'] : '',
|
|
137
155
|
];
|
|
138
156
|
|
|
139
157
|
return parts.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
|
|
@@ -145,26 +163,28 @@ export function buildPointAnnouncement(
|
|
|
145
163
|
positionLabel: string,
|
|
146
164
|
value: ValueState,
|
|
147
165
|
formatValue: (v: ValueState) => string,
|
|
166
|
+
strings: ChartStrings = chartStrings.defaults,
|
|
148
167
|
): string {
|
|
168
|
+
const vars = { position: positionLabel, series: seriesLabel };
|
|
149
169
|
if (value.kind === 'withheld') {
|
|
150
|
-
return
|
|
170
|
+
return formatMessage(strings['chart.point.withheld'], vars);
|
|
151
171
|
}
|
|
152
172
|
if (!isKnown(value)) {
|
|
153
|
-
return
|
|
173
|
+
return formatMessage(strings['chart.point.noData'], vars);
|
|
154
174
|
}
|
|
155
|
-
return
|
|
175
|
+
return formatMessage(strings['chart.point.value'], { ...vars, value: formatValue(value) });
|
|
156
176
|
}
|
|
157
177
|
|
|
158
178
|
/** Total plus an explicit note when it is not the whole picture (FR-013). */
|
|
159
179
|
export function buildTotalAnnouncement<M>(
|
|
160
180
|
datasource: ChartDatasource<M>,
|
|
161
181
|
formatValue: (v: ValueState) => string,
|
|
182
|
+
strings: ChartStrings = chartStrings.defaults,
|
|
162
183
|
): string {
|
|
163
184
|
const values = datasource.series.flatMap(s => s.points.map(p => p.value));
|
|
164
185
|
const total = sumKnown(values);
|
|
165
|
-
|
|
166
|
-
const text = `Total ${formatValue({ kind: 'known', value: total.total })}`;
|
|
186
|
+
const value = formatValue({ kind: 'known', value: total.total });
|
|
167
187
|
return total.incomplete
|
|
168
|
-
?
|
|
169
|
-
:
|
|
188
|
+
? formatMessage(strings['chart.total.incomplete'], { value, count: total.excludedPoints })
|
|
189
|
+
: formatMessage(strings['chart.total'], { value });
|
|
170
190
|
}
|
package/src/contract/format.ts
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
* applies a documented locale-neutral default, and uses app-supplied formatter
|
|
6
6
|
* output verbatim whenever one is given.
|
|
7
7
|
*/
|
|
8
|
+
import { createDiagnosticChannel } from '@chipmobilesdk/rn-core';
|
|
9
|
+
|
|
8
10
|
import type { Position, Series } from './datasource';
|
|
9
11
|
import { isKnown, type ValueState } from './value';
|
|
10
12
|
|
|
13
|
+
/** Chart diagnostics also reach the app's SDK sinks (`installSdkSink`, CORE-08). */
|
|
14
|
+
const channel = createDiagnosticChannel<'formatter_threw'>('rn-chart');
|
|
15
|
+
|
|
11
16
|
export type FormatHint =
|
|
12
17
|
| { readonly kind: 'number'; readonly decimals?: number }
|
|
13
18
|
| { readonly kind: 'percent'; readonly decimals?: number }
|
|
@@ -144,6 +149,7 @@ function warnFormatterFailureOnce(
|
|
|
144
149
|
return;
|
|
145
150
|
}
|
|
146
151
|
warnedFormatters.add(key);
|
|
152
|
+
channel.emit('formatter_threw', 'warn', { slot });
|
|
147
153
|
const reason = error instanceof Error ? error.message : 'unknown error';
|
|
148
154
|
console.warn(
|
|
149
155
|
`[rn-chart] A supplied "${slot}" formatter threw (${reason}). ` +
|
package/src/contract/props.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* See specs/007-rn-chart-package/contracts/public-api.md §2 for the contract
|
|
5
5
|
* behind each field.
|
|
6
6
|
*/
|
|
7
|
+
import type { ChartStrings } from './strings';
|
|
7
8
|
import type { ReactNode } from 'react';
|
|
8
9
|
import type { StyleProp, ViewStyle } from 'react-native';
|
|
9
10
|
|
|
@@ -46,6 +47,12 @@ export interface BaseChartProps<M = unknown> extends InteractionProps<M> {
|
|
|
46
47
|
readonly legend?: boolean | { readonly position: 'top' | 'bottom' };
|
|
47
48
|
|
|
48
49
|
readonly accessibility?: ChartAccessibility<M>;
|
|
50
|
+
/**
|
|
51
|
+
* The package's own texts in the app's language: summaries, announcements,
|
|
52
|
+
* action labels, built-in state text, legend hints (`chartStrings` holds the
|
|
53
|
+
* keys and English defaults).
|
|
54
|
+
*/
|
|
55
|
+
readonly strings?: Partial<ChartStrings>;
|
|
49
56
|
|
|
50
57
|
/** Width always comes from the container. */
|
|
51
58
|
readonly height?: number;
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export type ChartStrings = typeof chartStrings.defaults;
|
|
48
|
+
export type ChartStringKey = keyof ChartStrings;
|
package/src/index.ts
CHANGED
|
@@ -156,3 +156,7 @@ export { ChartThemeProvider, useChartTheme } from './react/ChartThemeProvider';
|
|
|
156
156
|
// Accessibility helpers
|
|
157
157
|
// ---------------------------------------------------------------------------
|
|
158
158
|
export { buildChartSummary, buildPointAnnouncement, buildTotalAnnouncement } from './a11y/summary';
|
|
159
|
+
|
|
160
|
+
// The package's own texts, for translation (CHT-05).
|
|
161
|
+
export { chartStrings } from './contract/strings';
|
|
162
|
+
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
|
}
|
package/src/react/ChartFrame.tsx
CHANGED
|
@@ -28,6 +28,7 @@ import { EMPTY_GEOMETRY } from '../geometry/types';
|
|
|
28
28
|
import { releaseBudgetWarning, warnIfOverBudget } from '../validation/budgets';
|
|
29
29
|
import { validateDatasource } from '../validation/validateDatasource';
|
|
30
30
|
import { buildChartSummary, buildPointAnnouncement } from '../a11y/summary';
|
|
31
|
+
import { chartStrings, type ChartStrings } from '../contract/strings';
|
|
31
32
|
import { Grid, XAxis, YAxis } from '../render/Axis';
|
|
32
33
|
import {
|
|
33
34
|
GeometryLabels,
|
|
@@ -59,6 +60,7 @@ const DEFAULT_HEIGHT = 220;
|
|
|
59
60
|
export function ChartFrame<M>(props: ChartFrameProps<M>) {
|
|
60
61
|
const family = props.family;
|
|
61
62
|
const theme = useChartTheme(props.theme);
|
|
63
|
+
const strings = useMemo(() => chartStrings.resolve(props.strings), [props.strings]);
|
|
62
64
|
const generatedId = useId();
|
|
63
65
|
const instanceKey = props.chartId ?? generatedId;
|
|
64
66
|
|
|
@@ -296,6 +298,11 @@ export function ChartFrame<M>(props: ChartFrameProps<M>) {
|
|
|
296
298
|
if (props.accessibility?.label) {
|
|
297
299
|
return props.accessibility.label;
|
|
298
300
|
}
|
|
301
|
+
// The app's own composer, when given (CHT-04): it was declared and never used.
|
|
302
|
+
const custom = props.accessibility?.summaryFormatter;
|
|
303
|
+
if (custom) {
|
|
304
|
+
return custom(props.datasource, { slot: 'summary', hint: props.datasource.format });
|
|
305
|
+
}
|
|
299
306
|
return buildChartSummary({
|
|
300
307
|
datasource: props.datasource,
|
|
301
308
|
family: props.family,
|
|
@@ -303,8 +310,10 @@ export function ChartFrame<M>(props: ChartFrameProps<M>) {
|
|
|
303
310
|
title: props.title,
|
|
304
311
|
formatValue,
|
|
305
312
|
incomplete: geometry.incomplete || validation.totalIncomplete,
|
|
313
|
+
strings,
|
|
306
314
|
});
|
|
307
315
|
}, [
|
|
316
|
+
strings,
|
|
308
317
|
props.accessibility,
|
|
309
318
|
props.datasource,
|
|
310
319
|
props.family,
|
|
@@ -346,7 +355,7 @@ export function ChartFrame<M>(props: ChartFrameProps<M>) {
|
|
|
346
355
|
}
|
|
347
356
|
// Minimal built-in wording, deliberately terse. Apps are expected to
|
|
348
357
|
// supply their own copy — the package must not impose product voice.
|
|
349
|
-
const fallback = builtInStateText(status, currentError);
|
|
358
|
+
const fallback = builtInStateText(status, strings, currentError);
|
|
350
359
|
if (!fallback) {
|
|
351
360
|
return null;
|
|
352
361
|
}
|
|
@@ -435,6 +444,7 @@ export function ChartFrame<M>(props: ChartFrameProps<M>) {
|
|
|
435
444
|
entries={legendEntries}
|
|
436
445
|
theme={theme}
|
|
437
446
|
onToggle={visibility.enabled ? visibility.toggle : undefined}
|
|
447
|
+
strings={strings}
|
|
438
448
|
/>
|
|
439
449
|
) : null}
|
|
440
450
|
|
|
@@ -450,6 +460,7 @@ export function ChartFrame<M>(props: ChartFrameProps<M>) {
|
|
|
450
460
|
formatPositionDefault(selectedPoint.point.position),
|
|
451
461
|
selectedPoint.point.value,
|
|
452
462
|
formatValue,
|
|
463
|
+
strings,
|
|
453
464
|
),
|
|
454
465
|
}
|
|
455
466
|
: undefined
|
|
@@ -457,8 +468,8 @@ export function ChartFrame<M>(props: ChartFrameProps<M>) {
|
|
|
457
468
|
accessibilityActions={
|
|
458
469
|
interactive
|
|
459
470
|
? [
|
|
460
|
-
{ name: 'increment', label: '
|
|
461
|
-
{ name: 'decrement', label: '
|
|
471
|
+
{ name: 'increment', label: strings['chart.action.next'] },
|
|
472
|
+
{ name: 'decrement', label: strings['chart.action.previous'] },
|
|
462
473
|
]
|
|
463
474
|
: undefined
|
|
464
475
|
}
|
|
@@ -472,6 +483,7 @@ export function ChartFrame<M>(props: ChartFrameProps<M>) {
|
|
|
472
483
|
entries={legendEntries}
|
|
473
484
|
theme={theme}
|
|
474
485
|
onToggle={visibility.enabled ? visibility.toggle : undefined}
|
|
486
|
+
strings={strings}
|
|
475
487
|
/>
|
|
476
488
|
) : null}
|
|
477
489
|
|
|
@@ -537,17 +549,18 @@ export function ChartFrame<M>(props: ChartFrameProps<M>) {
|
|
|
537
549
|
|
|
538
550
|
function builtInStateText(
|
|
539
551
|
status: ReturnType<typeof resolveStatus>,
|
|
552
|
+
strings: ChartStrings,
|
|
540
553
|
error?: ChartError,
|
|
541
554
|
): string | undefined {
|
|
542
555
|
switch (status) {
|
|
543
556
|
case 'loading':
|
|
544
|
-
return '
|
|
557
|
+
return strings['chart.state.loading'];
|
|
545
558
|
case 'empty':
|
|
546
|
-
return '
|
|
559
|
+
return strings['chart.state.empty'];
|
|
547
560
|
case 'error':
|
|
548
|
-
return error?.message ?? '
|
|
561
|
+
return error?.message ?? strings['chart.state.error'];
|
|
549
562
|
case 'noPermission':
|
|
550
|
-
return '
|
|
563
|
+
return strings['chart.state.noPermission'];
|
|
551
564
|
default:
|
|
552
565
|
return undefined;
|
|
553
566
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* carries a dash pattern as well as a colour, so series stay distinguishable
|
|
7
7
|
* without colour perception.
|
|
8
8
|
*/
|
|
9
|
+
import { chartStrings, type ChartStrings } from '../contract/strings';
|
|
9
10
|
import React from 'react';
|
|
10
11
|
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
11
12
|
|
|
@@ -117,10 +118,12 @@ export function ChartLegend({
|
|
|
117
118
|
entries,
|
|
118
119
|
theme,
|
|
119
120
|
onToggle,
|
|
121
|
+
strings = chartStrings.defaults,
|
|
120
122
|
}: {
|
|
121
123
|
readonly entries: readonly LegendEntry[];
|
|
122
124
|
readonly theme: ChartTheme;
|
|
123
125
|
readonly onToggle?: (seriesId: string) => void;
|
|
126
|
+
readonly strings?: ChartStrings;
|
|
124
127
|
}) {
|
|
125
128
|
if (entries.length === 0) {
|
|
126
129
|
return null;
|
|
@@ -162,7 +165,9 @@ export function ChartLegend({
|
|
|
162
165
|
accessibilityRole="switch"
|
|
163
166
|
accessibilityState={{ checked: entry.visible }}
|
|
164
167
|
accessibilityLabel={entry.label}
|
|
165
|
-
accessibilityHint={
|
|
168
|
+
accessibilityHint={
|
|
169
|
+
entry.visible ? strings['chart.legend.hide'] : strings['chart.legend.show']
|
|
170
|
+
}
|
|
166
171
|
style={[styles.entry, styles.tappable]}
|
|
167
172
|
>
|
|
168
173
|
{content}
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* specs/007-rn-chart-package/research.md R2 — element count scales with series,
|
|
15
15
|
* not with points, so these budgets are about JS work rather than view count.
|
|
16
16
|
*/
|
|
17
|
+
import { createDiagnosticChannel } from '@chipmobilesdk/rn-core';
|
|
18
|
+
|
|
17
19
|
import type { ChartDatasource, ChartFamily } from '../contract/datasource';
|
|
18
20
|
|
|
19
21
|
export interface DatasetBudget {
|
|
@@ -63,6 +65,9 @@ export function checkBudget<M>(datasource: ChartDatasource<M>, family: ChartFami
|
|
|
63
65
|
|
|
64
66
|
const warnedCharts = new Set<string>();
|
|
65
67
|
|
|
68
|
+
/** Chart diagnostics also reach the app's SDK sinks (`installSdkSink`, CORE-08). */
|
|
69
|
+
const channel = createDiagnosticChannel<'over_budget'>('rn-chart');
|
|
70
|
+
|
|
66
71
|
/**
|
|
67
72
|
* Warns once per chart instance per mount. De-duplicating matters: a scrolling
|
|
68
73
|
* dashboard would otherwise flood the console on every frame.
|
|
@@ -76,6 +81,11 @@ export function warnIfOverBudget<M>(
|
|
|
76
81
|
|
|
77
82
|
if (__DEV__ && !check.withinBudget && !warnedCharts.has(instanceKey)) {
|
|
78
83
|
warnedCharts.add(instanceKey);
|
|
84
|
+
channel.emit('over_budget', 'warn', {
|
|
85
|
+
family,
|
|
86
|
+
series: check.seriesCount,
|
|
87
|
+
maxPoints: check.maxPointsInAnySeries,
|
|
88
|
+
});
|
|
79
89
|
const budget = DATASET_BUDGETS[family];
|
|
80
90
|
console.warn(
|
|
81
91
|
`[rn-chart] ${family} chart "${instanceKey}" exceeds its documented dataset budget ` +
|