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.
- package/LICENSE +21 -0
- package/README.md +46 -2
- 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 +53 -3
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart furniture: the title (and its subtitle and caption) and the tooltip, which reads a `Cursor`. Annotations, which belong to a plane, are
|
|
3
|
+
* in annotation.ts.
|
|
4
|
+
*/
|
|
5
|
+
import { STRING } from './attrs.js';
|
|
6
|
+
import { ChartElement } from './element.js';
|
|
7
|
+
import { EVENT_ARRANGE, EVENT_ATTACH, EVENT_MEASURE, EVENT_PAINT } from './events.js';
|
|
8
|
+
import { f2 } from './geometry.js';
|
|
9
|
+
import { calloutSize, clearCallout, paintCallout } from './callout.js';
|
|
10
|
+
import { applyText, measureBlock, paintLines } from './text.js';
|
|
11
|
+
/** Which selector holds each role's look. */
|
|
12
|
+
const ROLE_SELECTOR = { title: 'chartTitle', subtitle: 'chartSubtitle', caption: 'chartCaption' };
|
|
13
|
+
/** Room above and below a line of text, so bands do not touch what they sit against. */
|
|
14
|
+
const BAND_PADDING = 4;
|
|
15
|
+
/**
|
|
16
|
+
* Text across the top or bottom of the chart. One element serves every role: a title, a subtitle, a caption or a
|
|
17
|
+
* footnote differ only in their `role` (and so their look) and `side`. Several on one edge stack outwards from it in the
|
|
18
|
+
* order they were added, so a title followed by a subtitle puts the subtitle underneath.
|
|
19
|
+
*
|
|
20
|
+
* Where the text sits along its band is the style property `align` (`style: { chartTitle: { align: 'middle' } }`).
|
|
21
|
+
*/
|
|
22
|
+
export class ChartTitle extends ChartElement {
|
|
23
|
+
static attrTypes = { ...ChartElement.attrTypes, text: STRING };
|
|
24
|
+
#rect = { x: 0, y: 0, width: 0, height: 0 };
|
|
25
|
+
#lines = 0;
|
|
26
|
+
get role() { return this.attrs.role ?? 'title'; }
|
|
27
|
+
get side() { return this.attrs.side ?? (this.role === 'caption' ? 'bottom' : 'top'); }
|
|
28
|
+
get look() { return this.styleFor([ROLE_SELECTOR[this.role], 'text']); }
|
|
29
|
+
onEvent(ev) {
|
|
30
|
+
switch (ev.type) {
|
|
31
|
+
case EVENT_ATTACH:
|
|
32
|
+
this.surface.wrapper();
|
|
33
|
+
break;
|
|
34
|
+
case EVENT_MEASURE: {
|
|
35
|
+
const size = measureBlock(this.requireParent().measureText, this.attrs.text, this.look);
|
|
36
|
+
const request = { width: size.width, height: size.height + BAND_PADDING };
|
|
37
|
+
ev.request = { min: request, max: request, side: this.side, band: true };
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case EVENT_ARRANGE:
|
|
41
|
+
this.#rect = ev.rect;
|
|
42
|
+
break;
|
|
43
|
+
case EVENT_PAINT: {
|
|
44
|
+
const s = this.surface, r = this.#rect, look = this.look;
|
|
45
|
+
s.set(s.wrapper(), 'transform', `translate(${f2(r.x)} ${f2(r.y)})`);
|
|
46
|
+
applyText(s, s.wrapper(), look);
|
|
47
|
+
// The anchor is the text's own: start, middle or end of the band.
|
|
48
|
+
const x = look.align === 'start' ? 0 : look.align === 'middle' ? r.width / 2 : r.width;
|
|
49
|
+
this.#lines = paintLines(s, 'line-', this.attrs.text, x, BAND_PADDING / 2, look, this.#lines);
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** The gap between the pointer or point and the box of a tooltip that follows. */
|
|
56
|
+
const FOLLOW_OFFSET = 14;
|
|
57
|
+
/** Which way the box sits from what it points at, in the order tried: up and right, then the other corners. */
|
|
58
|
+
const CORNERS = [[1, -1], [-1, -1], [1, 1], [-1, 1]];
|
|
59
|
+
/** The default words for one hit. */
|
|
60
|
+
function describe(hit) {
|
|
61
|
+
const { series } = hit, lines = [];
|
|
62
|
+
const name = series.attrs.name;
|
|
63
|
+
if (name)
|
|
64
|
+
lines.push(name);
|
|
65
|
+
const part = (axis, value) => `${axis.attrs.title ? axis.attrs.title + ': ' : ''}${axis.formatValue(value)}`;
|
|
66
|
+
lines.push(part(series.firstAxis, hit.first), part(series.secondAxis, hit.second));
|
|
67
|
+
for (const [channel, value] of Object.entries(hit.channels))
|
|
68
|
+
lines.push(`${channel}: ${String(+value.toPrecision(5))}`);
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Shows what the cursor reads, in a box styled like a callout. It is a child of a `Cursor`, which finds the reading (the
|
|
73
|
+
* nearest point, or every series at the cursor's place) from the pointer or the keyboard; this only draws it, above whatever
|
|
74
|
+
* comes before it in the cursor, so put a `Crosshair` first to have the lines under the box. It takes no room of its own.
|
|
75
|
+
*
|
|
76
|
+
* It looks like a callout (`tooltipBox`, `tooltipText` and `tooltip` metrics, each falling back to the callout's own), so a
|
|
77
|
+
* theme that styles callouts styles tooltips too. Touch has no hover, so it shows while a finger is down. It hides while
|
|
78
|
+
* a button is held, which is when something is being dragged.
|
|
79
|
+
*/
|
|
80
|
+
export class ChartTooltip extends ChartElement {
|
|
81
|
+
#lines = 0;
|
|
82
|
+
#announced = '';
|
|
83
|
+
#samples = new Map();
|
|
84
|
+
/** Moves with the cursor when it has no arm. */
|
|
85
|
+
get follows() { return this.#follow(this.requireParent().attrs.mode === 'shared'); }
|
|
86
|
+
#follow(shared) { return this.attrs.follow ?? shared; }
|
|
87
|
+
onEvent(ev) {
|
|
88
|
+
switch (ev.type) {
|
|
89
|
+
case EVENT_ATTACH:
|
|
90
|
+
this.surface.wrapper();
|
|
91
|
+
break;
|
|
92
|
+
case EVENT_PAINT:
|
|
93
|
+
this.paint();
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
look() {
|
|
98
|
+
return {
|
|
99
|
+
metrics: this.styleFor(['tooltip', 'callout']), text: this.styleFor(['tooltipText', 'calloutText', 'text']),
|
|
100
|
+
box: this.styleFor(['tooltipBox', 'calloutBox', 'shape']), arm: this.styleFor(['calloutArm', 'line']),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
paint() {
|
|
104
|
+
const s = this.surface, cursor = this.requireParent(), state = cursor.state, a = this.attrs, chart = cursor.requireParent();
|
|
105
|
+
s.wrapper();
|
|
106
|
+
if (!state || state.hits.length === 0) {
|
|
107
|
+
clearCallout(s, this.#lines, this.#samples);
|
|
108
|
+
this.#lines = 0;
|
|
109
|
+
this.#announced = '';
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const { plane, hits } = state, shared = cursor.attrs.mode === 'shared', follow = this.#follow(shared);
|
|
113
|
+
// 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
|
|
114
|
+
const look = this.look(), r = plane.rect, first = hits[0];
|
|
115
|
+
const format = a.format ?? describe;
|
|
116
|
+
// The words in blocks: a heading in shared mode, then one block for each hit. A hit's colour dots its block's first line.
|
|
117
|
+
const blocks = [];
|
|
118
|
+
if (shared && !a.format) {
|
|
119
|
+
// The heading says where the reading is: the point the series are read at, or, reading the line between points, the cursor's own place.
|
|
120
|
+
const at = cursor.attrs.interpolate === true ? plane.unplace(state.x, state.y, first.series.firstAxis, first.series.secondAxis)[0] : first.first;
|
|
121
|
+
blocks.push({ text: first.series.firstAxis.formatValue(at), series: null });
|
|
122
|
+
for (const h of hits) {
|
|
123
|
+
const name = h.series.attrs.name;
|
|
124
|
+
blocks.push({ text: `${name ? name + ': ' : ''}${h.series.secondAxis.formatValue(h.second)}`, series: h.series });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
for (const h of hits)
|
|
129
|
+
blocks.push({ text: format(h), series: h.series });
|
|
130
|
+
}
|
|
131
|
+
const text = blocks.map((b) => b.text).join('\n');
|
|
132
|
+
const swatches = a.swatch ? blocks.flatMap((b) => b.text.split('\n').map((_, i) => (i === 0 ? b.series : null))) : undefined;
|
|
133
|
+
const { width: w, height: h } = calloutSize(chart.measureText, text, look, !!swatches);
|
|
134
|
+
// What the box is set beside, in chart pixels: the point, or the cursor; and how far from it.
|
|
135
|
+
const point = [r.x + first.px, r.y + first.py];
|
|
136
|
+
const near = follow ? [r.x + state.x, r.y + state.y] : point;
|
|
137
|
+
const gap = follow ? FOLLOW_OFFSET : look.metrics.offset;
|
|
138
|
+
const size = chart.platform?.size() ?? { width: Infinity, height: Infinity };
|
|
139
|
+
const W = chart.attrs.width ?? size.width, H = chart.attrs.height ?? size.height;
|
|
140
|
+
let box = null;
|
|
141
|
+
for (const [ux, uy] of CORNERS) {
|
|
142
|
+
const cx = near[0] + ux * (w / 2 + gap * 0.6), cy = near[1] + uy * (h / 2 + gap * 0.6);
|
|
143
|
+
const candidate = { x: cx - w / 2, y: cy - h / 2, width: w, height: h };
|
|
144
|
+
if (candidate.x >= 0 && candidate.y >= 0 && candidate.x + w <= W && candidate.y + h <= H) {
|
|
145
|
+
box = candidate;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
// Nowhere fits whole (a small chart): the first corner, pushed back inside as far as it will go.
|
|
150
|
+
box ??= {
|
|
151
|
+
x: Math.max(0, Math.min(near[0] + gap * 0.6, W - w)), y: Math.max(0, Math.min(near[1] - h - gap * 0.6, H - h)), width: w, height: h,
|
|
152
|
+
};
|
|
153
|
+
this.#lines = paintCallout(s, text, box, follow ? null : point, look, this.#lines, swatches, this.#samples);
|
|
154
|
+
// Someone at the keyboard cannot see the box: say what it says, once for each thing it says.
|
|
155
|
+
if (state.source === 'keyboard' && a.announce !== false) {
|
|
156
|
+
const said = text.split('\n').join('. ');
|
|
157
|
+
if (said !== this.#announced) {
|
|
158
|
+
this.#announced = said;
|
|
159
|
+
chart.input?.speak(said);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.#announced = '';
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Rectangles, sizes and extents: the plain-data vocabulary shared by layout, hit-testing and drawing. */
|
|
2
|
+
export interface Size {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Rect {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
}
|
|
12
|
+
export interface Insets {
|
|
13
|
+
top: number;
|
|
14
|
+
right: number;
|
|
15
|
+
bottom: number;
|
|
16
|
+
left: number;
|
|
17
|
+
}
|
|
18
|
+
export interface Extent {
|
|
19
|
+
min: number;
|
|
20
|
+
max: number;
|
|
21
|
+
}
|
|
22
|
+
/** Half-open on the far edges, so two touching rectangles never both claim the same point. */
|
|
23
|
+
export declare const contains: (r: Rect, x: number, y: number) => boolean;
|
|
24
|
+
export type Side = 'top' | 'right' | 'bottom' | 'left';
|
|
25
|
+
/** What an element answers when asked how much room it wants (the MEASURE event): a range, so the parent can squeeze or grow it. */
|
|
26
|
+
export interface LayoutRequest {
|
|
27
|
+
min: Size;
|
|
28
|
+
max: Size;
|
|
29
|
+
/** For chart furniture: the edge of the chart it wants to sit against, taking its size from the plots. Others leave it out. */
|
|
30
|
+
side?: Side;
|
|
31
|
+
/**
|
|
32
|
+
* With `side`: spans the whole edge and stacks outwards from it with other bands (a title, a caption), instead of
|
|
33
|
+
* sharing a strip with the other furniture on that side (keys). Only the top and bottom edges take bands.
|
|
34
|
+
*/
|
|
35
|
+
band?: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** A circle as path data (SVG has no circle path command, so two half arcs), centred on (cx, cy). */
|
|
38
|
+
export declare const circlePath: (cx: number, cy: number, r: number) => string;
|
|
39
|
+
/** A number for path data and attributes: two decimals at most. */
|
|
40
|
+
export declare const f2: (n: number) => string;
|
package/dist/geometry.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Rectangles, sizes and extents: the plain-data vocabulary shared by layout, hit-testing and drawing. */
|
|
2
|
+
/** Half-open on the far edges, so two touching rectangles never both claim the same point. */
|
|
3
|
+
export const contains = (r, x, y) => x >= r.x && x < r.x + r.width && y >= r.y && y < r.y + r.height;
|
|
4
|
+
/** A circle as path data (SVG has no circle path command, so two half arcs), centred on (cx, cy). */
|
|
5
|
+
export const circlePath = (cx, cy, r) => `M${f2(cx - r)} ${f2(cy)}a${f2(r)} ${f2(r)} 0 1 0 ${f2(2 * r)} 0a${f2(r)} ${f2(r)} 0 1 0 ${f2(-2 * r)} 0`;
|
|
6
|
+
/** A number for path data and attributes: two decimals at most. */
|
|
7
|
+
export const f2 = (n) => String(Math.round(n * 100) / 100);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { HitMode } from './hit.ts';
|
|
2
|
+
import { type PolygonSet, type ResolvedBinding, SeriesRenderer, type SeriesRendererAttributes, type Spread, type SpreadView } from './renderer.ts';
|
|
3
|
+
import { type ChartEvent } from './events.ts';
|
|
4
|
+
import type { Series } from './series.ts';
|
|
5
|
+
import type { PlatformNode } from './platform.ts';
|
|
6
|
+
import type { Surface } from './surface.ts';
|
|
7
|
+
export interface HeatMapBindable {
|
|
8
|
+
fill: string;
|
|
9
|
+
opacity: number;
|
|
10
|
+
}
|
|
11
|
+
export interface HeatMapAttributes extends SeriesRendererAttributes<HeatMapBindable> {
|
|
12
|
+
/** The size of a cell along the first axis, in data units. Default: the `widthChannel`. */
|
|
13
|
+
cellWidth?: number;
|
|
14
|
+
/** The size along the second axis. Default: the `heightChannel`, or `cellWidth` when only that is given (square cells). */
|
|
15
|
+
cellHeight?: number;
|
|
16
|
+
/** The channel with each cell's width. Default 'width'. Not read when `cellWidth` is set. */
|
|
17
|
+
widthChannel?: string;
|
|
18
|
+
/** The channel with each cell's height. Default 'height'. Not read when a cell size attribute is set. */
|
|
19
|
+
heightChannel?: string;
|
|
20
|
+
}
|
|
21
|
+
/** A grid of rectangles, one path for each colour step. */
|
|
22
|
+
export declare class HeatMap extends SeriesRenderer<HeatMapAttributes> {
|
|
23
|
+
#private;
|
|
24
|
+
static attrTypes: {
|
|
25
|
+
fill: 1;
|
|
26
|
+
maxNodes: 2;
|
|
27
|
+
stroke: 1;
|
|
28
|
+
strokeWidth: 2;
|
|
29
|
+
id: 1;
|
|
30
|
+
meta: {
|
|
31
|
+
title: 1;
|
|
32
|
+
description: 1;
|
|
33
|
+
role: 1;
|
|
34
|
+
};
|
|
35
|
+
cellWidth: 2;
|
|
36
|
+
cellHeight: 2;
|
|
37
|
+
widthChannel: 1;
|
|
38
|
+
heightChannel: 1;
|
|
39
|
+
};
|
|
40
|
+
/** Which channels give the cell size; null for a dimension that has a constant. */
|
|
41
|
+
private get sizeChannels();
|
|
42
|
+
get channelNames(): readonly string[];
|
|
43
|
+
get bindings(): readonly ResolvedBinding[];
|
|
44
|
+
get stepsPerBinding(): number;
|
|
45
|
+
/** The extents the cells reach: the axes leave room for the half of a cell that sticks out past its centre. */
|
|
46
|
+
spread(view: SpreadView): Spread | null;
|
|
47
|
+
limit(series: Series): Spread | null;
|
|
48
|
+
get hitMode(): HitMode;
|
|
49
|
+
onEvent(ev: ChartEvent): void;
|
|
50
|
+
private look;
|
|
51
|
+
get swatch(): string | null;
|
|
52
|
+
sample(s: Surface, parent: PlatformNode, key: string, width: number, height: number): void;
|
|
53
|
+
/** The four corners of every cell in plot pixels (clockwise from the low corner), NaN for a gap; made again only after a new projection. */
|
|
54
|
+
private geometry;
|
|
55
|
+
shapes(): PolygonSet | null;
|
|
56
|
+
protected draw(): void;
|
|
57
|
+
}
|
package/dist/heatmap.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heat maps: a rectangle for each point, coloured by a channel.
|
|
3
|
+
*
|
|
4
|
+
* <XYSeries>
|
|
5
|
+
* <Density width={[5, 5]} by={['domain', 'value']}><ObjectDataSource source={rows} domain="x" value="y" /></Density>
|
|
6
|
+
* <HeatMap bind={{ fill: { axis: heat, channel: 'count' } }} />
|
|
7
|
+
* </XYSeries>
|
|
8
|
+
*
|
|
9
|
+
* Each point is the centre of a cell. The cell's size is `cellWidth` and `cellHeight` in data units, or else the
|
|
10
|
+
* channels `width` and `height` (which is what `Density` gives each bin, so the pair work together); one of the two
|
|
11
|
+
* attributes alone makes a square. A cell is drawn from its four corners, each placed by the plane: on a rectangle
|
|
12
|
+
* that is a rectangle, on a polar plane a wedge with straight edges. Cells that look alike (the same step of the bound
|
|
13
|
+
* colour) share a path, so the nodes drawn are bounded by `maxNodes` however many cells there are. A gap in a coordinate
|
|
14
|
+
* or a size leaves out its cell. Cells are hit as shapes.
|
|
15
|
+
*
|
|
16
|
+
* Bind `fill` (a `ColorAxis` over a channel) to colour by data, and `opacity` to fade by it. Without a binding every cell
|
|
17
|
+
* takes the series' colour.
|
|
18
|
+
*/
|
|
19
|
+
/* 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 */
|
|
20
|
+
import { NUMBER, STRING } from './attrs.js';
|
|
21
|
+
import { fractionsOf, lookKey, lookValues, resolveBindings, stepsFor } from './bucket.js';
|
|
22
|
+
import { f2 } from './geometry.js';
|
|
23
|
+
import { SeriesRenderer, paintBox } from './renderer.js';
|
|
24
|
+
import { EVENT_ATTACH } from './events.js';
|
|
25
|
+
/** How far the normalised position may stray outside the axis before it is held there. */
|
|
26
|
+
const REACH = 2;
|
|
27
|
+
const clamp = (v) => (v < -REACH ? -REACH : v > 1 + REACH ? 1 + REACH : v);
|
|
28
|
+
/** A grid of rectangles, one path for each colour step. */
|
|
29
|
+
export class HeatMap extends SeriesRenderer {
|
|
30
|
+
static attrTypes = { ...SeriesRenderer.attrTypes, cellWidth: NUMBER, cellHeight: NUMBER, widthChannel: STRING, heightChannel: STRING };
|
|
31
|
+
#keys = new Set();
|
|
32
|
+
#geometry = null;
|
|
33
|
+
/** Which channels give the cell size; null for a dimension that has a constant. */
|
|
34
|
+
get sizeChannels() {
|
|
35
|
+
const a = this.attrs;
|
|
36
|
+
const w = a.cellWidth === undefined ? a.widthChannel ?? 'width' : null;
|
|
37
|
+
const h = a.cellHeight !== undefined ? null : a.cellWidth !== undefined ? null : a.heightChannel ?? 'height';
|
|
38
|
+
return { w, h };
|
|
39
|
+
}
|
|
40
|
+
get channelNames() {
|
|
41
|
+
const { w, h } = this.sizeChannels;
|
|
42
|
+
return [...(w ? [w] : []), ...(h ? [h] : [])];
|
|
43
|
+
}
|
|
44
|
+
get bindings() { return resolveBindings(this.attrs.bind, this, () => false); }
|
|
45
|
+
get stepsPerBinding() { return stepsFor(this.bindings, this.attrs.maxNodes ?? 1000); }
|
|
46
|
+
/** The extents the cells reach: the axes leave room for the half of a cell that sticks out past its centre. */
|
|
47
|
+
spread(view) {
|
|
48
|
+
const a = this.attrs, { w: wc, h: hc } = this.sizeChannels;
|
|
49
|
+
const wCol = wc ? view.channelColumn(wc) : -1, hCol = hc ? view.channelColumn(hc) : -1;
|
|
50
|
+
let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
|
|
51
|
+
for (let i = 0; i < view.count; i++) {
|
|
52
|
+
const row = i * view.stride, x = view.raw[row + view.firstColumn], y = view.raw[row + view.secondColumn];
|
|
53
|
+
const w = wCol >= 0 ? view.raw[row + wCol] : a.cellWidth ?? NaN, h = hCol >= 0 ? view.raw[row + hCol] : a.cellHeight ?? a.cellWidth ?? NaN;
|
|
54
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(w) || !Number.isFinite(h))
|
|
55
|
+
continue;
|
|
56
|
+
if (x - w / 2 < x0)
|
|
57
|
+
x0 = x - w / 2;
|
|
58
|
+
if (x + w / 2 > x1)
|
|
59
|
+
x1 = x + w / 2;
|
|
60
|
+
if (y - h / 2 < y0)
|
|
61
|
+
y0 = y - h / 2;
|
|
62
|
+
if (y + h / 2 > y1)
|
|
63
|
+
y1 = y + h / 2;
|
|
64
|
+
}
|
|
65
|
+
return x0 > x1 ? null : { first: { min: x0, max: x1 }, second: { min: y0, max: y1 } };
|
|
66
|
+
}
|
|
67
|
+
limit(series) {
|
|
68
|
+
const a = this.attrs, { w: wc, h: hc } = this.sizeChannels;
|
|
69
|
+
const first = series.ownExtent('first'), second = series.ownExtent('second');
|
|
70
|
+
const most = (name, fixed) => {
|
|
71
|
+
if (!name)
|
|
72
|
+
return fixed ?? NaN;
|
|
73
|
+
const e = series.data.channelExtent(name);
|
|
74
|
+
return e ? Math.max(Math.abs(e.min), Math.abs(e.max)) : NaN;
|
|
75
|
+
};
|
|
76
|
+
const w = most(wc, a.cellWidth), h = a.cellHeight === undefined && !hc ? w : most(hc, a.cellHeight);
|
|
77
|
+
if (!first || !second || !Number.isFinite(w) || !Number.isFinite(h))
|
|
78
|
+
return null;
|
|
79
|
+
return { first: { min: first.min - w / 2, max: first.max + w / 2 }, second: { min: second.min - h / 2, max: second.max + h / 2 } };
|
|
80
|
+
}
|
|
81
|
+
get hitMode() { return 'shape'; }
|
|
82
|
+
onEvent(ev) {
|
|
83
|
+
if (ev.type === EVENT_ATTACH)
|
|
84
|
+
void this.stepsPerBinding;
|
|
85
|
+
super.onEvent(ev);
|
|
86
|
+
}
|
|
87
|
+
look() {
|
|
88
|
+
const a = this.attrs;
|
|
89
|
+
return this.styleFor(['heatCell', 'shape'], { fill: a.fill, stroke: a.stroke, strokeWidth: a.strokeWidth, opacity: a.opacity }, { fill: this.requireParent().accentColor });
|
|
90
|
+
}
|
|
91
|
+
get swatch() {
|
|
92
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- the `bind` attribute, typed here as the shape this element reads
|
|
93
|
+
const bind = this.attrs.bind;
|
|
94
|
+
return bind?.fill ? null : this.look().fill;
|
|
95
|
+
}
|
|
96
|
+
sample(s, parent, key, width, height) {
|
|
97
|
+
const look = { ...this.look() };
|
|
98
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- bound properties are written by name
|
|
99
|
+
for (const b of this.bindings)
|
|
100
|
+
look[b.property] = b.axis.valueAt(0.5);
|
|
101
|
+
paintBox(s, parent, key, width, height, look);
|
|
102
|
+
}
|
|
103
|
+
/** The four corners of every cell in plot pixels (clockwise from the low corner), NaN for a gap; made again only after a new projection. */
|
|
104
|
+
geometry() {
|
|
105
|
+
const series = this.requireParent(), d = series.drawn, plane = d?.plane;
|
|
106
|
+
if (!d || !plane)
|
|
107
|
+
return null;
|
|
108
|
+
if (this.#geometry && this.#geometry.projection === series.projection)
|
|
109
|
+
return this.#geometry;
|
|
110
|
+
const n = d.points.count, rs = d.stride, base = rs - d.channels.length, a = this.attrs;
|
|
111
|
+
const { w: wc, h: hc } = this.sizeChannels;
|
|
112
|
+
const colOf = (name) => base + d.channels.indexOf(name);
|
|
113
|
+
const wCol = wc ? colOf(wc) : -1, hCol = hc ? colOf(hc) : -1;
|
|
114
|
+
const xs = new Float64Array(n * 2), ys = new Float64Array(n * 2);
|
|
115
|
+
for (let i = 0; i < n; i++) {
|
|
116
|
+
const x = d.raw[i * rs + d.firstColumn], y = d.raw[i * rs + d.secondColumn];
|
|
117
|
+
const w = wCol >= 0 ? d.raw[i * rs + wCol] : a.cellWidth ?? NaN;
|
|
118
|
+
const h = hCol >= 0 ? d.raw[i * rs + hCol] : a.cellHeight ?? a.cellWidth ?? NaN;
|
|
119
|
+
xs[2 * i] = x - w / 2;
|
|
120
|
+
xs[2 * i + 1] = x + w / 2;
|
|
121
|
+
ys[2 * i] = y - h / 2;
|
|
122
|
+
ys[2 * i + 1] = y + h / 2;
|
|
123
|
+
}
|
|
124
|
+
series.firstAxis.normalize(xs, n * 2, 1, 0);
|
|
125
|
+
series.secondAxis.normalize(ys, n * 2, 1, 0);
|
|
126
|
+
const out = new Float64Array(n * 8);
|
|
127
|
+
for (let i = 0; i < n; i++) {
|
|
128
|
+
const x0 = xs[2 * i], x1 = xs[2 * i + 1], y0 = ys[2 * i], y1 = ys[2 * i + 1];
|
|
129
|
+
const bad = !Number.isFinite(x0) || !Number.isFinite(x1) || !Number.isFinite(y0) || !Number.isFinite(y1);
|
|
130
|
+
const k = i * 8;
|
|
131
|
+
const put = (j, x, y) => { out[k + 2 * j] = bad ? NaN : clamp(x); out[k + 2 * j + 1] = bad ? NaN : clamp(y); };
|
|
132
|
+
put(0, x0, y0);
|
|
133
|
+
put(1, x1, y0);
|
|
134
|
+
put(2, x1, y1);
|
|
135
|
+
put(3, x0, y1);
|
|
136
|
+
}
|
|
137
|
+
plane.place({ data: out, stride: 2, count: n * 4 }, series.firstAxis, series.secondAxis);
|
|
138
|
+
return (this.#geometry = { projection: series.projection, n, data: out });
|
|
139
|
+
}
|
|
140
|
+
shapes() {
|
|
141
|
+
const g = this.geometry();
|
|
142
|
+
return g ? { data: g.data, perShape: 4, count: g.n } : null;
|
|
143
|
+
}
|
|
144
|
+
draw() {
|
|
145
|
+
const series = this.requireParent(), plane = series.plane, s = this.surface, counters = this.counters;
|
|
146
|
+
const g = this.geometry(), d = series.drawn;
|
|
147
|
+
const look = this.look();
|
|
148
|
+
const t0 = performance.now();
|
|
149
|
+
const w = plane.rect.width, h = plane.rect.height;
|
|
150
|
+
const bindings = this.bindings, steps = this.stepsPerBinding;
|
|
151
|
+
const styled = g && d ? bindings.map((b) => ({ b, f: fractionsOf(b, d.raw, d.stride - d.channels.length + d.channels.indexOf(b.channel), d.stride, g.n) })) : [];
|
|
152
|
+
const paths = new Map();
|
|
153
|
+
let bytes = 0;
|
|
154
|
+
if (g) {
|
|
155
|
+
const p = g.data;
|
|
156
|
+
for (let i = 0; i < g.n; i++) {
|
|
157
|
+
const k = i * 8;
|
|
158
|
+
let lo = Infinity, hi = -Infinity, left = Infinity, right = -Infinity, ok = true;
|
|
159
|
+
for (let j = 0; j < 4; j++) {
|
|
160
|
+
const x = p[k + 2 * j], y = p[k + 2 * j + 1];
|
|
161
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
162
|
+
ok = false;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
if (x < left)
|
|
166
|
+
left = x;
|
|
167
|
+
if (x > right)
|
|
168
|
+
right = x;
|
|
169
|
+
if (y < lo)
|
|
170
|
+
lo = y;
|
|
171
|
+
if (y > hi)
|
|
172
|
+
hi = y;
|
|
173
|
+
}
|
|
174
|
+
if (!ok || right < 0 || left > w || hi < 0 || lo > h)
|
|
175
|
+
continue;
|
|
176
|
+
const c = (j) => `${f2(p[k + 2 * j])} ${f2(p[k + 2 * j + 1])}`;
|
|
177
|
+
const path = `M${c(0)}L${c(1)}L${c(2)}L${c(3)}Z`;
|
|
178
|
+
bytes += path.length;
|
|
179
|
+
const key = steps ? lookKey(styled, i, steps) : 0;
|
|
180
|
+
paths.set(key, (paths.get(key) ?? '') + path);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
counters.stages.path += performance.now() - t0;
|
|
184
|
+
counters.pathBytes += bytes;
|
|
185
|
+
for (const key of this.#keys)
|
|
186
|
+
if (!paths.has(key))
|
|
187
|
+
s.remove(`c${key}`);
|
|
188
|
+
this.#keys = new Set(paths.keys());
|
|
189
|
+
const v = { fill: look.fill, opacity: look.opacity };
|
|
190
|
+
for (const [key, path] of paths) {
|
|
191
|
+
v.fill = look.fill;
|
|
192
|
+
v.opacity = look.opacity;
|
|
193
|
+
if (steps)
|
|
194
|
+
lookValues(key, styled, steps, (property, value) => { v[property] = value; });
|
|
195
|
+
const node = s.node(`c${key}`, 'path');
|
|
196
|
+
s.set(node, 'd', path);
|
|
197
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- the style channels are filled for every cell before drawing
|
|
198
|
+
s.set(node, 'fill', v.fill);
|
|
199
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- the style channels are filled for every cell before drawing
|
|
200
|
+
s.set(node, 'fill-opacity', Math.round(v.opacity * 1000) / 1000);
|
|
201
|
+
s.set(node, 'stroke', look.stroke);
|
|
202
|
+
s.set(node, 'stroke-width', look.strokeWidth);
|
|
203
|
+
if (!plane.curved)
|
|
204
|
+
s.set(node, 'shape-rendering', 'crispEdges');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
package/dist/hit.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hit testing: which drawn point a pointer is near. It is all free-standing functions that read a series back through
|
|
3
|
+
* `Series.drawn`, and nothing in the core imports it, so a chart that does not hit-test (no tooltip, no listener) does
|
|
4
|
+
* not carry any of it. The geometry at the top works on plain buffers of placed points and needs no chart.
|
|
5
|
+
*
|
|
6
|
+
* What "near" means is the series' choice (its `hit` attribute), because only it knows what its renderers draw:
|
|
7
|
+
* - `point`: the distance to the nearest point. Right for markers and for a line that is only read at its points.
|
|
8
|
+
* - `line`: the distance to the nearest point *on the line* joining them, so the pointer can be anywhere along a segment.
|
|
9
|
+
* - `area`: zero anywhere inside the shape between the line and its baseline, else as `line`.
|
|
10
|
+
* - `shape`: zero anywhere inside a shape the series' renderer drew for the point (a bar), else the distance to its edge.
|
|
11
|
+
* Bars say this by themselves, so a bar series needs no `hit` setting.
|
|
12
|
+
* In every mode the hit is one of the points, never a place between two: the one nearest to where the pointer is.
|
|
13
|
+
*
|
|
14
|
+
* Steps and curves are measured as they are drawn: their renderers `trace` the path they draw, and 'line' and 'area' follow
|
|
15
|
+
* it (a point is still what is reported: the one whose level or stretch of curve the pointer is on). A renderer with no
|
|
16
|
+
* trace is drawn with straight lines through its points, which is what is measured.
|
|
17
|
+
*
|
|
18
|
+
* Either coordinate of the pointer may be NaN, which says "ignore this one": the distance is then measured along the
|
|
19
|
+
* other alone. That is how a crosshair finds the point at the pointer's x whatever its height, and with one coordinate
|
|
20
|
+
* missing a line or an area can no longer be between anything, so every mode measures to the nearest point. Note that
|
|
21
|
+
* these are plot pixels, not data axes: on a Cartesian plane x is the horizontal axis, on a polar plane it is just across.
|
|
22
|
+
*/
|
|
23
|
+
import type { PointBuffer } from './data.ts';
|
|
24
|
+
import type { Hit } from './events.ts';
|
|
25
|
+
import type { Plane } from './plane.ts';
|
|
26
|
+
import type { PolygonSet, Trace } from './renderer.ts';
|
|
27
|
+
import type { Series } from './series.ts';
|
|
28
|
+
export type HitMode = 'point' | 'line' | 'area' | 'shape';
|
|
29
|
+
/** How a series takes pointer hits: set as its `hit` attribute. */
|
|
30
|
+
export interface HitOptions {
|
|
31
|
+
/** What distance means: to the nearest point, to the line through them, or zero inside the area or shape. Default what the renderer says (bars: 'shape'), else 'point'. */
|
|
32
|
+
mode?: HitMode;
|
|
33
|
+
/** The farthest a pointer may be, in pixels, and still hit. Default 10. */
|
|
34
|
+
radius?: number;
|
|
35
|
+
/** For 'area': the value the shape is closed against, like the renderer's own baseline. Default 0. */
|
|
36
|
+
baseline?: number;
|
|
37
|
+
}
|
|
38
|
+
/** The nearest point, and how far the pointer is from what the mode measures. */
|
|
39
|
+
export interface Nearest {
|
|
40
|
+
readonly index: number;
|
|
41
|
+
readonly distance: number;
|
|
42
|
+
}
|
|
43
|
+
/** The nearest drawn point; points that are gaps (NaN) are skipped. Null when there is none, or both coordinates are NaN. */
|
|
44
|
+
export declare function nearestPoint(points: PointBuffer, x: number, y: number): Nearest | null;
|
|
45
|
+
/** The distance to the nearest point of the polyline through the points (a gap breaks it), and the nearer end of that segment. */
|
|
46
|
+
export declare function nearestSegment(points: PointBuffer, x: number, y: number): Nearest | null;
|
|
47
|
+
/**
|
|
48
|
+
* Whether (x, y) is inside the shape that a run of points makes with its baseline (the same points, placed against the
|
|
49
|
+
* baseline value, which `base` holds one row each for). Runs are the stretches between gaps, each its own shape. The
|
|
50
|
+
* nearest point of that run is returned when it is, else null.
|
|
51
|
+
*/
|
|
52
|
+
export declare function insideArea(points: PointBuffer, base: PointBuffer, x: number, y: number): number | null;
|
|
53
|
+
/**
|
|
54
|
+
* The shape the pointer is inside (distance 0), else the one whose outline is nearest, by the shape's index. Shapes
|
|
55
|
+
* made of NaN are left out. A NaN coordinate of the pointer means "ignore this direction", and the shapes' own points
|
|
56
|
+
* then stand for them, as for any other mode.
|
|
57
|
+
*/
|
|
58
|
+
export declare function nearestShape(shapes: PolygonSet, points: PointBuffer, x: number, y: number): Nearest | null;
|
|
59
|
+
/**
|
|
60
|
+
* Whether (x, y) is inside a shape a trace draws: each run of its line closed back along the same run of its baseline.
|
|
61
|
+
* The point it belongs to is the one owning the nearest vertex of the line in that run, else null.
|
|
62
|
+
*/
|
|
63
|
+
export declare function insideTrace(trace: Trace, x: number, y: number): number | null;
|
|
64
|
+
/** The nearest point under `mode` ('line' or 'area') to a path a renderer traces, by the point each vertex belongs to. */
|
|
65
|
+
export declare function nearestTraced(trace: Trace, mode: HitMode, x: number, y: number): Nearest | null;
|
|
66
|
+
/** The nearest point under `mode`; `base` is the baseline for an area (ignored otherwise). */
|
|
67
|
+
export declare function nearest(points: PointBuffer, mode: HitMode, base: (() => PointBuffer) | null, x: number, y: number): Nearest | null;
|
|
68
|
+
/** The hit for drawn point `index` of a series, with the pointer `distance` away. */
|
|
69
|
+
export declare function pointHit(series: Series, index: number, distance?: number): Hit | null;
|
|
70
|
+
/**
|
|
71
|
+
* The nearest drawn point of a series to a position in plot pixels, within `maxDistance` (default: its `hit` radius), as
|
|
72
|
+
* its `hit` mode measures it. A NaN for `x` or `y` leaves that direction out, so `hitTest(series, x, NaN, Infinity)` is
|
|
73
|
+
* the point nearest in x whatever its height. Null if nothing is near enough, or the series takes no hits.
|
|
74
|
+
*/
|
|
75
|
+
export declare function hitTest(series: Series, x: number, y: number, maxDistance?: number): Hit | null;
|
|
76
|
+
/**
|
|
77
|
+
* The nearest point of any series in the plane that takes hits, within each series' own radius, by that series' `hit`
|
|
78
|
+
* mode. A tie goes to the series drawn last, which is the one on top.
|
|
79
|
+
*/
|
|
80
|
+
export declare function hitPlane(plane: Plane, x: number, y: number): Hit | null;
|
|
81
|
+
/**
|
|
82
|
+
* The nearest point of every series that takes hits, topmost first. `radius` replaces each series' own, and is unlimited
|
|
83
|
+
* by default, so `hitAll(plane, x, NaN)` is each series at the pointer's x: what a crosshair tooltip lists.
|
|
84
|
+
*/
|
|
85
|
+
export declare function hitAll(plane: Plane, x: number, y: number, radius?: number): Hit[];
|
|
86
|
+
/**
|
|
87
|
+
* The point of every series that is nearest the pointer *along the first axis*, whatever its distance across: what a
|
|
88
|
+
* reading shared by all series, such as a crosshair tooltip, lists. It is measured in the axis' own terms and not in
|
|
89
|
+
* pixels, so it is right wherever the plane puts that axis: across a Cartesian plane, up one whose domain is vertical, and
|
|
90
|
+
* round a radial plane, where the nearest point is the one nearest in angle (and a full turn wraps, so a pointer just
|
|
91
|
+
* before the start of the sweep finds the last points). Topmost series first. Series that take no hits are left out.
|
|
92
|
+
*/
|
|
93
|
+
export declare function hitAcross(plane: Plane, x: number, y: number): Hit[];
|
|
94
|
+
/**
|
|
95
|
+
* Where a series' line is at position `first` of its first axis, read as a straight line between the two drawn points
|
|
96
|
+
* either side of it, in data terms (so it is exact for a `LinePath` on a linear scale, and may sit off a curve or a line
|
|
97
|
+
* drawn another way: see the tooltip's `interpolate`). Only a domain series has one; others, a series that takes no
|
|
98
|
+
* hits, a position outside the data and one beside a gap give null. The hit is `interpolated`: its `index` and `row`
|
|
99
|
+
* are the nearer of the two points, and `px`, `py` are where the line is.
|
|
100
|
+
*/
|
|
101
|
+
export declare function interpolate(series: Series, first: number): Hit | null;
|
|
102
|
+
/** Every series' line read at the pointer, topmost first: `interpolate` at the pointer's place along each series' first axis. */
|
|
103
|
+
export declare function interpolateAll(plane: Plane, x: number, y: number): Hit[];
|