inkplot 0.0.0-stage → 1.0.0-beta.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 (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/adapt.d.ts +10 -0
  4. package/dist/adapt.js +34 -0
  5. package/dist/annotation.d.ts +120 -0
  6. package/dist/annotation.js +267 -0
  7. package/dist/aria.d.ts +24 -0
  8. package/dist/aria.js +31 -0
  9. package/dist/attrs.d.ts +81 -0
  10. package/dist/attrs.js +84 -0
  11. package/dist/axis.d.ts +137 -0
  12. package/dist/axis.js +185 -0
  13. package/dist/bars.d.ts +80 -0
  14. package/dist/bars.js +255 -0
  15. package/dist/boxplot.d.ts +57 -0
  16. package/dist/boxplot.js +177 -0
  17. package/dist/boxzoom.d.ts +39 -0
  18. package/dist/boxzoom.js +138 -0
  19. package/dist/brush.d.ts +60 -0
  20. package/dist/brush.js +185 -0
  21. package/dist/bucket.d.ts +22 -0
  22. package/dist/bucket.js +62 -0
  23. package/dist/callout.d.ts +30 -0
  24. package/dist/callout.js +98 -0
  25. package/dist/chart.d.ts +126 -0
  26. package/dist/chart.js +257 -0
  27. package/dist/crosshair.d.ts +28 -0
  28. package/dist/crosshair.js +78 -0
  29. package/dist/cursor.d.ts +76 -0
  30. package/dist/cursor.js +344 -0
  31. package/dist/curve.d.ts +50 -0
  32. package/dist/curve.js +87 -0
  33. package/dist/curves.d.ts +104 -0
  34. package/dist/curves.js +375 -0
  35. package/dist/data.d.ts +209 -0
  36. package/dist/data.js +276 -0
  37. package/dist/decimation.d.ts +86 -0
  38. package/dist/decimation.js +410 -0
  39. package/dist/density.d.ts +55 -0
  40. package/dist/density.js +252 -0
  41. package/dist/dimension.d.ts +103 -0
  42. package/dist/dimension.js +205 -0
  43. package/dist/dom-platform.d.ts +53 -0
  44. package/dist/dom-platform.js +190 -0
  45. package/dist/element.d.ts +232 -0
  46. package/dist/element.js +414 -0
  47. package/dist/errors.d.ts +85 -0
  48. package/dist/errors.js +237 -0
  49. package/dist/events.d.ts +144 -0
  50. package/dist/events.js +19 -0
  51. package/dist/focus.d.ts +22 -0
  52. package/dist/focus.js +51 -0
  53. package/dist/furniture.d.ts +70 -0
  54. package/dist/furniture.js +166 -0
  55. package/dist/geometry.d.ts +40 -0
  56. package/dist/geometry.js +7 -0
  57. package/dist/heatmap.d.ts +57 -0
  58. package/dist/heatmap.js +207 -0
  59. package/dist/hit.d.ts +103 -0
  60. package/dist/hit.js +428 -0
  61. package/dist/index.d.ts +73 -0
  62. package/dist/index.js +73 -0
  63. package/dist/input.d.ts +42 -0
  64. package/dist/input.js +166 -0
  65. package/dist/jsx-dev-runtime.d.ts +9 -0
  66. package/dist/jsx-dev-runtime.js +13 -0
  67. package/dist/jsx-runtime.d.ts +59 -0
  68. package/dist/jsx-runtime.js +81 -0
  69. package/dist/key.d.ts +118 -0
  70. package/dist/key.js +199 -0
  71. package/dist/legend.d.ts +36 -0
  72. package/dist/legend.js +220 -0
  73. package/dist/mapped.d.ts +80 -0
  74. package/dist/mapped.js +106 -0
  75. package/dist/marker.d.ts +143 -0
  76. package/dist/marker.js +213 -0
  77. package/dist/panzoom.d.ts +119 -0
  78. package/dist/panzoom.js +467 -0
  79. package/dist/plane.d.ts +173 -0
  80. package/dist/plane.js +370 -0
  81. package/dist/platform.d.ts +113 -0
  82. package/dist/platform.js +80 -0
  83. package/dist/png.d.ts +44 -0
  84. package/dist/png.js +71 -0
  85. package/dist/pointer.d.ts +54 -0
  86. package/dist/pointer.js +132 -0
  87. package/dist/quantile.d.ts +3 -0
  88. package/dist/quantile.js +11 -0
  89. package/dist/radial.d.ts +116 -0
  90. package/dist/radial.js +336 -0
  91. package/dist/react.d.ts +441 -0
  92. package/dist/react.js +252 -0
  93. package/dist/registry.d.ts +2 -0
  94. package/dist/registry.js +51 -0
  95. package/dist/renderer.d.ts +191 -0
  96. package/dist/renderer.js +164 -0
  97. package/dist/scale.d.ts +37 -0
  98. package/dist/scale.js +19 -0
  99. package/dist/scales.d.ts +69 -0
  100. package/dist/scales.js +134 -0
  101. package/dist/serialize.d.ts +77 -0
  102. package/dist/serialize.js +321 -0
  103. package/dist/series.d.ts +304 -0
  104. package/dist/series.js +513 -0
  105. package/dist/simple.d.ts +35 -0
  106. package/dist/simple.js +20 -0
  107. package/dist/sources.d.ts +116 -0
  108. package/dist/sources.js +237 -0
  109. package/dist/stack.d.ts +61 -0
  110. package/dist/stack.js +172 -0
  111. package/dist/stats.d.ts +38 -0
  112. package/dist/stats.js +159 -0
  113. package/dist/steps.d.ts +50 -0
  114. package/dist/steps.js +154 -0
  115. package/dist/stub.d.ts +3 -0
  116. package/dist/stub.js +5 -0
  117. package/dist/style-names.d.ts +1 -0
  118. package/dist/style-names.js +6 -0
  119. package/dist/summary.d.ts +44 -0
  120. package/dist/summary.js +58 -0
  121. package/dist/surface.d.ts +94 -0
  122. package/dist/surface.js +145 -0
  123. package/dist/table.d.ts +39 -0
  124. package/dist/table.js +37 -0
  125. package/dist/text.d.ts +23 -0
  126. package/dist/text.js +44 -0
  127. package/dist/theme.d.ts +174 -0
  128. package/dist/theme.js +73 -0
  129. package/dist/themes.d.ts +9 -0
  130. package/dist/themes.js +56 -0
  131. package/dist/time-intl.d.ts +194 -0
  132. package/dist/time-intl.js +144 -0
  133. package/dist/time-temporal.d.ts +236 -0
  134. package/dist/time-temporal.js +58 -0
  135. package/dist/time.d.ts +216 -0
  136. package/dist/time.js +154 -0
  137. package/dist/trend.d.ts +151 -0
  138. package/dist/trend.js +500 -0
  139. package/dist/web-components.d.ts +62 -0
  140. package/dist/web-components.js +264 -0
  141. package/package.json +53 -3
package/dist/stats.js ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Statistics as a data transform: samples in, the numbers that summarise them out.
3
+ *
4
+ * <DomainSeries>
5
+ * <BoxStats><ObjectDataSource source={samples} domain="group" value="x" /></BoxStats>
6
+ * <BoxPath />
7
+ * </DomainSeries>
8
+ *
9
+ * `BoxStats` sits between a series and a source whose points are samples: the domain says which group a sample belongs to
10
+ * (samples with the same domain are one group) and the value is the measurement. Its output has a point for each group, at the
11
+ * group's domain, with the **median** as the value (`value="mean"` for the mean) and these channels:
12
+ *
13
+ * min, q1, median, q3, max the box and its whiskers
14
+ * mean, sd, sem, n for error bars (`sd` is the sample standard deviation, `sem` that over √n)
15
+ *
16
+ * so `BoxPath` finds what it needs by their names, and with `value="mean"`, `<ErrorBars error="sd" />` (or `sem`) on a
17
+ * series of its output draws the mean with its spread. Whiskers reach the furthest sample within `fence` times the interquartile range of the box (1.5 by default,
18
+ * Tukey's rule); samples beyond are **outliers**. `fence="minmax"` has the whiskers reach the extremes and no outliers.
19
+ * `output="outliers"` gives those outliers instead (a point for each, at its group's domain, with the sample as the value) to feed
20
+ * a series of markers, so the two series are made from the one source:
21
+ *
22
+ * <BoxStats output="outliers">{source}</BoxStats>
23
+ *
24
+ * `method` is how a quartile falls between samples: 'inclusive' (the default: linear between the closest ranks, as most
25
+ * packages do) or 'exclusive' (the quartile of the data without its ends, as the textbook method does). Gaps (NaN samples)
26
+ * are left out. The result is cached against the source's version, so a change of the samples recomputes it once.
27
+ */
28
+ /* oxlint-disable typescript/no-non-null-assertion -- numeric kernels over flat typed-array buffers (stride-addressed rows): every index is bounded by the row count, which `noUncheckedIndexedAccess` cannot see */
29
+ import { AUTO, STRING } from './attrs.js';
30
+ import { quantile } from './quantile.js';
31
+ import { DataSource, DataTransform } from './data.js';
32
+ import { fail } from './element.js';
33
+ /** The channels of a summary, in the order they are stored. */
34
+ const SUMMARY = ['min', 'q1', 'median', 'q3', 'max', 'mean', 'sd', 'sem', 'n'];
35
+ export class BoxStats extends DataTransform {
36
+ static attrTypes = { ...DataTransform.attrTypes, output: STRING, fence: AUTO, method: STRING, value: STRING };
37
+ #version = 0;
38
+ #cache = null;
39
+ get source() { return this.child(0, DataSource, DataTransform); }
40
+ get channelNames() { return this.attrs.output === 'outliers' ? [] : SUMMARY; }
41
+ /** Anything beneath (the source) bubbles an invalidation up through here, so one counter covers the source and the attributes. */
42
+ invalidate() {
43
+ this.#version++;
44
+ super.invalidate();
45
+ }
46
+ get version() { return this.#version; }
47
+ #compute() {
48
+ if (this.#cache && this.#cache.version === this.#version)
49
+ return this.#cache.value;
50
+ const src = this.source, plan = src.plan({ width: 0, height: 0, domainRange: { min: -Infinity, max: Infinity }, channels: [] });
51
+ const buf = new Float64Array(plan.capacity * 2), { count } = plan.fill(buf);
52
+ const a = this.attrs, fence = a.fence === 'minmax' ? Infinity : a.fence ?? 1.5, method = a.method ?? 'inclusive';
53
+ if (a.fence !== undefined && a.fence !== 'minmax' && !(typeof a.fence === 'number' && a.fence >= 0))
54
+ fail('BoxStats: fence is a number of interquartile ranges, or "minmax"');
55
+ const groups = [], cols = { min: [], q1: [], median: [], q3: [], max: [], mean: [], sd: [], sem: [], n: [] };
56
+ const outD = [], outV = [];
57
+ for (let i = 0; i < count;) {
58
+ const key = buf[2 * i];
59
+ let j = i;
60
+ const values = [];
61
+ for (; j < count && buf[2 * j] === key; j++) {
62
+ const y = buf[2 * j + 1];
63
+ if (Number.isFinite(y))
64
+ values.push(y);
65
+ }
66
+ i = j;
67
+ if (values.length === 0)
68
+ continue;
69
+ values.sort((p, q) => p - q);
70
+ const n = values.length, q1 = quantile(values, 0.25, method), med = quantile(values, 0.5, method), q3 = quantile(values, 0.75, method);
71
+ const reach = fence * (q3 - q1);
72
+ let lo = values[0], hi = values[n - 1];
73
+ if (reach !== Infinity) {
74
+ // The furthest samples still inside the fences; those beyond are outliers.
75
+ lo = values.find((v) => v >= q1 - reach) ?? q1;
76
+ hi = [...values].reverse().find((v) => v <= q3 + reach) ?? q3;
77
+ for (const v of values)
78
+ if (v < q1 - reach || v > q3 + reach) {
79
+ outD.push(key);
80
+ outV.push(v);
81
+ }
82
+ }
83
+ let sum = 0;
84
+ for (const v of values)
85
+ sum += v;
86
+ const mean = sum / n;
87
+ let ss = 0;
88
+ for (const v of values)
89
+ ss += (v - mean) * (v - mean);
90
+ const sd = n > 1 ? Math.sqrt(ss / (n - 1)) : NaN;
91
+ groups.push(key);
92
+ cols.min.push(lo);
93
+ cols.q1.push(q1);
94
+ cols.median.push(med);
95
+ cols.q3.push(q3);
96
+ cols.max.push(hi);
97
+ cols.mean.push(mean);
98
+ cols.sd.push(sd);
99
+ cols.sem.push(sd / Math.sqrt(n));
100
+ cols.n.push(n);
101
+ }
102
+ const value = {
103
+ groups: Float64Array.from(groups),
104
+ columns: Object.fromEntries(SUMMARY.map((c) => [c, Float64Array.from(cols[c])])),
105
+ outlierDomain: Float64Array.from(outD), outlierValue: Float64Array.from(outV),
106
+ };
107
+ this.#cache = { version: this.#version, value };
108
+ return value;
109
+ }
110
+ get pointCount() { const c = this.#compute(); return this.attrs.output === 'outliers' ? c.outlierDomain.length : c.groups.length; }
111
+ domain() {
112
+ const c = this.#compute(), d = this.attrs.output === 'outliers' ? c.outlierDomain : c.groups;
113
+ return d.length > 0 ? { min: d[0], max: d[d.length - 1] } : null;
114
+ }
115
+ #extent(a) {
116
+ let min = Infinity, max = -Infinity;
117
+ for (const v of a) {
118
+ if (v < min)
119
+ min = v;
120
+ if (v > max)
121
+ max = v;
122
+ }
123
+ return min <= max ? { min, max } : null;
124
+ }
125
+ valueExtent() {
126
+ const c = this.#compute();
127
+ return this.#extent(this.attrs.output === 'outliers' ? c.outlierValue : c.columns[this.attrs.value ?? 'median']);
128
+ }
129
+ channelExtent(name) {
130
+ if (this.attrs.output === 'outliers')
131
+ return null;
132
+ return SUMMARY.includes(name) ? this.#extent(this.#compute().columns[name]) : null;
133
+ }
134
+ plan(context) {
135
+ const c = this.#compute(), outliers = this.attrs.output === 'outliers';
136
+ const names = context.channels;
137
+ if (outliers && names.length > 0)
138
+ fail('BoxStats: the outliers have no channels, only the sample (the value)');
139
+ const want = names.map((n) => SUMMARY.includes(n) ? n : fail(`BoxStats has no channel "${n}" (it has ${SUMMARY.join(', ')})`));
140
+ const count = outliers ? c.outlierDomain.length : c.groups.length, stride = 2 + want.length;
141
+ return {
142
+ capacity: count,
143
+ fill: (out) => {
144
+ const domain = outliers ? c.outlierDomain : c.groups, value = outliers ? c.outlierValue : c.columns[this.attrs.value ?? 'median'];
145
+ const extents = [domain.length > 0 ? { min: domain[0], max: domain[domain.length - 1] } : { min: Infinity, max: -Infinity }];
146
+ for (let i = 0; i < count; i++) {
147
+ out[stride * i] = domain[i];
148
+ out[stride * i + 1] = value[i];
149
+ for (let k = 0; k < want.length; k++)
150
+ out[stride * i + 2 + k] = c.columns[want[k]][i];
151
+ }
152
+ extents.push(this.#extent(value) ?? { min: Infinity, max: -Infinity });
153
+ for (const w of want)
154
+ extents.push(this.#extent(c.columns[w]) ?? { min: Infinity, max: -Infinity });
155
+ return { count, extents };
156
+ },
157
+ };
158
+ }
159
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Steps: a value that holds until it changes, drawn as a line or a filled area by giving a `StepCurve` to a `LinePath` or `AreaPath`.
3
+ *
4
+ * Where it changes is `mode`, for a step between a point and the next:
5
+ * - `after` (default): the value holds from its point until the next one, then jumps there. A price, a setting.
6
+ * - `before`: it jumps at its own point, and holds back to the previous one. A count of what happened by then.
7
+ * - `center`: it changes halfway between the two.
8
+ *
9
+ * The step is worked out in the plane's own terms, never in pixels: the level part runs along the first axis at one value
10
+ * and the jump along the second, and both are placed like any other point. So on a rectangle the levels are horizontal and
11
+ * the jumps vertical (or the other way round when the domain is on the vertical axis), and on a polar plane a level is an
12
+ * arc round the middle and a jump a spoke. A level on a curved plane is cut into pieces to follow it (at the corners of
13
+ * a radar's polygon, exactly), which a straight chord between the two ends would cut across.
14
+ *
15
+ * The line passes through every point of the data, and a marker on the series sits on a corner. A gap in the data
16
+ * breaks the steps in two, unless the series says its `gaps` are 'connect'. The curve `trace`s what it draws, so a
17
+ * 'line' or 'area' hit is measured to the steps on the screen and reports the point whose level it is on, and a tooltip
18
+ * reading between points (`interpolate`) gives the level there, not a value on a straight line between the points.
19
+ */
20
+ import type { PointBuffer } from './data.ts';
21
+ import type { Series } from './series.ts';
22
+ import { Curve, type CurveAttributes } from './curve.ts';
23
+ import type { Trace } from './renderer.ts';
24
+ export type StepMode = 'after' | 'before' | 'center';
25
+ /**
26
+ * The line of a series as steps, and the line it is filled from, as placed points: runs of them, each ended by a NaN
27
+ * point. `owner` says, for each vertex, which of the series' own points the value there is (a level is its point's, and a
28
+ * jump is shared by the two it joins), which is a `Trace`.
29
+ */
30
+ export interface Stepped extends Trace {
31
+ readonly top: PointBuffer;
32
+ readonly base: PointBuffer | null;
33
+ }
34
+ /**
35
+ * The steps of a series in plot pixels. `baseline` is the value a step area is filled from (null for a line, which needs
36
+ * no second buffer); in a stack the baseline is the layer below, row by row, as everywhere else. Null before the first
37
+ * projection. The two buffers have the same shape, so a point of one has its foot at the same place in the other.
38
+ */
39
+ export declare function stepLines(series: Series, mode: StepMode, baseline: number | null): Stepped | null;
40
+ export interface StepCurveAttributes extends CurveAttributes {
41
+ /** Where the value changes between one point and the next. Default 'after'. */
42
+ mode?: StepMode;
43
+ }
44
+ /** A line, or the edge of an area, that holds its value and jumps. In a stack, the baseline steps too. */
45
+ export declare class StepCurve extends Curve<StepCurveAttributes> {
46
+ #private;
47
+ line(series: Series): string;
48
+ area(series: Series, baseline: number): string;
49
+ trace(series: Series, baseline: number | null): Trace | null;
50
+ }
package/dist/steps.js ADDED
@@ -0,0 +1,154 @@
1
+ import { Curve, areaOutline, polyline } from './curve.js';
2
+ /** Pieces a level is cut into for a whole turn of a curved plane. */
3
+ const PIECES = 96;
4
+ /** Most pieces one level is cut into. */
5
+ const MOST = 64;
6
+ /**
7
+ * The steps of a series in plot pixels. `baseline` is the value a step area is filled from (null for a line, which needs
8
+ * no second buffer); in a stack the baseline is the layer below, row by row, as everywhere else. Null before the first
9
+ * projection. The two buffers have the same shape, so a point of one has its foot at the same place in the other.
10
+ */
11
+ export function stepLines(series, mode, baseline) {
12
+ const d = series.drawn;
13
+ if (!d)
14
+ return null;
15
+ const plane = d.plane, first = series.firstAxis, second = series.secondAxis;
16
+ const n = d.points.count, rs = d.stride, raw = d.raw, wantBase = baseline !== null;
17
+ const zero = series.attrs.gaps === 'zero' && !d.top, connect = series.attrs.gaps === 'connect';
18
+ const norm = new Float64Array(n * 3);
19
+ for (let i = 0; i < n; i++) {
20
+ const x = raw[i * rs + d.firstColumn];
21
+ let v = raw[i * rs + d.secondColumn];
22
+ if (zero && !Number.isFinite(v) && Number.isFinite(x))
23
+ v = 0;
24
+ const ok = Number.isFinite(v);
25
+ norm[3 * i] = x;
26
+ norm[3 * i + 1] = ok ? (d.top ? d.top[i] : v) : NaN;
27
+ norm[3 * i + 2] = ok ? (d.base ? d.base[i] : baseline ?? 0) : NaN;
28
+ }
29
+ first.normalize(norm, n, 3, 0);
30
+ second.normalize(norm, n, 3, 1);
31
+ second.normalize(norm, n, 3, 2);
32
+ // The foot stays on the axis, as `Series.baseline` keeps it.
33
+ for (let i = 0; i < n; i++) {
34
+ const b = norm[3 * i + 2];
35
+ if (b < 0)
36
+ norm[3 * i + 2] = 0;
37
+ else if (b > 1)
38
+ norm[3 * i + 2] = 1;
39
+ }
40
+ const real = (i) => {
41
+ const x = norm[3 * i], t = norm[3 * i + 1], b = norm[3 * i + 2];
42
+ return Number.isFinite(x) && Number.isFinite(t) && (!wantBase || Number.isFinite(b));
43
+ };
44
+ const corners = plane.curved ? plane.breaks(first) : [];
45
+ let top = new Float64Array(Math.max(16, 4 * n)), foot = wantBase ? new Float64Array(top.length) : null, owner = new Int32Array(top.length / 2), k = 0;
46
+ const put = (x, t, b, by) => {
47
+ if (2 * k + 2 > top.length) {
48
+ const bigger = new Float64Array(top.length * 2);
49
+ bigger.set(top);
50
+ top = bigger;
51
+ const o2 = new Int32Array(top.length / 2);
52
+ o2.set(owner);
53
+ owner = o2;
54
+ if (foot) {
55
+ const f2 = new Float64Array(top.length);
56
+ f2.set(foot);
57
+ foot = f2;
58
+ }
59
+ }
60
+ top[2 * k] = x;
61
+ top[2 * k + 1] = t;
62
+ owner[k] = by;
63
+ if (foot) {
64
+ foot[2 * k] = x;
65
+ foot[2 * k + 1] = b;
66
+ }
67
+ k++;
68
+ };
69
+ /** A level from `a` to `b` along the first axis at one pair of values, all belonging to point `by`: the pieces between, then the end. */
70
+ const level = (a, b, t, bs, by) => {
71
+ if (plane.curved && a !== b) {
72
+ const lo = Math.min(a, b), hi = Math.max(a, b), cuts = [];
73
+ if (corners.length > 0) {
74
+ for (const c of corners)
75
+ if (c > lo + 1e-9 && c < hi - 1e-9)
76
+ cuts.push(c);
77
+ }
78
+ else {
79
+ const pieces = Math.min(MOST, Math.ceil((hi - lo) * PIECES));
80
+ for (let j = 1; j < pieces; j++)
81
+ cuts.push(lo + ((hi - lo) * j) / pieces);
82
+ }
83
+ if (a > b)
84
+ cuts.reverse();
85
+ for (const c of cuts)
86
+ put(c, t, bs, by);
87
+ }
88
+ put(b, t, bs, by);
89
+ };
90
+ const run = [];
91
+ const flush = () => {
92
+ if (run.length === 0)
93
+ return;
94
+ let p = run[0];
95
+ put(norm[3 * p], norm[3 * p + 1], norm[3 * p + 2], p);
96
+ for (let j = 1; j < run.length; j++) {
97
+ const c = run[j], xp = norm[3 * p], xc = norm[3 * c], tp = norm[3 * p + 1], tc = norm[3 * c + 1], bp = norm[3 * p + 2], bc = norm[3 * c + 2];
98
+ if (mode === 'after') {
99
+ level(xp, xc, tp, bp, p);
100
+ put(xc, tc, bc, c);
101
+ }
102
+ else if (mode === 'before') {
103
+ put(xp, tc, bc, c);
104
+ level(xp, xc, tc, bc, c);
105
+ }
106
+ else {
107
+ const m = (xp + xc) / 2;
108
+ level(xp, m, tp, bp, p);
109
+ put(m, tc, bc, c);
110
+ level(m, xc, tc, bc, c);
111
+ }
112
+ p = c;
113
+ }
114
+ put(NaN, NaN, NaN, -1);
115
+ run.length = 0;
116
+ };
117
+ for (let i = 0; i < n; i++) {
118
+ if (real(i))
119
+ run.push(i);
120
+ else if (!connect)
121
+ flush();
122
+ }
123
+ flush();
124
+ const out = (data) => {
125
+ const buffer = { data: data.subarray(0, 2 * k), stride: 2, count: k };
126
+ plane.place(buffer, first, second);
127
+ return buffer;
128
+ };
129
+ const placedTop = out(top);
130
+ return { top: placedTop, line: placedTop, base: foot ? out(foot) : null, owner: owner.subarray(0, k) };
131
+ }
132
+ /** A line, or the edge of an area, that holds its value and jumps. In a stack, the baseline steps too. */
133
+ export class StepCurve extends Curve {
134
+ #key = '';
135
+ #value = null;
136
+ /** The steps of a series, kept until it is projected again or they are asked for differently. */
137
+ #steps(series, baseline) {
138
+ const mode = this.attrs.mode ?? 'after', key = `${series.projection}|${mode}|${baseline}|${series.attrs.gaps}`;
139
+ if (key !== this.#key) {
140
+ this.#value = stepLines(series, mode, baseline);
141
+ this.#key = key;
142
+ }
143
+ return this.#value;
144
+ }
145
+ line(series) {
146
+ const steps = this.#steps(series, null);
147
+ return steps ? polyline(steps.top, false) : '';
148
+ }
149
+ area(series, baseline) {
150
+ const steps = this.#steps(series, baseline);
151
+ return steps?.base ? areaOutline(steps.top, steps.base, false) : '';
152
+ }
153
+ trace(series, baseline) { return this.#steps(series, baseline); }
154
+ }
package/dist/stub.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** What a part of the API that is declared and not yet supported says when it is used. */
2
+ /** Fails when used rather than drawing something wrong, so a half-supported option cannot be relied on by accident. */
3
+ export declare function notImplemented(what: string): never;
package/dist/stub.js ADDED
@@ -0,0 +1,5 @@
1
+ /** What a part of the API that is declared and not yet supported says when it is used. */
2
+ /** Fails when used rather than drawing something wrong, so a half-supported option cannot be relied on by accident. */
3
+ export function notImplemented(what) {
4
+ throw new Error(`${what} is not implemented yet`);
5
+ }
@@ -0,0 +1 @@
1
+ export declare const STYLE_NAMES: readonly ["annotationLabel", "annotationLine", "axisLine", "axisTitle", "boxLine", "calloutArm", "calloutBox", "calloutText", "chartCaption", "chartSubtitle", "chartTitle", "dotMarker", "errorBand", "errorBar", "focusRing", "heatCell", "keyLabel", "keyLine", "keyTitle", "legendLabel", "legendTitle", "lineMarker", "seriesArea", "seriesBar", "seriesBox", "seriesLine", "shapeMarker", "tickLabel", "tooltipBox", "tooltipText", "haloWidth", "strokeWidth", "tickLength", "labelGap"];
@@ -0,0 +1,6 @@
1
+ export const STYLE_NAMES = [
2
+ // Selectors
3
+ 'annotationLabel', 'annotationLine', 'axisLine', 'axisTitle', 'boxLine', 'calloutArm', 'calloutBox', 'calloutText', 'chartCaption', 'chartSubtitle', 'chartTitle', 'dotMarker', 'errorBand', 'errorBar', 'focusRing', 'heatCell', 'keyLabel', 'keyLine', 'keyTitle', 'legendLabel', 'legendTitle', 'lineMarker', 'seriesArea', 'seriesBar', 'seriesBox', 'seriesLine', 'shapeMarker', 'tickLabel', 'tooltipBox', 'tooltipText',
4
+ // Properties
5
+ 'haloWidth', 'strokeWidth', 'tickLength', 'labelGap',
6
+ ];
@@ -0,0 +1,44 @@
1
+ import type { Chart, Describer } from './chart.ts';
2
+ import { type BaseAttributes, ChartElement } from './element.ts';
3
+ /** What a summary is made from. Plain data, so a function in any language can word it. */
4
+ export interface ChartFacts {
5
+ /** The chart's own name, from `meta.title`. */
6
+ readonly title?: string;
7
+ readonly series: readonly {
8
+ readonly name: string;
9
+ readonly points: number;
10
+ readonly shown: boolean;
11
+ }[];
12
+ /** Each axis with a span: its title, whether it runs across, and its first and last tick labels. */
13
+ readonly axes: readonly {
14
+ readonly title?: string;
15
+ readonly horizontal: boolean;
16
+ readonly from: string;
17
+ readonly to: string;
18
+ }[];
19
+ }
20
+ export interface ChartSummaryAttributes extends BaseAttributes {
21
+ /** Words the facts. Default: English sentences. */
22
+ text?: (facts: ChartFacts) => string;
23
+ /** What the chart is, as a screen reader announces it. Default 'chart'. */
24
+ roleDescription?: string;
25
+ }
26
+ /** The English summary: how many series and points, then the span of each axis. */
27
+ export declare function englishSummary(f: ChartFacts): string;
28
+ export declare class ChartSummary extends ChartElement<Chart, [], ChartSummaryAttributes> implements Describer {
29
+ static attrTypes: {
30
+ roleDescription: 1;
31
+ id: 1;
32
+ meta: {
33
+ title: 1;
34
+ description: 1;
35
+ role: 1;
36
+ };
37
+ };
38
+ /** What the chart holds as of the last layout. */
39
+ get facts(): ChartFacts;
40
+ describe(): {
41
+ description: string;
42
+ roleDescription: string;
43
+ };
44
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A spoken summary of the chart, for a screen reader: what the chart holds, written out in words.
3
+ *
4
+ * new Chart({ meta: { title: 'Rates' } }, plane, new ChartSummary())
5
+ *
6
+ * It is a separate, opt-in chart child because the words are language-specific, and the core carries none: leave it out and a
7
+ * chart says only what its `meta` says. This one writes English. For another language, pass `text`, a function from the facts
8
+ * (`ChartFacts`) to the sentence, and `roleDescription` (what the chart is called, "chart" by default). `meta.description`
9
+ * on the chart, when given, is used instead of the summary.
10
+ */
11
+ import { STRING } from './attrs.js';
12
+ import { ChartElement } from './element.js';
13
+ import { Plane } from './plane.js';
14
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
15
+ /** The English summary: how many series and points, then the span of each axis. */
16
+ export function englishSummary(f) {
17
+ const parts = [];
18
+ if (f.series.length > 0) {
19
+ const list = f.series.map((s) => `${s.name}, ${plural(s.points, 'point', 'points')}${s.shown ? '' : ', hidden'}`).join('; ');
20
+ parts.push(`${plural(f.series.length, 'series', 'series')}: ${list}.`);
21
+ }
22
+ for (const a of f.axes)
23
+ parts.push(`${a.horizontal ? 'Horizontal' : 'Vertical'} axis${a.title ? ` ${a.title}` : ''}, from ${a.from} to ${a.to}.`);
24
+ return parts.join(' ');
25
+ }
26
+ export class ChartSummary extends ChartElement {
27
+ static attrTypes = { ...ChartElement.attrTypes, roleDescription: STRING };
28
+ /** What the chart holds as of the last layout. */
29
+ get facts() {
30
+ const chart = this.requireParent(), series = [], axes = [];
31
+ const seen = new Set();
32
+ let n = 0;
33
+ for (const plane of chart) {
34
+ if (!(plane instanceof Plane))
35
+ continue;
36
+ for (const s of plane.allSeries) {
37
+ n++;
38
+ series.push({ name: s.attrs.name ?? s.attrs.meta?.title ?? `${n}`, points: s.points.count, shown: s.shown });
39
+ }
40
+ for (const axis of plane.axes) {
41
+ if (seen.has(axis))
42
+ continue;
43
+ seen.add(axis);
44
+ const ticks = axis.getTicks(axis.ruler.length);
45
+ if (ticks.length < 2)
46
+ continue;
47
+ // oxlint-disable-next-line typescript/no-non-null-assertion -- in range: the index comes from a loop bound or a length checked in the same function
48
+ axes.push({ ...(axis.attrs.title && { title: axis.attrs.title }), horizontal: axis.ruler.horizontal, from: ticks[0].label, to: ticks[ticks.length - 1].label });
49
+ }
50
+ }
51
+ const title = chart.attrs.meta?.title;
52
+ return { ...(title !== undefined && { title }), series, axes };
53
+ }
54
+ describe() {
55
+ const a = this.attrs;
56
+ return { description: (a.text ?? englishSummary)(this.facts), roleDescription: a.roleDescription ?? 'chart' };
57
+ }
58
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * A surface: the platform nodes one element owns, and the counters a flush accumulates.
3
+ *
4
+ * Why this layer exists: every flush re-runs every element's drawing code, so drawing has to be idempotent and cheap.
5
+ * Surface makes it so by (a) handing out the same node for the same key, and (b) remembering the last value written
6
+ * to each attribute and skipping identical writes. Elements therefore just say "this node has these attributes" every
7
+ * time, and only real changes reach the platform (and, in a browser, the DOM).
8
+ */
9
+ import type { Platform, PlatformNode, SvgTag } from './platform.ts';
10
+ /** The timed phases of producing a series' path, for the chart's diagnostics. */
11
+ export type StageName = 'generate' | 'decimate' | 'normalize' | 'place' | 'path';
12
+ /** What a flush accumulates. One per chart, shared by every surface and element in it. */
13
+ export interface Counters {
14
+ writes: number;
15
+ skipped: number;
16
+ names: Set<string>;
17
+ filled: number;
18
+ cached: number;
19
+ projected: number;
20
+ pointsIn: number;
21
+ pointsDrawn: number;
22
+ pathBytes: number;
23
+ stages: Record<StageName, number>;
24
+ }
25
+ /** Fresh zeroed counters; the chart resets (rather than replaces) them each flush so surfaces keep a valid reference. */
26
+ export declare const newCounters: () => Counters;
27
+ /**
28
+ * Where an element's nodes go, answered from the live element tree each time one is created.
29
+ * Nothing about neighbours is stored, so nothing can go stale when siblings come and go.
30
+ */
31
+ export interface Placement {
32
+ /** The node own content goes into when the element has no wrapper: the nearest wrapper above, or the platform root. */
33
+ container(): PlatformNode;
34
+ /** The node this element's whole content must end before. Null means append. */
35
+ end(): PlatformNode | null;
36
+ /** The first node among the element's children, if any of them has drawn. */
37
+ childrenFirst(): PlatformNode | null;
38
+ }
39
+ export interface NodeOptions {
40
+ /** Put the node inside another of this element's nodes instead of at the top level. */
41
+ parent?: PlatformNode;
42
+ /** Draw after the element's children instead of before them (an axis' labels over the series, say). */
43
+ above?: boolean;
44
+ }
45
+ /**
46
+ * What an element draws with. Nodes are keyed, created on demand, positioned from the live tree,
47
+ * and written only when a value changed. An element's own content sits below its children (or above, with `above`).
48
+ *
49
+ * An element has no node of its own unless it asks for a wrapper, which it should only do when the wrapper
50
+ * does something: a transform, a clip, or style its content inherits. Otherwise its nodes sit directly
51
+ * in the parent's, in order, with nothing to mark where they start.
52
+ */
53
+ export declare class Surface {
54
+ private readonly platform;
55
+ private readonly counters;
56
+ private readonly placement;
57
+ private wrapperNode;
58
+ private hidden;
59
+ private readonly nodes;
60
+ private readonly cache;
61
+ constructor(platform: Platform, counters: Counters, placement: Placement);
62
+ /**
63
+ * The element's own `<g>` (or another tag). Children and own nodes go inside it. Create it before the
64
+ * element draws anything, typically on attach; asking again returns the same node.
65
+ */
66
+ wrapper(tag?: SvgTag): PlatformNode;
67
+ /** The wrapper, if the element has one. */
68
+ get wrapperRoot(): PlatformNode | null;
69
+ /** First own node below the children, if any. */
70
+ get firstBelow(): PlatformNode | null;
71
+ /** First own node above the children, if any. */
72
+ get firstAbove(): PlatformNode | null;
73
+ /**
74
+ * The node for `key`, created the first time and the same node afterwards. Keys are per element, so two elements
75
+ * can both use 'grid' without clashing.
76
+ */
77
+ node(key: string, tag: SvgTag, options?: NodeOptions): PlatformNode;
78
+ /** Set an attribute if (and only if) it differs from what was last written. The platform cannot unset one, so use a neutral value ('none', 0) instead of removing. */
79
+ set(node: PlatformNode, name: string, value: string | number): void;
80
+ /** Set a text node's content, skipping unchanged writes like `set`. */
81
+ text(node: PlatformNode, content: string): void;
82
+ /** Drop one keyed node, for content that comes and goes (a title, a clip path). */
83
+ remove(key: string): void;
84
+ /** Drop a keyed node and every node keyed beneath it (`key/…`), for a group made of several. */
85
+ removePrefix(key: string): void;
86
+ /** Hide or show everything this element draws (the wrapper, or each top-level node if it has none). */
87
+ setHidden(hidden: boolean): void;
88
+ /** Take this element's nodes out of the platform. */
89
+ dispose(): void;
90
+ /** The first top-level node of one layer; the layers are what `end()`/`first()` placement is computed from. */
91
+ private firstOf;
92
+ /** The write cache lives per node (a WeakMap) so a removed node's entries are collected with it. */
93
+ private write;
94
+ }