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.
Files changed (141) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -2
  3. package/dist/adapt.d.ts +10 -0
  4. package/dist/adapt.js +34 -0
  5. package/dist/annotation.d.ts +120 -0
  6. package/dist/annotation.js +267 -0
  7. package/dist/aria.d.ts +24 -0
  8. package/dist/aria.js +31 -0
  9. package/dist/attrs.d.ts +81 -0
  10. package/dist/attrs.js +84 -0
  11. package/dist/axis.d.ts +137 -0
  12. package/dist/axis.js +185 -0
  13. package/dist/bars.d.ts +80 -0
  14. package/dist/bars.js +255 -0
  15. package/dist/boxplot.d.ts +57 -0
  16. package/dist/boxplot.js +177 -0
  17. package/dist/boxzoom.d.ts +39 -0
  18. package/dist/boxzoom.js +138 -0
  19. package/dist/brush.d.ts +60 -0
  20. package/dist/brush.js +185 -0
  21. package/dist/bucket.d.ts +22 -0
  22. package/dist/bucket.js +62 -0
  23. package/dist/callout.d.ts +30 -0
  24. package/dist/callout.js +98 -0
  25. package/dist/chart.d.ts +126 -0
  26. package/dist/chart.js +257 -0
  27. package/dist/crosshair.d.ts +28 -0
  28. package/dist/crosshair.js +78 -0
  29. package/dist/cursor.d.ts +76 -0
  30. package/dist/cursor.js +344 -0
  31. package/dist/curve.d.ts +50 -0
  32. package/dist/curve.js +87 -0
  33. package/dist/curves.d.ts +104 -0
  34. package/dist/curves.js +375 -0
  35. package/dist/data.d.ts +209 -0
  36. package/dist/data.js +276 -0
  37. package/dist/decimation.d.ts +86 -0
  38. package/dist/decimation.js +410 -0
  39. package/dist/density.d.ts +55 -0
  40. package/dist/density.js +252 -0
  41. package/dist/dimension.d.ts +103 -0
  42. package/dist/dimension.js +205 -0
  43. package/dist/dom-platform.d.ts +53 -0
  44. package/dist/dom-platform.js +190 -0
  45. package/dist/element.d.ts +232 -0
  46. package/dist/element.js +414 -0
  47. package/dist/errors.d.ts +85 -0
  48. package/dist/errors.js +237 -0
  49. package/dist/events.d.ts +144 -0
  50. package/dist/events.js +19 -0
  51. package/dist/focus.d.ts +22 -0
  52. package/dist/focus.js +51 -0
  53. package/dist/furniture.d.ts +70 -0
  54. package/dist/furniture.js +166 -0
  55. package/dist/geometry.d.ts +40 -0
  56. package/dist/geometry.js +7 -0
  57. package/dist/heatmap.d.ts +57 -0
  58. package/dist/heatmap.js +207 -0
  59. package/dist/hit.d.ts +103 -0
  60. package/dist/hit.js +428 -0
  61. package/dist/index.d.ts +73 -0
  62. package/dist/index.js +73 -0
  63. package/dist/input.d.ts +42 -0
  64. package/dist/input.js +166 -0
  65. package/dist/jsx-dev-runtime.d.ts +9 -0
  66. package/dist/jsx-dev-runtime.js +13 -0
  67. package/dist/jsx-runtime.d.ts +59 -0
  68. package/dist/jsx-runtime.js +81 -0
  69. package/dist/key.d.ts +118 -0
  70. package/dist/key.js +199 -0
  71. package/dist/legend.d.ts +36 -0
  72. package/dist/legend.js +220 -0
  73. package/dist/mapped.d.ts +80 -0
  74. package/dist/mapped.js +106 -0
  75. package/dist/marker.d.ts +143 -0
  76. package/dist/marker.js +213 -0
  77. package/dist/panzoom.d.ts +119 -0
  78. package/dist/panzoom.js +467 -0
  79. package/dist/plane.d.ts +173 -0
  80. package/dist/plane.js +370 -0
  81. package/dist/platform.d.ts +113 -0
  82. package/dist/platform.js +80 -0
  83. package/dist/png.d.ts +44 -0
  84. package/dist/png.js +71 -0
  85. package/dist/pointer.d.ts +54 -0
  86. package/dist/pointer.js +132 -0
  87. package/dist/quantile.d.ts +3 -0
  88. package/dist/quantile.js +11 -0
  89. package/dist/radial.d.ts +116 -0
  90. package/dist/radial.js +336 -0
  91. package/dist/react.d.ts +441 -0
  92. package/dist/react.js +252 -0
  93. package/dist/registry.d.ts +2 -0
  94. package/dist/registry.js +51 -0
  95. package/dist/renderer.d.ts +191 -0
  96. package/dist/renderer.js +164 -0
  97. package/dist/scale.d.ts +37 -0
  98. package/dist/scale.js +19 -0
  99. package/dist/scales.d.ts +69 -0
  100. package/dist/scales.js +134 -0
  101. package/dist/serialize.d.ts +77 -0
  102. package/dist/serialize.js +321 -0
  103. package/dist/series.d.ts +304 -0
  104. package/dist/series.js +513 -0
  105. package/dist/simple.d.ts +35 -0
  106. package/dist/simple.js +20 -0
  107. package/dist/sources.d.ts +116 -0
  108. package/dist/sources.js +237 -0
  109. package/dist/stack.d.ts +61 -0
  110. package/dist/stack.js +172 -0
  111. package/dist/stats.d.ts +38 -0
  112. package/dist/stats.js +159 -0
  113. package/dist/steps.d.ts +50 -0
  114. package/dist/steps.js +154 -0
  115. package/dist/stub.d.ts +3 -0
  116. package/dist/stub.js +5 -0
  117. package/dist/style-names.d.ts +1 -0
  118. package/dist/style-names.js +6 -0
  119. package/dist/summary.d.ts +44 -0
  120. package/dist/summary.js +58 -0
  121. package/dist/surface.d.ts +94 -0
  122. package/dist/surface.js +145 -0
  123. package/dist/table.d.ts +39 -0
  124. package/dist/table.js +37 -0
  125. package/dist/text.d.ts +23 -0
  126. package/dist/text.js +44 -0
  127. package/dist/theme.d.ts +174 -0
  128. package/dist/theme.js +73 -0
  129. package/dist/themes.d.ts +9 -0
  130. package/dist/themes.js +56 -0
  131. package/dist/time-intl.d.ts +194 -0
  132. package/dist/time-intl.js +144 -0
  133. package/dist/time-temporal.d.ts +236 -0
  134. package/dist/time-temporal.js +58 -0
  135. package/dist/time.d.ts +216 -0
  136. package/dist/time.js +154 -0
  137. package/dist/trend.d.ts +151 -0
  138. package/dist/trend.js +500 -0
  139. package/dist/web-components.d.ts +62 -0
  140. package/dist/web-components.js +264 -0
  141. package/package.json +53 -3
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Series renderers: turn placed points into platform nodes.
3
+ *
4
+ * A renderer is a child of a series and is handed that series' projected points (plot pixels) on PAINT. It knows nothing
5
+ * of axes, planes or data: that is why one renderer works unchanged on a rectangle, a circle or a radar. Renderers that
6
+ * vary a look by data (markers) declare `bindings`, which the series reads to know which channels to request; the
7
+ * values themselves come from `MappedDimension`s (colour and size axes).
8
+ *
9
+ * Defaults come from the theme (see theme.ts); the attributes `stroke`, `strokeWidth`, `fill` and `opacity` are the same
10
+ * properties set directly. The colour a series gets from the palette is the lowest-priority default for its colour.
11
+ *
12
+ * A point that is NaN or infinite in either coordinate is a gap. `Number.isFinite` tests for it (it is as fast as
13
+ * any hand-rolled compare in V8, measured), so the hot loops use it.
14
+ */
15
+ import { STRING, NUMBER } from './attrs.js';
16
+ import { Curve } from './curve.js';
17
+ import { ChartElement } from './element.js';
18
+ import { EVENT_PAINT } from './events.js';
19
+ import { notImplemented } from './stub.js';
20
+ /** Draws a series' projected points into its own surface. */
21
+ export class SeriesRenderer extends ChartElement {
22
+ static attrTypes = { ...ChartElement.attrTypes, fill: STRING, maxNodes: NUMBER, stroke: STRING, strokeWidth: NUMBER };
23
+ /**
24
+ * The one colour that stands for what this draws, for a key or a tooltip to show beside the series' name: its line, its
25
+ * fill. Null when it has none (the colour is bound to data, so there is not one).
26
+ */
27
+ get swatch() { return null; }
28
+ /**
29
+ * Draw a sample of what this draws into `parent` (a node of `surface`, the caller's), in a box `width` by `height` from
30
+ * its own top left corner: a short line for a line, a box for an area or bars, one marker for markers. It takes the look
31
+ * the renderer would draw with (theme, attributes, dashes), so a legend or a tooltip shows what is on the plot. Several
32
+ * renderers on a series draw their samples one over another. Nodes are keyed `key…` in `surface`. Default: a box of the
33
+ * `swatch` colour, or nothing for a renderer that has none.
34
+ */
35
+ sample(surface, parent, key, width, height) {
36
+ const colour = this.swatch;
37
+ if (!colour || colour === 'none') {
38
+ surface.remove(key);
39
+ return;
40
+ }
41
+ const n = surface.node(key, 'path', { parent });
42
+ surface.set(n, 'd', `M0 0H${width}V${height}H0Z`);
43
+ surface.set(n, 'fill', colour);
44
+ surface.set(n, 'stroke', 'none');
45
+ }
46
+ /** Values this renderer needs the value axis to reach whatever the data says: the baseline bars grow from. Default none. */
47
+ get includes() { return []; }
48
+ /**
49
+ * Pixels this draws beyond the places of its points (a marker's radius), which the axes leave room for so that a mark
50
+ * on the end of the data is not cut by the edge of the plot. Default 0.
51
+ */
52
+ get reach() { return 0; }
53
+ /** Half the width of what this draws along the domain, as a fraction of the gap between neighbouring points (bars), which the domain axis leaves room for. Default 0. */
54
+ get spans() { return 0; }
55
+ /** What a pointer hit on the series should measure by default, if this renderer knows: bars say 'shape'. Default no opinion. */
56
+ get hitMode() { return undefined; }
57
+ /**
58
+ * Channels of the data this needs as plain numbers beside each point (error bars: how far each value may be out), with no mapped
59
+ * axis to turn them into anything. The series requests them with the bound ones; in `points` they follow the two coordinates,
60
+ * raw, in the order of `Series.channels`. Default none.
61
+ */
62
+ get channelNames() { return []; }
63
+ /** Nothing is bound until bindings are written; a renderer that has some will report them here. */
64
+ get bindings() { return []; }
65
+ /** Buckets per stylistic binding, from `maxNodes`. 0 when nothing is stylistically bound. */
66
+ get stepsPerBinding() { return 0; }
67
+ onEvent(ev) {
68
+ switch (ev.type) {
69
+ case EVENT_PAINT:
70
+ if (this.requireParent().shown)
71
+ this.draw(this.requireParent().points);
72
+ break;
73
+ }
74
+ }
75
+ }
76
+ /** A box in the look of a filled shape: what an area and a bar are in a legend. */
77
+ export function paintBox(s, parent, key, width, height, look) {
78
+ const n = s.node(key, 'path', { parent });
79
+ s.set(n, 'd', `M0 0H${width}V${height}H0Z`);
80
+ s.set(n, 'fill', look.fill);
81
+ s.set(n, 'fill-opacity', look.opacity);
82
+ s.set(n, 'stroke', look.stroke);
83
+ s.set(n, 'stroke-width', look.strokeWidth);
84
+ s.set(n, 'stroke-linejoin', 'round');
85
+ }
86
+ /**
87
+ * One path through the points, in the way its `Curve` says: straight lines (`LinearCurve`), a spline, steps. A gap in the
88
+ * data lifts the pen, or is crossed if the series says its `gaps` are 'connect'.
89
+ */
90
+ export class LinePath extends SeriesRenderer {
91
+ /** How the points are joined. */
92
+ get curve() { return this.child(0, Curve); }
93
+ /** The series' palette colour is the default; a dash pattern is style only (`style: { seriesLine: { dash } }`). */
94
+ look() {
95
+ const a = this.attrs;
96
+ return this.styleFor(['seriesLine', 'line'], { color: a.stroke, width: a.strokeWidth, opacity: a.opacity }, { color: this.requireParent().accentColor });
97
+ }
98
+ get swatch() { return this.look().color; }
99
+ trace() { return this.curve.trace(this.requireParent(), null); }
100
+ sample(s, parent, key, width, height) {
101
+ const look = this.look(), n = s.node(key, 'path', { parent });
102
+ s.set(n, 'd', `M0 ${height / 2}H${width}`);
103
+ s.set(n, 'fill', 'none');
104
+ s.set(n, 'stroke', look.color);
105
+ s.set(n, 'stroke-width', look.width);
106
+ s.set(n, 'stroke-linecap', 'butt');
107
+ s.set(n, 'opacity', look.opacity);
108
+ s.set(n, 'stroke-dasharray', look.dash);
109
+ }
110
+ draw() {
111
+ const counters = this.counters, s = this.surface, a = this.attrs;
112
+ if (a.bind)
113
+ notImplemented('LinePath bindings');
114
+ const look = this.look();
115
+ const t0 = performance.now();
116
+ const d = this.curve.line(this.requireParent());
117
+ counters.stages.path += performance.now() - t0;
118
+ counters.pathBytes += d.length;
119
+ const n = s.node('line', 'path');
120
+ s.set(n, 'd', d);
121
+ s.set(n, 'fill', 'none');
122
+ s.set(n, 'stroke', look.color);
123
+ s.set(n, 'stroke-width', look.width);
124
+ s.set(n, 'stroke-linejoin', 'round');
125
+ s.set(n, 'stroke-linecap', 'round');
126
+ s.set(n, 'opacity', look.opacity);
127
+ s.set(n, 'stroke-dasharray', look.dash);
128
+ }
129
+ }
130
+ /**
131
+ * The region between a line and a baseline, the line drawn in the way its `Curve` says. It is the data forward and the
132
+ * baseline backward, closed: the baseline is placed by the plane exactly as the data is, so on a rectangle this is the
133
+ * usual area under a line, and on a PolarPlane it is the ring or wedge between the curve and the baseline circle, with
134
+ * nothing here to say so. A gap in the data splits it into separate shapes. Outlines are off by default; give a `stroke`
135
+ * to draw one.
136
+ */
137
+ export class AreaPath extends SeriesRenderer {
138
+ /** How the points are joined. */
139
+ get curve() { return this.child(0, Curve); }
140
+ look() {
141
+ const a = this.attrs;
142
+ return this.styleFor(['seriesArea', 'shape'], { fill: a.fill, stroke: a.stroke, strokeWidth: a.strokeWidth, opacity: a.opacity }, { fill: this.requireParent().accentColor });
143
+ }
144
+ get swatch() { return this.look().fill; }
145
+ trace() { return this.curve.trace(this.requireParent(), this.attrs.baseline ?? 0); }
146
+ sample(s, parent, key, width, height) { paintBox(s, parent, key, width, height, this.look()); }
147
+ draw() {
148
+ const counters = this.counters, s = this.surface, a = this.attrs;
149
+ if (a.bind)
150
+ notImplemented('AreaPath bindings');
151
+ const look = this.look();
152
+ const t0 = performance.now();
153
+ const d = this.curve.area(this.requireParent(), a.baseline ?? 0);
154
+ counters.stages.path += performance.now() - t0;
155
+ counters.pathBytes += d.length;
156
+ const n = s.node('area', 'path');
157
+ s.set(n, 'd', d);
158
+ s.set(n, 'fill', look.fill);
159
+ s.set(n, 'fill-opacity', look.opacity);
160
+ s.set(n, 'stroke', look.stroke);
161
+ s.set(n, 'stroke-width', look.strokeWidth);
162
+ s.set(n, 'stroke-linejoin', 'round');
163
+ }
164
+ }
@@ -0,0 +1,37 @@
1
+ /** The base of scales, which map a data range to 0..1 and back, and choose tick values. The scales themselves are in scales.ts. */
2
+ import type { Dimension } from './dimension.ts';
3
+ import { type BaseAttributes, ChartElement } from './element.ts';
4
+ import type { Extent } from './geometry.ts';
5
+ /** Shared with scales.ts. Trims binary noise (0.1 + 0.2) so tick values and labels come out as the decimals a person expects. */
6
+ export declare const clean: (v: number) => number;
7
+ /** Rounds a raw step up to 1, 2, 5 or 10 times a power of ten: the steps people read easily. The cut-offs favour fewer, rounder ticks. */
8
+ export declare function niceStep(raw: number): number;
9
+ /** No options of its own; subclasses add their parameters (a log base). */
10
+ export interface ScaleAttributes extends BaseAttributes {
11
+ }
12
+ /**
13
+ * The maths of a dimension, as an element: the first (and only) child of an axis. Parameters are attributes,
14
+ * so a scale serializes like anything else, and a new transform is a new class with no change to axes.
15
+ */
16
+ export declare abstract class Scale<A extends ScaleAttributes = ScaleAttributes> extends ChartElement<Dimension, [], A> {
17
+ /** Value to 0..1, in place over an interleaved buffer. Invalid input (log of a non-positive) becomes NaN. */
18
+ abstract normalize(buffer: Float64Array, count: number, stride: number, offset: number, range: Extent): void;
19
+ /** The inverse of `normalize` for one value: how pointer positions become data values. */
20
+ abstract denormalize(fraction: number, range: Extent): number;
21
+ /** Replaces ends the scale cannot represent (a log range reaching zero). */
22
+ abstract constrain(range: Extent): Extent;
23
+ /** Pull a raw range out to tidy ends. */
24
+ abstract nice(range: Extent): Extent;
25
+ /** Tick values inside the range, about `count` of them. */
26
+ abstract ticks(range: Extent, count: number): readonly number[];
27
+ /** True when equal steps in the data are equal steps along the axis, so a straight line in data is straight on the page. */
28
+ get isLinear(): boolean;
29
+ /** The text for a tick, when the scale knows better than a number does (a category name). Default: the axis formats the number. */
30
+ label?(value: number): string | undefined;
31
+ /**
32
+ * The labels for a whole set of ticks, when what a tick should say depends on the others (a time scale writes a new day
33
+ * as "Jun 11" and the minutes in it as "22:10"). Gets the values the axis chose, whoever chose them, in order. Beats `label` for ticks; an
34
+ * axis's own `format` beats both.
35
+ */
36
+ labels?(values: readonly number[], range: Extent): readonly string[];
37
+ }
package/dist/scale.js ADDED
@@ -0,0 +1,19 @@
1
+ import { ChartElement } from './element.js';
2
+ /** Shared with scales.ts. Trims binary noise (0.1 + 0.2) so tick values and labels come out as the decimals a person expects. */
3
+ export const clean = (v) => Number(v.toPrecision(12));
4
+ /** Rounds a raw step up to 1, 2, 5 or 10 times a power of ten: the steps people read easily. The cut-offs favour fewer, rounder ticks. */
5
+ export function niceStep(raw) {
6
+ if (!(raw > 0) || !isFinite(raw))
7
+ return 1;
8
+ const exp = Math.floor(Math.log10(raw));
9
+ const f = raw / 10 ** exp;
10
+ return (f < 1.5 ? 1 : f < 3 ? 2 : f < 7 ? 5 : 10) * 10 ** exp;
11
+ }
12
+ /**
13
+ * The maths of a dimension, as an element: the first (and only) child of an axis. Parameters are attributes,
14
+ * so a scale serializes like anything else, and a new transform is a new class with no change to axes.
15
+ */
16
+ export class Scale extends ChartElement {
17
+ /** True when equal steps in the data are equal steps along the axis, so a straight line in data is straight on the page. */
18
+ get isLinear() { return false; }
19
+ }
@@ -0,0 +1,69 @@
1
+ /** The scales: linear, logarithmic, square-root and category. The base class is in scale.ts. */
2
+ import type { Extent } from './geometry.ts';
3
+ import { Scale, type ScaleAttributes } from './scale.ts';
4
+ /** Evenly spaced values: the default, and the only scale implemented so far. */
5
+ export declare class LinearScale<A extends ScaleAttributes = ScaleAttributes> extends Scale<A> {
6
+ get isLinear(): boolean;
7
+ normalize(buf: Float64Array, count: number, stride: number, offset: number, range: Extent): void;
8
+ denormalize(f: number, r: Extent): number;
9
+ constrain(r: Extent): Extent;
10
+ nice(r: Extent): Extent;
11
+ ticks(r: Extent, count: number): readonly number[];
12
+ }
13
+ /**
14
+ * Powers of a base: equal ratios take equal distances, which is what spans of many orders of magnitude need.
15
+ * Values that are not positive have no place on it and come out NaN (a gap), and the range is kept positive.
16
+ */
17
+ export interface LogScaleAttributes extends ScaleAttributes {
18
+ /** Default 10. */
19
+ base?: number;
20
+ }
21
+ export declare class LogScale extends Scale<LogScaleAttributes> {
22
+ private get base();
23
+ normalize(buf: Float64Array, count: number, stride: number, offset: number, range: Extent): void;
24
+ denormalize(f: number, r: Extent): number;
25
+ /**
26
+ * A range reaching zero or below cannot be drawn. Pull the low end up to three orders of magnitude under the high end
27
+ * (or the high end up from it, if both are bad): a choice, since there is no right answer, but a readable one.
28
+ */
29
+ constrain(r: Extent): Extent;
30
+ nice(r: Extent): Extent;
31
+ /** The powers of the base in range, thinned to about `count`; with only a few decades, 2 and 5 times each as well. */
32
+ ticks(r: Extent, count: number): readonly number[];
33
+ }
34
+ /**
35
+ * Square root: gives zero a place (unlike a log) while giving small values more room than a linear scale does, as for
36
+ * counts or areas. Negative values come out NaN.
37
+ */
38
+ export declare class SqrtScale extends Scale {
39
+ #private;
40
+ normalize(buf: Float64Array, count: number, stride: number, offset: number, range: Extent): void;
41
+ denormalize(f: number, r: Extent): number;
42
+ /** The low end cannot go below zero. */
43
+ constrain(r: Extent): Extent;
44
+ nice(r: Extent): Extent;
45
+ ticks(r: Extent, count: number): readonly number[];
46
+ }
47
+ export interface BandScaleAttributes extends ScaleAttributes {
48
+ /** Category labels in order; the domain values are indexes into this list. */
49
+ categories: readonly string[];
50
+ /** Fraction of each band left empty between neighbours, 0..1. Default 0.2. */
51
+ gap?: number;
52
+ }
53
+ /**
54
+ * Categories: the domain values 0, 1, 2… are positions of equal bands, one per category, and a value is drawn at the
55
+ * middle of its band. The range is fixed by the categories (data cannot change it), the ticks are the categories, and
56
+ * every label is the category's name. Labels are never thinned, so give a long list room or a short name.
57
+ */
58
+ export declare class BandScale extends Scale<BandScaleAttributes> {
59
+ private get n();
60
+ /** Width of the drawn part of one band as a fraction of the range, for a renderer that draws bars. */
61
+ get bandwidth(): number;
62
+ normalize(buf: Float64Array, count: number, stride: number, offset: number, range: Extent): void;
63
+ denormalize(f: number, r: Extent): number;
64
+ /** Half a band either side of the first and last category, whatever the data or attributes asked. */
65
+ constrain(): Extent;
66
+ nice(r: Extent): Extent;
67
+ ticks(r: Extent): readonly number[];
68
+ label(value: number): string | undefined;
69
+ }
package/dist/scales.js ADDED
@@ -0,0 +1,134 @@
1
+ import { Scale, clean, niceStep } from './scale.js';
2
+ /** Evenly spaced values: the default, and the only scale implemented so far. */
3
+ export class LinearScale extends Scale {
4
+ get isLinear() { return true; }
5
+ normalize(buf, count, stride, offset, range) {
6
+ const span = range.max - range.min;
7
+ // A zero span maps everything to 0 rather than dividing by zero.
8
+ const inv = span > 0 ? 1 / span : 0;
9
+ const min = range.min;
10
+ // 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
11
+ for (let k = 0, i = offset; k < count; k++, i += stride)
12
+ buf[i] = (buf[i] - min) * inv;
13
+ }
14
+ denormalize(f, r) { return r.min + f * (r.max - r.min); }
15
+ constrain(r) { return r; }
16
+ nice(r) {
17
+ // Aim for about five divisions; the 1e-9 keeps an already tidy end from stepping out a whole tick on rounding error.
18
+ const step = niceStep((r.max - r.min) / 5);
19
+ return { min: clean(Math.floor(r.min / step + 1e-9) * step), max: clean(Math.ceil(r.max / step - 1e-9) * step) };
20
+ }
21
+ ticks(r, count) {
22
+ const step = niceStep((r.max - r.min) / Math.max(1, count));
23
+ const first = Math.ceil(r.min / step - 1e-9), last = Math.floor(r.max / step + 1e-9);
24
+ const out = [];
25
+ // The cap is a guard against a huge range with a tiny tick step, not a real limit.
26
+ for (let k = first; k <= last && out.length < 1000; k++)
27
+ out.push(clean(k * step));
28
+ return out;
29
+ }
30
+ }
31
+ export class LogScale extends Scale {
32
+ get base() { return this.attrs.base ?? 10; }
33
+ normalize(buf, count, stride, offset, range) {
34
+ const lo = Math.log(range.min), span = Math.log(range.max) - lo;
35
+ const inv = span > 0 ? 1 / span : 0;
36
+ for (let k = 0, i = offset; k < count; k++, i += stride) {
37
+ // 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
38
+ const v = buf[i];
39
+ buf[i] = v > 0 ? (Math.log(v) - lo) * inv : NaN;
40
+ }
41
+ }
42
+ denormalize(f, r) {
43
+ const lo = Math.log(r.min);
44
+ return Math.exp(lo + f * (Math.log(r.max) - lo));
45
+ }
46
+ /**
47
+ * A range reaching zero or below cannot be drawn. Pull the low end up to three orders of magnitude under the high end
48
+ * (or the high end up from it, if both are bad): a choice, since there is no right answer, but a readable one.
49
+ */
50
+ constrain(r) {
51
+ const b = this.base;
52
+ if (r.max <= 0)
53
+ return { min: 1, max: b };
54
+ if (r.min <= 0)
55
+ return { min: r.max / b ** 3, max: r.max };
56
+ return r;
57
+ }
58
+ nice(r) {
59
+ const b = this.base, lg = (v) => Math.log(v) / Math.log(b);
60
+ // The epsilons stop an exact power from stepping out a whole decade on rounding error.
61
+ return { min: clean(b ** Math.floor(lg(r.min) + 1e-9)), max: clean(b ** Math.ceil(lg(r.max) - 1e-9)) };
62
+ }
63
+ /** The powers of the base in range, thinned to about `count`; with only a few decades, 2 and 5 times each as well. */
64
+ ticks(r, count) {
65
+ const b = this.base, lg = (v) => Math.log(v) / Math.log(b);
66
+ const first = Math.ceil(lg(r.min) - 1e-9), last = Math.floor(lg(r.max) + 1e-9);
67
+ const out = [];
68
+ const decades = last - first + 1;
69
+ if (decades < count && b === 10) {
70
+ for (let e = first - 1; e <= last; e++)
71
+ for (const m of [1, 2, 5]) {
72
+ const v = clean(m * 10 ** e);
73
+ if (v >= r.min && v <= r.max)
74
+ out.push(v);
75
+ }
76
+ return out;
77
+ }
78
+ const stride = Math.max(1, Math.ceil(decades / Math.max(1, count)));
79
+ for (let e = first; e <= last && out.length < 1000; e += stride)
80
+ out.push(clean(b ** e));
81
+ return out;
82
+ }
83
+ }
84
+ /**
85
+ * Square root: gives zero a place (unlike a log) while giving small values more room than a linear scale does, as for
86
+ * counts or areas. Negative values come out NaN.
87
+ */
88
+ export class SqrtScale extends Scale {
89
+ #linear = new LinearScale(); // tidy ends and ticks are chosen in data space, where people read them
90
+ normalize(buf, count, stride, offset, range) {
91
+ const lo = Math.sqrt(range.min), span = Math.sqrt(range.max) - lo;
92
+ const inv = span > 0 ? 1 / span : 0;
93
+ for (let k = 0, i = offset; k < count; k++, i += stride) {
94
+ // 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
95
+ const v = buf[i];
96
+ buf[i] = v >= 0 ? (Math.sqrt(v) - lo) * inv : NaN;
97
+ }
98
+ }
99
+ denormalize(f, r) {
100
+ const lo = Math.sqrt(r.min), u = lo + f * (Math.sqrt(r.max) - lo);
101
+ return u * u;
102
+ }
103
+ /** The low end cannot go below zero. */
104
+ constrain(r) { return r.min < 0 ? { min: 0, max: Math.max(r.max, 0) } : r; }
105
+ nice(r) { return this.constrain(this.#linear.nice(r)); }
106
+ ticks(r, count) { return this.#linear.ticks(r, count); }
107
+ }
108
+ /**
109
+ * Categories: the domain values 0, 1, 2… are positions of equal bands, one per category, and a value is drawn at the
110
+ * middle of its band. The range is fixed by the categories (data cannot change it), the ticks are the categories, and
111
+ * every label is the category's name. Labels are never thinned, so give a long list room or a short name.
112
+ */
113
+ export class BandScale extends Scale {
114
+ get n() { return Math.max(1, this.attrs.categories.length); }
115
+ /** Width of the drawn part of one band as a fraction of the range, for a renderer that draws bars. */
116
+ get bandwidth() { return (1 - (this.attrs.gap ?? 0.2)) / this.n; }
117
+ normalize(buf, count, stride, offset, range) {
118
+ const span = range.max - range.min, inv = span > 0 ? 1 / span : 0;
119
+ // 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
120
+ for (let k = 0, i = offset; k < count; k++, i += stride)
121
+ buf[i] = (buf[i] - range.min) * inv;
122
+ }
123
+ denormalize(f, r) { return r.min + f * (r.max - r.min); }
124
+ /** Half a band either side of the first and last category, whatever the data or attributes asked. */
125
+ constrain() { return { min: -0.5, max: this.n - 0.5 }; }
126
+ nice(r) { return r; }
127
+ ticks(r) {
128
+ const out = [];
129
+ for (let k = Math.max(0, Math.ceil(r.min)); k <= Math.min(this.n - 1, Math.floor(r.max)); k++)
130
+ out.push(k);
131
+ return out;
132
+ }
133
+ label(value) { return this.attrs.categories[Math.round(value)]; }
134
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Elements to and from plain JSON. Attributes are already plain data and children are ordered, so a tree maps directly to
3
+ * `{ type, attrs, children }`, which is what lets adapters, saved configs and an editor share one format.
4
+ *
5
+ * const json = toJSON(chart, registry); // JSON.stringify-able
6
+ * const copy = fromJSON(json, registry); // a new, unattached tree
7
+ * applyJSON(chart, editedJson, registry); // the same tree, changed to match
8
+ *
9
+ * The registry
10
+ * ────────────
11
+ * `type` is the name a class is registered under (`registry.ts` has all of the library's). An element is written under the
12
+ * name its exact class has, so a class of your own is one more entry: `{ ...registry, MyScale }`. Naming a class that is
13
+ * not in the registry is an error that says so, with the place in the tree.
14
+ *
15
+ * What an attribute may hold
16
+ * ──────────────────────────
17
+ * - Plain data goes through as it is: strings, booleans, finite numbers, arrays, plain objects (themes, `style`, bindings).
18
+ * - A number that is not finite (`NaN`, `Infinity`) is `{ "$number": "NaN" }`, since JSON has no such thing.
19
+ * - A typed array is `{ "$typed": "Float64Array", "data": [...] }` with `null` for NaN, so a gap in the data survives.
20
+ * - An element is its `id` (which is what every attribute that takes an element accepts), so it must have one.
21
+ * - Anything else JSON cannot hold, which is chiefly code (a `format` function, an `onClick`) and big or live data (an
22
+ * array of rows you go on editing), is `{ "$ref": "name" }`, and `name` is looked up in the `Refs` you give when reading.
23
+ *
24
+ * Refs
25
+ * ────
26
+ * A `Refs` is a table of names for objects and functions. Writing looks values up in it by identity, so a theme you named
27
+ * `lightTheme` is written as `{ "$ref": "lightTheme" }` rather than spelled out. With `auto: true` it names what it is
28
+ * asked about and does not know (`onFlush`, `format2`…), so any chart can be written; without it, an unnamed function is an
29
+ * error that tells you what to name. Arrays longer than `inlineLimit` (default 1000) count as data too and go by reference:
30
+ * that is the line between a chart's configuration and its data. Reading needs the same table.
31
+ *
32
+ * `applyJSON` changes a live tree to match edited JSON, in place: an attribute that already reads the same is left alone
33
+ * (so a ref'd array keeps its identity, and a decimator its cache), one that differs is set, one that is gone is removed, and
34
+ * children are matched by position and type, replaced if the type changed. It works out everything first and changes
35
+ * nothing if the JSON turns out to be unreadable, so a bad edit leaves the chart as it was.
36
+ */
37
+ import { type AnyArgs, type AnyElement } from './element.ts';
38
+ /** The JSON form of one element: its registered class name, attributes, and children in order. */
39
+ export interface ElementJSON {
40
+ type: string;
41
+ attrs?: Record<string, unknown>;
42
+ children?: ElementJSON[];
43
+ }
44
+ export type ElementRegistry = Readonly<Record<string, new (...args: AnyArgs) => AnyElement>>;
45
+ /** Names for objects and functions that JSON cannot hold, so that writing can point at them and reading can find them again. */
46
+ export declare class Refs {
47
+ #private;
48
+ /** Name whatever is asked about and not yet named, after the attribute it was found in. */
49
+ auto: boolean;
50
+ constructor(values?: Readonly<Record<string, unknown>>, options?: {
51
+ auto?: boolean;
52
+ });
53
+ /** Name a value. The name must be free; a value that already has a name keeps it first and answers to both on reading. */
54
+ set(name: string, value: unknown): this;
55
+ has(name: string): boolean;
56
+ get(name: string): unknown;
57
+ /** The name a value goes by, if it has one. */
58
+ nameOf(value: unknown): string | undefined;
59
+ /** Give a value a name of its own, built on `hint` and made unique. Returns the one it has already, if so. */
60
+ adopt(value: unknown, hint?: string): string;
61
+ get names(): readonly string[];
62
+ }
63
+ export interface SerializeOptions {
64
+ /** Where functions and other things JSON cannot hold are found, and named. */
65
+ refs?: Refs;
66
+ /** Arrays longer than this are data and go by reference, not inline. Default 1000. */
67
+ inlineLimit?: number;
68
+ }
69
+ /** An element and everything below it as JSON. See the top of this file for what an attribute may hold. */
70
+ export declare function toJSON(element: AnyElement, registry: ElementRegistry, options?: SerializeOptions): ElementJSON;
71
+ /** A new, unattached element tree from JSON. `refs` is where the `$ref`s are found. */
72
+ export declare function fromJSON<T extends AnyElement = AnyElement>(json: ElementJSON, registry: ElementRegistry, options?: SerializeOptions): T;
73
+ /**
74
+ * Change a live tree to match JSON, in place, so state it holds (a zoom, a data source's identity) is kept wherever the JSON
75
+ * does not say otherwise. Nothing is changed unless all of the JSON reads; the root's type has to be the same.
76
+ */
77
+ export declare function applyJSON(element: AnyElement, json: ElementJSON, registry: ElementRegistry, options?: SerializeOptions): void;