@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 CHANGED
@@ -1,10 +1,13 @@
1
1
  # @fusion.dev/helix-plot
2
2
 
3
- Connects the Helix `Spatial Heatmap`, `Spatial Scatter Chart` and
4
- `Spatial Surface Chart` to three.js, which draws them in three dimensions,
5
- lit, in a view you can drag round and zoom, colored from your tokens. Each
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 `SurfaceField` |
145
- | `@fusion.dev/helix-plot/three` | `threeHeatmapDriver(box, options)`, `threeScatterDriver(box, options)` and `threeSurfaceDriver(box, options)`, and the types `ThreeHeatmapDriverOptions`, `ThreeScatterDriverOptions` and `ThreeSurfaceDriverOptions` |
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 { extra: named, grid, ground, groups: colors, ink, quiet, ramp: { high, low } };
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 color = cell.tone ? new Color(colors.extra[`tone-${cell.tone}`] ?? colors.ink) : tile;
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();
@@ -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';
@@ -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';