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
package/dist/png.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A chart as a PNG in the browser: `toPng(chart)` for one on the page, `svgToPng(svg)` for an SVG string you already have.
|
|
3
|
+
*
|
|
4
|
+
* const blob = await toPng(chart, { scale: 2, background: '#fff' });
|
|
5
|
+
*
|
|
6
|
+
* It draws the chart's SVG into a canvas, which is the one thing a browser does that node does not, so it lives apart from
|
|
7
|
+
* the core and costs nothing unless imported. For a chart on the page it takes the SVG that is showing, so the picture is
|
|
8
|
+
* what the person sees; for one not attached it renders a copy with `toSvgString` and leaves the chart as it was.
|
|
9
|
+
* `scale` multiplies the pixel size (2 for a sharp picture on a dense screen). Fonts the page loaded are not
|
|
10
|
+
* available to an SVG drawn as an image, so text falls back to the system's; names a system has (the default
|
|
11
|
+
* `system-ui`) come out right. A transparent background stays transparent unless `background` says otherwise. On a server use a WASM
|
|
12
|
+
* renderer instead: see the recipe "Make a PNG on a server".
|
|
13
|
+
*/
|
|
14
|
+
import { toSvgString } from './chart.js';
|
|
15
|
+
import { StringPlatform } from './platform.js';
|
|
16
|
+
const browserHost = () => ({
|
|
17
|
+
loadImage: (url) => new Promise((resolve, reject) => {
|
|
18
|
+
const image = new Image();
|
|
19
|
+
image.onload = () => resolve({ width: image.naturalWidth, height: image.naturalHeight, image });
|
|
20
|
+
image.onerror = () => reject(new Error('toPng: the browser could not draw this SVG'));
|
|
21
|
+
image.src = url;
|
|
22
|
+
}),
|
|
23
|
+
canvas: (width, height) => {
|
|
24
|
+
if (typeof OffscreenCanvas !== 'undefined') {
|
|
25
|
+
const c = new OffscreenCanvas(width, height);
|
|
26
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- the canvas context is handed to the renderer as the one type it draws on
|
|
27
|
+
return { context: c.getContext('2d'), blob: () => c.convertToBlob({ type: 'image/png' }) };
|
|
28
|
+
}
|
|
29
|
+
const c = document.createElement('canvas');
|
|
30
|
+
c.width = width;
|
|
31
|
+
c.height = height;
|
|
32
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- the canvas context is handed to the renderer as the one type it draws on
|
|
33
|
+
return { context: c.getContext('2d'), blob: () => new Promise((resolve, reject) => c.toBlob((b) => (b ? resolve(b) : reject(new Error('toPng: the canvas gave no image'))), 'image/png')) };
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
/** An SVG document as a PNG. Its own `width` and `height` give the size, else `options.size`. */
|
|
37
|
+
export async function svgToPng(svg, options = {}, host = browserHost()) {
|
|
38
|
+
const scale = options.scale ?? 1;
|
|
39
|
+
if (!(scale > 0))
|
|
40
|
+
throw new Error('toPng: scale must be above 0');
|
|
41
|
+
const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg);
|
|
42
|
+
const { width: w, height: h, image } = await host.loadImage(url);
|
|
43
|
+
const width = Math.max(1, Math.round((options.size?.width ?? w) * scale)), height = Math.max(1, Math.round((options.size?.height ?? h) * scale));
|
|
44
|
+
const { context, blob } = host.canvas(width, height);
|
|
45
|
+
if (options.background) {
|
|
46
|
+
context.fillStyle = options.background;
|
|
47
|
+
context.fillRect(0, 0, width, height);
|
|
48
|
+
}
|
|
49
|
+
context.drawImage(image, 0, 0, width, height);
|
|
50
|
+
return blob();
|
|
51
|
+
}
|
|
52
|
+
/** The chart as a PNG: what is showing if it is on the page, else a rendering of it. */
|
|
53
|
+
export function toPng(chart, options = {}, host) {
|
|
54
|
+
// A real SVG element is serialised as XML, which puts its namespace on it (outerHTML does not, and an image needs it).
|
|
55
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- the platform root is opaque; it is checked with `instanceof` on the same line
|
|
56
|
+
const platform = chart.platform, root = platform?.root, element = typeof XMLSerializer !== 'undefined' && typeof Element !== 'undefined' && root instanceof Element ? root : null;
|
|
57
|
+
if (element)
|
|
58
|
+
chart.flush(); // what is showing includes anything still waiting to be drawn
|
|
59
|
+
const live = element ? new XMLSerializer().serializeToString(element) : null;
|
|
60
|
+
if (live && platform) {
|
|
61
|
+
return svgToPng(live, { ...options, size: options.size ?? platform.size() }, host);
|
|
62
|
+
}
|
|
63
|
+
if (chart.platform) {
|
|
64
|
+
// On a string platform the SVG is already in hand; on anything else there is nothing to draw it from.
|
|
65
|
+
if (!(chart.platform instanceof StringPlatform))
|
|
66
|
+
throw new Error('toPng: the chart is attached to a platform that is not a DOM one');
|
|
67
|
+
return svgToPng(chart.platform.toString(), { ...options, size: options.size ?? chart.platform.size() }, host);
|
|
68
|
+
}
|
|
69
|
+
const a = chart.attrs, size = options.size ?? { width: a.width ?? 640, height: a.height ?? 400 };
|
|
70
|
+
return svgToPng(toSvgString(chart, size), { ...options, size }, host);
|
|
71
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type BaseAttributes, ChartElement } from './element.ts';
|
|
2
|
+
import { inputOf } from './input.ts';
|
|
3
|
+
import { type ChartEvent, type Hit } from './events.ts';
|
|
4
|
+
import type { Plane } from './plane.ts';
|
|
5
|
+
/** What every callback is told. */
|
|
6
|
+
export interface PointerInfo {
|
|
7
|
+
readonly plane: Plane;
|
|
8
|
+
/** The pointer in plot pixels. */
|
|
9
|
+
readonly x: number;
|
|
10
|
+
readonly y: number;
|
|
11
|
+
/**
|
|
12
|
+
* Where that is in data terms, on the axes of the hit's series, or else the plane's first and second axes. While
|
|
13
|
+
* dragging it is where the grabbed point is to go: the pointer's position carrying the grab's offset, so the point
|
|
14
|
+
* does not jump to be under the pointer's tip.
|
|
15
|
+
*/
|
|
16
|
+
readonly first: number;
|
|
17
|
+
readonly second: number;
|
|
18
|
+
/** The point under the pointer (for a drag, the one being dragged, as it was when grabbed); null over nothing. */
|
|
19
|
+
readonly hit: Hit | null;
|
|
20
|
+
readonly shift: boolean;
|
|
21
|
+
readonly ctrl: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** A drag always has a point. `phase` is 'start' on the first movement past the threshold, 'end' on release. */
|
|
24
|
+
export interface DragInfo extends PointerInfo {
|
|
25
|
+
readonly hit: Hit;
|
|
26
|
+
readonly phase: 'start' | 'move' | 'end';
|
|
27
|
+
}
|
|
28
|
+
export interface PointerListenerAttributes extends BaseAttributes {
|
|
29
|
+
/** The pointer came onto a point, left it, or moved to another: `info.hit` is the new one, null for none. */
|
|
30
|
+
onHover?: (info: PointerInfo) => void;
|
|
31
|
+
/** A press and release in the same place: on a point, or `info.hit` null on empty plot. */
|
|
32
|
+
onClick?: (info: PointerInfo) => void;
|
|
33
|
+
/** A drag that began on a point. The listener claims such presses, so they do not pan. */
|
|
34
|
+
onDrag?: (info: DragInfo) => void;
|
|
35
|
+
/** How far in pixels the pointer must travel before a press is a drag and no longer a click. Default 3. */
|
|
36
|
+
dragThreshold?: number;
|
|
37
|
+
}
|
|
38
|
+
export declare class PointerListener extends ChartElement<Plane, [], PointerListenerAttributes> {
|
|
39
|
+
#private;
|
|
40
|
+
static takesInput: typeof inputOf;
|
|
41
|
+
static attrTypes: {
|
|
42
|
+
dragThreshold: 2;
|
|
43
|
+
onClick: 0;
|
|
44
|
+
onDrag: 0;
|
|
45
|
+
onHover: 0;
|
|
46
|
+
id: 1;
|
|
47
|
+
meta: {
|
|
48
|
+
title: 1;
|
|
49
|
+
description: 1;
|
|
50
|
+
role: 1;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
onEvent(ev: ChartEvent): void;
|
|
54
|
+
}
|
package/dist/pointer.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PointerListener: what a user reaches for to react to the pointer over a plane, without handling raw pointer events.
|
|
3
|
+
*
|
|
4
|
+
* It sits in a plane, like `PanZoom`, and turns the plane's pointer input into three things people actually want:
|
|
5
|
+
* which point the pointer is over (`onHover`), a click on a point or on empty plot (`onClick`), and a point being
|
|
6
|
+
* dragged (`onDrag`). Each callback gets one `PointerInfo`, which carries the hit, where the pointer is in plot pixels
|
|
7
|
+
* and what that is in data terms, so an edit is a line or two:
|
|
8
|
+
*
|
|
9
|
+
* new PointerListener({ onDrag: ({ hit, first, second }) => { rows[hit.row!].x = first; rows[hit.row!].y = second; source.invalidate(); } })
|
|
10
|
+
*
|
|
11
|
+
* What "over" means is decided by each series' own `hit` setting (see hit.ts); the listener only asks the plane.
|
|
12
|
+
*
|
|
13
|
+
* Sharing the plane with `PanZoom`
|
|
14
|
+
* ────────────────────────────────
|
|
15
|
+
* Both want a press on the plot. A listener claims the press only when it has something to do with it: a drag that starts
|
|
16
|
+
* on a point (when `onDrag` is set). Anything else, including a press on empty plot, goes on to `PanZoom`, so dragging
|
|
17
|
+
* the background still pans. The claim works by order: a plane's children receive input in the order they were added, and
|
|
18
|
+
* `PanZoom` skips what is already handled, so the listener has to come first. Adding it after a `PanZoom` is an error.
|
|
19
|
+
*/
|
|
20
|
+
import { AUTO, NUMBER } from './attrs.js';
|
|
21
|
+
import { ChartElement, fail } from './element.js';
|
|
22
|
+
import { inputOf } from './input.js';
|
|
23
|
+
import { EVENT_ATTACH, EVENT_POINTER } from './events.js';
|
|
24
|
+
import { hitPlane } from './hit.js';
|
|
25
|
+
/** Same point as before? A row is the identity that survives re-sorting; without one the drawn index will do. */
|
|
26
|
+
const same = (a, b) => a === b || (!!a && !!b && a.series === b.series && (a.row ?? a.index) === (b.row ?? b.index));
|
|
27
|
+
export class PointerListener extends ChartElement {
|
|
28
|
+
static takesInput = inputOf;
|
|
29
|
+
static attrTypes = { ...ChartElement.attrTypes, dragThreshold: NUMBER, onClick: AUTO, onDrag: AUTO, onHover: AUTO };
|
|
30
|
+
#hover = null;
|
|
31
|
+
// A press in progress: where it began, the point under it, and the offset from the pointer to that point.
|
|
32
|
+
#press = null;
|
|
33
|
+
onEvent(ev) {
|
|
34
|
+
if (ev.type === EVENT_ATTACH) {
|
|
35
|
+
const plane = this.requireParent();
|
|
36
|
+
for (const sibling of plane) {
|
|
37
|
+
if (sibling === this)
|
|
38
|
+
break;
|
|
39
|
+
if (sibling.claimsPress)
|
|
40
|
+
fail('add a PointerListener before PanZoom in a plane: the listener has to see a press first to claim it');
|
|
41
|
+
}
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (ev.type !== EVENT_POINTER)
|
|
45
|
+
return;
|
|
46
|
+
const a = this.attrs, plane = this.requireParent(), platform = this.chart?.platform;
|
|
47
|
+
// Over an axis the pointer is not over anything plotted. (A press that began on the plot goes on being ours until it ends.)
|
|
48
|
+
if (ev.outside && !this.#press && ev.kind !== 'leave') {
|
|
49
|
+
if (this.#hover && ev.kind === 'move') {
|
|
50
|
+
this.#hover = null;
|
|
51
|
+
a.onHover?.({ plane, x: ev.x, y: ev.y, first: NaN, second: NaN, hit: null, shift: ev.shift, ctrl: ev.ctrl });
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (ev.kind === 'wheel' || ev.kind === 'pinch')
|
|
56
|
+
return;
|
|
57
|
+
const threshold = a.dragThreshold ?? 3;
|
|
58
|
+
const info = (hit, x, y, dx = 0, dy = 0) => {
|
|
59
|
+
const first = hit ? hit.series.firstAxis : plane.defaultAxis('first'), second = hit ? hit.series.secondAxis : plane.defaultAxis('second');
|
|
60
|
+
const at = first && second ? plane.unplace(x + dx, y + dy, first, second) : [NaN, NaN];
|
|
61
|
+
// In a stack the pointer reads the stacked height: the layer's own value is that less what it sits on.
|
|
62
|
+
return { plane, x, y, first: at[0], second: at[1] - (hit?.lift ?? 0), hit, shift: ev.shift, ctrl: ev.ctrl };
|
|
63
|
+
};
|
|
64
|
+
switch (ev.kind) {
|
|
65
|
+
case 'down': {
|
|
66
|
+
if (!(ev.buttons & 1))
|
|
67
|
+
break;
|
|
68
|
+
const hit = hitPlane(plane, ev.x, ev.y);
|
|
69
|
+
const claimed = !!hit && !!a.onDrag;
|
|
70
|
+
this.#press = { x: ev.x, y: ev.y, hit, dx: hit ? hit.px - ev.x : 0, dy: hit ? hit.py - ev.y : 0, claimed, dragging: false };
|
|
71
|
+
if (claimed)
|
|
72
|
+
ev.handled = true;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'move': {
|
|
76
|
+
const press = this.#press;
|
|
77
|
+
if (press) {
|
|
78
|
+
if (!(ev.buttons & 1)) {
|
|
79
|
+
this.#press = null;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
if (!press.dragging && Math.hypot(ev.x - press.x, ev.y - press.y) < threshold)
|
|
83
|
+
break;
|
|
84
|
+
const first = !press.dragging;
|
|
85
|
+
press.dragging = true;
|
|
86
|
+
if (press.claimed && press.hit && a.onDrag) {
|
|
87
|
+
a.onDrag({ ...info(press.hit, ev.x, ev.y, press.dx, press.dy), hit: press.hit, phase: first ? 'start' : 'move' });
|
|
88
|
+
platform?.setCursor?.('grabbing');
|
|
89
|
+
ev.handled = true;
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (!a.onHover && !a.onDrag && !a.onClick)
|
|
94
|
+
break;
|
|
95
|
+
const hit = hitPlane(plane, ev.x, ev.y);
|
|
96
|
+
if (!same(hit, this.#hover)) {
|
|
97
|
+
this.#hover = hit;
|
|
98
|
+
a.onHover?.(info(hit, ev.x, ev.y));
|
|
99
|
+
}
|
|
100
|
+
// Over a point the cursor says what it can do. Handled, so that `PanZoom` does not put its own over it.
|
|
101
|
+
if (hit) {
|
|
102
|
+
platform?.setCursor?.(a.onDrag ? 'grab' : 'pointer');
|
|
103
|
+
ev.handled = true;
|
|
104
|
+
}
|
|
105
|
+
else if (!a.onDrag)
|
|
106
|
+
platform?.setCursor?.('');
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'up': {
|
|
110
|
+
const press = this.#press;
|
|
111
|
+
this.#press = null;
|
|
112
|
+
if (!press)
|
|
113
|
+
break;
|
|
114
|
+
if (press.dragging && press.claimed && press.hit && a.onDrag) {
|
|
115
|
+
a.onDrag({ ...info(press.hit, ev.x, ev.y, press.dx, press.dy), hit: press.hit, phase: 'end' });
|
|
116
|
+
ev.handled = true;
|
|
117
|
+
}
|
|
118
|
+
else if (!press.dragging) {
|
|
119
|
+
a.onClick?.(info(press.hit, ev.x, ev.y));
|
|
120
|
+
}
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case 'leave':
|
|
124
|
+
this.#press = null;
|
|
125
|
+
if (this.#hover) {
|
|
126
|
+
this.#hover = null;
|
|
127
|
+
a.onHover?.(info(null, ev.x, ev.y));
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Quantiles of sorted numbers, shared by the statistics that need them (`BoxStats`, `Density`). */
|
|
2
|
+
/** The p-th quantile of ascending `v`, by the inclusive rule (rank p(n-1)) or the exclusive one (rank p(n+1)-1, held to the ends). */
|
|
3
|
+
export declare function quantile(v: ArrayLike<number>, p: number, method?: 'inclusive' | 'exclusive'): number;
|
package/dist/quantile.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Quantiles of sorted numbers, shared by the statistics that need them (`BoxStats`, `Density`). */
|
|
2
|
+
/** The p-th quantile of ascending `v`, by the inclusive rule (rank p(n-1)) or the exclusive one (rank p(n+1)-1, held to the ends). */
|
|
3
|
+
export function quantile(v, p, method = 'inclusive') {
|
|
4
|
+
const n = v.length;
|
|
5
|
+
if (n === 0)
|
|
6
|
+
return NaN;
|
|
7
|
+
const h = method === 'exclusive' ? Math.min(n - 1, Math.max(0, p * (n + 1) - 1)) : p * (n - 1);
|
|
8
|
+
const lo = Math.floor(h), hi = Math.ceil(h);
|
|
9
|
+
// 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
|
|
10
|
+
return v[lo] + (h - lo) * (v[hi] - v[lo]);
|
|
11
|
+
}
|
package/dist/radial.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Axis } from './axis.ts';
|
|
2
|
+
import type { PointBuffer } from './data.ts';
|
|
3
|
+
import { type ChartEvent } from './events.ts';
|
|
4
|
+
import { Plane, type PlaneAttributes } from './plane.ts';
|
|
5
|
+
export interface RadialPlaneAttributes extends PlaneAttributes {
|
|
6
|
+
/** Radians clockwise from twelve o'clock. Default 0. */
|
|
7
|
+
startAngle?: number;
|
|
8
|
+
/** Radians swept by the angle axis, clockwise; negative runs the other way. Default 2π. */
|
|
9
|
+
sweep?: number;
|
|
10
|
+
/** Fraction of the radius left empty in the middle. Default 0. */
|
|
11
|
+
innerRadius?: number;
|
|
12
|
+
/** Rings at the radius axis' ticks and spokes at the angle axis'. Default true. */
|
|
13
|
+
gridlines?: boolean;
|
|
14
|
+
/** Clip series to the plot's outline. Default true. */
|
|
15
|
+
clip?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The shared machinery of radial planes: layout in a circle, rulers for both axes, clip, gridlines, and `place`.
|
|
19
|
+
* Subclasses supply only the geometry (`locate`, `ring`, `outerRadius`, `prepare`).
|
|
20
|
+
*/
|
|
21
|
+
export declare abstract class RadialPlane<A extends RadialPlaneAttributes = RadialPlaneAttributes> extends Plane<A> {
|
|
22
|
+
#private;
|
|
23
|
+
static attrTypes: {
|
|
24
|
+
id: 1;
|
|
25
|
+
meta: {
|
|
26
|
+
title: 1;
|
|
27
|
+
description: 1;
|
|
28
|
+
role: 1;
|
|
29
|
+
};
|
|
30
|
+
innerRadius: 2;
|
|
31
|
+
startAngle: 2;
|
|
32
|
+
};
|
|
33
|
+
private readonly clipId;
|
|
34
|
+
get clipUrl(): string | null;
|
|
35
|
+
get curved(): boolean;
|
|
36
|
+
get startAngle(): number;
|
|
37
|
+
get sweep(): number;
|
|
38
|
+
get innerRadius(): number;
|
|
39
|
+
/** The outer radius in plot pixels. Valid after arrange. */
|
|
40
|
+
get radius(): number;
|
|
41
|
+
protected get cx(): number;
|
|
42
|
+
protected get cy(): number;
|
|
43
|
+
/** The line of constant value at pixel radius `r`, from the start of the sweep to its end (or back), as path data starting with M. */
|
|
44
|
+
protected abstract ring(r: number, reverse?: boolean): string;
|
|
45
|
+
/** Where a fraction of the way along the angle axis is, at pixel radius `r`. Writes x then y into `out`. */
|
|
46
|
+
protected abstract locate(fraction: number, r: number, out: Float64Array): void;
|
|
47
|
+
/** Distance from the middle to the outer edge of the plot in the direction (dx, dy): where the ray from the middle meets the rim. */
|
|
48
|
+
protected abstract outerRadius(dx: number, dy: number): number;
|
|
49
|
+
/** How far round the angle axis the direction (dx, dy) from the middle is, as a fraction of its sweep (wrapped into 0..1 for a full turn). */
|
|
50
|
+
protected abstract angleFraction(dx: number, dy: number): number;
|
|
51
|
+
/** Called when the circle is settled, before any series is projected. */
|
|
52
|
+
protected prepare(): void;
|
|
53
|
+
cyclic(axis: Axis): boolean;
|
|
54
|
+
/** On the plot when inside the rim in that direction, outside the hole, and (for a part of a turn) within the sweep. */
|
|
55
|
+
covers(x: number, y: number): boolean;
|
|
56
|
+
/** A radius is a ring (round, or a polygon on a radar); an angle is a spoke, which the plane's own way of placing gives. */
|
|
57
|
+
guide(role: 'first' | 'second', value: number): string;
|
|
58
|
+
/** The first coordinate is the angle, the second the radius. */
|
|
59
|
+
defaultAxis(role: 'first' | 'second'): Axis | undefined;
|
|
60
|
+
/** Pixels along an axis: what its ticks have to fit into. */
|
|
61
|
+
protected lengthOf(axis: Axis): number;
|
|
62
|
+
/** The fractions 0..1 of an axis' ticks that fall inside its range. */
|
|
63
|
+
protected tickFractions(axis: Axis | undefined): number[];
|
|
64
|
+
unplace(x: number, y: number, first: Axis, second: Axis): readonly [number, number];
|
|
65
|
+
place(points: PointBuffer, first: Axis, second: Axis): void;
|
|
66
|
+
onEvent(ev: ChartEvent): void;
|
|
67
|
+
/**
|
|
68
|
+
* Every axis gets the curve it runs along: the rim for the angle, a spoke at the start angle for the radius. The line
|
|
69
|
+
* is worked out when it is drawn, since a radar's depends on ticks that are only settled by then.
|
|
70
|
+
*/
|
|
71
|
+
private setRulers;
|
|
72
|
+
/**
|
|
73
|
+
* Same three passes as a Cartesian plane (estimate → fit → measure → final size → project), except that the plot is
|
|
74
|
+
* the largest circle in what is left, and the only margin that matters is what the angle axis' labels need around it.
|
|
75
|
+
*/
|
|
76
|
+
private arrange;
|
|
77
|
+
/** What the plot covers: the whole outline for a full turn (less the hole), a sector for part of one. */
|
|
78
|
+
private region;
|
|
79
|
+
/** Clip region, then spokes at the angle axis' ticks and rings at the radius axis' ticks. */
|
|
80
|
+
private paint;
|
|
81
|
+
}
|
|
82
|
+
/** No options of its own beyond the shared radial ones. */
|
|
83
|
+
export interface PolarPlaneAttributes extends RadialPlaneAttributes {
|
|
84
|
+
}
|
|
85
|
+
/** A continuous plane: the angle is an angle, so lines of constant value are circles. */
|
|
86
|
+
export declare class PolarPlane extends RadialPlane<PolarPlaneAttributes> {
|
|
87
|
+
protected locate(fraction: number, r: number, out: Float64Array): void;
|
|
88
|
+
protected outerRadius(): number;
|
|
89
|
+
protected angleFraction(dx: number, dy: number): number;
|
|
90
|
+
protected ring(r: number, reverse?: boolean): string;
|
|
91
|
+
}
|
|
92
|
+
/** No options of its own beyond the shared radial ones. */
|
|
93
|
+
export interface RadarPlaneAttributes extends RadialPlaneAttributes {
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A plane with a spoke at every tick of the angle axis, the ends of the sweep too, and straight lines between them.
|
|
97
|
+
* Between two spokes a point is on the chord between where its radius is on each, so a ring is a polygon, a line
|
|
98
|
+
* through dense data follows the same chords, and an `AreaPath` fills a polygon without being told it is one.
|
|
99
|
+
*
|
|
100
|
+
* Give the angle axis a fixed `min` and `max` and a `tickCount` that puts a tick on each category (and `nice` off,
|
|
101
|
+
* or on only for integers); auto-ranging works, but the spokes move as the data does. A full turn ends where it began,
|
|
102
|
+
* so a closed shape needs the first point repeated at the end of the domain.
|
|
103
|
+
*/
|
|
104
|
+
export declare class RadarPlane extends RadialPlane<RadarPlaneAttributes> {
|
|
105
|
+
#private;
|
|
106
|
+
protected prepare(): void;
|
|
107
|
+
/** The spokes: a line along the angle axis bends at each. */
|
|
108
|
+
breaks(axis: Axis): readonly number[];
|
|
109
|
+
protected locate(fraction: number, r: number, out: Float64Array): void;
|
|
110
|
+
/** The wedge between two spokes that the direction (dx, dy) points into: the spoke before it, and how far round the sweep it is. */
|
|
111
|
+
private wedge;
|
|
112
|
+
protected outerRadius(dx: number, dy: number): number;
|
|
113
|
+
/** Where along the chord the pointer's ray crosses it, mapped to the fractions of the two spokes (not linear in the angle). */
|
|
114
|
+
protected angleFraction(dx: number, dy: number): number;
|
|
115
|
+
protected ring(r: number, reverse?: boolean): string;
|
|
116
|
+
}
|