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
package/dist/attrs.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Attribute names and types for the places that only have strings and flat names: XML-like syntaxes (JSX, HTML).
3
+ *
4
+ * Two jobs, both small:
5
+ *
6
+ * 1. `expandAttributes` turns hyphenated names into nested objects, because those syntaxes carry only primitives:
7
+ * `meta-title="Sales"` is `{ meta: { title: 'Sales' } }`. Having both an object and nested names for one key is an error.
8
+ * 2. Every element class lists, in `static attrTypes`, the attributes an HTML parser would get wrong: names with capitals
9
+ * (HTML lowercases them, so `strokewidth` must find `strokeWidth`) and text that looks like something else (a title
10
+ * of `2024` must stay text). Attributes that hold objects list their parts the same way, nested:
11
+ * `{ meta: { title: STRING }, padding: { topSide: NUMBER } }`. Everything else is left to automatic coercion.
12
+ * The compiler enforces the list (`AttrTypesFor`), so a new attribute cannot be forgotten.
13
+ *
14
+ * Only the Web Components adapter reads `attrTypes`; the classes carry them as plain data, which costs a few bytes each.
15
+ */
16
+ /** How an HTML attribute's text becomes a value. AUTO: `true`/`false` and numbers convert, `[…]` and `{…}` are JSON, the rest is text. */
17
+ export const AUTO = 0;
18
+ /** Always text: titles, names, ids, colours. Needed wherever the text might look like a number or a boolean. */
19
+ export const STRING = 1;
20
+ /** A number, whatever it looks like. */
21
+ export const NUMBER = 2;
22
+ /** A boolean: present with no value, or `true`, is true; `false` is false. */
23
+ export const BOOLEAN = 3;
24
+ /**
25
+ * The attributes an element takes, from a flat set of names and values in which hyphens mean nesting.
26
+ * Names without hyphens pass through, and values are never copied, so objects and functions stay the same objects.
27
+ * An attribute whose value is `undefined` counts as not given for conflicts, but stays in the result when nothing else claims the key.
28
+ * Throws when one key is given both as a value and as a path to nested values (`meta` and `meta-title`, or `a-b` and `a-b-c`),
29
+ * when a name has an empty part (`a--b`), or for `__proto__`.
30
+ */
31
+ export function expandAttributes(flat, options = {}) {
32
+ const out = {};
33
+ /** The objects made here, as opposed to ones the caller passed, which must not be written into. */
34
+ const made = new WeakSet();
35
+ for (const key of Object.keys(flat)) {
36
+ const raw = key.split('-');
37
+ const path = [];
38
+ for (const part of raw) {
39
+ if (part === '')
40
+ throw new Error(`attribute '${key}': empty name part`);
41
+ const name = options.name ? options.name(part, path) : part;
42
+ if (name === '__proto__')
43
+ throw new Error(`attribute '${key}': not allowed`);
44
+ path.push(name);
45
+ }
46
+ let at = out;
47
+ for (let i = 0; i < path.length - 1; i++) {
48
+ // 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
49
+ const name = path[i];
50
+ const have = Object.hasOwn(at, name) ? at[name] : undefined;
51
+ if (have === undefined) {
52
+ const next = {};
53
+ made.add(next);
54
+ at[name] = next;
55
+ at = next;
56
+ }
57
+ else if (typeof have === 'object' && have !== null && made.has(have)) {
58
+ // oxlint-disable-next-line typescript/consistent-type-assertions -- `made` holds only the objects this function created, so a member of it is one of our `Obj`
59
+ at = have;
60
+ }
61
+ else {
62
+ throw new Error(`attribute '${key}' conflicts with '${path.slice(0, i + 1).join('-')}', which is a value`);
63
+ }
64
+ }
65
+ // 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
66
+ const last = path[path.length - 1];
67
+ const value = options.value ? options.value(path, flat[key]) : flat[key];
68
+ if (Object.hasOwn(at, last)) {
69
+ // An attribute given as `undefined` means "not set", so it never conflicts with the same key given nested.
70
+ if (value === undefined)
71
+ continue;
72
+ const have = at[last];
73
+ if (have === undefined) {
74
+ at[last] = value;
75
+ continue;
76
+ }
77
+ throw new Error(typeof have === 'object' && have !== null && made.has(have)
78
+ ? `attribute '${key}' conflicts with '${key}-…', which are nested values`
79
+ : `attribute '${key}' is given twice`);
80
+ }
81
+ at[last] = value;
82
+ }
83
+ return out;
84
+ }
package/dist/axis.d.ts ADDED
@@ -0,0 +1,137 @@
1
+ import { Dimension, type DimensionAttributes } from './dimension.ts';
2
+ import { type ElementRef } from './element.ts';
3
+ import { type ChartEvent } from './events.ts';
4
+ import { type Side } from './geometry.ts';
5
+ import type { Plane } from './plane.ts';
6
+ export type CartesianOrientation = 'top' | 'bottom' | 'left' | 'right';
7
+ export type PolarOrientation = 'angle' | 'radius';
8
+ export type AxisOrientation = CartesianOrientation | PolarOrientation;
9
+ /** Horizontal axes run left-right, so their labels sit above or below; vertical ones put them beside. */
10
+ export declare const isHorizontal: (o: AxisOrientation) => o is "bottom" | "top";
11
+ /** Narrows to the sides a Cartesian plane can draw. Polar sides belong to a PolarPlane. */
12
+ export declare function cartesianSide(o: AxisOrientation): CartesianOrientation;
13
+ /** Narrows to the sides a PolarPlane can draw. */
14
+ export declare function polarSide(o: AxisOrientation): PolarOrientation;
15
+ /**
16
+ * The curve an axis is drawn along, and what that means for its ticks and labels. Whatever the shape of the plot (a
17
+ * rectangle's side, a rim, a spoke) the plane builds one for each axis, so an axis knows nothing about the plane.
18
+ */
19
+ export interface Ruler {
20
+ /** Pixels along it: what the ticks have to fit into. */
21
+ readonly length: number;
22
+ /** About how many pixels each tick needs, labels included. */
23
+ readonly tickSpacing: number;
24
+ /** True when its labels sit above or below it, so only their height counts across it; false when their width does. */
25
+ readonly horizontal: boolean;
26
+ /** The curve as path data. Read when drawn, so it may depend on what the plane settles last. */
27
+ readonly line: string;
28
+ /** The point a fraction 0..1 along it, and the unit vector pointing away from the plot there: x, y, nx, ny. */
29
+ at(fraction: number): readonly [x: number, y: number, nx: number, ny: number];
30
+ /**
31
+ * How far along it a position in the plot is, as a fraction 0..1 (outside that range for positions beyond its ends),
32
+ * so that dragging and zooming can follow the pointer along it. Absent for a curve that cannot be dragged along.
33
+ */
34
+ fractionAt?(x: number, y: number): number;
35
+ /**
36
+ * Where the title goes for an axis whose title asks to be on `position` (default: the axis' own side). Titles sit on
37
+ * the axis line, past its labels, where an axis is fixed to the side it is on; otherwise at the middle of the edge
38
+ * of the plot, past whatever the plane has put there.
39
+ */
40
+ title(position: Side | undefined): TitleSpot;
41
+ }
42
+ /**
43
+ * A place for a title: a point, the unit vector pointing away from the plot there, and whether the title belongs past
44
+ * the axis' own ticks and labels (the point is on the axis line) or the point is already where the title starts.
45
+ */
46
+ export interface TitleSpot {
47
+ x: number;
48
+ y: number;
49
+ nx: number;
50
+ ny: number;
51
+ beyond: boolean;
52
+ }
53
+ export interface AxisAttributes extends DimensionAttributes {
54
+ /**
55
+ * The side of the plot the axis sits on. Default: 'bottom' for the first axis in a plane, then 'left',
56
+ * 'top', 'right'. Axes sharing a side stack outwards from the plot in child order.
57
+ */
58
+ orientation?: AxisOrientation;
59
+ /**
60
+ * The edge of the plot the title sits at: past the axis' labels for an axis fixed to that side, otherwise at the middle of
61
+ * the edge, outside any axes there, which then take the room it needs. Default: the axis' own side. Cartesian planes only.
62
+ */
63
+ titlePosition?: Side;
64
+ /**
65
+ * Whether the title runs along the horizontal or the vertical. Default: horizontal at the top and bottom, vertical at the
66
+ * left and right. Cartesian planes only.
67
+ */
68
+ titleDirection?: 'horizontal' | 'vertical';
69
+ /**
70
+ * Float across the plot at this value of the crossing axis (the other one, or `crossAxis`), instead of sitting on a
71
+ * side. Where that is outside the range, the axis stays at the nearest edge. A floating axis takes no room from the
72
+ * plot, except where it sits at the edge on its own side, which is just where a fixed axis would be. Its labels
73
+ * overlay the plot, so a halo on the labels (`style: { tickLabel: { halo: '#fff' } }`) is worth setting. Its title, unless told otherwise, is at the edge of
74
+ * the plot on the axis' side. Cartesian planes only.
75
+ */
76
+ crossesAt?: number;
77
+ /** The axis `crossesAt` is a value of. Default: the plane's first axis on the other orientation. */
78
+ crossAxis?: ElementRef<Axis>;
79
+ }
80
+ /** A spatial dimension, drawn along a ruler its plane gives it. Needs one scale child: `new Axis({}, new LinearScale())`. */
81
+ export declare class Axis extends Dimension<Plane, AxisAttributes> {
82
+ #private;
83
+ static attrTypes: {
84
+ title: 1;
85
+ tickCount: 2;
86
+ tickStep: 2;
87
+ locale: 1;
88
+ onViewChange: 0;
89
+ titlePosition: 1;
90
+ titleDirection: 1;
91
+ crossesAt: 2;
92
+ crossAxis: 1;
93
+ id: 1;
94
+ meta: {
95
+ title: 1;
96
+ description: 1;
97
+ role: 1;
98
+ };
99
+ };
100
+ get orientation(): AxisOrientation;
101
+ /** The curve it is drawn along. Valid after arrange. */
102
+ get ruler(): Ruler;
103
+ /** Position in the stack for its side, 0 being the one nearest the plot. */
104
+ get slot(): number;
105
+ /** Space it needs perpendicular to the plot edge. Valid after it has been measured. */
106
+ get thickness(): number;
107
+ /** Room a title needs at the edge of the plot, when it is not part of the axis' own thickness. Valid after it has been measured. */
108
+ get titleThickness(): number;
109
+ /** The edge of the plot the title is at. */
110
+ get titleSide(): Side;
111
+ /** Half the width of the widest label: how far the first or last label may stick out past the plot. */
112
+ get labelOverhang(): number;
113
+ /** Which side, and which place in that side's stack. */
114
+ assign(orientation: AxisOrientation, slot: number): void;
115
+ /** The curve to draw along. The plane sets it before measuring, and again once the plot is settled. */
116
+ setRuler(ruler: Ruler): void;
117
+ protected tickCountFor(length: number): number;
118
+ /**
119
+ * Everything the axis draws with, settled in one place so measuring and painting always agree on sizes. The axis' own
120
+ * colour (`style: { axis: { color } }`) reaches its labels, line and ticks alike because all three chains pass through `axis`.
121
+ */
122
+ private look;
123
+ onEvent(ev: ChartEvent): void;
124
+ /**
125
+ * Works out `thickness` (room across the plot edge for ticks, labels and a title on the axis line) and
126
+ * `titleThickness` (room for a title that sits at the plot's edge instead), and answers the MEASURE request.
127
+ */
128
+ private measure;
129
+ /** Which way the title text runs: as asked, else along the edge it is at. */
130
+ private titleVertical;
131
+ /** True when the text runs across the edge it is at, so its length is what takes room there. */
132
+ private titleRotated;
133
+ /** How much room the labels take in the direction away from the plot. */
134
+ private labelsAcross;
135
+ /** The line, a mark and a label at each tick, each facing away from the plot, then the title. */
136
+ private paint;
137
+ }
package/dist/axis.js ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Axes: a dimension drawn along one side of a plane.
3
+ *
4
+ * An axis does two jobs. As a `Dimension` it owns a scale and a range, and answers value ↔ fraction. As a drawing it
5
+ * paints a line, ticks, labels and a title along a `Ruler` that its plane supplies, so the same code serves the side of
6
+ * a rectangle, a floating cross-line, a rim and a spoke. The plane tells it where (`assign`, `setRuler`); the axis tells
7
+ * the plane how much room it needs (the MEASURE event → `thickness`, `titleThickness`, `labelOverhang`).
8
+ */
9
+ import { NUMBER, STRING } from './attrs.js';
10
+ import { Dimension } from './dimension.js';
11
+ import { fail } from './element.js';
12
+ import { hideFromAssistive, writeAria } from './aria.js';
13
+ import { EVENT_ATTACH, EVENT_MEASURE, EVENT_PAINT } from './events.js';
14
+ import { f2 } from './geometry.js';
15
+ import { estimateTextWidth } from './platform.js';
16
+ import { applyText } from './text.js';
17
+ /** Horizontal axes run left-right, so their labels sit above or below; vertical ones put them beside. */
18
+ export const isHorizontal = (o) => o === 'top' || o === 'bottom';
19
+ /** Narrows to the sides a Cartesian plane can draw. Polar sides belong to a PolarPlane. */
20
+ export function cartesianSide(o) {
21
+ return o === 'angle' || o === 'radius' ? fail(`an axis on the "${o}" side needs a PolarPlane`) : o;
22
+ }
23
+ /** Narrows to the sides a PolarPlane can draw. */
24
+ export function polarSide(o) {
25
+ return o === 'angle' || o === 'radius' ? o : fail(`an axis on the "${o}" side needs a CartesianPlane`);
26
+ }
27
+ /** What an axis has before its plane has placed it. */
28
+ const UNPLACED = {
29
+ length: 1, tickSpacing: 70, horizontal: true, line: '', at: () => [0, 0, 0, 1], title: () => ({ x: 0, y: 0, nx: 0, ny: 1, beyond: true }),
30
+ };
31
+ /** A spatial dimension, drawn along a ruler its plane gives it. Needs one scale child: `new Axis({}, new LinearScale())`. */
32
+ export class Axis extends Dimension {
33
+ static attrTypes = { ...Dimension.attrTypes, titlePosition: STRING, titleDirection: STRING, crossesAt: NUMBER, crossAxis: STRING };
34
+ #orientation = 'bottom';
35
+ #slot = 0;
36
+ #thickness = 0;
37
+ #titleThickness = 0;
38
+ #titleAcross = 0;
39
+ #ruler = UNPLACED;
40
+ #labelCount = 0;
41
+ #labelWidth = 0;
42
+ get orientation() { return this.#orientation; }
43
+ /** The curve it is drawn along. Valid after arrange. */
44
+ get ruler() { return this.#ruler; }
45
+ /** Position in the stack for its side, 0 being the one nearest the plot. */
46
+ get slot() { return this.#slot; }
47
+ /** Space it needs perpendicular to the plot edge. Valid after it has been measured. */
48
+ get thickness() { return this.#thickness; }
49
+ /** Room a title needs at the edge of the plot, when it is not part of the axis' own thickness. Valid after it has been measured. */
50
+ get titleThickness() { return this.#titleThickness; }
51
+ /** The edge of the plot the title is at. */
52
+ get titleSide() { return this.attrs.titlePosition ?? cartesianSide(this.#orientation); }
53
+ /** Half the width of the widest label: how far the first or last label may stick out past the plot. */
54
+ get labelOverhang() { return this.#labelWidth / 2; }
55
+ // ─── Engine protocol: the plane says where the axis goes. ───
56
+ /** Which side, and which place in that side's stack. */
57
+ assign(orientation, slot) {
58
+ this.#orientation = orientation;
59
+ this.#slot = slot;
60
+ }
61
+ /** The curve to draw along. The plane sets it before measuring, and again once the plot is settled. */
62
+ setRuler(ruler) { this.#ruler = ruler; }
63
+ tickCountFor(length) {
64
+ return Math.max(2, Math.min(12, Math.floor(length / this.#ruler.tickSpacing)));
65
+ }
66
+ /**
67
+ * Everything the axis draws with, settled in one place so measuring and painting always agree on sizes. The axis' own
68
+ * colour (`style: { axis: { color } }`) reaches its labels, line and ticks alike because all three chains pass through `axis`.
69
+ */
70
+ look() {
71
+ return {
72
+ metrics: this.styleFor(['axis']),
73
+ labels: this.styleFor(['tickLabel', 'axis', 'text']),
74
+ title: this.styleFor(['axisTitle', 'axis', 'text']),
75
+ line: this.styleFor(['axisLine', 'axis', 'line']),
76
+ };
77
+ }
78
+ onEvent(ev) {
79
+ switch (ev.type) {
80
+ case EVENT_ATTACH:
81
+ this.surface.wrapper(); // the text style below is inherited by every label
82
+ break;
83
+ case EVENT_MEASURE:
84
+ this.measure(ev);
85
+ break;
86
+ case EVENT_PAINT:
87
+ this.paint();
88
+ break;
89
+ }
90
+ }
91
+ /**
92
+ * Works out `thickness` (room across the plot edge for ticks, labels and a title on the axis line) and
93
+ * `titleThickness` (room for a title that sits at the plot's edge instead), and answers the MEASURE request.
94
+ */
95
+ measure(ev) {
96
+ const s = this.look(), ruler = this.#ruler;
97
+ const chart = this.chart;
98
+ // 'inherit' means whatever the SVG gives; measuring needs a concrete family, so a generic one stands in.
99
+ const font = (r) => ({ family: r.family === 'inherit' ? 'sans-serif' : r.family, size: r.size, weight: r.weight });
100
+ const measureText = chart ? chart.measureText : estimateTextWidth;
101
+ let widest = 0;
102
+ for (const t of this.getTicks(ruler.length))
103
+ widest = Math.max(widest, measureText(t.label, font(s.labels)).width);
104
+ // The plot area is whatever the labels leave, so labels changing width would move the plot under your hand while you
105
+ // zoom. Widths grow at once but only shrink once there is a clear gain (a margin of about a digit and a half).
106
+ if (widest > this.#labelWidth || widest < this.#labelWidth - s.labels.size * 1.5)
107
+ this.#labelWidth = widest;
108
+ // The title takes room across the edge it is at: its height, or its length when it runs along the way it is stacked.
109
+ const title = this.attrs.title, spot = ruler.title(this.attrs.titlePosition);
110
+ let across = 0;
111
+ if (title) {
112
+ across = this.titleRotated(spot) ? measureText(title, font(s.title)).width : s.title.size * 1.2;
113
+ this.#titleAcross = across;
114
+ }
115
+ this.#thickness = s.metrics.tickLength + s.metrics.labelGap + this.labelsAcross(s.labels.size) + (title && spot.beyond ? s.metrics.spacing + across : 0) + s.metrics.spacing;
116
+ this.#titleThickness = title && !spot.beyond ? across + s.metrics.spacing : 0;
117
+ ev.request = ruler.horizontal
118
+ ? { min: { width: 0, height: this.#thickness }, max: { width: Infinity, height: this.#thickness } }
119
+ : { min: { width: this.#thickness, height: 0 }, max: { width: this.#thickness, height: Infinity } };
120
+ }
121
+ /** Which way the title text runs: as asked, else along the edge it is at. */
122
+ titleVertical(spot) {
123
+ const asked = this.attrs.titleDirection;
124
+ return asked ? asked === 'vertical' : Math.abs(spot.nx) > Math.abs(spot.ny);
125
+ }
126
+ /** True when the text runs across the edge it is at, so its length is what takes room there. */
127
+ titleRotated(spot) { return this.titleVertical(spot) === (Math.abs(spot.ny) >= Math.abs(spot.nx)); }
128
+ /** How much room the labels take in the direction away from the plot. */
129
+ labelsAcross(fontSize) { return this.#ruler.horizontal ? fontSize * 1.15 : this.#labelWidth; }
130
+ /** The line, a mark and a label at each tick, each facing away from the plot, then the title. */
131
+ paint() {
132
+ const s = this.look(), surface = this.surface, ruler = this.#ruler, fs = s.labels.size;
133
+ let d = ruler.line;
134
+ const labels = [];
135
+ for (const t of this.getTicks(ruler.length)) {
136
+ const f = this.fractionOf(t.value);
137
+ if (!(f >= -1e-9 && f <= 1 + 1e-9))
138
+ continue;
139
+ const [x, y, nx, ny] = ruler.at(f);
140
+ d += `M${f2(x)} ${f2(y)}l${f2(nx * s.metrics.tickLength)} ${f2(ny * s.metrics.tickLength)}`;
141
+ const out = s.metrics.tickLength + s.metrics.labelGap;
142
+ labels.push({ text: t.label, x: x + nx * out, y: y + ny * out + fs * (0.35 + 0.6 * ny), nx });
143
+ }
144
+ applyText(surface, surface.wrapper(), s.labels);
145
+ // The numbers on an axis are read to no use one by one; a screen reader gets the summary, or the axis' own `meta`.
146
+ if (this.attrs.meta)
147
+ writeAria(surface, surface.wrapper(), this.attrs.meta, { role: 'group' });
148
+ else
149
+ hideFromAssistive(surface, surface.wrapper());
150
+ const line = surface.node('axis', 'path');
151
+ surface.set(line, 'd', d);
152
+ surface.set(line, 'fill', 'none');
153
+ surface.set(line, 'stroke', s.line.color);
154
+ surface.set(line, 'stroke-width', s.line.width);
155
+ surface.set(line, 'stroke-dasharray', s.line.dash);
156
+ surface.set(line, 'opacity', s.line.opacity);
157
+ labels.forEach((l, i) => {
158
+ const n = surface.node(`label-${i}`, 'text');
159
+ surface.set(n, 'x', f2(l.x));
160
+ surface.set(n, 'y', f2(l.y));
161
+ surface.set(n, 'text-anchor', l.nx > 0.3 ? 'start' : l.nx < -0.3 ? 'end' : 'middle');
162
+ surface.text(n, l.text);
163
+ });
164
+ for (let i = labels.length; i < this.#labelCount; i++)
165
+ surface.remove(`label-${i}`);
166
+ this.#labelCount = labels.length;
167
+ // The title is centred in the room it has: past the labels beside the axis line, or past whatever else is at the edge.
168
+ const title = this.attrs.title;
169
+ if (!title) {
170
+ surface.remove('title');
171
+ return;
172
+ }
173
+ const spot = ruler.title(this.attrs.titlePosition);
174
+ const distance = (spot.beyond ? s.metrics.tickLength + s.metrics.labelGap + this.labelsAcross(fs) + s.metrics.spacing : s.metrics.spacing / 2) + this.#titleAcross / 2;
175
+ const cx = spot.x + spot.nx * distance, cy = spot.y + spot.ny * distance;
176
+ const n = surface.node('title', 'text');
177
+ applyText(surface, n, s.title, 'middle');
178
+ surface.set(n, 'x', 0);
179
+ surface.set(n, 'y', f2(s.title.size * 0.35));
180
+ // Text that runs up the page reads from the bottom; on the right of the plot it is turned the other way, as is usual.
181
+ const turn = this.titleVertical(spot) ? (spot.nx > 0 ? 90 : -90) : 0;
182
+ surface.set(n, 'transform', `translate(${f2(cx)} ${f2(cy)})${turn ? ` rotate(${turn})` : ''}`);
183
+ surface.text(n, title);
184
+ }
185
+ }
package/dist/bars.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ import type { HitMode } from './hit.ts';
2
+ import type { PlatformNode } from './platform.ts';
3
+ import { type PolygonSet, SeriesRenderer, type SeriesRendererAttributes } from './renderer.ts';
4
+ import type { Series } from './series.ts';
5
+ import type { Surface } from './surface.ts';
6
+ export interface BarOptions {
7
+ /** Fraction of the room each bar fills. Default: a band's own `bandwidth`, else 0.8. */
8
+ width?: number;
9
+ /** The value bars grow from when there is no stack to say. Default 0. */
10
+ baseline?: number;
11
+ }
12
+ /** Everything about a series' bars that does not depend on how they are drawn, in fractions of the axes. */
13
+ export interface BarLayout {
14
+ readonly n: number;
15
+ /** Centre along the domain, start and end along the value, for each bar: three numbers a row. */
16
+ readonly norm: Float64Array;
17
+ /** The width of one bar's slot along the domain, and how far this series' slot is from the centre. */
18
+ readonly slot: number;
19
+ readonly shift: number;
20
+ }
21
+ export declare function layoutBars(series: Series, options: BarOptions): BarLayout | null;
22
+ /**
23
+ * How far along the domain (as a fraction of the axis) a series' own bars sit from the point's place, when it shares a band
24
+ * with others (`Stack` in 'group' mode). What goes on the bars (error bars) uses it to stay on them. 0 for a series alone.
25
+ */
26
+ export declare function barShift(series: Series, options?: BarOptions): number;
27
+ /**
28
+ * The outline of every bar of a series in plot pixels, `perShape` points each, in the order of the series' points
29
+ * (a point that is a gap leaves a shape of NaN). Null before the first projection.
30
+ */
31
+ export declare function barShapes(series: Series, options?: BarOptions): PolygonSet | null;
32
+ /**
33
+ * The stem of every bar: a line from its foot to its value, two points each, as a `PolygonSet` of two-point shapes (so a
34
+ * hit measures the distance to the line). Straight on every plane, since a line along the value is a straight line (a
35
+ * spoke) on a polar one. Gaps leave NaN.
36
+ */
37
+ export declare function stemLines(series: Series, options?: BarOptions): PolygonSet | null;
38
+ export interface BarRendererAttributes extends SeriesRendererAttributes, BarOptions {
39
+ }
40
+ /**
41
+ * What every bar renderer shares: where bars start and how wide they are (`BarOptions`), that the value axis has to reach
42
+ * the baseline, and that a pointer hit is measured against the geometry it draws. A subclass says what that geometry is
43
+ * (`outline`) and how to draw it.
44
+ */
45
+ export declare abstract class BarRenderer<A extends BarRendererAttributes = BarRendererAttributes> extends SeriesRenderer<A> {
46
+ protected abstract outline(): PolygonSet | null;
47
+ get includes(): readonly number[];
48
+ /** Half of a bar's room: its width, in the gaps between points (a band's own bars fit it already). */
49
+ get spans(): number;
50
+ get hitMode(): HitMode;
51
+ shapes(): PolygonSet | null;
52
+ }
53
+ export interface BarPathAttributes extends BarRendererAttributes {
54
+ }
55
+ /** One path holding every bar of the series: the cost of a bar chart is its path text, not a node for each bar. */
56
+ export declare class BarPath extends BarRenderer<BarPathAttributes> {
57
+ private look;
58
+ get swatch(): string;
59
+ protected outline(): PolygonSet | null;
60
+ sample(s: Surface, parent: PlatformNode, key: string, width: number, height: number): void;
61
+ protected draw(): void;
62
+ }
63
+ export interface BarLinePathAttributes extends BarRendererAttributes {
64
+ /** How the end of a stem is finished: 'butt' (flat, at the value), 'round' or 'square' (both reach half a width past). Default 'butt'. */
65
+ cap?: 'butt' | 'round' | 'square';
66
+ }
67
+ /**
68
+ * Bars as lines: each is a stem from the foot to the value, stroked rather than filled, in one path. The width is the
69
+ * stroke's, so it stays the same however many bars there are, which is what makes it the answer for dense data where
70
+ * filled bars would be slivers. The look is the line's (`seriesLine`), with the series' colour as its stroke; give the
71
+ * series a marker as well for a lollipop.
72
+ */
73
+ export declare class BarLinePath extends BarRenderer<BarLinePathAttributes> {
74
+ private look;
75
+ get swatch(): string;
76
+ protected outline(): PolygonSet | null;
77
+ /** Three stems of different heights, as wide as the stroke says. */
78
+ sample(s: Surface, parent: PlatformNode, key: string, width: number, height: number): void;
79
+ protected draw(): void;
80
+ }