@fusion.dev/helix-headless 0.188.0 → 0.189.0

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 (36) hide show
  1. package/api.json +1 -1
  2. package/custom-elements.json +39844 -36049
  3. package/dist/element-events.generated.js +21 -7
  4. package/dist/index.d.ts +3 -1
  5. package/dist/index.js +3 -1
  6. package/dist/spatial-heatmap/index.d.ts +1 -0
  7. package/dist/spatial-heatmap/index.js +1 -0
  8. package/dist/{isometric-heatmap → spatial-heatmap}/projection.d.ts +32 -6
  9. package/dist/{isometric-heatmap → spatial-heatmap}/projection.js +33 -8
  10. package/dist/spatial-heatmap/root/spatial-heatmap-root.d.ts +239 -0
  11. package/dist/spatial-heatmap/root/spatial-heatmap-root.js +252 -0
  12. package/dist/spatial-scatter-chart/field.d.ts +63 -0
  13. package/dist/spatial-scatter-chart/field.js +61 -0
  14. package/dist/spatial-scatter-chart/index.d.ts +2 -0
  15. package/dist/spatial-scatter-chart/index.js +2 -0
  16. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.d.ts +207 -0
  17. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.js +222 -0
  18. package/dist/spatial-scatter-chart/view.d.ts +93 -0
  19. package/dist/spatial-scatter-chart/view.js +115 -0
  20. package/dist/spatial-surface-chart/grid.d.ts +50 -0
  21. package/dist/spatial-surface-chart/grid.js +46 -0
  22. package/dist/spatial-surface-chart/index.d.ts +2 -0
  23. package/dist/spatial-surface-chart/index.js +2 -0
  24. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.d.ts +234 -0
  25. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.js +281 -0
  26. package/examples.json +1 -1
  27. package/lint.json +1 -1
  28. package/package.json +5 -5
  29. package/types/jsx.d.ts +5417 -3661
  30. package/types/svelte.d.ts +3 -1
  31. package/types/vue.d.ts +590 -298
  32. package/types/words.d.ts +153 -69
  33. package/dist/isometric-heatmap/index.d.ts +0 -1
  34. package/dist/isometric-heatmap/index.js +0 -1
  35. package/dist/isometric-heatmap/root/isometric-heatmap-root.d.ts +0 -118
  36. package/dist/isometric-heatmap/root/isometric-heatmap-root.js +0 -127
@@ -0,0 +1,222 @@
1
+ import { __decorate } from "tslib";
2
+ import { AriaForwardController, CHART_PLOT, customElement, drawTooltip, formatWords, HelixElement, HelixPlotChangeEvent, HelixPlotTurnEvent, HelixReadingChangeEvent, HelixReadingPressEvent, isRtl, MixinChartSource, MixinChartStatus, spokenReading, words, } from '@fusion.dev/helix-core';
3
+ import { html, nothing, svg } from 'lit';
4
+ import { property, state } from 'lit/decorators.js';
5
+ import '../../tooltip/index.js';
6
+ import { fieldOf } from '../field.js';
7
+ import { boxView, drawBox, inBox, nearestTo, SPATIAL_VIEW, turned, } from '../view.js';
8
+ const SERIES = 24;
9
+ const TURNS = {
10
+ ArrowDown: { across: 0, up: -1 },
11
+ ArrowLeft: { across: -1, up: 0 },
12
+ ArrowRight: { across: 1, up: 0 },
13
+ ArrowUp: { across: 0, up: 1 },
14
+ };
15
+ let HelixHeadlessSpatialScatterChart = class HelixHeadlessSpatialScatterChart extends MixinChartStatus(MixinChartSource(HelixElement)) {
16
+ constructor() {
17
+ super(...arguments);
18
+ this.groupBy = '';
19
+ this.plotLabel = '3D scatter chart';
20
+ this.summaryLabel = '{count, plural, one {# point} other {# points}}: {x} across, {y} up, and {z} in depth.';
21
+ this.rendered = false;
22
+ this.camera = SPATIAL_VIEW;
23
+ this.placed = [];
24
+ this.last = undefined;
25
+ this.told = -1;
26
+ this.aria = new AriaForwardController(this, {
27
+ target: () => this.partNamed('plot'),
28
+ });
29
+ this.onPlotKeydown = (event) => {
30
+ const turn = TURNS[event.key];
31
+ if (!event.shiftKey || !turn || event.altKey || event.ctrlKey || event.metaKey) {
32
+ return;
33
+ }
34
+ event.preventDefault();
35
+ event.stopPropagation();
36
+ this.camera = turned(this.camera, turn.across, turn.up);
37
+ this.dispatchEvent(new HelixPlotTurnEvent(turn));
38
+ };
39
+ this.onDrawingMove = (event) => {
40
+ if (event.pointerType === 'touch') {
41
+ return;
42
+ }
43
+ const drawing = event.currentTarget;
44
+ this.requestActive(nearestTo(drawing, event, this.placed), event.pointerType === 'pen' ? 'pen' : 'mouse');
45
+ };
46
+ this.onDrawingLeave = (event) => {
47
+ if (event.pointerType !== 'touch') {
48
+ this.requestActive(-1, 'mouse');
49
+ }
50
+ };
51
+ this.onDrawingTap = (event) => {
52
+ if (event.pointerType === 'touch') {
53
+ this.requestActive(nearestTo(event.currentTarget, event, this.placed, 24), 'touch');
54
+ }
55
+ };
56
+ }
57
+ static { this.words = 'helix-spatial-scatter-chart'; }
58
+ get field() {
59
+ return fieldOf(this.sourceRows, this.plottedColumns, this.groupBy);
60
+ }
61
+ get active() {
62
+ return this.pointer.at;
63
+ }
64
+ get readings() {
65
+ return this.field.points.length;
66
+ }
67
+ rendererDrawn(on = true) {
68
+ this.rendered = on;
69
+ this.toggleState('drawn', on);
70
+ }
71
+ requestActive(index, kind = 'mouse') {
72
+ if (kind === 'touch') {
73
+ this.pointer.tapTo(index);
74
+ }
75
+ else if (index >= 0 || !this.pointer.keyed) {
76
+ this.pointer.set(index);
77
+ }
78
+ }
79
+ placeReading(at) {
80
+ this.place = at;
81
+ }
82
+ connectedCallback() {
83
+ super.connectedCallback();
84
+ this.pointer.axis = 'none';
85
+ this.pointer.onReading = (heard, detail) => {
86
+ const point = this.field.points[detail.index];
87
+ const told = {
88
+ ...detail,
89
+ index: point ? point.row : -1,
90
+ label: point?.label ?? '',
91
+ };
92
+ this.dispatchEvent(heard === 'press' ? new HelixReadingPressEvent(told) : new HelixReadingChangeEvent(told));
93
+ };
94
+ }
95
+ updated(changed) {
96
+ super.updated(changed);
97
+ const field = this.field;
98
+ if (!this.last || JSON.stringify(field) !== JSON.stringify(this.last)) {
99
+ this.last = field;
100
+ this.dispatchEvent(new HelixPlotChangeEvent({ kind: 'data' }));
101
+ }
102
+ if (this.pointer.at !== this.told) {
103
+ this.told = this.pointer.at;
104
+ this.dispatchEvent(new HelixPlotChangeEvent({ kind: 'focus' }));
105
+ }
106
+ if (changed.has('place')) {
107
+ ;
108
+ this.renderRoot.querySelector('[for="helix-chart-mark"]')?.reposition?.();
109
+ }
110
+ }
111
+ render() {
112
+ const field = this.field;
113
+ const { points } = field;
114
+ const readable = points.length > 0;
115
+ const at = this.pointer.at;
116
+ return html `<div
117
+ aria-describedby=${readable ? 'helix-chart-summary' : nothing}
118
+ aria-label=${this.named || this.plotLabel}
119
+ id=${CHART_PLOT}
120
+ part="plot"
121
+ role=${readable ? 'application' : 'img'}
122
+ tabindex=${readable ? '0' : nothing}
123
+ @keydown=${this.onPlotKeydown}
124
+ >
125
+ <slot></slot>
126
+ ${readable ? this.renderFallback(field, at) : nothing}
127
+ ${this.renderLive(points, at)}
128
+ ${this.renderTooltip(points[at]?.row)}
129
+ </div>
130
+ ${readable ? this.renderSummary(field) : this.renderStatus()}${this.renderTable()}`;
131
+ }
132
+ renderFallback(field, at) {
133
+ const view = boxView(field.axes, this.camera);
134
+ const places = field.points.map((point, index) => {
135
+ const box = [
136
+ inBox(field.axes.x, point.x),
137
+ inBox(field.axes.y, point.y),
138
+ inBox(field.axes.z, point.z),
139
+ ];
140
+ return { depth: view.depth(box), index, point, ...view.on(box) };
141
+ });
142
+ this.placed = places;
143
+ const painted = [...places].sort((one, other) => one.depth - other.depth);
144
+ const active = places[at];
145
+ const { bounds } = view;
146
+ return html `<div aria-hidden="true" dir="ltr" ?hidden=${this.rendered} part="fallback">
147
+ <svg
148
+ part="drawing"
149
+ preserveAspectRatio="xMidYMid meet"
150
+ viewBox="${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}"
151
+ xmlns="http://www.w3.org/2000/svg"
152
+ @pointerleave=${this.onDrawingLeave}
153
+ @pointermove=${this.onDrawingMove}
154
+ @pointerup=${this.onDrawingTap}
155
+ >${drawBox(view)}${painted.map((place) => svg `<circle cx=${place.x} cy=${place.y} part="point${place.index === at ? ' point-active' : ''}" r="0.045" style="--chart-series: var(--chart-series-${(place.point.groupIndex % SERIES) + 1})"></circle>`)}${active
156
+ ? svg `<circle cx=${active.x} cy=${active.y} part="ring" r="0.085"></circle><foreignObject height="0.001" overflow="visible" width="0.001" x=${active.x} y=${active.y}>${this.renderOwnTooltip(active.point.row)}</foreignObject>`
157
+ : nothing}</svg>
158
+ </div>`;
159
+ }
160
+ renderLive(points, at) {
161
+ if (points.length === 0) {
162
+ return nothing;
163
+ }
164
+ const row = this.sourceRows[points[at]?.row ?? -1];
165
+ const said = this.pointer.keyed && row
166
+ ? spokenReading(row, this.pointer.column, (cell) => this.pointer.shows(cell), this.pointer.position)
167
+ : '';
168
+ return html `<span aria-live="polite" part="live" role="status">${said}</span>`;
169
+ }
170
+ renderOwnTooltip(row) {
171
+ if (this.rendered || !this.sourceRows[row]) {
172
+ return nothing;
173
+ }
174
+ return drawTooltip(this.sourceRows, row, this.tag('tooltip'), { dir: isRtl(this) ? 'rtl' : 'ltr', fixed: true, placement: 'top', x: '0%', y: '0%' }, () => undefined);
175
+ }
176
+ renderTooltip(row) {
177
+ const place = this.place;
178
+ if (!this.rendered || !place || row === undefined || !this.sourceRows[row]) {
179
+ return nothing;
180
+ }
181
+ return drawTooltip(this.sourceRows, row, this.tag('tooltip'), {
182
+ dir: isRtl(this) ? 'rtl' : 'ltr',
183
+ fixed: true,
184
+ placement: 'top',
185
+ x: `${place.x * 100}%`,
186
+ y: `${place.y * 100}%`,
187
+ }, () => undefined);
188
+ }
189
+ renderSummary(field) {
190
+ const said = formatWords(this, 'summaryLabel', {
191
+ count: field.points.length,
192
+ x: field.axes.x.name,
193
+ y: field.axes.y.name,
194
+ z: field.axes.z.name,
195
+ });
196
+ return html `<p id="helix-chart-summary" part="summary">${said}</p>`;
197
+ }
198
+ };
199
+ __decorate([
200
+ property({ attribute: 'group-by' })
201
+ ], HelixHeadlessSpatialScatterChart.prototype, "groupBy", void 0);
202
+ __decorate([
203
+ words,
204
+ property({ attribute: 'plot-label' })
205
+ ], HelixHeadlessSpatialScatterChart.prototype, "plotLabel", void 0);
206
+ __decorate([
207
+ words,
208
+ property({ attribute: 'summary-label' })
209
+ ], HelixHeadlessSpatialScatterChart.prototype, "summaryLabel", void 0);
210
+ __decorate([
211
+ state()
212
+ ], HelixHeadlessSpatialScatterChart.prototype, "rendered", void 0);
213
+ __decorate([
214
+ state()
215
+ ], HelixHeadlessSpatialScatterChart.prototype, "camera", void 0);
216
+ __decorate([
217
+ state()
218
+ ], HelixHeadlessSpatialScatterChart.prototype, "place", void 0);
219
+ HelixHeadlessSpatialScatterChart = __decorate([
220
+ customElement('helix-headless-spatial-scatter-chart')
221
+ ], HelixHeadlessSpatialScatterChart);
222
+ export { HelixHeadlessSpatialScatterChart };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * A spatial chart's box as SVG, seen from a camera that turns and tilts: its
3
+ * edges, a grid at the ticks on its floor and two walls, and where its
4
+ * numbers and its measures' names stand. Shared by the spatial scatter and
5
+ * the spatial surface, which draw their own readings inside it.
6
+ *
7
+ * Kept apart from the elements because it is arithmetic a picture hides: a
8
+ * tick drawn at the wrong place reads as a number the data never had.
9
+ *
10
+ * **The box is two units on every side**, from -1 to 1, as the three.js
11
+ * driver's is, so the same reading stands at the same place in either
12
+ * drawing. Across is the floor's across, up is the height, and in depth runs
13
+ * toward the viewer, which is the way back from the floor's back.
14
+ */
15
+ import { type Camera, type PagePoint, type Projection } from '@fusion.dev/helix-core';
16
+ import { type SVGTemplateResult } from 'lit';
17
+ import type { PlotAxis } from './field.js';
18
+ /**
19
+ * Where a spatial chart's SVG drawing is seen from until it is turned: from
20
+ * the front right and a little above, about where the three.js driver's
21
+ * camera starts, so a renderer that takes over does not swing the view.
22
+ */
23
+ export declare const SPATIAL_VIEW: Camera;
24
+ /** How far one step of Shift with an arrow turns or tilts the view, in degrees, as the renderer's. */
25
+ export declare const VIEW_STEP = 15;
26
+ /** A place in the box: across, up, and in depth, each from -1 to 1. */
27
+ export type BoxPoint = readonly [number, number, number];
28
+ /** A number or a name written in the drawing, where it stands on the page. */
29
+ export interface ViewLabel {
30
+ /** Whether it is a measure's name, drawn stronger, rather than a tick's number. */
31
+ name: boolean;
32
+ text: string;
33
+ x: number;
34
+ y: number;
35
+ }
36
+ /** The box as the camera sees it. */
37
+ export interface BoxView {
38
+ /** The box every line, label, and reading fits in, on the page: its left, top, width, and height. */
39
+ bounds: {
40
+ height: number;
41
+ width: number;
42
+ x: number;
43
+ y: number;
44
+ };
45
+ /** The box's edges and its grid, as one path. */
46
+ grid: string;
47
+ labels: ViewLabel[];
48
+ /** A place in the box, on the page. */
49
+ on(point: BoxPoint): PagePoint;
50
+ /** How near the camera a place in the box stands: the larger, the nearer. */
51
+ depth(point: BoxPoint): number;
52
+ projection: Projection;
53
+ }
54
+ /** Where a number stands in the box, from -1 at its axis' low end to 1 at its high end. */
55
+ export declare const inBox: (axis: PlotAxis, value: number) => number;
56
+ /** A number on an axis, written short enough for a tick. */
57
+ export declare const tickText: (value: number) => string;
58
+ /** How large a tick's number and a measure's name are, on the page, in the box's units. */
59
+ export declare const TICK_SIZE = 0.13;
60
+ export declare const NAME_SIZE = 0.15;
61
+ /**
62
+ * The box for `axes`, seen from `camera`, and the readings' own places on
63
+ * the page, which `extra` adds to the bounds.
64
+ */
65
+ export declare const boxView: (axes: {
66
+ x: PlotAxis;
67
+ y: PlotAxis;
68
+ z: PlotAxis;
69
+ }, camera: Camera, extra?: readonly PagePoint[]) => BoxView;
70
+ /** A place on the page as a share of the bounds, from their top left, for a mark the tooltip points at. */
71
+ export declare const shareOf: (view: BoxView, point: PagePoint) => {
72
+ x: number;
73
+ y: number;
74
+ };
75
+ /** Path data for a closed shape through `points`. */
76
+ export declare const shape: (points: readonly PagePoint[]) => string;
77
+ /** The next view after a step of Shift with an arrow, held between the lowest tilt and straight above. */
78
+ export declare const turned: (camera: Camera, across: number, up: number) => Camera;
79
+ /** The box's grid, its numbers, and its measures' names, drawn first so every reading stands in front of them. */
80
+ export declare const drawBox: (view: BoxView) => SVGTemplateResult;
81
+ /** A reading drawn on the page, which the pointer finds. */
82
+ export interface Placed {
83
+ index: number;
84
+ x: number;
85
+ y: number;
86
+ }
87
+ /**
88
+ * The reading drawn nearest the pointer, within `reach` pixels of it, or
89
+ * `-1`. **Measured on the screen**, through the drawing's own transform, so
90
+ * a small point is found from as far as a large one, at any size the
91
+ * drawing is shown.
92
+ */
93
+ export declare const nearestTo: (drawing: SVGSVGElement | null | undefined, event: PointerEvent, places: readonly Placed[], reach?: number) => number;
@@ -0,0 +1,115 @@
1
+ import { pathData, projectionOf, shortNumber, } from '@fusion.dev/helix-core';
2
+ import { svg } from 'lit';
3
+ export const SPATIAL_VIEW = { tilt: 25, turn: 42 };
4
+ export const VIEW_STEP = 15;
5
+ export const inBox = (axis, value) => {
6
+ const [low, high] = axis.range;
7
+ return high === low ? 0 : ((value - low) / (high - low)) * 2 - 1;
8
+ };
9
+ export const tickText = (value) => String(Number(value.toPrecision(6)));
10
+ const WIDE = 0.58;
11
+ export const TICK_SIZE = 0.13;
12
+ export const NAME_SIZE = 0.15;
13
+ export const boxView = (axes, camera, extra = []) => {
14
+ const projection = projectionOf(camera);
15
+ const on = ([x, y, z]) => projection.at(x, -z, y);
16
+ const depth = ([x, , z]) => projection.depth(x, -z);
17
+ const lines = [];
18
+ const line = (from, to) => {
19
+ const [a, b] = [on(from), on(to)];
20
+ lines.push(`M${shortNumber(a.x)} ${shortNumber(a.y)}L${shortNumber(b.x)} ${shortNumber(b.y)}`);
21
+ };
22
+ for (const a of [-1, 1]) {
23
+ for (const b of [-1, 1]) {
24
+ line([-1, a, b], [1, a, b]);
25
+ line([a, -1, b], [a, 1, b]);
26
+ line([a, b, -1], [a, b, 1]);
27
+ }
28
+ }
29
+ for (const tick of axes.x.ticks) {
30
+ const x = inBox(axes.x, tick);
31
+ line([x, -1, -1], [x, -1, 1]);
32
+ line([x, -1, -1], [x, 1, -1]);
33
+ }
34
+ for (const tick of axes.z.ticks) {
35
+ const z = inBox(axes.z, tick);
36
+ line([-1, -1, z], [1, -1, z]);
37
+ line([-1, -1, z], [-1, 1, z]);
38
+ }
39
+ for (const tick of axes.y.ticks) {
40
+ const y = inBox(axes.y, tick);
41
+ line([-1, y, -1], [1, y, -1]);
42
+ line([-1, y, -1], [-1, y, 1]);
43
+ }
44
+ const labels = [];
45
+ const label = (at, text, name = false) => {
46
+ labels.push({ name, text, ...on(at) });
47
+ };
48
+ for (const tick of axes.x.ticks) {
49
+ label([inBox(axes.x, tick), -1, 1.14], tickText(tick));
50
+ }
51
+ for (const tick of axes.z.ticks) {
52
+ label([1.14, -1, inBox(axes.z, tick)], tickText(tick));
53
+ }
54
+ for (const tick of axes.y.ticks.slice(1)) {
55
+ label([-1.14, inBox(axes.y, tick), 1], tickText(tick));
56
+ }
57
+ label([0, -1, 1.38], axes.x.name, true);
58
+ label([1.6, -1, 0], axes.z.name, true);
59
+ label([-1, 1.28, 1], axes.y.name, true);
60
+ const corners = [];
61
+ for (const x of [-1, 1]) {
62
+ for (const y of [-1, 1]) {
63
+ for (const z of [-1, 1]) {
64
+ corners.push(on([x, y, z]));
65
+ }
66
+ }
67
+ }
68
+ for (const one of labels) {
69
+ const size = one.name ? NAME_SIZE : TICK_SIZE;
70
+ const half = (one.text.length * size * WIDE) / 2;
71
+ corners.push({ x: one.x - half, y: one.y - size }, { x: one.x + half, y: one.y + size });
72
+ }
73
+ corners.push(...extra);
74
+ const xs = corners.map((point) => point.x);
75
+ const ys = corners.map((point) => point.y);
76
+ const [left, top] = [Math.min(...xs), Math.min(...ys)];
77
+ return {
78
+ bounds: { height: Math.max(...ys) - top, width: Math.max(...xs) - left, x: left, y: top },
79
+ depth,
80
+ grid: lines.join(''),
81
+ labels,
82
+ on,
83
+ projection,
84
+ };
85
+ };
86
+ export const shareOf = (view, point) => ({
87
+ x: (point.x - view.bounds.x) / view.bounds.width,
88
+ y: (point.y - view.bounds.y) / view.bounds.height,
89
+ });
90
+ export const shape = (points) => pathData(points);
91
+ export const turned = (camera, across, up) => ({
92
+ tilt: Math.min(Math.max(camera.tilt + up * VIEW_STEP, 15), 90),
93
+ turn: camera.turn + across * VIEW_STEP,
94
+ });
95
+ export const drawBox = (view) => svg `<path d=${view.grid} part="grid"></path>${view.labels.map((label) => label.name
96
+ ? svg `<text dominant-baseline="central" font-size=${NAME_SIZE} part="axis-name" text-anchor="middle" x=${shortNumber(label.x)} y=${shortNumber(label.y)}>${label.text}</text>`
97
+ : svg `<text dominant-baseline="central" font-size=${TICK_SIZE} part="tick" text-anchor="middle" x=${shortNumber(label.x)} y=${shortNumber(label.y)}>${label.text}</text>`)}`;
98
+ export const nearestTo = (drawing, event, places, reach = 14) => {
99
+ const matrix = drawing?.getScreenCTM?.();
100
+ if (!matrix) {
101
+ return -1;
102
+ }
103
+ let found = -1;
104
+ let distance = reach;
105
+ for (const place of places) {
106
+ const x = matrix.a * place.x + matrix.c * place.y + matrix.e;
107
+ const y = matrix.b * place.x + matrix.d * place.y + matrix.f;
108
+ const away = Math.hypot(x - event.clientX, y - event.clientY);
109
+ if (away < distance) {
110
+ distance = away;
111
+ found = place.index;
112
+ }
113
+ }
114
+ return found;
115
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A 3D surface's table read as a grid of heights.
3
+ *
4
+ * Kept apart from the element because it is arithmetic a picture hides: a
5
+ * reading put in the wrong cell bends the surface in a place the data never
6
+ * did, and a missing reading drawn as zero digs a pit that is not there.
7
+ */
8
+ import { type FieldRow, type PlotAxis } from '../spatial-scatter-chart/field.js';
9
+ /** One place on the grid: where it stands across and in depth, and its reading, where the table has one. */
10
+ export interface SurfaceCell {
11
+ /** The row's own label, where the table has a reading here. */
12
+ label?: string | undefined;
13
+ /** The row it was read from, counting from zero. */
14
+ row?: number | undefined;
15
+ /** The height, where the table has a reading here. */
16
+ value?: number | undefined;
17
+ /** Across and in depth, in the table's own numbers. */
18
+ x: number;
19
+ z: number;
20
+ }
21
+ /** The table as a 3D surface reads it. */
22
+ export interface SurfaceField {
23
+ /** Across, up (the height), and in depth. */
24
+ axes: {
25
+ x: PlotAxis;
26
+ y: PlotAxis;
27
+ z: PlotAxis;
28
+ };
29
+ /**
30
+ * Every place on the grid, across first: the cell `across` places along
31
+ * and `depth` places back is at `across * depths.length + depth`.
32
+ */
33
+ cells: SurfaceCell[];
34
+ /** The distinct places across, lowest first. */
35
+ acrosses: number[];
36
+ /** The distinct places in depth, lowest first. */
37
+ depths: number[];
38
+ /** How many cells hold a reading. */
39
+ readings: number;
40
+ }
41
+ /**
42
+ * Reads the table: **the first numeric column is across, the second is in
43
+ * depth, and the third is the height**, one reading per row. The grid takes
44
+ * every distinct place across and in depth, so a place the table has no
45
+ * reading for is a cell with no value, which the surface leaves open. A
46
+ * later row for a place already read replaces it.
47
+ */
48
+ export declare const surfaceOf: (rows: readonly FieldRow[], columns: readonly number[]) => SurfaceField;
49
+ /** How high a cell stands between the lowest and the highest reading, from 0 to 1, or undefined where it has none. */
50
+ export declare const levelOf: (field: SurfaceField, cell: SurfaceCell) => number | undefined;
@@ -0,0 +1,46 @@
1
+ import { ticks } from '@fusion.dev/helix-core';
2
+ import { niceRange } from '../spatial-scatter-chart/field.js';
3
+ export const surfaceOf = (rows, columns) => {
4
+ const [xAt, zAt, yAt] = columns;
5
+ const nameOf = (column) => column === undefined ? '' : (rows.find((row) => row.cells[column])?.cells[column]?.name ?? '');
6
+ const read = [];
7
+ rows.forEach((row, at) => {
8
+ const value = (column) => column === undefined ? undefined : row.cells[column]?.value;
9
+ const [x, z, y] = [value(xAt), value(zAt), value(yAt)];
10
+ if (Number.isFinite(x) && Number.isFinite(z) && Number.isFinite(y)) {
11
+ read.push({ label: row.label, row: at, value: y, x: x, z: z });
12
+ }
13
+ });
14
+ const distinct = (values) => [...new Set(values)].sort((a, b) => a - b);
15
+ const acrosses = distinct(read.map((one) => one.x));
16
+ const depths = distinct(read.map((one) => one.z));
17
+ const cells = acrosses.flatMap((x) => depths.map((z) => ({ x, z })));
18
+ for (const one of read) {
19
+ const at = acrosses.indexOf(one.x) * depths.length + depths.indexOf(one.z);
20
+ cells[at] = { label: one.label, row: one.row, value: one.value, x: one.x, z: one.z };
21
+ }
22
+ const axis = (column, values, exact) => {
23
+ const [low, high] = [Math.min(...values), Math.max(...values)];
24
+ const range = exact && low < high ? [low, high] : niceRange(low, high);
25
+ return { name: nameOf(column), range, ticks: ticks(range) };
26
+ };
27
+ return {
28
+ acrosses,
29
+ axes: {
30
+ x: axis(xAt, acrosses, true),
31
+ y: axis(yAt, read.map((one) => one.value), false),
32
+ z: axis(zAt, depths, true),
33
+ },
34
+ cells,
35
+ depths,
36
+ readings: read.length,
37
+ };
38
+ };
39
+ export const levelOf = (field, cell) => {
40
+ if (cell.value === undefined) {
41
+ return undefined;
42
+ }
43
+ const values = field.cells.flatMap((one) => (one.value === undefined ? [] : [one.value]));
44
+ const [low, high] = [Math.min(...values), Math.max(...values)];
45
+ return high === low ? 0.5 : (cell.value - low) / (high - low);
46
+ };
@@ -0,0 +1,2 @@
1
+ export { levelOf, type SurfaceCell, type SurfaceField, surfaceOf } from './grid.js';
2
+ export { HelixHeadlessSpatialSurfaceChart } from './root/spatial-surface-chart-root.js';
@@ -0,0 +1,2 @@
1
+ export { levelOf, surfaceOf } from './grid.js';
2
+ export { HelixHeadlessSpatialSurfaceChart } from './root/spatial-surface-chart-root.js';