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,116 @@
1
+ import { DataSource, type DataSourceAttributes, type FillPlan, type FillResult, type PlanContext, type PointView } from './data.ts';
2
+ import type { Extent } from './geometry.ts';
3
+ /** A row's number, by property name or by function. A function also gets the index, which is how a domain can be derived. */
4
+ export type Field<Row> = (keyof Row & (string | number)) | ((row: Row, index: number) => number);
5
+ export interface ObjectDataSourceAttributes<Row, Channel extends string = string> extends DataSourceAttributes {
6
+ source: readonly Row[];
7
+ /** Where each row is along the domain. Leave it out and it is the row's index: 0, 1, 2 ... in order. */
8
+ domain?: Field<Row>;
9
+ value: Field<Row>;
10
+ /** Extra columns that bindings can address by name: a size, a colour, a category, the x of an XY plot. */
11
+ channels?: Readonly<Record<Channel, Field<Row>>>;
12
+ }
13
+ /** Rows as objects. Convenient and not the fastest: reading goes through the field accessors, so the numbers are loaded into the managed buffer once per change. */
14
+ export declare class ObjectDataSource<Row = any, Channel extends string = string> extends DataSource<ObjectDataSourceAttributes<Row, Channel>, Channel> {
15
+ constructor(attrs: ObjectDataSourceAttributes<Row, Channel>);
16
+ /** Without a domain the rows are numbered in order, which is ascending by construction. */
17
+ protected get ordered(): boolean;
18
+ get pointCount(): number;
19
+ get channelNames(): readonly string[];
20
+ protected scanDomain(): Extent | null;
21
+ /** Rows are not numbers in an array, so reading them in place means loading them once into the managed buffer. */
22
+ protected orderedView(): PointView;
23
+ protected fill(out: Float64Array, context: PlanContext): FillResult;
24
+ }
25
+ export interface FlatDataSourceAttributes<Channel extends string = string> extends DataSourceAttributes {
26
+ /** Float64Array, Float32Array, or any numeric array. Read in place; never copied. */
27
+ data: ArrayLike<number>;
28
+ /** Numbers per point. Default 2 (x, y pairs), or 1 when `regularDomain` is set. */
29
+ stride?: number;
30
+ /** Where the domain and the value sit within a point's `stride` numbers. Default 0 and 1 (0 when there is no stored domain). */
31
+ domainOffset?: number;
32
+ /** Where the value sits within a point's `stride` numbers. Default 1 (0 when there is no stored domain). */
33
+ valueOffset?: number;
34
+ /** Number of points. Default: `data.length / stride`. */
35
+ count?: number;
36
+ /** Evenly spaced domain with no stored x values: a y-only array. */
37
+ regularDomain?: {
38
+ start: number;
39
+ step: number;
40
+ };
41
+ /** Extra columns that bindings can address by name, as offsets within a point's `stride` numbers. */
42
+ channels?: Readonly<Record<Channel, number>>;
43
+ }
44
+ /** Numbers in a typed array (or any numeric array), read where they are: the fast path for large data. */
45
+ export declare class FlatDataSource<Channel extends string = string> extends DataSource<FlatDataSourceAttributes<Channel>, Channel> {
46
+ static attrTypes: {
47
+ isSorted: 3;
48
+ id: 1;
49
+ meta: {
50
+ title: 1;
51
+ description: 1;
52
+ role: 1;
53
+ };
54
+ domainOffset: 2;
55
+ regularDomain: 0;
56
+ valueOffset: 2;
57
+ };
58
+ constructor(attrs: FlatDataSourceAttributes<Channel>);
59
+ private get stride();
60
+ get pointCount(): number;
61
+ get channelNames(): readonly string[];
62
+ /** A regular domain ascends by construction (if its step is positive); anything else says so with `isSorted`. */
63
+ protected get ordered(): boolean;
64
+ protected orderedView(): PointView;
65
+ protected scanDomain(): Extent | null;
66
+ protected fill(out: Float64Array, context: PlanContext): FillResult;
67
+ }
68
+ export interface SyntheticDataSourceAttributes extends DataSourceAttributes {
69
+ /** Value at a domain position. NaN is a gap. */
70
+ fn: (x: number) => number;
71
+ /** The span the function is defined over: the data's whole domain. */
72
+ domain: Extent;
73
+ /**
74
+ * Number of samples, evenly spaced across the domain, always. Leave it out and the source samples as finely as the
75
+ * plot needs: one per pixel of width across the part of the domain in view, so zooming in stays smooth and zooming
76
+ * out never draws more than the screen can show.
77
+ */
78
+ count?: number;
79
+ }
80
+ /**
81
+ * A function of the domain, sampled when drawn. There is no storage, so points are made up, not rows: they carry no
82
+ * `rowOf` and cannot be edited. Samples ascend by construction. It offers no channels.
83
+ */
84
+ export declare class SyntheticDataSource extends DataSource<SyntheticDataSourceAttributes> {
85
+ constructor(attrs: SyntheticDataSourceAttributes);
86
+ protected get ordered(): boolean;
87
+ get pointCount(): number;
88
+ protected scanDomain(): Extent | null;
89
+ /** How many samples, over which part of the domain, for this plot. */
90
+ private span;
91
+ protected fill(out: Float64Array, context: PlanContext): FillResult;
92
+ /** Room for the samples this plot asks for, which depends on its width; the points are made up, so there is no `rowOf`. */
93
+ plan(context: PlanContext): FillPlan;
94
+ }
95
+ /** A named column in a composite source: an array (typed or plain), or a function of the row index. */
96
+ export type Column = ArrayLike<number> | ((index: number) => number);
97
+ export interface CompositeDataSourceAttributes<Channel extends string = string> extends DataSourceAttributes {
98
+ count: number;
99
+ /** Where each row is along the domain: for a made-up domain, `(i) => start + step * i`. */
100
+ domain: Column;
101
+ value: Column;
102
+ /** Extra columns that bindings and tooltips can address by name. */
103
+ channels?: Readonly<Record<Channel, Column>>;
104
+ }
105
+ /**
106
+ * Each column kept by itself: the shape of column-oriented data (an `x` array and a `y` array, a table by column), and of
107
+ * data whose domain is made up, with any column a function of the row index. Nothing is copied from arrays that are
108
+ * already in order (`isSorted`); otherwise the columns are gathered into the managed buffer once per change.
109
+ */
110
+ export declare class CompositeDataSource<Channel extends string = string> extends DataSource<CompositeDataSourceAttributes<Channel>, Channel> {
111
+ constructor(attrs: CompositeDataSourceAttributes<Channel>);
112
+ get pointCount(): number;
113
+ get channelNames(): readonly string[];
114
+ protected scanDomain(): Extent | null;
115
+ protected fill(out: Float64Array, context: PlanContext): FillResult;
116
+ }
@@ -0,0 +1,237 @@
1
+ /** The data sources: rows of objects, flat typed arrays, generated points and separate columns. The contract and the base class are in data.ts. */
2
+ /* oxlint-disable typescript/no-non-null-assertion -- numeric kernels over flat typed-array buffers (stride-addressed rows): every index is bounded by the row count, which `noUncheckedIndexedAccess` cannot see */
3
+ import { AUTO, NUMBER } from './attrs.js';
4
+ import { DataSource, NO_POINTS, endsOf, num } from './data.js';
5
+ import { fail } from './element.js';
6
+ /** Rows as objects. Convenient and not the fastest: reading goes through the field accessors, so the numbers are loaded into the managed buffer once per change. */
7
+ // oxlint-disable-next-line typescript/no-explicit-any -- the default row type: untyped rows (from JSON, or JSX/React where no type argument can be written) must be accepted, and `unknown` would reject every field accessor
8
+ export class ObjectDataSource extends DataSource {
9
+ // oxlint-disable-next-line eslint/no-useless-constructor -- makes the attributes required (the base class accepts none)
10
+ constructor(attrs) { super(attrs); }
11
+ /** Without a domain the rows are numbered in order, which is ascending by construction. */
12
+ get ordered() { return this.attrs.domain === undefined || this.attrs.isSorted === true; }
13
+ get pointCount() { return this.attrs.source.length; }
14
+ get channelNames() { return Object.keys(this.attrs.channels ?? {}); }
15
+ scanDomain() {
16
+ const { source, domain } = this.attrs;
17
+ const n = source.length;
18
+ if (domain === undefined)
19
+ return n > 0 ? { min: 0, max: n - 1 } : null;
20
+ const read = typeof domain === 'function' ? domain : (row) => num(row[domain]);
21
+ return endsOf(n, (i) => read(source[i], i));
22
+ }
23
+ /** Rows are not numbers in an array, so reading them in place means loading them once into the managed buffer. */
24
+ orderedView() { return this.managedView(); }
25
+ fill(out, context) {
26
+ const { source, domain, value, channels } = this.attrs;
27
+ const xFn = typeof domain === 'function' ? domain : null;
28
+ const yFn = typeof value === 'function' ? value : null;
29
+ const names = context.channels, k = names.length, stride = 2 + k;
30
+ const cFns = names.map((name) => {
31
+ const f = channels?.[name] ?? fail(`the data has no channel "${name}"`);
32
+ return typeof f === 'function' ? f : (row) => num(row[f]);
33
+ });
34
+ const cMin = new Float64Array(k).fill(Infinity), cMax = new Float64Array(k).fill(-Infinity);
35
+ const n = source.length;
36
+ let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
37
+ for (let i = 0; i < n; i++) {
38
+ const row = source[i];
39
+ const x = xFn ? xFn(row, i) : domain === undefined ? i : num(row[domain]);
40
+ const y = yFn ? yFn(row, i) : num(row[value]);
41
+ out[stride * i] = x;
42
+ out[stride * i + 1] = y;
43
+ if (x < xMin)
44
+ xMin = x;
45
+ if (x > xMax)
46
+ xMax = x;
47
+ if (y < yMin)
48
+ yMin = y;
49
+ if (y > yMax)
50
+ yMax = y;
51
+ for (let c = 0; c < k; c++) {
52
+ const v = cFns[c](row, i);
53
+ out[stride * i + 2 + c] = v;
54
+ if (v < cMin[c])
55
+ cMin[c] = v;
56
+ if (v > cMax[c])
57
+ cMax[c] = v;
58
+ }
59
+ }
60
+ const extents = [{ min: xMin, max: xMax }, { min: yMin, max: yMax }];
61
+ for (let c = 0; c < k; c++)
62
+ extents.push({ min: cMin[c], max: cMax[c] });
63
+ return { count: n, extents };
64
+ }
65
+ }
66
+ /** Numbers in a typed array (or any numeric array), read where they are: the fast path for large data. */
67
+ export class FlatDataSource extends DataSource {
68
+ static attrTypes = { ...DataSource.attrTypes, domainOffset: NUMBER, regularDomain: AUTO, valueOffset: NUMBER };
69
+ // oxlint-disable-next-line eslint/no-useless-constructor -- makes the attributes required (the base class accepts none)
70
+ constructor(attrs) { super(attrs); }
71
+ get stride() { return this.attrs.stride ?? (this.attrs.regularDomain ? 1 : 2); }
72
+ get pointCount() { return this.attrs.count ?? Math.floor(this.attrs.data.length / this.stride); }
73
+ get channelNames() { return Object.keys(this.attrs.channels ?? {}); }
74
+ /** A regular domain ascends by construction (if its step is positive); anything else says so with `isSorted`. */
75
+ get ordered() {
76
+ const r = this.attrs.regularDomain;
77
+ return r ? r.step > 0 : this.attrs.isSorted === true;
78
+ }
79
+ orderedView() {
80
+ const a = this.attrs, r = a.regularDomain;
81
+ return {
82
+ data: a.data,
83
+ count: this.pointCount,
84
+ stride: this.stride,
85
+ domainOffset: a.domainOffset ?? 0,
86
+ valueOffset: a.valueOffset ?? (r ? 0 : 1),
87
+ regularDomain: r ?? null,
88
+ };
89
+ }
90
+ scanDomain() {
91
+ const { data, regularDomain: r, domainOffset = 0 } = this.attrs;
92
+ const n = this.pointCount;
93
+ if (n === 0)
94
+ return null;
95
+ if (r)
96
+ return { min: r.start, max: r.start + r.step * (n - 1) };
97
+ const stride = this.stride;
98
+ return endsOf(n, (i) => data[i * stride + domainOffset]);
99
+ }
100
+ fill(out, context) {
101
+ const { data, regularDomain: r, domainOffset = 0, channels } = this.attrs;
102
+ const stride = this.stride, n = this.pointCount;
103
+ const valueOffset = this.attrs.valueOffset ?? (r ? 0 : 1);
104
+ const names = context.channels, k = names.length, os = 2 + k;
105
+ const offsets = names.map((name) => channels?.[name] ?? fail(`the data has no channel "${name}"`));
106
+ const cMin = new Float64Array(k).fill(Infinity), cMax = new Float64Array(k).fill(-Infinity);
107
+ let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
108
+ for (let i = 0, j = 0; i < n; i++, j += stride) {
109
+ const x = r ? r.start + r.step * i : data[j + domainOffset];
110
+ const y = data[j + valueOffset];
111
+ out[os * i] = x;
112
+ out[os * i + 1] = y;
113
+ if (x < xMin)
114
+ xMin = x;
115
+ if (x > xMax)
116
+ xMax = x;
117
+ if (y < yMin)
118
+ yMin = y;
119
+ if (y > yMax)
120
+ yMax = y;
121
+ for (let c = 0; c < k; c++) {
122
+ const v = data[j + offsets[c]];
123
+ out[os * i + 2 + c] = v;
124
+ if (v < cMin[c])
125
+ cMin[c] = v;
126
+ if (v > cMax[c])
127
+ cMax[c] = v;
128
+ }
129
+ }
130
+ if (n === 0)
131
+ return { count: 0, extents: [NO_POINTS, NO_POINTS, ...names.map(() => NO_POINTS)] };
132
+ const extents = [{ min: xMin, max: xMax }, { min: yMin, max: yMax }];
133
+ for (let c = 0; c < k; c++)
134
+ extents.push({ min: cMin[c], max: cMax[c] });
135
+ return { count: n, extents };
136
+ }
137
+ }
138
+ // ─── Generated and column-wise sources ───
139
+ /** Samples when nothing says how many: the plot's width is not known when a source is only asked for its extent. */
140
+ const DEFAULT_SAMPLES = 512;
141
+ /**
142
+ * A function of the domain, sampled when drawn. There is no storage, so points are made up, not rows: they carry no
143
+ * `rowOf` and cannot be edited. Samples ascend by construction. It offers no channels.
144
+ */
145
+ export class SyntheticDataSource extends DataSource {
146
+ // oxlint-disable-next-line eslint/no-useless-constructor -- makes the attributes required (the base class accepts none)
147
+ constructor(attrs) { super(attrs); }
148
+ get ordered() { return true; }
149
+ get pointCount() { return this.attrs.count ?? DEFAULT_SAMPLES; }
150
+ scanDomain() {
151
+ const d = this.attrs.domain;
152
+ return d.min <= d.max ? d : null;
153
+ }
154
+ /** How many samples, over which part of the domain, for this plot. */
155
+ span(context) {
156
+ const d = this.attrs.domain, lo = Math.max(d.min, context.domainRange.min), hi = Math.min(d.max, context.domainRange.max);
157
+ if (!(lo <= hi))
158
+ return { lo, hi, n: 0 };
159
+ return { lo, hi, n: this.attrs.count ?? (context.width > 0 ? Math.max(2, Math.ceil(context.width)) : DEFAULT_SAMPLES) };
160
+ }
161
+ fill(out, context) {
162
+ if (context.channels.length)
163
+ fail(`the data has no channel "${context.channels[0]}"`);
164
+ // With a fixed count the span is the whole domain whatever is in view; otherwise only the part in view is sampled.
165
+ const { fn, domain, count } = this.attrs;
166
+ const { lo, hi, n } = count === undefined ? this.span(context) : { lo: domain.min, hi: domain.max, n: count };
167
+ if (n === 0)
168
+ return { count: 0, extents: [NO_POINTS, NO_POINTS] };
169
+ let yMin = Infinity, yMax = -Infinity;
170
+ for (let i = 0; i < n; i++) {
171
+ const x = n === 1 ? lo : i === n - 1 ? hi : lo + ((hi - lo) * i) / (n - 1), y = fn(x);
172
+ out[2 * i] = x;
173
+ out[2 * i + 1] = y;
174
+ if (y < yMin)
175
+ yMin = y;
176
+ if (y > yMax)
177
+ yMax = y;
178
+ }
179
+ return { count: n, extents: [{ min: lo, max: hi }, yMin <= yMax ? { min: yMin, max: yMax } : NO_POINTS] };
180
+ }
181
+ /** Room for the samples this plot asks for, which depends on its width; the points are made up, so there is no `rowOf`. */
182
+ plan(context) {
183
+ if (this.attrs.wrap !== undefined)
184
+ return super.plan(context);
185
+ return { capacity: this.attrs.count ?? this.span(context).n, fill: (out) => this.fill(out, context) };
186
+ }
187
+ }
188
+ const reader = (c) => (typeof c === 'function' ? c : (i) => c[i]);
189
+ /**
190
+ * Each column kept by itself: the shape of column-oriented data (an `x` array and a `y` array, a table by column), and of
191
+ * data whose domain is made up, with any column a function of the row index. Nothing is copied from arrays that are
192
+ * already in order (`isSorted`); otherwise the columns are gathered into the managed buffer once per change.
193
+ */
194
+ export class CompositeDataSource extends DataSource {
195
+ // oxlint-disable-next-line eslint/no-useless-constructor -- makes the attributes required (the base class accepts none)
196
+ constructor(attrs) { super(attrs); }
197
+ get pointCount() { return this.attrs.count; }
198
+ get channelNames() { return Object.keys(this.attrs.channels ?? {}); }
199
+ scanDomain() {
200
+ const at = reader(this.attrs.domain);
201
+ return endsOf(this.attrs.count, at);
202
+ }
203
+ fill(out, context) {
204
+ const { count: n, channels } = this.attrs;
205
+ const xAt = reader(this.attrs.domain), yAt = reader(this.attrs.value);
206
+ const names = context.channels, k = names.length, stride = 2 + k;
207
+ const cAt = names.map((name) => reader(channels?.[name] ?? fail(`the data has no channel "${name}"`)));
208
+ const cMin = new Float64Array(k).fill(Infinity), cMax = new Float64Array(k).fill(-Infinity);
209
+ let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
210
+ for (let i = 0; i < n; i++) {
211
+ const x = num(xAt(i)), y = num(yAt(i));
212
+ out[stride * i] = x;
213
+ out[stride * i + 1] = y;
214
+ if (x < xMin)
215
+ xMin = x;
216
+ if (x > xMax)
217
+ xMax = x;
218
+ if (y < yMin)
219
+ yMin = y;
220
+ if (y > yMax)
221
+ yMax = y;
222
+ for (let c = 0; c < k; c++) {
223
+ const v = num(cAt[c](i));
224
+ out[stride * i + 2 + c] = v;
225
+ if (v < cMin[c])
226
+ cMin[c] = v;
227
+ if (v > cMax[c])
228
+ cMax[c] = v;
229
+ }
230
+ }
231
+ const ext = (min, max) => (min <= max ? { min, max } : NO_POINTS);
232
+ const extents = [ext(xMin, xMax), ext(yMin, yMax)];
233
+ for (let c = 0; c < k; c++)
234
+ extents.push(ext(cMin[c], cMax[c]));
235
+ return { count: n, extents };
236
+ }
237
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Stack: series that are drawn on top of each other, or side by side, sharing one domain and one value axis.
3
+ *
4
+ * A stack is a group in the plane, and its members are ordinary series, each with its own source, renderers, name and
5
+ * `hit`. All it adds is where each series' rows sit: for every row a *base* (where it starts) and a *top* (where it is
6
+ * plotted), which the series reads in place of its own value. So nothing downstream is special:
7
+ * - a line or a marker is drawn at the tops, once for each series;
8
+ * - an area (or bars) fills from the series' base, which `Series.baseline` now returns row by row;
9
+ * - a tooltip or an edit still sees the series' own value, since the raw rows are untouched.
10
+ *
11
+ * Modes (`mode`):
12
+ * - `stack` (default): each row's values are laid one on another in the order of the members, the first at the bottom.
13
+ * Positive values stack up from zero and negative ones down from it, so mixed signs do not cancel.
14
+ * - `normalize`: like `stack`, with each row scaled to total 1 (the value axis then runs 0 to 1: give it a format).
15
+ * - `center`: a streamgraph, the stack of each row centred on zero.
16
+ * - `group`: nothing is laid on anything; every series is drawn from the baseline, and a bar renderer places the series
17
+ * side by side, in the order they are listed.
18
+ * A value that is a gap (NaN) leaves its own layer out and adds nothing to the others in that row.
19
+ *
20
+ * The rows of the members have to line up. Normally they do (the same categories, the same table's columns), and the
21
+ * default `align: 'strict'` checks it. Otherwise `align` says how to merge their domains into the union of all of them,
22
+ * a row at every position any member has:
23
+ * - `gaps`: a member that has no value at a position has a gap there. Right for bars.
24
+ * - `interpolate`: it has the straight line between its neighbours' values, and a gap beyond its own first and last
25
+ * position. Right for areas and lines, which then add up exactly. The rows made up this way belong to no source row,
26
+ * so they cannot be edited, and any channels they bind are gaps. A member with two rows at one position keeps the first.
27
+ *
28
+ * Decimation is per series and would thin each differently, so decimate a stack's sources before it, if at all: members
29
+ * whose rows then differ in domain need `align` as above.
30
+ */
31
+ import { type BaseAttributes, ChartElement } from './element.ts';
32
+ import type { Extent, Size } from './geometry.ts';
33
+ import type { Plane } from './plane.ts';
34
+ import { DomainSeries, type Rows, type SeriesGroup } from './series.ts';
35
+ export type StackMode = 'stack' | 'normalize' | 'center' | 'group';
36
+ export type StackAlign = 'strict' | 'gaps' | 'interpolate';
37
+ export interface StackAttributes extends BaseAttributes {
38
+ /** How the members' values are laid out. Default 'stack'. */
39
+ mode?: StackMode;
40
+ /** How rows are matched up when the members' domains differ. Default 'strict': they must not. */
41
+ align?: StackAlign;
42
+ }
43
+ /**
44
+ * Tables of rows made to share one domain: the union of all of theirs, each with a row at every position, `gaps` or
45
+ * `interpolate` where it had none. The made-up rows have no source row (`rowOf` negative).
46
+ */
47
+ export declare function alignRows(tables: readonly Rows[], how: 'gaps' | 'interpolate'): Rows[];
48
+ /** Where every row of every member starts and is plotted, and the extent of it all. */
49
+ export interface Layout {
50
+ readonly base: Float64Array[];
51
+ readonly top: Float64Array[];
52
+ readonly extent: Extent | null;
53
+ }
54
+ /** Lay the values of the (already lined-up) tables out by `mode`. */
55
+ export declare function layOut(tables: readonly Rows[], mode: StackMode): Layout;
56
+ export declare class Stack extends ChartElement<Plane, DomainSeries[], StackAttributes> implements SeriesGroup {
57
+ /** The series in the stack, in the order they stack. */
58
+ get members(): readonly DomainSeries[];
59
+ /** Engine protocol, called by the plane in place of each member's own `load`: read them all, lay them out, report the extents. */
60
+ load(estimate: Size): void;
61
+ }
package/dist/stack.js ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Stack: series that are drawn on top of each other, or side by side, sharing one domain and one value axis.
3
+ *
4
+ * A stack is a group in the plane, and its members are ordinary series, each with its own source, renderers, name and
5
+ * `hit`. All it adds is where each series' rows sit: for every row a *base* (where it starts) and a *top* (where it is
6
+ * plotted), which the series reads in place of its own value. So nothing downstream is special:
7
+ * - a line or a marker is drawn at the tops, once for each series;
8
+ * - an area (or bars) fills from the series' base, which `Series.baseline` now returns row by row;
9
+ * - a tooltip or an edit still sees the series' own value, since the raw rows are untouched.
10
+ *
11
+ * Modes (`mode`):
12
+ * - `stack` (default): each row's values are laid one on another in the order of the members, the first at the bottom.
13
+ * Positive values stack up from zero and negative ones down from it, so mixed signs do not cancel.
14
+ * - `normalize`: like `stack`, with each row scaled to total 1 (the value axis then runs 0 to 1: give it a format).
15
+ * - `center`: a streamgraph, the stack of each row centred on zero.
16
+ * - `group`: nothing is laid on anything; every series is drawn from the baseline, and a bar renderer places the series
17
+ * side by side, in the order they are listed.
18
+ * A value that is a gap (NaN) leaves its own layer out and adds nothing to the others in that row.
19
+ *
20
+ * The rows of the members have to line up. Normally they do (the same categories, the same table's columns), and the
21
+ * default `align: 'strict'` checks it. Otherwise `align` says how to merge their domains into the union of all of them,
22
+ * a row at every position any member has:
23
+ * - `gaps`: a member that has no value at a position has a gap there. Right for bars.
24
+ * - `interpolate`: it has the straight line between its neighbours' values, and a gap beyond its own first and last
25
+ * position. Right for areas and lines, which then add up exactly. The rows made up this way belong to no source row,
26
+ * so they cannot be edited, and any channels they bind are gaps. A member with two rows at one position keeps the first.
27
+ *
28
+ * Decimation is per series and would thin each differently, so decimate a stack's sources before it, if at all: members
29
+ * whose rows then differ in domain need `align` as above.
30
+ */
31
+ /* oxlint-disable typescript/no-non-null-assertion -- numeric kernels over flat typed-array buffers (stride-addressed rows): every index is bounded by the row count, which `noUncheckedIndexedAccess` cannot see */
32
+ import { ChartElement, fail } from './element.js';
33
+ import { DomainSeries } from './series.js';
34
+ /** The first column of each table is its domain and the second its value (a `DomainSeries`' raw layout). */
35
+ const sameDomain = (a, b) => {
36
+ if (a.count !== b.count)
37
+ return false;
38
+ for (let i = 0; i < a.count; i++)
39
+ if (a.raw[i * a.stride] !== b.raw[i * b.stride])
40
+ return false;
41
+ return true;
42
+ };
43
+ /**
44
+ * Tables of rows made to share one domain: the union of all of theirs, each with a row at every position, `gaps` or
45
+ * `interpolate` where it had none. The made-up rows have no source row (`rowOf` negative).
46
+ */
47
+ export function alignRows(tables, how) {
48
+ let total = 0;
49
+ for (const t of tables)
50
+ total += t.count;
51
+ const all = new Float64Array(total);
52
+ let at = 0;
53
+ for (const t of tables)
54
+ for (let i = 0; i < t.count; i++)
55
+ all[at++] = t.raw[i * t.stride];
56
+ all.sort();
57
+ let size = 0;
58
+ for (let i = 0; i < all.length; i++)
59
+ if (i === 0 || all[i] !== all[i - 1])
60
+ all[size++] = all[i];
61
+ return tables.map((t) => {
62
+ const { raw, count: n, stride: s } = t;
63
+ const out = new Float64Array(size * s).fill(NaN), rows = new Int32Array(size).fill(-1);
64
+ for (let u = 0, j = 0; u < size; u++) {
65
+ const x = all[u];
66
+ while (j < n && raw[j * s] < x)
67
+ j++;
68
+ out[u * s] = x;
69
+ if (j < n && raw[j * s] === x) {
70
+ for (let c = 1; c < s; c++)
71
+ out[u * s + c] = raw[j * s + c];
72
+ rows[u] = t.rowOf ? t.rowOf(j) : -1;
73
+ while (j < n && raw[j * s] === x)
74
+ j++;
75
+ }
76
+ else if (how === 'interpolate' && j > 0 && j < n) {
77
+ const x0 = raw[(j - 1) * s], x1 = raw[j * s], v0 = raw[(j - 1) * s + 1], v1 = raw[j * s + 1];
78
+ if (Number.isFinite(v0) && Number.isFinite(v1))
79
+ out[u * s + 1] = v0 + ((x - x0) / (x1 - x0)) * (v1 - v0);
80
+ }
81
+ }
82
+ return { raw: out, count: size, stride: s, rowOf: (i) => rows[i] };
83
+ });
84
+ }
85
+ /** Lay the values of the (already lined-up) tables out by `mode`. */
86
+ export function layOut(tables, mode) {
87
+ const members = tables.length, n = tables[0]?.count ?? 0;
88
+ const base = tables.map(() => new Float64Array(n)), top = tables.map(() => new Float64Array(n));
89
+ let min = Infinity, max = -Infinity;
90
+ const note = (v) => { if (v < min)
91
+ min = v; if (v > max)
92
+ max = v; };
93
+ for (let i = 0; i < n; i++) {
94
+ let total = 0, sum = 0;
95
+ if (mode === 'normalize' || mode === 'center') {
96
+ for (let m = 0; m < members; m++) {
97
+ const t = tables[m], v = t.raw[i * t.stride + 1];
98
+ if (Number.isFinite(v)) {
99
+ total += Math.abs(v);
100
+ sum += v;
101
+ }
102
+ }
103
+ }
104
+ const scale = mode === 'normalize' ? (total > 0 ? 1 / total : 0) : 1;
105
+ let pos = 0, neg = 0, run = mode === 'center' ? (-sum * scale) / 2 : 0;
106
+ for (let m = 0; m < members; m++) {
107
+ const t = tables[m], v = t.raw[i * t.stride + 1];
108
+ if (!Number.isFinite(v)) {
109
+ base[m][i] = top[m][i] = NaN;
110
+ continue;
111
+ }
112
+ const w = v * scale;
113
+ let from, to;
114
+ if (mode === 'group') {
115
+ from = 0;
116
+ to = v;
117
+ }
118
+ else if (mode === 'center') {
119
+ from = run;
120
+ to = run + w;
121
+ run = to;
122
+ }
123
+ else if (w >= 0) {
124
+ from = pos;
125
+ to = pos + w;
126
+ pos = to;
127
+ }
128
+ else {
129
+ from = neg;
130
+ to = neg + w;
131
+ neg = to;
132
+ }
133
+ base[m][i] = from;
134
+ top[m][i] = to;
135
+ note(from);
136
+ note(to);
137
+ }
138
+ }
139
+ return { base, top, extent: min <= max ? { min, max } : null };
140
+ }
141
+ export class Stack extends ChartElement {
142
+ /** The series in the stack, in the order they stack. */
143
+ get members() {
144
+ return [...this].filter((c) => c instanceof DomainSeries);
145
+ }
146
+ /** Engine protocol, called by the plane in place of each member's own `load`: read them all, lay them out, report the extents. */
147
+ load(estimate) {
148
+ const members = this.members.filter((m) => m.shown); // a member that is not shown leaves the stack: the rest close up
149
+ if (members.length === 0)
150
+ return;
151
+ const { domainAxis, valueAxis } = members[0];
152
+ for (const m of members)
153
+ if (m.domainAxis !== domainAxis || m.valueAxis !== valueAxis)
154
+ fail('the series of a stack have to share their domain axis and their value axis');
155
+ const own = members.map((m) => m.prepare(estimate));
156
+ const how = this.attrs.align ?? 'strict';
157
+ let tables = own, made = null;
158
+ if (!own.every((t) => sameDomain(own[0], t))) {
159
+ if (how === 'strict')
160
+ fail("the series of a stack have to share their domain values: give the stack `align: 'gaps'` (bars) or `align: 'interpolate'` (lines, areas) to merge them");
161
+ tables = made = alignRows(own, how);
162
+ }
163
+ const mode = this.attrs.mode ?? 'stack', layout = layOut(tables, mode);
164
+ members.forEach((m, index) => {
165
+ m.join({
166
+ base: layout.base[index], top: layout.top[index], additive: mode !== 'normalize', extent: layout.extent,
167
+ slot: mode === 'group' ? { index, count: members.length } : { index: 0, count: 1 },
168
+ }, made ? made[index] : null);
169
+ m.settle(layout.extent);
170
+ });
171
+ }
172
+ }
@@ -0,0 +1,38 @@
1
+ import { type DataNode, DataTransform, type FillPlan, type PlanContext } from './data.ts';
2
+ import { type BaseAttributes } from './element.ts';
3
+ import type { Extent } from './geometry.ts';
4
+ export interface BoxStatsAttributes extends BaseAttributes {
5
+ /** 'summary' (the default): a point for each group. 'outliers': a point for each sample beyond the whiskers. */
6
+ output?: 'summary' | 'outliers';
7
+ /** How far the whiskers may reach past the box, in interquartile ranges (default 1.5), or 'minmax' for the whole range. */
8
+ fence?: number | 'minmax';
9
+ /** How a quartile is found between samples. Default 'inclusive'. */
10
+ method?: 'inclusive' | 'exclusive';
11
+ /** Which statistic is the point's value: 'median' (the default; what a box is drawn round) or 'mean' (what error bars from `sd` or `sem` are drawn round). */
12
+ value?: 'median' | 'mean';
13
+ }
14
+ export declare class BoxStats extends DataTransform<BoxStatsAttributes, [source: DataNode]> {
15
+ #private;
16
+ static attrTypes: {
17
+ id: 1;
18
+ meta: {
19
+ title: 1;
20
+ description: 1;
21
+ role: 1;
22
+ };
23
+ output: 1;
24
+ fence: 0;
25
+ method: 1;
26
+ value: 1;
27
+ };
28
+ get source(): DataNode;
29
+ get channelNames(): readonly string[];
30
+ /** Anything beneath (the source) bubbles an invalidation up through here, so one counter covers the source and the attributes. */
31
+ invalidate(): void;
32
+ get version(): number;
33
+ get pointCount(): number;
34
+ domain(): Extent | null;
35
+ valueExtent(): Extent | null;
36
+ channelExtent(name: string): Extent | null;
37
+ plan(context: PlanContext): FillPlan;
38
+ }