@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,174 +1,174 @@
1
- /**
2
- * Datasource validation (FR-015, FR-017, FR-007a).
3
- *
4
- * Rules V1-V8 from specs/007-rn-chart-package/data-model.md §6. Every failure
5
- * returns a `ChartError` built from the fixed message catalogue, so validation
6
- * output can never carry a data value or a series label.
7
- *
8
- * Note V8: an empty datasource is **not** an error. It resolves to the empty
9
- * state, which is a first-class outcome rather than a failure.
10
- */
11
- import {
12
- familyAcceptsPositionKind,
13
- type ChartDatasource,
14
- type ChartFamily,
15
- } from '../contract/datasource';
16
- import { validationError, type ChartError } from '../contract/errors';
17
- import { isKnown } from '../contract/value';
18
-
19
- export interface ValidationResult {
20
- readonly error?: ChartError;
21
- /** No points at all across every series (V8). */
22
- readonly isEmpty: boolean;
23
- /** Some points are known and others are not (FR-032). */
24
- readonly isPartial: boolean;
25
- /** Distribution segments that do not sum to the whole (V6, FR-024). */
26
- readonly totalIncomplete: boolean;
27
- }
28
-
29
- const DISTRIBUTION_SUM_TOLERANCE = 0.001;
30
-
31
- export function validateDatasource<M>(
32
- datasource: ChartDatasource<M>,
33
- family: ChartFamily,
34
- chartId?: string,
35
- ): ValidationResult {
36
- const base = { isEmpty: true, isPartial: false, totalIncomplete: false };
37
-
38
- // V4 — the receiving family must accept the declared position kind. Checked
39
- // first because a mismatch here makes everything downstream meaningless.
40
- if (!familyAcceptsPositionKind(family, datasource.positionKind)) {
41
- return {
42
- ...base,
43
- isEmpty: false,
44
- error: validationError('familyPositionKindUnsupported', {
45
- chartId,
46
- family,
47
- detail: { actualKind: datasource.positionKind },
48
- }),
49
- };
50
- }
51
-
52
- // V1 — series ids unique.
53
- const seenSeriesIds = new Set<string>();
54
- for (const series of datasource.series) {
55
- if (seenSeriesIds.has(series.id)) {
56
- return {
57
- ...base,
58
- isEmpty: false,
59
- error: validationError('duplicateSeriesId', {
60
- chartId,
61
- family,
62
- detail: { occurrences: seenSeriesIds.size },
63
- }),
64
- };
65
- }
66
- seenSeriesIds.add(series.id);
67
- }
68
-
69
- let totalPoints = 0;
70
- let knownPoints = 0;
71
- const matrixRows = new Set<string>();
72
- const matrixColumns = new Set<string>();
73
-
74
- for (const series of datasource.series) {
75
- // V2 — point ids unique within their series.
76
- const seenPointIds = new Set<string>();
77
-
78
- for (const point of series.points) {
79
- if (seenPointIds.has(point.id)) {
80
- return {
81
- ...base,
82
- isEmpty: false,
83
- error: validationError('duplicatePointId', {
84
- chartId,
85
- family,
86
- detail: { occurrences: seenPointIds.size },
87
- }),
88
- };
89
- }
90
- seenPointIds.add(point.id);
91
-
92
- // V3 — every point matches the declared position kind.
93
- if (point.position.kind !== datasource.positionKind) {
94
- return {
95
- ...base,
96
- isEmpty: false,
97
- error: validationError('positionKindMismatch', {
98
- chartId,
99
- family,
100
- detail: {
101
- expectedKind: datasource.positionKind,
102
- actualKind: point.position.kind,
103
- },
104
- }),
105
- };
106
- }
107
-
108
- if (point.position.kind === 'matrix') {
109
- matrixRows.add(point.position.row);
110
- matrixColumns.add(point.position.column);
111
- }
112
-
113
- totalPoints += 1;
114
- if (isKnown(point.value)) {
115
- knownPoints += 1;
116
- }
117
- }
118
- }
119
-
120
- // V5 — a matrix must be rectangular: every row/column pair accounted for.
121
- if (datasource.positionKind === 'matrix' && totalPoints > 0) {
122
- const expected = matrixRows.size * matrixColumns.size;
123
- if (totalPoints !== expected) {
124
- return {
125
- ...base,
126
- isEmpty: false,
127
- error: validationError('inconsistentMatrix', {
128
- chartId,
129
- family,
130
- detail: { occurrences: Math.abs(expected - totalPoints) },
131
- }),
132
- };
133
- }
134
- }
135
-
136
- // V6 — distribution segments that do not sum to the whole are flagged, never
137
- // rescaled. This is a flag rather than a rejection: the chart still renders
138
- // and marks the total incomplete.
139
- let totalIncomplete = knownPoints < totalPoints;
140
- if (family === 'distribution' && totalPoints > 0) {
141
- totalIncomplete = totalIncomplete || !distributionSumsToWhole(datasource);
142
- }
143
-
144
- return {
145
- isEmpty: totalPoints === 0,
146
- isPartial: totalPoints > 0 && knownPoints > 0 && knownPoints < totalPoints,
147
- totalIncomplete,
148
- };
149
- }
150
-
151
- /**
152
- * Distribution values may be supplied either as proportions summing to 1 or as
153
- * absolute values that the chart normalises for display. Only the proportion
154
- * case can be checked for wholeness, and only when every point is known.
155
- */
156
- function distributionSumsToWhole<M>(datasource: ChartDatasource<M>): boolean {
157
- const values: number[] = [];
158
- for (const series of datasource.series) {
159
- for (const point of series.points) {
160
- if (!isKnown(point.value)) {
161
- return false;
162
- }
163
- values.push(point.value.value);
164
- }
165
- }
166
-
167
- const sum = values.reduce((acc, n) => acc + n, 0);
168
- const looksLikeProportions = values.every(v => v >= 0 && v <= 1);
169
- if (!looksLikeProportions) {
170
- // Absolute values: any positive sum is a whole by definition.
171
- return sum > 0;
172
- }
173
- return Math.abs(sum - 1) <= DISTRIBUTION_SUM_TOLERANCE;
174
- }
1
+ /**
2
+ * Datasource validation (FR-015, FR-017, FR-007a).
3
+ *
4
+ * Rules V1-V8 from specs/007-rn-chart-package/data-model.md §6. Every failure
5
+ * returns a `ChartError` built from the fixed message catalogue, so validation
6
+ * output can never carry a data value or a series label.
7
+ *
8
+ * Note V8: an empty datasource is **not** an error. It resolves to the empty
9
+ * state, which is a first-class outcome rather than a failure.
10
+ */
11
+ import {
12
+ familyAcceptsPositionKind,
13
+ type ChartDatasource,
14
+ type ChartFamily,
15
+ } from '../contract/datasource';
16
+ import { validationError, type ChartError } from '../contract/errors';
17
+ import { isKnown } from '../contract/value';
18
+
19
+ export interface ValidationResult {
20
+ readonly error?: ChartError;
21
+ /** No points at all across every series (V8). */
22
+ readonly isEmpty: boolean;
23
+ /** Some points are known and others are not (FR-032). */
24
+ readonly isPartial: boolean;
25
+ /** Distribution segments that do not sum to the whole (V6, FR-024). */
26
+ readonly totalIncomplete: boolean;
27
+ }
28
+
29
+ const DISTRIBUTION_SUM_TOLERANCE = 0.001;
30
+
31
+ export function validateDatasource<M>(
32
+ datasource: ChartDatasource<M>,
33
+ family: ChartFamily,
34
+ chartId?: string,
35
+ ): ValidationResult {
36
+ const base = { isEmpty: true, isPartial: false, totalIncomplete: false };
37
+
38
+ // V4 — the receiving family must accept the declared position kind. Checked
39
+ // first because a mismatch here makes everything downstream meaningless.
40
+ if (!familyAcceptsPositionKind(family, datasource.positionKind)) {
41
+ return {
42
+ ...base,
43
+ isEmpty: false,
44
+ error: validationError('familyPositionKindUnsupported', {
45
+ chartId,
46
+ family,
47
+ detail: { actualKind: datasource.positionKind },
48
+ }),
49
+ };
50
+ }
51
+
52
+ // V1 — series ids unique.
53
+ const seenSeriesIds = new Set<string>();
54
+ for (const series of datasource.series) {
55
+ if (seenSeriesIds.has(series.id)) {
56
+ return {
57
+ ...base,
58
+ isEmpty: false,
59
+ error: validationError('duplicateSeriesId', {
60
+ chartId,
61
+ family,
62
+ detail: { occurrences: seenSeriesIds.size },
63
+ }),
64
+ };
65
+ }
66
+ seenSeriesIds.add(series.id);
67
+ }
68
+
69
+ let totalPoints = 0;
70
+ let knownPoints = 0;
71
+ const matrixRows = new Set<string>();
72
+ const matrixColumns = new Set<string>();
73
+
74
+ for (const series of datasource.series) {
75
+ // V2 — point ids unique within their series.
76
+ const seenPointIds = new Set<string>();
77
+
78
+ for (const point of series.points) {
79
+ if (seenPointIds.has(point.id)) {
80
+ return {
81
+ ...base,
82
+ isEmpty: false,
83
+ error: validationError('duplicatePointId', {
84
+ chartId,
85
+ family,
86
+ detail: { occurrences: seenPointIds.size },
87
+ }),
88
+ };
89
+ }
90
+ seenPointIds.add(point.id);
91
+
92
+ // V3 — every point matches the declared position kind.
93
+ if (point.position.kind !== datasource.positionKind) {
94
+ return {
95
+ ...base,
96
+ isEmpty: false,
97
+ error: validationError('positionKindMismatch', {
98
+ chartId,
99
+ family,
100
+ detail: {
101
+ expectedKind: datasource.positionKind,
102
+ actualKind: point.position.kind,
103
+ },
104
+ }),
105
+ };
106
+ }
107
+
108
+ if (point.position.kind === 'matrix') {
109
+ matrixRows.add(point.position.row);
110
+ matrixColumns.add(point.position.column);
111
+ }
112
+
113
+ totalPoints += 1;
114
+ if (isKnown(point.value)) {
115
+ knownPoints += 1;
116
+ }
117
+ }
118
+ }
119
+
120
+ // V5 — a matrix must be rectangular: every row/column pair accounted for.
121
+ if (datasource.positionKind === 'matrix' && totalPoints > 0) {
122
+ const expected = matrixRows.size * matrixColumns.size;
123
+ if (totalPoints !== expected) {
124
+ return {
125
+ ...base,
126
+ isEmpty: false,
127
+ error: validationError('inconsistentMatrix', {
128
+ chartId,
129
+ family,
130
+ detail: { occurrences: Math.abs(expected - totalPoints) },
131
+ }),
132
+ };
133
+ }
134
+ }
135
+
136
+ // V6 — distribution segments that do not sum to the whole are flagged, never
137
+ // rescaled. This is a flag rather than a rejection: the chart still renders
138
+ // and marks the total incomplete.
139
+ let totalIncomplete = knownPoints < totalPoints;
140
+ if (family === 'distribution' && totalPoints > 0) {
141
+ totalIncomplete = totalIncomplete || !distributionSumsToWhole(datasource);
142
+ }
143
+
144
+ return {
145
+ isEmpty: totalPoints === 0,
146
+ isPartial: totalPoints > 0 && knownPoints > 0 && knownPoints < totalPoints,
147
+ totalIncomplete,
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Distribution values may be supplied either as proportions summing to 1 or as
153
+ * absolute values that the chart normalises for display. Only the proportion
154
+ * case can be checked for wholeness, and only when every point is known.
155
+ */
156
+ function distributionSumsToWhole<M>(datasource: ChartDatasource<M>): boolean {
157
+ const values: number[] = [];
158
+ for (const series of datasource.series) {
159
+ for (const point of series.points) {
160
+ if (!isKnown(point.value)) {
161
+ return false;
162
+ }
163
+ values.push(point.value.value);
164
+ }
165
+ }
166
+
167
+ const sum = values.reduce((acc, n) => acc + n, 0);
168
+ const looksLikeProportions = values.every(v => v >= 0 && v <= 1);
169
+ if (!looksLikeProportions) {
170
+ // Absolute values: any positive sum is a whole by definition.
171
+ return sum > 0;
172
+ }
173
+ return Math.abs(sum - 1) <= DISTRIBUTION_SUM_TOLERANCE;
174
+ }