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