@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,136 +1,126 @@
1
- /**
2
- * The shared datasource contract (FR-007, FR-007a).
3
- *
4
- * One contract spans every chart family: each point carries the same envelope —
5
- * identity, value state, optional unit and format hint, opaque metadata — and
6
- * only its **position** varies by kind. Families that accept the same position
7
- * kind consume one another's datasources unchanged; anything else is a
8
- * validation error rather than a rendering guess.
9
- *
10
- * See specs/007-rn-chart-package/data-model.md §2–§6.
11
- */
12
- import type { FormatHint } from './format';
13
- import type { ReferenceMarker } from './marker';
14
- import type { ValueState } from './value';
15
-
16
- export type ChartFamily =
17
- | 'progress'
18
- | 'distribution'
19
- | 'timeSeries'
20
- | 'bar'
21
- | 'heatmap';
22
-
23
- /** A named bucket: a category axis, a distribution segment, a progress metric. */
24
- export interface CategoryPosition {
25
- readonly kind: 'category';
26
- readonly category: string;
27
- }
28
-
29
- /**
30
- * An absolute instant as epoch milliseconds, UTC.
31
- *
32
- * A number rather than a `Date` or an ISO string: cheap to compare and
33
- * memoize, no per-render parsing across hundreds of points, no identity churn
34
- * that would defeat the re-render guarantee, and no ambiguous offset. All
35
- * date wording comes from app formatters (FR-016).
36
- */
37
- export interface InstantPosition {
38
- readonly kind: 'instant';
39
- readonly at: number;
40
- }
41
-
42
- /** A cell in a row-by-column matrix, for heatmaps. */
43
- export interface MatrixPosition {
44
- readonly kind: 'matrix';
45
- readonly row: string;
46
- readonly column: string;
47
- }
48
-
49
- export type Position = CategoryPosition | InstantPosition | MatrixPosition;
50
- export type PositionKind = Position['kind'];
51
-
52
- /** Non-colour encoding for a series, so meaning never rides on hue alone (FR-064). */
53
- export type SeriesPattern = 'solid' | 'dashed' | 'dotted' | 'dashDot';
54
-
55
- export interface DataPoint<M = unknown> {
56
- /** Stable and unique within its series. Duplicates are a validation error. */
57
- readonly id: string;
58
- readonly position: Position;
59
- readonly value: ValueState;
60
- /** Overrides the series unit. */
61
- readonly unit?: string;
62
- /** Overrides the series format hint. */
63
- readonly format?: FormatHint;
64
- /** Colour of this point's segment in a distribution chart. Ignored elsewhere. */
65
- readonly color?: string;
66
- /** Opaque to the package: never read, never logged, returned unchanged. */
67
- readonly metadata?: M;
68
- }
69
-
70
- export interface Series<M = unknown> {
71
- /** Stable and unique within the datasource. */
72
- readonly id: string;
73
- readonly label: string;
74
- readonly points: readonly DataPoint<M>[];
75
- readonly unit?: string;
76
- readonly format?: FormatHint;
77
- /** Explicit colour override; otherwise the theme palette assigns one. */
78
- readonly color?: string;
79
- readonly pattern?: SeriesPattern;
80
- /** Initial visibility. Runtime toggling is controlled separately. */
81
- readonly hidden?: boolean;
82
- readonly metadata?: M;
83
- }
84
-
85
- export interface ChartDatasource<M = unknown> {
86
- /** Declared once, then validated against every point and the receiving family. */
87
- readonly positionKind: PositionKind;
88
- readonly series: readonly Series<M>[];
89
- readonly markers?: readonly ReferenceMarker[];
90
- readonly unit?: string;
91
- readonly format?: FormatHint;
92
- readonly metadata?: M;
93
- }
94
-
95
- /**
96
- * Which position kinds each family accepts.
97
- *
98
- * Bar accepting both `category` and `instant` is what makes swapping a
99
- * time-series chart for a bar chart free — the datasource does not change.
100
- */
101
- export const FAMILY_POSITION_KINDS: Readonly<
102
- Record<ChartFamily, readonly PositionKind[]>
103
- > = {
104
- progress: ['category'],
105
- distribution: ['category'],
106
- bar: ['category', 'instant'],
107
- timeSeries: ['instant'],
108
- heatmap: ['matrix'],
109
- };
110
-
111
- export function familyAcceptsPositionKind(
112
- family: ChartFamily,
113
- kind: PositionKind,
114
- ): boolean {
115
- return FAMILY_POSITION_KINDS[family].includes(kind);
116
- }
117
-
118
- /**
119
- * Resolves unit and format through the documented precedence:
120
- * point -> series -> datasource -> undefined (caller falls back to its default).
121
- */
122
- export function resolveFormat<M>(
123
- point: DataPoint<M> | undefined,
124
- series: Series<M> | undefined,
125
- datasource: ChartDatasource<M>,
126
- ): FormatHint | undefined {
127
- return point?.format ?? series?.format ?? datasource.format;
128
- }
129
-
130
- export function resolveUnit<M>(
131
- point: DataPoint<M> | undefined,
132
- series: Series<M> | undefined,
133
- datasource: ChartDatasource<M>,
134
- ): string | undefined {
135
- return point?.unit ?? series?.unit ?? datasource.unit;
136
- }
1
+ /**
2
+ * The shared datasource contract (FR-007, FR-007a).
3
+ *
4
+ * One contract spans every chart family: each point carries the same envelope —
5
+ * identity, value state, optional unit and format hint, opaque metadata — and
6
+ * only its **position** varies by kind. Families that accept the same position
7
+ * kind consume one another's datasources unchanged; anything else is a
8
+ * validation error rather than a rendering guess.
9
+ *
10
+ * See specs/007-rn-chart-package/data-model.md §2–§6.
11
+ */
12
+ import type { FormatHint } from './format';
13
+ import type { ReferenceMarker } from './marker';
14
+ import type { ValueState } from './value';
15
+
16
+ export type ChartFamily = 'progress' | 'distribution' | 'timeSeries' | 'bar' | 'heatmap';
17
+
18
+ /** A named bucket: a category axis, a distribution segment, a progress metric. */
19
+ export interface CategoryPosition {
20
+ readonly kind: 'category';
21
+ readonly category: string;
22
+ }
23
+
24
+ /**
25
+ * An absolute instant as epoch milliseconds, UTC.
26
+ *
27
+ * A number rather than a `Date` or an ISO string: cheap to compare and
28
+ * memoize, no per-render parsing across hundreds of points, no identity churn
29
+ * that would defeat the re-render guarantee, and no ambiguous offset. All
30
+ * date wording comes from app formatters (FR-016).
31
+ */
32
+ export interface InstantPosition {
33
+ readonly kind: 'instant';
34
+ readonly at: number;
35
+ }
36
+
37
+ /** A cell in a row-by-column matrix, for heatmaps. */
38
+ export interface MatrixPosition {
39
+ readonly kind: 'matrix';
40
+ readonly row: string;
41
+ readonly column: string;
42
+ }
43
+
44
+ export type Position = CategoryPosition | InstantPosition | MatrixPosition;
45
+ export type PositionKind = Position['kind'];
46
+
47
+ /** Non-colour encoding for a series, so meaning never rides on hue alone (FR-064). */
48
+ export type SeriesPattern = 'solid' | 'dashed' | 'dotted' | 'dashDot';
49
+
50
+ export interface DataPoint<M = unknown> {
51
+ /** Stable and unique within its series. Duplicates are a validation error. */
52
+ readonly id: string;
53
+ readonly position: Position;
54
+ readonly value: ValueState;
55
+ /** Overrides the series unit. */
56
+ readonly unit?: string;
57
+ /** Overrides the series format hint. */
58
+ readonly format?: FormatHint;
59
+ /** Colour of this point's segment in a distribution chart. Ignored elsewhere. */
60
+ readonly color?: string;
61
+ /** Opaque to the package: never read, never logged, returned unchanged. */
62
+ readonly metadata?: M;
63
+ }
64
+
65
+ export interface Series<M = unknown> {
66
+ /** Stable and unique within the datasource. */
67
+ readonly id: string;
68
+ readonly label: string;
69
+ readonly points: readonly DataPoint<M>[];
70
+ readonly unit?: string;
71
+ readonly format?: FormatHint;
72
+ /** Explicit colour override; otherwise the theme palette assigns one. */
73
+ readonly color?: string;
74
+ readonly pattern?: SeriesPattern;
75
+ /** Initial visibility. Runtime toggling is controlled separately. */
76
+ readonly hidden?: boolean;
77
+ readonly metadata?: M;
78
+ }
79
+
80
+ export interface ChartDatasource<M = unknown> {
81
+ /** Declared once, then validated against every point and the receiving family. */
82
+ readonly positionKind: PositionKind;
83
+ readonly series: readonly Series<M>[];
84
+ readonly markers?: readonly ReferenceMarker[];
85
+ readonly unit?: string;
86
+ readonly format?: FormatHint;
87
+ readonly metadata?: M;
88
+ }
89
+
90
+ /**
91
+ * Which position kinds each family accepts.
92
+ *
93
+ * Bar accepting both `category` and `instant` is what makes swapping a
94
+ * time-series chart for a bar chart free — the datasource does not change.
95
+ */
96
+ export const FAMILY_POSITION_KINDS: Readonly<Record<ChartFamily, readonly PositionKind[]>> = {
97
+ progress: ['category'],
98
+ distribution: ['category'],
99
+ bar: ['category', 'instant'],
100
+ timeSeries: ['instant'],
101
+ heatmap: ['matrix'],
102
+ };
103
+
104
+ export function familyAcceptsPositionKind(family: ChartFamily, kind: PositionKind): boolean {
105
+ return FAMILY_POSITION_KINDS[family].includes(kind);
106
+ }
107
+
108
+ /**
109
+ * Resolves unit and format through the documented precedence:
110
+ * point -> series -> datasource -> undefined (caller falls back to its default).
111
+ */
112
+ export function resolveFormat<M>(
113
+ point: DataPoint<M> | undefined,
114
+ series: Series<M> | undefined,
115
+ datasource: ChartDatasource<M>,
116
+ ): FormatHint | undefined {
117
+ return point?.format ?? series?.format ?? datasource.format;
118
+ }
119
+
120
+ export function resolveUnit<M>(
121
+ point: DataPoint<M> | undefined,
122
+ series: Series<M> | undefined,
123
+ datasource: ChartDatasource<M>,
124
+ ): string | undefined {
125
+ return point?.unit ?? series?.unit ?? datasource.unit;
126
+ }
@@ -1,133 +1,129 @@
1
- /**
2
- * Typed error taxonomy and failure reporting (FR-038, FR-038a, FR-038b).
3
- *
4
- * Messages come from a **fixed catalogue with no interpolated data**. That is
5
- * what makes the redaction guarantee structural rather than a rule someone has
6
- * to remember: because the package never builds a message out of a value or a
7
- * label, a failure report cannot carry patient data or account balances into a
8
- * consuming app's crash reporting.
9
- */
10
- import type { ChartFamily, PositionKind } from './datasource';
11
-
12
- export type ChartErrorCategory = 'validation' | 'declared' | 'render';
13
-
14
- export type ChartErrorCode =
15
- | 'duplicateSeriesId'
16
- | 'duplicatePointId'
17
- | 'positionKindMismatch'
18
- | 'familyPositionKindUnsupported'
19
- | 'inconsistentMatrix'
20
- | 'distributionNotWhole'
21
- | 'declaredByApp'
22
- | 'renderException';
23
-
24
- /**
25
- * Fixed message catalogue. Every entry is a constant string — no template
26
- * holes, so no data can leak through a message.
27
- */
28
- const MESSAGES: Readonly<Record<ChartErrorCode, string>> = {
29
- duplicateSeriesId:
30
- 'Datasource contains duplicate series identifiers. Series ids must be unique.',
31
- duplicatePointId:
32
- 'A series contains duplicate point identifiers. Point ids must be unique within their series.',
33
- positionKindMismatch:
34
- 'A data point position kind does not match the datasource positionKind.',
35
- familyPositionKindUnsupported:
36
- 'This chart family does not accept the datasource position kind.',
37
- inconsistentMatrix:
38
- 'Matrix datasource rows and columns are inconsistent across points.',
39
- distributionNotWhole:
40
- 'Distribution segments do not sum to the whole. The chart marks the total incomplete rather than rescaling.',
41
- declaredByApp: 'The application declared this chart to be in an error state.',
42
- renderException:
43
- 'The chart failed while rendering and was contained. See the report for details.',
44
- };
45
-
46
- export interface ChartError {
47
- readonly category: ChartErrorCategory;
48
- readonly code: ChartErrorCode;
49
- readonly message: string;
50
- readonly chartId?: string;
51
- readonly family?: ChartFamily;
52
- /**
53
- * Structural detail that is safe to expose: counts and kinds only, never a
54
- * value or a label.
55
- */
56
- readonly detail?: {
57
- readonly expectedKind?: PositionKind;
58
- readonly actualKind?: PositionKind;
59
- readonly occurrences?: number;
60
- };
61
- }
62
-
63
- export interface ChartFailureReport {
64
- readonly chartId?: string;
65
- readonly family: ChartFamily;
66
- readonly error: ChartError;
67
- readonly componentStack?: string;
68
- }
69
-
70
- export type ChartFailureHandler = (report: ChartFailureReport) => void;
71
-
72
- export function chartError(
73
- category: ChartErrorCategory,
74
- code: ChartErrorCode,
75
- extra?: Omit<ChartError, 'category' | 'code' | 'message'>,
76
- ): ChartError {
77
- return {
78
- category,
79
- code,
80
- message: MESSAGES[code],
81
- ...extra,
82
- };
83
- }
84
-
85
- export function validationError(
86
- code: ChartErrorCode,
87
- extra?: Omit<ChartError, 'category' | 'code' | 'message'>,
88
- ): ChartError {
89
- return chartError('validation', code, extra);
90
- }
91
-
92
- export function isChartError(e: unknown): e is ChartError {
93
- if (typeof e !== 'object' || e === null) {
94
- return false;
95
- }
96
- const candidate = e as Partial<ChartError>;
97
- return (
98
- typeof candidate.code === 'string' &&
99
- typeof candidate.message === 'string' &&
100
- (candidate.category === 'validation' ||
101
- candidate.category === 'declared' ||
102
- candidate.category === 'render')
103
- );
104
- }
105
-
106
- /**
107
- * Builds the report handed to the app after a contained render exception.
108
- *
109
- * A `ChartError` thrown by the package keeps its catalogue message. Anything
110
- * else — a defect in the rendering library, say — is reported under the generic
111
- * `renderException` message. We do **not** copy a third-party exception's own
112
- * message into the report, because we cannot promise it is free of data.
113
- */
114
- export function toFailureReport(
115
- family: ChartFamily,
116
- thrown: unknown,
117
- chartId?: string,
118
- componentStack?: string,
119
- ): ChartFailureReport {
120
- const error: ChartError = isChartError(thrown)
121
- ? { ...thrown, chartId, family }
122
- : chartError('render', 'renderException', { chartId, family });
123
-
124
- return { chartId, family, error, componentStack };
125
- }
126
-
127
- /**
128
- * Stable identity for a failure, used to de-duplicate reports so an unrelated
129
- * parent re-render does not re-report the same problem.
130
- */
131
- export function failureSignature(report: ChartFailureReport): string {
132
- return `${report.chartId ?? 'anon'}:${report.family}:${report.error.code}`;
133
- }
1
+ /**
2
+ * Typed error taxonomy and failure reporting (FR-038, FR-038a, FR-038b).
3
+ *
4
+ * Messages come from a **fixed catalogue with no interpolated data**. That is
5
+ * what makes the redaction guarantee structural rather than a rule someone has
6
+ * to remember: because the package never builds a message out of a value or a
7
+ * label, a failure report cannot carry patient data or account balances into a
8
+ * consuming app's crash reporting.
9
+ */
10
+ import type { ChartFamily, PositionKind } from './datasource';
11
+
12
+ export type ChartErrorCategory = 'validation' | 'declared' | 'render';
13
+
14
+ export type ChartErrorCode =
15
+ | 'duplicateSeriesId'
16
+ | 'duplicatePointId'
17
+ | 'positionKindMismatch'
18
+ | 'familyPositionKindUnsupported'
19
+ | 'inconsistentMatrix'
20
+ | 'distributionNotWhole'
21
+ | 'declaredByApp'
22
+ | 'renderException';
23
+
24
+ /**
25
+ * Fixed message catalogue. Every entry is a constant string — no template
26
+ * holes, so no data can leak through a message.
27
+ */
28
+ const MESSAGES: Readonly<Record<ChartErrorCode, string>> = {
29
+ duplicateSeriesId: 'Datasource contains duplicate series identifiers. Series ids must be unique.',
30
+ duplicatePointId:
31
+ 'A series contains duplicate point identifiers. Point ids must be unique within their series.',
32
+ positionKindMismatch: 'A data point position kind does not match the datasource positionKind.',
33
+ familyPositionKindUnsupported: 'This chart family does not accept the datasource position kind.',
34
+ inconsistentMatrix: 'Matrix datasource rows and columns are inconsistent across points.',
35
+ distributionNotWhole:
36
+ 'Distribution segments do not sum to the whole. The chart marks the total incomplete rather than rescaling.',
37
+ declaredByApp: 'The application declared this chart to be in an error state.',
38
+ renderException:
39
+ 'The chart failed while rendering and was contained. See the report for details.',
40
+ };
41
+
42
+ export interface ChartError {
43
+ readonly category: ChartErrorCategory;
44
+ readonly code: ChartErrorCode;
45
+ readonly message: string;
46
+ readonly chartId?: string;
47
+ readonly family?: ChartFamily;
48
+ /**
49
+ * Structural detail that is safe to expose: counts and kinds only, never a
50
+ * value or a label.
51
+ */
52
+ readonly detail?: {
53
+ readonly expectedKind?: PositionKind;
54
+ readonly actualKind?: PositionKind;
55
+ readonly occurrences?: number;
56
+ };
57
+ }
58
+
59
+ export interface ChartFailureReport {
60
+ readonly chartId?: string;
61
+ readonly family: ChartFamily;
62
+ readonly error: ChartError;
63
+ readonly componentStack?: string;
64
+ }
65
+
66
+ export type ChartFailureHandler = (report: ChartFailureReport) => void;
67
+
68
+ export function chartError(
69
+ category: ChartErrorCategory,
70
+ code: ChartErrorCode,
71
+ extra?: Omit<ChartError, 'category' | 'code' | 'message'>,
72
+ ): ChartError {
73
+ return {
74
+ category,
75
+ code,
76
+ message: MESSAGES[code],
77
+ ...extra,
78
+ };
79
+ }
80
+
81
+ export function validationError(
82
+ code: ChartErrorCode,
83
+ extra?: Omit<ChartError, 'category' | 'code' | 'message'>,
84
+ ): ChartError {
85
+ return chartError('validation', code, extra);
86
+ }
87
+
88
+ export function isChartError(e: unknown): e is ChartError {
89
+ if (typeof e !== 'object' || e === null) {
90
+ return false;
91
+ }
92
+ const candidate = e as Partial<ChartError>;
93
+ return (
94
+ typeof candidate.code === 'string' &&
95
+ typeof candidate.message === 'string' &&
96
+ (candidate.category === 'validation' ||
97
+ candidate.category === 'declared' ||
98
+ candidate.category === 'render')
99
+ );
100
+ }
101
+
102
+ /**
103
+ * Builds the report handed to the app after a contained render exception.
104
+ *
105
+ * A `ChartError` thrown by the package keeps its catalogue message. Anything
106
+ * else — a defect in the rendering library, say — is reported under the generic
107
+ * `renderException` message. We do **not** copy a third-party exception's own
108
+ * message into the report, because we cannot promise it is free of data.
109
+ */
110
+ export function toFailureReport(
111
+ family: ChartFamily,
112
+ thrown: unknown,
113
+ chartId?: string,
114
+ componentStack?: string,
115
+ ): ChartFailureReport {
116
+ const error: ChartError = isChartError(thrown)
117
+ ? { ...thrown, chartId, family }
118
+ : chartError('render', 'renderException', { chartId, family });
119
+
120
+ return { chartId, family, error, componentStack };
121
+ }
122
+
123
+ /**
124
+ * Stable identity for a failure, used to de-duplicate reports so an unrelated
125
+ * parent re-render does not re-report the same problem.
126
+ */
127
+ export function failureSignature(report: ChartFailureReport): string {
128
+ return `${report.chartId ?? 'anon'}:${report.family}:${report.error.code}`;
129
+ }