@fusion.dev/helix-headless 0.193.0 → 0.195.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.
- package/api.json +1 -1
- package/custom-elements.json +348 -42
- package/dist/spatial-heatmap/projection.d.ts +35 -3
- package/dist/spatial-heatmap/projection.js +58 -3
- package/dist/spatial-heatmap/root/spatial-heatmap-root.d.ts +51 -29
- package/dist/spatial-heatmap/root/spatial-heatmap-root.js +106 -27
- package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.d.ts +17 -6
- package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.js +30 -4
- package/dist/spatial-scatter-chart/view.d.ts +55 -0
- package/dist/spatial-scatter-chart/view.js +114 -17
- package/dist/spatial-surface-chart/grid.d.ts +3 -0
- package/dist/spatial-surface-chart/grid.js +3 -2
- package/dist/spatial-surface-chart/root/spatial-surface-chart-root.d.ts +18 -7
- package/dist/spatial-surface-chart/root/spatial-surface-chart-root.js +26 -5
- package/examples.json +1 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +54 -56
- package/types/vue.d.ts +7 -13
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { depthOrder, ISOMETRIC, pathData, projectionOf, solid, } from '@fusion.dev/helix-core';
|
|
1
|
+
import { depthOrder, hullOf, ISOMETRIC, pathData, projectionOf, solid, } from '@fusion.dev/helix-core';
|
|
2
2
|
export const ACROSS = Math.sqrt(3) / 2;
|
|
3
3
|
export const MARGIN = 0.3;
|
|
4
4
|
export const THICKNESS = 0.25;
|
|
@@ -76,7 +76,9 @@ export const reliefOf = (heights, rows, footprint, camera = ISOMETRIC) => {
|
|
|
76
76
|
base: undefined,
|
|
77
77
|
box: { height: 0, width: 0, x: 0, y: 0 },
|
|
78
78
|
corners: undefined,
|
|
79
|
+
middle: { x: 0, y: 0 },
|
|
79
80
|
order: [],
|
|
81
|
+
outline: [],
|
|
80
82
|
prisms,
|
|
81
83
|
};
|
|
82
84
|
}
|
|
@@ -89,14 +91,67 @@ export const reliefOf = (heights, rows, footprint, camera = ISOMETRIC) => {
|
|
|
89
91
|
const xs = points.map((point) => point.x);
|
|
90
92
|
const ys = points.map((point) => point.y);
|
|
91
93
|
const [left, top] = [Math.min(...xs), Math.min(...ys)];
|
|
94
|
+
const tallest = Math.max(0, ...heights);
|
|
95
|
+
const [lastRow, firstRow] = [-(deep + MARGIN), MARGIN];
|
|
96
|
+
const front = projection.depth(columns / 2, lastRow) >= projection.depth(columns / 2, firstRow)
|
|
97
|
+
? lastRow
|
|
98
|
+
: firstRow;
|
|
99
|
+
const outline = [];
|
|
100
|
+
for (const x of [-MARGIN, columns + MARGIN]) {
|
|
101
|
+
for (const y of [lastRow, firstRow]) {
|
|
102
|
+
outline.push(projection.at(x, y, -THICKNESS), projection.at(x, y, tallest));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
92
105
|
return {
|
|
93
106
|
base,
|
|
94
107
|
box: { height: Math.max(...ys) - top, width: Math.max(...xs) - left, x: left, y: top },
|
|
95
108
|
corners: {
|
|
96
|
-
end: projection.at(columns + MARGIN,
|
|
97
|
-
start: projection.at(-MARGIN,
|
|
109
|
+
end: projection.at(columns + MARGIN, front, -THICKNESS),
|
|
110
|
+
start: projection.at(-MARGIN, front, -THICKNESS),
|
|
98
111
|
},
|
|
112
|
+
middle: projection.at(columns / 2, -deep / 2, tallest / 2),
|
|
99
113
|
order: depthOrder(projection, places).map((place) => place.index),
|
|
114
|
+
outline,
|
|
100
115
|
prisms,
|
|
101
116
|
};
|
|
102
117
|
};
|
|
118
|
+
const lowestOver = (hull, left, right) => {
|
|
119
|
+
let lowest = Number.NEGATIVE_INFINITY;
|
|
120
|
+
hull.forEach((from, index) => {
|
|
121
|
+
const to = hull[(index + 1) % hull.length];
|
|
122
|
+
const [a, b] = from.x <= to.x ? [from, to] : [to, from];
|
|
123
|
+
if (b.x < left || a.x > right) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const at = (x) => b.x === a.x ? Math.max(a.y, b.y) : a.y + ((x - a.x) / (b.x - a.x)) * (b.y - a.y);
|
|
127
|
+
lowest = Math.max(lowest, at(Math.max(a.x, left)), at(Math.min(b.x, right)));
|
|
128
|
+
});
|
|
129
|
+
return lowest;
|
|
130
|
+
};
|
|
131
|
+
export const labelPlaces = (outline, corners, sizes, span, gap, lowest = Number.POSITIVE_INFINITY) => {
|
|
132
|
+
const hull = hullOf(outline);
|
|
133
|
+
const across = (left, size) => Math.max(0, Math.min(left, span - size.width));
|
|
134
|
+
const place = (at, toward, size) => {
|
|
135
|
+
const away = Math.sign(toward.x - at.x);
|
|
136
|
+
const wanted = away > 0 ? at.x - size.width : away < 0 ? at.x : at.x - size.width / 2;
|
|
137
|
+
const left = across(wanted, size);
|
|
138
|
+
const top = Math.max(at.y, lowestOver(hull, left, left + size.width)) + gap;
|
|
139
|
+
return { x: left, y: Math.min(top, lowest) };
|
|
140
|
+
};
|
|
141
|
+
const start = place(corners.start, corners.end, sizes.start);
|
|
142
|
+
const end = place(corners.end, corners.start, sizes.end);
|
|
143
|
+
const meet = start.x < end.x + sizes.end.width &&
|
|
144
|
+
end.x < start.x + sizes.start.width &&
|
|
145
|
+
start.y < end.y + sizes.end.height &&
|
|
146
|
+
end.y < start.y + sizes.start.height;
|
|
147
|
+
if (!meet) {
|
|
148
|
+
return { end, start };
|
|
149
|
+
}
|
|
150
|
+
const under = start.y + sizes.start.height + gap / 2;
|
|
151
|
+
if (under <= lowest) {
|
|
152
|
+
return { end: { ...end, y: under }, start };
|
|
153
|
+
}
|
|
154
|
+
const right = corners.end.x >= corners.start.x;
|
|
155
|
+
const beside = right ? start.x + sizes.start.width + gap : start.x - sizes.end.width - gap;
|
|
156
|
+
return { end: { x: across(beside, sizes.end), y: start.y }, start };
|
|
157
|
+
};
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { nothing, type PropertyValues, type TemplateResult } from 'lit';
|
|
2
2
|
import '../../tooltip/index.js';
|
|
3
3
|
import { HelixHeadlessHeatmap } from '../../heatmap/root/heatmap-root.js';
|
|
4
|
+
import { type Point } from '../projection.js';
|
|
5
|
+
/** Where a renderer draws the front edge's two ends, and the corners of the box round the relief. */
|
|
6
|
+
type DrawnCorners = {
|
|
7
|
+
end: Point;
|
|
8
|
+
outline?: Point[];
|
|
9
|
+
start: Point;
|
|
10
|
+
};
|
|
4
11
|
/**
|
|
5
12
|
* @summary A heatmap drawn as a relief, where each reading stands as tall as its value.
|
|
6
13
|
*
|
|
@@ -35,27 +42,30 @@ import { HelixHeadlessHeatmap } from '../../heatmap/root/heatmap-root.js';
|
|
|
35
42
|
*
|
|
36
43
|
* **The prisms stand on a slab**, the `base`, which reaches a little past the
|
|
37
44
|
* grid, so the relief reads as one object. The `start` and `end` slots name
|
|
38
|
-
* the two ends of the front edge,
|
|
39
|
-
* the
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
45
|
+
* the two ends of the front edge, the long edge nearer the reader, which is
|
|
46
|
+
* the strip's first and last column. The component measures where each label
|
|
47
|
+
* stands under its corner, clear of the drawing, and writes it on the label as
|
|
48
|
+
* `--spatial-heatmap-label-x` and `--spatial-heatmap-label-y`. A renderer
|
|
49
|
+
* tells the chart where it draws those corners, so the labels stand at them
|
|
50
|
+
* there too, and follow them as the view turns.
|
|
43
51
|
*
|
|
44
52
|
* **`gap` and `min-cell` set the smallest cell.** The drawing scales as a
|
|
45
53
|
* whole, so a wider box draws larger cells and the gap grows with them. A box
|
|
46
54
|
* too narrow for every column at `min-cell` draws the newest that fit.
|
|
47
55
|
*
|
|
48
56
|
* **The drawing is SVG and holds still.** It scales with its box through its
|
|
49
|
-
* `viewBox`, needs no WebGL, and has no motion to pause.
|
|
50
|
-
*
|
|
57
|
+
* `viewBox`, needs no WebGL, and has no motion to pause. A drag turns it and
|
|
58
|
+
* tilts it, and Shift with an arrow turns it a step, from the isometric view
|
|
59
|
+
* it starts at.
|
|
51
60
|
* **Under `selection-mode`, the prisms are painted back to front for the
|
|
52
61
|
* view, and the grid's rows and cells stand beside them in the table's
|
|
53
62
|
* order**, holding no drawing of their own, so a turned view paints
|
|
54
63
|
* correctly and a screen reader still reads the grid in order.
|
|
55
64
|
*
|
|
56
65
|
* **A renderer can draw it in 3D instead.** Put a box for it in the
|
|
57
|
-
* `renderer` slot, and `connectSpatialHeatmap` from `@fusion.dev/helix-plot
|
|
58
|
-
*
|
|
66
|
+
* `renderer` slot, and `connectSpatialHeatmap` from `@fusion.dev/helix-plot`,
|
|
67
|
+
* given `threeHeatmapDriver` from `@fusion.dev/helix-plot/three`, draws the
|
|
68
|
+
* prisms in it with three.js, lit, in the same tones, in a view
|
|
59
69
|
* that drags round and zooms. The chart publishes its readings in `field`,
|
|
60
70
|
* and its own drawing stays under the renderer's, unseen, where a screen
|
|
61
71
|
* reader and the keys still reach it. Shift with an arrow turns the view,
|
|
@@ -100,12 +110,9 @@ import { HelixHeadlessHeatmap } from '../../heatmap/root/heatmap-root.js';
|
|
|
100
110
|
* @csspart strip - The box the drawing fills, which takes focus to be read by key. It is named by the host's `aria-labelledby` or `aria-label`, then by `label`, then by the caption of the table.
|
|
101
111
|
* @csspart top-face - The top of a prism, which carries the reading's tone.
|
|
102
112
|
*
|
|
103
|
-
* @cssproperty --spatial-heatmap-
|
|
104
|
-
* @cssproperty --spatial-heatmap-
|
|
105
|
-
* @cssproperty --spatial-heatmap-end-y - How far down the drawing the front edge ends, under the newest column, as a share of the drawing's width: its whole depth in the chart's own drawing, and wherever a renderer draws it. The component writes it on the footer, and a stylesheet that stands the `end` label there reads it.
|
|
113
|
+
* @cssproperty --spatial-heatmap-label-x - Where the `start` or the `end` label stands clear of the drawing, from the footer's inline start, in pixels. The component measures it and writes it on the label, with `data-placed`, as the view turns.
|
|
114
|
+
* @cssproperty --spatial-heatmap-label-y - Where the `start` or the `end` label stands clear of the drawing, from the footer's top, in pixels. The component measures it and writes it on the label, with `data-placed`, as the view turns.
|
|
106
115
|
* @cssproperty --spatial-heatmap-span - How many cells the drawing spans across: its columns and its rows together. The component writes it on the strip, and a stylesheet that caps the cell size reads it.
|
|
107
|
-
* @cssproperty --spatial-heatmap-start-x - Where the front edge starts, under the first column, across the drawing, from 0 at its inline start to 1: 0 in the chart's own drawing, and wherever a renderer draws it. The component writes it on the footer, and a stylesheet that stands the `start` label there reads it.
|
|
108
|
-
* @cssproperty --spatial-heatmap-start-y - How far down the drawing the front edge starts, under the first column, as a share of the drawing's width. The component writes it on the footer, and a stylesheet that stands the `start` label there reads it.
|
|
109
116
|
*
|
|
110
117
|
* @cssstate drawn - While a renderer has drawn the relief, and the chart's own drawing stays unseen under it.
|
|
111
118
|
*/
|
|
@@ -127,8 +134,28 @@ export declare class HelixHeadlessSpatialHeatmap extends HelixHeadlessHeatmap {
|
|
|
127
134
|
relief: number;
|
|
128
135
|
/** The prisms drawn on the last render, which the tooltip is placed against. */
|
|
129
136
|
private shown;
|
|
130
|
-
/** Where the chart's own drawing is seen from, which Shift with an arrow turns. */
|
|
137
|
+
/** Where the chart's own drawing is seen from, which a drag or Shift with an arrow turns. */
|
|
131
138
|
private camera;
|
|
139
|
+
/** What places the first and last labels again when one of them, or the footer, changes size. */
|
|
140
|
+
private labelSizes;
|
|
141
|
+
connectedCallback(): void;
|
|
142
|
+
disconnectedCallback(): void;
|
|
143
|
+
/**
|
|
144
|
+
* Places the labels again whenever one of them or the footer changes size,
|
|
145
|
+
* as a webfont arriving after the first measure changes a label's width,
|
|
146
|
+
* and with it how far down the front edge reaches over it. Once the footer
|
|
147
|
+
* is rendered, and guarded because Lit SSR provides no `ResizeObserver`.
|
|
148
|
+
*/
|
|
149
|
+
private observeLabels;
|
|
150
|
+
/** A drag across the chart's own drawing, which turns its view. */
|
|
151
|
+
private readonly drag;
|
|
152
|
+
private readonly onDrawingDown;
|
|
153
|
+
private readonly onDrawingMove;
|
|
154
|
+
private readonly onDrawingUp;
|
|
155
|
+
/** A tap shows the prism under it, unless the touch was a drag, which turned the view. */
|
|
156
|
+
private readonly onStripUp;
|
|
157
|
+
/** A pointer passing over a prism shows it, unless it is turning the view. */
|
|
158
|
+
private readonly onPrismOver;
|
|
132
159
|
/** Whether a renderer has drawn the relief, which its own drawing then stays under. */
|
|
133
160
|
private rendered;
|
|
134
161
|
/** Where the renderer draws the reading, which the tooltip points at. */
|
|
@@ -182,16 +209,7 @@ export declare class HelixHeadlessSpatialHeatmap extends HelixHeadlessHeatmap {
|
|
|
182
209
|
* last place they had. The `start` and `end` labels stand there. Called by
|
|
183
210
|
* the connector.
|
|
184
211
|
*/
|
|
185
|
-
placeCorners(at:
|
|
186
|
-
end: {
|
|
187
|
-
x: number;
|
|
188
|
-
y: number;
|
|
189
|
-
};
|
|
190
|
-
start: {
|
|
191
|
-
x: number;
|
|
192
|
-
y: number;
|
|
193
|
-
};
|
|
194
|
-
} | undefined): void;
|
|
212
|
+
placeCorners(at: DrawnCorners | undefined): void;
|
|
195
213
|
protected updated(changed: PropertyValues): void;
|
|
196
214
|
/**
|
|
197
215
|
* Shift with an arrow turns the view, the chart's own drawing or a
|
|
@@ -223,11 +241,14 @@ export declare class HelixHeadlessSpatialHeatmap extends HelixHeadlessHeatmap {
|
|
|
223
241
|
*/
|
|
224
242
|
private renderBase;
|
|
225
243
|
/**
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
244
|
+
* Stands the first and last labels under their ends of the front edge,
|
|
245
|
+
* clear of the drawing, as the page lays them out: on each label,
|
|
246
|
+
* `--spatial-heatmap-label-x` from the footer's inline start and
|
|
247
|
+
* `--spatial-heatmap-label-y` from its top, and `data-placed` once it has
|
|
248
|
+
* a place. **Measured**, because a label is the page's type and the
|
|
249
|
+
* drawing scales with its box, so neither size is known before layout.
|
|
229
250
|
*/
|
|
230
|
-
private
|
|
251
|
+
private placeLabels;
|
|
231
252
|
/**
|
|
232
253
|
* The grid's rows, one for each column of the strip, and its cells in the
|
|
233
254
|
* table's order, each named by its row and saying whether it is selected,
|
|
@@ -246,3 +267,4 @@ declare global {
|
|
|
246
267
|
'helix-headless-spatial-heatmap': HelixHeadlessSpatialHeatmap;
|
|
247
268
|
}
|
|
248
269
|
}
|
|
270
|
+
export {};
|
|
@@ -5,8 +5,9 @@ import { property, state } from 'lit/decorators.js';
|
|
|
5
5
|
import '../../tooltip/index.js';
|
|
6
6
|
import { fits, toneOf } from '../../heatmap/root/buckets.js';
|
|
7
7
|
import { HelixHeadlessHeatmap } from '../../heatmap/root/heatmap-root.js';
|
|
8
|
-
import { turned } from '../../spatial-scatter-chart/view.js';
|
|
9
|
-
import { ACROSS, heightsOf, highestOf, reliefOf, } from '../projection.js';
|
|
8
|
+
import { DragTurn, turned } from '../../spatial-scatter-chart/view.js';
|
|
9
|
+
import { ACROSS, heightsOf, highestOf, labelPlaces, reliefOf, } from '../projection.js';
|
|
10
|
+
const LABEL_GAP = 4;
|
|
10
11
|
const TURNS = {
|
|
11
12
|
ArrowDown: { across: 0, up: -1 },
|
|
12
13
|
ArrowLeft: { across: -1, up: 0 },
|
|
@@ -20,6 +21,29 @@ let HelixHeadlessSpatialHeatmap = class HelixHeadlessSpatialHeatmap extends Heli
|
|
|
20
21
|
this.relief = 2;
|
|
21
22
|
this.shown = undefined;
|
|
22
23
|
this.camera = ISOMETRIC;
|
|
24
|
+
this.labelSizes = undefined;
|
|
25
|
+
this.drag = new DragTurn((next) => {
|
|
26
|
+
this.camera = next(this.camera);
|
|
27
|
+
}, () => this.requestUpdate());
|
|
28
|
+
this.onDrawingDown = (event) => {
|
|
29
|
+
this.drag.down(event);
|
|
30
|
+
};
|
|
31
|
+
this.onDrawingMove = (event) => {
|
|
32
|
+
this.drag.move(event);
|
|
33
|
+
};
|
|
34
|
+
this.onDrawingUp = (event) => {
|
|
35
|
+
this.drag.up(event);
|
|
36
|
+
};
|
|
37
|
+
this.onStripUp = (event) => {
|
|
38
|
+
if (!this.drag.dragged) {
|
|
39
|
+
this.onPointerUp(event);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
this.onPrismOver = (event) => {
|
|
43
|
+
if (!this.drag.moving) {
|
|
44
|
+
this.onPointerOver(event);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
23
47
|
this.rendered = false;
|
|
24
48
|
this.told = { at: -1, field: '' };
|
|
25
49
|
this.onTurnKeydown = (event) => {
|
|
@@ -45,6 +69,25 @@ let HelixHeadlessSpatialHeatmap = class HelixHeadlessSpatialHeatmap extends Heli
|
|
|
45
69
|
}
|
|
46
70
|
static { this.words = 'helix-spatial-heatmap'; }
|
|
47
71
|
static { this.markRules = [MIRROR_RULE]; }
|
|
72
|
+
connectedCallback() {
|
|
73
|
+
super.connectedCallback();
|
|
74
|
+
this.observeLabels();
|
|
75
|
+
}
|
|
76
|
+
disconnectedCallback() {
|
|
77
|
+
super.disconnectedCallback();
|
|
78
|
+
this.labelSizes?.disconnect();
|
|
79
|
+
this.labelSizes = undefined;
|
|
80
|
+
}
|
|
81
|
+
observeLabels() {
|
|
82
|
+
const boxes = ['footer', 'start', 'end'].map((name) => this.partNamed(name));
|
|
83
|
+
if (this.labelSizes || typeof ResizeObserver === 'undefined' || boxes.includes(null)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.labelSizes = new ResizeObserver(() => this.placeLabels());
|
|
87
|
+
for (const box of boxes) {
|
|
88
|
+
this.labelSizes.observe(box);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
48
91
|
get field() {
|
|
49
92
|
const cells = this.drawn;
|
|
50
93
|
const rows = this.rowCount;
|
|
@@ -90,6 +133,7 @@ let HelixHeadlessSpatialHeatmap = class HelixHeadlessSpatialHeatmap extends Heli
|
|
|
90
133
|
}
|
|
91
134
|
updated(changed) {
|
|
92
135
|
super.updated(changed);
|
|
136
|
+
this.placeLabels();
|
|
93
137
|
const field = JSON.stringify(this.field);
|
|
94
138
|
if (field !== this.told.field) {
|
|
95
139
|
this.told.field = field;
|
|
@@ -127,7 +171,8 @@ let HelixHeadlessSpatialHeatmap = class HelixHeadlessSpatialHeatmap extends Heli
|
|
|
127
171
|
const grid = readable && this.selectionMode !== undefined;
|
|
128
172
|
const keys = this.selectionMode ? this.keysOf(cells) : [];
|
|
129
173
|
const chosen = new Set(this.selectionMode ? commaList(this.value) : []);
|
|
130
|
-
const
|
|
174
|
+
const fits = this.drawnRelief();
|
|
175
|
+
const relief = { ...fits, box: this.drag.frame(fits.box, fits.middle) };
|
|
131
176
|
this.shown = relief;
|
|
132
177
|
const at = this.pointer.at;
|
|
133
178
|
const prisms = relief.order.map((index) => this.renderPrism(relief.prisms[index], at === index, chosen.has(keys[index] ?? ''), grid, toneOf(cells[index] ?? { value: undefined }, bounds, palette)));
|
|
@@ -141,6 +186,11 @@ let HelixHeadlessSpatialHeatmap = class HelixHeadlessSpatialHeatmap extends Heli
|
|
|
141
186
|
style=${this.rendered ? 'opacity: 0; pointer-events: none' : nothing}
|
|
142
187
|
viewBox="${box.x} ${box.y} ${box.width} ${box.height}"
|
|
143
188
|
xmlns="http://www.w3.org/2000/svg"
|
|
189
|
+
@click=${this.drag.click}
|
|
190
|
+
@pointercancel=${this.onDrawingUp}
|
|
191
|
+
@pointerdown=${this.onDrawingDown}
|
|
192
|
+
@pointermove=${this.onDrawingMove}
|
|
193
|
+
@pointerup=${this.onDrawingUp}
|
|
144
194
|
>${relief.base ? this.renderBase(relief.base) : nothing}${prisms}${grid ? this.renderGridRows(cells.length, keys, chosen) : nothing}</svg>`
|
|
145
195
|
: nothing;
|
|
146
196
|
return html `<div
|
|
@@ -155,12 +205,12 @@ let HelixHeadlessSpatialHeatmap = class HelixHeadlessSpatialHeatmap extends Heli
|
|
|
155
205
|
tabindex=${readable ? '0' : nothing}
|
|
156
206
|
@click=${this.onClick}
|
|
157
207
|
@keydown=${this.onTurnKeydown}
|
|
158
|
-
@pointerover=${this.rendered ? undefined : this.
|
|
208
|
+
@pointerover=${this.rendered ? undefined : this.onPrismOver}
|
|
159
209
|
@pointerleave=${this.onPointerLeave}
|
|
160
|
-
@pointerup=${this.rendered ? undefined : this.
|
|
210
|
+
@pointerup=${this.rendered ? undefined : this.onStripUp}
|
|
161
211
|
><slot name="renderer"></slot>${drawing}${this.renderTooltip(grid)}</div>
|
|
162
212
|
${cells.length === 0 ? this.renderStatus() : nothing}
|
|
163
|
-
<div part="footer"
|
|
213
|
+
<div part="footer">
|
|
164
214
|
<span part="start"><slot name="start"></slot></span>
|
|
165
215
|
<span part="summary"><slot name="summary"></slot></span>
|
|
166
216
|
<span part="end"><slot name="end"></slot></span>
|
|
@@ -180,28 +230,57 @@ let HelixHeadlessSpatialHeatmap = class HelixHeadlessSpatialHeatmap extends Heli
|
|
|
180
230
|
renderBase(base) {
|
|
181
231
|
return svg `<g aria-hidden="true" part="base"><path d=${base.left} part="base-left"></path><path d=${base.right} part="base-right"></path><path d=${base.top} part="base-top"></path></g>`;
|
|
182
232
|
}
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
233
|
+
placeLabels() {
|
|
234
|
+
const footer = this.partNamed('footer');
|
|
235
|
+
const [end, start] = [this.partNamed('end'), this.partNamed('start')];
|
|
236
|
+
const relief = this.shown;
|
|
237
|
+
if (!footer || !end || !start || !relief?.corners) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const labels = { end, start };
|
|
241
|
+
this.observeLabels();
|
|
242
|
+
const frame = footer.getBoundingClientRect();
|
|
243
|
+
if (frame.width <= 0) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
let drawn = { ...relief.corners, outline: relief.outline };
|
|
247
|
+
let page;
|
|
248
|
+
let lowest = Number.POSITIVE_INFINITY;
|
|
249
|
+
if (this.rendered) {
|
|
250
|
+
const strip = this.partNamed('strip')?.getBoundingClientRect();
|
|
251
|
+
if (!this.corners?.outline || !strip) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
drawn = this.corners;
|
|
255
|
+
lowest = strip.bottom - frame.top + LABEL_GAP;
|
|
256
|
+
page = (point) => ({
|
|
257
|
+
x: strip.left + point.x * strip.width - frame.left,
|
|
258
|
+
y: strip.top + point.y * strip.height - frame.top,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
const matrix = this.partNamed('drawing')?.getScreenCTM?.();
|
|
263
|
+
if (!matrix) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
page = (point) => ({
|
|
267
|
+
x: matrix.a * point.x + matrix.c * point.y + matrix.e - frame.left,
|
|
268
|
+
y: matrix.b * point.x + matrix.d * point.y + matrix.f - frame.top,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const sizeOf = (label) => {
|
|
272
|
+
const { height, width } = label.getBoundingClientRect();
|
|
273
|
+
return { height, width };
|
|
274
|
+
};
|
|
275
|
+
const sizes = { end: sizeOf(labels.end), start: sizeOf(labels.start) };
|
|
276
|
+
const placed = labelPlaces((drawn.outline ?? []).map(page), { end: page(drawn.end), start: page(drawn.start) }, sizes, frame.width, LABEL_GAP, lowest);
|
|
277
|
+
for (const key of ['start', 'end']) {
|
|
278
|
+
const at = placed[key];
|
|
279
|
+
const inline = isRtl(this) ? frame.width - at.x - sizes[key].width : at.x;
|
|
280
|
+
labels[key].style.setProperty('--spatial-heatmap-label-x', `${Math.round(inline)}px`);
|
|
281
|
+
labels[key].style.setProperty('--spatial-heatmap-label-y', `${Math.round(at.y)}px`);
|
|
282
|
+
labels[key].toggleAttribute('data-placed', true);
|
|
187
283
|
}
|
|
188
|
-
const share = (value) => Math.round(value * 10000) / 10000;
|
|
189
|
-
const depth = box.height / box.width;
|
|
190
|
-
const drawn = this.rendered ? this.corners : undefined;
|
|
191
|
-
const across = (x) => (isRtl(this) ? 1 - x : x);
|
|
192
|
-
const start = drawn
|
|
193
|
-
? { x: across(drawn.start.x), y: drawn.start.y * depth }
|
|
194
|
-
: { x: 0, y: (corners.start.y - box.y) / box.width };
|
|
195
|
-
const end = drawn
|
|
196
|
-
? { x: across(drawn.end.x), y: drawn.end.y * depth }
|
|
197
|
-
: { x: (corners.end.x - box.x) / box.width, y: depth };
|
|
198
|
-
return [
|
|
199
|
-
`--spatial-heatmap-depth: ${share(depth)}`,
|
|
200
|
-
`--spatial-heatmap-end-x: ${share(end.x)}`,
|
|
201
|
-
`--spatial-heatmap-end-y: ${share(end.y)}`,
|
|
202
|
-
`--spatial-heatmap-start-x: ${share(start.x)}`,
|
|
203
|
-
`--spatial-heatmap-start-y: ${share(start.y)}`,
|
|
204
|
-
].join('; ');
|
|
205
284
|
}
|
|
206
285
|
renderGridRows(count, keys, chosen) {
|
|
207
286
|
const size = this.rowCount === 1 ? count : this.rowCount;
|
|
@@ -14,11 +14,12 @@ declare const HelixHeadlessSpatialScatterChart_base: typeof HelixElement & (new
|
|
|
14
14
|
*
|
|
15
15
|
* **Helix draws the frame, and a renderer draws the points in 3D.** Put a
|
|
16
16
|
* box for the renderer inside the chart, and `connectSpatialScatterChart` from
|
|
17
|
-
* `@fusion.dev/helix-plot
|
|
18
|
-
*
|
|
17
|
+
* `@fusion.dev/helix-plot`, given `threeScatterDriver` from
|
|
18
|
+
* `@fusion.dev/helix-plot/three`, draws the points in it with three.js and
|
|
19
|
+
* keeps it in step with the chart. Until a renderer has drawn, and where none is
|
|
19
20
|
* connected, the chart draws the points in 3D itself, in SVG, from the same
|
|
20
|
-
* place the renderer's camera starts: a
|
|
21
|
-
*
|
|
21
|
+
* place the renderer's camera starts: a picture a print and a page without
|
|
22
|
+
* WebGL show too, which a drag or Shift with an arrow turns.
|
|
22
23
|
*
|
|
23
24
|
* **The table is the reading.** Every number is in it, and screen readers
|
|
24
25
|
* read it. The `summary` part names the three measures and the number of
|
|
@@ -121,8 +122,10 @@ export declare class HelixHeadlessSpatialScatterChart extends HelixHeadlessSpati
|
|
|
121
122
|
summaryLabel: string;
|
|
122
123
|
/** Whether a renderer has drawn the points, which hides the chart's own drawing. */
|
|
123
124
|
private rendered;
|
|
124
|
-
/** Where the chart's own drawing is seen from, which Shift with an arrow turns. */
|
|
125
|
+
/** Where the chart's own drawing is seen from, which a drag or Shift with an arrow turns. */
|
|
125
126
|
private camera;
|
|
127
|
+
/** A drag across the chart's own drawing, which turns its view. */
|
|
128
|
+
private readonly drag;
|
|
126
129
|
/** Each point where the chart's own drawing put it, which the pointer finds. */
|
|
127
130
|
private placed;
|
|
128
131
|
/** Where the renderer draws the reading, which the tooltip points at. */
|
|
@@ -139,6 +142,8 @@ export declare class HelixHeadlessSpatialScatterChart extends HelixHeadlessSpati
|
|
|
139
142
|
* left out. A renderer reads it.
|
|
140
143
|
*/
|
|
141
144
|
get field(): PlotField;
|
|
145
|
+
/** The field last read, and what it was read from. */
|
|
146
|
+
private memo;
|
|
142
147
|
/** The point the keys or the pointer rest on, as its place in `field.points`, or `-1`. */
|
|
143
148
|
get active(): number;
|
|
144
149
|
/** The number of points, which the keys walk. */
|
|
@@ -173,8 +178,14 @@ export declare class HelixHeadlessSpatialScatterChart extends HelixHeadlessSpati
|
|
|
173
178
|
private readonly onPlotKeydown;
|
|
174
179
|
/** A mouse or a pen over the chart's own drawing puts the reading on the point nearest it. */
|
|
175
180
|
private readonly onDrawingMove;
|
|
181
|
+
private readonly onDrawingDown;
|
|
182
|
+
/** A press the browser took back, such as for a scroll, which ends a drag as a release does. */
|
|
183
|
+
private readonly onDrawingCancel;
|
|
176
184
|
private readonly onDrawingLeave;
|
|
177
|
-
/**
|
|
185
|
+
/**
|
|
186
|
+
* A tap is a touch reader's hover, unless it was a drag, which turned the
|
|
187
|
+
* view: it shows the point nearest it until a tap lands elsewhere.
|
|
188
|
+
*/
|
|
178
189
|
private readonly onDrawingTap;
|
|
179
190
|
protected render(): TemplateResult;
|
|
180
191
|
/**
|
|
@@ -4,7 +4,7 @@ import { html, nothing, svg } from 'lit';
|
|
|
4
4
|
import { property, state } from 'lit/decorators.js';
|
|
5
5
|
import '../../tooltip/index.js';
|
|
6
6
|
import { fieldOf } from '../field.js';
|
|
7
|
-
import { boxView, drawBox, inBox, nearestTo, SPATIAL_VIEW, turned, } from '../view.js';
|
|
7
|
+
import { boxView, DragTurn, drawBox, inBox, nearestTo, SPATIAL_VIEW, turned, } from '../view.js';
|
|
8
8
|
const SERIES = 24;
|
|
9
9
|
const TURNS = {
|
|
10
10
|
ArrowDown: { across: 0, up: -1 },
|
|
@@ -20,6 +20,9 @@ let HelixHeadlessSpatialScatterChart = class HelixHeadlessSpatialScatterChart ex
|
|
|
20
20
|
this.summaryLabel = '{count, plural, one {# point} other {# points}}: {x} across, {y} up, and {z} in depth.';
|
|
21
21
|
this.rendered = false;
|
|
22
22
|
this.camera = SPATIAL_VIEW;
|
|
23
|
+
this.drag = new DragTurn((next) => {
|
|
24
|
+
this.camera = next(this.camera);
|
|
25
|
+
}, () => this.requestUpdate());
|
|
23
26
|
this.placed = [];
|
|
24
27
|
this.last = undefined;
|
|
25
28
|
this.told = -1;
|
|
@@ -37,18 +40,30 @@ let HelixHeadlessSpatialScatterChart = class HelixHeadlessSpatialScatterChart ex
|
|
|
37
40
|
this.dispatchEvent(new HelixPlotTurnEvent(turn));
|
|
38
41
|
};
|
|
39
42
|
this.onDrawingMove = (event) => {
|
|
43
|
+
if (this.drag.move(event)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
40
46
|
if (event.pointerType === 'touch') {
|
|
41
47
|
return;
|
|
42
48
|
}
|
|
43
49
|
const drawing = event.currentTarget;
|
|
44
50
|
this.requestActive(nearestTo(drawing, event, this.placed), event.pointerType === 'pen' ? 'pen' : 'mouse');
|
|
45
51
|
};
|
|
52
|
+
this.onDrawingDown = (event) => {
|
|
53
|
+
this.drag.down(event);
|
|
54
|
+
};
|
|
55
|
+
this.onDrawingCancel = (event) => {
|
|
56
|
+
this.drag.up(event);
|
|
57
|
+
};
|
|
46
58
|
this.onDrawingLeave = (event) => {
|
|
47
59
|
if (event.pointerType !== 'touch') {
|
|
48
60
|
this.requestActive(-1, 'mouse');
|
|
49
61
|
}
|
|
50
62
|
};
|
|
51
63
|
this.onDrawingTap = (event) => {
|
|
64
|
+
if (this.drag.up(event)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
52
67
|
if (event.pointerType === 'touch') {
|
|
53
68
|
this.requestActive(nearestTo(event.currentTarget, event, this.placed, 24), 'touch');
|
|
54
69
|
}
|
|
@@ -56,7 +71,15 @@ let HelixHeadlessSpatialScatterChart = class HelixHeadlessSpatialScatterChart ex
|
|
|
56
71
|
}
|
|
57
72
|
static { this.words = 'helix-spatial-scatter-chart'; }
|
|
58
73
|
get field() {
|
|
59
|
-
|
|
74
|
+
const rows = this.sourceRows;
|
|
75
|
+
if (this.memo?.rows !== rows || this.memo.groupBy !== this.groupBy) {
|
|
76
|
+
this.memo = {
|
|
77
|
+
field: fieldOf(rows, this.plottedColumns, this.groupBy),
|
|
78
|
+
groupBy: this.groupBy,
|
|
79
|
+
rows,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return this.memo.field;
|
|
60
83
|
}
|
|
61
84
|
get active() {
|
|
62
85
|
return this.pointer.at;
|
|
@@ -95,7 +118,7 @@ let HelixHeadlessSpatialScatterChart = class HelixHeadlessSpatialScatterChart ex
|
|
|
95
118
|
updated(changed) {
|
|
96
119
|
super.updated(changed);
|
|
97
120
|
const field = this.field;
|
|
98
|
-
if (
|
|
121
|
+
if (field !== this.last) {
|
|
99
122
|
this.last = field;
|
|
100
123
|
this.dispatchEvent(new HelixPlotChangeEvent({ kind: 'data' }));
|
|
101
124
|
}
|
|
@@ -142,13 +165,16 @@ let HelixHeadlessSpatialScatterChart = class HelixHeadlessSpatialScatterChart ex
|
|
|
142
165
|
this.placed = places;
|
|
143
166
|
const painted = [...places].sort((one, other) => one.depth - other.depth);
|
|
144
167
|
const active = places[at];
|
|
145
|
-
const
|
|
168
|
+
const bounds = this.drag.frame(view.bounds, view.on([0, 0, 0]));
|
|
146
169
|
return html `<div aria-hidden="true" dir="ltr" ?hidden=${this.rendered} part="fallback">
|
|
147
170
|
<svg
|
|
148
171
|
part="drawing"
|
|
149
172
|
preserveAspectRatio="xMidYMid meet"
|
|
150
173
|
viewBox="${bounds.x} ${bounds.y} ${bounds.width} ${bounds.height}"
|
|
151
174
|
xmlns="http://www.w3.org/2000/svg"
|
|
175
|
+
@click=${this.drag.click}
|
|
176
|
+
@pointercancel=${this.onDrawingCancel}
|
|
177
|
+
@pointerdown=${this.onDrawingDown}
|
|
152
178
|
@pointerleave=${this.onDrawingLeave}
|
|
153
179
|
@pointermove=${this.onDrawingMove}
|
|
154
180
|
@pointerup=${this.onDrawingTap}
|
|
@@ -91,3 +91,58 @@ export interface Placed {
|
|
|
91
91
|
* drawing is shown.
|
|
92
92
|
*/
|
|
93
93
|
export declare const nearestTo: (drawing: SVGSVGElement | null | undefined, event: PointerEvent, places: readonly Placed[], reach?: number) => number;
|
|
94
|
+
/** The part of the page a drawing shows, as its `viewBox`: its left, top, width, and height. */
|
|
95
|
+
export interface Frame {
|
|
96
|
+
height: number;
|
|
97
|
+
width: number;
|
|
98
|
+
x: number;
|
|
99
|
+
y: number;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A drag across a spatial chart's own drawing, which turns its view as the
|
|
103
|
+
* pointer moves: across turns it, and down tilts it. **A press that moves
|
|
104
|
+
* less than a few pixels is a press**, so a click still puts the reading on
|
|
105
|
+
* a point, and the click that ends a drag is the drag's: `dragged` says so,
|
|
106
|
+
* for the chart to keep it from pressing anything.
|
|
107
|
+
*
|
|
108
|
+
* **The drawing turns in place.** Through a drag, `frame` holds the drawing
|
|
109
|
+
* at the size it had when the drag began, with the middle of the data where
|
|
110
|
+
* it stood, as the renderer's camera turns round its target. The drawing
|
|
111
|
+
* may reach past its edges as it turns, and nothing on the page moves. Once
|
|
112
|
+
* the drag ends, it settles into the frame that fits it.
|
|
113
|
+
*
|
|
114
|
+
* A touch turns the view when it moves across, and the page's stylesheet
|
|
115
|
+
* leaves a touch moving up and down to scroll the page.
|
|
116
|
+
*/
|
|
117
|
+
export declare class DragTurn {
|
|
118
|
+
private readonly turn;
|
|
119
|
+
private readonly redraw;
|
|
120
|
+
private held;
|
|
121
|
+
/** The frame and the middle of the data, as the last render drew them. */
|
|
122
|
+
private last;
|
|
123
|
+
/** The frame held through a drag, and where the middle of the data stands in it. */
|
|
124
|
+
private kept;
|
|
125
|
+
/** The frame a drag ended on, and when it ended, while the drawing settles. */
|
|
126
|
+
private settling;
|
|
127
|
+
/** Whether the press that ended last was a drag. */
|
|
128
|
+
dragged: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* `turn` moves the view, and `redraw` asks for a render, which `frame`
|
|
131
|
+
* needs once a drag ends and on each step as the drawing settles.
|
|
132
|
+
*/
|
|
133
|
+
constructor(turn: (next: (camera: Camera) => Camera) => void, redraw: () => void);
|
|
134
|
+
/** Whether a drag is moving the view now. */
|
|
135
|
+
get moving(): boolean;
|
|
136
|
+
down(event: PointerEvent): void;
|
|
137
|
+
/** Turns the view for a drag, and says whether the event was one. */
|
|
138
|
+
move(event: PointerEvent): boolean;
|
|
139
|
+
/** Ends a press, and says whether it was a drag. */
|
|
140
|
+
up(event: PointerEvent): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* The frame to draw in, given the one that fits the drawing now and where
|
|
143
|
+
* the middle of the data stands on the page. Called on every render.
|
|
144
|
+
*/
|
|
145
|
+
frame(fits: Frame, middle: PagePoint): Frame;
|
|
146
|
+
/** Keeps the click that ends a drag from pressing a reading. */
|
|
147
|
+
readonly click: (event: Event) => void;
|
|
148
|
+
}
|