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/input.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { EVENT_FOCUS, EVENT_HOVER, EVENT_KEY, EVENT_POINTER } from './events.js';
|
|
2
|
+
import { contains } from './geometry.js';
|
|
3
|
+
import { Plane } from './plane.js';
|
|
4
|
+
export class InputHost {
|
|
5
|
+
/** True while the last pointer input came from a finger, which hits use to be more forgiving. */
|
|
6
|
+
coarse = false;
|
|
7
|
+
#chart;
|
|
8
|
+
#subscriptions = [];
|
|
9
|
+
// Pointer capture: the plane a drag started on keeps receiving events even when the pointer leaves it.
|
|
10
|
+
#pressed = null;
|
|
11
|
+
#hovered = null;
|
|
12
|
+
// Elements that want HOVER events: the tree walk to deliver one is only made when someone asked.
|
|
13
|
+
#pointerWatchers = new Set();
|
|
14
|
+
#inputWatchers = new Set();
|
|
15
|
+
// Which element owns each focusable node, so keys and focus changes reach the right one.
|
|
16
|
+
#focusOwners = new WeakMap();
|
|
17
|
+
constructor(chart) { this.#chart = chart; }
|
|
18
|
+
/** Listen to the chart's platform. Called by `inputOf`; a platform with no input of a kind simply gives none. */
|
|
19
|
+
start() {
|
|
20
|
+
const platform = this.#chart.platform;
|
|
21
|
+
if (!platform || this.#subscriptions.length)
|
|
22
|
+
return;
|
|
23
|
+
for (const u of [
|
|
24
|
+
platform.onPointer?.((p) => this.route(p)),
|
|
25
|
+
platform.onKey?.((k, node) => this.routeKey(k, node)),
|
|
26
|
+
platform.onFocus?.((f) => this.routeFocus(f)),
|
|
27
|
+
])
|
|
28
|
+
if (u)
|
|
29
|
+
this.#subscriptions.push(u);
|
|
30
|
+
}
|
|
31
|
+
/** Stop listening and forget everything held: `Chart.detach()` calls it. */
|
|
32
|
+
stop() {
|
|
33
|
+
for (const u of this.#subscriptions.splice(0))
|
|
34
|
+
u();
|
|
35
|
+
this.#pressed = this.#hovered = null;
|
|
36
|
+
this.#pointerWatchers.clear();
|
|
37
|
+
this.#inputWatchers.clear();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Ask for HOVER events (where the pointer is in a plane) for as long as the returned function has not been called.
|
|
41
|
+
* Elements outside the plane that follow the pointer (a tooltip) subscribe when they attach.
|
|
42
|
+
*/
|
|
43
|
+
watchPointer(element) {
|
|
44
|
+
this.#pointerWatchers.add(element);
|
|
45
|
+
return () => { this.#pointerWatchers.delete(element); };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Ask for POINTER events in the chart's own pixels (not a plane's), for elements that are not in a plane and take
|
|
49
|
+
* input of their own: a legend's entries. Set `handled` on one to keep the planes from also acting on it. Returns the
|
|
50
|
+
* way to stop.
|
|
51
|
+
*/
|
|
52
|
+
watchInput(element) {
|
|
53
|
+
this.#inputWatchers.add(element);
|
|
54
|
+
return () => { this.#inputWatchers.delete(element); };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Make a node reachable by keyboard: give it a `tabindex` (0 for a place in the tab order, -1 to be focusable by script
|
|
58
|
+
* only) and say which element it belongs to, which then gets its KEY and FOCUS events. Call again with the same node
|
|
59
|
+
* freely.
|
|
60
|
+
*/
|
|
61
|
+
focusable(element, node) {
|
|
62
|
+
this.#focusOwners.set(node, element);
|
|
63
|
+
}
|
|
64
|
+
/** Say something to a screen reader, if the platform has one to tell. */
|
|
65
|
+
speak(text) { this.#chart.platform?.announce?.(text); }
|
|
66
|
+
/** Move keyboard focus to a node an element made `focusable`. Does nothing on a platform without focus. */
|
|
67
|
+
focus(node) { this.#chart.platform?.focus?.(node); }
|
|
68
|
+
/** One raw pointer: to what listens outside planes first, then to the plane it is over. True when something used it. */
|
|
69
|
+
route(p) {
|
|
70
|
+
const chart = this.#chart;
|
|
71
|
+
if (p.kind !== 'leave' && p.kind !== 'wheel' && p.kind !== 'pinch')
|
|
72
|
+
this.coarse = p.pointerType === 'touch';
|
|
73
|
+
// Whatever outside a plane listens (a legend) is told first, in the chart's own pixels; an element that uses the input keeps it from the planes.
|
|
74
|
+
let taken = false;
|
|
75
|
+
for (const w of this.#inputWatchers) {
|
|
76
|
+
if (w.chart !== chart) {
|
|
77
|
+
this.#inputWatchers.delete(w);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- `deliver` returns the event union; a pointer event comes back as one
|
|
81
|
+
const ev = chart.deliver({
|
|
82
|
+
type: EVENT_POINTER, kind: p.kind, x: p.x, y: p.y, buttons: p.buttons, deltaY: p.deltaY, deltaX: p.deltaX ?? 0, scale: p.scale ?? 1,
|
|
83
|
+
id: p.id ?? 0, pointerType: p.pointerType ?? 'mouse', axis: null, outside: false, shift: p.shift, ctrl: p.ctrl, handled: false,
|
|
84
|
+
}, w);
|
|
85
|
+
if (ev.handled)
|
|
86
|
+
taken = true;
|
|
87
|
+
}
|
|
88
|
+
if (taken)
|
|
89
|
+
return true;
|
|
90
|
+
let at = this.#pressed;
|
|
91
|
+
// A pointer reaches the plane it is over: its plot, or the strip an axis takes beside it.
|
|
92
|
+
if (!at && p.kind !== 'leave') {
|
|
93
|
+
for (const c of chart) {
|
|
94
|
+
if (!(c instanceof Plane))
|
|
95
|
+
continue;
|
|
96
|
+
if (contains(c.rect, p.x, p.y) || c.axisAt(p.x - c.rect.x, p.y - c.rect.y)) {
|
|
97
|
+
at = c;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const send = (plane, kind) => {
|
|
103
|
+
const r = plane.rect, x = p.x - r.x, y = p.y - r.y;
|
|
104
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- `deliver` returns the event union; a pointer event comes back as one
|
|
105
|
+
return chart.deliver({
|
|
106
|
+
type: EVENT_POINTER, kind, x, y, buttons: p.buttons, deltaY: p.deltaY, deltaX: p.deltaX ?? 0, scale: p.scale ?? 1,
|
|
107
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- `axisAt` returns an element, and the axis it found is an `Axis` or null
|
|
108
|
+
id: p.id ?? 0, pointerType: p.pointerType ?? 'mouse', axis: kind === 'leave' ? null : plane.axisAt(x, y), outside: kind !== 'leave' && !contains(r, p.x, p.y),
|
|
109
|
+
shift: p.shift, ctrl: p.ctrl, handled: false,
|
|
110
|
+
}, plane).handled;
|
|
111
|
+
};
|
|
112
|
+
if (this.#hovered && this.#hovered !== at) {
|
|
113
|
+
send(this.#hovered, 'leave');
|
|
114
|
+
this.#announce(this.#hovered, NaN, NaN, 0);
|
|
115
|
+
}
|
|
116
|
+
this.#hovered = p.kind === 'leave' ? null : at;
|
|
117
|
+
if (!at)
|
|
118
|
+
return false;
|
|
119
|
+
if (p.kind === 'down')
|
|
120
|
+
this.#pressed = at;
|
|
121
|
+
const handled = p.kind === 'leave' ? false : send(at, p.kind);
|
|
122
|
+
if (p.kind === 'move' || p.kind === 'down' || p.kind === 'up') {
|
|
123
|
+
// Over an axis the pointer is not on the plot, so what follows the pointer over the plot (a tooltip) is told it has left.
|
|
124
|
+
const away = !contains(at.rect, p.x, p.y);
|
|
125
|
+
this.#announce(at, away ? NaN : p.x - at.rect.x, away ? NaN : p.y - at.rect.y, away ? 0 : p.buttons);
|
|
126
|
+
}
|
|
127
|
+
if (p.kind === 'up')
|
|
128
|
+
this.#pressed = null;
|
|
129
|
+
return handled;
|
|
130
|
+
}
|
|
131
|
+
/** A key on a focused node, to the element that made the node focusable. */
|
|
132
|
+
routeKey(k, node) {
|
|
133
|
+
const owner = this.#focusOwners.get(node);
|
|
134
|
+
if (!owner || owner.chart !== this.#chart)
|
|
135
|
+
return false;
|
|
136
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- `deliver` returns the event union; a key event comes back as one
|
|
137
|
+
return this.#chart.deliver({
|
|
138
|
+
type: EVENT_KEY, kind: k.kind, key: k.key, shift: k.shift, ctrl: k.ctrl, alt: k.alt, repeat: k.repeat, node, handled: false,
|
|
139
|
+
}, owner).handled;
|
|
140
|
+
}
|
|
141
|
+
/** Focus entering or leaving a node, to its owner. */
|
|
142
|
+
routeFocus(f) {
|
|
143
|
+
const owner = this.#focusOwners.get(f.node);
|
|
144
|
+
if (!owner || owner.chart !== this.#chart)
|
|
145
|
+
return;
|
|
146
|
+
this.#chart.deliver({ type: EVENT_FOCUS, node: f.node, focused: f.focused, visible: f.visible }, owner);
|
|
147
|
+
}
|
|
148
|
+
/** Tell every element where the pointer is in `plane` (NaN: it has gone). */
|
|
149
|
+
#announce(plane, x, y, buttons) {
|
|
150
|
+
for (const w of this.#pointerWatchers)
|
|
151
|
+
if (w.chart !== this.#chart)
|
|
152
|
+
this.#pointerWatchers.delete(w); // taken out of the tree
|
|
153
|
+
if (this.#pointerWatchers.size === 0)
|
|
154
|
+
return;
|
|
155
|
+
this.#chart.deliver({ type: EVENT_HOVER, plane, x, y, buttons });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/** The chart's input host, made and started on the first ask. Elements that take input call this as they attach. */
|
|
159
|
+
export function inputOf(chart) {
|
|
160
|
+
let host = chart.input;
|
|
161
|
+
if (!host) {
|
|
162
|
+
host = chart.input = new InputHost(chart);
|
|
163
|
+
host.start();
|
|
164
|
+
}
|
|
165
|
+
return host;
|
|
166
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnyElement } from './element.ts';
|
|
2
|
+
import { type JsxType } from './jsx-runtime.ts';
|
|
3
|
+
export { Fragment, type JSX, type JsxType, jsxs } from './jsx-runtime.ts';
|
|
4
|
+
export interface JsxSource {
|
|
5
|
+
fileName?: string;
|
|
6
|
+
lineNumber?: number;
|
|
7
|
+
columnNumber?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function jsxDEV(type: JsxType, props: Record<string, unknown>, key?: unknown, _isStatic?: boolean, source?: JsxSource): AnyElement;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx } from './jsx-runtime.js';
|
|
2
|
+
export { Fragment, jsxs } from './jsx-runtime.js';
|
|
3
|
+
export function jsxDEV(type, props, key, _isStatic, source) {
|
|
4
|
+
try {
|
|
5
|
+
return jsx(type, props, key);
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
if (error instanceof Error && source?.fileName && !error.message.includes(source.fileName)) {
|
|
9
|
+
error.message += ` (${source.fileName}:${source.lineNumber}:${source.columnNumber})`;
|
|
10
|
+
}
|
|
11
|
+
throw error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A JSX runtime for building charts as element trees: use it with `jsxImportSource` and write
|
|
3
|
+
*
|
|
4
|
+
* /** @jsxImportSource inkplot *\/
|
|
5
|
+
* const chart = (
|
|
6
|
+
* <Chart theme={lightTheme}>
|
|
7
|
+
* <CartesianPlane>
|
|
8
|
+
* <Axis meta-title="Time"><LinearScale /></Axis>
|
|
9
|
+
* <DomainSeries><ObjectDataSource source={rows} domain="t" value="v" /><LinePath><LinearCurve /></LinePath></DomainSeries>
|
|
10
|
+
* </CartesianPlane>
|
|
11
|
+
* </Chart>
|
|
12
|
+
* );
|
|
13
|
+
*
|
|
14
|
+
* Every tag makes the element at once, so the result is the same `Chart` you would have written with `new`, and can
|
|
15
|
+
* be attached to a platform, serialized, or handed to the React adapter. Runtimes can be mixed per file.
|
|
16
|
+
*
|
|
17
|
+
* - Attribute names with hyphens are nested: `meta-title="x"` is `meta={{ title: 'x' }}` (see attrs.ts).
|
|
18
|
+
* - Children go to the constructor in order. A parent's fixed children (an axis' scale, a series' data) come first and in
|
|
19
|
+
* the order its documentation gives; they cannot be checked by the types, because JSX does not type children one by one.
|
|
20
|
+
* `null`, `undefined` and `false` (a skipped `{cond && <X />}`) are dropped, so a fixed child cannot be skipped.
|
|
21
|
+
* - `ref` receives the element (a function, or an object with `current`), to keep hold of it.
|
|
22
|
+
* - A function can stand in for a tag to make a reusable piece: it is called with the props, `children` included, and returns an element.
|
|
23
|
+
* - The text of a tag (`<ChartTitle>Sales</ChartTitle>`) is not supported: write `text="Sales"`.
|
|
24
|
+
*/
|
|
25
|
+
import { type AnyArgs, type AnyElement } from './element.ts';
|
|
26
|
+
import { type JsxRef } from './attrs.ts';
|
|
27
|
+
type Props = Record<string, unknown>;
|
|
28
|
+
type ElementConstructor = abstract new (...args: AnyArgs) => AnyElement;
|
|
29
|
+
/** A tag: a chart element class, or a function making an element (or several, as a fragment). */
|
|
30
|
+
export type JsxType = ElementConstructor | ((props: never) => AnyElement | readonly AnyElement[]);
|
|
31
|
+
/** The JSX factory: one call per tag, with the children already made. `key` has no meaning for a retained tree and is ignored. */
|
|
32
|
+
export declare function jsx(type: JsxType, props: Props, _key?: unknown): AnyElement;
|
|
33
|
+
/** Several children given as a list of literals rather than written out: the same as `jsx`. */
|
|
34
|
+
export declare const jsxs: typeof jsx;
|
|
35
|
+
/** `<>…</>`: the children, so that a function tag or a parent can take several elements at once. */
|
|
36
|
+
export declare function Fragment(props: {
|
|
37
|
+
children?: unknown;
|
|
38
|
+
}): AnyElement[];
|
|
39
|
+
/** The types TypeScript reads to check JSX. A tag is a chart element class; `$props` on it says what it takes. */
|
|
40
|
+
export declare namespace JSX {
|
|
41
|
+
type Element = AnyElement;
|
|
42
|
+
interface ElementClass extends AnyElement {
|
|
43
|
+
}
|
|
44
|
+
interface ElementAttributesProperty {
|
|
45
|
+
$props: {};
|
|
46
|
+
}
|
|
47
|
+
interface ElementChildrenAttribute {
|
|
48
|
+
children: {};
|
|
49
|
+
}
|
|
50
|
+
interface IntrinsicElements {
|
|
51
|
+
}
|
|
52
|
+
interface IntrinsicAttributes {
|
|
53
|
+
key?: string | number | null;
|
|
54
|
+
}
|
|
55
|
+
interface IntrinsicClassAttributes<T> {
|
|
56
|
+
ref?: JsxRef<T>;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A JSX runtime for building charts as element trees: use it with `jsxImportSource` and write
|
|
3
|
+
*
|
|
4
|
+
* /** @jsxImportSource inkplot *\/
|
|
5
|
+
* const chart = (
|
|
6
|
+
* <Chart theme={lightTheme}>
|
|
7
|
+
* <CartesianPlane>
|
|
8
|
+
* <Axis meta-title="Time"><LinearScale /></Axis>
|
|
9
|
+
* <DomainSeries><ObjectDataSource source={rows} domain="t" value="v" /><LinePath><LinearCurve /></LinePath></DomainSeries>
|
|
10
|
+
* </CartesianPlane>
|
|
11
|
+
* </Chart>
|
|
12
|
+
* );
|
|
13
|
+
*
|
|
14
|
+
* Every tag makes the element at once, so the result is the same `Chart` you would have written with `new`, and can
|
|
15
|
+
* be attached to a platform, serialized, or handed to the React adapter. Runtimes can be mixed per file.
|
|
16
|
+
*
|
|
17
|
+
* - Attribute names with hyphens are nested: `meta-title="x"` is `meta={{ title: 'x' }}` (see attrs.ts).
|
|
18
|
+
* - Children go to the constructor in order. A parent's fixed children (an axis' scale, a series' data) come first and in
|
|
19
|
+
* the order its documentation gives; they cannot be checked by the types, because JSX does not type children one by one.
|
|
20
|
+
* `null`, `undefined` and `false` (a skipped `{cond && <X />}`) are dropped, so a fixed child cannot be skipped.
|
|
21
|
+
* - `ref` receives the element (a function, or an object with `current`), to keep hold of it.
|
|
22
|
+
* - A function can stand in for a tag to make a reusable piece: it is called with the props, `children` included, and returns an element.
|
|
23
|
+
* - The text of a tag (`<ChartTitle>Sales</ChartTitle>`) is not supported: write `text="Sales"`.
|
|
24
|
+
*/
|
|
25
|
+
import { ChartElement } from './element.js';
|
|
26
|
+
import { expandAttributes } from './attrs.js';
|
|
27
|
+
function flatten(children, into, tag) {
|
|
28
|
+
if (children == null || children === false)
|
|
29
|
+
return into;
|
|
30
|
+
if (Array.isArray(children)) {
|
|
31
|
+
for (const c of children)
|
|
32
|
+
flatten(c, into, tag);
|
|
33
|
+
}
|
|
34
|
+
else if (children instanceof ChartElement) {
|
|
35
|
+
into.push(children);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
throw new TypeError(`<${tag}>: a child must be a chart element, not ${typeof children === 'object' ? 'an object' : `${typeof children} ${JSON.stringify(children)}`}`);
|
|
39
|
+
}
|
|
40
|
+
return into;
|
|
41
|
+
}
|
|
42
|
+
const nameOf = (type) => type.name || 'anonymous';
|
|
43
|
+
/** The JSX factory: one call per tag, with the children already made. `key` has no meaning for a retained tree and is ignored. */
|
|
44
|
+
export function jsx(type, props, _key) {
|
|
45
|
+
const { children, ref, ...rest } = props;
|
|
46
|
+
const tag = nameOf(type);
|
|
47
|
+
let element;
|
|
48
|
+
const isClass = type.prototype instanceof ChartElement;
|
|
49
|
+
if (isClass) {
|
|
50
|
+
const attrs = expandAttributes(rest);
|
|
51
|
+
for (const k of Object.keys(attrs))
|
|
52
|
+
if (attrs[k] === undefined)
|
|
53
|
+
delete attrs[k];
|
|
54
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- JSX tags are either classes or function components: a class is told apart by `isClass` above
|
|
55
|
+
element = new type(attrs, ...flatten(children, [], tag));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- JSX tags are either classes or function components: a function is told apart by `isClass` above
|
|
59
|
+
const made = type(children === undefined ? rest : { ...rest, children });
|
|
60
|
+
// A list (a fragment, or a function returning several elements) is passed on as it is; the parent flattens it.
|
|
61
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- a function component may return an array of elements, which `flatten` turns into one element
|
|
62
|
+
if (Array.isArray(made))
|
|
63
|
+
element = flatten(made, [], tag);
|
|
64
|
+
else if (made instanceof ChartElement)
|
|
65
|
+
element = made;
|
|
66
|
+
else
|
|
67
|
+
throw new TypeError(`<${tag}>: a function used as a tag must return a chart element`);
|
|
68
|
+
}
|
|
69
|
+
if (typeof ref === 'function')
|
|
70
|
+
ref(element);
|
|
71
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- a `ref` is an object with `current`, or a callback, which is handled on the line above
|
|
72
|
+
else if (ref)
|
|
73
|
+
ref.current = element;
|
|
74
|
+
return element;
|
|
75
|
+
}
|
|
76
|
+
/** Several children given as a list of literals rather than written out: the same as `jsx`. */
|
|
77
|
+
export const jsxs = jsx;
|
|
78
|
+
/** `<>…</>`: the children, so that a function tag or a parent can take several elements at once. */
|
|
79
|
+
export function Fragment(props) {
|
|
80
|
+
return flatten(props.children, [], 'Fragment');
|
|
81
|
+
}
|
package/dist/key.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Chart } from './chart.ts';
|
|
2
|
+
import { type BaseAttributes, ChartElement, type ElementRef } from './element.ts';
|
|
3
|
+
import { type ChartEvent } from './events.ts';
|
|
4
|
+
import { type Side } from './geometry.ts';
|
|
5
|
+
import type { ColorAxis, MappedDimension, SizeAxis } from './mapped.ts';
|
|
6
|
+
import type { PlatformNode } from './platform.ts';
|
|
7
|
+
export type KeyOrientation = 'vertical' | 'horizontal';
|
|
8
|
+
/** Options common to all keys. */
|
|
9
|
+
export interface KeyAttributes<Dimension extends MappedDimension<unknown>> extends BaseAttributes {
|
|
10
|
+
/** The dimension this key shows, or its id. A colour key takes a colour dimension, a size key a size one. */
|
|
11
|
+
dimension: ElementRef<Dimension>;
|
|
12
|
+
/** The edge of the chart it sits along. Default 'right'. */
|
|
13
|
+
side?: Side;
|
|
14
|
+
/** Which way its scale runs. Default: along its side, so vertical on the left and right, horizontal on the top and bottom. */
|
|
15
|
+
orientation?: KeyOrientation;
|
|
16
|
+
/** Default: the dimension's own title. */
|
|
17
|
+
title?: string;
|
|
18
|
+
/** Length of the scale in pixels. Default 140. */
|
|
19
|
+
length?: number;
|
|
20
|
+
/** Width of a vertical key: the scale and its labels. Default 72. */
|
|
21
|
+
breadth?: number;
|
|
22
|
+
}
|
|
23
|
+
/** Where things go in a key; passed to the drawing methods so they do not each recompute it. Everything is measured from the key's own top left corner. */
|
|
24
|
+
interface Layout {
|
|
25
|
+
vertical: boolean;
|
|
26
|
+
/** Pixels taken by the title. */
|
|
27
|
+
top: number;
|
|
28
|
+
length: number;
|
|
29
|
+
/** Room at the ends of the scale for what is drawn there, such as the biggest circle of a size key. */
|
|
30
|
+
inset: number;
|
|
31
|
+
/** Where a horizontal scale starts, leaving room for the first label to overhang. */
|
|
32
|
+
left: number;
|
|
33
|
+
}
|
|
34
|
+
/** A tick of a key: its label, how far along the dimension it is, and where that is on the scale. */
|
|
35
|
+
interface Tick {
|
|
36
|
+
text: string;
|
|
37
|
+
fraction: number;
|
|
38
|
+
at: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The shared frame of a key: reserves a strip along a chart edge, draws the title, and offers the scale-drawing helpers
|
|
42
|
+
* (`along`, `ticks`, `paintTicks`) that subclasses use. A subclass says only what its scale looks like.
|
|
43
|
+
*
|
|
44
|
+
* Layout: the chart asks the key how big it wants to be (MEASURE answers with `side`), carves a strip for it, and then
|
|
45
|
+
* arranges it into that strip. The key is a *chart* child rather than a plane's so one key can sit beside several planes.
|
|
46
|
+
*/
|
|
47
|
+
export declare abstract class Key<Dimension extends MappedDimension<unknown>, A extends KeyAttributes<Dimension> = KeyAttributes<Dimension>> extends ChartElement<Chart, [], A> {
|
|
48
|
+
#private;
|
|
49
|
+
static attrTypes: {
|
|
50
|
+
title: 1;
|
|
51
|
+
id: 1;
|
|
52
|
+
meta: {
|
|
53
|
+
title: 1;
|
|
54
|
+
description: 1;
|
|
55
|
+
role: 1;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
/** Room the scale needs past its ends. */
|
|
59
|
+
protected get inset(): number;
|
|
60
|
+
/** How thick the drawing is, across the scale: a colour bar, the biggest circle. */
|
|
61
|
+
protected abstract get thickness(): number;
|
|
62
|
+
/** How many ticks suit a scale this long, or undefined to leave it to the dimension. */
|
|
63
|
+
protected ticksFor(_length: number, _vertical: boolean): number | undefined;
|
|
64
|
+
/** Draw the scale, its ticks and labels. */
|
|
65
|
+
protected abstract paintScale(layout: Layout): void;
|
|
66
|
+
get dimension(): Dimension;
|
|
67
|
+
get side(): Side;
|
|
68
|
+
get vertical(): boolean;
|
|
69
|
+
private get title();
|
|
70
|
+
protected get length(): number;
|
|
71
|
+
/** The size of the labels, which the layout is built on. */
|
|
72
|
+
protected get fontSize(): number;
|
|
73
|
+
/** Put the key's line look (ticks, circles) on a node. */
|
|
74
|
+
protected strokeLine(node: PlatformNode): void;
|
|
75
|
+
onEvent(ev: ChartEvent): void;
|
|
76
|
+
/** Position along the scale, in key pixels, of a fraction 0..1 of the dimension. Low is at the bottom or the left. */
|
|
77
|
+
protected along(l: Layout, fraction: number): number;
|
|
78
|
+
/** The dimension's ticks, each with its fraction of the dimension and its place along the scale. Ticks outside the range are left out. */
|
|
79
|
+
protected ticks(l: Layout): Tick[];
|
|
80
|
+
/**
|
|
81
|
+
* Labels, and optionally a mark for each tick. `markAt` is the line across the scale the marks start from, and
|
|
82
|
+
* `labelAt` is where the labels go on it: their left edge on a vertical scale, their baseline on a horizontal one.
|
|
83
|
+
*/
|
|
84
|
+
protected paintTicks(l: Layout, ticks: readonly Tick[], markAt: number, labelAt: number, marks: boolean): void;
|
|
85
|
+
}
|
|
86
|
+
export interface ColorKeyAttributes extends KeyAttributes<ColorAxis> {
|
|
87
|
+
/** Thickness of the colour bar in pixels. Default 14. */
|
|
88
|
+
thickness?: number;
|
|
89
|
+
}
|
|
90
|
+
/** A bar of the dimension's colours, low to high, with its ticks. */
|
|
91
|
+
export declare class ColorKey extends Key<ColorAxis, ColorKeyAttributes> {
|
|
92
|
+
#private;
|
|
93
|
+
static attrTypes: {
|
|
94
|
+
title: 1;
|
|
95
|
+
dimension: 1;
|
|
96
|
+
id: 1;
|
|
97
|
+
meta: {
|
|
98
|
+
title: 1;
|
|
99
|
+
description: 1;
|
|
100
|
+
role: 1;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
private readonly gradientId;
|
|
104
|
+
protected get thickness(): number;
|
|
105
|
+
protected paintScale(l: Layout): void;
|
|
106
|
+
}
|
|
107
|
+
/** No options of its own beyond the common ones. */
|
|
108
|
+
export interface SizeKeyAttributes extends KeyAttributes<SizeAxis> {
|
|
109
|
+
}
|
|
110
|
+
/** Circles at the dimension's tick values, each as big as the size it stands for. */
|
|
111
|
+
export declare class SizeKey extends Key<SizeAxis, SizeKeyAttributes> {
|
|
112
|
+
protected get inset(): number;
|
|
113
|
+
protected get thickness(): number;
|
|
114
|
+
/** Circles as big as the biggest need room: no more ticks than fit without piling up. */
|
|
115
|
+
protected ticksFor(length: number): number;
|
|
116
|
+
protected paintScale(l: Layout): void;
|
|
117
|
+
}
|
|
118
|
+
export {};
|
package/dist/key.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys: what a mapped dimension means, drawn beside the plots.
|
|
3
|
+
*
|
|
4
|
+
* A key is an element of its own, a child of the chart that refers to the dimension it shows:
|
|
5
|
+
*
|
|
6
|
+
* new Chart({}, plane, heat, new ColorKey({ dimension: heat, title: 'Pressure' }))
|
|
7
|
+
*
|
|
8
|
+
* The dimension says what the colours are (ramp, scale, range); the key says where and how to draw them (side,
|
|
9
|
+
* orientation, length). Different keys can show one dimension, and a dimension needs none.
|
|
10
|
+
*/
|
|
11
|
+
import { STRING } from './attrs.js';
|
|
12
|
+
import { ChartElement, resolveRef } from './element.js';
|
|
13
|
+
import { EVENT_ARRANGE, EVENT_ATTACH, EVENT_MEASURE, EVENT_PAINT } from './events.js';
|
|
14
|
+
import { circlePath, f2 } from './geometry.js';
|
|
15
|
+
import { applyText } from './text.js';
|
|
16
|
+
// Fixed metrics in pixels: keys are small furniture, so these are constants rather than options.
|
|
17
|
+
const TITLE_BLOCK = 18;
|
|
18
|
+
const LABEL_OVERHANG = 14;
|
|
19
|
+
const round2 = (n) => Math.round(n * 100) / 100;
|
|
20
|
+
/**
|
|
21
|
+
* The shared frame of a key: reserves a strip along a chart edge, draws the title, and offers the scale-drawing helpers
|
|
22
|
+
* (`along`, `ticks`, `paintTicks`) that subclasses use. A subclass says only what its scale looks like.
|
|
23
|
+
*
|
|
24
|
+
* Layout: the chart asks the key how big it wants to be (MEASURE answers with `side`), carves a strip for it, and then
|
|
25
|
+
* arranges it into that strip. The key is a *chart* child rather than a plane's so one key can sit beside several planes.
|
|
26
|
+
*/
|
|
27
|
+
export class Key extends ChartElement {
|
|
28
|
+
static attrTypes = { ...ChartElement.attrTypes, title: STRING };
|
|
29
|
+
/** Room the scale needs past its ends. */
|
|
30
|
+
get inset() { return 0; }
|
|
31
|
+
/** How many ticks suit a scale this long, or undefined to leave it to the dimension. */
|
|
32
|
+
ticksFor(_length, _vertical) { return undefined; }
|
|
33
|
+
#rect = { x: 0, y: 0, width: 0, height: 0 };
|
|
34
|
+
#labels = 0;
|
|
35
|
+
// oxlint-disable-next-line typescript/consistent-type-assertions -- `resolveRef` returns an element; the attribute type says it is a dimension
|
|
36
|
+
get dimension() { return resolveRef(this.attrs.dimension, this); }
|
|
37
|
+
get side() { return this.attrs.side ?? 'right'; }
|
|
38
|
+
get vertical() {
|
|
39
|
+
return (this.attrs.orientation ?? (this.side === 'left' || this.side === 'right' ? 'vertical' : 'horizontal')) === 'vertical';
|
|
40
|
+
}
|
|
41
|
+
get title() { return this.attrs.title ?? this.dimension.attrs.title; }
|
|
42
|
+
get length() { return this.attrs.length ?? 140; }
|
|
43
|
+
/** The size of the labels, which the layout is built on. */
|
|
44
|
+
get fontSize() { return this.styleFor(['keyLabel', 'key', 'text']).size; }
|
|
45
|
+
/** Put the key's line look (ticks, circles) on a node. */
|
|
46
|
+
strokeLine(node) {
|
|
47
|
+
const look = this.styleFor(['keyLine', 'key', 'line']), s = this.surface;
|
|
48
|
+
s.set(node, 'stroke', look.color);
|
|
49
|
+
s.set(node, 'stroke-width', look.width);
|
|
50
|
+
s.set(node, 'opacity', look.opacity);
|
|
51
|
+
}
|
|
52
|
+
onEvent(ev) {
|
|
53
|
+
switch (ev.type) {
|
|
54
|
+
case EVENT_ATTACH:
|
|
55
|
+
this.surface.wrapper();
|
|
56
|
+
break;
|
|
57
|
+
case EVENT_MEASURE: {
|
|
58
|
+
const title = this.title ? TITLE_BLOCK : 0, length = this.length, inset = this.inset;
|
|
59
|
+
const size = this.vertical
|
|
60
|
+
? { width: this.attrs.breadth ?? 72, height: title + 2 * inset + length + 12 }
|
|
61
|
+
: { width: LABEL_OVERHANG + 2 * inset + length + LABEL_OVERHANG, height: title + this.thickness + 4 + this.fontSize * 1.4 };
|
|
62
|
+
ev.request = { min: size, max: size, side: this.side };
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case EVENT_ARRANGE:
|
|
66
|
+
this.#rect = ev.rect;
|
|
67
|
+
break;
|
|
68
|
+
case EVENT_PAINT: {
|
|
69
|
+
const s = this.surface, root = s.wrapper(), vertical = this.vertical, title = this.title;
|
|
70
|
+
s.set(root, 'transform', `translate(${f2(this.#rect.x)} ${f2(this.#rect.y)})`);
|
|
71
|
+
applyText(s, root, this.styleFor(['keyLabel', 'key', 'text']), vertical ? 'start' : 'middle');
|
|
72
|
+
if (title) {
|
|
73
|
+
const t = s.node('title', 'text');
|
|
74
|
+
applyText(s, t, this.styleFor(['keyTitle', 'key', 'text']), 'start');
|
|
75
|
+
s.set(t, 'y', this.fontSize);
|
|
76
|
+
s.text(t, title);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
s.remove('title');
|
|
80
|
+
}
|
|
81
|
+
this.paintScale({ vertical, top: title ? TITLE_BLOCK : 0, length: this.length, inset: this.inset, left: LABEL_OVERHANG });
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Position along the scale, in key pixels, of a fraction 0..1 of the dimension. Low is at the bottom or the left. */
|
|
87
|
+
along(l, fraction) {
|
|
88
|
+
return l.vertical ? l.top + l.inset + (1 - fraction) * l.length : l.left + l.inset + fraction * l.length;
|
|
89
|
+
}
|
|
90
|
+
/** The dimension's ticks, each with its fraction of the dimension and its place along the scale. Ticks outside the range are left out. */
|
|
91
|
+
ticks(l) {
|
|
92
|
+
const dim = this.dimension, out = [];
|
|
93
|
+
for (const t of dim.getTicks(l.length, this.ticksFor(l.length, l.vertical))) {
|
|
94
|
+
const fraction = dim.fractionOf(t.value);
|
|
95
|
+
if (fraction >= -1e-9 && fraction <= 1 + 1e-9)
|
|
96
|
+
out.push({ text: t.label, fraction, at: round2(this.along(l, fraction)) });
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Labels, and optionally a mark for each tick. `markAt` is the line across the scale the marks start from, and
|
|
102
|
+
* `labelAt` is where the labels go on it: their left edge on a vertical scale, their baseline on a horizontal one.
|
|
103
|
+
*/
|
|
104
|
+
paintTicks(l, ticks, markAt, labelAt, marks) {
|
|
105
|
+
const s = this.surface;
|
|
106
|
+
if (marks) {
|
|
107
|
+
let d = '';
|
|
108
|
+
for (const t of ticks)
|
|
109
|
+
d += l.vertical ? `M${markAt} ${t.at}h3` : `M${t.at} ${markAt}v3`;
|
|
110
|
+
const node = s.node('ticks', 'path');
|
|
111
|
+
s.set(node, 'd', d);
|
|
112
|
+
this.strokeLine(node);
|
|
113
|
+
s.set(node, 'fill', 'none');
|
|
114
|
+
}
|
|
115
|
+
ticks.forEach((t, i) => {
|
|
116
|
+
const n = s.node(`label-${i}`, 'text');
|
|
117
|
+
s.set(n, 'x', l.vertical ? labelAt : t.at);
|
|
118
|
+
s.set(n, 'y', l.vertical ? round2(t.at + this.fontSize * 0.35) : labelAt);
|
|
119
|
+
s.text(n, t.text);
|
|
120
|
+
});
|
|
121
|
+
for (let i = ticks.length; i < this.#labels; i++)
|
|
122
|
+
s.remove(`label-${i}`);
|
|
123
|
+
this.#labels = ticks.length;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Module-level so gradient ids stay unique across keys and charts on one page.
|
|
127
|
+
let gradientCounter = 0;
|
|
128
|
+
/** A bar of the dimension's colours, low to high, with its ticks. */
|
|
129
|
+
export class ColorKey extends Key {
|
|
130
|
+
static attrTypes = { ...Key.attrTypes, dimension: STRING };
|
|
131
|
+
gradientId = `grad${++gradientCounter}`;
|
|
132
|
+
#stops = 0;
|
|
133
|
+
get thickness() { return this.attrs.thickness ?? 14; }
|
|
134
|
+
paintScale(l) {
|
|
135
|
+
const s = this.surface, dim = this.dimension, thickness = this.thickness;
|
|
136
|
+
// One rect filled by a gradient. A stepped dimension gets hard stops, so the bands stay bands.
|
|
137
|
+
const grad = s.node('gradient', 'linearGradient', { parent: s.node('defs', 'defs') });
|
|
138
|
+
s.set(grad, 'id', this.gradientId);
|
|
139
|
+
s.set(grad, 'x1', 0);
|
|
140
|
+
s.set(grad, 'y1', l.vertical ? 1 : 0);
|
|
141
|
+
s.set(grad, 'x2', l.vertical ? 0 : 1);
|
|
142
|
+
s.set(grad, 'y2', 0);
|
|
143
|
+
const stops = [];
|
|
144
|
+
if (dim.attrs.interpolation === 'step') {
|
|
145
|
+
const n = dim.stops.length;
|
|
146
|
+
// 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
|
|
147
|
+
for (let i = 0; i < n; i++) {
|
|
148
|
+
const c = dim.stops[i];
|
|
149
|
+
stops.push([i / n, c], [(i + 1) / n, c]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
const n = Math.max(2, Math.min(24, dim.stops.length * 2));
|
|
154
|
+
for (let i = 0; i < n; i++)
|
|
155
|
+
stops.push([i / (n - 1), dim.valueAt(i / (n - 1))]);
|
|
156
|
+
}
|
|
157
|
+
stops.forEach(([offset, color], i) => {
|
|
158
|
+
const n = s.node(`stop-${i}`, 'stop', { parent: grad });
|
|
159
|
+
s.set(n, 'offset', Math.round(offset * 1000) / 1000);
|
|
160
|
+
s.set(n, 'stop-color', color);
|
|
161
|
+
});
|
|
162
|
+
for (let i = stops.length; i < this.#stops; i++)
|
|
163
|
+
s.remove(`stop-${i}`);
|
|
164
|
+
this.#stops = stops.length;
|
|
165
|
+
const bar = s.node('bar', 'rect');
|
|
166
|
+
s.set(bar, 'x', l.vertical ? 0 : l.left);
|
|
167
|
+
s.set(bar, 'y', l.top);
|
|
168
|
+
s.set(bar, 'width', l.vertical ? thickness : l.length);
|
|
169
|
+
s.set(bar, 'height', l.vertical ? l.length : thickness);
|
|
170
|
+
s.set(bar, 'fill', `url(#${this.gradientId})`);
|
|
171
|
+
this.paintTicks(l, this.ticks(l), l.vertical ? thickness : l.top + thickness, l.vertical ? thickness + 6 : l.top + thickness + 3 + this.fontSize, true);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/** Circles at the dimension's tick values, each as big as the size it stands for. */
|
|
175
|
+
export class SizeKey extends Key {
|
|
176
|
+
get inset() { return this.dimension.output[1]; }
|
|
177
|
+
get thickness() { return 2 * this.dimension.output[1]; }
|
|
178
|
+
/** Circles as big as the biggest need room: no more ticks than fit without piling up. */
|
|
179
|
+
ticksFor(length) {
|
|
180
|
+
return Math.max(2, Math.floor(length / (2 * this.dimension.output[1] + 4)));
|
|
181
|
+
}
|
|
182
|
+
paintScale(l) {
|
|
183
|
+
const s = this.surface, dim = this.dimension, hi = dim.output[1];
|
|
184
|
+
const ticks = this.ticks(l);
|
|
185
|
+
// The circles sit on a line through their centres: down the key, or along it.
|
|
186
|
+
const centre = hi + 1;
|
|
187
|
+
let d = '';
|
|
188
|
+
for (const t of ticks) {
|
|
189
|
+
const r = dim.valueAt(t.fraction);
|
|
190
|
+
const [cx, cy] = l.vertical ? [centre, t.at] : [t.at, l.top + hi];
|
|
191
|
+
d += circlePath(cx, cy, r);
|
|
192
|
+
}
|
|
193
|
+
const circles = s.node('circles', 'path');
|
|
194
|
+
s.set(circles, 'd', d);
|
|
195
|
+
s.set(circles, 'fill', 'none');
|
|
196
|
+
this.strokeLine(circles);
|
|
197
|
+
this.paintTicks(l, ticks, 0, l.vertical ? centre + hi + 6 : l.top + 2 * hi + 3 + this.fontSize, false);
|
|
198
|
+
}
|
|
199
|
+
}
|