@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +126 -74
  2. package/README.md +337 -314
  3. package/package.json +46 -49
  4. package/skills/sdk-rn-chart/SKILL.md +1 -2
  5. package/src/a11y/summary.ts +170 -177
  6. package/src/contract/datasource.ts +126 -134
  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 -139
  14. package/src/contract/value.ts +121 -125
  15. package/src/geometry/bar.ts +271 -256
  16. package/src/geometry/distribution.ts +197 -200
  17. package/src/geometry/heatmap.ts +189 -195
  18. package/src/geometry/hitTest.ts +117 -120
  19. package/src/geometry/layout.ts +162 -121
  20. package/src/geometry/markers.ts +55 -0
  21. package/src/geometry/path.ts +193 -199
  22. package/src/geometry/progress.ts +223 -204
  23. package/src/geometry/scale.ts +140 -146
  24. package/src/geometry/ticks.ts +145 -153
  25. package/src/geometry/timeSeries.ts +168 -158
  26. package/src/geometry/types.ts +94 -88
  27. package/src/index.ts +158 -176
  28. package/src/react/ChartBoundary.tsx +96 -96
  29. package/src/react/ChartFrame.tsx +583 -620
  30. package/src/react/ChartLegend.tsx +211 -177
  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,121 +1,162 @@
1
- /**
2
- * Plot layout: turning a container size into a drawable plot rectangle.
3
- *
4
- * Font scale arrives as a parameter rather than being read from `PixelRatio`
5
- * here, so this module stays pure and testable at any scale without mocking a
6
- * platform API. The React layer reads the real value and passes it in.
7
- *
8
- * PURE. No React, no SVG.
9
- */
10
- import { reserveAxisSpace } from './ticks';
11
-
12
- export interface PlotRect {
13
- readonly x: number;
14
- readonly y: number;
15
- readonly width: number;
16
- readonly height: number;
17
- }
18
-
19
- export interface ContainerSize {
20
- readonly width: number;
21
- readonly height: number;
22
- }
23
-
24
- export interface LayoutOptions {
25
- readonly container: ContainerSize;
26
- /** Axis label text, used to reserve exactly as much space as is needed. */
27
- readonly xLabels?: readonly string[];
28
- readonly yLabels?: readonly string[];
29
- readonly baseFontSize: number;
30
- /** System text-size multiplier, from `PixelRatio.getFontScale()` (FR-071). */
31
- readonly fontScale: number;
32
- readonly padding?: number;
33
- }
34
-
35
- /**
36
- * Ceiling on how far system text scaling may inflate chart labels.
37
- *
38
- * Beyond this the plot area collapses faster than the labels become useful, so
39
- * we cap the growth rather than let the chart squeeze itself out of existence.
40
- * The cap is documented in the README as the supported dynamic-type range.
41
- */
42
- export const MAX_FONT_SCALE = 1.6;
43
-
44
- export function effectiveFontSize(
45
- baseFontSize: number,
46
- fontScale: number,
47
- ): number {
48
- const clamped = Math.max(1, Math.min(fontScale, MAX_FONT_SCALE));
49
- return Math.round(baseFontSize * clamped);
50
- }
51
-
52
- /** A container that has not been measured yet, or has collapsed (FR-078). */
53
- export function isMeasurable(size: ContainerSize): boolean {
54
- return (
55
- Number.isFinite(size.width) &&
56
- Number.isFinite(size.height) &&
57
- size.width > 0 &&
58
- size.height > 0
59
- );
60
- }
61
-
62
- export const EMPTY_PLOT: PlotRect = { x: 0, y: 0, width: 0, height: 0 };
63
-
64
- /**
65
- * Reserves axis space from estimated label widths, then hands back what is
66
- * left for the marks.
67
- *
68
- * If the reservations would leave nothing to draw on, they are scaled back:
69
- * a cramped axis beats a zero-area plot.
70
- */
71
- export function resolvePlotRect(options: LayoutOptions): PlotRect {
72
- if (!isMeasurable(options.container)) {
73
- return EMPTY_PLOT;
74
- }
75
-
76
- const pad = options.padding ?? 0;
77
- const fontSize = effectiveFontSize(options.baseFontSize, options.fontScale);
78
-
79
- let left = pad;
80
- let bottom = pad;
81
-
82
- if (options.yLabels && options.yLabels.length > 0) {
83
- left += reserveAxisSpace(options.yLabels, fontSize, 'vertical');
84
- }
85
- if (options.xLabels && options.xLabels.length > 0) {
86
- bottom += reserveAxisSpace(options.xLabels, fontSize, 'horizontal');
87
- }
88
-
89
- const availableWidth = options.container.width - pad;
90
- const availableHeight = options.container.height - pad;
91
-
92
- // Never let axis reservations consume more than 40% of either dimension.
93
- const maxLeft = availableWidth * 0.4;
94
- const maxBottom = availableHeight * 0.4;
95
- left = Math.min(left, maxLeft);
96
- bottom = Math.min(bottom, maxBottom);
97
-
98
- return {
99
- x: left,
100
- y: pad,
101
- width: Math.max(0, options.container.width - left - pad),
102
- height: Math.max(0, options.container.height - bottom - pad),
103
- };
104
- }
105
-
106
- export function plotBottom(plot: PlotRect): number {
107
- return plot.y + plot.height;
108
- }
109
-
110
- export function plotRight(plot: PlotRect): number {
111
- return plot.x + plot.width;
112
- }
113
-
114
- export function containsPoint(plot: PlotRect, x: number, y: number): boolean {
115
- return (
116
- x >= plot.x &&
117
- x <= plotRight(plot) &&
118
- y >= plot.y &&
119
- y <= plotBottom(plot)
120
- );
121
- }
1
+ /**
2
+ * Plot layout: turning a container size into a drawable plot rectangle.
3
+ *
4
+ * Font scale arrives as a parameter rather than being read from `PixelRatio`
5
+ * here, so this module stays pure and testable at any scale without mocking a
6
+ * platform API. The React layer reads the real value and passes it in.
7
+ *
8
+ * PURE. No React, no SVG.
9
+ */
10
+ import { estimateTextWidth, reserveAxisSpace } from './ticks';
11
+
12
+ export interface PlotRect {
13
+ readonly x: number;
14
+ readonly y: number;
15
+ readonly width: number;
16
+ readonly height: number;
17
+ }
18
+
19
+ export interface ContainerSize {
20
+ readonly width: number;
21
+ readonly height: number;
22
+ }
23
+
24
+ export interface LayoutOptions {
25
+ readonly container: ContainerSize;
26
+ /** Axis label text, used to reserve exactly as much space as is needed. */
27
+ readonly xLabels?: readonly string[];
28
+ readonly yLabels?: readonly string[];
29
+ readonly baseFontSize: number;
30
+ /** System text-size multiplier, from `PixelRatio.getFontScale()` (FR-071). */
31
+ readonly fontScale: number;
32
+ readonly padding?: number;
33
+ }
34
+
35
+ /**
36
+ * Ceiling on how far system text scaling may inflate chart labels.
37
+ *
38
+ * Beyond this the plot area collapses faster than the labels become useful, so
39
+ * we cap the growth rather than let the chart squeeze itself out of existence.
40
+ * The cap is documented in the README as the supported dynamic-type range.
41
+ */
42
+ export const MAX_FONT_SCALE = 1.6;
43
+
44
+ export function effectiveFontSize(baseFontSize: number, fontScale: number): number {
45
+ const clamped = Math.max(1, Math.min(fontScale, MAX_FONT_SCALE));
46
+ return Math.round(baseFontSize * clamped);
47
+ }
48
+
49
+ /** A container that has not been measured yet, or has collapsed (FR-078). */
50
+ export function isMeasurable(size: ContainerSize): boolean {
51
+ return (
52
+ Number.isFinite(size.width) && Number.isFinite(size.height) && size.width > 0 && size.height > 0
53
+ );
54
+ }
55
+
56
+ export const EMPTY_PLOT: PlotRect = { x: 0, y: 0, width: 0, height: 0 };
57
+
58
+ /**
59
+ * Reserves axis space from estimated label widths, then hands back what is
60
+ * left for the marks.
61
+ *
62
+ * If the reservations would leave nothing to draw on, they are scaled back:
63
+ * a cramped axis beats a zero-area plot.
64
+ */
65
+ export function resolvePlotRect(options: LayoutOptions): PlotRect {
66
+ if (!isMeasurable(options.container)) {
67
+ return EMPTY_PLOT;
68
+ }
69
+
70
+ const pad = options.padding ?? 0;
71
+ const fontSize = effectiveFontSize(options.baseFontSize, options.fontScale);
72
+
73
+ let left = pad;
74
+ let bottom = pad;
75
+
76
+ if (options.yLabels && options.yLabels.length > 0) {
77
+ left += reserveAxisSpace(options.yLabels, fontSize, 'vertical');
78
+ }
79
+ if (options.xLabels && options.xLabels.length > 0) {
80
+ bottom += reserveAxisSpace(options.xLabels, fontSize, 'horizontal');
81
+ }
82
+
83
+ const availableWidth = options.container.width - pad;
84
+ const availableHeight = options.container.height - pad;
85
+
86
+ // Never let axis reservations consume more than 40% of either dimension.
87
+ const maxLeft = availableWidth * 0.4;
88
+ const maxBottom = availableHeight * 0.4;
89
+ left = Math.min(left, maxLeft);
90
+ bottom = Math.min(bottom, maxBottom);
91
+
92
+ return {
93
+ x: left,
94
+ y: pad,
95
+ width: Math.max(0, options.container.width - left - pad),
96
+ height: Math.max(0, options.container.height - bottom - pad),
97
+ };
98
+ }
99
+
100
+ export interface AxisLabels {
101
+ readonly xLabels: readonly string[];
102
+ readonly yLabels: readonly string[];
103
+ readonly fontSize: number;
104
+ }
105
+
106
+ /**
107
+ * Shrinks a plot so its own axis labels fit inside the container.
108
+ *
109
+ * Builders call this once they know their tick labels (which depend only on
110
+ * the data domain) and before projecting anything, so the reservation costs no
111
+ * second pass. Without it the frame handed every builder the full container:
112
+ * Y labels, drawn right-aligned left of the plot, landed at negative x, and X
113
+ * labels below the baseline fell past the bottom of the SVG (CHT-01).
114
+ *
115
+ * - left: the widest Y label plus a gutter, and at least half the widest X
116
+ * label, because the first X label is centred on the plot's left edge;
117
+ * - right: half the widest X label, for the last one;
118
+ * - top: the part of the topmost Y label that rises above its tick;
119
+ * - bottom: one line of X labels plus a gutter.
120
+ *
121
+ * Each side is capped so labels can never squeeze the plot out of existence.
122
+ */
123
+ export function insetForAxes(outer: PlotRect, labels: AxisLabels): PlotRect {
124
+ const { fontSize } = labels;
125
+ let widestX = 0;
126
+ for (const label of labels.xLabels) {
127
+ widestX = Math.max(widestX, estimateTextWidth(label, fontSize));
128
+ }
129
+ const hasY = labels.yLabels.length > 0;
130
+ const hasX = labels.xLabels.length > 0;
131
+
132
+ const left = Math.min(
133
+ Math.max(hasY ? reserveAxisSpace(labels.yLabels, fontSize, 'vertical') : 0, widestX / 2),
134
+ outer.width * 0.4,
135
+ );
136
+ const right = Math.min(widestX / 2, outer.width * 0.25);
137
+ // Y labels sit on their tick at `position + fontSize / 3`, so two thirds of
138
+ // the glyph box rise above it.
139
+ const top = hasY ? Math.min(Math.ceil((fontSize * 2) / 3), outer.height * 0.2) : 0;
140
+ const bottom = hasX
141
+ ? Math.min(reserveAxisSpace(labels.xLabels, fontSize, 'horizontal'), outer.height * 0.4)
142
+ : 0;
143
+
144
+ return {
145
+ x: outer.x + left,
146
+ y: outer.y + top,
147
+ width: Math.max(0, outer.width - left - right),
148
+ height: Math.max(0, outer.height - top - bottom),
149
+ };
150
+ }
151
+
152
+ export function plotBottom(plot: PlotRect): number {
153
+ return plot.y + plot.height;
154
+ }
155
+
156
+ export function plotRight(plot: PlotRect): number {
157
+ return plot.x + plot.width;
158
+ }
159
+
160
+ export function containsPoint(plot: PlotRect, x: number, y: number): boolean {
161
+ return x >= plot.x && x <= plotRight(plot) && y >= plot.y && y <= plotBottom(plot);
162
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Reference markers on a value axis: thresholds, baselines and targets drawn
3
+ * as horizontal lines across the plot (FR-008, FR-072).
4
+ *
5
+ * Every builder used to return `markers: []`, so a threshold the app supplied
6
+ * reached the accessibility summary and nowhere else (CHT-03).
7
+ *
8
+ * PURE. No React, no SVG.
9
+ */
10
+ import type { ReferenceMarker } from '../contract/marker';
11
+ import { isKnown } from '../contract/value';
12
+ import { plotRight, type PlotRect } from './layout';
13
+ import type { GeometryLabel, MarkerLine } from './types';
14
+
15
+ type PlacedMarker = ReferenceMarker & { readonly value: { kind: 'known'; value: number } };
16
+
17
+ /** Markers that can be placed on a value axis: those with a known value. */
18
+ export function placeableMarkers(markers: readonly ReferenceMarker[] | undefined): PlacedMarker[] {
19
+ return (markers ?? []).filter((m): m is PlacedMarker => isKnown(m.value));
20
+ }
21
+
22
+ /** Values the domain must include so every marker lands inside the plot. */
23
+ export function markerDomainValues(markers: readonly ReferenceMarker[] | undefined): number[] {
24
+ return placeableMarkers(markers).map(m => m.value.value);
25
+ }
26
+
27
+ /** One line per marker across the plot, plus its label when it has one. */
28
+ export function horizontalMarkers(
29
+ markers: readonly ReferenceMarker[] | undefined,
30
+ plot: PlotRect,
31
+ project: (value: number) => number,
32
+ ): { lines: MarkerLine[]; labels: GeometryLabel[] } {
33
+ const lines: MarkerLine[] = [];
34
+ const labels: GeometryLabel[] = [];
35
+ for (const marker of placeableMarkers(markers)) {
36
+ const y = project(marker.value.value);
37
+ lines.push({
38
+ id: `marker-${marker.id}`,
39
+ d: `M${plot.x},${y}H${plotRight(plot)}`,
40
+ label: marker.label,
41
+ kind: marker.kind,
42
+ });
43
+ if (marker.label) {
44
+ labels.push({
45
+ key: `marker-label-${marker.id}`,
46
+ text: marker.label,
47
+ x: plotRight(plot),
48
+ // Above the line, unless that would leave the plot.
49
+ y: y - 4 >= plot.y + 8 ? y - 4 : y + 12,
50
+ anchor: 'end',
51
+ });
52
+ }
53
+ }
54
+ return { lines, labels };
55
+ }