@fusion.dev/helix-plot 0.198.0 → 0.199.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -5
- package/dist/connect-plot/connect-plot.d.ts +31 -0
- package/dist/connect-plot/connect-plot.js +34 -2
- package/dist/connect-spatial-bar-chart/connect-spatial-bar-chart.d.ts +39 -0
- package/dist/connect-spatial-bar-chart/connect-spatial-bar-chart.js +6 -0
- package/dist/connect-spatial-ribbon-chart/connect-spatial-ribbon-chart.d.ts +39 -0
- package/dist/connect-spatial-ribbon-chart/connect-spatial-ribbon-chart.js +6 -0
- package/dist/connect-spatial-surface-chart/connect-spatial-surface-chart.d.ts +8 -0
- package/dist/connect-spatial-treemap/connect-spatial-treemap.d.ts +47 -0
- package/dist/connect-spatial-treemap/connect-spatial-treemap.js +6 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/three/bar-driver.d.ts +35 -0
- package/dist/three/bar-driver.js +104 -0
- package/dist/three/heatmap-driver.js +9 -8
- package/dist/three/index.d.ts +3 -0
- package/dist/three/index.js +3 -0
- package/dist/three/ribbon-driver.d.ts +35 -0
- package/dist/three/ribbon-driver.js +128 -0
- package/dist/three/scatter-driver.js +45 -5
- package/dist/three/stage.d.ts +15 -2
- package/dist/three/stage.js +97 -17
- package/dist/three/surface-driver.js +66 -4
- package/dist/three/treemap-driver.d.ts +36 -0
- package/dist/three/treemap-driver.js +105 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# @fusion.dev/helix-plot
|
|
2
2
|
|
|
3
|
-
Connects the Helix
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Connects the Helix spatial charts to three.js, which draws them in three
|
|
4
|
+
dimensions, lit, in a view you can drag round and zoom, colored from your
|
|
5
|
+
tokens: `Spatial Bar Chart`, `Spatial Heatmap`, `Spatial Ribbon Chart`,
|
|
6
|
+
`Spatial Scatter Chart`, `Spatial Surface Chart`, and `Spatial Treemap`. Each
|
|
6
7
|
also draws itself in SVG, which this package replaces where it is connected.
|
|
7
8
|
|
|
9
|
+
The spatial charts and this package are experimental, so their API can change.
|
|
10
|
+
|
|
8
11
|
```sh
|
|
9
12
|
bun add @fusion.dev/helix @fusion.dev/helix-plot three
|
|
10
13
|
```
|
|
@@ -98,6 +101,102 @@ const connection = connectSpatialSurfaceChart(chart, threeSurfaceDriver(chart.qu
|
|
|
98
101
|
|---|---|---|
|
|
99
102
|
| `density` | screen × 1.5, 2 to 3; on a touch screen, the screen's own, at most 2 | How many pixels the drawing has for each of the page's |
|
|
100
103
|
|
|
104
|
+
## Bars
|
|
105
|
+
|
|
106
|
+
`Spatial Bar Chart` connects the same way:
|
|
107
|
+
|
|
108
|
+
```html
|
|
109
|
+
<table id="signups">…</table>
|
|
110
|
+
|
|
111
|
+
<helix-spatial-bar-chart for="signups" label="Signups by region and quarter">
|
|
112
|
+
<div></div>
|
|
113
|
+
</helix-spatial-bar-chart>
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
import '@fusion.dev/helix/spatial-bar-chart'
|
|
118
|
+
import { connectSpatialBarChart } from '@fusion.dev/helix-plot'
|
|
119
|
+
import { threeBarDriver } from '@fusion.dev/helix-plot/three'
|
|
120
|
+
|
|
121
|
+
const chart = document.querySelector('helix-spatial-bar-chart')
|
|
122
|
+
const connection = connectSpatialBarChart(chart, threeBarDriver(chart.querySelector('div')))
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- **The chart reads its table as `Bar Chart` does.** Each row is a category
|
|
126
|
+
and each numeric column a series. The bars stand on a grid of categories
|
|
127
|
+
across and series in depth, the first series in front, each from zero to its
|
|
128
|
+
value. The box takes the grid's proportions, so one series is a long, low
|
|
129
|
+
row.
|
|
130
|
+
- **Each series takes a series color**, `--chart-series-1` and on, lit from
|
|
131
|
+
above.
|
|
132
|
+
- **A bar the mouse rests on, or one tapped or pressed, is the chart's
|
|
133
|
+
reading**, ringed and shown in the chart's tooltip. The left and right arrows
|
|
134
|
+
move from category to category, and the down and up arrows from series to
|
|
135
|
+
series.
|
|
136
|
+
- The box, the turning, the frames, and the shared contexts are the scatter's.
|
|
137
|
+
|
|
138
|
+
| `threeBarDriver` option | Default | What else it takes |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `density` | screen × 1.5, 2 to 3; on a touch screen, the screen's own, at most 2 | How many pixels the drawing has for each of the page's |
|
|
141
|
+
|
|
142
|
+
## Ribbons
|
|
143
|
+
|
|
144
|
+
`Spatial Ribbon Chart` connects the same way, with `connectSpatialRibbonChart`
|
|
145
|
+
and `threeRibbonDriver`:
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
import '@fusion.dev/helix/spatial-ribbon-chart'
|
|
149
|
+
import { connectSpatialRibbonChart } from '@fusion.dev/helix-plot'
|
|
150
|
+
import { threeRibbonDriver } from '@fusion.dev/helix-plot/three'
|
|
151
|
+
|
|
152
|
+
const chart = document.querySelector('helix-spatial-ribbon-chart')
|
|
153
|
+
const connection = connectSpatialRibbonChart(chart, threeRibbonDriver(chart.querySelector('div')))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- **The chart reads its table as `Line Chart` does.** Each row is a place
|
|
157
|
+
along the ribbons and each numeric column a series, one ribbon behind
|
|
158
|
+
another, the first in front, in a box long across and shallow in depth.
|
|
159
|
+
- **Each series takes a series color**, `--chart-series-1` and on, lit from
|
|
160
|
+
above.
|
|
161
|
+
- **A reading the mouse rests on, or one tapped or pressed, is the chart's
|
|
162
|
+
reading**, ringed and shown in the chart's tooltip. The left and right
|
|
163
|
+
arrows move along a ribbon, and the down and up arrows from ribbon to
|
|
164
|
+
ribbon.
|
|
165
|
+
- The box, the turning, the frames, and the shared contexts are the scatter's.
|
|
166
|
+
|
|
167
|
+
| `threeRibbonDriver` option | Default | What else it takes |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `density` | screen × 1.5, 2 to 3; on a touch screen, the screen's own, at most 2 | How many pixels the drawing has for each of the page's |
|
|
170
|
+
|
|
171
|
+
## A treemap
|
|
172
|
+
|
|
173
|
+
`Spatial Treemap` connects the same way, with `connectSpatialTreemap` and
|
|
174
|
+
`threeTreemapDriver`:
|
|
175
|
+
|
|
176
|
+
```js
|
|
177
|
+
import '@fusion.dev/helix/spatial-treemap'
|
|
178
|
+
import { connectSpatialTreemap } from '@fusion.dev/helix-plot'
|
|
179
|
+
import { threeTreemapDriver } from '@fusion.dev/helix-plot/three'
|
|
180
|
+
|
|
181
|
+
const chart = document.querySelector('helix-spatial-treemap')
|
|
182
|
+
const connection = connectSpatialTreemap(chart, threeTreemapDriver(chart.querySelector('div')))
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- **The chart shares its floor out among its rows.** The first numeric column
|
|
186
|
+
is each block's size, its share of the floor, and the second its height.
|
|
187
|
+
`group-by` names a column of text that gathers rows into one part of the
|
|
188
|
+
floor.
|
|
189
|
+
- **Each group takes a series color**, `--chart-series-1` and on, lit from
|
|
190
|
+
above.
|
|
191
|
+
- **A block the mouse rests on, or one tapped or pressed, is the chart's
|
|
192
|
+
reading**, ringed and shown in the chart's tooltip. The left and right
|
|
193
|
+
arrows move from block to block in the table's order.
|
|
194
|
+
- The box, the turning, the frames, and the shared contexts are the scatter's.
|
|
195
|
+
|
|
196
|
+
| `threeTreemapDriver` option | Default | What else it takes |
|
|
197
|
+
|---|---|---|
|
|
198
|
+
| `density` | screen × 1.5, 2 to 3; on a touch screen, the screen's own, at most 2 | How many pixels the drawing has for each of the page's |
|
|
199
|
+
|
|
101
200
|
## A heatmap
|
|
102
201
|
|
|
103
202
|
`Spatial Heatmap` takes the renderer's box in its `renderer` slot, since its
|
|
@@ -141,7 +240,7 @@ const connection = connectSpatialHeatmap(chart, threeHeatmapDriver(box))
|
|
|
141
240
|
|
|
142
241
|
| Import | What |
|
|
143
242
|
|---|---|
|
|
144
|
-
| `@fusion.dev/helix-plot` | `connectSpatialHeatmap(chart, driver)`, `connectSpatialScatterChart(chart, driver)`, `connectSpatialSurfaceChart(chart, driver)`, `plotColorsFrom(chart, groups, extra)`, and the types `Axes`, `Axis`, `Field`, `HeatmapCell`, `Place`, `PlotColors`, `PlotConnection`, `PlotDriver`, `Point`, `PointerKind`, `SpatialHeatmapConnection`, `SpatialHeatmapDriver`, `SpatialHeatmapField`, `SpatialScatterChartConnection`, `SpatialScatterChartDriver`, `SpatialSurfaceChartConnection`, `SpatialSurfaceChartDriver`, `SurfaceCell` and `
|
|
145
|
-
| `@fusion.dev/helix-plot/three` | `threeHeatmapDriver(box, options)`, `threeScatterDriver(box, options)` and `
|
|
243
|
+
| `@fusion.dev/helix-plot` | `connectSpatialBarChart(chart, driver)`, `connectSpatialHeatmap(chart, driver)`, `connectSpatialRibbonChart(chart, driver)`, `connectSpatialScatterChart(chart, driver)`, `connectSpatialSurfaceChart(chart, driver)`, `connectSpatialTreemap(chart, driver)`, `plotColorsFrom(chart, groups, extra)`, and the types `Axes`, `Axis`, `BarCell`, `BarField`, `Corners`, `Field`, `HeatmapCell`, `Place`, `PlotColors`, `PlotConnection`, `PlotDriver`, `Point`, `PointerKind`, `RibbonCell`, `RibbonField`, `SpatialBarChartConnection`, `SpatialBarChartDriver`, `SpatialHeatmapConnection`, `SpatialHeatmapDriver`, `SpatialHeatmapField`, `SpatialRibbonChartConnection`, `SpatialRibbonChartDriver`, `SpatialScatterChartConnection`, `SpatialScatterChartDriver`, `SpatialSurfaceChartConnection`, `SpatialSurfaceChartDriver`, `SpatialTreemapConnection`, `SpatialTreemapDriver`, `SurfaceCell`, `SurfaceField`, `TreemapBlock`, and `TreemapField` |
|
|
244
|
+
| `@fusion.dev/helix-plot/three` | `threeBarDriver(box, options)`, `threeHeatmapDriver(box, options)`, `threeRibbonDriver(box, options)`, `threeScatterDriver(box, options)`, `threeSurfaceDriver(box, options)`, and `threeTreemapDriver(box, options)`, and the types `ThreeBarDriverOptions`, `ThreeHeatmapDriverOptions`, `ThreeRibbonDriverOptions`, `ThreeScatterDriverOptions`, `ThreeSurfaceDriverOptions`, and `ThreeTreemapDriverOptions` |
|
|
146
245
|
|
|
147
246
|
**three.js is an optional peer**, so a page brings the version it has. `@fusion.dev/helix-plot` itself never imports it: only `@fusion.dev/helix-plot/three` does, so a page with a driver of its own needs no three.js.
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
/** One axis of a plot: its column's name, its range, and the ticks on it. */
|
|
2
2
|
export interface Axis {
|
|
3
|
+
/** The words written at the ticks, for an axis of names such as a bar chart's categories. */
|
|
4
|
+
labels?: readonly string[] | undefined;
|
|
3
5
|
name: string;
|
|
4
6
|
range: readonly [number, number];
|
|
7
|
+
/** Where the grid's lines run along this axis, where not at the ticks. */
|
|
8
|
+
rules?: readonly number[] | undefined;
|
|
5
9
|
ticks: readonly number[];
|
|
6
10
|
}
|
|
7
11
|
/** A plot's three axes: across, up, and in depth. */
|
|
8
12
|
export interface Axes {
|
|
13
|
+
/**
|
|
14
|
+
* How long the box is along each axis, across, up, and in depth, as a share
|
|
15
|
+
* of the cube's, where a chart's grid is not square: `[1, 1, 1]` where it
|
|
16
|
+
* does not say.
|
|
17
|
+
*/
|
|
18
|
+
readonly extent?: readonly [number, number, number] | undefined;
|
|
9
19
|
readonly x: Axis;
|
|
10
20
|
readonly y: Axis;
|
|
11
21
|
readonly z: Axis;
|
|
@@ -25,6 +35,27 @@ export interface PlotColors {
|
|
|
25
35
|
groups: string[];
|
|
26
36
|
/** The names and the ring on what the keys rest on: `foreground`. */
|
|
27
37
|
ink: string;
|
|
38
|
+
/**
|
|
39
|
+
* How the marks are drawn, from the chart's `emphasis` and `material`, as
|
|
40
|
+
* the chart's own drawing reads them: `emphasis` is `solid`, `outlined` (an
|
|
41
|
+
* edge in the mark's color over a fill faded toward `page`) or `subtle` (a
|
|
42
|
+
* faint wash under that edge), and `through` how much of the mark comes
|
|
43
|
+
* through, 1 with no material, `glass` 0.62 and `frosted` 0.88, times the
|
|
44
|
+
* theme's `--helix-material-opacity`. `box` is whether the box, its grid,
|
|
45
|
+
* and its numbers and names are drawn, which the chart's `no-box` turns off.
|
|
46
|
+
* `contours` and `drops` are whether a surface's contour lines and a
|
|
47
|
+
* scatter's lines down to the floor are drawn, as its `contours` and
|
|
48
|
+
* `drop-lines` ask.
|
|
49
|
+
*/
|
|
50
|
+
look: {
|
|
51
|
+
box: boolean;
|
|
52
|
+
contours: boolean;
|
|
53
|
+
drops: boolean;
|
|
54
|
+
emphasis: 'outlined' | 'solid' | 'subtle';
|
|
55
|
+
through: number;
|
|
56
|
+
};
|
|
57
|
+
/** The page a faded fill moves toward: `background`. */
|
|
58
|
+
page: string;
|
|
28
59
|
/** The ticks' numbers: `foreground-muted`. */
|
|
29
60
|
quiet: string;
|
|
30
61
|
/**
|
|
@@ -24,15 +24,27 @@ export const plotColorsFrom = (chart, groups, extra = {}) => {
|
|
|
24
24
|
? null
|
|
25
25
|
: read(`var(--helix-color-${name})`);
|
|
26
26
|
};
|
|
27
|
-
const [grid, ground, ink, quiet] = [
|
|
27
|
+
const [grid, ground, ink, quiet, page] = [
|
|
28
28
|
'border',
|
|
29
29
|
'surface-sunken',
|
|
30
30
|
'foreground',
|
|
31
31
|
'foreground-muted',
|
|
32
|
+
'background',
|
|
32
33
|
].map(token);
|
|
33
34
|
if (!grid || !ground || !ink || !quiet) {
|
|
34
35
|
return null;
|
|
35
36
|
}
|
|
37
|
+
const emphasis = chart.getAttribute('emphasis');
|
|
38
|
+
const material = chart.getAttribute('material');
|
|
39
|
+
const strength = Number.parseFloat(getComputedStyle(chart).getPropertyValue('--helix-material-opacity'));
|
|
40
|
+
const scale = Number.isFinite(strength) ? strength : 1;
|
|
41
|
+
const look = {
|
|
42
|
+
box: !chart.hasAttribute('no-box'),
|
|
43
|
+
contours: chart.hasAttribute('contours'),
|
|
44
|
+
drops: chart.hasAttribute('drop-lines'),
|
|
45
|
+
emphasis: emphasis === 'outlined' || emphasis === 'subtle' ? emphasis : 'solid',
|
|
46
|
+
through: material === 'glass' ? 0.62 * scale : material === 'frosted' ? 0.88 * scale : 1,
|
|
47
|
+
};
|
|
36
48
|
const colors = [];
|
|
37
49
|
for (let at = 0; at < Math.max(groups, 1); at++) {
|
|
38
50
|
colors.push(read(`var(--chart-series-${(at % SERIES) + 1}, var(--helix-color-primary))`) ?? ink);
|
|
@@ -43,7 +55,17 @@ export const plotColorsFrom = (chart, groups, extra = {}) => {
|
|
|
43
55
|
for (const [name, value] of Object.entries(extra)) {
|
|
44
56
|
named[name] = read(value) ?? ink;
|
|
45
57
|
}
|
|
46
|
-
return {
|
|
58
|
+
return {
|
|
59
|
+
extra: named,
|
|
60
|
+
grid,
|
|
61
|
+
ground,
|
|
62
|
+
groups: colors,
|
|
63
|
+
ink,
|
|
64
|
+
look,
|
|
65
|
+
page: page ?? ground,
|
|
66
|
+
quiet,
|
|
67
|
+
ramp: { high, low },
|
|
68
|
+
};
|
|
47
69
|
}
|
|
48
70
|
finally {
|
|
49
71
|
probe.remove();
|
|
@@ -108,6 +130,15 @@ export const connectPlot = (frame, driver, options) => {
|
|
|
108
130
|
const stopTheme = watchTheme(frame, () => {
|
|
109
131
|
draw();
|
|
110
132
|
});
|
|
133
|
+
const looks = typeof MutationObserver === 'function'
|
|
134
|
+
? new MutationObserver(() => {
|
|
135
|
+
draw();
|
|
136
|
+
})
|
|
137
|
+
: undefined;
|
|
138
|
+
looks?.observe(frame, {
|
|
139
|
+
attributeFilter: ['contours', 'drop-lines', 'emphasis', 'material', 'no-box'],
|
|
140
|
+
attributes: true,
|
|
141
|
+
});
|
|
111
142
|
void Promise.resolve(chart.updateComplete).then(draw);
|
|
112
143
|
return {
|
|
113
144
|
disconnect: () => {
|
|
@@ -121,6 +152,7 @@ export const connectPlot = (frame, driver, options) => {
|
|
|
121
152
|
chart.placeReading?.(undefined);
|
|
122
153
|
chart.placeCorners?.(undefined);
|
|
123
154
|
stopTheme();
|
|
155
|
+
looks?.disconnect();
|
|
124
156
|
driver.stop?.();
|
|
125
157
|
chart.rendererDrawn?.(false);
|
|
126
158
|
},
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Axes, type PlotConnection, type PlotDriver } from '../connect-plot/connect-plot.js';
|
|
2
|
+
/** One place on a bar chart's grid: a category across, a series in depth, and its value, where the table has one. */
|
|
3
|
+
export interface BarCell {
|
|
4
|
+
across: number;
|
|
5
|
+
column: number;
|
|
6
|
+
depth: number;
|
|
7
|
+
label: string;
|
|
8
|
+
row: number;
|
|
9
|
+
value?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* What a Spatial Bar Chart publishes for a renderer to draw: every place on
|
|
13
|
+
* its grid, across first, so the bar for category `across` in series
|
|
14
|
+
* `depth` is `cells[across * series.length + depth]`, and its axes, whose
|
|
15
|
+
* `extent` gives the box the grid's proportions.
|
|
16
|
+
*/
|
|
17
|
+
export interface BarField {
|
|
18
|
+
axes: Axes;
|
|
19
|
+
categories: readonly string[];
|
|
20
|
+
cells: readonly BarCell[];
|
|
21
|
+
readings: number;
|
|
22
|
+
series: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
/** A renderer for a Spatial Bar Chart: `focus` and `onPress` take a bar's index in `field.cells`. */
|
|
25
|
+
export type SpatialBarChartDriver = PlotDriver<BarField>;
|
|
26
|
+
/** A connection between a Spatial Bar Chart and its renderer. */
|
|
27
|
+
export type SpatialBarChartConnection = PlotConnection;
|
|
28
|
+
/**
|
|
29
|
+
* Connects a Spatial Bar Chart to whatever draws it in three dimensions.
|
|
30
|
+
*
|
|
31
|
+
* The chart is `helix-spatial-bar-chart` or `helix-headless-spatial-bar-chart`,
|
|
32
|
+
* and the driver is `threeBarDriver` from `@fusion.dev/helix-plot/three`, or
|
|
33
|
+
* one of your own. The connection draws the chart's bars in the driver, each
|
|
34
|
+
* series in a series color, marks the bar the keys rest on, turns the view
|
|
35
|
+
* for Shift with an arrow, and puts a bar pressed in the drawing on the
|
|
36
|
+
* chart's reading. Once the driver has drawn, the chart hides its own SVG
|
|
37
|
+
* drawing; `disconnect()` shows it again.
|
|
38
|
+
*/
|
|
39
|
+
export declare const connectSpatialBarChart: (frame: HTMLElement, driver: SpatialBarChartDriver) => SpatialBarChartConnection;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { connectPlot, } from '../connect-plot/connect-plot.js';
|
|
2
|
+
export const connectSpatialBarChart = (frame, driver) => connectPlot(frame, driver, {
|
|
3
|
+
groupsOf: (field) => field.series.length,
|
|
4
|
+
name: 'connectSpatialBarChart',
|
|
5
|
+
readings: (field) => field.readings,
|
|
6
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Axes, type PlotConnection, type PlotDriver } from '../connect-plot/connect-plot.js';
|
|
2
|
+
/** One reading of a ribbon chart: a place across, a series in depth, and its value, where the table has one. */
|
|
3
|
+
export interface RibbonCell {
|
|
4
|
+
across: number;
|
|
5
|
+
column: number;
|
|
6
|
+
depth: number;
|
|
7
|
+
label: string;
|
|
8
|
+
row: number;
|
|
9
|
+
value?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* What a Spatial Ribbon Chart publishes for a renderer to draw: every
|
|
13
|
+
* reading, across first, so the reading at place `across` in series `depth`
|
|
14
|
+
* is `cells[across * series.length + depth]`, and its axes, whose `extent`
|
|
15
|
+
* makes the box long across and shallow in depth.
|
|
16
|
+
*/
|
|
17
|
+
export interface RibbonField {
|
|
18
|
+
axes: Axes;
|
|
19
|
+
categories: readonly string[];
|
|
20
|
+
cells: readonly RibbonCell[];
|
|
21
|
+
readings: number;
|
|
22
|
+
series: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
/** A renderer for a Spatial Ribbon Chart: `focus` and `onPress` take a reading's index in `field.cells`. */
|
|
25
|
+
export type SpatialRibbonChartDriver = PlotDriver<RibbonField>;
|
|
26
|
+
/** A connection between a Spatial Ribbon Chart and its renderer. */
|
|
27
|
+
export type SpatialRibbonChartConnection = PlotConnection;
|
|
28
|
+
/**
|
|
29
|
+
* Connects a Spatial Ribbon Chart to whatever draws it in three dimensions.
|
|
30
|
+
*
|
|
31
|
+
* The chart is `helix-spatial-ribbon-chart` or
|
|
32
|
+
* `helix-headless-spatial-ribbon-chart`, and the driver is `threeRibbonDriver`
|
|
33
|
+
* from `@fusion.dev/helix-plot/three`, or one of your own. The connection
|
|
34
|
+
* draws the chart's ribbons in the driver, each series in a series color,
|
|
35
|
+
* marks the reading the keys rest on, turns the view for Shift with an
|
|
36
|
+
* arrow, and puts a reading pressed in the drawing on the chart's reading. Once the driver has drawn, the chart hides its own SVG
|
|
37
|
+
* drawing; `disconnect()` shows it again.
|
|
38
|
+
*/
|
|
39
|
+
export declare const connectSpatialRibbonChart: (frame: HTMLElement, driver: SpatialRibbonChartDriver) => SpatialRibbonChartConnection;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { connectPlot, } from '../connect-plot/connect-plot.js';
|
|
2
|
+
export const connectSpatialRibbonChart = (frame, driver) => connectPlot(frame, driver, {
|
|
3
|
+
groupsOf: (field) => field.series.length,
|
|
4
|
+
name: 'connectSpatialRibbonChart',
|
|
5
|
+
readings: (field) => field.readings,
|
|
6
|
+
});
|
|
@@ -16,7 +16,15 @@ export interface SurfaceField {
|
|
|
16
16
|
acrosses: readonly number[];
|
|
17
17
|
axes: Axes;
|
|
18
18
|
cells: readonly SurfaceCell[];
|
|
19
|
+
/** The contour lines at each tick of the height, each segment two places across and in depth, which `contours` draws on the surface. */
|
|
20
|
+
contours?: readonly {
|
|
21
|
+
level: number;
|
|
22
|
+
segments: readonly (readonly [readonly [number, number], readonly [number, number]])[];
|
|
23
|
+
}[];
|
|
19
24
|
depths: readonly number[];
|
|
25
|
+
/** The lowest and the highest reading. */
|
|
26
|
+
highest?: number;
|
|
27
|
+
lowest?: number;
|
|
20
28
|
readings: number;
|
|
21
29
|
}
|
|
22
30
|
/** A renderer for a Spatial Surface Chart: `focus` and `onPress` take a place's index in `field.cells`. */
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type Axes, type PlotConnection, type PlotDriver } from '../connect-plot/connect-plot.js';
|
|
2
|
+
/** One row of a treemap drawn as a block: its part of the floor, its size, its height where the table has one, and its group. */
|
|
3
|
+
export interface TreemapBlock {
|
|
4
|
+
/** Its part of the floor, across from `x0` to `x1` and in depth from `z0` to `z1`, each from -1 to 1. */
|
|
5
|
+
area: {
|
|
6
|
+
x0: number;
|
|
7
|
+
x1: number;
|
|
8
|
+
z0: number;
|
|
9
|
+
z1: number;
|
|
10
|
+
};
|
|
11
|
+
group: string;
|
|
12
|
+
groupIndex: number;
|
|
13
|
+
height?: number | undefined;
|
|
14
|
+
label: string;
|
|
15
|
+
row: number;
|
|
16
|
+
size: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What a Spatial Treemap publishes for a renderer to draw: every block, in
|
|
20
|
+
* the table's order, each on its part of the floor, and the axes, whose `y`
|
|
21
|
+
* is the height and whose `extent` gives the box its proportions. `height`
|
|
22
|
+
* is `''` where no column gives one, and each block is then a slab as tall
|
|
23
|
+
* as the box.
|
|
24
|
+
*/
|
|
25
|
+
export interface TreemapField {
|
|
26
|
+
axes: Axes;
|
|
27
|
+
blocks: readonly TreemapBlock[];
|
|
28
|
+
groups: readonly string[];
|
|
29
|
+
height: string;
|
|
30
|
+
size: string;
|
|
31
|
+
}
|
|
32
|
+
/** A renderer for a Spatial Treemap: `focus` and `onPress` take a block's index in `field.blocks`. */
|
|
33
|
+
export type SpatialTreemapDriver = PlotDriver<TreemapField>;
|
|
34
|
+
/** A connection between a Spatial Treemap and its renderer. */
|
|
35
|
+
export type SpatialTreemapConnection = PlotConnection;
|
|
36
|
+
/**
|
|
37
|
+
* Connects a Spatial Treemap to whatever draws it in three dimensions.
|
|
38
|
+
*
|
|
39
|
+
* The chart is `helix-spatial-treemap` or `helix-headless-spatial-treemap`,
|
|
40
|
+
* and the driver is `threeTreemapDriver` from `@fusion.dev/helix-plot/three`,
|
|
41
|
+
* or one of your own. The connection draws the chart's blocks in the driver,
|
|
42
|
+
* each group in a series color, marks the block the keys rest on, turns the
|
|
43
|
+
* view for Shift with an arrow, and puts a block pressed in the drawing on
|
|
44
|
+
* the chart's reading. Once the driver has drawn, the chart hides its own SVG
|
|
45
|
+
* drawing; `disconnect()` shows it again.
|
|
46
|
+
*/
|
|
47
|
+
export declare const connectSpatialTreemap: (frame: HTMLElement, driver: SpatialTreemapDriver) => SpatialTreemapConnection;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { connectPlot, } from '../connect-plot/connect-plot.js';
|
|
2
|
+
export const connectSpatialTreemap = (frame, driver) => connectPlot(frame, driver, {
|
|
3
|
+
groupsOf: (field) => field.groups.length,
|
|
4
|
+
name: 'connectSpatialTreemap',
|
|
5
|
+
readings: (field) => field.blocks.length,
|
|
6
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { type Axes, type Axis, type Corners, type Place, type PlotColors, type PlotConnection, type PlotDriver, type PointerKind, plotColorsFrom, } from './connect-plot/connect-plot.js';
|
|
2
|
+
export { type BarCell, type BarField, connectSpatialBarChart, type SpatialBarChartConnection, type SpatialBarChartDriver, } from './connect-spatial-bar-chart/connect-spatial-bar-chart.js';
|
|
2
3
|
export { connectSpatialHeatmap, type HeatmapCell, type SpatialHeatmapConnection, type SpatialHeatmapDriver, type SpatialHeatmapField, } from './connect-spatial-heatmap/connect-spatial-heatmap.js';
|
|
4
|
+
export { connectSpatialRibbonChart, type RibbonCell, type RibbonField, type SpatialRibbonChartConnection, type SpatialRibbonChartDriver, } from './connect-spatial-ribbon-chart/connect-spatial-ribbon-chart.js';
|
|
3
5
|
export { connectSpatialScatterChart, type Field, type Point, type SpatialScatterChartConnection, type SpatialScatterChartDriver, } from './connect-spatial-scatter-chart/connect-spatial-scatter-chart.js';
|
|
4
6
|
export { connectSpatialSurfaceChart, type SpatialSurfaceChartConnection, type SpatialSurfaceChartDriver, type SurfaceCell, type SurfaceField, } from './connect-spatial-surface-chart/connect-spatial-surface-chart.js';
|
|
7
|
+
export { connectSpatialTreemap, type SpatialTreemapConnection, type SpatialTreemapDriver, type TreemapBlock, type TreemapField, } from './connect-spatial-treemap/connect-spatial-treemap.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
export { plotColorsFrom, } from './connect-plot/connect-plot.js';
|
|
2
|
+
export { connectSpatialBarChart, } from './connect-spatial-bar-chart/connect-spatial-bar-chart.js';
|
|
2
3
|
export { connectSpatialHeatmap, } from './connect-spatial-heatmap/connect-spatial-heatmap.js';
|
|
4
|
+
export { connectSpatialRibbonChart, } from './connect-spatial-ribbon-chart/connect-spatial-ribbon-chart.js';
|
|
3
5
|
export { connectSpatialScatterChart, } from './connect-spatial-scatter-chart/connect-spatial-scatter-chart.js';
|
|
4
6
|
export { connectSpatialSurfaceChart, } from './connect-spatial-surface-chart/connect-spatial-surface-chart.js';
|
|
7
|
+
export { connectSpatialTreemap, } from './connect-spatial-treemap/connect-spatial-treemap.js';
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { SpatialBarChartDriver } from '../connect-spatial-bar-chart/connect-spatial-bar-chart.js';
|
|
2
|
+
/** How the three.js bar driver behaves where the page does not say. */
|
|
3
|
+
export interface ThreeBarDriverOptions {
|
|
4
|
+
/**
|
|
5
|
+
* How many pixels the drawing has for each of the page's. By default, on a
|
|
6
|
+
* screen worked with a mouse, half as many again as the screen has, at
|
|
7
|
+
* least two and at most three; on a touch screen, as many as the screen
|
|
8
|
+
* has, at most two.
|
|
9
|
+
*/
|
|
10
|
+
density?: number | undefined;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Draws a Spatial Bar Chart's bars in three dimensions with three.js. Pass it
|
|
14
|
+
* to `connectSpatialBarChart` with the element to draw in, which the chart
|
|
15
|
+
* holds in its plot:
|
|
16
|
+
*
|
|
17
|
+
* ```js
|
|
18
|
+
* import { connectSpatialBarChart } from '@fusion.dev/helix-plot'
|
|
19
|
+
* import { threeBarDriver } from '@fusion.dev/helix-plot/three'
|
|
20
|
+
*
|
|
21
|
+
* connectSpatialBarChart(chart, threeBarDriver(chart.querySelector('div')))
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* **Each bar is a block from zero to its value**, on a grid of categories
|
|
25
|
+
* across and series in depth, in a box with a grid on its floor and two of
|
|
26
|
+
* its walls, lit from above. Each series takes a series color from the theme.
|
|
27
|
+
*
|
|
28
|
+
* **Dragging turns the box, and the wheel and a pinch bring it nearer.**
|
|
29
|
+
* Shift with an arrow on the chart turns it a step. It never turns by itself.
|
|
30
|
+
* It draws only while something moves, asks for no frames at rest, and
|
|
31
|
+
* shares the page's WebGL contexts with every three.js drawing Helix makes.
|
|
32
|
+
*
|
|
33
|
+
* The renderer is the driver's own, and `disconnect()` frees it.
|
|
34
|
+
*/
|
|
35
|
+
export declare const threeBarDriver: (container: HTMLElement, options?: ThreeBarDriverOptions) => SpatialBarChartDriver;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Box3, BoxGeometry, BufferGeometry, Color, DirectionalLight, EdgesGeometry, Float32BufferAttribute, HemisphereLight, LineBasicMaterial, LineSegments, Mesh, MeshLambertMaterial, Vector3, } from 'three';
|
|
2
|
+
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
|
3
|
+
import { createStage, edgeOf, HALF, inBox, lookThrough } from './stage.js';
|
|
4
|
+
const FOOTPRINT = 0.6;
|
|
5
|
+
export const threeBarDriver = (container, options = {}) => {
|
|
6
|
+
let field;
|
|
7
|
+
let drawnCells = [];
|
|
8
|
+
let tops = new Map();
|
|
9
|
+
const barMaterial = new MeshLambertMaterial({
|
|
10
|
+
polygonOffset: true,
|
|
11
|
+
polygonOffsetFactor: 1,
|
|
12
|
+
polygonOffsetUnits: 1,
|
|
13
|
+
vertexColors: true,
|
|
14
|
+
});
|
|
15
|
+
const bars = new Mesh(new BufferGeometry(), barMaterial);
|
|
16
|
+
const edgeMaterial = new LineBasicMaterial({ vertexColors: true });
|
|
17
|
+
const edges = new LineSegments(new BufferGeometry(), edgeMaterial);
|
|
18
|
+
const stage = createStage(container, {
|
|
19
|
+
density: options.density,
|
|
20
|
+
pick: (ray) => {
|
|
21
|
+
const hit = ray.intersectObject(bars)[0];
|
|
22
|
+
if (hit?.faceIndex === undefined || hit.faceIndex === null) {
|
|
23
|
+
return -1;
|
|
24
|
+
}
|
|
25
|
+
return drawnCells[Math.floor(hit.faceIndex / 12)] ?? -1;
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const sky = new HemisphereLight(0xffffff, 0xffffff, 2.2);
|
|
29
|
+
const sun = new DirectionalLight(0xffffff, 1);
|
|
30
|
+
sun.position.set(-2, 4, 1);
|
|
31
|
+
stage.scene.add(bars, edges, sky, sun);
|
|
32
|
+
const painted = (geometry, color) => {
|
|
33
|
+
const count = geometry.getAttribute('position').count;
|
|
34
|
+
const colors = new Float32Array(count * 3);
|
|
35
|
+
for (let at = 0; at < count; at++) {
|
|
36
|
+
colors.set([color.r, color.g, color.b], at * 3);
|
|
37
|
+
}
|
|
38
|
+
geometry.setAttribute('color', new Float32BufferAttribute(colors, 3));
|
|
39
|
+
return geometry;
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
draw: (next, colors, font) => {
|
|
43
|
+
field = next;
|
|
44
|
+
const [ex, ey, ez] = next.axes.extent ?? [1, 1, 1];
|
|
45
|
+
const spacing = Math.min((2 * HALF * ex) / Math.max(next.categories.length, 1), (2 * HALF * ez) / Math.max(next.series.length, 1));
|
|
46
|
+
const width = spacing * FOOTPRINT;
|
|
47
|
+
const zero = inBox(next.axes.y, 0) * ey;
|
|
48
|
+
const page = new Color(colors.page);
|
|
49
|
+
const { emphasis, through } = colors.look;
|
|
50
|
+
const solids = [];
|
|
51
|
+
const lines = [];
|
|
52
|
+
drawnCells = [];
|
|
53
|
+
tops = new Map();
|
|
54
|
+
next.cells.forEach((cell, index) => {
|
|
55
|
+
if (cell.value === undefined) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const tone = new Color(colors.groups[cell.depth % colors.groups.length] ?? colors.ink);
|
|
59
|
+
const color = emphasis === 'outlined' ? tone.clone().lerp(page, 0.4) : tone;
|
|
60
|
+
const top = inBox(next.axes.y, cell.value) * ey;
|
|
61
|
+
const tall = Math.max(Math.abs(top - zero), 1e-3);
|
|
62
|
+
const x = inBox(next.axes.x, cell.across) * ex;
|
|
63
|
+
const z = inBox(next.axes.z, cell.depth) * ez;
|
|
64
|
+
const box = new BoxGeometry(width, tall, width);
|
|
65
|
+
box.translate(x, Math.min(top, zero) + tall / 2, z);
|
|
66
|
+
solids.push(painted(box, color));
|
|
67
|
+
lines.push(painted(new EdgesGeometry(box), emphasis === 'solid' ? edgeOf(color) : tone));
|
|
68
|
+
drawnCells.push(index);
|
|
69
|
+
tops.set(index, new Vector3(x, Math.max(top, zero), z));
|
|
70
|
+
});
|
|
71
|
+
lookThrough(barMaterial, (emphasis === 'subtle' ? 0.2 : 1) * through);
|
|
72
|
+
lookThrough(edgeMaterial, through);
|
|
73
|
+
bars.geometry.dispose();
|
|
74
|
+
edges.geometry.dispose();
|
|
75
|
+
bars.geometry = (solids.length > 0 && mergeGeometries(solids)) || new BufferGeometry();
|
|
76
|
+
edges.geometry = (lines.length > 0 && mergeGeometries(lines)) || new BufferGeometry();
|
|
77
|
+
for (const one of [...solids, ...lines]) {
|
|
78
|
+
one.dispose();
|
|
79
|
+
}
|
|
80
|
+
stage.setAxes(next.axes, colors, font);
|
|
81
|
+
if (colors.look.box || tops.size === 0) {
|
|
82
|
+
stage.frame(new Vector3(-HALF * ex, -HALF * ey, -HALF * ez), new Vector3(HALF * ex, HALF * ey, HALF * ez));
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const around = new Box3().setFromBufferAttribute(bars.geometry.getAttribute('position'));
|
|
86
|
+
stage.frame(around.min, around.max);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
focus: (index) => {
|
|
90
|
+
stage.setMarker(field ? tops.get(index) : undefined);
|
|
91
|
+
},
|
|
92
|
+
onHover: (listener) => stage.onHover(listener),
|
|
93
|
+
onPlace: (listener) => stage.onPlace(listener),
|
|
94
|
+
onPress: (listener) => stage.onPress(listener),
|
|
95
|
+
stop: () => {
|
|
96
|
+
bars.geometry.dispose();
|
|
97
|
+
barMaterial.dispose();
|
|
98
|
+
edges.geometry.dispose();
|
|
99
|
+
edgeMaterial.dispose();
|
|
100
|
+
stage.stop();
|
|
101
|
+
},
|
|
102
|
+
turnBy: (across, up, animate) => stage.turnBy(across, up, animate),
|
|
103
|
+
};
|
|
104
|
+
};
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { BoxGeometry, BufferGeometry, Color, DirectionalLight, EdgesGeometry, Float32BufferAttribute, HemisphereLight, LineBasicMaterial, LineSegments, Mesh, MeshLambertMaterial, Vector3, } from 'three';
|
|
2
2
|
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
|
3
|
-
import { createStage } from './stage.js';
|
|
3
|
+
import { createStage, edgeOf, lookThrough } from './stage.js';
|
|
4
4
|
const FOOTPRINT = 0.84;
|
|
5
5
|
const MARGIN = 0.3;
|
|
6
6
|
const THICKNESS = 0.25;
|
|
7
7
|
const TILE = 0.05;
|
|
8
8
|
const FLOOR = -0.3;
|
|
9
9
|
const SPAN = 1.9;
|
|
10
|
-
const edgeOf = (face) => {
|
|
11
|
-
const hsl = { h: 0, l: 0, s: 0 };
|
|
12
|
-
face.getHSL(hsl);
|
|
13
|
-
return face.clone().lerp(new Color(hsl.l < 0.5 ? 0xffffff : 0x000000), hsl.l < 0.5 ? 0.22 : 0.18);
|
|
14
|
-
};
|
|
15
10
|
export const threeHeatmapDriver = (container, options = {}) => {
|
|
16
11
|
let field;
|
|
17
12
|
let framed = false;
|
|
@@ -57,19 +52,25 @@ export const threeHeatmapDriver = (container, options = {}) => {
|
|
|
57
52
|
field = next;
|
|
58
53
|
const unit = SPAN / Math.max(next.columns, next.rows, 1);
|
|
59
54
|
const tile = new Color(colors.extra.tile ?? colors.ground);
|
|
55
|
+
const page = new Color(colors.page);
|
|
56
|
+
const { emphasis, through } = colors.look;
|
|
60
57
|
const solids = [];
|
|
61
58
|
const lines = [];
|
|
62
59
|
tops = next.cells.map((cell) => {
|
|
63
|
-
const
|
|
60
|
+
const tone = cell.tone ? new Color(colors.extra[`tone-${cell.tone}`] ?? colors.ink) : tile;
|
|
61
|
+
const color = emphasis === 'outlined' ? tone.clone().lerp(page, 0.4) : tone;
|
|
64
62
|
const tall = Math.max(cell.height, TILE) * unit;
|
|
65
63
|
const x = (cell.column + 0.5 - next.columns / 2) * unit;
|
|
66
64
|
const z = (cell.row + 0.5 - next.rows / 2) * unit;
|
|
67
65
|
const box = new BoxGeometry(unit * FOOTPRINT, tall, unit * FOOTPRINT);
|
|
68
66
|
box.translate(x, FLOOR + tall / 2, z);
|
|
69
67
|
solids.push(painted(box, color));
|
|
70
|
-
lines.push(painted(new EdgesGeometry(box), edgeOf(color)));
|
|
68
|
+
lines.push(painted(new EdgesGeometry(box), emphasis === 'solid' ? edgeOf(color) : tone));
|
|
71
69
|
return new Vector3(x, FLOOR + tall, z);
|
|
72
70
|
});
|
|
71
|
+
const area = emphasis === 'subtle' ? 0.2 : 1;
|
|
72
|
+
lookThrough(prismMaterial, area * through);
|
|
73
|
+
lookThrough(edgeMaterial, through);
|
|
73
74
|
prisms.geometry.dispose();
|
|
74
75
|
edges.geometry.dispose();
|
|
75
76
|
prisms.geometry = (solids.length > 0 && mergeGeometries(solids)) || new BufferGeometry();
|
package/dist/three/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { type ThreeBarDriverOptions, threeBarDriver } from './bar-driver.js';
|
|
1
2
|
export { type ThreeHeatmapDriverOptions, threeHeatmapDriver } from './heatmap-driver.js';
|
|
3
|
+
export { type ThreeRibbonDriverOptions, threeRibbonDriver } from './ribbon-driver.js';
|
|
2
4
|
export { type ThreeScatterDriverOptions, threeScatterDriver } from './scatter-driver.js';
|
|
3
5
|
export { type ThreeSurfaceDriverOptions, threeSurfaceDriver } from './surface-driver.js';
|
|
6
|
+
export { type ThreeTreemapDriverOptions, threeTreemapDriver } from './treemap-driver.js';
|
package/dist/three/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { threeBarDriver } from './bar-driver.js';
|
|
1
2
|
export { threeHeatmapDriver } from './heatmap-driver.js';
|
|
3
|
+
export { threeRibbonDriver } from './ribbon-driver.js';
|
|
2
4
|
export { threeScatterDriver } from './scatter-driver.js';
|
|
3
5
|
export { threeSurfaceDriver } from './surface-driver.js';
|
|
6
|
+
export { threeTreemapDriver } from './treemap-driver.js';
|