inkplot 0.0.1-placeholder.0 → 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.
- package/LICENSE +21 -0
- package/README.md +45 -1
- package/dist/adapt.d.ts +10 -0
- package/dist/adapt.js +34 -0
- package/dist/annotation.d.ts +120 -0
- package/dist/annotation.js +267 -0
- package/dist/aria.d.ts +24 -0
- package/dist/aria.js +31 -0
- package/dist/attrs.d.ts +81 -0
- package/dist/attrs.js +84 -0
- package/dist/axis.d.ts +137 -0
- package/dist/axis.js +185 -0
- package/dist/bars.d.ts +80 -0
- package/dist/bars.js +255 -0
- package/dist/boxplot.d.ts +57 -0
- package/dist/boxplot.js +177 -0
- package/dist/boxzoom.d.ts +39 -0
- package/dist/boxzoom.js +138 -0
- package/dist/brush.d.ts +60 -0
- package/dist/brush.js +185 -0
- package/dist/bucket.d.ts +22 -0
- package/dist/bucket.js +62 -0
- package/dist/callout.d.ts +30 -0
- package/dist/callout.js +98 -0
- package/dist/chart.d.ts +126 -0
- package/dist/chart.js +257 -0
- package/dist/crosshair.d.ts +28 -0
- package/dist/crosshair.js +78 -0
- package/dist/cursor.d.ts +76 -0
- package/dist/cursor.js +344 -0
- package/dist/curve.d.ts +50 -0
- package/dist/curve.js +87 -0
- package/dist/curves.d.ts +104 -0
- package/dist/curves.js +375 -0
- package/dist/data.d.ts +209 -0
- package/dist/data.js +276 -0
- package/dist/decimation.d.ts +86 -0
- package/dist/decimation.js +410 -0
- package/dist/density.d.ts +55 -0
- package/dist/density.js +252 -0
- package/dist/dimension.d.ts +103 -0
- package/dist/dimension.js +205 -0
- package/dist/dom-platform.d.ts +53 -0
- package/dist/dom-platform.js +190 -0
- package/dist/element.d.ts +232 -0
- package/dist/element.js +414 -0
- package/dist/errors.d.ts +85 -0
- package/dist/errors.js +237 -0
- package/dist/events.d.ts +144 -0
- package/dist/events.js +19 -0
- package/dist/focus.d.ts +22 -0
- package/dist/focus.js +51 -0
- package/dist/furniture.d.ts +70 -0
- package/dist/furniture.js +166 -0
- package/dist/geometry.d.ts +40 -0
- package/dist/geometry.js +7 -0
- package/dist/heatmap.d.ts +57 -0
- package/dist/heatmap.js +207 -0
- package/dist/hit.d.ts +103 -0
- package/dist/hit.js +428 -0
- package/dist/index.d.ts +73 -0
- package/dist/index.js +73 -0
- package/dist/input.d.ts +42 -0
- package/dist/input.js +166 -0
- package/dist/jsx-dev-runtime.d.ts +9 -0
- package/dist/jsx-dev-runtime.js +13 -0
- package/dist/jsx-runtime.d.ts +59 -0
- package/dist/jsx-runtime.js +81 -0
- package/dist/key.d.ts +118 -0
- package/dist/key.js +199 -0
- package/dist/legend.d.ts +36 -0
- package/dist/legend.js +220 -0
- package/dist/mapped.d.ts +80 -0
- package/dist/mapped.js +106 -0
- package/dist/marker.d.ts +143 -0
- package/dist/marker.js +213 -0
- package/dist/panzoom.d.ts +119 -0
- package/dist/panzoom.js +467 -0
- package/dist/plane.d.ts +173 -0
- package/dist/plane.js +370 -0
- package/dist/platform.d.ts +113 -0
- package/dist/platform.js +80 -0
- package/dist/png.d.ts +44 -0
- package/dist/png.js +71 -0
- package/dist/pointer.d.ts +54 -0
- package/dist/pointer.js +132 -0
- package/dist/quantile.d.ts +3 -0
- package/dist/quantile.js +11 -0
- package/dist/radial.d.ts +116 -0
- package/dist/radial.js +336 -0
- package/dist/react.d.ts +441 -0
- package/dist/react.js +252 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +51 -0
- package/dist/renderer.d.ts +191 -0
- package/dist/renderer.js +164 -0
- package/dist/scale.d.ts +37 -0
- package/dist/scale.js +19 -0
- package/dist/scales.d.ts +69 -0
- package/dist/scales.js +134 -0
- package/dist/serialize.d.ts +77 -0
- package/dist/serialize.js +321 -0
- package/dist/series.d.ts +304 -0
- package/dist/series.js +513 -0
- package/dist/simple.d.ts +35 -0
- package/dist/simple.js +20 -0
- package/dist/sources.d.ts +116 -0
- package/dist/sources.js +237 -0
- package/dist/stack.d.ts +61 -0
- package/dist/stack.js +172 -0
- package/dist/stats.d.ts +38 -0
- package/dist/stats.js +159 -0
- package/dist/steps.d.ts +50 -0
- package/dist/steps.js +154 -0
- package/dist/stub.d.ts +3 -0
- package/dist/stub.js +5 -0
- package/dist/style-names.d.ts +1 -0
- package/dist/style-names.js +6 -0
- package/dist/summary.d.ts +44 -0
- package/dist/summary.js +58 -0
- package/dist/surface.d.ts +94 -0
- package/dist/surface.js +145 -0
- package/dist/table.d.ts +39 -0
- package/dist/table.js +37 -0
- package/dist/text.d.ts +23 -0
- package/dist/text.js +44 -0
- package/dist/theme.d.ts +174 -0
- package/dist/theme.js +73 -0
- package/dist/themes.d.ts +9 -0
- package/dist/themes.js +56 -0
- package/dist/time-intl.d.ts +194 -0
- package/dist/time-intl.js +144 -0
- package/dist/time-temporal.d.ts +236 -0
- package/dist/time-temporal.js +58 -0
- package/dist/time.d.ts +216 -0
- package/dist/time.js +154 -0
- package/dist/trend.d.ts +151 -0
- package/dist/trend.js +500 -0
- package/dist/web-components.d.ts +62 -0
- package/dist/web-components.js +264 -0
- package/package.json +48 -3
package/dist/brush.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Axis } from './axis.ts';
|
|
2
|
+
import { type BaseAttributes, ChartElement, type ElementRef } from './element.ts';
|
|
3
|
+
import { inputOf } from './input.ts';
|
|
4
|
+
import { type ChartEvent } from './events.ts';
|
|
5
|
+
import type { Plane } from './plane.ts';
|
|
6
|
+
/** What a brush covers on one axis, in that axis' own values. */
|
|
7
|
+
export interface BrushRange {
|
|
8
|
+
axis: Axis;
|
|
9
|
+
min: number;
|
|
10
|
+
max: number;
|
|
11
|
+
}
|
|
12
|
+
/** What `onBrush` hears: every axis covered, with the first horizontal and first vertical ones' ranges also by name. */
|
|
13
|
+
export interface BrushSelection {
|
|
14
|
+
ranges: readonly BrushRange[];
|
|
15
|
+
x?: {
|
|
16
|
+
min: number;
|
|
17
|
+
max: number;
|
|
18
|
+
};
|
|
19
|
+
y?: {
|
|
20
|
+
min: number;
|
|
21
|
+
max: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface BrushAttributes extends BaseAttributes {
|
|
25
|
+
/** The axes it may cover, as elements or ids. Default: all of the plane's that can be followed along. */
|
|
26
|
+
axes?: readonly ElementRef<Axis>[];
|
|
27
|
+
/** When a drag brushes: always (the default) or with Shift held, to leave a plain drag to `PanZoom`. */
|
|
28
|
+
activate?: 'shift' | 'always';
|
|
29
|
+
/** Which axes it covers. Default 'xy'. */
|
|
30
|
+
mode?: 'xy' | 'x' | 'y';
|
|
31
|
+
/** Smallest side of a brush, in pixels, along an axis it covers; below that a drag only clears. Default 4. */
|
|
32
|
+
minSize?: number;
|
|
33
|
+
/** Call `onBrush` while dragging too. Default false. */
|
|
34
|
+
live?: boolean;
|
|
35
|
+
/** Called with the selection, or null when it is cleared. */
|
|
36
|
+
onBrush?: (selection: BrushSelection | null) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare class Brush extends ChartElement<Plane, [], BrushAttributes> {
|
|
39
|
+
#private;
|
|
40
|
+
static takesInput: typeof inputOf;
|
|
41
|
+
static attrTypes: {
|
|
42
|
+
minSize: 2;
|
|
43
|
+
live: 3;
|
|
44
|
+
onBrush: 0;
|
|
45
|
+
id: 1;
|
|
46
|
+
meta: {
|
|
47
|
+
title: 1;
|
|
48
|
+
description: 1;
|
|
49
|
+
role: 1;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/** True while a box is being dragged. */
|
|
53
|
+
get selecting(): boolean;
|
|
54
|
+
/** What is brushed now, or null. */
|
|
55
|
+
get selection(): BrushSelection | null;
|
|
56
|
+
/** Brushes these ranges (as if dragged), or clears with an empty list. */
|
|
57
|
+
select(ranges: readonly BrushRange[]): void;
|
|
58
|
+
clear(): void;
|
|
59
|
+
onEvent(ev: ChartEvent): void;
|
|
60
|
+
}
|
package/dist/brush.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brush: drag a rectangle on the plot to pick a range of the data, and say what was picked. It zooms nothing.
|
|
3
|
+
*
|
|
4
|
+
* new CartesianPlane({}, x, y, series, new Brush({ mode: 'x', onBrush: (b) => filter(b?.ranges) }), new PanZoom())
|
|
5
|
+
*
|
|
6
|
+
* It is the other half of `BoxZoom`: the same drag, but the rectangle stays drawn (and follows the axes as they pan and
|
|
7
|
+
* zoom, since it is held in data terms) and the ranges it covers go to `onBrush`, for the app to filter a table, link another
|
|
8
|
+
* chart or total a selection. A click (a box under `minSize`) or `clear()` takes it away and calls `onBrush(null)`.
|
|
9
|
+
*
|
|
10
|
+
* `mode` is which axes the brush covers: 'xy', or 'x' / 'y' for a band across the plot (a time range). By default
|
|
11
|
+
* (`activate: 'always'`) every drag brushes, so give a plane that also pans `activate: 'shift'`; like `BoxZoom` it has to come
|
|
12
|
+
* **before** `PanZoom` to see a press first. With `live`, `onBrush` is called as the box is dragged and not only at the end.
|
|
13
|
+
* A touch brushes only with `activate: 'always'` and the plane taking every touch; a rim (polar) cannot be brushed.
|
|
14
|
+
* `select()` sets it from code, which is how two charts follow one another.
|
|
15
|
+
*/
|
|
16
|
+
import { AUTO, BOOLEAN, NUMBER } from './attrs.js';
|
|
17
|
+
import { Axis } from './axis.js';
|
|
18
|
+
import { ChartElement, fail, resolveRef } from './element.js';
|
|
19
|
+
import { inputOf } from './input.js';
|
|
20
|
+
import { EVENT_ATTACH, EVENT_PAINT, EVENT_POINTER } from './events.js';
|
|
21
|
+
import { f2 } from './geometry.js';
|
|
22
|
+
export class Brush extends ChartElement {
|
|
23
|
+
static takesInput = inputOf;
|
|
24
|
+
static attrTypes = { ...ChartElement.attrTypes, minSize: NUMBER, live: BOOLEAN, onBrush: AUTO };
|
|
25
|
+
#from = null;
|
|
26
|
+
#to = { x: 0, y: 0 };
|
|
27
|
+
#ranges = [];
|
|
28
|
+
/** True while a box is being dragged. */
|
|
29
|
+
get selecting() { return this.#from !== null; }
|
|
30
|
+
/** What is brushed now, or null. */
|
|
31
|
+
get selection() { return this.#ranges.length ? this.#describe(this.#ranges) : null; }
|
|
32
|
+
/** Brushes these ranges (as if dragged), or clears with an empty list. */
|
|
33
|
+
select(ranges) {
|
|
34
|
+
this.#ranges = ranges.map((r) => ({ axis: r.axis, min: Math.min(r.min, r.max), max: Math.max(r.min, r.max) }));
|
|
35
|
+
this.invalidate();
|
|
36
|
+
this.attrs.onBrush?.(this.selection);
|
|
37
|
+
}
|
|
38
|
+
clear() {
|
|
39
|
+
if (!this.#ranges.length)
|
|
40
|
+
return;
|
|
41
|
+
this.#ranges = [];
|
|
42
|
+
this.invalidate();
|
|
43
|
+
this.attrs.onBrush?.(null);
|
|
44
|
+
}
|
|
45
|
+
onEvent(ev) {
|
|
46
|
+
switch (ev.type) {
|
|
47
|
+
case EVENT_ATTACH: {
|
|
48
|
+
const plane = this.requireParent();
|
|
49
|
+
for (const sibling of plane) {
|
|
50
|
+
if (sibling === this)
|
|
51
|
+
break;
|
|
52
|
+
if (sibling.claimsPress)
|
|
53
|
+
fail('add a Brush before PanZoom in a plane: the brush has to see a press first to take it');
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
case EVENT_PAINT:
|
|
58
|
+
this.#paint();
|
|
59
|
+
return;
|
|
60
|
+
case EVENT_POINTER:
|
|
61
|
+
this.#pointer(ev);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
#describe(ranges) {
|
|
66
|
+
const out = { ranges };
|
|
67
|
+
for (const r of ranges) {
|
|
68
|
+
const key = r.axis.ruler.horizontal ? 'x' : 'y';
|
|
69
|
+
out[key] ??= { min: r.min, max: r.max };
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
#axes() {
|
|
74
|
+
const a = this.attrs, mode = a.mode ?? 'xy';
|
|
75
|
+
const axes = a.axes ? a.axes.map((ref) => resolveRef(ref, this, Axis)) : [...this.requireParent().axes];
|
|
76
|
+
return axes.filter((axis) => axis.ruler.fractionAt && (mode === 'xy' || (mode === 'x') === axis.ruler.horizontal));
|
|
77
|
+
}
|
|
78
|
+
/** The ranges a box from `from` to `to` covers. */
|
|
79
|
+
#cover(from, to) {
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const axis of this.#axes()) {
|
|
82
|
+
const ruler = axis.ruler, at = ruler.fractionAt;
|
|
83
|
+
if (!at)
|
|
84
|
+
continue; // `#axes` keeps only rulers that have one
|
|
85
|
+
const v0 = axis.denormalize(at.call(ruler, from.x, from.y)), v1 = axis.denormalize(at.call(ruler, to.x, to.y));
|
|
86
|
+
if (v0 !== v0 || v1 !== v1)
|
|
87
|
+
continue;
|
|
88
|
+
out.push({ axis, min: Math.min(v0, v1), max: Math.max(v0, v1) });
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
#pointer(ev) {
|
|
93
|
+
const a = this.attrs, always = (a.activate ?? 'always') === 'always';
|
|
94
|
+
if ((ev.pointerType === 'touch' && !always) || ev.kind === 'wheel' || ev.kind === 'pinch')
|
|
95
|
+
return;
|
|
96
|
+
const plane = this.requireParent(), r = plane.rect, min = a.minSize ?? 4, mode = a.mode ?? 'xy';
|
|
97
|
+
const clampX = (x) => Math.min(r.width, Math.max(0, x)), clampY = (y) => Math.min(r.height, Math.max(0, y));
|
|
98
|
+
// A box a pixel or two across is a click; along an axis it does not cover its size does not count.
|
|
99
|
+
const small = (from, to) => (mode !== 'y' && Math.abs(to.x - from.x) < min) || (mode !== 'x' && Math.abs(to.y - from.y) < min);
|
|
100
|
+
switch (ev.kind) {
|
|
101
|
+
case 'down':
|
|
102
|
+
if (ev.handled || ev.outside || !(ev.buttons & 1) || !plane.covers(ev.x, ev.y))
|
|
103
|
+
return;
|
|
104
|
+
if (!always && !ev.shift)
|
|
105
|
+
return;
|
|
106
|
+
this.#from = this.#to = { x: ev.x, y: ev.y };
|
|
107
|
+
this.chart?.platform?.setCursor?.('crosshair');
|
|
108
|
+
ev.handled = true;
|
|
109
|
+
return;
|
|
110
|
+
case 'move': {
|
|
111
|
+
if (!this.#from)
|
|
112
|
+
return;
|
|
113
|
+
if (!(ev.buttons & 1)) {
|
|
114
|
+
this.#abandon();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.#to = { x: clampX(ev.x), y: clampY(ev.y) };
|
|
118
|
+
this.invalidate();
|
|
119
|
+
if (a.live && !small(this.#from, this.#to))
|
|
120
|
+
a.onBrush?.(this.#describe(this.#cover(this.#from, this.#to)));
|
|
121
|
+
ev.handled = true;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
case 'up': {
|
|
125
|
+
const from = this.#from;
|
|
126
|
+
if (!from)
|
|
127
|
+
return;
|
|
128
|
+
const to = { x: clampX(ev.x), y: clampY(ev.y) };
|
|
129
|
+
this.#abandon();
|
|
130
|
+
if (small(from, to))
|
|
131
|
+
this.clear();
|
|
132
|
+
else
|
|
133
|
+
this.select(this.#cover(from, to));
|
|
134
|
+
ev.handled = true;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
case 'leave':
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
#abandon() {
|
|
142
|
+
this.#from = null;
|
|
143
|
+
this.chart?.platform?.setCursor?.('');
|
|
144
|
+
this.invalidate();
|
|
145
|
+
}
|
|
146
|
+
/** The rectangle in plot pixels for the box being dragged, else for what is brushed (which follows the axes). */
|
|
147
|
+
#rect() {
|
|
148
|
+
const r = this.requireParent().rect;
|
|
149
|
+
if (this.#from)
|
|
150
|
+
return { x0: Math.min(this.#from.x, this.#to.x), y0: Math.min(this.#from.y, this.#to.y), x1: Math.max(this.#from.x, this.#to.x), y1: Math.max(this.#from.y, this.#to.y) };
|
|
151
|
+
if (!this.#ranges.length)
|
|
152
|
+
return null;
|
|
153
|
+
let x0 = 0, x1 = r.width, y0 = 0, y1 = r.height;
|
|
154
|
+
for (const { axis, min, max } of this.#ranges) {
|
|
155
|
+
const ruler = axis.ruler, p = ruler.at(axis.fractionOf(min)), q = ruler.at(axis.fractionOf(max));
|
|
156
|
+
if (ruler.horizontal) {
|
|
157
|
+
x0 = Math.min(p[0], q[0]);
|
|
158
|
+
x1 = Math.max(p[0], q[0]);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
y0 = Math.min(p[1], q[1]);
|
|
162
|
+
y1 = Math.max(p[1], q[1]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { x0: Math.max(0, x0), y0: Math.max(0, y0), x1: Math.min(r.width, x1), y1: Math.min(r.height, y1) };
|
|
166
|
+
}
|
|
167
|
+
#paint() {
|
|
168
|
+
const s = this.surface, box = this.#rect();
|
|
169
|
+
if (!box || box.x1 - box.x0 !== box.x1 - box.x0 || box.y1 - box.y0 !== box.y1 - box.y0) {
|
|
170
|
+
s.remove('brush');
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const look = this.styleFor(['selection', 'shape']);
|
|
174
|
+
const node = s.node('brush', 'rect', { above: true });
|
|
175
|
+
s.set(node, 'x', f2(box.x0));
|
|
176
|
+
s.set(node, 'y', f2(box.y0));
|
|
177
|
+
s.set(node, 'width', f2(box.x1 - box.x0));
|
|
178
|
+
s.set(node, 'height', f2(box.y1 - box.y0));
|
|
179
|
+
s.set(node, 'fill', look.fill);
|
|
180
|
+
s.set(node, 'stroke', look.stroke);
|
|
181
|
+
s.set(node, 'stroke-width', look.strokeWidth);
|
|
182
|
+
s.set(node, 'opacity', look.opacity);
|
|
183
|
+
s.set(node, 'pointer-events', 'none');
|
|
184
|
+
}
|
|
185
|
+
}
|
package/dist/bucket.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grouping by look: what renderers that vary a style by data (markers, heat map cells) share.
|
|
3
|
+
*
|
|
4
|
+
* Each stylistic binding is quantized into a few steps, a shape's look is the combination of its steps (its `key`), and
|
|
5
|
+
* every shape with the same key joins one path. The number of paths is then bounded by `maxNodes`, not by the data.
|
|
6
|
+
*/
|
|
7
|
+
import { resolveRef } from './element.ts';
|
|
8
|
+
import type { ResolvedBinding } from './renderer.ts';
|
|
9
|
+
/** The bound properties of `attrs.bind` with their dimensions resolved from `from`. */
|
|
10
|
+
export declare function resolveBindings(bind: unknown, from: Parameters<typeof resolveRef>[1], isGeometric: (property: string) => boolean): ResolvedBinding[];
|
|
11
|
+
/** How many steps each of the stylistic bindings is quantized to so that the combinations stay within `maxNodes`; 0 for none. */
|
|
12
|
+
export declare function stepsFor(bindings: readonly ResolvedBinding[], maxNodes: number): number;
|
|
13
|
+
/** A binding's channel as a column of 0..1 positions along its dimension. `column` is where the channel sits in each row. */
|
|
14
|
+
export declare function fractionsOf(b: ResolvedBinding, data: ArrayLike<number>, column: number, stride: number, count: number): Float64Array;
|
|
15
|
+
/** Which look a shape has: one step along each bound style, the last step meaning "no value". */
|
|
16
|
+
export declare function lookKey(styled: readonly {
|
|
17
|
+
f: Float64Array;
|
|
18
|
+
}[], i: number, steps: number): number;
|
|
19
|
+
/** Undo `lookKey`: the value each bound style has for that key (the middle of its step), handed to `write` by property. */
|
|
20
|
+
export declare function lookValues(key: number, styled: readonly {
|
|
21
|
+
b: ResolvedBinding;
|
|
22
|
+
}[], steps: number, write: (property: string, value: string | number) => void): void;
|
package/dist/bucket.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grouping by look: what renderers that vary a style by data (markers, heat map cells) share.
|
|
3
|
+
*
|
|
4
|
+
* Each stylistic binding is quantized into a few steps, a shape's look is the combination of its steps (its `key`), and
|
|
5
|
+
* every shape with the same key joins one path. The number of paths is then bounded by `maxNodes`, not by the data.
|
|
6
|
+
*/
|
|
7
|
+
import { fail, resolveRef } from './element.js';
|
|
8
|
+
/** The bound properties of `attrs.bind` with their dimensions resolved from `from`. */
|
|
9
|
+
export function resolveBindings(bind, from, isGeometric) {
|
|
10
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- the `bind` attribute, typed here as the shape this element reads
|
|
11
|
+
const map = bind;
|
|
12
|
+
if (!map)
|
|
13
|
+
return [];
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const property in map) {
|
|
16
|
+
const b = map[property];
|
|
17
|
+
if (b)
|
|
18
|
+
out.push({ property, channel: b.channel, axis: resolveRef(b.axis, from), stylistic: !isGeometric(property) });
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
/** How many steps each of the stylistic bindings is quantized to so that the combinations stay within `maxNodes`; 0 for none. */
|
|
23
|
+
export function stepsFor(bindings, maxNodes) {
|
|
24
|
+
const k = bindings.reduce((n, b) => n + (b.stylistic ? 1 : 0), 0);
|
|
25
|
+
if (k === 0)
|
|
26
|
+
return 0;
|
|
27
|
+
// The last step of each binding is kept for data with no value, so a binding colours with one fewer.
|
|
28
|
+
const steps = Math.floor(maxNodes ** (1 / k) + 1e-9);
|
|
29
|
+
if (steps < 2)
|
|
30
|
+
fail(`maxNodes ${maxNodes} leaves fewer than 2 steps for ${k} bound styles`);
|
|
31
|
+
return steps;
|
|
32
|
+
}
|
|
33
|
+
/** A binding's channel as a column of 0..1 positions along its dimension. `column` is where the channel sits in each row. */
|
|
34
|
+
export function fractionsOf(b, data, column, stride, count) {
|
|
35
|
+
const f = new Float64Array(count);
|
|
36
|
+
// 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
|
|
37
|
+
for (let i = 0, j = column; i < count; i++, j += stride)
|
|
38
|
+
f[i] = data[j];
|
|
39
|
+
b.axis.normalize(f, count, 1, 0);
|
|
40
|
+
return f;
|
|
41
|
+
}
|
|
42
|
+
/** Which look a shape has: one step along each bound style, the last step meaning "no value". */
|
|
43
|
+
export function lookKey(styled, i, steps) {
|
|
44
|
+
let key = 0, mult = 1;
|
|
45
|
+
for (const { f } of styled) {
|
|
46
|
+
// 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
|
|
47
|
+
const p = f[i];
|
|
48
|
+
key += (p >= -1e-9 && p <= 1 + 1e-9 ? Math.min(steps - 2, Math.floor((p < 0 ? 0 : p) * (steps - 1))) : steps - 1) * mult;
|
|
49
|
+
mult *= steps;
|
|
50
|
+
}
|
|
51
|
+
return key;
|
|
52
|
+
}
|
|
53
|
+
/** Undo `lookKey`: the value each bound style has for that key (the middle of its step), handed to `write` by property. */
|
|
54
|
+
export function lookValues(key, styled, steps, write) {
|
|
55
|
+
let rest = key;
|
|
56
|
+
for (const { b } of styled) {
|
|
57
|
+
const step = rest % steps;
|
|
58
|
+
rest = (rest - step) / steps;
|
|
59
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- an axis hands back `string | number` for a property the attribute types vouch for
|
|
60
|
+
write(b.property, (step === steps - 1 ? b.axis.missing : b.axis.valueAt((step + 0.5) / (steps - 1))));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drawing a callout: a box of text, optionally joined by an arm to the point it is about. Shared by the annotation and
|
|
3
|
+
* the tooltip, which differ in where the box goes and when it is there, not in what it looks like.
|
|
4
|
+
*/
|
|
5
|
+
import type { Rect, Size } from './geometry.ts';
|
|
6
|
+
import type { TextMeasurer } from './platform.ts';
|
|
7
|
+
import { type SampleCache, type Series } from './series.ts';
|
|
8
|
+
import type { Surface } from './surface.ts';
|
|
9
|
+
import type { CalloutMetrics, LineProps, ShapeProps, TextProps } from './theme.ts';
|
|
10
|
+
/** Everything a callout's look takes from the theme, resolved by the element that draws it (whose chains differ). */
|
|
11
|
+
export interface CalloutLook {
|
|
12
|
+
metrics: CalloutMetrics;
|
|
13
|
+
text: TextProps;
|
|
14
|
+
box: ShapeProps;
|
|
15
|
+
arm: LineProps;
|
|
16
|
+
}
|
|
17
|
+
/** How big the box is for `text`: the text's block and the padding all round, and room for a column of series samples if `swatches`. */
|
|
18
|
+
export declare function calloutSize(measure: TextMeasurer, text: string, look: CalloutLook, swatches?: boolean): Size;
|
|
19
|
+
/**
|
|
20
|
+
* Draw the box at `box`. With an `anchor`, an arm runs from the middle of the edge the anchor is furthest beyond to it
|
|
21
|
+
* (measured in half-sides, so a wide box does not always pick its top and bottom), and the 'edge' outline draws that
|
|
22
|
+
* edge alone. Needs a surface whose wrapper carries the text style. Returns the lines drawn, to pass back next time.
|
|
23
|
+
*
|
|
24
|
+
* `swatches` gives a series for each line that starts with a sample of it (null for one that does not): its line, box or
|
|
25
|
+
* marker as drawn on the plot. The text then starts after a column the width of a sample, which `calloutSize` must have been
|
|
26
|
+
* told of. `samples` is what is remembered of them between calls (see `paintSample`).
|
|
27
|
+
*/
|
|
28
|
+
export declare function paintCallout(s: Surface, text: string, box: Rect, anchor: readonly [number, number] | null, look: CalloutLook, lines: number, swatches?: readonly (Series | null)[], samples?: SampleCache): number;
|
|
29
|
+
/** Take a callout off the surface. */
|
|
30
|
+
export declare function clearCallout(s: Surface, lines: number, samples?: SampleCache): void;
|
package/dist/callout.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { circlePath, f2 } from './geometry.js';
|
|
2
|
+
import { paintSample } from './series.js';
|
|
3
|
+
import { LINE_HEIGHT, applyText, measureBlock, paintLines } from './text.js';
|
|
4
|
+
/** A sample of a series before a line of text: as wide as a short line, and as tall as the lowercase letters. */
|
|
5
|
+
const sampleWidth = (look) => Math.round(look.text.size * 1.4);
|
|
6
|
+
const sampleHeight = (look) => Math.max(5, Math.round(look.text.size * 0.7));
|
|
7
|
+
/** The room a column of samples takes at the start of the text: a sample and the gap after it. */
|
|
8
|
+
const swatchIndent = (look) => sampleWidth(look) + 5;
|
|
9
|
+
/** How big the box is for `text`: the text's block and the padding all round, and room for a column of series samples if `swatches`. */
|
|
10
|
+
export function calloutSize(measure, text, look, swatches = false) {
|
|
11
|
+
const size = measureBlock(measure, text, look.text), pad = 2 * look.metrics.padding;
|
|
12
|
+
return { width: size.width + pad + (swatches ? swatchIndent(look) : 0), height: size.height + pad };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Draw the box at `box`. With an `anchor`, an arm runs from the middle of the edge the anchor is furthest beyond to it
|
|
16
|
+
* (measured in half-sides, so a wide box does not always pick its top and bottom), and the 'edge' outline draws that
|
|
17
|
+
* edge alone. Needs a surface whose wrapper carries the text style. Returns the lines drawn, to pass back next time.
|
|
18
|
+
*
|
|
19
|
+
* `swatches` gives a series for each line that starts with a sample of it (null for one that does not): its line, box or
|
|
20
|
+
* marker as drawn on the plot. The text then starts after a column the width of a sample, which `calloutSize` must have been
|
|
21
|
+
* told of. `samples` is what is remembered of them between calls (see `paintSample`).
|
|
22
|
+
*/
|
|
23
|
+
export function paintCallout(s, text, box, anchor, look, lines, swatches, samples) {
|
|
24
|
+
const { metrics, box: boxLook, arm } = look, w = box.width, h = box.height;
|
|
25
|
+
const cx = box.x + w / 2, cy = box.y + h / 2;
|
|
26
|
+
const rect = s.node('box', 'path');
|
|
27
|
+
s.set(rect, 'd', `M${f2(box.x)} ${f2(box.y)}h${f2(w)}v${f2(h)}h${f2(-w)}z`);
|
|
28
|
+
s.set(rect, 'fill', boxLook.fill);
|
|
29
|
+
s.set(rect, 'fill-opacity', boxLook.opacity);
|
|
30
|
+
s.set(rect, 'stroke', metrics.outline === 'box' ? boxLook.stroke : 'none');
|
|
31
|
+
s.set(rect, 'stroke-width', boxLook.strokeWidth);
|
|
32
|
+
if (anchor) {
|
|
33
|
+
const [px, py] = anchor;
|
|
34
|
+
const rx = (px - cx) / (w / 2), ry = (py - cy) / (h / 2);
|
|
35
|
+
const horizontal = Math.abs(rx) >= Math.abs(ry), sign = (horizontal ? rx : ry) < 0 ? -1 : 1;
|
|
36
|
+
const [ex1, ey1, ex2, ey2] = horizontal
|
|
37
|
+
? [cx + sign * w / 2, box.y, cx + sign * w / 2, box.y + h]
|
|
38
|
+
: [box.x, cy + sign * h / 2, box.x + w, cy + sign * h / 2];
|
|
39
|
+
const [ax, ay] = horizontal ? [cx + sign * w / 2, cy] : [cx, cy + sign * h / 2];
|
|
40
|
+
if (metrics.outline === 'edge') {
|
|
41
|
+
const edge = s.node('edge', 'path');
|
|
42
|
+
s.set(edge, 'd', `M${f2(ex1)} ${f2(ey1)}L${f2(ex2)} ${f2(ey2)}`);
|
|
43
|
+
s.set(edge, 'fill', 'none');
|
|
44
|
+
s.set(edge, 'stroke', boxLook.stroke);
|
|
45
|
+
s.set(edge, 'stroke-width', boxLook.strokeWidth);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
s.remove('edge');
|
|
49
|
+
}
|
|
50
|
+
const armNode = s.node('arm', 'path');
|
|
51
|
+
s.set(armNode, 'd', `M${f2(px)} ${f2(py)}L${f2(ax)} ${f2(ay)}`);
|
|
52
|
+
s.set(armNode, 'fill', 'none');
|
|
53
|
+
s.set(armNode, 'stroke', arm.color);
|
|
54
|
+
s.set(armNode, 'stroke-width', arm.width);
|
|
55
|
+
s.set(armNode, 'stroke-dasharray', arm.dash);
|
|
56
|
+
s.set(armNode, 'opacity', arm.opacity);
|
|
57
|
+
if (metrics.pointer > 0) {
|
|
58
|
+
const dot = s.node('dot', 'path');
|
|
59
|
+
s.set(dot, 'd', circlePath(px, py, metrics.pointer));
|
|
60
|
+
s.set(dot, 'fill', arm.color);
|
|
61
|
+
s.set(dot, 'stroke', 'none');
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
s.remove('dot');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
for (const key of ['edge', 'arm', 'dot'])
|
|
69
|
+
s.remove(key);
|
|
70
|
+
}
|
|
71
|
+
applyText(s, s.wrapper(), look.text);
|
|
72
|
+
const pad = metrics.padding, align = look.text.align, indent = swatches ? swatchIndent(look) : 0;
|
|
73
|
+
const x = align === 'start' ? box.x + pad + indent : align === 'middle' ? cx + indent / 2 : box.x + w - pad;
|
|
74
|
+
const drawn = paintLines(s, 'line-', text, x, box.y + pad, look.text, lines);
|
|
75
|
+
const size = look.text.size, sw = sampleWidth(look), sh = sampleHeight(look);
|
|
76
|
+
for (let i = 0; i < Math.max(drawn, lines); i++) {
|
|
77
|
+
const series = swatches?.[i];
|
|
78
|
+
if (series && samples && i < drawn) {
|
|
79
|
+
// The middle of the x-height of the line: its baseline is at 0.85 of the size down.
|
|
80
|
+
paintSample(s, `sw-${i}`, series, box.x + pad, box.y + pad + size * (0.85 - 0.32 + LINE_HEIGHT * i) - sh / 2, sw, sh, samples);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
s.removePrefix(`sw-${i}`);
|
|
84
|
+
samples?.delete(`sw-${i}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return drawn;
|
|
88
|
+
}
|
|
89
|
+
/** Take a callout off the surface. */
|
|
90
|
+
export function clearCallout(s, lines, samples) {
|
|
91
|
+
for (const key of ['box', 'edge', 'arm', 'dot'])
|
|
92
|
+
s.remove(key);
|
|
93
|
+
for (let i = 0; i < lines; i++) {
|
|
94
|
+
s.remove(`line-${i}`);
|
|
95
|
+
s.removePrefix(`sw-${i}`);
|
|
96
|
+
samples?.delete(`sw-${i}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/chart.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { type AnyElement, type BaseAttributes, ChartElement } from './element.ts';
|
|
2
|
+
import { type ChartEvent, type ChartEventInit } from './events.ts';
|
|
3
|
+
import type { InputHost } from './input.ts';
|
|
4
|
+
import type { ChartTitle } from './furniture.ts';
|
|
5
|
+
import type { Cursor } from './cursor.ts';
|
|
6
|
+
import { type Insets } from './geometry.ts';
|
|
7
|
+
import type { ColorAxis, SizeAxis } from './mapped.ts';
|
|
8
|
+
import type { ColorKey, SizeKey } from './key.ts';
|
|
9
|
+
import type { Legend } from './legend.ts';
|
|
10
|
+
import type { ChartSummary } from './summary.ts';
|
|
11
|
+
import { Plane } from './plane.ts';
|
|
12
|
+
import type { BaseTheme } from './theme.ts';
|
|
13
|
+
import { type Platform, type TextMeasurer } from './platform.ts';
|
|
14
|
+
import { type StageName } from './surface.ts';
|
|
15
|
+
/** What one flush did and cost: the numbers the playground shows, and what tests assert caching against. */
|
|
16
|
+
export interface FlushStats {
|
|
17
|
+
readonly total: number;
|
|
18
|
+
readonly stages: Readonly<Record<StageName, number>>;
|
|
19
|
+
readonly filled: number;
|
|
20
|
+
readonly fillsCached: number;
|
|
21
|
+
readonly projected: number;
|
|
22
|
+
readonly attributeWrites: number;
|
|
23
|
+
readonly attributeWritesSkipped: number;
|
|
24
|
+
readonly attributesWritten: readonly string[];
|
|
25
|
+
readonly pointsIn: number;
|
|
26
|
+
readonly pointsDrawn: number;
|
|
27
|
+
readonly pathBytes: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A chart child that words what the chart shows, for a screen reader. The words are its own (so the core has none, and a
|
|
31
|
+
* chart in another language supplies its own), and the chart uses them when `meta.description` says nothing.
|
|
32
|
+
*/
|
|
33
|
+
export interface Describer {
|
|
34
|
+
describe(): {
|
|
35
|
+
description: string;
|
|
36
|
+
roleDescription?: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export interface ChartAttributes extends BaseAttributes {
|
|
40
|
+
/** Every default look: required, so a chart bundles exactly the theme it names (`lightTheme`, `darkTheme`, or your own). */
|
|
41
|
+
theme: BaseTheme;
|
|
42
|
+
/** Width of the drawing. Default: whatever the platform reports, followed as it changes. */
|
|
43
|
+
width?: number;
|
|
44
|
+
/** Height of the drawing. Default: whatever the platform reports. When set on the chart it beats the size a platform reports, including the one given to `toSvgString`. */
|
|
45
|
+
height?: number;
|
|
46
|
+
/** Margin around everything, in pixels: one number for all sides, or any subset of the four. Default 10. */
|
|
47
|
+
padding?: number | Partial<Insets>;
|
|
48
|
+
/** Replaces the platform's text measurement, e.g. to match a font the platform cannot see. */
|
|
49
|
+
measureText?: TextMeasurer;
|
|
50
|
+
/**
|
|
51
|
+
* A BCP 47 tag (`'de'`, `'fr-CA'`) for how numbers and dates are written: digit grouping, the decimal mark, month names.
|
|
52
|
+
* Axes and time scales that have no `locale` of their own use it. Default: numbers as plain digits with a point, and the
|
|
53
|
+
* runtime's own locale for dates. The words the library says (a summary, a tooltip's labels) are not translated by this; those
|
|
54
|
+
* come from the components that say them, which take your words.
|
|
55
|
+
*/
|
|
56
|
+
locale?: string;
|
|
57
|
+
/** Called after every flush with its statistics. */
|
|
58
|
+
onFlush?: (stats: FlushStats) => void;
|
|
59
|
+
}
|
|
60
|
+
/** Open for augmentation, like PlaneChildMap. */
|
|
61
|
+
export interface ChartChildMap {
|
|
62
|
+
plane: Plane;
|
|
63
|
+
title: ChartTitle;
|
|
64
|
+
cursor: Cursor;
|
|
65
|
+
colorAxis: ColorAxis;
|
|
66
|
+
sizeAxis: SizeAxis;
|
|
67
|
+
colorKey: ColorKey;
|
|
68
|
+
sizeKey: SizeKey;
|
|
69
|
+
legend: Legend;
|
|
70
|
+
summary: ChartSummary;
|
|
71
|
+
}
|
|
72
|
+
export type ChartChildren = ChartChildMap[keyof ChartChildMap][];
|
|
73
|
+
/** The root: the only element that knows about the platform lifecycle (attach, flush, detach). */
|
|
74
|
+
export declare class Chart extends ChartElement<null, ChartChildren, ChartAttributes> {
|
|
75
|
+
#private;
|
|
76
|
+
static attrTypes: {
|
|
77
|
+
measureText: 0;
|
|
78
|
+
onFlush: 0;
|
|
79
|
+
locale: 1;
|
|
80
|
+
id: 1;
|
|
81
|
+
meta: {
|
|
82
|
+
title: 1;
|
|
83
|
+
description: 1;
|
|
84
|
+
role: 1;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
get chart(): Chart;
|
|
88
|
+
private readonly counts;
|
|
89
|
+
private unschedule;
|
|
90
|
+
private readonly subscriptions;
|
|
91
|
+
/** The input host, once something in the chart has asked for it (see `inputOf`): null on a chart nothing listens to. */
|
|
92
|
+
input: InputHost | null;
|
|
93
|
+
get platform(): Platform | null;
|
|
94
|
+
get lastFlush(): FlushStats | null;
|
|
95
|
+
/** How many flushes have begun. Elements that belong to the chart rather than a plane use it to tell a new pass from the same one. */
|
|
96
|
+
get flushCount(): number;
|
|
97
|
+
/** True from any invalidation until the next flush. */
|
|
98
|
+
get needsFlush(): boolean;
|
|
99
|
+
/** Construction has no side effects; this makes the chart live. */
|
|
100
|
+
attach(platform: Platform): this;
|
|
101
|
+
/** Undo `attach`: unsubscribe, take the nodes out of the platform. The tree is kept, so the chart can be attached again. */
|
|
102
|
+
detach(): this;
|
|
103
|
+
/** The request has reached the root: note it, and on a scheduling platform arrange a flush. */
|
|
104
|
+
invalidate(): void;
|
|
105
|
+
/** Put every axis of every plane back to the range its data asks for. */
|
|
106
|
+
resetViews(): void;
|
|
107
|
+
/** Send an event through the tree from `root` (default the chart). For the input host, which lives outside this class. */
|
|
108
|
+
deliver(init: ChartEventInit, root?: AnyElement): ChartEvent;
|
|
109
|
+
/** Preference order: the chart's own, the platform's (real fonts), then a deliberately wide estimate. */
|
|
110
|
+
get measureText(): TextMeasurer;
|
|
111
|
+
/** Run layout and paint now. The only way to render on a platform with no `schedule`. */
|
|
112
|
+
flush(): FlushStats;
|
|
113
|
+
}
|
|
114
|
+
/** What `toSvgString` takes: a size for what the chart's own `width` and `height` leave open (else 640 x 400), and how to name the ids inside the picture. */
|
|
115
|
+
export interface SvgOptions {
|
|
116
|
+
width?: number;
|
|
117
|
+
height?: number;
|
|
118
|
+
/**
|
|
119
|
+
* Starts every `id` in the SVG (clip paths, gradients) and every `url(#…)` that points at one. Default: `ink` and eight hex digits of a
|
|
120
|
+
* hash of the picture itself, so two different charts never share an id (a page that holds several SVGs has one namespace for ids) while the
|
|
121
|
+
* same chart always gives the same text.
|
|
122
|
+
*/
|
|
123
|
+
idPrefix?: string;
|
|
124
|
+
}
|
|
125
|
+
/** One-shot export: attach a StringPlatform, flush, detach. */
|
|
126
|
+
export declare function toSvgString(chart: Chart, options?: SvgOptions): string;
|