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/surface.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Fresh zeroed counters; the chart resets (rather than replaces) them each flush so surfaces keep a valid reference. */
|
|
2
|
+
export const newCounters = () => ({
|
|
3
|
+
writes: 0, skipped: 0, names: new Set(), filled: 0, cached: 0, projected: 0, pointsIn: 0, pointsDrawn: 0, pathBytes: 0,
|
|
4
|
+
stages: { generate: 0, decimate: 0, normalize: 0, place: 0, path: 0 },
|
|
5
|
+
});
|
|
6
|
+
/**
|
|
7
|
+
* What an element draws with. Nodes are keyed, created on demand, positioned from the live tree,
|
|
8
|
+
* and written only when a value changed. An element's own content sits below its children (or above, with `above`).
|
|
9
|
+
*
|
|
10
|
+
* An element has no node of its own unless it asks for a wrapper, which it should only do when the wrapper
|
|
11
|
+
* does something: a transform, a clip, or style its content inherits. Otherwise its nodes sit directly
|
|
12
|
+
* in the parent's, in order, with nothing to mark where they start.
|
|
13
|
+
*/
|
|
14
|
+
export class Surface {
|
|
15
|
+
platform;
|
|
16
|
+
counters;
|
|
17
|
+
placement;
|
|
18
|
+
wrapperNode = null;
|
|
19
|
+
hidden = false;
|
|
20
|
+
nodes = new Map();
|
|
21
|
+
cache = new WeakMap();
|
|
22
|
+
constructor(platform, counters, placement) {
|
|
23
|
+
this.platform = platform;
|
|
24
|
+
this.counters = counters;
|
|
25
|
+
this.placement = placement;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The element's own `<g>` (or another tag). Children and own nodes go inside it. Create it before the
|
|
29
|
+
* element draws anything, typically on attach; asking again returns the same node.
|
|
30
|
+
*/
|
|
31
|
+
wrapper(tag = 'g') {
|
|
32
|
+
if (!this.wrapperNode) {
|
|
33
|
+
if (this.nodes.size > 0 || this.placement.childrenFirst())
|
|
34
|
+
throw new Error('wrapper() must come before the element draws anything');
|
|
35
|
+
this.wrapperNode = this.platform.createNode(tag);
|
|
36
|
+
this.platform.insertBefore(this.placement.container(), this.wrapperNode, this.placement.end());
|
|
37
|
+
if (this.hidden)
|
|
38
|
+
this.set(this.wrapperNode, 'display', 'none');
|
|
39
|
+
}
|
|
40
|
+
return this.wrapperNode;
|
|
41
|
+
}
|
|
42
|
+
/** The wrapper, if the element has one. */
|
|
43
|
+
get wrapperRoot() { return this.wrapperNode; }
|
|
44
|
+
/** First own node below the children, if any. */
|
|
45
|
+
get firstBelow() { return this.firstOf(false); }
|
|
46
|
+
/** First own node above the children, if any. */
|
|
47
|
+
get firstAbove() { return this.firstOf(true); }
|
|
48
|
+
/**
|
|
49
|
+
* The node for `key`, created the first time and the same node afterwards. Keys are per element, so two elements
|
|
50
|
+
* can both use 'grid' without clashing.
|
|
51
|
+
*/
|
|
52
|
+
node(key, tag, options = {}) {
|
|
53
|
+
let entry = this.nodes.get(key);
|
|
54
|
+
if (entry)
|
|
55
|
+
return entry.node;
|
|
56
|
+
const node = this.platform.createNode(tag);
|
|
57
|
+
const above = options.above ?? false;
|
|
58
|
+
if (options.parent) {
|
|
59
|
+
this.platform.insertBefore(options.parent, node, null);
|
|
60
|
+
entry = { node, parent: options.parent, above };
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Inside a wrapper the range ends at the wrapper's end; without one it ends where the element does.
|
|
64
|
+
const end = this.wrapperNode ? null : this.placement.end();
|
|
65
|
+
const before = above ? end : (this.placement.childrenFirst() ?? this.firstAbove ?? end);
|
|
66
|
+
this.platform.insertBefore(this.wrapperNode ?? this.placement.container(), node, before);
|
|
67
|
+
entry = { node, parent: null, above };
|
|
68
|
+
if (this.hidden && !this.wrapperNode)
|
|
69
|
+
this.set(node, 'display', 'none');
|
|
70
|
+
}
|
|
71
|
+
this.nodes.set(key, entry);
|
|
72
|
+
return node;
|
|
73
|
+
}
|
|
74
|
+
/** Set an attribute if (and only if) it differs from what was last written. The platform cannot unset one, so use a neutral value ('none', 0) instead of removing. */
|
|
75
|
+
set(node, name, value) {
|
|
76
|
+
this.write(node, name, value, () => this.platform.setAttribute(node, name, value));
|
|
77
|
+
}
|
|
78
|
+
/** Set a text node's content, skipping unchanged writes like `set`. */
|
|
79
|
+
text(node, content) {
|
|
80
|
+
this.write(node, '#text', content, () => this.platform.setText(node, content));
|
|
81
|
+
}
|
|
82
|
+
/** Drop one keyed node, for content that comes and goes (a title, a clip path). */
|
|
83
|
+
remove(key) {
|
|
84
|
+
const entry = this.nodes.get(key);
|
|
85
|
+
if (!entry)
|
|
86
|
+
return;
|
|
87
|
+
this.nodes.delete(key);
|
|
88
|
+
this.platform.remove(entry.node);
|
|
89
|
+
}
|
|
90
|
+
/** Drop a keyed node and every node keyed beneath it (`key/…`), for a group made of several. */
|
|
91
|
+
removePrefix(key) {
|
|
92
|
+
this.remove(key);
|
|
93
|
+
const below = key + '/';
|
|
94
|
+
// oxlint-disable-next-line unicorn/no-useless-spread -- a snapshot: `remove` takes nodes out of the map while we walk it
|
|
95
|
+
for (const k of [...this.nodes.keys()])
|
|
96
|
+
if (k.startsWith(below))
|
|
97
|
+
this.remove(k);
|
|
98
|
+
}
|
|
99
|
+
/** Hide or show everything this element draws (the wrapper, or each top-level node if it has none). */
|
|
100
|
+
setHidden(hidden) {
|
|
101
|
+
this.hidden = hidden;
|
|
102
|
+
const display = hidden ? 'none' : 'inline';
|
|
103
|
+
if (this.wrapperNode) {
|
|
104
|
+
this.set(this.wrapperNode, 'display', display);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
for (const e of this.nodes.values())
|
|
108
|
+
if (!e.parent)
|
|
109
|
+
this.set(e.node, 'display', display);
|
|
110
|
+
}
|
|
111
|
+
/** Take this element's nodes out of the platform. */
|
|
112
|
+
dispose() {
|
|
113
|
+
if (this.wrapperNode)
|
|
114
|
+
this.platform.remove(this.wrapperNode);
|
|
115
|
+
else
|
|
116
|
+
for (const e of this.nodes.values())
|
|
117
|
+
if (!e.parent)
|
|
118
|
+
this.platform.remove(e.node);
|
|
119
|
+
this.nodes.clear();
|
|
120
|
+
this.wrapperNode = null;
|
|
121
|
+
}
|
|
122
|
+
/** The first top-level node of one layer; the layers are what `end()`/`first()` placement is computed from. */
|
|
123
|
+
firstOf(above) {
|
|
124
|
+
for (const e of this.nodes.values())
|
|
125
|
+
if (!e.parent && e.above === above)
|
|
126
|
+
return e.node;
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/** The write cache lives per node (a WeakMap) so a removed node's entries are collected with it. */
|
|
130
|
+
write(node, name, value, commit) {
|
|
131
|
+
let cached = this.cache.get(node);
|
|
132
|
+
if (!cached) {
|
|
133
|
+
cached = new Map();
|
|
134
|
+
this.cache.set(node, cached);
|
|
135
|
+
}
|
|
136
|
+
if (cached.get(name) === value) {
|
|
137
|
+
this.counters.skipped++;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
cached.set(name, value);
|
|
141
|
+
commit();
|
|
142
|
+
this.counters.writes++;
|
|
143
|
+
this.counters.names.add(name);
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/table.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data as a table, outside any chart: `readTable(node)` reads a source or a transform into columns.
|
|
3
|
+
*
|
|
4
|
+
* const t = readTable(boxStats); // domain, value, then every channel it offers
|
|
5
|
+
* t.rows(); // [{ domain: 0, value: 5, min: 1, q1: 3, … }, …]
|
|
6
|
+
* readTable(trend, { domain: { min: 0, max: 10 }, samples: 11 }); // a trendline or function, sampled where you say
|
|
7
|
+
*
|
|
8
|
+
* A source or transform is a function of nothing but its own attributes, so it needs no chart and no platform: the
|
|
9
|
+
* table is what a series of it would be given to draw, in the same order (ascending in the domain), before any scale,
|
|
10
|
+
* pixel or decimation. For data that is made up rather than stored (a `SyntheticDataSource`, a trendline) the rows are
|
|
11
|
+
* wherever you ask: `domain` is the span to cover and `samples` how many evenly spaced points. A `Decimator` is a
|
|
12
|
+
* transform like any other and reads as its thinned output, so read its source to get all the rows.
|
|
13
|
+
*/
|
|
14
|
+
import type { DataNode } from './data.ts';
|
|
15
|
+
import type { Extent } from './geometry.ts';
|
|
16
|
+
export interface TableOptions {
|
|
17
|
+
/** The channels to read, after the domain and the value. Default: every one the node offers (`channelNames`). */
|
|
18
|
+
channels?: readonly string[];
|
|
19
|
+
/** The part of the domain to read. Default: all of it. For generated data (a function, a trendline) it is the span sampled. */
|
|
20
|
+
domain?: Extent;
|
|
21
|
+
/** For generated data: how many evenly spaced points to make across the span (default: the node's own count, else 512). For a `Decimator`: the width in pixels it reduces to. */
|
|
22
|
+
samples?: number;
|
|
23
|
+
/** What to call the first two columns. Default `domain` and `value`. */
|
|
24
|
+
names?: {
|
|
25
|
+
domain?: string;
|
|
26
|
+
value?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Columns of numbers, all `count` long. NaN is a gap. */
|
|
30
|
+
export interface Table {
|
|
31
|
+
readonly columns: readonly string[];
|
|
32
|
+
readonly count: number;
|
|
33
|
+
/** One typed array for each of `columns`, in that order. */
|
|
34
|
+
readonly data: readonly Float64Array[];
|
|
35
|
+
column(name: string): Float64Array;
|
|
36
|
+
/** One object for each row, keyed by column name. */
|
|
37
|
+
rows(): Record<string, number>[];
|
|
38
|
+
}
|
|
39
|
+
export declare function readTable(node: DataNode, options?: TableOptions): Table;
|
package/dist/table.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export function readTable(node, options = {}) {
|
|
2
|
+
const channels = options.channels ?? (node.channelNames ?? []).filter((n) => n !== 'domain' && n !== 'value');
|
|
3
|
+
const context = {
|
|
4
|
+
width: options.samples ?? 0, height: 0, domainRange: options.domain ?? { min: -Infinity, max: Infinity }, channels,
|
|
5
|
+
};
|
|
6
|
+
const plan = node.plan(context), stride = 2 + channels.length;
|
|
7
|
+
const buffer = new Float64Array(plan.capacity * stride), { count } = plan.fill(buffer);
|
|
8
|
+
const columns = [options.names?.domain ?? 'domain', options.names?.value ?? 'value', ...channels];
|
|
9
|
+
const data = columns.map((_, c) => {
|
|
10
|
+
const out = new Float64Array(count);
|
|
11
|
+
// 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
|
|
12
|
+
for (let i = 0, j = c; i < count; i++, j += stride)
|
|
13
|
+
out[i] = buffer[j];
|
|
14
|
+
return out;
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
columns, count, data,
|
|
18
|
+
column(name) {
|
|
19
|
+
const at = columns.indexOf(name);
|
|
20
|
+
if (at < 0)
|
|
21
|
+
throw new Error(`readTable: no column "${name}" (have ${columns.join(', ')})`);
|
|
22
|
+
// 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
|
|
23
|
+
return data[at];
|
|
24
|
+
},
|
|
25
|
+
rows() {
|
|
26
|
+
const rows = [];
|
|
27
|
+
for (let i = 0; i < count; i++) {
|
|
28
|
+
const row = {};
|
|
29
|
+
// 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
|
|
30
|
+
for (let c = 0; c < columns.length; c++)
|
|
31
|
+
row[columns[c]] = data[c][i];
|
|
32
|
+
rows.push(row);
|
|
33
|
+
}
|
|
34
|
+
return rows;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Putting a resolved text look on a node. The look itself is a theme matter (`TextProps`, see theme.ts). */
|
|
2
|
+
import type { PlatformNode, TextMeasurer } from './platform.ts';
|
|
3
|
+
import type { Size } from './geometry.ts';
|
|
4
|
+
import type { Surface } from './surface.ts';
|
|
5
|
+
import type { TextProps } from './theme.ts';
|
|
6
|
+
/**
|
|
7
|
+
* Put a style on a node, so that the text inside it, and in nodes below it that say nothing of their own, follows it.
|
|
8
|
+
* The anchor is the style's `align` unless the caller places the text by its own geometry and says so: setting it twice
|
|
9
|
+
* would write it twice every flush.
|
|
10
|
+
*/
|
|
11
|
+
export declare function applyText(surface: Surface, node: PlatformNode, t: TextProps, anchor?: 'start' | 'middle' | 'end'): void;
|
|
12
|
+
/** Line height as a multiple of the font size. */
|
|
13
|
+
export declare const LINE_HEIGHT = 1.25;
|
|
14
|
+
/** The size of text that may span several lines (split at '\n'), and how many it has. */
|
|
15
|
+
export declare function measureBlock(measure: TextMeasurer, text: string, t: TextProps): Size & {
|
|
16
|
+
lines: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Draw text that may span lines, one node per line since the platform has no tspans. `x` is where each line is anchored
|
|
20
|
+
* (its own `text-anchor` says which end of it), `top` the top of the block. Lines left over from a longer text are
|
|
21
|
+
* removed; `previous` is how many there were, and the new count is returned for the next call.
|
|
22
|
+
*/
|
|
23
|
+
export declare function paintLines(surface: Surface, prefix: string, text: string, x: number, top: number, t: TextProps, previous: number, parent?: PlatformNode): number;
|
package/dist/text.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Put a style on a node, so that the text inside it, and in nodes below it that say nothing of their own, follows it.
|
|
3
|
+
* The anchor is the style's `align` unless the caller places the text by its own geometry and says so: setting it twice
|
|
4
|
+
* would write it twice every flush.
|
|
5
|
+
*/
|
|
6
|
+
export function applyText(surface, node, t, anchor = t.align) {
|
|
7
|
+
surface.set(node, 'font-size', t.size);
|
|
8
|
+
surface.set(node, 'fill', t.color);
|
|
9
|
+
surface.set(node, 'text-anchor', anchor);
|
|
10
|
+
surface.set(node, 'font-family', t.family);
|
|
11
|
+
surface.set(node, 'font-weight', t.weight);
|
|
12
|
+
surface.set(node, 'stroke', t.halo);
|
|
13
|
+
surface.set(node, 'stroke-width', t.haloWidth);
|
|
14
|
+
// Paint the halo (the stroke) first, so it sits behind the glyphs instead of eating into them.
|
|
15
|
+
surface.set(node, 'paint-order', 'stroke');
|
|
16
|
+
surface.set(node, 'stroke-linejoin', 'round');
|
|
17
|
+
}
|
|
18
|
+
/** Line height as a multiple of the font size. */
|
|
19
|
+
export const LINE_HEIGHT = 1.25;
|
|
20
|
+
/** The size of text that may span several lines (split at '\n'), and how many it has. */
|
|
21
|
+
export function measureBlock(measure, text, t) {
|
|
22
|
+
const lines = text.split('\n');
|
|
23
|
+
let width = 0;
|
|
24
|
+
for (const line of lines)
|
|
25
|
+
width = Math.max(width, measure(line, { family: t.family === 'inherit' ? 'sans-serif' : t.family, size: t.size, weight: t.weight }).width);
|
|
26
|
+
return { width, height: lines.length * t.size * LINE_HEIGHT, lines: lines.length };
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Draw text that may span lines, one node per line since the platform has no tspans. `x` is where each line is anchored
|
|
30
|
+
* (its own `text-anchor` says which end of it), `top` the top of the block. Lines left over from a longer text are
|
|
31
|
+
* removed; `previous` is how many there were, and the new count is returned for the next call.
|
|
32
|
+
*/
|
|
33
|
+
export function paintLines(surface, prefix, text, x, top, t, previous, parent) {
|
|
34
|
+
const lines = text.split('\n');
|
|
35
|
+
lines.forEach((line, i) => {
|
|
36
|
+
const n = surface.node(`${prefix}${i}`, 'text', parent ? { parent } : {});
|
|
37
|
+
surface.set(n, 'x', Math.round(x * 100) / 100);
|
|
38
|
+
surface.set(n, 'y', Math.round((top + t.size * (0.85 + LINE_HEIGHT * i)) * 100) / 100);
|
|
39
|
+
surface.text(n, line);
|
|
40
|
+
});
|
|
41
|
+
for (let i = lines.length; i < previous; i++)
|
|
42
|
+
surface.remove(`${prefix}${i}`);
|
|
43
|
+
return lines.length;
|
|
44
|
+
}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Themes: every default look in one place, and the rule for choosing between them.
|
|
3
|
+
*
|
|
4
|
+
* Why a stylesheet and not flat options: a chart has dozens of things that can be styled (tick labels, titles, gridlines,
|
|
5
|
+
* markers…) and new elements add more. A theme maps *selectors* (names for kinds of thing) to bags of style properties,
|
|
6
|
+
* so a new element needs a new name and no change here, and a user can restyle a whole family at once (`text`) or one
|
|
7
|
+
* member (`tickLabel`).
|
|
8
|
+
*
|
|
9
|
+
* How a look is decided
|
|
10
|
+
* ─────────────────────
|
|
11
|
+
* An element asks for its look with a *chain* of selectors, most specific first, ending in a *root*:
|
|
12
|
+
*
|
|
13
|
+
* this.styleFor(['tickLabel', 'axis', 'text']) // → every text property, all defined
|
|
14
|
+
*
|
|
15
|
+
* Each property is found independently, by the first of these that defines it:
|
|
16
|
+
* 1. The element's own attributes (`local`): the common ones, such as `stroke` or `fill`, which are exactly the
|
|
17
|
+
* style properties of the same name.
|
|
18
|
+
* 2. The `style` blocks of the element and then each ancestor up to the chart: nearest first. Inside one block the
|
|
19
|
+
* chain is tried in order, so `tickLabel` beats `axis` beats `text`.
|
|
20
|
+
* 3. The chart's base theme, the same way, except that the element's *accent* (the colour its series was given from the
|
|
21
|
+
* palette) is tried just before the root. So the palette is the default colour, and anything that names the colour
|
|
22
|
+
* beats it.
|
|
23
|
+
*
|
|
24
|
+
* Nearest-first means a block you write close to an element wins over the whole of the base theme, however specific the
|
|
25
|
+
* base theme's selector was: `plane.style = { text: { color: 'red' } }` really does make that plane's text red. The
|
|
26
|
+
* price is that to override one specific selector against a nearer general one, set the specific one at that level too.
|
|
27
|
+
*
|
|
28
|
+
* Why the types hold together: the base theme must define every property of every root (`Roots`), and a chain must end
|
|
29
|
+
* in a root, so there is always a value to find and `styleFor` can be typed as returning all of them. Both interfaces
|
|
30
|
+
* below can be augmented, which is how a custom element joins in:
|
|
31
|
+
*
|
|
32
|
+
* declare module 'inkplot' { interface Selectors { boxLabel: TextProps } }
|
|
33
|
+
* // in the element: this.styleFor(['boxLabel', 'text'])
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* The look of text. `halo` is a colour drawn round the glyphs to keep them legible over data, `haloWidth` its total width.
|
|
37
|
+
* `align` is where text sits in the room it has: a title along its band, the lines of a callout in its box, a line
|
|
38
|
+
* annotation's label along its line. Text that is placed at a point by its own geometry (tick labels) ignores it.
|
|
39
|
+
*/
|
|
40
|
+
export interface TextProps {
|
|
41
|
+
size: number;
|
|
42
|
+
color: string;
|
|
43
|
+
family: string;
|
|
44
|
+
weight: number | string;
|
|
45
|
+
halo: string;
|
|
46
|
+
haloWidth: number;
|
|
47
|
+
align: 'start' | 'middle' | 'end';
|
|
48
|
+
}
|
|
49
|
+
/** The look of a stroked line. `dash` is an SVG dash pattern, or 'none'. */
|
|
50
|
+
export interface LineProps {
|
|
51
|
+
color: string;
|
|
52
|
+
width: number;
|
|
53
|
+
dash: string;
|
|
54
|
+
opacity: number;
|
|
55
|
+
}
|
|
56
|
+
/** The look of a filled shape. `stroke` may be 'none'. */
|
|
57
|
+
export interface ShapeProps {
|
|
58
|
+
fill: string;
|
|
59
|
+
stroke: string;
|
|
60
|
+
strokeWidth: number;
|
|
61
|
+
opacity: number;
|
|
62
|
+
}
|
|
63
|
+
/** Room an axis takes, in pixels: tick length, gap between ticks and labels, and padding round the title and after the axis. */
|
|
64
|
+
export interface AxisMetrics {
|
|
65
|
+
tickLength: number;
|
|
66
|
+
labelGap: number;
|
|
67
|
+
spacing: number;
|
|
68
|
+
}
|
|
69
|
+
/** Half the width of a marker in pixels. */
|
|
70
|
+
export interface MarkerMetrics {
|
|
71
|
+
size: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* What a callout is made of, besides how it looks. `outline` is which of the box's edges are drawn: 'box' all four,
|
|
75
|
+
* 'edge' only the one the arm meets, 'none' none (the fill, if any, stays). `offset` is how far an automatically placed
|
|
76
|
+
* box sits from its point, `padding` the room round the text inside it, and `pointer` the radius of the dot on the point.
|
|
77
|
+
*/
|
|
78
|
+
export interface CalloutMetrics {
|
|
79
|
+
padding: number;
|
|
80
|
+
outline: 'box' | 'edge' | 'none';
|
|
81
|
+
offset: number;
|
|
82
|
+
pointer: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The selectors, and the properties each can set. `axis` and `key` carry a colour that flows to both their text and
|
|
86
|
+
* their lines, so one setting colours a whole axis. Augment to add your own.
|
|
87
|
+
*/
|
|
88
|
+
export interface Selectors {
|
|
89
|
+
text: TextProps;
|
|
90
|
+
line: LineProps;
|
|
91
|
+
shape: ShapeProps;
|
|
92
|
+
axis: Partial<AxisMetrics & TextProps & LineProps>;
|
|
93
|
+
marker: Partial<MarkerMetrics & ShapeProps>;
|
|
94
|
+
callout: Partial<CalloutMetrics>;
|
|
95
|
+
/** The chart itself. `background` is a colour for a rect filling the whole drawing; unset (the default) paints nothing. */
|
|
96
|
+
chart: {
|
|
97
|
+
background?: string;
|
|
98
|
+
};
|
|
99
|
+
/** A tooltip's metrics: what it leaves unsaid it takes from `callout`. */
|
|
100
|
+
tooltip: Partial<CalloutMetrics>;
|
|
101
|
+
tickLabel: TextProps;
|
|
102
|
+
axisTitle: TextProps;
|
|
103
|
+
chartTitle: TextProps;
|
|
104
|
+
chartSubtitle: TextProps;
|
|
105
|
+
chartCaption: TextProps;
|
|
106
|
+
annotationLabel: TextProps;
|
|
107
|
+
calloutText: TextProps;
|
|
108
|
+
tooltipText: TextProps;
|
|
109
|
+
key: Partial<TextProps & LineProps>;
|
|
110
|
+
keyTitle: TextProps;
|
|
111
|
+
keyLabel: TextProps;
|
|
112
|
+
legend: Partial<TextProps>;
|
|
113
|
+
legendTitle: TextProps;
|
|
114
|
+
legendLabel: TextProps;
|
|
115
|
+
axisLine: LineProps;
|
|
116
|
+
gridline: LineProps;
|
|
117
|
+
frame: LineProps;
|
|
118
|
+
/** The guide lines of a crosshair. */
|
|
119
|
+
crosshair: LineProps;
|
|
120
|
+
/** The outline round what has keyboard focus. */
|
|
121
|
+
focusRing: LineProps;
|
|
122
|
+
keyLine: LineProps;
|
|
123
|
+
seriesLine: LineProps;
|
|
124
|
+
annotationLine: LineProps;
|
|
125
|
+
/** The whiskers and caps of an `ErrorBars`. */
|
|
126
|
+
errorBar: LineProps;
|
|
127
|
+
/** The median, whiskers and caps of a `BoxPath`. */
|
|
128
|
+
boxLine: LineProps;
|
|
129
|
+
calloutArm: LineProps;
|
|
130
|
+
/** The box a `BoxZoom` drags out, and the one a `Brush` leaves. */
|
|
131
|
+
selection: ShapeProps;
|
|
132
|
+
seriesArea: ShapeProps;
|
|
133
|
+
/** The region of an `ErrorBand`. */
|
|
134
|
+
errorBand: ShapeProps;
|
|
135
|
+
/** The box of a `BoxPath`. */
|
|
136
|
+
seriesBox: ShapeProps;
|
|
137
|
+
seriesBar: ShapeProps;
|
|
138
|
+
/** The cells of a `HeatMap`. */
|
|
139
|
+
heatCell: ShapeProps;
|
|
140
|
+
calloutBox: ShapeProps;
|
|
141
|
+
tooltipBox: ShapeProps;
|
|
142
|
+
dotMarker: Partial<MarkerMetrics & ShapeProps>;
|
|
143
|
+
lineMarker: Partial<MarkerMetrics & ShapeProps>;
|
|
144
|
+
shapeMarker: Partial<MarkerMetrics & ShapeProps>;
|
|
145
|
+
}
|
|
146
|
+
/** The selectors every chain ends in. The base theme defines all of their properties. Augment together with `Selectors`. */
|
|
147
|
+
export interface Roots {
|
|
148
|
+
text: TextProps;
|
|
149
|
+
line: LineProps;
|
|
150
|
+
shape: ShapeProps;
|
|
151
|
+
axis: AxisMetrics;
|
|
152
|
+
marker: MarkerMetrics;
|
|
153
|
+
callout: CalloutMetrics;
|
|
154
|
+
}
|
|
155
|
+
export type SelectorName = keyof Selectors;
|
|
156
|
+
/** Selectors to style, each with only the properties it wants to change. */
|
|
157
|
+
export type Theme = {
|
|
158
|
+
[S in keyof Selectors]?: Partial<Selectors[S]>;
|
|
159
|
+
};
|
|
160
|
+
/** A complete theme: every root fully defined, and the colours series take in turn. */
|
|
161
|
+
export type BaseTheme = Theme & {
|
|
162
|
+
[R in keyof Roots]: Roots[R];
|
|
163
|
+
} & {
|
|
164
|
+
palette: readonly string[];
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Finds each property of the chain's root. `scopes` run from the nearest `style` block outwards; `base` is last.
|
|
168
|
+
* Pure, so it can be tested without a chart.
|
|
169
|
+
*/
|
|
170
|
+
export declare function resolveStyle<R extends keyof Roots>(chain: readonly [...SelectorName[], R], scopes: readonly (Theme | undefined)[], base: BaseTheme, local?: Partial<Roots[R]>, accent?: Partial<Roots[R]>): Roots[R];
|
|
171
|
+
/** A theme derived from another: each selector in `patch` is merged over the same one in `base`, property by property. */
|
|
172
|
+
export declare function extendTheme<B extends BaseTheme>(base: B, patch: Theme & {
|
|
173
|
+
palette?: readonly string[];
|
|
174
|
+
}): B;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* oxlint-disable typescript/consistent-type-assertions -- theme bags are open maps merged by key at runtime; the result is typed as the schema it was checked against */
|
|
2
|
+
/**
|
|
3
|
+
* Themes: every default look in one place, and the rule for choosing between them.
|
|
4
|
+
*
|
|
5
|
+
* Why a stylesheet and not flat options: a chart has dozens of things that can be styled (tick labels, titles, gridlines,
|
|
6
|
+
* markers…) and new elements add more. A theme maps *selectors* (names for kinds of thing) to bags of style properties,
|
|
7
|
+
* so a new element needs a new name and no change here, and a user can restyle a whole family at once (`text`) or one
|
|
8
|
+
* member (`tickLabel`).
|
|
9
|
+
*
|
|
10
|
+
* How a look is decided
|
|
11
|
+
* ─────────────────────
|
|
12
|
+
* An element asks for its look with a *chain* of selectors, most specific first, ending in a *root*:
|
|
13
|
+
*
|
|
14
|
+
* this.styleFor(['tickLabel', 'axis', 'text']) // → every text property, all defined
|
|
15
|
+
*
|
|
16
|
+
* Each property is found independently, by the first of these that defines it:
|
|
17
|
+
* 1. The element's own attributes (`local`): the common ones, such as `stroke` or `fill`, which are exactly the
|
|
18
|
+
* style properties of the same name.
|
|
19
|
+
* 2. The `style` blocks of the element and then each ancestor up to the chart: nearest first. Inside one block the
|
|
20
|
+
* chain is tried in order, so `tickLabel` beats `axis` beats `text`.
|
|
21
|
+
* 3. The chart's base theme, the same way, except that the element's *accent* (the colour its series was given from the
|
|
22
|
+
* palette) is tried just before the root. So the palette is the default colour, and anything that names the colour
|
|
23
|
+
* beats it.
|
|
24
|
+
*
|
|
25
|
+
* Nearest-first means a block you write close to an element wins over the whole of the base theme, however specific the
|
|
26
|
+
* base theme's selector was: `plane.style = { text: { color: 'red' } }` really does make that plane's text red. The
|
|
27
|
+
* price is that to override one specific selector against a nearer general one, set the specific one at that level too.
|
|
28
|
+
*
|
|
29
|
+
* Why the types hold together: the base theme must define every property of every root (`Roots`), and a chain must end
|
|
30
|
+
* in a root, so there is always a value to find and `styleFor` can be typed as returning all of them. Both interfaces
|
|
31
|
+
* below can be augmented, which is how a custom element joins in:
|
|
32
|
+
*
|
|
33
|
+
* declare module 'inkplot' { interface Selectors { boxLabel: TextProps } }
|
|
34
|
+
* // in the element: this.styleFor(['boxLabel', 'text'])
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Finds each property of the chain's root. `scopes` run from the nearest `style` block outwards; `base` is last.
|
|
38
|
+
* Pure, so it can be tested without a chart.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveStyle(chain, scopes, base, local, accent) {
|
|
41
|
+
// The chain's names and the theme's blocks are typed by selector; here they are looked up by string.
|
|
42
|
+
const root = chain[chain.length - 1], names = chain, blocks = base;
|
|
43
|
+
const out = {};
|
|
44
|
+
const loc = local, acc = accent;
|
|
45
|
+
// 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
|
|
46
|
+
for (const key of Object.keys(blocks[root])) {
|
|
47
|
+
let v = loc?.[key];
|
|
48
|
+
for (let s = 0; v === undefined && s < scopes.length; s++) {
|
|
49
|
+
const scope = scopes[s];
|
|
50
|
+
// 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
|
|
51
|
+
if (scope)
|
|
52
|
+
for (let c = 0; v === undefined && c < names.length; c++)
|
|
53
|
+
v = scope[names[c]]?.[key];
|
|
54
|
+
}
|
|
55
|
+
for (let c = 0; v === undefined && c < names.length; c++) {
|
|
56
|
+
// 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
|
|
57
|
+
const sel = names[c];
|
|
58
|
+
if (sel === root)
|
|
59
|
+
v = acc?.[key];
|
|
60
|
+
v ??= blocks[sel]?.[key];
|
|
61
|
+
}
|
|
62
|
+
out[key] = v;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/** A theme derived from another: each selector in `patch` is merged over the same one in `base`, property by property. */
|
|
67
|
+
export function extendTheme(base, patch) {
|
|
68
|
+
const out = { ...base };
|
|
69
|
+
for (const [selector, props] of Object.entries(patch)) {
|
|
70
|
+
out[selector] = selector === 'palette' ? props : { ...base[selector], ...props };
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
package/dist/themes.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in themes. Separate from `theme.ts` and from each other's use so that a chart bundles only the one it names:
|
|
3
|
+
* a theme is a plain object, and one that nothing imports is dropped by the bundler.
|
|
4
|
+
*/
|
|
5
|
+
import { type BaseTheme } from './theme.ts';
|
|
6
|
+
/** Dark text and lines on a light page. The look the library had before themes existed. */
|
|
7
|
+
export declare const lightTheme: BaseTheme;
|
|
8
|
+
/** Light text and lines for a dark page. The chart paints no background unless `chart.background` is set. */
|
|
9
|
+
export declare const darkTheme: BaseTheme;
|
package/dist/themes.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in themes. Separate from `theme.ts` and from each other's use so that a chart bundles only the one it names:
|
|
3
|
+
* a theme is a plain object, and one that nothing imports is dropped by the bundler.
|
|
4
|
+
*/
|
|
5
|
+
import { extendTheme } from './theme.js';
|
|
6
|
+
const FONT = 'system-ui, -apple-system, "Segoe UI", sans-serif';
|
|
7
|
+
/** Dark text and lines on a light page. The look the library had before themes existed. */
|
|
8
|
+
export const lightTheme = {
|
|
9
|
+
palette: ['#2a6fdb', '#e0483e', '#2e9e5b', '#e0a01b', '#8a56c9', '#1c9aa6'],
|
|
10
|
+
text: { size: 11, color: '#444', family: FONT, weight: 'normal', halo: 'none', haloWidth: 3, align: 'start' },
|
|
11
|
+
line: { color: '#444', width: 1, dash: 'none', opacity: 1 },
|
|
12
|
+
shape: { fill: '#2a6fdb', stroke: 'none', strokeWidth: 1, opacity: 1 },
|
|
13
|
+
axis: { tickLength: 5, labelGap: 3, spacing: 4 },
|
|
14
|
+
marker: { size: 3 },
|
|
15
|
+
callout: { padding: 6, outline: 'box', offset: 22, pointer: 3 },
|
|
16
|
+
axisTitle: { size: 12 },
|
|
17
|
+
chartTitle: { size: 16, weight: 600 },
|
|
18
|
+
chartSubtitle: { size: 12, color: '#666' },
|
|
19
|
+
chartCaption: { size: 10, color: '#777' },
|
|
20
|
+
annotationLine: { color: '#8a5a44', dash: '5 3' },
|
|
21
|
+
calloutBox: { fill: '#fff', stroke: '#999' },
|
|
22
|
+
calloutArm: { color: '#999' },
|
|
23
|
+
calloutText: { color: '#333' },
|
|
24
|
+
keyTitle: { weight: 600 },
|
|
25
|
+
legendTitle: { weight: 600 },
|
|
26
|
+
gridline: { color: '#e6e6e6' },
|
|
27
|
+
frame: { color: '#bbb' },
|
|
28
|
+
crosshair: { color: '#888', dash: '4 3' },
|
|
29
|
+
focusRing: { color: '#0b57d0', width: 2 },
|
|
30
|
+
selection: { fill: '#2a6fdb', stroke: '#2a6fdb', opacity: 0.2 },
|
|
31
|
+
seriesLine: { width: 1.5 },
|
|
32
|
+
seriesArea: { opacity: 0.3, strokeWidth: 1.5 },
|
|
33
|
+
errorBar: { width: 1.5 },
|
|
34
|
+
boxLine: { width: 1.5 },
|
|
35
|
+
seriesBox: { opacity: 0.4, strokeWidth: 1.5 },
|
|
36
|
+
errorBand: { opacity: 0.2 },
|
|
37
|
+
lineMarker: { strokeWidth: 1.5 },
|
|
38
|
+
};
|
|
39
|
+
/** Light text and lines for a dark page. The chart paints no background unless `chart.background` is set. */
|
|
40
|
+
// Marked pure so that a bundle that only uses `lightTheme` leaves this out: a call at the top of a module is otherwise kept.
|
|
41
|
+
export const darkTheme = /* @__PURE__ */ extendTheme(lightTheme, {
|
|
42
|
+
palette: ['#5b9bff', '#ff6b61', '#4fcf85', '#f2bc45', '#b18af0', '#4cc4d1'],
|
|
43
|
+
text: { color: '#d4d4d4' },
|
|
44
|
+
line: { color: '#d4d4d4' },
|
|
45
|
+
gridline: { color: '#333' },
|
|
46
|
+
frame: { color: '#666' },
|
|
47
|
+
crosshair: { color: '#999' },
|
|
48
|
+
focusRing: { color: '#8ab4f8' },
|
|
49
|
+
chartSubtitle: { color: '#aaa' },
|
|
50
|
+
chartCaption: { color: '#999' },
|
|
51
|
+
annotationLine: { color: '#d9a98f' },
|
|
52
|
+
calloutBox: { fill: '#1b252b', stroke: '#778' },
|
|
53
|
+
calloutArm: { color: '#889' },
|
|
54
|
+
calloutText: { color: '#e0e0e0' },
|
|
55
|
+
selection: { fill: '#5b9bff', stroke: '#5b9bff' },
|
|
56
|
+
});
|