@chipmobilesdk/rn-chart 0.3.0 → 0.4.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.
- package/CHANGELOG.md +18 -0
- package/README.md +44 -2
- package/package.json +1 -1
- package/skills/sdk-rn-chart/SKILL.md +49 -14
- package/src/a11y/summary.ts +34 -15
- package/src/contract/distribution.ts +44 -0
- package/src/contract/errors.ts +6 -0
- package/src/contract/interaction.ts +2 -2
- package/src/contract/strings.ts +2 -0
- package/src/geometry/bar.ts +27 -16
- package/src/geometry/distribution.ts +11 -6
- package/src/geometry/heatmap.ts +34 -17
- package/src/geometry/hitTest.ts +35 -0
- package/src/geometry/progress.ts +0 -1
- package/src/geometry/ticks.ts +0 -13
- package/src/geometry/timeSeries.ts +7 -2
- package/src/geometry/types.ts +11 -0
- package/src/index.ts +4 -1
- package/src/react/ChartFrame.tsx +126 -373
- package/src/react/ChartLegend.tsx +60 -3
- package/src/react/ChartPlot.tsx +57 -0
- package/src/react/ChartStateOverlay.tsx +94 -0
- package/src/react/charts.tsx +27 -12
- package/src/react/pointEvent.ts +40 -0
- package/src/react/useChartA11y.ts +113 -0
- package/src/react/useChartFormatters.ts +105 -0
- package/src/react/useChartLayout.ts +5 -2
- package/src/react/useChartSelection.ts +77 -40
- package/src/react/useChartStatus.ts +85 -0
- package/src/render/Axis.tsx +26 -22
- package/src/render/Marks.tsx +2 -9
- package/src/validation/budgets.ts +6 -4
- package/src/validation/validateDatasource.ts +38 -14
package/src/geometry/hitTest.ts
CHANGED
|
@@ -23,6 +23,19 @@ export interface HitRegion {
|
|
|
23
23
|
readonly width: number;
|
|
24
24
|
readonly height: number;
|
|
25
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* An arc segment: containment by radius and angle. Angles are radians,
|
|
28
|
+
* clockwise from twelve o'clock, as the distribution geometry draws them.
|
|
29
|
+
* A region with an arc is hit only inside it, never by proximity.
|
|
30
|
+
*/
|
|
31
|
+
readonly arc?: {
|
|
32
|
+
readonly cx: number;
|
|
33
|
+
readonly cy: number;
|
|
34
|
+
readonly innerRadius: number;
|
|
35
|
+
readonly outerRadius: number;
|
|
36
|
+
readonly startAngle: number;
|
|
37
|
+
readonly endAngle: number;
|
|
38
|
+
};
|
|
26
39
|
/**
|
|
27
40
|
* False for points that must never be selectable: withheld values, and
|
|
28
41
|
* anything the chart is not currently drawing (FR-053).
|
|
@@ -47,6 +60,20 @@ function inBounds(region: HitRegion, x: number, y: number): boolean {
|
|
|
47
60
|
return x >= b.x && x <= b.x + b.width && y >= b.y && y <= b.y + b.height;
|
|
48
61
|
}
|
|
49
62
|
|
|
63
|
+
const TAU = Math.PI * 2;
|
|
64
|
+
|
|
65
|
+
function inArc(arc: NonNullable<HitRegion['arc']>, x: number, y: number): boolean {
|
|
66
|
+
const dx = x - arc.cx;
|
|
67
|
+
const dy = y - arc.cy;
|
|
68
|
+
const radius = Math.sqrt(dx * dx + dy * dy);
|
|
69
|
+
if (radius < arc.innerRadius || radius > arc.outerRadius) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Clockwise from the top, the convention the arcs are drawn in.
|
|
73
|
+
const angle = (Math.atan2(dy, dx) + Math.PI / 2 + TAU) % TAU;
|
|
74
|
+
return angle >= arc.startAngle && angle < arc.endAngle;
|
|
75
|
+
}
|
|
76
|
+
|
|
50
77
|
/**
|
|
51
78
|
* Resolves a touch to a mark.
|
|
52
79
|
*
|
|
@@ -70,6 +97,14 @@ export function hitTest(
|
|
|
70
97
|
if (inBounds(region, x, y)) {
|
|
71
98
|
return { seriesId: region.seriesId, pointId: region.pointId, distance: 0 };
|
|
72
99
|
}
|
|
100
|
+
if (region.arc) {
|
|
101
|
+
// A large segment's edge is far from its midpoint, and a neighbour's
|
|
102
|
+
// midpoint can be nearer: only the segment under the finger counts.
|
|
103
|
+
if (inArc(region.arc, x, y)) {
|
|
104
|
+
return { seriesId: region.seriesId, pointId: region.pointId, distance: 0 };
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
73
108
|
|
|
74
109
|
const dx = region.x - x;
|
|
75
110
|
const dy = region.y - y;
|
package/src/geometry/progress.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { rectSubpath } from './path';
|
|
|
17
17
|
import type { GeometryInput, GeometryLabel, MarkerLine, MarkPath, ResolvedGeometry } from './types';
|
|
18
18
|
|
|
19
19
|
export interface ProgressOptions {
|
|
20
|
-
readonly orientation?: 'horizontal' | 'vertical';
|
|
21
20
|
readonly trackRadius?: number;
|
|
22
21
|
}
|
|
23
22
|
|
package/src/geometry/ticks.ts
CHANGED
|
@@ -130,16 +130,3 @@ export function reserveAxisSpace(
|
|
|
130
130
|
}
|
|
131
131
|
return widest + LABEL_GUTTER;
|
|
132
132
|
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Shortens a label that cannot fit, signalling the truncation with an ellipsis.
|
|
136
|
-
* Callers must expose the full value through an accessible label (FR-070).
|
|
137
|
-
*/
|
|
138
|
-
export function truncateToWidth(text: string, fontSize: number, maxWidthPx: number): string {
|
|
139
|
-
if (estimateTextWidth(text, fontSize) <= maxWidthPx) {
|
|
140
|
-
return text;
|
|
141
|
-
}
|
|
142
|
-
const perChar = fontSize * AVERAGE_ADVANCE_RATIO;
|
|
143
|
-
const fits = Math.max(1, Math.floor(maxWidthPx / perChar) - 1);
|
|
144
|
-
return `${text.slice(0, fits)}…`;
|
|
145
|
-
}
|
|
@@ -50,8 +50,13 @@ export function buildTimeSeriesGeometry<M>(
|
|
|
50
50
|
const values: number[] = [];
|
|
51
51
|
let excluded = 0;
|
|
52
52
|
|
|
53
|
+
// Sorted and windowed once per series; both passes below read the result.
|
|
54
|
+
const ordered = new Map(
|
|
55
|
+
datasource.series.map(series => [series, visiblePoints(series, options.range)] as const),
|
|
56
|
+
);
|
|
57
|
+
|
|
53
58
|
for (const series of datasource.series) {
|
|
54
|
-
for (const point of
|
|
59
|
+
for (const point of ordered.get(series) ?? []) {
|
|
55
60
|
if (point.position.kind === 'instant') {
|
|
56
61
|
instants.push(point.position.at);
|
|
57
62
|
}
|
|
@@ -104,7 +109,7 @@ export function buildTimeSeriesGeometry<M>(
|
|
|
104
109
|
|
|
105
110
|
const pts: MaybePt[] = [];
|
|
106
111
|
|
|
107
|
-
for (const point of
|
|
112
|
+
for (const point of ordered.get(series) ?? []) {
|
|
108
113
|
const at = point.position.kind === 'instant' ? point.position.at : 0;
|
|
109
114
|
const px = x.project(at);
|
|
110
115
|
|
package/src/geometry/types.ts
CHANGED
|
@@ -46,6 +46,13 @@ export interface GeometryLabel {
|
|
|
46
46
|
readonly muted?: boolean;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** What a colour ramp runs from and to, for the legend that explains it. */
|
|
50
|
+
export interface ColorScale {
|
|
51
|
+
readonly min: number;
|
|
52
|
+
readonly max: number;
|
|
53
|
+
readonly colors: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
49
56
|
export interface ResolvedGeometry {
|
|
50
57
|
readonly plot: PlotRect;
|
|
51
58
|
readonly paths: readonly MarkPath[];
|
|
@@ -54,6 +61,8 @@ export interface ResolvedGeometry {
|
|
|
54
61
|
readonly xTicks: readonly Tick[];
|
|
55
62
|
readonly yTicks: readonly Tick[];
|
|
56
63
|
readonly hitRegions: readonly HitRegion[];
|
|
64
|
+
/** Present when colour encodes a value, as in a heatmap. */
|
|
65
|
+
readonly colorScale?: ColorScale;
|
|
57
66
|
/**
|
|
58
67
|
* True when something was excluded from a total or proportion, so the chart
|
|
59
68
|
* can say so rather than presenting a partial figure as whole (FR-013).
|
|
@@ -88,6 +97,8 @@ export interface GeometryInput {
|
|
|
88
97
|
Record<'unknown' | 'notApplicable' | 'withheld', NonValueTexture>
|
|
89
98
|
>;
|
|
90
99
|
readonly heatmapRamp: readonly string[];
|
|
100
|
+
/** Corner radii from the theme, for bars and heatmap cells. */
|
|
101
|
+
readonly radius?: { readonly bar: number; readonly cell: number };
|
|
91
102
|
readonly formatValue: (value: number) => string;
|
|
92
103
|
readonly formatCategory: (raw: string) => string;
|
|
93
104
|
readonly formatInstant: (at: number) => string;
|
package/src/index.ts
CHANGED
|
@@ -62,6 +62,8 @@ export {
|
|
|
62
62
|
resolveUnit,
|
|
63
63
|
} from './contract/datasource';
|
|
64
64
|
|
|
65
|
+
export type { DistributionValues } from './contract/distribution';
|
|
66
|
+
|
|
65
67
|
export type { MarkerKind, ProgressReading, ReferenceMarker } from './contract/marker';
|
|
66
68
|
export { findTarget, isUsableTarget, readProgress } from './contract/marker';
|
|
67
69
|
|
|
@@ -149,7 +151,8 @@ export {
|
|
|
149
151
|
TimeSeriesChart,
|
|
150
152
|
} from './react/charts';
|
|
151
153
|
|
|
152
|
-
export { ChartLegend, buildLegendEntries } from './react/ChartLegend';
|
|
154
|
+
export { ChartLegend, ColorScaleLegend, buildLegendEntries } from './react/ChartLegend';
|
|
155
|
+
export type { ColorScale } from './geometry/types';
|
|
153
156
|
export { ChartThemeProvider, useChartTheme } from './react/ChartThemeProvider';
|
|
154
157
|
|
|
155
158
|
// ---------------------------------------------------------------------------
|