@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.
- package/api.json +1 -1
- package/custom-elements.json +39844 -36049
- package/dist/element-events.generated.js +21 -7
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/spatial-heatmap/index.d.ts +1 -0
- package/dist/spatial-heatmap/index.js +1 -0
- package/dist/{isometric-heatmap → spatial-heatmap}/projection.d.ts +32 -6
- package/dist/{isometric-heatmap → spatial-heatmap}/projection.js +33 -8
- package/dist/spatial-heatmap/root/spatial-heatmap-root.d.ts +239 -0
- package/dist/spatial-heatmap/root/spatial-heatmap-root.js +252 -0
- package/dist/spatial-scatter-chart/field.d.ts +63 -0
- package/dist/spatial-scatter-chart/field.js +61 -0
- package/dist/spatial-scatter-chart/index.d.ts +2 -0
- package/dist/spatial-scatter-chart/index.js +2 -0
- package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.d.ts +207 -0
- package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.js +222 -0
- package/dist/spatial-scatter-chart/view.d.ts +93 -0
- package/dist/spatial-scatter-chart/view.js +115 -0
- package/dist/spatial-surface-chart/grid.d.ts +50 -0
- package/dist/spatial-surface-chart/grid.js +46 -0
- package/dist/spatial-surface-chart/index.d.ts +2 -0
- package/dist/spatial-surface-chart/index.js +2 -0
- package/dist/spatial-surface-chart/root/spatial-surface-chart-root.d.ts +234 -0
- package/dist/spatial-surface-chart/root/spatial-surface-chart-root.js +281 -0
- package/examples.json +1 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +5417 -3661
- package/types/svelte.d.ts +3 -1
- package/types/vue.d.ts +590 -298
- package/types/words.d.ts +153 -69
- package/dist/isometric-heatmap/index.d.ts +0 -1
- package/dist/isometric-heatmap/index.js +0 -1
- package/dist/isometric-heatmap/root/isometric-heatmap-root.d.ts +0 -118
- package/dist/isometric-heatmap/root/isometric-heatmap-root.js +0 -127
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { CHART_PLOT, commaList, customElement, drawTooltip, HelixPlotChangeEvent, HelixPlotTurnEvent, isRtl, MIRROR_RULE, spokenReading, writtenList, writtenNumbers, } 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 { fits, toneOf } from '../../heatmap/root/buckets.js';
|
|
7
|
+
import { HelixHeadlessHeatmap } from '../../heatmap/root/heatmap-root.js';
|
|
8
|
+
import { ACROSS, heightsOf, highestOf, reliefOf } from '../projection.js';
|
|
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 HelixHeadlessSpatialHeatmap = class HelixHeadlessSpatialHeatmap extends HelixHeadlessHeatmap {
|
|
16
|
+
constructor() {
|
|
17
|
+
super(...arguments);
|
|
18
|
+
this.flat = false;
|
|
19
|
+
this.relief = 2;
|
|
20
|
+
this.shown = undefined;
|
|
21
|
+
this.rendered = false;
|
|
22
|
+
this.told = { at: -1, field: '' };
|
|
23
|
+
this.onTurnKeydown = (event) => {
|
|
24
|
+
const turn = TURNS[event.key];
|
|
25
|
+
if (!this.rendered ||
|
|
26
|
+
!event.shiftKey ||
|
|
27
|
+
!turn ||
|
|
28
|
+
this.selectionMode === 'multiple' ||
|
|
29
|
+
event.altKey ||
|
|
30
|
+
event.ctrlKey ||
|
|
31
|
+
event.metaKey) {
|
|
32
|
+
this.onStripKeydown();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
event.stopPropagation();
|
|
37
|
+
this.dispatchEvent(new HelixPlotTurnEvent(turn));
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
static { this.words = 'helix-spatial-heatmap'; }
|
|
41
|
+
static { this.markRules = [MIRROR_RULE]; }
|
|
42
|
+
get field() {
|
|
43
|
+
const cells = this.drawn;
|
|
44
|
+
const rows = this.rowCount;
|
|
45
|
+
const bounds = writtenNumbers(this.thresholds);
|
|
46
|
+
const palette = writtenList(this.tones);
|
|
47
|
+
const tallest = this.flat ? 0 : Math.max(0, this.relief || 0);
|
|
48
|
+
const heights = heightsOf(cells.map((point) => point.value), highestOf(this.points.map((point) => point.value)), tallest);
|
|
49
|
+
const first = this.points.length - cells.length;
|
|
50
|
+
return {
|
|
51
|
+
cells: cells.map((cell, index) => ({
|
|
52
|
+
column: Math.floor(index / rows),
|
|
53
|
+
height: heights[index] ?? 0,
|
|
54
|
+
label: this.sourceRows[first + index]?.label ?? '',
|
|
55
|
+
row: index % rows,
|
|
56
|
+
tone: toneOf(cell, bounds, palette),
|
|
57
|
+
})),
|
|
58
|
+
columns: Math.ceil(cells.length / rows),
|
|
59
|
+
rows: Math.min(rows, cells.length),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
get active() {
|
|
63
|
+
return this.pointer.at;
|
|
64
|
+
}
|
|
65
|
+
rendererDrawn(on = true) {
|
|
66
|
+
this.rendered = on;
|
|
67
|
+
this.toggleState('drawn', on);
|
|
68
|
+
}
|
|
69
|
+
requestActive(index, kind = 'mouse') {
|
|
70
|
+
if (kind === 'touch') {
|
|
71
|
+
this.pointer.tapTo(index);
|
|
72
|
+
}
|
|
73
|
+
else if (index >= 0 || !this.pointer.keyed) {
|
|
74
|
+
this.pointer.set(index);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
placeReading(at) {
|
|
78
|
+
this.place = at;
|
|
79
|
+
}
|
|
80
|
+
placeCorners(at) {
|
|
81
|
+
if (at || !this.rendered) {
|
|
82
|
+
this.corners = at;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
updated(changed) {
|
|
86
|
+
super.updated(changed);
|
|
87
|
+
const field = JSON.stringify(this.field);
|
|
88
|
+
if (field !== this.told.field) {
|
|
89
|
+
this.told.field = field;
|
|
90
|
+
this.dispatchEvent(new HelixPlotChangeEvent({ kind: 'data' }));
|
|
91
|
+
}
|
|
92
|
+
if (this.pointer.at !== this.told.at) {
|
|
93
|
+
this.told.at = this.pointer.at;
|
|
94
|
+
this.dispatchEvent(new HelixPlotChangeEvent({ kind: 'focus' }));
|
|
95
|
+
}
|
|
96
|
+
if (changed.has('place')) {
|
|
97
|
+
;
|
|
98
|
+
this.renderRoot.querySelector('[for="helix-chart-mark"]')?.reposition?.();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
get room() {
|
|
102
|
+
const width = this.width;
|
|
103
|
+
if (width === undefined) {
|
|
104
|
+
return this.points.length;
|
|
105
|
+
}
|
|
106
|
+
const pitch = this.cellPx + this.gapPx;
|
|
107
|
+
const across = width / ACROSS - this.rowCount * pitch - this.gapPx;
|
|
108
|
+
return fits(this.points.length, Math.max(across, Number.MIN_VALUE), this.cellPx, this.gapPx, this.rowCount);
|
|
109
|
+
}
|
|
110
|
+
drawnRelief() {
|
|
111
|
+
const tallest = this.flat ? 0 : Math.max(0, this.relief || 0);
|
|
112
|
+
const highest = highestOf(this.points.map((point) => point.value));
|
|
113
|
+
const heights = heightsOf(this.drawn.map((point) => point.value), highest, tallest);
|
|
114
|
+
return reliefOf(heights, this.rowCount, this.cellPx / (this.cellPx + this.gapPx));
|
|
115
|
+
}
|
|
116
|
+
render() {
|
|
117
|
+
const bounds = writtenNumbers(this.thresholds);
|
|
118
|
+
const palette = writtenList(this.tones);
|
|
119
|
+
const cells = this.drawn;
|
|
120
|
+
const readable = cells.length > 0;
|
|
121
|
+
const grid = readable && this.selectionMode !== undefined;
|
|
122
|
+
const keys = this.selectionMode ? this.keysOf(cells) : [];
|
|
123
|
+
const chosen = new Set(this.selectionMode ? commaList(this.value) : []);
|
|
124
|
+
const relief = this.drawnRelief();
|
|
125
|
+
this.shown = relief;
|
|
126
|
+
const at = this.pointer.at;
|
|
127
|
+
const prisms = relief.prisms.map((prism) => this.renderPrism(prism.index, prism.top, prism.left, prism.right, at === prism.index, chosen.has(keys[prism.index] ?? ''), grid, toneOf(cells[prism.index] ?? { value: undefined }, bounds, palette)));
|
|
128
|
+
const { box } = relief;
|
|
129
|
+
const drawing = readable
|
|
130
|
+
? html `<svg
|
|
131
|
+
aria-hidden=${grid ? nothing : 'true'}
|
|
132
|
+
data-mirror
|
|
133
|
+
part="drawing"
|
|
134
|
+
role=${grid ? 'none' : nothing}
|
|
135
|
+
style=${this.rendered ? 'opacity: 0; pointer-events: none' : nothing}
|
|
136
|
+
viewBox="${box.x} ${box.y} ${box.width} ${box.height}"
|
|
137
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
138
|
+
>${relief.base ? this.renderBase(relief.base) : nothing}${grid ? this.renderPrismRows(prisms) : prisms}</svg>`
|
|
139
|
+
: nothing;
|
|
140
|
+
return html `<div
|
|
141
|
+
aria-activedescendant=${grid && at >= 0 ? `cell-${at}` : nothing}
|
|
142
|
+
aria-describedby=${readable ? 'helix-chart-summary' : nothing}
|
|
143
|
+
aria-label=${this.named || this.stripLabel}
|
|
144
|
+
aria-multiselectable=${grid && this.selectionMode === 'multiple' ? 'true' : nothing}
|
|
145
|
+
part="strip plot"
|
|
146
|
+
id=${CHART_PLOT}
|
|
147
|
+
role=${grid ? 'grid' : readable ? 'application' : 'img'}
|
|
148
|
+
style="--spatial-heatmap-span: ${Math.ceil(cells.length / this.rowCount) + Math.min(this.rowCount, cells.length)}"
|
|
149
|
+
tabindex=${readable ? '0' : nothing}
|
|
150
|
+
@click=${this.onClick}
|
|
151
|
+
@keydown=${this.onTurnKeydown}
|
|
152
|
+
@pointerover=${this.rendered ? undefined : this.onPointerOver}
|
|
153
|
+
@pointerleave=${this.onPointerLeave}
|
|
154
|
+
@pointerup=${this.rendered ? undefined : this.onPointerUp}
|
|
155
|
+
><slot name="renderer"></slot>${drawing}${this.renderTooltip(grid)}</div>
|
|
156
|
+
${cells.length === 0 ? this.renderStatus() : nothing}
|
|
157
|
+
<div part="footer" style=${this.cornersStyle(relief)}>
|
|
158
|
+
<span part="start"><slot name="start"></slot></span>
|
|
159
|
+
<span part="summary"><slot name="summary"></slot></span>
|
|
160
|
+
<span part="end"><slot name="end"></slot></span>
|
|
161
|
+
</div>${readable ? this.renderOverview(cells) : nothing}${this.status.spoken
|
|
162
|
+
? html `<span part="selection" role="status">${this.status.said}</span>`
|
|
163
|
+
: nothing}${this.renderTable()}`;
|
|
164
|
+
}
|
|
165
|
+
renderPrism(index, top, left, right, active, picked, grid, tone) {
|
|
166
|
+
const row = grid ? this.sourceRows[this.points.length - this.drawn.length + index] : undefined;
|
|
167
|
+
const name = row ? spokenReading(row) : '';
|
|
168
|
+
return svg `<g
|
|
169
|
+
aria-label=${grid && name ? name : nothing}
|
|
170
|
+
aria-selected=${grid ? (picked ? 'true' : 'false') : nothing}
|
|
171
|
+
data-index=${index}
|
|
172
|
+
data-tone=${tone ?? nothing}
|
|
173
|
+
id=${grid ? `cell-${index}` : nothing}
|
|
174
|
+
part="cell${active ? ' cell-active' : ''}${picked ? ' cell-selected' : ''}"
|
|
175
|
+
role=${grid ? 'gridcell' : nothing}
|
|
176
|
+
style=${tone === undefined ? nothing : `--heatmap-tone: var(--heatmap-tone-${tone})`}
|
|
177
|
+
><path d=${left} part="left-face"></path><path d=${right} part="right-face"></path><path d=${top} part="top-face"></path></g>`;
|
|
178
|
+
}
|
|
179
|
+
renderBase(base) {
|
|
180
|
+
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>`;
|
|
181
|
+
}
|
|
182
|
+
cornersStyle(relief) {
|
|
183
|
+
const { box, corners } = relief;
|
|
184
|
+
if (!corners || box.width <= 0) {
|
|
185
|
+
return nothing;
|
|
186
|
+
}
|
|
187
|
+
const share = (value) => Math.round(value * 10000) / 10000;
|
|
188
|
+
const depth = box.height / box.width;
|
|
189
|
+
const drawn = this.rendered ? this.corners : undefined;
|
|
190
|
+
const across = (x) => (isRtl(this) ? 1 - x : x);
|
|
191
|
+
const start = drawn
|
|
192
|
+
? { x: across(drawn.start.x), y: drawn.start.y * depth }
|
|
193
|
+
: { x: 0, y: (corners.start.y - box.y) / box.width };
|
|
194
|
+
const end = drawn
|
|
195
|
+
? { x: across(drawn.end.x), y: drawn.end.y * depth }
|
|
196
|
+
: { x: (corners.end.x - box.x) / box.width, y: depth };
|
|
197
|
+
return [
|
|
198
|
+
`--spatial-heatmap-depth: ${share(depth)}`,
|
|
199
|
+
`--spatial-heatmap-end-x: ${share(end.x)}`,
|
|
200
|
+
`--spatial-heatmap-end-y: ${share(end.y)}`,
|
|
201
|
+
`--spatial-heatmap-start-x: ${share(start.x)}`,
|
|
202
|
+
`--spatial-heatmap-start-y: ${share(start.y)}`,
|
|
203
|
+
].join('; ');
|
|
204
|
+
}
|
|
205
|
+
renderPrismRows(prisms) {
|
|
206
|
+
const size = this.rowCount === 1 ? prisms.length : this.rowCount;
|
|
207
|
+
const rows = [];
|
|
208
|
+
for (let start = 0; start < prisms.length; start += size) {
|
|
209
|
+
rows.push(svg `<g part="row" role="row">${prisms.slice(start, start + size)}</g>`);
|
|
210
|
+
}
|
|
211
|
+
return rows;
|
|
212
|
+
}
|
|
213
|
+
renderTooltip(grid = false) {
|
|
214
|
+
const at = this.pointer.at;
|
|
215
|
+
const prism = this.shown?.prisms[at];
|
|
216
|
+
const box = this.shown?.box;
|
|
217
|
+
const reading = grid ? undefined : this.pointer;
|
|
218
|
+
if (this.rendered) {
|
|
219
|
+
const place = this.place;
|
|
220
|
+
return drawTooltip(this.sourceRows, at >= 0 && place ? this.points.length - this.drawn.length + at : -1, this.tag('tooltip'), place
|
|
221
|
+
? { fixed: true, placement: 'top', x: `${place.x * 100}%`, y: `${place.y * 100}%` }
|
|
222
|
+
: undefined, undefined, reading);
|
|
223
|
+
}
|
|
224
|
+
if (at < 0 || !prism || !box || box.width <= 0 || box.height <= 0) {
|
|
225
|
+
return drawTooltip(this.sourceRows, -1, this.tag('tooltip'), undefined, undefined, reading);
|
|
226
|
+
}
|
|
227
|
+
return drawTooltip(this.sourceRows, this.points.length - this.drawn.length + at, this.tag('tooltip'), {
|
|
228
|
+
placement: 'top',
|
|
229
|
+
x: `${((prism.peak.x - box.x) / box.width) * 100}%`,
|
|
230
|
+
y: `${((prism.peak.y - box.y) / box.height) * 100}%`,
|
|
231
|
+
}, undefined, reading);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
__decorate([
|
|
235
|
+
property({ type: Boolean })
|
|
236
|
+
], HelixHeadlessSpatialHeatmap.prototype, "flat", void 0);
|
|
237
|
+
__decorate([
|
|
238
|
+
property({ type: Number })
|
|
239
|
+
], HelixHeadlessSpatialHeatmap.prototype, "relief", void 0);
|
|
240
|
+
__decorate([
|
|
241
|
+
state()
|
|
242
|
+
], HelixHeadlessSpatialHeatmap.prototype, "rendered", void 0);
|
|
243
|
+
__decorate([
|
|
244
|
+
state()
|
|
245
|
+
], HelixHeadlessSpatialHeatmap.prototype, "place", void 0);
|
|
246
|
+
__decorate([
|
|
247
|
+
state()
|
|
248
|
+
], HelixHeadlessSpatialHeatmap.prototype, "corners", void 0);
|
|
249
|
+
HelixHeadlessSpatialHeatmap = __decorate([
|
|
250
|
+
customElement('helix-headless-spatial-heatmap')
|
|
251
|
+
], HelixHeadlessSpatialHeatmap);
|
|
252
|
+
export { HelixHeadlessSpatialHeatmap };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A 3D scatter's table read as points, axes, and groups.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from the element because it is arithmetic a picture hides: a
|
|
5
|
+
* range that does not reach the round numbers either side puts a point on
|
|
6
|
+
* the edge of the box, and a row read into the wrong axis looks like data.
|
|
7
|
+
*/
|
|
8
|
+
/** What the field reads of a row: its label and its cells, with their headings and numbers. */
|
|
9
|
+
export interface FieldRow {
|
|
10
|
+
cells: readonly {
|
|
11
|
+
name: string;
|
|
12
|
+
text: string;
|
|
13
|
+
value?: number | undefined;
|
|
14
|
+
}[];
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
/** One row drawn as a point. */
|
|
18
|
+
export interface PlotPoint {
|
|
19
|
+
/** The point's group, from the `group-by` column, or `''` where there is none. */
|
|
20
|
+
group: string;
|
|
21
|
+
/** The point's place among the groups, which its color is read by. */
|
|
22
|
+
groupIndex: number;
|
|
23
|
+
/** The row's own label. */
|
|
24
|
+
label: string;
|
|
25
|
+
/** The row it was read from, counting from zero. */
|
|
26
|
+
row: number;
|
|
27
|
+
/** Across, up, and in depth, in the table's own numbers. */
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
z: number;
|
|
31
|
+
}
|
|
32
|
+
/** One axis: its column's name, its range widened to round numbers, and the ticks on it. */
|
|
33
|
+
export interface PlotAxis {
|
|
34
|
+
name: string;
|
|
35
|
+
range: [number, number];
|
|
36
|
+
ticks: number[];
|
|
37
|
+
}
|
|
38
|
+
/** The table as a 3D scatter reads it. */
|
|
39
|
+
export interface PlotField {
|
|
40
|
+
axes: {
|
|
41
|
+
x: PlotAxis;
|
|
42
|
+
y: PlotAxis;
|
|
43
|
+
z: PlotAxis;
|
|
44
|
+
};
|
|
45
|
+
/** Every group, in the order its first row comes. */
|
|
46
|
+
groups: string[];
|
|
47
|
+
points: PlotPoint[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A range widened to the round numbers either side of it, so the first and
|
|
51
|
+
* last tick are the edges of the box and no point sits on its border. A
|
|
52
|
+
* range of one number is widened by one either way.
|
|
53
|
+
*/
|
|
54
|
+
export declare const niceRange: (low: number, high: number) => [number, number];
|
|
55
|
+
/**
|
|
56
|
+
* Reads the table: **the first three numeric columns are across, up, and in
|
|
57
|
+
* depth**, and `groupBy` names a column whose text puts each point in a
|
|
58
|
+
* group. A row that lacks any of the three numbers is left out, since a
|
|
59
|
+
* point needs all three to stand anywhere.
|
|
60
|
+
*/
|
|
61
|
+
export declare const fieldOf: (rows: readonly FieldRow[], columns: readonly number[], groupBy?: string) => PlotField;
|
|
62
|
+
/** Where a number stands along an axis, from 0 at its low end to 1 at its high end. */
|
|
63
|
+
export declare const along: (axis: PlotAxis, value: number) => number;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ticks } from '@fusion.dev/helix-core';
|
|
2
|
+
export const niceRange = (low, high) => {
|
|
3
|
+
if (!Number.isFinite(low) || !Number.isFinite(high)) {
|
|
4
|
+
return [0, 1];
|
|
5
|
+
}
|
|
6
|
+
if (low === high) {
|
|
7
|
+
return [low - 1, high + 1];
|
|
8
|
+
}
|
|
9
|
+
const levels = ticks([low, high]);
|
|
10
|
+
const step = levels.length > 1 ? levels[1] - levels[0] : high - low;
|
|
11
|
+
const round = (value) => Number(value.toPrecision(12));
|
|
12
|
+
return [round(Math.floor(low / step) * step), round(Math.ceil(high / step) * step)];
|
|
13
|
+
};
|
|
14
|
+
export const fieldOf = (rows, columns, groupBy = '') => {
|
|
15
|
+
const [xAt, yAt, zAt] = columns.filter((column) => {
|
|
16
|
+
const name = rows.find((row) => row.cells[column])?.cells[column]?.name ?? '';
|
|
17
|
+
return name !== groupBy;
|
|
18
|
+
});
|
|
19
|
+
const nameOf = (column) => column === undefined ? '' : (rows.find((row) => row.cells[column])?.cells[column]?.name ?? '');
|
|
20
|
+
const groups = [];
|
|
21
|
+
const points = [];
|
|
22
|
+
rows.forEach((row, at) => {
|
|
23
|
+
const value = (column) => column === undefined ? undefined : row.cells[column]?.value;
|
|
24
|
+
const [x, y, z] = [value(xAt), value(yAt), value(zAt)];
|
|
25
|
+
if (!(Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z))) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const group = groupBy ? (row.cells.find((cell) => cell.name === groupBy)?.text ?? '') : '';
|
|
29
|
+
let groupIndex = groups.indexOf(group);
|
|
30
|
+
if (groupIndex < 0) {
|
|
31
|
+
groupIndex = groups.push(group) - 1;
|
|
32
|
+
}
|
|
33
|
+
points.push({
|
|
34
|
+
group,
|
|
35
|
+
groupIndex,
|
|
36
|
+
label: row.label,
|
|
37
|
+
row: at,
|
|
38
|
+
x: x,
|
|
39
|
+
y: y,
|
|
40
|
+
z: z,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
const axis = (column, read) => {
|
|
44
|
+
const values = points.map(read);
|
|
45
|
+
const range = niceRange(Math.min(...values), Math.max(...values));
|
|
46
|
+
return { name: nameOf(column), range, ticks: ticks(range) };
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
axes: {
|
|
50
|
+
x: axis(xAt, (point) => point.x),
|
|
51
|
+
y: axis(yAt, (point) => point.y),
|
|
52
|
+
z: axis(zAt, (point) => point.z),
|
|
53
|
+
},
|
|
54
|
+
groups,
|
|
55
|
+
points,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
export const along = (axis, value) => {
|
|
59
|
+
const [low, high] = axis.range;
|
|
60
|
+
return high === low ? 0.5 : (value - low) / (high - low);
|
|
61
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { AriaForwardController, HelixElement } from '@fusion.dev/helix-core';
|
|
2
|
+
import { type PropertyValues, type TemplateResult } from 'lit';
|
|
3
|
+
import '../../tooltip/index.js';
|
|
4
|
+
import { type PlotField } from '../field.js';
|
|
5
|
+
declare const HelixHeadlessSpatialScatterChart_base: typeof HelixElement & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinChartSourceInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinChartStatusInterface);
|
|
6
|
+
/**
|
|
7
|
+
* @summary A chart that plots each row as a point in three dimensions, for data with three measures.
|
|
8
|
+
*
|
|
9
|
+
* @description
|
|
10
|
+
* A 3D scatter chart shows how three measures relate. The table's first
|
|
11
|
+
* three numeric columns are measured across, up, and in depth, and each row
|
|
12
|
+
* is one point. `group-by` names a column of text, such as a team or a
|
|
13
|
+
* cluster, and each group is drawn in its own color.
|
|
14
|
+
*
|
|
15
|
+
* **Helix draws the frame, and a renderer draws the points in 3D.** Put a
|
|
16
|
+
* box for the renderer inside the chart, and `connectSpatialScatterChart` from
|
|
17
|
+
* `@fusion.dev/helix-plot` draws the points in it with three.js and keeps it
|
|
18
|
+
* in step with the chart. Until a renderer has drawn, and where none is
|
|
19
|
+
* connected, the chart draws the points in 3D itself, in SVG, from the same
|
|
20
|
+
* place the renderer's camera starts: a still picture the server, a print,
|
|
21
|
+
* and a page without WebGL all show, which Shift with an arrow turns.
|
|
22
|
+
*
|
|
23
|
+
* **The table is the reading.** Every number is in it, and screen readers
|
|
24
|
+
* read it. The `summary` part names the three measures and the number of
|
|
25
|
+
* points, and describes the plot.
|
|
26
|
+
*
|
|
27
|
+
* **The plot is read by key.** It takes focus, named by `label` or the
|
|
28
|
+
* caption. The left and right arrows move from point to point in the table's
|
|
29
|
+
* order, Home and End go to the first and the last, and the up and down
|
|
30
|
+
* arrows move among the measures the readout says first. The `live` part
|
|
31
|
+
* says the point's row in words, and the renderer marks it. Shift with an
|
|
32
|
+
* arrow turns the view, and Escape lets the point go.
|
|
33
|
+
*
|
|
34
|
+
* **A reading shows its row in a tooltip**, as on every chart: the point a
|
|
35
|
+
* mouse or a pen rests on in the drawing, the point tapped, or the one the
|
|
36
|
+
* keys move to, once a renderer has drawn.
|
|
37
|
+
*
|
|
38
|
+
* **Readings are sent as every chart sends them.** `helix-reading-change`
|
|
39
|
+
* says each move of the reading, by pointer or key, and `helix-reading-press`
|
|
40
|
+
* a point clicked, tapped, or pressed with Enter or Space, each with the row
|
|
41
|
+
* of the table in `index`.
|
|
42
|
+
*
|
|
43
|
+
* **Keyboard.** The plot is one tab stop once it has points.
|
|
44
|
+
*
|
|
45
|
+
* | Key | What it does |
|
|
46
|
+
* |---|---|
|
|
47
|
+
* | Tab | Moves focus to the plot. |
|
|
48
|
+
* | Right, Left | Moves to the next or the previous point, in the table's order. |
|
|
49
|
+
* | Up, Down | Moves among the point's measures, which the readout says first. |
|
|
50
|
+
* | Home, End | Moves to the first or the last point. |
|
|
51
|
+
* | Shift with an arrow | Turns the view across, or tilts it up and down. |
|
|
52
|
+
* | Enter, Space | Presses the point, which sends `helix-reading-press`. |
|
|
53
|
+
* | Escape | Lets the point go. |
|
|
54
|
+
*
|
|
55
|
+
* @since 0.189.0
|
|
56
|
+
* @status experimental
|
|
57
|
+
* @category charts
|
|
58
|
+
* @aliases 3D scatter chart, 3D scatter plot, Point cloud, Three-dimensional scatter
|
|
59
|
+
* @documentation https://fusion.dev/helix/components/spatial-scatter-chart
|
|
60
|
+
*
|
|
61
|
+
* @slot - The renderer's box, which the 3D drawing is made in.
|
|
62
|
+
* @slot empty - Content shown when the table has no points. Falls back to `empty-text`.
|
|
63
|
+
* @slot loading - Content shown while `loading` is set and there are no points yet. Falls back to `loading-text`.
|
|
64
|
+
*
|
|
65
|
+
* @event {HelixPlotChangeEvent} helix-plot-change - The points or the point the keys rest on changed, which the renderer is told. It stays on the chart.
|
|
66
|
+
* @event {HelixPlotTurnEvent} helix-plot-turn - Shift with an arrow asked the view to turn, by a step across or up.
|
|
67
|
+
* @event {HelixReadingChangeEvent} helix-reading-change - The reading moved, or was put away: `index` is the row, or `-1` for none, with its `label` and the `pointerType` that moved it.
|
|
68
|
+
* @event {HelixReadingPressEvent} helix-reading-press - A point was clicked, tapped, or pressed with Enter or Space, with the same detail as `helix-reading-change`.
|
|
69
|
+
*
|
|
70
|
+
* @csspart axis-name - A measure's name, along its edge of the box, in the chart's own drawing.
|
|
71
|
+
* @csspart drawing - The chart's own drawing in SVG: the box, its grid and numbers, and the points.
|
|
72
|
+
* @csspart empty - The words when the table has no points: the `empty` slot, falling back to `empty-text`.
|
|
73
|
+
* @csspart fallback - The box the chart's own drawing fills, shown until a renderer has drawn and where none is connected. Hidden from screen readers.
|
|
74
|
+
* @csspart grid - The box's edges and its grid at the ticks, in the chart's own drawing.
|
|
75
|
+
* @csspart live - The readout: a polite live region that says the point the keys moved to. Hide it visually.
|
|
76
|
+
* @csspart loading - The words while `loading` is set and there are no points.
|
|
77
|
+
* @csspart mark - The invisible point the tooltip is anchored to, where the renderer draws the reading.
|
|
78
|
+
* @csspart plot - 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.
|
|
79
|
+
* @csspart point - One point of the chart's own drawing.
|
|
80
|
+
* @csspart point-active - The point the pointer or the keys rest on, in the chart's own drawing. Set alongside `point`.
|
|
81
|
+
* @csspart ring - The ring around the point the pointer or the keys rest on, drawn over every point.
|
|
82
|
+
* @csspart summary - The three measures and the number of points, written as a sentence for screen readers, which describes the plot. Hide it visually.
|
|
83
|
+
* @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`. Hide it visually.
|
|
84
|
+
* @csspart tick - A number at a tick, along its edge of the box, in the chart's own drawing.
|
|
85
|
+
* @csspart tooltip - The bubble, forwarded from `Tooltip`.
|
|
86
|
+
* @csspart tooltip-anchor - The `Tooltip` element itself.
|
|
87
|
+
* @csspart tooltip-arrow - The bubble's point, forwarded from `Tooltip`.
|
|
88
|
+
* @csspart tooltip-arrow-bottom - The point while the bubble is below the reading, on its top edge. Set alongside `tooltip-arrow`.
|
|
89
|
+
* @csspart tooltip-arrow-left - The point while the bubble is left of the reading, on its right edge. Set alongside `tooltip-arrow`.
|
|
90
|
+
* @csspart tooltip-arrow-right - The point while the bubble is right of the reading, on its left edge. Set alongside `tooltip-arrow`.
|
|
91
|
+
* @csspart tooltip-arrow-top - The point while the bubble is above the reading, on its bottom edge. Set alongside `tooltip-arrow`.
|
|
92
|
+
* @csspart tooltip-bottom - The bubble while it is below the reading. Set alongside `tooltip`.
|
|
93
|
+
* @csspart tooltip-cell - One column's name and value inside the tooltip.
|
|
94
|
+
* @csspart tooltip-cell-current - The cell a chart's series keys rest on, which every chart's tooltip can mark. This chart marks none, since its readout says the measure. Set alongside `tooltip-cell`.
|
|
95
|
+
* @csspart tooltip-label - The row's own name inside the tooltip.
|
|
96
|
+
* @csspart tooltip-left - The bubble while it is left of the reading. Set alongside `tooltip`.
|
|
97
|
+
* @csspart tooltip-name - A column's name inside the tooltip.
|
|
98
|
+
* @csspart tooltip-right - The bubble while it is right of the reading. Set alongside `tooltip`.
|
|
99
|
+
* @csspart tooltip-top - The bubble while it is above the reading. Set alongside `tooltip`.
|
|
100
|
+
* @csspart tooltip-value - A column's value inside the tooltip.
|
|
101
|
+
*
|
|
102
|
+
* @cssstate active - While a point is shown, under the pointer or the keys.
|
|
103
|
+
* @cssstate drawn - While a renderer has drawn the points, and the chart's own drawing is hidden.
|
|
104
|
+
* @cssstate empty - While the `empty` part stands in place of the points.
|
|
105
|
+
* @cssstate loading - While the `loading` part stands in place of the points.
|
|
106
|
+
*/
|
|
107
|
+
export declare class HelixHeadlessSpatialScatterChart extends HelixHeadlessSpatialScatterChart_base {
|
|
108
|
+
/**
|
|
109
|
+
* The key this family's words are registered under, its styled tag:
|
|
110
|
+
* `registerWords('de', { 'helix-spatial-scatter-chart': { emptyText: 'Keine Punkte' } })`.
|
|
111
|
+
*/
|
|
112
|
+
static words: string;
|
|
113
|
+
/** The column of text that puts each point in a group, by its heading. None by default. */
|
|
114
|
+
groupBy: string;
|
|
115
|
+
/** The plot's name where the chart has none: no `aria-label`, `label`, or table caption. */
|
|
116
|
+
plotLabel: string;
|
|
117
|
+
/**
|
|
118
|
+
* The `summary` part's sentence. `{count}` is the number of points, written
|
|
119
|
+
* in the locale, and `{x}`, `{y}`, and `{z}` are the three measures' names.
|
|
120
|
+
*/
|
|
121
|
+
summaryLabel: string;
|
|
122
|
+
/** Whether a renderer has drawn the points, which hides the chart's own drawing. */
|
|
123
|
+
private rendered;
|
|
124
|
+
/** Where the chart's own drawing is seen from, which Shift with an arrow turns. */
|
|
125
|
+
private camera;
|
|
126
|
+
/** Each point where the chart's own drawing put it, which the pointer finds. */
|
|
127
|
+
private placed;
|
|
128
|
+
/** Where the renderer draws the reading, which the tooltip points at. */
|
|
129
|
+
private place;
|
|
130
|
+
/** The field last read, kept so a change of it can be told. */
|
|
131
|
+
private last;
|
|
132
|
+
/** The point the keys rested on last, so a change of it can be told. */
|
|
133
|
+
private told;
|
|
134
|
+
/** Carries a host `aria-labelledby` and `aria-describedby` to the plot. */
|
|
135
|
+
protected readonly aria: AriaForwardController;
|
|
136
|
+
/**
|
|
137
|
+
* The table read as points, axes, and groups. **The first three numeric
|
|
138
|
+
* columns are across, up, and in depth**, and a row lacking any of them is
|
|
139
|
+
* left out. A renderer reads it.
|
|
140
|
+
*/
|
|
141
|
+
get field(): PlotField;
|
|
142
|
+
/** The point the keys or the pointer rest on, as its place in `field.points`, or `-1`. */
|
|
143
|
+
get active(): number;
|
|
144
|
+
/** The number of points, which the keys walk. */
|
|
145
|
+
get readings(): number;
|
|
146
|
+
/**
|
|
147
|
+
* Says whether a renderer has drawn the points, which hides the chart's
|
|
148
|
+
* own drawing or shows it again. Called by the connector.
|
|
149
|
+
*/
|
|
150
|
+
rendererDrawn(on?: boolean): void;
|
|
151
|
+
/**
|
|
152
|
+
* Puts the reading on the point at `index`, or lets it go with `-1`, as a
|
|
153
|
+
* pointer resting on it in the renderer's drawing or a press there does.
|
|
154
|
+
* A touch keeps it until a tap lands elsewhere, and a mouse leaving a
|
|
155
|
+
* reading the keys moved to leaves it. Called by the connector.
|
|
156
|
+
*/
|
|
157
|
+
requestActive(index: number, kind?: 'mouse' | 'pen' | 'touch'): void;
|
|
158
|
+
/**
|
|
159
|
+
* Says where the renderer draws the reading, as fractions of the plot from
|
|
160
|
+
* its top left, or `undefined` while it draws none. The tooltip points
|
|
161
|
+
* there. Called by the connector.
|
|
162
|
+
*/
|
|
163
|
+
placeReading(at: {
|
|
164
|
+
x: number;
|
|
165
|
+
y: number;
|
|
166
|
+
} | undefined): void;
|
|
167
|
+
connectedCallback(): void;
|
|
168
|
+
protected updated(changed: PropertyValues): void;
|
|
169
|
+
/**
|
|
170
|
+
* Shift with an arrow turns the view, the chart's own drawing and the
|
|
171
|
+
* renderer's alike, and moves no reading.
|
|
172
|
+
*/
|
|
173
|
+
private readonly onPlotKeydown;
|
|
174
|
+
/** A mouse or a pen over the chart's own drawing puts the reading on the point nearest it. */
|
|
175
|
+
private readonly onDrawingMove;
|
|
176
|
+
private readonly onDrawingLeave;
|
|
177
|
+
/** A tap is a touch reader's hover: it shows the point nearest it until a tap lands elsewhere. */
|
|
178
|
+
private readonly onDrawingTap;
|
|
179
|
+
protected render(): TemplateResult;
|
|
180
|
+
/**
|
|
181
|
+
* The points in 3D, in SVG: the box with its grid, numbers, and names, and
|
|
182
|
+
* each point painted back to front, as the camera sees them. The chart's
|
|
183
|
+
* own drawing, until a renderer has drawn and where none is connected.
|
|
184
|
+
*/
|
|
185
|
+
private renderFallback;
|
|
186
|
+
/** The readout: the point's row in words, once the keys have moved to one. */
|
|
187
|
+
private renderLive;
|
|
188
|
+
/**
|
|
189
|
+
* The reading's row in the tooltip, pointed at the point in the chart's
|
|
190
|
+
* own drawing, which holds it at the point's place.
|
|
191
|
+
*/
|
|
192
|
+
private renderOwnTooltip;
|
|
193
|
+
/**
|
|
194
|
+
* The reading's row in the tooltip every chart shows, pointed at where the
|
|
195
|
+
* renderer draws it. Only once a renderer has drawn; until then the
|
|
196
|
+
* chart's own drawing holds the tooltip at the reading.
|
|
197
|
+
*/
|
|
198
|
+
private renderTooltip;
|
|
199
|
+
/** What the plot shows, in a sentence, which describes the plot. */
|
|
200
|
+
private renderSummary;
|
|
201
|
+
}
|
|
202
|
+
declare global {
|
|
203
|
+
interface HTMLElementTagNameMap {
|
|
204
|
+
'helix-headless-spatial-scatter-chart': HelixHeadlessSpatialScatterChart;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export {};
|