@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
@@ -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
+ }