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/panzoom.js
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/** Pan and zoom: pointer input turned into a change of view on a plane's axes. */
|
|
2
|
+
/* 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 */
|
|
3
|
+
import { AUTO, NUMBER, BOOLEAN } from './attrs.js';
|
|
4
|
+
import { writeAria } from './aria.js';
|
|
5
|
+
import { Axis } from './axis.js';
|
|
6
|
+
import { ChartElement, resolveRef } from './element.js';
|
|
7
|
+
import { inputOf } from './input.js';
|
|
8
|
+
import { EVENT_FOCUS, EVENT_KEY, EVENT_PAINT, EVENT_POINTER } from './events.js';
|
|
9
|
+
import { drawFocusRing, makeTabStop } from './focus.js';
|
|
10
|
+
/**
|
|
11
|
+
* The keys it answers to unless told otherwise. Arrows move the view a step; with Shift, or the Page keys, a page. `+` and `-`
|
|
12
|
+
* (with the `=` and `_` that share their keys) zoom about the middle, and `0` or Home puts the view back.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_PANZOOM_KEYS = {
|
|
15
|
+
left: ['ArrowLeft'], right: ['ArrowRight'], up: ['ArrowUp'], down: ['ArrowDown'],
|
|
16
|
+
pageLeft: [], pageRight: [], pageUp: ['PageUp'], pageDown: ['PageDown'],
|
|
17
|
+
zoomIn: ['+', '='], zoomOut: ['-', '_'], reset: ['0', 'Home'],
|
|
18
|
+
};
|
|
19
|
+
/** For each moving action: along a horizontal axis or not, which way along it (+1 towards higher values), and whether it is a page. */
|
|
20
|
+
const MOVES = {
|
|
21
|
+
left: [true, -1, false], right: [true, 1, false], up: [false, 1, false], down: [false, -1, false],
|
|
22
|
+
pageLeft: [true, -1, true], pageRight: [true, 1, true], pageUp: [false, 1, true], pageDown: [false, -1, true],
|
|
23
|
+
};
|
|
24
|
+
/** Fractions of the view moved by an arrow, and by a page. */
|
|
25
|
+
const KEY_STEP = 0.1;
|
|
26
|
+
const KEY_PAGE = 0.5;
|
|
27
|
+
const KEY_ZOOM = 1.25;
|
|
28
|
+
/** Least distance in pixels between two fingers along an axis for the pinch to zoom it; closer than that it only pans along. */
|
|
29
|
+
const PINCH_MIN_SPAN = 24;
|
|
30
|
+
/** Zoom per pixel of a trackpad pinch (a wheel turn with Ctrl held), as a natural-log factor. */
|
|
31
|
+
const PINCH_SPEED = 0.02;
|
|
32
|
+
/** A flick: it counts if the finger moved within `FLICK_REST` ms of letting go, at least `FLICK_MIN` px/ms measured over the last `FLICK_WINDOW` ms; speed falls by 1/e every `FLICK_TAU` ms down to `FLICK_STOP`. */
|
|
33
|
+
const FLICK_REST = 80, FLICK_WINDOW = 100, FLICK_MIN = 0.15, FLICK_TAU = 325, FLICK_STOP = 0.02;
|
|
34
|
+
/**
|
|
35
|
+
* Pan and zoom for the plane it sits in: a plane child that turns pointer input into view ranges on axes.
|
|
36
|
+
* Everything is worked out in the fractions of the scale, so it behaves the same on a linear axis, an inverted one,
|
|
37
|
+
* one on the far side, or one with a different scale: the value under the pointer stays under the pointer.
|
|
38
|
+
*
|
|
39
|
+
* It does not know what a plane looks like. Each axis' `Ruler` says how far along it a plot position is (`fractionAt`),
|
|
40
|
+
* so dragging along a spoke or a rectangle's edge is the same code; an axis whose ruler cannot say (the rim) is left alone.
|
|
41
|
+
*/
|
|
42
|
+
export class PanZoom extends ChartElement {
|
|
43
|
+
static takesInput = inputOf;
|
|
44
|
+
static attrTypes = { ...ChartElement.attrTypes, keyStep: NUMBER, keyZoom: NUMBER, pinchSpeed: NUMBER, inertia: BOOLEAN, axisZoom: BOOLEAN, resetOnDoubleClick: BOOLEAN, zoomSpeed: NUMBER, keys: { pageLeft: AUTO, pageRight: AUTO, pageUp: AUTO, pageDown: AUTO, zoomIn: AUTO, zoomOut: AUTO } };
|
|
45
|
+
// The drag in progress, with each axis' range as it was at the start; and the last press, for double-click detection.
|
|
46
|
+
#drag = null;
|
|
47
|
+
/** A flick in progress: stopped by the next touch, wheel or reset. */
|
|
48
|
+
#coast = null;
|
|
49
|
+
#touches = new Map();
|
|
50
|
+
#pinch = null;
|
|
51
|
+
#lastDown = { time: -Infinity, x: 0, y: 0 };
|
|
52
|
+
/** Put every controlled axis back to the range its data asks for. */
|
|
53
|
+
reset() {
|
|
54
|
+
this.#stopCoast();
|
|
55
|
+
for (const { axis } of this.controlled())
|
|
56
|
+
axis.setViewRange(null);
|
|
57
|
+
}
|
|
58
|
+
/** Marks it for `PointerListener`, which must come before it: it takes presses, so what it claims is not ours. */
|
|
59
|
+
claimsPress = true;
|
|
60
|
+
#focusNode = null;
|
|
61
|
+
#ring = false;
|
|
62
|
+
#lastKeys = '';
|
|
63
|
+
onEvent(ev) {
|
|
64
|
+
switch (ev.type) {
|
|
65
|
+
case EVENT_PAINT:
|
|
66
|
+
this.#paintFocus();
|
|
67
|
+
if (this.attrs.pan !== false || this.attrs.zoom !== false)
|
|
68
|
+
this.chart?.platform?.setTouchMode?.(this.attrs.touch === 'always' ? 'always' : 'two-finger');
|
|
69
|
+
return;
|
|
70
|
+
case EVENT_KEY:
|
|
71
|
+
this.#key(ev);
|
|
72
|
+
return;
|
|
73
|
+
case EVENT_FOCUS:
|
|
74
|
+
if (ev.focused) {
|
|
75
|
+
this.#focusNode = ev.node;
|
|
76
|
+
this.#ring = ev.visible;
|
|
77
|
+
}
|
|
78
|
+
else if (this.#focusNode === ev.node) {
|
|
79
|
+
this.#focusNode = null;
|
|
80
|
+
this.#ring = false;
|
|
81
|
+
}
|
|
82
|
+
this.invalidate();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (ev.type !== EVENT_POINTER)
|
|
86
|
+
return;
|
|
87
|
+
const a = this.attrs, platform = this.chart?.platform;
|
|
88
|
+
// An element ahead of this one (a `PointerListener`) may already have used a press or claimed the cursor.
|
|
89
|
+
if (ev.handled && (ev.kind === 'down' || ev.kind === 'move') && !this.#drag && !this.#pinch)
|
|
90
|
+
return;
|
|
91
|
+
// Beside the plot, over an axis, only when asked: with `axisZoom` off the axes are inert.
|
|
92
|
+
if (ev.outside && a.axisZoom === false && !this.#drag && !this.#pinch)
|
|
93
|
+
return;
|
|
94
|
+
if (ev.pointerType === 'touch' && ev.kind !== 'wheel' && ev.kind !== 'pinch') {
|
|
95
|
+
this.#touch(ev);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
switch (ev.kind) {
|
|
99
|
+
case 'down': {
|
|
100
|
+
if (!(ev.buttons & 1))
|
|
101
|
+
break;
|
|
102
|
+
if (this.#doubleClick(ev))
|
|
103
|
+
break;
|
|
104
|
+
if (a.pan === false)
|
|
105
|
+
break;
|
|
106
|
+
this.#drag = { id: ev.id, x: ev.x, y: ev.y, moves: this.targets(ev.axis) };
|
|
107
|
+
platform?.setCursor?.('grabbing');
|
|
108
|
+
ev.handled = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
case 'move': {
|
|
112
|
+
const drag = this.#drag;
|
|
113
|
+
if (!drag) {
|
|
114
|
+
if (a.pan !== false)
|
|
115
|
+
platform?.setCursor?.('grab');
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
if (!(ev.buttons & 1)) {
|
|
119
|
+
this.#drag = null;
|
|
120
|
+
platform?.setCursor?.('grab');
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
for (const { axis, at, snap } of drag.moves)
|
|
124
|
+
this.apply(axis, { snap, scale: 1, anchor: 0, shift: at(drag.x, drag.y) - at(ev.x, ev.y) });
|
|
125
|
+
ev.handled = true;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'up':
|
|
129
|
+
if (this.#drag) {
|
|
130
|
+
this.#drag = null;
|
|
131
|
+
platform?.setCursor?.('grab');
|
|
132
|
+
ev.handled = true;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case 'leave':
|
|
136
|
+
platform?.setCursor?.('');
|
|
137
|
+
break;
|
|
138
|
+
case 'wheel': {
|
|
139
|
+
this.#stopCoast();
|
|
140
|
+
const targets = this.targets(ev.axis);
|
|
141
|
+
if (targets.length === 0)
|
|
142
|
+
break;
|
|
143
|
+
// A turn that is mostly sideways moves the view along (a trackpad's two-finger swipe, or Shift and the wheel).
|
|
144
|
+
if (!ev.ctrl && Math.abs(ev.deltaX) > Math.abs(ev.deltaY)) {
|
|
145
|
+
if (a.pan === false)
|
|
146
|
+
break;
|
|
147
|
+
for (const { axis, at, snap } of targets) {
|
|
148
|
+
if (axis.ruler.horizontal)
|
|
149
|
+
this.apply(axis, { snap, scale: 1, anchor: 0, shift: at(ev.x + ev.deltaX, ev.y) - at(ev.x, ev.y) });
|
|
150
|
+
}
|
|
151
|
+
ev.handled = true;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
if (a.zoom === false || ev.deltaY === 0)
|
|
155
|
+
break;
|
|
156
|
+
// A pinch on a trackpad arrives as a wheel turn with Ctrl held, in much smaller steps than a wheel's notch: it gets its own speed.
|
|
157
|
+
const speed = ev.ctrl ? (a.pinchSpeed ?? PINCH_SPEED) : (a.zoomSpeed ?? 0.002);
|
|
158
|
+
const scale = Math.exp(ev.deltaY * speed);
|
|
159
|
+
for (const { axis, at, snap } of targets)
|
|
160
|
+
this.apply(axis, { snap, scale, anchor: at(ev.x, ev.y), shift: 0 });
|
|
161
|
+
ev.handled = true;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
case 'pinch': {
|
|
165
|
+
// A gesture the platform recognised (Safari): the fingers spread by `scale`, so the view shrinks by it.
|
|
166
|
+
const targets = this.targets(ev.axis);
|
|
167
|
+
if (a.zoom === false || targets.length === 0 || !(ev.scale > 0))
|
|
168
|
+
break;
|
|
169
|
+
for (const { axis, at, snap } of targets)
|
|
170
|
+
this.apply(axis, { snap, scale: 1 / ev.scale, anchor: at(ev.x, ev.y), shift: 0 });
|
|
171
|
+
ev.handled = true;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/** Two presses close together reset the axis under them, or all of them. True when this press was the second. */
|
|
177
|
+
#doubleClick(ev) {
|
|
178
|
+
const now = performance.now(), last = this.#lastDown;
|
|
179
|
+
if (this.attrs.resetOnDoubleClick !== false && now - last.time < 350 && Math.hypot(ev.x - last.x, ev.y - last.y) < 6) {
|
|
180
|
+
this.#lastDown = { time: -Infinity, x: 0, y: 0 };
|
|
181
|
+
for (const { axis } of this.targets(ev.axis))
|
|
182
|
+
axis.setViewRange(null);
|
|
183
|
+
ev.handled = true;
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
this.#lastDown = { time: now, x: ev.x, y: ev.y };
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Touch: one finger pans (unless `touch` is 'two-finger', where the page keeps it for scrolling) and two fingers pinch.
|
|
191
|
+
* A pinch keeps the content under each finger under it, so it zooms and pans in the same movement; on an axis along which
|
|
192
|
+
* the fingers are barely apart it only pans, so a level pinch zooms the horizontal axes alone and a plumb one the vertical.
|
|
193
|
+
*/
|
|
194
|
+
#touch(ev) {
|
|
195
|
+
const a = this.attrs, always = a.touch === 'always', touches = this.#touches;
|
|
196
|
+
switch (ev.kind) {
|
|
197
|
+
case 'down': {
|
|
198
|
+
this.#stopCoast();
|
|
199
|
+
touches.set(ev.id, { x: ev.x, y: ev.y, axis: ev.axis });
|
|
200
|
+
if (touches.size === 2 && a.zoom !== false) {
|
|
201
|
+
this.#drag = null;
|
|
202
|
+
this.#startPinch();
|
|
203
|
+
ev.handled = true;
|
|
204
|
+
}
|
|
205
|
+
else if (touches.size === 1) {
|
|
206
|
+
if (this.#doubleClick(ev))
|
|
207
|
+
break;
|
|
208
|
+
if (always && a.pan !== false) {
|
|
209
|
+
this.#drag = { id: ev.id, x: ev.x, y: ev.y, moves: this.targets(ev.axis), samples: [{ t: performance.now(), x: ev.x, y: ev.y }] };
|
|
210
|
+
ev.handled = true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case 'move': {
|
|
216
|
+
const t = touches.get(ev.id);
|
|
217
|
+
if (!t)
|
|
218
|
+
break;
|
|
219
|
+
t.x = ev.x;
|
|
220
|
+
t.y = ev.y;
|
|
221
|
+
if (this.#pinch) {
|
|
222
|
+
this.#movePinch();
|
|
223
|
+
ev.handled = true;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
const drag = this.#drag;
|
|
227
|
+
if (drag && drag.id === ev.id) {
|
|
228
|
+
for (const { axis, at, snap } of drag.moves)
|
|
229
|
+
this.apply(axis, { snap, scale: 1, anchor: 0, shift: at(drag.x, drag.y) - at(ev.x, ev.y) });
|
|
230
|
+
drag.samples?.push({ t: performance.now(), x: ev.x, y: ev.y });
|
|
231
|
+
if (drag.samples && drag.samples.length > 8)
|
|
232
|
+
drag.samples.shift();
|
|
233
|
+
ev.handled = true;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case 'up': {
|
|
238
|
+
touches.delete(ev.id);
|
|
239
|
+
if (this.#drag?.id === ev.id) {
|
|
240
|
+
const drag = this.#drag;
|
|
241
|
+
this.#drag = null;
|
|
242
|
+
if (a.inertia !== false && !this.#pinch)
|
|
243
|
+
this.#flick(drag);
|
|
244
|
+
}
|
|
245
|
+
if (this.#pinch) {
|
|
246
|
+
this.#pinch = null;
|
|
247
|
+
// In 'always' the finger that is left carries on as a pan from where it is.
|
|
248
|
+
const [rest] = [...touches];
|
|
249
|
+
if (always && a.pan !== false && rest) {
|
|
250
|
+
this.#drag = { id: rest[0], x: rest[1].x, y: rest[1].y, moves: this.targets(rest[1].axis) };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
ev.handled = true;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
#stopCoast() {
|
|
259
|
+
this.#coast?.cancel();
|
|
260
|
+
this.#coast = null;
|
|
261
|
+
}
|
|
262
|
+
/** The finger left while moving: carry on at its speed, slowing as a flick does, a frame at a time until it is slow enough to stop. */
|
|
263
|
+
#flick(drag) {
|
|
264
|
+
const platform = this.chart?.platform, samples = drag.samples;
|
|
265
|
+
if (!platform?.schedule || !samples || samples.length < 2)
|
|
266
|
+
return;
|
|
267
|
+
const now = performance.now(), last = samples[samples.length - 1];
|
|
268
|
+
if (now - last.t > FLICK_REST)
|
|
269
|
+
return; // it stopped before it let go
|
|
270
|
+
const first = samples.find((p) => last.t - p.t <= FLICK_WINDOW) ?? last;
|
|
271
|
+
const dt = Math.max(last.t - first.t, 1);
|
|
272
|
+
let vx = (last.x - first.x) / dt, vy = (last.y - first.y) / dt;
|
|
273
|
+
if (Math.hypot(vx, vy) < FLICK_MIN)
|
|
274
|
+
return;
|
|
275
|
+
// Virtual finger position, advanced by the speed each frame; the view follows it the way a drag would.
|
|
276
|
+
let x = last.x, y = last.y, then = now, stop = null, live = true;
|
|
277
|
+
const tick = () => {
|
|
278
|
+
stop = null;
|
|
279
|
+
const p = this.chart?.platform;
|
|
280
|
+
if (!live || !p?.schedule)
|
|
281
|
+
return;
|
|
282
|
+
const t = performance.now(), step = Math.min(t - then, 64);
|
|
283
|
+
then = t;
|
|
284
|
+
x += vx * step;
|
|
285
|
+
y += vy * step;
|
|
286
|
+
const decay = Math.exp(-step / FLICK_TAU);
|
|
287
|
+
vx *= decay;
|
|
288
|
+
vy *= decay;
|
|
289
|
+
for (const { axis, at, snap } of drag.moves)
|
|
290
|
+
this.apply(axis, { snap, scale: 1, anchor: 0, shift: at(drag.x, drag.y) - at(x, y) });
|
|
291
|
+
if (Math.hypot(vx, vy) < FLICK_STOP) {
|
|
292
|
+
this.#coast = null;
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
stop = p.schedule(tick);
|
|
296
|
+
};
|
|
297
|
+
this.#coast = { cancel: () => { live = false; stop?.(); } };
|
|
298
|
+
stop = platform.schedule(tick);
|
|
299
|
+
}
|
|
300
|
+
#startPinch() {
|
|
301
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- the size of `#touches` was checked as two just above
|
|
302
|
+
const [[id1, p1], [id2, p2]] = [...this.#touches];
|
|
303
|
+
// Both fingers on one axis' strip: that axis alone.
|
|
304
|
+
const over = p1.axis && p1.axis === p2.axis && this.attrs.axisZoom !== false ? p1.axis : null;
|
|
305
|
+
const moves = this.targets(over).map((m) => ({ ...m, a1: m.at(p1.x, p1.y), a2: m.at(p2.x, p2.y), length: m.axis.ruler.length }));
|
|
306
|
+
this.#pinch = { ids: [id1, id2], moves };
|
|
307
|
+
}
|
|
308
|
+
#movePinch() {
|
|
309
|
+
const pinch = this.#pinch;
|
|
310
|
+
if (!pinch)
|
|
311
|
+
return;
|
|
312
|
+
const p1 = this.#touches.get(pinch.ids[0]), p2 = this.#touches.get(pinch.ids[1]);
|
|
313
|
+
if (!p1 || !p2)
|
|
314
|
+
return;
|
|
315
|
+
for (const m of pinch.moves) {
|
|
316
|
+
const b1 = m.at(p1.x, p1.y), b2 = m.at(p2.x, p2.y), span = m.a2 - m.a1, now = b2 - b1;
|
|
317
|
+
if (Math.abs(span) * m.length >= PINCH_MIN_SPAN && Math.abs(now) * m.length >= 1) {
|
|
318
|
+
const k = span / now;
|
|
319
|
+
this.apply(m.axis, { snap: m.snap, scale: k, anchor: 0, shift: m.a1 - b1 * k });
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
this.apply(m.axis, { snap: m.snap, scale: 1, anchor: 0, shift: (m.a1 + m.a2) / 2 - (b1 + b2) / 2 });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** The axes a gesture at an axis' strip moves: that axis alone (`axisZoom`), or everything it controls. */
|
|
327
|
+
targets(over) {
|
|
328
|
+
const all = this.controlled();
|
|
329
|
+
return over && this.attrs.axisZoom !== false ? all.filter((m) => m.axis === over) : all;
|
|
330
|
+
}
|
|
331
|
+
/** The keys in force: the defaults with the ones asked for laid over them, or none. */
|
|
332
|
+
get keyMap() {
|
|
333
|
+
const k = this.attrs.keys;
|
|
334
|
+
return k === false ? null : { ...DEFAULT_PANZOOM_KEYS, ...k };
|
|
335
|
+
}
|
|
336
|
+
/** The plot as a tab stop with the application role, which is what tells a screen reader to pass the arrow keys on to it. */
|
|
337
|
+
#paintFocus() {
|
|
338
|
+
const s = this.surface, plane = this.requireParent(), map = this.keyMap;
|
|
339
|
+
if (!map) {
|
|
340
|
+
s.removePrefix('focus');
|
|
341
|
+
s.remove('ring');
|
|
342
|
+
this.#lastKeys = '';
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const r = plane.rect, node = s.node('focus', 'rect', { above: true });
|
|
346
|
+
s.set(node, 'x', 0);
|
|
347
|
+
s.set(node, 'y', 0);
|
|
348
|
+
s.set(node, 'width', r.width);
|
|
349
|
+
s.set(node, 'height', r.height);
|
|
350
|
+
s.set(node, 'fill', 'none');
|
|
351
|
+
s.set(node, 'pointer-events', 'none');
|
|
352
|
+
makeTabStop(s, node);
|
|
353
|
+
writeAria(s, node, this.attrs.meta ?? (plane.attrs.meta?.title !== undefined ? { title: plane.attrs.meta.title } : undefined), { role: 'application' });
|
|
354
|
+
const shortcuts = [...new Set(Object.values(map).flat())].map((k) => (k === '+' ? 'Plus' : k === ' ' ? 'Space' : k)).join(' ');
|
|
355
|
+
if (shortcuts !== this.#lastKeys) {
|
|
356
|
+
this.#lastKeys = shortcuts;
|
|
357
|
+
s.set(node, 'aria-keyshortcuts', shortcuts);
|
|
358
|
+
}
|
|
359
|
+
inputOf(this.requireChart()).focusable(this, node);
|
|
360
|
+
this.#node = node;
|
|
361
|
+
drawFocusRing(s, 'ring', this.styleFor(['focusRing', 'line']), this.#ring ? { x: 3, y: 3, width: Math.max(0, r.width - 6), height: Math.max(0, r.height - 6) } : null);
|
|
362
|
+
}
|
|
363
|
+
#node = null;
|
|
364
|
+
/** Move the view by a fraction of itself along the way an action points, or zoom it about its middle, or reset it. */
|
|
365
|
+
#key(ev) {
|
|
366
|
+
const map = this.keyMap;
|
|
367
|
+
if (!map || ev.kind !== 'down' || ev.node !== this.#node || ev.ctrl || ev.alt)
|
|
368
|
+
return;
|
|
369
|
+
let action;
|
|
370
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- `for…in` gives `string`; the keys of `map` are `PanZoomAction`s
|
|
371
|
+
for (const name in map)
|
|
372
|
+
if (map[name].includes(ev.key)) {
|
|
373
|
+
action = name;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
if (!action)
|
|
377
|
+
return;
|
|
378
|
+
ev.handled = true;
|
|
379
|
+
const a = this.attrs;
|
|
380
|
+
if (action === 'reset') {
|
|
381
|
+
this.reset();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (action === 'zoomIn' || action === 'zoomOut') {
|
|
385
|
+
const k = a.keyZoom ?? KEY_ZOOM, scale = action === 'zoomIn' ? 1 / k : k;
|
|
386
|
+
for (const { axis, snap } of this.controlled())
|
|
387
|
+
this.apply(axis, { snap, scale, anchor: 0.5, shift: 0 });
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const [horizontal, sign, page] = MOVES[action];
|
|
391
|
+
const step = page || ev.shift ? Math.min(KEY_PAGE, 5 * (a.keyStep ?? KEY_STEP)) : (a.keyStep ?? KEY_STEP);
|
|
392
|
+
for (const { axis, snap } of this.controlled()) {
|
|
393
|
+
if (axis.ruler.horizontal === horizontal)
|
|
394
|
+
this.apply(axis, { snap, scale: 1, anchor: 0, shift: sign * step });
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
/** The axes this will move, as of now: the chosen ones (or all the plane's) that can be followed along and fit `direction`. */
|
|
398
|
+
controlled() {
|
|
399
|
+
const a = this.attrs, only = a.direction ?? 'both';
|
|
400
|
+
const axes = a.axes ? a.axes.map((ref) => resolveRef(ref, this, Axis)) : [...this.requireParent().axes];
|
|
401
|
+
const out = [];
|
|
402
|
+
for (const axis of axes) {
|
|
403
|
+
const at = axis.ruler.fractionAt;
|
|
404
|
+
if (at && (only === 'both' || axis.ruler.horizontal === (only === 'horizontal'))) {
|
|
405
|
+
out.push({ axis, at: at.bind(axis.ruler), snap: { ...(axis.viewRange ?? axis.range) } });
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* What the limit is measured against: the range all of the data on this axis asks for, not the part in view.
|
|
412
|
+
* (If it were the part in view, zooming one axis would move the limit of the other under your hand.)
|
|
413
|
+
*/
|
|
414
|
+
limitFor(axis) {
|
|
415
|
+
let lo = Infinity, hi = -Infinity;
|
|
416
|
+
for (const series of this.requireParent().series) {
|
|
417
|
+
const extent = series.extentFor(axis);
|
|
418
|
+
if (extent) {
|
|
419
|
+
if (extent.min < lo)
|
|
420
|
+
lo = extent.min;
|
|
421
|
+
if (extent.max > hi)
|
|
422
|
+
hi = extent.max;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return axis.fit(lo, hi);
|
|
426
|
+
}
|
|
427
|
+
/** Applies the change and sets the axis' view range; a view that covers the natural range is recorded as no view at all. */
|
|
428
|
+
apply(axis, change) {
|
|
429
|
+
const view = this.viewFor(axis, change);
|
|
430
|
+
if (view !== null)
|
|
431
|
+
axis.setViewRange(view === 'natural' ? null : view);
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* The new range, 'natural' if the change ends up covering all of it, or null to leave things as they are.
|
|
435
|
+
* A display fraction `d` along the axis is `u` in the scale's own fractions, flipped when the axis is inverted.
|
|
436
|
+
*/
|
|
437
|
+
viewFor(axis, { snap, scale: k, anchor, shift }) {
|
|
438
|
+
const scale = axis.scale, inverted = axis.attrs.inverted === true;
|
|
439
|
+
const map = (u) => {
|
|
440
|
+
const d = inverted ? 1 - u : u;
|
|
441
|
+
const moved = anchor + (d - anchor) * k + shift;
|
|
442
|
+
return inverted ? 1 - moved : moved;
|
|
443
|
+
};
|
|
444
|
+
let u0 = map(0), u1 = map(1);
|
|
445
|
+
if (this.attrs.limit !== false) {
|
|
446
|
+
const wanted = this.limitFor(axis);
|
|
447
|
+
const natural = new Float64Array([wanted.min, wanted.max]);
|
|
448
|
+
scale.normalize(natural, 2, 1, 0, snap);
|
|
449
|
+
const lo = Math.min(natural[0], natural[1]), hi = Math.max(natural[0], natural[1]);
|
|
450
|
+
if (u1 - u0 >= hi - lo)
|
|
451
|
+
return 'natural';
|
|
452
|
+
if (u0 < lo) {
|
|
453
|
+
u1 += lo - u0;
|
|
454
|
+
u0 = lo;
|
|
455
|
+
}
|
|
456
|
+
else if (u1 > hi) {
|
|
457
|
+
u0 -= u1 - hi;
|
|
458
|
+
u1 = hi;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const min = scale.denormalize(u0, snap), max = scale.denormalize(u1, snap);
|
|
462
|
+
// Past the precision of a double there is nothing left to zoom into.
|
|
463
|
+
if (!(max - min > 1e-12 * Math.max(1, Math.abs(min), Math.abs(max))))
|
|
464
|
+
return null;
|
|
465
|
+
return { min, max };
|
|
466
|
+
}
|
|
467
|
+
}
|
package/dist/plane.d.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planes: a coordinate space that owns axes and series, lays them out and routes pointer input.
|
|
3
|
+
*
|
|
4
|
+
* How a plane, its axes and its series cooperate (the part that is hardest to see from any one file)
|
|
5
|
+
* ─────────────────────────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
* Sizes depend on ranges (wider tick labels need more margin) and ranges depend on sizes (decimation reads as many
|
|
7
|
+
* points as there are pixels). The plane breaks the circle by working in passes, in `arrange`:
|
|
8
|
+
* 1. Estimate the plot size (the available rectangle less a typical margin) and fit with it: `Plane.fit` has the
|
|
9
|
+
* series declare their domain, load their data, and contribute value extents, then settles every axis' range.
|
|
10
|
+
* 2. With ranges known, the axes are measured (their tick labels give thickness) and the real plot rectangle is the
|
|
11
|
+
* available area less those, with `insets` overriding.
|
|
12
|
+
* 3. With the real size known, the axes get their final `Ruler`s and each series is `project`ed: data → fractions
|
|
13
|
+
* through the axes → pixels through the plane's `place`.
|
|
14
|
+
* The plane alone knows what the two axes *mean* spatially (`place`, and rulers), the axes alone know values ↔ fractions,
|
|
15
|
+
* and the series alone know data. That separation is why a radar chart only needs a new plane and the same series.
|
|
16
|
+
*
|
|
17
|
+
* A `Ruler` is the one thing an axis draws against: a line in plot pixels that maps a fraction 0..1 to a position and
|
|
18
|
+
* a normal. Straight edges, floating axes and radial spokes are all just different rulers, so Axis has no per-shape code.
|
|
19
|
+
*/
|
|
20
|
+
import { Axis, type AxisOrientation } from './axis.ts';
|
|
21
|
+
import type { Chart } from './chart.ts';
|
|
22
|
+
import type { PointBuffer } from './data.ts';
|
|
23
|
+
import { type BaseAttributes, ChartElement } from './element.ts';
|
|
24
|
+
import { type ChartEvent } from './events.ts';
|
|
25
|
+
import type { CalloutAnnotation, LineAnnotation } from './annotation.ts';
|
|
26
|
+
import { type Insets, type Rect, type Size } from './geometry.ts';
|
|
27
|
+
import type { BoxZoom } from './boxzoom.ts';
|
|
28
|
+
import type { Brush } from './brush.ts';
|
|
29
|
+
import type { PanZoom } from './panzoom.ts';
|
|
30
|
+
import type { PointerListener } from './pointer.ts';
|
|
31
|
+
import type { PlatformNode } from './platform.ts';
|
|
32
|
+
import { Series } from './series.ts';
|
|
33
|
+
import type { Stack } from './stack.ts';
|
|
34
|
+
import type { LineProps } from './theme.ts';
|
|
35
|
+
export interface PlaneAttributes extends BaseAttributes {
|
|
36
|
+
/** Fixed margins round the plot (room for axes). Any side left out is sized to fit its axes. */
|
|
37
|
+
insets?: Partial<Insets>;
|
|
38
|
+
}
|
|
39
|
+
/** Open for augmentation: `declare module 'inkplot' { interface PlaneChildMap { mine: MyAnnotation } }`. */
|
|
40
|
+
export interface PlaneChildMap {
|
|
41
|
+
axis: Axis;
|
|
42
|
+
series: Series;
|
|
43
|
+
stack: Stack;
|
|
44
|
+
lineAnnotation: LineAnnotation;
|
|
45
|
+
calloutAnnotation: CalloutAnnotation;
|
|
46
|
+
panZoom: PanZoom;
|
|
47
|
+
boxZoom: BoxZoom;
|
|
48
|
+
brush: Brush;
|
|
49
|
+
pointerListener: PointerListener;
|
|
50
|
+
}
|
|
51
|
+
export type PlaneChildren = PlaneChildMap[keyof PlaneChildMap][];
|
|
52
|
+
/** What every plane does the same way: owns the axes and series among its children and runs the fit. */
|
|
53
|
+
export declare abstract class Plane<A extends PlaneAttributes = PlaneAttributes> extends ChartElement<Chart, PlaneChildren, A> {
|
|
54
|
+
#private;
|
|
55
|
+
/** The allocated plot area, in the chart's coordinates. Valid after arrange. */
|
|
56
|
+
get rect(): Readonly<Rect>;
|
|
57
|
+
/** Subclasses record the plot area they settle on. */
|
|
58
|
+
protected setRect(rect: Rect): void;
|
|
59
|
+
get axes(): readonly Axis[];
|
|
60
|
+
/** Every series on the plane, in drawing order, those inside a `Stack` included, whether shown or not: the order that gives each its palette colour. */
|
|
61
|
+
get allSeries(): readonly Series[];
|
|
62
|
+
/** The series that are on show (`visible` is not false): the ones that are drawn, fit the axes and are hit. */
|
|
63
|
+
get series(): readonly Series[];
|
|
64
|
+
/**
|
|
65
|
+
* The axis a series uses when it does not name one, for the axis its first or its second coordinate rides on. What
|
|
66
|
+
* those are is up to the plane: a Cartesian plane's horizontal and vertical axes, a radial plane's angle and radius.
|
|
67
|
+
*/
|
|
68
|
+
abstract defaultAxis(role: 'first' | 'second'): Axis | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Whether a line that is straight in the plane's own terms (constant value, say) comes out curved. False for a
|
|
71
|
+
* rectangle, true for anything with an angle. Annotations use it to decide how many points to trace a line with.
|
|
72
|
+
*/
|
|
73
|
+
abstract get curved(): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Fractions of `axis` at which a line running along it changes direction, ascending, for planes that are not smooth
|
|
76
|
+
* (a radar's spokes). A line traced through these is exact where sampling evenly would cut the corners. Default none.
|
|
77
|
+
*/
|
|
78
|
+
breaks(_axis: Axis): readonly number[];
|
|
79
|
+
/**
|
|
80
|
+
* About how long `axis` will be drawn, in pixels, given the room estimated for the plot, so that a series' reach in
|
|
81
|
+
* pixels can be turned into room in the axis' range. 0 where that has no meaning (the default, and any axis round a circle).
|
|
82
|
+
*/
|
|
83
|
+
protected axisLength(_axis: Axis, _estimate: Size): number;
|
|
84
|
+
/** CSS reference to this plane's clip region, for children to clip themselves with. `null` when not clipped. */
|
|
85
|
+
abstract get clipUrl(): string | null;
|
|
86
|
+
/**
|
|
87
|
+
* The first two columns of `points`, normalized (0..1) through `first` and `second`, to plot-area pixels, in place.
|
|
88
|
+
* The plane decides what the two axes mean spatially: a Cartesian plane looks at their sides.
|
|
89
|
+
*/
|
|
90
|
+
abstract place(points: PointBuffer, first: Axis, second: Axis): void;
|
|
91
|
+
/**
|
|
92
|
+
* The inverse of `place` for one position: what a spot in plot pixels is in data terms, as the values `first` and
|
|
93
|
+
* `second` read there. It is what turns a dragged pointer into an edit. A position outside the plot gives values
|
|
94
|
+
* outside the axes' ranges; an angle that wraps comes back inside the range.
|
|
95
|
+
*/
|
|
96
|
+
abstract unplace(x: number, y: number, first: Axis, second: Axis): readonly [first: number, second: number];
|
|
97
|
+
/** Put every axis of the plane back to the range its data asks for, undoing pan, zoom and box zoom. */
|
|
98
|
+
resetViews(): void;
|
|
99
|
+
/** Whether `axis` comes back round to where it started, so that its two ends are the same place (the angle of a full turn). Default no. */
|
|
100
|
+
cyclic(_axis: Axis): boolean;
|
|
101
|
+
/**
|
|
102
|
+
* The axis whose strip beside the plot a position (in plot pixels) is on, or null: the strip is where its line, ticks and labels
|
|
103
|
+
* are. This is what lets a wheel turn or a drag over an axis act on that axis alone. Default: none, so only the plot itself takes input.
|
|
104
|
+
*/
|
|
105
|
+
axisAt(_x: number, _y: number): Axis | null;
|
|
106
|
+
/** Whether a position in plot pixels is on the plot: inside the rectangle, and for a circular plot inside the circle. Default: inside the rectangle. */
|
|
107
|
+
covers(x: number, y: number): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* The line along which the first (or second) coordinate has the data value `value`, across the whole plot, as path data in
|
|
110
|
+
* plot pixels: a vertical line for a horizontal first axis, a spoke for an angle, a circle for a radius. It is what a
|
|
111
|
+
* crosshair draws, and is as the plane draws its own gridlines. Empty when the plane has no such axis.
|
|
112
|
+
*/
|
|
113
|
+
guide(role: 'first' | 'second', value: number): string;
|
|
114
|
+
/**
|
|
115
|
+
* Give every axis an orientation and a stack slot: its own `orientation` attribute if it has one, else the next of
|
|
116
|
+
* `defaults` in child order. `check` rejects an orientation this kind of plane does not have. The slot counts how
|
|
117
|
+
* many axes share an orientation, so they can be stacked outwards.
|
|
118
|
+
*/
|
|
119
|
+
protected assignAxes<O extends AxisOrientation>(defaults: readonly O[], check: (o: AxisOrientation) => O): void;
|
|
120
|
+
/** Put a resolved line look on a node of this plane (gridlines, frame). */
|
|
121
|
+
protected strokeWith(node: PlatformNode, look: LineProps): void;
|
|
122
|
+
/**
|
|
123
|
+
* The first half of every layout: start the axes, let the series name theirs, agree the domain before any data is
|
|
124
|
+
* read, then fill every series (which gives the value extents) and settle every range. `estimate` is how much room the
|
|
125
|
+
* plot is expected to have, which sizes decimation; the real plot is only known once the axes have been measured.
|
|
126
|
+
*/
|
|
127
|
+
protected fit(estimate: Size): void;
|
|
128
|
+
}
|
|
129
|
+
export interface CartesianPlaneAttributes extends PlaneAttributes {
|
|
130
|
+
/** Lines at the first horizontal and first vertical axis' ticks. Default true. */
|
|
131
|
+
gridlines?: boolean;
|
|
132
|
+
/** Draw a border round the plot area, above the series. */
|
|
133
|
+
frame?: boolean;
|
|
134
|
+
/** Clip series to the plot area. Default true. */
|
|
135
|
+
clip?: boolean;
|
|
136
|
+
}
|
|
137
|
+
/** A fresh id for a plane's clip region, unique within the page. */
|
|
138
|
+
export declare const nextClipId: () => string;
|
|
139
|
+
/** The least room left round a plot, whatever the axes need. */
|
|
140
|
+
export declare const MIN_INSET = 8;
|
|
141
|
+
/** x/y plane: horizontal and vertical axes on its edges (or floating across it), clipped, with gridlines at the first axes' ticks. */
|
|
142
|
+
export declare class CartesianPlane extends Plane<CartesianPlaneAttributes> {
|
|
143
|
+
private readonly clipId;
|
|
144
|
+
get clipUrl(): string | null;
|
|
145
|
+
get curved(): boolean;
|
|
146
|
+
protected axisLength(axis: Axis, estimate: Size): number;
|
|
147
|
+
/** The first coordinate is on the horizontal axis, the second on the vertical one. */
|
|
148
|
+
defaultAxis(role: 'first' | 'second'): Axis | undefined;
|
|
149
|
+
/**
|
|
150
|
+
* Fractions to pixels. The 'first' coordinate goes on whichever axis is horizontal, so a series' first/second
|
|
151
|
+
* columns need no knowledge of orientation (swapping axis sides transposes the plot).
|
|
152
|
+
*/
|
|
153
|
+
place(points: PointBuffer, first: Axis, second: Axis): void;
|
|
154
|
+
unplace(x: number, y: number, first: Axis, second: Axis): readonly [number, number];
|
|
155
|
+
axisAt(x: number, y: number): Axis | null;
|
|
156
|
+
onEvent(ev: ChartEvent): void;
|
|
157
|
+
/**
|
|
158
|
+
* Where a floating axis crosses, as a fraction of the plot across the crossing axis (0 at the left or bottom, 1 at
|
|
159
|
+
* the right or top), or null for an axis fixed to a side. A position out of range is held at the nearest edge.
|
|
160
|
+
*/
|
|
161
|
+
private crossing;
|
|
162
|
+
/** Whether an axis takes room round the plot: one fixed to a side does, and so does one held at the edge it faces. */
|
|
163
|
+
private reserves;
|
|
164
|
+
/**
|
|
165
|
+
* Every axis gets its place in a plot `w` by `h`: a fixed side, stacked outwards by the axes before it, or across the
|
|
166
|
+
* plot. Titles that are not on an axis line go outside everything else at their edge, one after another.
|
|
167
|
+
*/
|
|
168
|
+
private setRulers;
|
|
169
|
+
/** Negotiates ranges, lays out the axes, projects every series. */
|
|
170
|
+
private arrange;
|
|
171
|
+
/** Clip region, gridlines (from the first horizontal and vertical axis) and frame. */
|
|
172
|
+
private paint;
|
|
173
|
+
}
|