@chipmobilesdk/rn-chart 0.1.1 → 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.
- package/CHANGELOG.md +126 -74
- package/README.md +337 -314
- package/package.json +46 -49
- package/skills/sdk-rn-chart/SKILL.md +1 -2
- package/src/a11y/summary.ts +170 -177
- package/src/contract/datasource.ts +126 -134
- package/src/contract/errors.ts +129 -133
- package/src/contract/format.ts +162 -168
- package/src/contract/interaction.ts +70 -71
- package/src/contract/marker.ts +75 -75
- package/src/contract/props.ts +57 -60
- package/src/contract/state.ts +88 -94
- package/src/contract/theme.ts +169 -139
- package/src/contract/value.ts +121 -125
- package/src/geometry/bar.ts +271 -256
- package/src/geometry/distribution.ts +197 -200
- package/src/geometry/heatmap.ts +189 -195
- package/src/geometry/hitTest.ts +117 -120
- package/src/geometry/layout.ts +162 -121
- package/src/geometry/markers.ts +55 -0
- package/src/geometry/path.ts +193 -199
- package/src/geometry/progress.ts +223 -204
- package/src/geometry/scale.ts +140 -146
- package/src/geometry/ticks.ts +145 -153
- package/src/geometry/timeSeries.ts +168 -158
- package/src/geometry/types.ts +94 -88
- package/src/index.ts +158 -176
- package/src/react/ChartBoundary.tsx +96 -96
- package/src/react/ChartFrame.tsx +583 -620
- package/src/react/ChartLegend.tsx +211 -177
- package/src/react/ChartThemeProvider.tsx +44 -48
- package/src/react/charts.tsx +154 -161
- package/src/react/useChartLayout.ts +124 -124
- package/src/react/useChartSelection.ts +152 -170
- package/src/render/Axis.tsx +120 -122
- package/src/render/Marks.tsx +173 -205
- package/src/validation/budgets.ts +103 -110
- 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
|
+
}
|