@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +164 -115
  2. package/README.md +391 -381
  3. package/package.json +49 -49
  4. package/skills/sdk-rn-chart/SKILL.md +68 -69
  5. package/src/a11y/summary.ts +190 -177
  6. package/src/contract/datasource.ts +126 -136
  7. package/src/contract/errors.ts +129 -133
  8. package/src/contract/format.ts +168 -168
  9. package/src/contract/interaction.ts +70 -71
  10. package/src/contract/marker.ts +75 -75
  11. package/src/contract/props.ts +64 -60
  12. package/src/contract/state.ts +88 -94
  13. package/src/contract/strings.ts +48 -0
  14. package/src/contract/theme.ts +169 -177
  15. package/src/contract/value.ts +121 -125
  16. package/src/geometry/bar.ts +271 -273
  17. package/src/geometry/distribution.ts +197 -203
  18. package/src/geometry/heatmap.ts +189 -195
  19. package/src/geometry/hitTest.ts +117 -120
  20. package/src/geometry/layout.ts +162 -173
  21. package/src/geometry/markers.ts +55 -57
  22. package/src/geometry/path.ts +193 -199
  23. package/src/geometry/progress.ts +223 -230
  24. package/src/geometry/scale.ts +140 -146
  25. package/src/geometry/ticks.ts +145 -153
  26. package/src/geometry/timeSeries.ts +168 -168
  27. package/src/geometry/types.ts +94 -94
  28. package/src/index.ts +162 -176
  29. package/src/react/ChartBoundary.tsx +96 -96
  30. package/src/react/ChartFrame.tsx +596 -622
  31. package/src/react/ChartLegend.tsx +216 -215
  32. package/src/react/ChartThemeProvider.tsx +44 -48
  33. package/src/react/charts.tsx +154 -161
  34. package/src/react/useChartLayout.ts +124 -124
  35. package/src/react/useChartSelection.ts +152 -170
  36. package/src/render/Axis.tsx +120 -122
  37. package/src/render/Marks.tsx +173 -205
  38. package/src/validation/budgets.ts +113 -110
  39. package/src/validation/validateDatasource.ts +174 -174
@@ -1,161 +1,154 @@
1
- /**
2
- * The five chart family components.
3
- *
4
- * Each is a thin wrapper over `ChartFrame` plus its own geometry builder, so
5
- * validation, state handling, theming, selection, accessibility, and failure
6
- * containment behave identically across every family.
7
- *
8
- * All five are wrapped in `React.memo`: value-identical props stop at the memo
9
- * boundary and never reach geometry (FR-073, SC-017).
10
- */
11
- import React, { useCallback } from 'react';
12
-
13
- import type { ChartDatasource } from '../contract/datasource';
14
- import type { BaseChartProps } from '../contract/props';
15
- import { buildBarGeometry, type BarGrouping } from '../geometry/bar';
16
- import {
17
- buildDistributionGeometry,
18
- type DistributionShape,
19
- } from '../geometry/distribution';
20
- import { buildHeatmapGeometry } from '../geometry/heatmap';
21
- import { buildProgressGeometry } from '../geometry/progress';
22
- import { buildTimeSeriesGeometry } from '../geometry/timeSeries';
23
- import type { GeometryInput } from '../geometry/types';
24
- import { ChartFrame } from './ChartFrame';
25
-
26
- export interface ProgressChartProps<M = unknown> extends BaseChartProps<M> {
27
- readonly orientation?: 'horizontal' | 'vertical';
28
- }
29
-
30
- function ProgressChartImpl<M>(props: ProgressChartProps<M>) {
31
- const build = useCallback(
32
- (ds: ChartDatasource<M>, input: GeometryInput) =>
33
- buildProgressGeometry(ds, input, { orientation: props.orientation }),
34
- [props.orientation],
35
- );
36
-
37
- return (
38
- <ChartFrame
39
- {...props}
40
- family="progress"
41
- buildGeometry={build}
42
- showAxes={false}
43
- defaultLegend={false}
44
- />
45
- );
46
- }
47
-
48
- export interface DistributionChartProps<M = unknown> extends BaseChartProps<M> {
49
- readonly shape?: DistributionShape;
50
- readonly innerRadiusRatio?: number;
51
- }
52
-
53
- function DistributionChartImpl<M>(props: DistributionChartProps<M>) {
54
- const build = useCallback(
55
- (ds: ChartDatasource<M>, input: GeometryInput) =>
56
- buildDistributionGeometry(ds, input, {
57
- shape: props.shape,
58
- innerRadiusRatio: props.innerRadiusRatio,
59
- }),
60
- [props.shape, props.innerRadiusRatio],
61
- );
62
-
63
- return (
64
- <ChartFrame
65
- {...props}
66
- family="distribution"
67
- buildGeometry={build}
68
- showAxes={false}
69
- defaultLegend
70
- />
71
- );
72
- }
73
-
74
- export interface TimeSeriesChartProps<M = unknown> extends BaseChartProps<M> {
75
- /** A window the app supplies. Users cannot pan or zoom (out of scope). */
76
- readonly range?: { readonly from: number; readonly to: number };
77
- }
78
-
79
- function TimeSeriesChartImpl<M>(props: TimeSeriesChartProps<M>) {
80
- const build = useCallback(
81
- (ds: ChartDatasource<M>, input: GeometryInput) =>
82
- buildTimeSeriesGeometry(ds, input, { range: props.range }),
83
- [props.range],
84
- );
85
-
86
- return (
87
- <ChartFrame
88
- {...props}
89
- family="timeSeries"
90
- buildGeometry={build}
91
- // A user aims at a moment in time, not at a pixel on the line.
92
- hitTestMode="byX"
93
- defaultLegend
94
- />
95
- );
96
- }
97
-
98
- export interface BarChartProps<M = unknown> extends BaseChartProps<M> {
99
- readonly grouping?: BarGrouping;
100
- }
101
-
102
- function BarChartImpl<M>(props: BarChartProps<M>) {
103
- const build = useCallback(
104
- (ds: ChartDatasource<M>, input: GeometryInput) =>
105
- buildBarGeometry(ds, input, { grouping: props.grouping }),
106
- [props.grouping],
107
- );
108
-
109
- return (
110
- <ChartFrame {...props} family="bar" buildGeometry={build} defaultLegend />
111
- );
112
- }
113
-
114
- export interface HeatmapChartProps<M = unknown> extends BaseChartProps<M> {
115
- readonly buckets?: number;
116
- }
117
-
118
- function HeatmapChartImpl<M>(props: HeatmapChartProps<M>) {
119
- const build = useCallback(
120
- (ds: ChartDatasource<M>, input: GeometryInput) =>
121
- buildHeatmapGeometry(ds, input, { buckets: props.buckets }),
122
- [props.buckets],
123
- );
124
-
125
- return (
126
- <ChartFrame
127
- {...props}
128
- family="heatmap"
129
- buildGeometry={build}
130
- showAxes={false}
131
- defaultLegend={false}
132
- />
133
- );
134
- }
135
-
136
- /**
137
- * `React.memo` erases the generic parameter, so each component is re-cast to a
138
- * generic function type. This keeps `<BarChart<MyMeta> ... />` working for
139
- * consumers while still getting the memo boundary.
140
- */
141
- export const ProgressChart = React.memo(ProgressChartImpl) as <M = unknown>(
142
- props: ProgressChartProps<M>,
143
- ) => React.ReactElement;
144
-
145
- export const DistributionChart = React.memo(DistributionChartImpl) as <
146
- M = unknown,
147
- >(
148
- props: DistributionChartProps<M>,
149
- ) => React.ReactElement;
150
-
151
- export const TimeSeriesChart = React.memo(TimeSeriesChartImpl) as <M = unknown>(
152
- props: TimeSeriesChartProps<M>,
153
- ) => React.ReactElement;
154
-
155
- export const BarChart = React.memo(BarChartImpl) as <M = unknown>(
156
- props: BarChartProps<M>,
157
- ) => React.ReactElement;
158
-
159
- export const HeatmapChart = React.memo(HeatmapChartImpl) as <M = unknown>(
160
- props: HeatmapChartProps<M>,
161
- ) => React.ReactElement;
1
+ /**
2
+ * The five chart family components.
3
+ *
4
+ * Each is a thin wrapper over `ChartFrame` plus its own geometry builder, so
5
+ * validation, state handling, theming, selection, accessibility, and failure
6
+ * containment behave identically across every family.
7
+ *
8
+ * All five are wrapped in `React.memo`: value-identical props stop at the memo
9
+ * boundary and never reach geometry (FR-073, SC-017).
10
+ */
11
+ import React, { useCallback } from 'react';
12
+
13
+ import type { ChartDatasource } from '../contract/datasource';
14
+ import type { BaseChartProps } from '../contract/props';
15
+ import { buildBarGeometry, type BarGrouping } from '../geometry/bar';
16
+ import { buildDistributionGeometry, type DistributionShape } from '../geometry/distribution';
17
+ import { buildHeatmapGeometry } from '../geometry/heatmap';
18
+ import { buildProgressGeometry } from '../geometry/progress';
19
+ import { buildTimeSeriesGeometry } from '../geometry/timeSeries';
20
+ import type { GeometryInput } from '../geometry/types';
21
+ import { ChartFrame } from './ChartFrame';
22
+
23
+ export interface ProgressChartProps<M = unknown> extends BaseChartProps<M> {
24
+ readonly orientation?: 'horizontal' | 'vertical';
25
+ }
26
+
27
+ function ProgressChartImpl<M>(props: ProgressChartProps<M>) {
28
+ const build = useCallback(
29
+ (ds: ChartDatasource<M>, input: GeometryInput) =>
30
+ buildProgressGeometry(ds, input, { orientation: props.orientation }),
31
+ [props.orientation],
32
+ );
33
+
34
+ return (
35
+ <ChartFrame
36
+ {...props}
37
+ family="progress"
38
+ buildGeometry={build}
39
+ showAxes={false}
40
+ defaultLegend={false}
41
+ />
42
+ );
43
+ }
44
+
45
+ export interface DistributionChartProps<M = unknown> extends BaseChartProps<M> {
46
+ readonly shape?: DistributionShape;
47
+ readonly innerRadiusRatio?: number;
48
+ }
49
+
50
+ function DistributionChartImpl<M>(props: DistributionChartProps<M>) {
51
+ const build = useCallback(
52
+ (ds: ChartDatasource<M>, input: GeometryInput) =>
53
+ buildDistributionGeometry(ds, input, {
54
+ shape: props.shape,
55
+ innerRadiusRatio: props.innerRadiusRatio,
56
+ }),
57
+ [props.shape, props.innerRadiusRatio],
58
+ );
59
+
60
+ return (
61
+ <ChartFrame
62
+ {...props}
63
+ family="distribution"
64
+ buildGeometry={build}
65
+ showAxes={false}
66
+ defaultLegend
67
+ />
68
+ );
69
+ }
70
+
71
+ export interface TimeSeriesChartProps<M = unknown> extends BaseChartProps<M> {
72
+ /** A window the app supplies. Users cannot pan or zoom (out of scope). */
73
+ readonly range?: { readonly from: number; readonly to: number };
74
+ }
75
+
76
+ function TimeSeriesChartImpl<M>(props: TimeSeriesChartProps<M>) {
77
+ const build = useCallback(
78
+ (ds: ChartDatasource<M>, input: GeometryInput) =>
79
+ buildTimeSeriesGeometry(ds, input, { range: props.range }),
80
+ [props.range],
81
+ );
82
+
83
+ return (
84
+ <ChartFrame
85
+ {...props}
86
+ family="timeSeries"
87
+ buildGeometry={build}
88
+ // A user aims at a moment in time, not at a pixel on the line.
89
+ hitTestMode="byX"
90
+ defaultLegend
91
+ />
92
+ );
93
+ }
94
+
95
+ export interface BarChartProps<M = unknown> extends BaseChartProps<M> {
96
+ readonly grouping?: BarGrouping;
97
+ }
98
+
99
+ function BarChartImpl<M>(props: BarChartProps<M>) {
100
+ const build = useCallback(
101
+ (ds: ChartDatasource<M>, input: GeometryInput) =>
102
+ buildBarGeometry(ds, input, { grouping: props.grouping }),
103
+ [props.grouping],
104
+ );
105
+
106
+ return <ChartFrame {...props} family="bar" buildGeometry={build} defaultLegend />;
107
+ }
108
+
109
+ export interface HeatmapChartProps<M = unknown> extends BaseChartProps<M> {
110
+ readonly buckets?: number;
111
+ }
112
+
113
+ function HeatmapChartImpl<M>(props: HeatmapChartProps<M>) {
114
+ const build = useCallback(
115
+ (ds: ChartDatasource<M>, input: GeometryInput) =>
116
+ buildHeatmapGeometry(ds, input, { buckets: props.buckets }),
117
+ [props.buckets],
118
+ );
119
+
120
+ return (
121
+ <ChartFrame
122
+ {...props}
123
+ family="heatmap"
124
+ buildGeometry={build}
125
+ showAxes={false}
126
+ defaultLegend={false}
127
+ />
128
+ );
129
+ }
130
+
131
+ /**
132
+ * `React.memo` erases the generic parameter, so each component is re-cast to a
133
+ * generic function type. This keeps `<BarChart<MyMeta> ... />` working for
134
+ * consumers while still getting the memo boundary.
135
+ */
136
+ export const ProgressChart = React.memo(ProgressChartImpl) as <M = unknown>(
137
+ props: ProgressChartProps<M>,
138
+ ) => React.ReactElement;
139
+
140
+ export const DistributionChart = React.memo(DistributionChartImpl) as <M = unknown>(
141
+ props: DistributionChartProps<M>,
142
+ ) => React.ReactElement;
143
+
144
+ export const TimeSeriesChart = React.memo(TimeSeriesChartImpl) as <M = unknown>(
145
+ props: TimeSeriesChartProps<M>,
146
+ ) => React.ReactElement;
147
+
148
+ export const BarChart = React.memo(BarChartImpl) as <M = unknown>(
149
+ props: BarChartProps<M>,
150
+ ) => React.ReactElement;
151
+
152
+ export const HeatmapChart = React.memo(HeatmapChartImpl) as <M = unknown>(
153
+ props: HeatmapChartProps<M>,
154
+ ) => React.ReactElement;
@@ -1,124 +1,124 @@
1
- /**
2
- * Container measurement and the geometry memo.
3
- *
4
- * `ResolvedGeometry` is memoized on datasource identity, plot rectangle, theme
5
- * identity, and formatter identity — and deliberately **not** on selection, so
6
- * highlighting a point repaints the highlight without recomputing scales or
7
- * path strings (FR-073, SC-017).
8
- *
9
- * A consequence worth documenting for consumers: rebuilding the datasource
10
- * object inline on every render defeats this. The README says so, and the demo
11
- * app models the correct pattern.
12
- */
13
- import { useCallback, useMemo, useState } from 'react';
14
- import { PixelRatio, type LayoutChangeEvent } from 'react-native';
15
-
16
- import type { ChartDatasource, ChartFamily } from '../contract/datasource';
17
- import { resolveSegmentColors, resolveSeriesColor, type ChartTheme } from '../contract/theme';
18
- import {
19
- EMPTY_PLOT,
20
- isMeasurable,
21
- resolvePlotRect,
22
- type ContainerSize,
23
- type PlotRect,
24
- } from '../geometry/layout';
25
- import { effectiveFontSize } from '../geometry/layout';
26
- import type { GeometryInput } from '../geometry/types';
27
-
28
- export interface ChartLayout {
29
- readonly onLayout: (event: LayoutChangeEvent) => void;
30
- readonly size: ContainerSize;
31
- readonly measured: boolean;
32
- readonly plot: PlotRect;
33
- readonly fontSize: number;
34
- }
35
-
36
- export function useChartLayout(
37
- theme: ChartTheme,
38
- options: {
39
- readonly height?: number;
40
- readonly xLabels?: readonly string[];
41
- readonly yLabels?: readonly string[];
42
- } = {},
43
- ): ChartLayout {
44
- const [size, setSize] = useState<ContainerSize>({ width: 0, height: 0 });
45
-
46
- const onLayout = useCallback((event: LayoutChangeEvent) => {
47
- const { width, height } = event.nativeEvent.layout;
48
- setSize(prev =>
49
- // Avoid a state churn loop on sub-pixel layout jitter.
50
- Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5
51
- ? prev
52
- : { width, height },
53
- );
54
- }, []);
55
-
56
- const fontScale = PixelRatio.getFontScale();
57
- const fontSize = effectiveFontSize(theme.typography.axisLabel.fontSize, fontScale);
58
-
59
- const measured = isMeasurable(size);
60
-
61
- const plot = useMemo(() => {
62
- if (!measured) {
63
- // Wait for a real measurement rather than computing a broken layout
64
- // or throwing (FR-078).
65
- return EMPTY_PLOT;
66
- }
67
- return resolvePlotRect({
68
- container: size,
69
- xLabels: options.xLabels,
70
- yLabels: options.yLabels,
71
- baseFontSize: theme.typography.axisLabel.fontSize,
72
- fontScale,
73
- padding: theme.spacing.xs,
74
- });
75
- }, [
76
- measured,
77
- size,
78
- options.xLabels,
79
- options.yLabels,
80
- theme.typography.axisLabel.fontSize,
81
- theme.spacing.xs,
82
- fontScale,
83
- ]);
84
-
85
- return { onLayout, size, measured, plot, fontSize };
86
- }
87
-
88
- /**
89
- * Builds the geometry input, resolving per-series colours once so geometry
90
- * modules never reach into the theme themselves.
91
- */
92
- export function useGeometryInput<M>(
93
- datasource: ChartDatasource<M>,
94
- theme: ChartTheme,
95
- layout: ChartLayout,
96
- formatters: {
97
- readonly formatValue: (value: number) => string;
98
- readonly formatCategory: (raw: string) => string;
99
- readonly formatInstant: (at: number) => string;
100
- },
101
- family?: ChartFamily,
102
- ): GeometryInput {
103
- return useMemo(() => {
104
- const seriesColors: Record<string, string> = {};
105
- datasource.series.forEach((series, index) => {
106
- seriesColors[series.id] = resolveSeriesColor(theme, index, series.color);
107
- });
108
-
109
- return {
110
- plot: layout.plot,
111
- fontSize: layout.fontSize,
112
- seriesColors,
113
- // Per point only where segments exist; resolving one per point for
114
- // every family cost a colour per data point at the time-series budget.
115
- segmentColors:
116
- family === 'distribution' ? resolveSegmentColors(datasource, theme) : undefined,
117
- nonValueTextures: theme.nonValue,
118
- heatmapRamp: theme.heatmapRamp,
119
- formatValue: formatters.formatValue,
120
- formatCategory: formatters.formatCategory,
121
- formatInstant: formatters.formatInstant,
122
- };
123
- }, [datasource, theme, layout.plot, layout.fontSize, formatters, family]);
124
- }
1
+ /**
2
+ * Container measurement and the geometry memo.
3
+ *
4
+ * `ResolvedGeometry` is memoized on datasource identity, plot rectangle, theme
5
+ * identity, and formatter identity — and deliberately **not** on selection, so
6
+ * highlighting a point repaints the highlight without recomputing scales or
7
+ * path strings (FR-073, SC-017).
8
+ *
9
+ * A consequence worth documenting for consumers: rebuilding the datasource
10
+ * object inline on every render defeats this. The README says so, and the demo
11
+ * app models the correct pattern.
12
+ */
13
+ import { useCallback, useMemo, useState } from 'react';
14
+ import { PixelRatio, type LayoutChangeEvent } from 'react-native';
15
+
16
+ import type { ChartDatasource, ChartFamily } from '../contract/datasource';
17
+ import { resolveSegmentColors, resolveSeriesColor, type ChartTheme } from '../contract/theme';
18
+ import {
19
+ EMPTY_PLOT,
20
+ isMeasurable,
21
+ resolvePlotRect,
22
+ type ContainerSize,
23
+ type PlotRect,
24
+ } from '../geometry/layout';
25
+ import { effectiveFontSize } from '../geometry/layout';
26
+ import type { GeometryInput } from '../geometry/types';
27
+
28
+ export interface ChartLayout {
29
+ readonly onLayout: (event: LayoutChangeEvent) => void;
30
+ readonly size: ContainerSize;
31
+ readonly measured: boolean;
32
+ readonly plot: PlotRect;
33
+ readonly fontSize: number;
34
+ }
35
+
36
+ export function useChartLayout(
37
+ theme: ChartTheme,
38
+ options: {
39
+ readonly height?: number;
40
+ readonly xLabels?: readonly string[];
41
+ readonly yLabels?: readonly string[];
42
+ } = {},
43
+ ): ChartLayout {
44
+ const [size, setSize] = useState<ContainerSize>({ width: 0, height: 0 });
45
+
46
+ const onLayout = useCallback((event: LayoutChangeEvent) => {
47
+ const { width, height } = event.nativeEvent.layout;
48
+ setSize(prev =>
49
+ // Avoid a state churn loop on sub-pixel layout jitter.
50
+ Math.abs(prev.width - width) < 0.5 && Math.abs(prev.height - height) < 0.5
51
+ ? prev
52
+ : { width, height },
53
+ );
54
+ }, []);
55
+
56
+ const fontScale = PixelRatio.getFontScale();
57
+ const fontSize = effectiveFontSize(theme.typography.axisLabel.fontSize, fontScale);
58
+
59
+ const measured = isMeasurable(size);
60
+
61
+ const plot = useMemo(() => {
62
+ if (!measured) {
63
+ // Wait for a real measurement rather than computing a broken layout
64
+ // or throwing (FR-078).
65
+ return EMPTY_PLOT;
66
+ }
67
+ return resolvePlotRect({
68
+ container: size,
69
+ xLabels: options.xLabels,
70
+ yLabels: options.yLabels,
71
+ baseFontSize: theme.typography.axisLabel.fontSize,
72
+ fontScale,
73
+ padding: theme.spacing.xs,
74
+ });
75
+ }, [
76
+ measured,
77
+ size,
78
+ options.xLabels,
79
+ options.yLabels,
80
+ theme.typography.axisLabel.fontSize,
81
+ theme.spacing.xs,
82
+ fontScale,
83
+ ]);
84
+
85
+ return { onLayout, size, measured, plot, fontSize };
86
+ }
87
+
88
+ /**
89
+ * Builds the geometry input, resolving per-series colours once so geometry
90
+ * modules never reach into the theme themselves.
91
+ */
92
+ export function useGeometryInput<M>(
93
+ datasource: ChartDatasource<M>,
94
+ theme: ChartTheme,
95
+ layout: ChartLayout,
96
+ formatters: {
97
+ readonly formatValue: (value: number) => string;
98
+ readonly formatCategory: (raw: string) => string;
99
+ readonly formatInstant: (at: number) => string;
100
+ },
101
+ family?: ChartFamily,
102
+ ): GeometryInput {
103
+ return useMemo(() => {
104
+ const seriesColors: Record<string, string> = {};
105
+ datasource.series.forEach((series, index) => {
106
+ seriesColors[series.id] = resolveSeriesColor(theme, index, series.color);
107
+ });
108
+
109
+ return {
110
+ plot: layout.plot,
111
+ fontSize: layout.fontSize,
112
+ seriesColors,
113
+ // Per point only where segments exist; resolving one per point for
114
+ // every family cost a colour per data point at the time-series budget.
115
+ segmentColors:
116
+ family === 'distribution' ? resolveSegmentColors(datasource, theme) : undefined,
117
+ nonValueTextures: theme.nonValue,
118
+ heatmapRamp: theme.heatmapRamp,
119
+ formatValue: formatters.formatValue,
120
+ formatCategory: formatters.formatCategory,
121
+ formatInstant: formatters.formatInstant,
122
+ };
123
+ }, [datasource, theme, layout.plot, layout.fontSize, formatters, family]);
124
+ }