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
@@ -0,0 +1,36 @@
1
+ import type { Chart } from './chart.ts';
2
+ import { type BaseAttributes, ChartElement } from './element.ts';
3
+ import { inputOf } from './input.ts';
4
+ import { type ChartEvent } from './events.ts';
5
+ import { type Rect, type Side } from './geometry.ts';
6
+ import { type Series } from './series.ts';
7
+ export interface LegendAttributes extends BaseAttributes {
8
+ /** The edge of the chart it sits along. Default 'right'. */
9
+ side?: Side;
10
+ /** Entries in a column or a row. Default: along its side, so a column on the left and right, a row on the top and bottom. */
11
+ orientation?: 'vertical' | 'horizontal';
12
+ /** A heading above the entries. */
13
+ title?: string;
14
+ /** Clicking an entry hides or shows its series. Default true. */
15
+ toggle?: boolean;
16
+ }
17
+ export declare class Legend extends ChartElement<Chart, [], LegendAttributes> {
18
+ #private;
19
+ static takesInput: typeof inputOf;
20
+ static attrTypes: {
21
+ title: 1;
22
+ id: 1;
23
+ meta: {
24
+ title: 1;
25
+ description: 1;
26
+ role: 1;
27
+ };
28
+ };
29
+ get side(): Side;
30
+ get horizontal(): boolean;
31
+ /** The series listed: those of all the chart's planes that have a name. */
32
+ get entries(): readonly Series[];
33
+ onEvent(ev: ChartEvent): void;
34
+ /** Where a series' entry is, in the chart's pixels (valid after layout), or null if it is not listed. */
35
+ entryRect(series: Series): Rect | null;
36
+ }
package/dist/legend.js ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * The legend: what each series is called, with a sample of how it is drawn, beside the plots.
3
+ *
4
+ * new Chart({}, plane, new Legend({ side: 'right' }))
5
+ *
6
+ * It lists the series of every plane in the chart that have a `name`, in drawing order. The sample is the series' own
7
+ * (`Series.sample`): a line is a short line in its dash and colour, an area or a bar a box, a marker its shape, and a series
8
+ * with a line and markers has the markers over the line, so it reads as the plot does.
9
+ *
10
+ * Clicking an entry hides its series, or shows it again (`toggle`, on by default): a series that is not shown is left out of
11
+ * the plot's axes, its stack and its hits, and its entry is dimmed, so it can be brought back.
12
+ *
13
+ * Each entry is a button for the keyboard too: Tab reaches it, Enter or Space presses it, and a ring shows where focus is.
14
+ * A press with the pointer takes effect when it is released over the same entry, so one can be abandoned by moving away.
15
+ *
16
+ * A legend is a chart child like a key, and takes a strip along a side of the chart; entries that do not fit along a top
17
+ * or bottom legend wrap onto another row. It looks as `legend`, `legendLabel` and `legendTitle` say, which fall back to `text`.
18
+ */
19
+ /* oxlint-disable typescript/no-non-null-assertion -- entries, widths and items are parallel arrays indexed together in one loop */
20
+ import { STRING } from './attrs.js';
21
+ import { ChartElement } from './element.js';
22
+ import { hideFromAssistive, writeAria } from './aria.js';
23
+ import { inputOf } from './input.js';
24
+ import { EVENT_ARRANGE, EVENT_ATTACH, EVENT_FOCUS, EVENT_KEY, EVENT_MEASURE, EVENT_PAINT, EVENT_POINTER } from './events.js';
25
+ import { drawFocusRing, makeTabStop, pressesButton } from './focus.js';
26
+ import { f2 } from './geometry.js';
27
+ import { Plane } from './plane.js';
28
+ import { paintSample } from './series.js';
29
+ import { LINE_HEIGHT, applyText, measureBlock } from './text.js';
30
+ /** Width of a sample, and the gap between it and the name. */
31
+ const SAMPLE_WIDTH = 24;
32
+ const SAMPLE_GAP = 6;
33
+ /** Space between entries along a row, and between rows. */
34
+ const ENTRY_GAP = 14;
35
+ const ROW_GAP = 4;
36
+ const TITLE_BLOCK = 18;
37
+ /** How faded an entry is when its series is hidden. */
38
+ const DIMMED = 0.4;
39
+ export class Legend extends ChartElement {
40
+ static takesInput = inputOf;
41
+ static attrTypes = { ...ChartElement.attrTypes, title: STRING };
42
+ #rect = { x: 0, y: 0, width: 0, height: 0 };
43
+ #items = [];
44
+ #rowHeight = 0;
45
+ #drawn = 0;
46
+ /** The entry the pointer went down on, and the entry holding keyboard focus (only while a ring is wanted). */
47
+ #pressed = null;
48
+ #focusNode = null;
49
+ #ring = false;
50
+ #nodes = [];
51
+ #samples = new Map();
52
+ get side() { return this.attrs.side ?? 'right'; }
53
+ get horizontal() {
54
+ return (this.attrs.orientation ?? (this.side === 'top' || this.side === 'bottom' ? 'horizontal' : 'vertical')) === 'horizontal';
55
+ }
56
+ /** The series listed: those of all the chart's planes that have a name. */
57
+ get entries() {
58
+ const out = [];
59
+ for (const c of this.requireParent())
60
+ if (c instanceof Plane)
61
+ for (const s of c.allSeries)
62
+ if (s.attrs.name)
63
+ out.push(s);
64
+ return out;
65
+ }
66
+ onEvent(ev) {
67
+ switch (ev.type) {
68
+ case EVENT_ATTACH:
69
+ this.surface.wrapper();
70
+ inputOf(this.requireParent()).watchInput(this);
71
+ break;
72
+ case EVENT_MEASURE: {
73
+ const size = this.#layout(ev.available.width);
74
+ if (size)
75
+ ev.request = { min: size, max: size, side: this.side };
76
+ break;
77
+ }
78
+ case EVENT_ARRANGE:
79
+ this.#rect = ev.rect;
80
+ break;
81
+ case EVENT_PAINT:
82
+ this.#paint();
83
+ break;
84
+ case EVENT_POINTER: {
85
+ if (this.attrs.toggle === false)
86
+ break;
87
+ const item = this.#at(ev.x - this.#rect.x, ev.y - this.#rect.y);
88
+ if (ev.kind === 'down' && (ev.buttons & 1) && item) {
89
+ this.#pressed = item.series;
90
+ ev.handled = true;
91
+ }
92
+ else if (ev.kind === 'up' && this.#pressed) {
93
+ const press = this.#pressed;
94
+ this.#pressed = null;
95
+ if (item?.series === press)
96
+ this.#toggle(press);
97
+ ev.handled = true;
98
+ }
99
+ break;
100
+ }
101
+ case EVENT_KEY: {
102
+ const i = this.#nodes.indexOf(ev.node), item = this.#items[i];
103
+ if (item && pressesButton(ev))
104
+ this.#toggle(item.series);
105
+ break;
106
+ }
107
+ case EVENT_FOCUS:
108
+ if (ev.focused) {
109
+ this.#focusNode = ev.node;
110
+ this.#ring = ev.visible;
111
+ }
112
+ else if (this.#focusNode === ev.node) {
113
+ this.#focusNode = null;
114
+ this.#ring = false;
115
+ }
116
+ this.invalidate();
117
+ break;
118
+ }
119
+ }
120
+ #toggle(series) { series.setAttribute('visible', !series.shown); }
121
+ /** Where a series' entry is, in the chart's pixels (valid after layout), or null if it is not listed. */
122
+ entryRect(series) {
123
+ const item = this.#items.find((i) => i.series === series);
124
+ return item ? { x: this.#rect.x + item.x, y: this.#rect.y + item.y, width: item.width, height: this.#rowHeight } : null;
125
+ }
126
+ /** The entry at a point in the legend's own pixels. */
127
+ #at(x, y) {
128
+ const pad = ENTRY_GAP / 2;
129
+ return this.#items.find((i) => x >= i.x - 2 && x <= i.x + i.width + pad && y >= i.y - ROW_GAP / 2 && y <= i.y + this.#rowHeight + ROW_GAP / 2);
130
+ }
131
+ #label() { return this.styleFor(['legendLabel', 'legend', 'text']); }
132
+ /** Place every entry, given how wide the chart is, and say how big the legend is; null when there is nothing to list. */
133
+ #layout(available) {
134
+ const entries = this.entries, chart = this.requireParent();
135
+ this.#items = [];
136
+ if (entries.length === 0)
137
+ return null;
138
+ const label = this.#label(), measure = chart.measureText;
139
+ const top = this.attrs.title ? TITLE_BLOCK : 0;
140
+ const rowHeight = this.#rowHeight = Math.ceil(label.size * LINE_HEIGHT) + 2;
141
+ const widths = entries.map((s) => SAMPLE_WIDTH + SAMPLE_GAP + measureBlock(measure, s.attrs.name ?? '', label).width);
142
+ const titleWidth = this.attrs.title ? measureBlock(measure, this.attrs.title, this.styleFor(['legendTitle', 'legend', 'text'])).width : 0;
143
+ let width = titleWidth, y = top, x = 0, rows = 1;
144
+ entries.forEach((series, i) => {
145
+ if (this.horizontal) {
146
+ if (x > 0 && x + widths[i] > available) {
147
+ x = 0;
148
+ y += rowHeight + ROW_GAP;
149
+ rows++;
150
+ }
151
+ this.#items.push({ series, name: series.attrs.name ?? '', x, y, width: widths[i] });
152
+ x += widths[i] + ENTRY_GAP;
153
+ width = Math.max(width, x - ENTRY_GAP);
154
+ }
155
+ else {
156
+ this.#items.push({ series, name: series.attrs.name ?? '', x: 0, y: top + i * (rowHeight + ROW_GAP), width: widths[i] });
157
+ width = Math.max(width, widths[i]);
158
+ }
159
+ });
160
+ const count = this.horizontal ? rows : entries.length;
161
+ return { width: Math.ceil(width), height: top + count * rowHeight + (count - 1) * ROW_GAP };
162
+ }
163
+ #paint() {
164
+ const s = this.surface, root = s.wrapper(), items = this.#items, label = this.#label(), toggle = this.attrs.toggle !== false;
165
+ s.set(root, 'transform', `translate(${f2(this.#rect.x)} ${f2(this.#rect.y)})`);
166
+ applyText(s, root, label, 'start');
167
+ writeAria(s, root, this.attrs.meta, { role: 'group', ...(this.attrs.title && { label: this.attrs.title }) });
168
+ const title = this.attrs.title;
169
+ if (title) {
170
+ const t = s.node('title', 'text');
171
+ applyText(s, t, this.styleFor(['legendTitle', 'legend', 'text']), 'start');
172
+ s.set(t, 'y', label.size);
173
+ s.text(t, title);
174
+ }
175
+ else {
176
+ s.remove('title');
177
+ }
178
+ const h = this.#rowHeight, sampleH = Math.max(6, h - 6);
179
+ items.forEach((item, i) => {
180
+ const shown = item.series.shown, opacity = shown ? 1 : DIMMED;
181
+ const sample = paintSample(s, `s${i}`, item.series, item.x, item.y + (h - sampleH) / 2, SAMPLE_WIDTH, sampleH, this.#samples);
182
+ s.set(sample, 'opacity', opacity);
183
+ hideFromAssistive(s, sample);
184
+ const t = s.node(`t${i}`, 'text');
185
+ s.set(t, 'x', item.x + SAMPLE_WIDTH + SAMPLE_GAP);
186
+ s.set(t, 'y', f2(item.y + h / 2 + label.size * 0.35));
187
+ s.set(t, 'opacity', opacity);
188
+ hideFromAssistive(s, t); // the entry's own node carries the name
189
+ s.text(t, item.name);
190
+ // An invisible box over the entry is what the pointer is over, and what a screen reader finds.
191
+ const hit = s.node(`h${i}`, 'rect');
192
+ s.set(hit, 'x', item.x);
193
+ s.set(hit, 'y', item.y);
194
+ s.set(hit, 'width', item.width);
195
+ s.set(hit, 'height', h);
196
+ s.set(hit, 'fill', '#000');
197
+ s.set(hit, 'fill-opacity', 0);
198
+ s.set(hit, 'stroke', 'none');
199
+ s.set(hit, 'cursor', toggle ? 'pointer' : 'default');
200
+ s.set(hit, 'role', toggle ? 'button' : 'img');
201
+ s.set(hit, 'aria-label', item.name);
202
+ if (toggle) {
203
+ s.set(hit, 'aria-pressed', String(shown));
204
+ makeTabStop(s, hit);
205
+ inputOf(this.requireParent()).focusable(this, hit);
206
+ }
207
+ this.#nodes[i] = hit;
208
+ });
209
+ this.#nodes.length = items.length;
210
+ const focused = this.#ring && this.#focusNode ? this.#nodes.indexOf(this.#focusNode) : -1;
211
+ drawFocusRing(s, 'focus', this.styleFor(['focusRing', 'line']), focused >= 0 ? { x: items[focused].x, y: items[focused].y, width: items[focused].width, height: h } : null);
212
+ for (let i = items.length; i < this.#drawn; i++) {
213
+ s.removePrefix(`s${i}`);
214
+ this.#samples.delete(`s${i}`);
215
+ s.remove(`t${i}`);
216
+ s.remove(`h${i}`);
217
+ }
218
+ this.#drawn = items.length;
219
+ }
220
+ }
@@ -0,0 +1,80 @@
1
+ import type { Chart } from './chart.ts';
2
+ import { Dimension, type DimensionAttributes } from './dimension.ts';
3
+ import { type ElementRef } from './element.ts';
4
+ import type { Extent } from './geometry.ts';
5
+ /** No options of its own beyond a dimension's (range, padding, scale…). */
6
+ export interface MappedDimensionAttributes extends DimensionAttributes {
7
+ }
8
+ /**
9
+ * A dimension whose output is not a position but a value: a colour, a size. It takes its range from every series
10
+ * that binds to it. Put it in the chart, beside the planes, and bind renderers to it; to show what it means, add a key
11
+ * (`ColorKey`, `SizeKey`) that refers to it.
12
+ *
13
+ * No one drives it: a plane's axes are started and fixed by their plane, but this belongs to the chart, so it notices
14
+ * for itself when a new flush has begun. The first contribution of a flush starts it over, and the first read of the
15
+ * range, which is when something draws, fixes it.
16
+ */
17
+ export declare abstract class MappedDimension<Out, A extends MappedDimensionAttributes = MappedDimensionAttributes> extends Dimension<Chart, A> {
18
+ #private;
19
+ /** The output for a position 0..1 along the dimension. */
20
+ abstract valueAt(fraction: number): Out;
21
+ /** What data outside the range, or NaN, maps to. */
22
+ abstract get missing(): Out;
23
+ contribute(min: number, max: number): void;
24
+ get range(): Readonly<Extent>;
25
+ }
26
+ export type ColorRampName = 'viridis' | 'magma' | 'turbo' | 'blues' | 'greys' | 'diverging';
27
+ export interface ColorAxisAttributes extends MappedDimensionAttributes {
28
+ /**
29
+ * A named ramp, or a list of colours spread evenly. Linear interpolation needs `#rgb` or `#rrggbb` colours;
30
+ * a stepped ramp takes any CSS colour. Default 'viridis'.
31
+ */
32
+ ramp?: ColorRampName | readonly string[];
33
+ /** Blend between the stops or pick the nearest band. Default 'linear'. */
34
+ interpolation?: 'linear' | 'step';
35
+ /** Colour for data with no value or outside the range. Default '#bbb'. */
36
+ missing?: string;
37
+ }
38
+ /** Data to colour: `new ColorAxis({ title: 'Pressure' }, new LinearScale())`. */
39
+ export declare class ColorAxis extends MappedDimension<string, ColorAxisAttributes> {
40
+ static attrTypes: {
41
+ title: 1;
42
+ tickCount: 2;
43
+ tickStep: 2;
44
+ locale: 1;
45
+ onViewChange: 0;
46
+ missing: 1;
47
+ id: 1;
48
+ meta: {
49
+ title: 1;
50
+ description: 1;
51
+ role: 1;
52
+ };
53
+ };
54
+ /** The ramp's colours, low to high. */
55
+ get stops(): readonly string[];
56
+ valueAt(fraction: number): string;
57
+ get missing(): string;
58
+ }
59
+ export interface SizeAxisAttributes extends MappedDimensionAttributes {
60
+ /** The output range. Pixels for a size, so [2, 12] by default; it is also how to map to opacity ([0.2, 1]) or a width. */
61
+ output?: readonly [min: number, max: number];
62
+ /** Output for data with no value or outside the range. Default: the low end of `output`. */
63
+ missing?: number;
64
+ }
65
+ /** Data to a number: a marker size, a stroke width, an opacity. `new SizeAxis({ output: [3, 14] }, new LinearScale())`. */
66
+ export declare class SizeAxis extends MappedDimension<number, SizeAxisAttributes> {
67
+ /** The output range. */
68
+ get output(): readonly [number, number];
69
+ valueAt(fraction: number): number;
70
+ get missing(): number;
71
+ }
72
+ /** One attribute fed from one channel of the data through a mapped dimension. */
73
+ export interface Binding<Out> {
74
+ axis: ElementRef<MappedDimension<Out>>;
75
+ channel: string;
76
+ }
77
+ /** The bindings a renderer may declare: only the attributes in `Bindable`, each with a dimension of the matching output type. */
78
+ export type Bindings<Bindable extends object> = {
79
+ [K in keyof Bindable]?: Binding<Bindable[K]>;
80
+ };
package/dist/mapped.js ADDED
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Mapped dimensions: data to colour, size and so on, and the bindings that feed them to renderers.
3
+ *
4
+ * How a binding reaches the screen (an interdependent chain worth knowing):
5
+ * renderer `bind: { fill: { axis, channel } }` → the series reads `renderer.bindings` and asks its source for that
6
+ * channel → the series `contribute`s the channel's extent to the MappedDimension → the dimension fits a range from
7
+ * every series that bound it → at paint time the renderer normalises the channel through the dimension (0..1) and calls
8
+ * `valueAt` to get the colour/size. Keys read the same dimension, so legend and marks cannot disagree.
9
+ */
10
+ /* 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 */
11
+ import { STRING } from './attrs.js';
12
+ import { Dimension } from './dimension.js';
13
+ import { fail } from './element.js';
14
+ /**
15
+ * A dimension whose output is not a position but a value: a colour, a size. It takes its range from every series
16
+ * that binds to it. Put it in the chart, beside the planes, and bind renderers to it; to show what it means, add a key
17
+ * (`ColorKey`, `SizeKey`) that refers to it.
18
+ *
19
+ * No one drives it: a plane's axes are started and fixed by their plane, but this belongs to the chart, so it notices
20
+ * for itself when a new flush has begun. The first contribution of a flush starts it over, and the first read of the
21
+ * range, which is when something draws, fixes it.
22
+ */
23
+ export class MappedDimension extends Dimension {
24
+ // The flush this dimension last started afresh for. A chart-level dimension is not driven by a plane's `begin()`, so
25
+ // it compares the chart's flush counter with its own to know when contributions from the previous pass are stale.
26
+ #flush = -1;
27
+ #sync() {
28
+ const flush = this.chart?.flushCount ?? 0;
29
+ if (flush !== this.#flush) {
30
+ this.#flush = flush;
31
+ this.begin();
32
+ }
33
+ }
34
+ contribute(min, max) {
35
+ this.#sync();
36
+ super.contribute(min, max);
37
+ }
38
+ get range() {
39
+ this.#sync();
40
+ this.resolve();
41
+ return super.range;
42
+ }
43
+ }
44
+ const RAMPS = {
45
+ viridis: ['#440154', '#482878', '#3e4989', '#31688e', '#26828e', '#1f9e89', '#35b779', '#6ece58', '#b5de2b', '#fde725'],
46
+ magma: ['#000004', '#180f3e', '#451077', '#721f81', '#9f2f7f', '#cd4071', '#f1605d', '#fd9567', '#fec98d', '#fcfdbf'],
47
+ turbo: ['#30123b', '#4662d7', '#36aaf9', '#1ae4b6', '#72fe5e', '#c7ef34', '#faba39', '#f66b19', '#cb2a04', '#7a0403'],
48
+ blues: ['#f7fbff', '#deebf7', '#c6dbef', '#9ecae1', '#6baed6', '#4292c6', '#2171b5', '#08519c', '#08306b'],
49
+ greys: ['#ffffff', '#f0f0f0', '#d9d9d9', '#bdbdbd', '#969696', '#737373', '#525252', '#252525', '#000000'],
50
+ diverging: ['#2166ac', '#4393c3', '#92c5de', '#d1e5f0', '#f7f7f7', '#fddbc7', '#f4a582', '#d6604d', '#b2182b'],
51
+ };
52
+ /** A ramp's stops as flat r, g, b numbers, parsed once per list (keyed by the array, so the cache follows the ramp's lifetime). */
53
+ const rgbOf = new WeakMap();
54
+ function stopsToRgb(stops) {
55
+ const cached = rgbOf.get(stops);
56
+ if (cached)
57
+ return cached;
58
+ const rgb = new Float64Array(stops.length * 3);
59
+ stops.forEach((c, i) => {
60
+ const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(c) ?? fail(`a linear colour ramp takes #rgb or #rrggbb stops, not "${c}"`);
61
+ const h = m[1].length === 3 ? m[1].replace(/./g, '$&$&') : m[1];
62
+ for (let k = 0; k < 3; k++)
63
+ rgb[3 * i + k] = parseInt(h.slice(2 * k, 2 * k + 2), 16);
64
+ });
65
+ rgbOf.set(stops, rgb);
66
+ return rgb;
67
+ }
68
+ /** Two hex digits for a 0..255 channel. */
69
+ const hex2 = (v) => Math.round(v).toString(16).padStart(2, '0');
70
+ /** Data to colour: `new ColorAxis({ title: 'Pressure' }, new LinearScale())`. */
71
+ export class ColorAxis extends MappedDimension {
72
+ static attrTypes = { ...MappedDimension.attrTypes, missing: STRING };
73
+ /** The ramp's colours, low to high. */
74
+ get stops() {
75
+ const r = this.attrs.ramp ?? 'viridis';
76
+ return typeof r === 'string' ? RAMPS[r] : r;
77
+ }
78
+ valueAt(fraction) {
79
+ if (!(fraction >= -1e-9 && fraction <= 1 + 1e-9))
80
+ return this.missing;
81
+ const f = fraction < 0 ? 0 : fraction > 1 ? 1 : fraction;
82
+ const stops = this.stops, n = stops.length;
83
+ if (this.attrs.interpolation === 'step')
84
+ return stops[Math.min(n - 1, Math.floor(f * n))];
85
+ const rgb = stopsToRgb(stops), pos = f * (n - 1), i = Math.min(n - 2, Math.floor(pos));
86
+ if (n === 1)
87
+ return stops[0];
88
+ const t = pos - i;
89
+ return '#' + hex2(rgb[3 * i] + t * (rgb[3 * i + 3] - rgb[3 * i]))
90
+ + hex2(rgb[3 * i + 1] + t * (rgb[3 * i + 4] - rgb[3 * i + 1]))
91
+ + hex2(rgb[3 * i + 2] + t * (rgb[3 * i + 5] - rgb[3 * i + 2]));
92
+ }
93
+ get missing() { return this.attrs.missing ?? '#bbb'; }
94
+ }
95
+ /** Data to a number: a marker size, a stroke width, an opacity. `new SizeAxis({ output: [3, 14] }, new LinearScale())`. */
96
+ export class SizeAxis extends MappedDimension {
97
+ /** The output range. */
98
+ get output() { return this.attrs.output ?? [2, 12]; }
99
+ valueAt(fraction) {
100
+ if (!(fraction >= -1e-9 && fraction <= 1 + 1e-9))
101
+ return this.missing;
102
+ const [lo, hi] = this.output;
103
+ return lo + (fraction < 0 ? 0 : fraction > 1 ? 1 : fraction) * (hi - lo);
104
+ }
105
+ get missing() { return this.attrs.missing ?? this.output[0]; }
106
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Markers: one small shape per point.
3
+ *
4
+ * Marker
5
+ * ├─ DotMarker a zero-length stroke, round or square capped; its size is a stroke width
6
+ * ├─ LineMarker open shapes drawn with a stroke; `size` goes into the path data
7
+ * │ ├─ CrossMarker ×
8
+ * │ └─ PlusMarker +
9
+ * └─ ShapeMarker closed shapes, filled and stroked; `size` goes into the path data
10
+ * ├─ CircleMarker a bubble series is a CircleMarker with `size` bound to a channel
11
+ * └─ SquareMarker
12
+ *
13
+ * To write another, extend `Marker` (or one of the two below it) and say what one shape looks like.
14
+ */
15
+ import type { PointBuffer } from './data.ts';
16
+ import { type ChartEvent } from './events.ts';
17
+ import { type ResolvedBinding, SeriesRenderer, type SeriesRendererAttributes, type StyleBindable } from './renderer.ts';
18
+ import type { PlatformNode } from './platform.ts';
19
+ import type { Surface } from './surface.ts';
20
+ import type { ShapeProps } from './theme.ts';
21
+ /** The options shared by all markers; `Bindable` is what this kind of marker lets you bind (so a wrong one fails to compile). */
22
+ export interface MarkerAttributes<Bindable extends object> extends SeriesRendererAttributes<Bindable> {
23
+ /** Half the width of a marker in pixels: a radius, or the distance from the centre to an edge. Default 3. */
24
+ size?: number;
25
+ /**
26
+ * Draw all the markers that look alike as one path. Default true.
27
+ *
28
+ * One path per look is what keeps a large scatter cheap. The catch is that markers in one path do not blend where
29
+ * they overlap, so `opacity` darkens nothing. With `false` every marker is a node of its own and they do blend,
30
+ * but a node per point is slow to build, to write and to render: a bad idea for big data. Each marker then also
31
+ * gets exactly the value it binds, rather than the nearest of the `maxNodes`-limited steps.
32
+ */
33
+ merge?: boolean;
34
+ }
35
+ /** What a marker's node is told to look like: constants and bindings resolved, `size` in pixels. */
36
+ export interface MarkerStyle {
37
+ stroke: string;
38
+ strokeWidth: number;
39
+ fill: string;
40
+ opacity: number;
41
+ size: number;
42
+ }
43
+ /**
44
+ * The common work of all markers: cull what is off the plot, work out each point's look from its bindings, and group
45
+ * points that look alike into one path.
46
+ *
47
+ * Why grouping: a scatter of 100k points as 100k nodes is slow to build and to render. Instead each bound style is
48
+ * quantized into a few steps, a point's look is the combination of its steps (the `key`), and every point with the same
49
+ * key joins a single path. The number of nodes is then bounded by `maxNodes`, not by the data. Geometry (a bubble's
50
+ * radius) is not quantized: it goes straight into the path data, which costs the same however it varies.
51
+ */
52
+ export declare abstract class Marker<A extends MarkerAttributes<object>> extends SeriesRenderer<A> {
53
+ #private;
54
+ /** Path data for one marker centred on (x, y), `r` pixels from the centre to an edge. */
55
+ protected abstract shape(x: number, y: number, r: number): string;
56
+ /** True for a bound property that goes into `shape`, false for one that sets an attribute of the node. Only `size` can be geometric. */
57
+ protected abstract isGeometric(property: string): boolean;
58
+ /** Which theme selector holds this kind of marker's own look, between `marker` and the general `shape`. */
59
+ protected abstract readonly kind: 'dotMarker' | 'lineMarker' | 'shapeMarker';
60
+ /** Where the series' palette colour goes: the stroke for markers that are strokes, the fill for those that are filled. */
61
+ protected abstract accent(color: string): Partial<ShapeProps>;
62
+ /** Write a node's look. Every property that has a value, by constant or binding, is in `v`. */
63
+ protected abstract style(s: Surface, node: PlatformNode, v: MarkerStyle): void;
64
+ /** Its fill (or its stroke, for a marker that is only a stroke), unless that is bound to data. */
65
+ get swatch(): string | null;
66
+ /**
67
+ * One marker in the middle of the box, as big as it is on the plot (no bigger than the box). What is bound to data takes
68
+ * the value at the middle of its dimension, since a sample stands for no point in particular.
69
+ */
70
+ sample(s: Surface, parent: PlatformNode, key: string, width: number, height: number): void;
71
+ /** Its size, or the largest a bound size gives, and a pixel for the stroke round it. */
72
+ get reach(): number;
73
+ /** The bound properties with their dimensions resolved. A property is stylistic unless the marker puts it in the path. */
74
+ get bindings(): readonly ResolvedBinding[];
75
+ /** How many steps each stylistic binding is quantized to, so that the combinations stay within `maxNodes`. 0 when not grouping. */
76
+ get stepsPerBinding(): number;
77
+ onEvent(ev: ChartEvent): void;
78
+ /**
79
+ * Three phases: (1) turn each bound channel into a column of 0..1 positions along its dimension; (2) walk the points,
80
+ * dropping gaps and those off the plot, building each marker's path data and choosing its node; (3) write the nodes
81
+ * and remove those that no longer have any marker.
82
+ */
83
+ protected draw(points: Readonly<PointBuffer>): void;
84
+ }
85
+ export interface DotMarkerBindable {
86
+ stroke: string;
87
+ size: number;
88
+ opacity: number;
89
+ }
90
+ export interface DotMarkerAttributes extends MarkerAttributes<DotMarkerBindable> {
91
+ /** Round caps make circles, square caps make squares. Default 'round'. */
92
+ cap?: 'round' | 'square';
93
+ }
94
+ /**
95
+ * A dot is a stroke with no length, as wide as the dot is: the cheapest marker, and the one a dense scatter wants.
96
+ * Since its size is a stroke width, a bound `size` is one of the quantized styles, unlike on the other markers.
97
+ */
98
+ export declare class DotMarker extends Marker<DotMarkerAttributes> {
99
+ protected readonly kind = "dotMarker";
100
+ protected accent(color: string): Partial<ShapeProps>;
101
+ protected shape(x: number, y: number): string;
102
+ protected isGeometric(): boolean;
103
+ protected style(s: Surface, n: PlatformNode, v: MarkerStyle): void;
104
+ }
105
+ export interface LineMarkerBindable {
106
+ stroke: string;
107
+ strokeWidth: number;
108
+ opacity: number;
109
+ size: number;
110
+ }
111
+ /** Shapes made of strokes. The size is in the path data, so it varies per point at no cost. */
112
+ export declare abstract class LineMarker<A extends MarkerAttributes<LineMarkerBindable> = MarkerAttributes<LineMarkerBindable>> extends Marker<A> {
113
+ protected readonly kind = "lineMarker";
114
+ protected accent(color: string): Partial<ShapeProps>;
115
+ protected isGeometric(property: string): boolean;
116
+ protected style(s: Surface, n: PlatformNode, v: MarkerStyle): void;
117
+ }
118
+ /** ×: two diagonals, `size` from the centre to each end. */
119
+ export declare class CrossMarker extends LineMarker {
120
+ protected shape(x: number, y: number, r: number): string;
121
+ }
122
+ /** +: a horizontal and a vertical stroke. */
123
+ export declare class PlusMarker extends LineMarker {
124
+ protected shape(x: number, y: number, r: number): string;
125
+ }
126
+ export interface ShapeMarkerBindable extends StyleBindable {
127
+ size: number;
128
+ }
129
+ /** Closed shapes, filled and stroked. The most flexible markers: stroke, stroke width, fill, opacity and size are all bindable. */
130
+ export declare abstract class ShapeMarker<A extends MarkerAttributes<ShapeMarkerBindable> = MarkerAttributes<ShapeMarkerBindable>> extends Marker<A> {
131
+ protected readonly kind = "shapeMarker";
132
+ protected accent(color: string): Partial<ShapeProps>;
133
+ protected isGeometric(property: string): boolean;
134
+ protected style(s: Surface, n: PlatformNode, v: MarkerStyle): void;
135
+ }
136
+ /** A filled circle. A bubble series is this with `size` bound to a channel. */
137
+ export declare class CircleMarker extends ShapeMarker {
138
+ protected shape(x: number, y: number, r: number): string;
139
+ }
140
+ /** A square, `size` from the centre to each side. */
141
+ export declare class SquareMarker extends ShapeMarker {
142
+ protected shape(x: number, y: number, r: number): string;
143
+ }