@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 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.2.1",
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 --noEmit",
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",
@@ -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, string> = {
17
- progress: 'progress chart',
18
- distribution: 'distribution chart',
19
- timeSeries: 'time series chart',
20
- bar: 'bar chart',
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, string | undefined> = {
29
- loading: 'Loading data.',
30
- empty: 'No data yet.',
31
- error: 'Data could not be shown.',
32
- stale: 'Showing data that may be out of date.',
33
- noPermission: 'This data is not available to you.',
34
- partial: 'Some data is missing.',
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 `Value ${formatValue({ kind: 'known', value: max.value })}.`;
87
+ return formatMessage(strings['chart.summary.single'], { value: maxText });
81
88
  }
82
89
 
83
- return (
84
- `Highest ${max.label} at ${formatValue({ kind: 'known', value: max.value })}. ` +
85
- `Lowest ${min.label} at ${formatValue({ kind: 'known', value: min.value })}.`
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(`${name} ${formatValue(marker.value)}`);
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 noun = FAMILY_NOUN[input.family];
117
- const heading = input.title ? `${input.title}, ${noun}.` : `${noun}.`;
118
-
119
- const statusPhrase = STATUS_PHRASE[input.status];
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 ? `${seriesCount} series, ${pointCount} points.` : `${pointCount} points.`;
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 ? 'Some values are missing, so totals are 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 `${positionLabel}, ${seriesLabel}, not available.`;
170
+ return formatMessage(strings['chart.point.withheld'], vars);
151
171
  }
152
172
  if (!isKnown(value)) {
153
- return `${positionLabel}, ${seriesLabel}, no data.`;
173
+ return formatMessage(strings['chart.point.noData'], vars);
154
174
  }
155
- return `${positionLabel}, ${seriesLabel}, ${formatValue(value)}.`;
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
- ? `${text}, excluding ${total.excludedPoints} values that are not available.`
169
- : `${text}.`;
188
+ ? formatMessage(strings['chart.total.incomplete'], { value, count: total.excludedPoints })
189
+ : formatMessage(strings['chart.total'], { value });
170
190
  }
@@ -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}). ` +
@@ -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
  }
@@ -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: 'Next data point' },
461
- { name: 'decrement', label: 'Previous data point' },
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 'Loading…';
557
+ return strings['chart.state.loading'];
545
558
  case 'empty':
546
- return 'No data yet';
559
+ return strings['chart.state.empty'];
547
560
  case 'error':
548
- return error?.message ?? 'Unable to show this chart';
561
+ return error?.message ?? strings['chart.state.error'];
549
562
  case 'noPermission':
550
- return 'Not available';
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={entry.visible ? 'Hides this series' : 'Shows this series'}
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 ` +