@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +126 -115
  2. package/README.md +381 -381
  3. package/package.json +46 -49
  4. package/skills/sdk-rn-chart/SKILL.md +68 -69
  5. package/src/a11y/summary.ts +170 -177
  6. package/src/contract/datasource.ts +126 -136
  7. package/src/contract/errors.ts +129 -133
  8. package/src/contract/format.ts +162 -168
  9. package/src/contract/interaction.ts +70 -71
  10. package/src/contract/marker.ts +75 -75
  11. package/src/contract/props.ts +57 -60
  12. package/src/contract/state.ts +88 -94
  13. package/src/contract/theme.ts +169 -177
  14. package/src/contract/value.ts +121 -125
  15. package/src/geometry/bar.ts +271 -273
  16. package/src/geometry/distribution.ts +197 -203
  17. package/src/geometry/heatmap.ts +189 -195
  18. package/src/geometry/hitTest.ts +117 -120
  19. package/src/geometry/layout.ts +162 -173
  20. package/src/geometry/markers.ts +55 -57
  21. package/src/geometry/path.ts +193 -199
  22. package/src/geometry/progress.ts +223 -230
  23. package/src/geometry/scale.ts +140 -146
  24. package/src/geometry/ticks.ts +145 -153
  25. package/src/geometry/timeSeries.ts +168 -168
  26. package/src/geometry/types.ts +94 -94
  27. package/src/index.ts +158 -176
  28. package/src/react/ChartBoundary.tsx +96 -96
  29. package/src/react/ChartFrame.tsx +583 -622
  30. package/src/react/ChartLegend.tsx +211 -215
  31. package/src/react/ChartThemeProvider.tsx +44 -48
  32. package/src/react/charts.tsx +154 -161
  33. package/src/react/useChartLayout.ts +124 -124
  34. package/src/react/useChartSelection.ts +152 -170
  35. package/src/render/Axis.tsx +120 -122
  36. package/src/render/Marks.tsx +173 -205
  37. package/src/validation/budgets.ts +103 -110
  38. package/src/validation/validateDatasource.ts +174 -174
package/README.md CHANGED
@@ -1,381 +1,381 @@
1
- # @chipmobilesdk/rn-chart
2
-
3
- Domain-agnostic charts for ChipMobileSdk-family React Native apps.
4
-
5
- > **License:** `UNLICENSED`. Published publicly for use by the owner's
6
- > applications; no open-source license is granted.
7
-
8
- - **Version**: `0.2.x` (see [CHANGELOG](CHANGELOG.md))
9
- - **Entrypoint**: `@chipmobilesdk/rn-chart`
10
-
11
- One datasource contract spans five chart families. The package knows nothing
12
- about nutrition, finance, health, or productivity — you hand it aggregated,
13
- normalised data and it draws it.
14
-
15
- ## What it does not do
16
-
17
- Worth stating up front, because these are deliberate:
18
-
19
- - **No insight or narrative surface.** The package draws charts. Render your own
20
- explanatory text above or below a chart; it imposes no outer margin that would
21
- fight your layout.
22
- - **No data fetching, no database access, no AI calls, no network of any kind.**
23
- Everything arrives as props.
24
- - **No aggregation.** Your domain layer decides what the numbers mean.
25
- - **No navigation.** Interactions call back; what happens next is yours.
26
-
27
- ## Install
28
-
29
- ```bash
30
- npm install @chipmobilesdk/rn-chart react-native-svg
31
- cd ios && pod install
32
- ```
33
-
34
- `react-native-svg` is a required peer. There is no degraded no-SVG mode.
35
-
36
- No permissions, entitlements, `Info.plist` usage strings, or Android manifest
37
- changes are needed. Note that this package's **no-permission chart state** is an
38
- app-level data-access concept, not an operating-system permission.
39
-
40
- ## Compatibility
41
-
42
- | | Supported |
43
- |---|---|
44
- | React | 19.2+ |
45
- | React Native | 0.85+ |
46
- | `react-native-svg` | 15.15+ |
47
- | Android | `minSdkVersion` 24, `targetSdkVersion` 36 |
48
- | iOS | 15.1+ |
49
-
50
- ## Quick start
51
-
52
- ```tsx
53
- import { BarChart, known, unknown, type ChartDatasource } from '@chipmobilesdk/rn-chart';
54
-
55
- const datasource: ChartDatasource = {
56
- positionKind: 'category',
57
- series: [{
58
- id: 'weekly',
59
- label: 'Weekly total',
60
- points: [
61
- { id: 'mon', position: { kind: 'category', category: 'Mon' }, value: known(12) },
62
- { id: 'tue', position: { kind: 'category', category: 'Tue' }, value: known(0) },
63
- { id: 'wed', position: { kind: 'category', category: 'Wed' }, value: unknown() },
64
- ],
65
- }],
66
- };
67
-
68
- export function Example() {
69
- return <BarChart datasource={datasource} height={220} title="This week" />;
70
- }
71
- ```
72
-
73
- Monday draws a bar. Tuesday draws a **zero-height bar that reads as a real
74
- zero**. Wednesday draws **nothing and is marked as a gap** — never a zero.
75
-
76
- > **Keep your datasource referentially stable.** Hold it in state or a `useMemo`.
77
- > Rebuilding the object inline on every render defeats the memoization the
78
- > package relies on, and that is not something a library can fix for you.
79
-
80
- ## Value semantics
81
-
82
- The most important part of the contract. Four classes, and only one carries a
83
- number:
84
-
85
- ```ts
86
- known(42) // a real observation, including known(0)
87
- unknown() // we do not know
88
- notApplicable() // does not apply here
89
- withheld() // hidden: permission, or not enough data
90
- ```
91
-
92
- There is no `value` property on the three non-value cases, so `value ?? 0`
93
- cannot compile. That is the point — turning a gap into a zero is the single most
94
- damaging thing a charting library can do to someone's data.
95
-
96
- Consequences you can rely on:
97
-
98
- - An unknown value is never rendered as zero and never interpolated across. In a
99
- line chart the gap ends the subpath, so there is physically no segment to
100
- bridge it.
101
- - `notApplicable` and `withheld` are excluded from totals, and any total that
102
- excluded something is reported as incomplete.
103
- - A withheld value never appears in a rendering, a label, an accessible summary,
104
- or an interaction callback.
105
- - Each non-value class renders as its own **texture**, not a colour difference,
106
- so they stay distinguishable in greyscale.
107
-
108
- ## The datasource contract
109
-
110
- Every point shares one envelope — identity, value, optional unit and format
111
- hint, opaque metadata — and only its **position** varies:
112
-
113
- ```ts
114
- { kind: 'category', category: 'Mon' }
115
- { kind: 'instant', at: 1767225600000 } // epoch ms, UTC
116
- { kind: 'matrix', row: 'week-1', column: 'Mon' }
117
- ```
118
-
119
- Which families accept which:
120
-
121
- | Family | `category` | `instant` | `matrix` |
122
- |---|:---:|:---:|:---:|
123
- | `ProgressChart` | ✓ | | |
124
- | `DistributionChart` | ✓ | | |
125
- | `BarChart` | ✓ | ✓ | |
126
- | `TimeSeriesChart` | | ✓ | |
127
- | `HeatmapChart` | | | ✓ |
128
-
129
- Bar accepting both is what makes swapping a time-series chart for a bar chart
130
- free — same datasource, different component. Any other pairing is a **validation
131
- error**, not a rendering guess.
132
-
133
- Time is epoch milliseconds and carries no timezone. All date wording comes from
134
- your formatters.
135
-
136
- ### Reference markers
137
-
138
- `datasource.markers` holds targets, baselines and thresholds. Those with a known
139
- value are drawn as dashed lines:
140
-
141
- - `BarChart` and `TimeSeriesChart` draw each one across the plot, with its
142
- `label` at the right end. The value axis is stretched to include it, so a
143
- threshold above every data point is still on the chart.
144
- - `ProgressChart` draws the target on each row's bar. A value past its target
145
- is drawn as overshoot beyond that line, inside the plot: the track is scaled
146
- so the target sits at `1 / min(largest proportion, 1.25)` of its width.
147
- - `DistributionChart` and `HeatmapChart` ignore markers.
148
-
149
- Axis labels have room reserved for them: the plot shrinks to fit the widest
150
- value label on the left and one line of time or category labels below, so no
151
- label is clipped by the chart's edge.
152
-
153
- ## States
154
-
155
- Seven, and the package derives the two that only the data can determine:
156
-
157
- | Status | Who decides |
158
- |---|---|
159
- | `empty`, `partial` | derived from your datasource |
160
- | `loading`, `stale`, `noPermission`, `error` | you declare via `status` |
161
-
162
- Precedence: `noPermission > error > loading > empty > stale > partial > ready`.
163
- `noPermission` outranks everything so restricted values cannot slip through
164
- another state.
165
-
166
- Supply your own copy — the package's built-in wording is a terse placeholder,
167
- not product voice:
168
-
169
- ```tsx
170
- <BarChart
171
- datasource={ds}
172
- status="loading"
173
- stateContent={{
174
- empty: <Text>Log your first meal to see this chart</Text>,
175
- loading: <Spinner />,
176
- }}
177
- />
178
- ```
179
-
180
- ### Failure containment
181
-
182
- Each chart contains its own render exceptions. One bad datasource collapses one
183
- chart, never the dashboard around it:
184
-
185
- ```tsx
186
- <BarChart datasource={ds} chartId="calories" onFailure={report => crashReporter.log(report)} />
187
- ```
188
-
189
- Failure reports carry the chart identity, family, and a catalogue message —
190
- **never data values or series labels**. This matters if you chart health or
191
- financial data, because a report is the easiest accidental path from a chart
192
- into a third-party crash reporting service.
193
-
194
- ## Interaction
195
-
196
- Uncontrolled by default. Supply a `selection` value (including `null`) to take
197
- ownership; the same callbacks fire either way.
198
-
199
- ```tsx
200
- // Uncontrolled: the chart tracks its own selection.
201
- <BarChart datasource={ds} onSelectionChange={(ref, event) => log(event)} />
202
-
203
- // Controlled: you are the source of truth.
204
- <BarChart
205
- datasource={ds}
206
- selection={selection}
207
- onSelectionChange={setSelectionFromRef}
208
- onSelectionUnresolvable={ref => setSelection(null)}
209
- />
210
- ```
211
-
212
- Every payload carries the point id, series id, value, position, and whatever
213
- metadata you attached. The package never navigates, fetches, or mutates.
214
-
215
- Legends can also toggle series visibility. The state is uncontrolled unless
216
- you supply `hiddenSeriesIds`:
217
-
218
- ```tsx
219
- <BarChart
220
- datasource={ds}
221
- seriesToggleEnabled
222
- hiddenSeriesIds={hiddenSeriesIds}
223
- onVisibilityChange={event => updateHiddenSeries(event.seriesId)}
224
- />
225
- ```
226
-
227
- Pan, pinch-zoom, and brush-to-select are out of scope. `range` on
228
- `TimeSeriesChart` is a window **you** supply, not one the user draws.
229
-
230
- ## Theming
231
-
232
- The package defines its own `ChartTheme` value type and takes no dependency on
233
- `@chipmobilesdk/rn-theme` or `react-native-unistyles`. Any design system can
234
- satisfy it:
235
-
236
- ```tsx
237
- import { ChartThemeProvider, defaultChartTheme } from '@chipmobilesdk/rn-chart';
238
-
239
- const chartTheme = {
240
- ...defaultChartTheme(scheme),
241
- palette: myTokens.dataviz,
242
- surface: myTokens.surface,
243
- };
244
-
245
- <ChartThemeProvider theme={chartTheme}>{children}</ChartThemeProvider>
246
- ```
247
-
248
- Resolution order: explicit `theme` prop → provider context → built-in default
249
- keyed on `useColorScheme()`. With no configuration at all, charts still render
250
- correctly in light and dark.
251
-
252
- Meaning never rides on colour alone: series carry dash patterns as well as
253
- colours, and non-value classes use textures.
254
-
255
- `DistributionChart` colours **segments**, not series. Palette entries are taken
256
- in order across every point, so one series of four points and four series of
257
- one point both give four colours. A point's own `color` overrides its segment;
258
- a series with a single point is that segment, so its series `color` applies.
259
- The legend has one entry per segment, in the same colour as its arc.
260
-
261
- ## Formatting
262
-
263
- ```tsx
264
- <TimeSeriesChart
265
- datasource={ds}
266
- formatters={{
267
- value: v => (v.kind === 'known' ? intl.number(v.value) : '—'),
268
- axisLabel: p => ('at' in p ? intl.date(p.at) : String(p)),
269
- }}
270
- />
271
- ```
272
-
273
- Your output is used verbatim. With no formatter, a documented locale-neutral
274
- default applies — the package never infers or converts a business unit. A
275
- formatter that throws degrades to the default **for that one label** and does
276
- not crash the screen.
277
-
278
- ## Accessibility
279
-
280
- Each chart is a single accessible element with a composed summary: what it is,
281
- what it shows, where the extremes are, what any target says. With selection
282
- enabled the root becomes `adjustable`, so increment and decrement walk the data
283
- points — the same access touch gives.
284
-
285
- Non-ready charts announce their state rather than reading out figures they are
286
- not showing. Accessibility props you supply are never overridden.
287
-
288
- Axis labels are thinned to fit rather than overlapped, and scale with the system
289
- text size up to **1.6×**; beyond that the growth is capped so the plot area
290
- survives.
291
-
292
- ## Dataset budgets
293
-
294
- | Family | Budget |
295
- |---|---|
296
- | Time series | 5 series × 500 points |
297
- | Bar | 5 series × 60 categories |
298
- | Distribution | 12 segments |
299
- | Progress | 8 metrics |
300
- | Heatmap | 1,000 cells |
301
-
302
- Over budget, a chart **still renders and drops nothing**. It warns once per
303
- chart in development builds, and never in production. Aggregating or
304
- downsampling above these sizes is your call — the package will not silently
305
- decide what data to discard.
306
-
307
- Element count scales with series, not points: an at-budget time-series chart
308
- emits about six SVG elements, not 2,500.
309
-
310
- ## Data behaviour (for App Privacy and Data Safety)
311
-
312
- For consuming apps completing Apple App Privacy or Google Play Data Safety
313
- declarations:
314
-
315
- - The package holds the values and labels you pass it **in memory only**, for the
316
- lifetime of the rendered component.
317
- - It collects nothing, generates nothing, persists nothing, transmits nothing,
318
- and shares nothing with third parties.
319
- - It logs no data point value and no series label in production builds,
320
- including inside failure reports.
321
- - It requests no permissions and reads no device identifiers.
322
-
323
- **This package introduces no data-collection declaration of its own.** If your
324
- app sends charted data anywhere, that collection is yours to declare.
325
-
326
- If you render your own narrative text near a chart — particularly text from an
327
- AI service or from users — moderation and content obligations are entirely
328
- yours. The package ships no free-text surface at all.
329
-
330
- ## Public API
331
-
332
- `src/index.ts` is the only entrypoint. Geometry, scales, layout maths, path
333
- builders, the chart shell, and the SVG adapter are private so they can change
334
- without a breaking release.
335
-
336
- **Stable**: value semantics, `Position`, the datasource types, the five chart
337
- components and their base props, `ChartStatus`, the error types, interaction
338
- events.
339
-
340
- The single entrypoint exports these public groups:
341
-
342
- - Components: `BarChart`, `DistributionChart`, `HeatmapChart`,
343
- `ProgressChart`, `TimeSeriesChart`, `ChartLegend`, `ChartThemeProvider`,
344
- `useChartTheme`, `buildLegendEntries`, and their props
345
- - Values and datasource: `known`, `unknown`, `notApplicable`, `withheld`,
346
- `isKnown`, `isWithheld`, `sumKnown`, position/series/datasource types,
347
- marker helpers, and format resolution helpers
348
- - State and validation: `resolveStatus`, `drawsData`, `suppressesInteraction`,
349
- `STATUS_PRECEDENCE`, `DATASET_BUDGETS`, `checkBudget`, error/failure types,
350
- and `isChartError`
351
- - Interaction: selection and series interaction types plus `sameSelection`
352
- - Theme and formatting: `defaultChartTheme`, `resolveSeriesColor`, default
353
- formatter helpers, `NON_VALUE_TEXT`, and public theme/formatter contracts
354
- - Accessibility: `buildChartSummary`, `buildPointAnnouncement`, and
355
- `buildTotalAnnouncement`
356
-
357
- **Experimental** — may change in a minor: `ChartTheme` token set, `LegendEntry`,
358
- family-specific props (`shape`, `grouping`, `buckets`), and the
359
- `DATASET_BUDGETS` figures.
360
-
361
- **Reserved for a future additive minor**: an insight presentation surface and
362
- composable chart primitives. Neither will change the datasource, state, or
363
- interaction contracts when it lands.
364
-
365
- ## Contributing
366
-
367
- The one architectural rule worth knowing: **nothing under `src/geometry/` may
368
- import React or `react-native-svg`.** It is pure functions over plain data,
369
- enforced by an ESLint override and by `geometryPurity.test.ts`. That is what
370
- keeps the geometry testable without a renderer and the drawing substrate
371
- replaceable.
372
-
373
- ```bash
374
- npm run lint
375
- npm test
376
- npm run typecheck:chart
377
- npm run pack:chart
378
- npm run validate:chart # all of the above
379
- ```
380
-
381
- The demo integration lives in `src/screens/ChartDemoScreen.tsx`.
1
+ # @chipmobilesdk/rn-chart
2
+
3
+ Domain-agnostic charts for ChipMobileSdk-family React Native apps.
4
+
5
+ > **License:** `UNLICENSED`. Published publicly for use by the owner's
6
+ > applications; no open-source license is granted.
7
+
8
+ - **Version**: see [CHANGELOG](CHANGELOG.md)
9
+ - **Entrypoint**: `@chipmobilesdk/rn-chart`
10
+
11
+ One datasource contract spans five chart families. The package knows nothing
12
+ about nutrition, finance, health, or productivity — you hand it aggregated,
13
+ normalised data and it draws it.
14
+
15
+ ## What it does not do
16
+
17
+ Worth stating up front, because these are deliberate:
18
+
19
+ - **No insight or narrative surface.** The package draws charts. Render your own
20
+ explanatory text above or below a chart; it imposes no outer margin that would
21
+ fight your layout.
22
+ - **No data fetching, no database access, no AI calls, no network of any kind.**
23
+ Everything arrives as props.
24
+ - **No aggregation.** Your domain layer decides what the numbers mean.
25
+ - **No navigation.** Interactions call back; what happens next is yours.
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ npm install @chipmobilesdk/rn-chart react-native-svg
31
+ cd ios && pod install
32
+ ```
33
+
34
+ `react-native-svg` is a required peer. There is no degraded no-SVG mode.
35
+
36
+ No permissions, entitlements, `Info.plist` usage strings, or Android manifest
37
+ changes are needed. Note that this package's **no-permission chart state** is an
38
+ app-level data-access concept, not an operating-system permission.
39
+
40
+ ## Compatibility
41
+
42
+ | | Supported |
43
+ |---|---|
44
+ | React | 19.2+ |
45
+ | React Native | 0.85+ |
46
+ | `react-native-svg` | 15.15+ |
47
+ | Android | `minSdkVersion` 24, `targetSdkVersion` 36 |
48
+ | iOS | 15.1+ |
49
+
50
+ ## Quick start
51
+
52
+ ```tsx
53
+ import { BarChart, known, unknown, type ChartDatasource } from '@chipmobilesdk/rn-chart';
54
+
55
+ const datasource: ChartDatasource = {
56
+ positionKind: 'category',
57
+ series: [{
58
+ id: 'weekly',
59
+ label: 'Weekly total',
60
+ points: [
61
+ { id: 'mon', position: { kind: 'category', category: 'Mon' }, value: known(12) },
62
+ { id: 'tue', position: { kind: 'category', category: 'Tue' }, value: known(0) },
63
+ { id: 'wed', position: { kind: 'category', category: 'Wed' }, value: unknown() },
64
+ ],
65
+ }],
66
+ };
67
+
68
+ export function Example() {
69
+ return <BarChart datasource={datasource} height={220} title="This week" />;
70
+ }
71
+ ```
72
+
73
+ Monday draws a bar. Tuesday draws a **zero-height bar that reads as a real
74
+ zero**. Wednesday draws **nothing and is marked as a gap** — never a zero.
75
+
76
+ > **Keep your datasource referentially stable.** Hold it in state or a `useMemo`.
77
+ > Rebuilding the object inline on every render defeats the memoization the
78
+ > package relies on, and that is not something a library can fix for you.
79
+
80
+ ## Value semantics
81
+
82
+ The most important part of the contract. Four classes, and only one carries a
83
+ number:
84
+
85
+ ```ts
86
+ known(42) // a real observation, including known(0)
87
+ unknown() // we do not know
88
+ notApplicable() // does not apply here
89
+ withheld() // hidden: permission, or not enough data
90
+ ```
91
+
92
+ There is no `value` property on the three non-value cases, so `value ?? 0`
93
+ cannot compile. That is the point — turning a gap into a zero is the single most
94
+ damaging thing a charting library can do to someone's data.
95
+
96
+ Consequences you can rely on:
97
+
98
+ - An unknown value is never rendered as zero and never interpolated across. In a
99
+ line chart the gap ends the subpath, so there is physically no segment to
100
+ bridge it.
101
+ - `notApplicable` and `withheld` are excluded from totals, and any total that
102
+ excluded something is reported as incomplete.
103
+ - A withheld value never appears in a rendering, a label, an accessible summary,
104
+ or an interaction callback.
105
+ - Each non-value class renders as its own **texture**, not a colour difference,
106
+ so they stay distinguishable in greyscale.
107
+
108
+ ## The datasource contract
109
+
110
+ Every point shares one envelope — identity, value, optional unit and format
111
+ hint, opaque metadata — and only its **position** varies:
112
+
113
+ ```ts
114
+ { kind: 'category', category: 'Mon' }
115
+ { kind: 'instant', at: 1767225600000 } // epoch ms, UTC
116
+ { kind: 'matrix', row: 'week-1', column: 'Mon' }
117
+ ```
118
+
119
+ Which families accept which:
120
+
121
+ | Family | `category` | `instant` | `matrix` |
122
+ |---|:---:|:---:|:---:|
123
+ | `ProgressChart` | ✓ | | |
124
+ | `DistributionChart` | ✓ | | |
125
+ | `BarChart` | ✓ | ✓ | |
126
+ | `TimeSeriesChart` | | ✓ | |
127
+ | `HeatmapChart` | | | ✓ |
128
+
129
+ Bar accepting both is what makes swapping a time-series chart for a bar chart
130
+ free — same datasource, different component. Any other pairing is a **validation
131
+ error**, not a rendering guess.
132
+
133
+ Time is epoch milliseconds and carries no timezone. All date wording comes from
134
+ your formatters.
135
+
136
+ ### Reference markers
137
+
138
+ `datasource.markers` holds targets, baselines and thresholds. Those with a known
139
+ value are drawn as dashed lines:
140
+
141
+ - `BarChart` and `TimeSeriesChart` draw each one across the plot, with its
142
+ `label` at the right end. The value axis is stretched to include it, so a
143
+ threshold above every data point is still on the chart.
144
+ - `ProgressChart` draws the target on each row's bar. A value past its target
145
+ is drawn as overshoot beyond that line, inside the plot: the track is scaled
146
+ so the target sits at `1 / min(largest proportion, 1.25)` of its width.
147
+ - `DistributionChart` and `HeatmapChart` ignore markers.
148
+
149
+ Axis labels have room reserved for them: the plot shrinks to fit the widest
150
+ value label on the left and one line of time or category labels below, so no
151
+ label is clipped by the chart's edge.
152
+
153
+ ## States
154
+
155
+ Seven, and the package derives the two that only the data can determine:
156
+
157
+ | Status | Who decides |
158
+ |---|---|
159
+ | `empty`, `partial` | derived from your datasource |
160
+ | `loading`, `stale`, `noPermission`, `error` | you declare via `status` |
161
+
162
+ Precedence: `noPermission > error > loading > empty > stale > partial > ready`.
163
+ `noPermission` outranks everything so restricted values cannot slip through
164
+ another state.
165
+
166
+ Supply your own copy — the package's built-in wording is a terse placeholder,
167
+ not product voice:
168
+
169
+ ```tsx
170
+ <BarChart
171
+ datasource={ds}
172
+ status="loading"
173
+ stateContent={{
174
+ empty: <Text>Log your first meal to see this chart</Text>,
175
+ loading: <Spinner />,
176
+ }}
177
+ />
178
+ ```
179
+
180
+ ### Failure containment
181
+
182
+ Each chart contains its own render exceptions. One bad datasource collapses one
183
+ chart, never the dashboard around it:
184
+
185
+ ```tsx
186
+ <BarChart datasource={ds} chartId="calories" onFailure={report => crashReporter.log(report)} />
187
+ ```
188
+
189
+ Failure reports carry the chart identity, family, and a catalogue message —
190
+ **never data values or series labels**. This matters if you chart health or
191
+ financial data, because a report is the easiest accidental path from a chart
192
+ into a third-party crash reporting service.
193
+
194
+ ## Interaction
195
+
196
+ Uncontrolled by default. Supply a `selection` value (including `null`) to take
197
+ ownership; the same callbacks fire either way.
198
+
199
+ ```tsx
200
+ // Uncontrolled: the chart tracks its own selection.
201
+ <BarChart datasource={ds} onSelectionChange={(ref, event) => log(event)} />
202
+
203
+ // Controlled: you are the source of truth.
204
+ <BarChart
205
+ datasource={ds}
206
+ selection={selection}
207
+ onSelectionChange={setSelectionFromRef}
208
+ onSelectionUnresolvable={ref => setSelection(null)}
209
+ />
210
+ ```
211
+
212
+ Every payload carries the point id, series id, value, position, and whatever
213
+ metadata you attached. The package never navigates, fetches, or mutates.
214
+
215
+ Legends can also toggle series visibility. The state is uncontrolled unless
216
+ you supply `hiddenSeriesIds`:
217
+
218
+ ```tsx
219
+ <BarChart
220
+ datasource={ds}
221
+ seriesToggleEnabled
222
+ hiddenSeriesIds={hiddenSeriesIds}
223
+ onVisibilityChange={event => updateHiddenSeries(event.seriesId)}
224
+ />
225
+ ```
226
+
227
+ Pan, pinch-zoom, and brush-to-select are out of scope. `range` on
228
+ `TimeSeriesChart` is a window **you** supply, not one the user draws.
229
+
230
+ ## Theming
231
+
232
+ The package defines its own `ChartTheme` value type and takes no dependency on
233
+ `@chipmobilesdk/rn-theme` or `react-native-unistyles`. Any design system can
234
+ satisfy it:
235
+
236
+ ```tsx
237
+ import { ChartThemeProvider, defaultChartTheme } from '@chipmobilesdk/rn-chart';
238
+
239
+ const chartTheme = {
240
+ ...defaultChartTheme(scheme),
241
+ palette: myTokens.dataviz,
242
+ surface: myTokens.surface,
243
+ };
244
+
245
+ <ChartThemeProvider theme={chartTheme}>{children}</ChartThemeProvider>
246
+ ```
247
+
248
+ Resolution order: explicit `theme` prop → provider context → built-in default
249
+ keyed on `useColorScheme()`. With no configuration at all, charts still render
250
+ correctly in light and dark.
251
+
252
+ Meaning never rides on colour alone: series carry dash patterns as well as
253
+ colours, and non-value classes use textures.
254
+
255
+ `DistributionChart` colours **segments**, not series. Palette entries are taken
256
+ in order across every point, so one series of four points and four series of
257
+ one point both give four colours. A point's own `color` overrides its segment;
258
+ a series with a single point is that segment, so its series `color` applies.
259
+ The legend has one entry per segment, in the same colour as its arc.
260
+
261
+ ## Formatting
262
+
263
+ ```tsx
264
+ <TimeSeriesChart
265
+ datasource={ds}
266
+ formatters={{
267
+ value: v => (v.kind === 'known' ? intl.number(v.value) : '—'),
268
+ axisLabel: p => ('at' in p ? intl.date(p.at) : String(p)),
269
+ }}
270
+ />
271
+ ```
272
+
273
+ Your output is used verbatim. With no formatter, a documented locale-neutral
274
+ default applies — the package never infers or converts a business unit. A
275
+ formatter that throws degrades to the default **for that one label** and does
276
+ not crash the screen.
277
+
278
+ ## Accessibility
279
+
280
+ Each chart is a single accessible element with a composed summary: what it is,
281
+ what it shows, where the extremes are, what any target says. With selection
282
+ enabled the root becomes `adjustable`, so increment and decrement walk the data
283
+ points — the same access touch gives.
284
+
285
+ Non-ready charts announce their state rather than reading out figures they are
286
+ not showing. Accessibility props you supply are never overridden.
287
+
288
+ Axis labels are thinned to fit rather than overlapped, and scale with the system
289
+ text size up to **1.6×**; beyond that the growth is capped so the plot area
290
+ survives.
291
+
292
+ ## Dataset budgets
293
+
294
+ | Family | Budget |
295
+ |---|---|
296
+ | Time series | 5 series × 500 points |
297
+ | Bar | 5 series × 60 categories |
298
+ | Distribution | 12 segments |
299
+ | Progress | 8 metrics |
300
+ | Heatmap | 1,000 cells |
301
+
302
+ Over budget, a chart **still renders and drops nothing**. It warns once per
303
+ chart in development builds, and never in production. Aggregating or
304
+ downsampling above these sizes is your call — the package will not silently
305
+ decide what data to discard.
306
+
307
+ Element count scales with series, not points: an at-budget time-series chart
308
+ emits about six SVG elements, not 2,500.
309
+
310
+ ## Data behaviour (for App Privacy and Data Safety)
311
+
312
+ For consuming apps completing Apple App Privacy or Google Play Data Safety
313
+ declarations:
314
+
315
+ - The package holds the values and labels you pass it **in memory only**, for the
316
+ lifetime of the rendered component.
317
+ - It collects nothing, generates nothing, persists nothing, transmits nothing,
318
+ and shares nothing with third parties.
319
+ - It logs no data point value and no series label in production builds,
320
+ including inside failure reports.
321
+ - It requests no permissions and reads no device identifiers.
322
+
323
+ **This package introduces no data-collection declaration of its own.** If your
324
+ app sends charted data anywhere, that collection is yours to declare.
325
+
326
+ If you render your own narrative text near a chart — particularly text from an
327
+ AI service or from users — moderation and content obligations are entirely
328
+ yours. The package ships no free-text surface at all.
329
+
330
+ ## Public API
331
+
332
+ `src/index.ts` is the only entrypoint. Geometry, scales, layout maths, path
333
+ builders, the chart shell, and the SVG adapter are private so they can change
334
+ without a breaking release.
335
+
336
+ **Stable**: value semantics, `Position`, the datasource types, the five chart
337
+ components and their base props, `ChartStatus`, the error types, interaction
338
+ events.
339
+
340
+ The single entrypoint exports these public groups:
341
+
342
+ - Components: `BarChart`, `DistributionChart`, `HeatmapChart`,
343
+ `ProgressChart`, `TimeSeriesChart`, `ChartLegend`, `ChartThemeProvider`,
344
+ `useChartTheme`, `buildLegendEntries`, and their props
345
+ - Values and datasource: `known`, `unknown`, `notApplicable`, `withheld`,
346
+ `isKnown`, `isWithheld`, `sumKnown`, position/series/datasource types,
347
+ marker helpers, and format resolution helpers
348
+ - State and validation: `resolveStatus`, `drawsData`, `suppressesInteraction`,
349
+ `STATUS_PRECEDENCE`, `DATASET_BUDGETS`, `checkBudget`, error/failure types,
350
+ and `isChartError`
351
+ - Interaction: selection and series interaction types plus `sameSelection`
352
+ - Theme and formatting: `defaultChartTheme`, `resolveSeriesColor`, default
353
+ formatter helpers, `NON_VALUE_TEXT`, and public theme/formatter contracts
354
+ - Accessibility: `buildChartSummary`, `buildPointAnnouncement`, and
355
+ `buildTotalAnnouncement`
356
+
357
+ **Experimental** — may change in a minor: `ChartTheme` token set, `LegendEntry`,
358
+ family-specific props (`shape`, `grouping`, `buckets`), and the
359
+ `DATASET_BUDGETS` figures.
360
+
361
+ **Reserved for a future additive minor**: an insight presentation surface and
362
+ composable chart primitives. Neither will change the datasource, state, or
363
+ interaction contracts when it lands.
364
+
365
+ ## Contributing
366
+
367
+ The one architectural rule worth knowing: **nothing under `src/geometry/` may
368
+ import React or `react-native-svg`.** It is pure functions over plain data,
369
+ enforced by an ESLint override and by `geometryPurity.test.ts`. That is what
370
+ keeps the geometry testable without a renderer and the drawing substrate
371
+ replaceable.
372
+
373
+ ```bash
374
+ npm run lint
375
+ npm test
376
+ npm run typecheck --workspace @chipmobilesdk/rn-chart
377
+ npm run pack:check --workspace @chipmobilesdk/rn-chart
378
+ npm run validate # all of the above
379
+ ```
380
+
381
+ The demo integration lives in `src/screens/ChartDemoScreen.tsx`.