@invana/graph-layer-d3-contour 0.0.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/dist/index.d.ts +276 -0
- package/dist/index.js +391 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { EventMap, WorldLayer, WorldLayerHit, LayerOptions, CanvasContext } from '@invana/canvas';
|
|
2
|
+
import { ContourMultiPolygon } from 'd3-contour';
|
|
3
|
+
import { Graphics } from 'pixi.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in colour ramps for {@link DensityContourLayer}.
|
|
7
|
+
*
|
|
8
|
+
* Each palette is an ordered array of `0xRRGGBB` stops from low-density to
|
|
9
|
+
* high-density. The layer interpolates between adjacent stops so any band
|
|
10
|
+
* count (3, 10, 30...) lands on a perceptually-smooth colour.
|
|
11
|
+
*
|
|
12
|
+
* Sequential single-hue palettes (`blues`, `greens`, ...) are drawn from
|
|
13
|
+
* ColorBrewer; perceptual ramps (`viridis`, `plasma`, `magma`, `inferno`)
|
|
14
|
+
* are 10-stop quantizations of matplotlib's perceptual colour maps. `warm`
|
|
15
|
+
* and `cool` are ColorBrewer YlOrRd / BuPu equivalents.
|
|
16
|
+
*/
|
|
17
|
+
type DensityContourPaletteName = 'blues' | 'greens' | 'oranges' | 'purples' | 'reds' | 'viridis' | 'plasma' | 'magma' | 'inferno' | 'warm' | 'cool';
|
|
18
|
+
declare const DENSITY_CONTOUR_PALETTES: Record<DensityContourPaletteName, number[]>;
|
|
19
|
+
/** All built-in palette names in declaration order. Useful for GUI menus. */
|
|
20
|
+
declare const DENSITY_CONTOUR_PALETTE_NAMES: readonly DensityContourPaletteName[];
|
|
21
|
+
/**
|
|
22
|
+
* Linear interpolation between two `0xRRGGBB` colours in sRGB space.
|
|
23
|
+
* `t` is clamped to `[0, 1]`. sRGB-linear is "good enough" for adjacent
|
|
24
|
+
* stops in a smooth ramp; for perceptually-uniform mixing across distant
|
|
25
|
+
* hues, supply a function via `paletteFn`.
|
|
26
|
+
*/
|
|
27
|
+
declare function lerpColor(a: number, b: number, t: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* Linearly interpolate a `0xRRGGBB` colour from a stop array based on the
|
|
30
|
+
* band's position `(index / (total - 1))`. Returns the last stop if there's
|
|
31
|
+
* only one band or one stop.
|
|
32
|
+
*/
|
|
33
|
+
declare function sampleStops(stops: number[], index: number, total: number): number;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Options shared by every `DensityContourLayer*` — the d3-contour compute
|
|
37
|
+
* inputs and the recompute lifecycle. Both {@link DensityContourFillLayer}
|
|
38
|
+
* and {@link DensityContourStrokeLayer} extend this; their layer-specific
|
|
39
|
+
* presentation knobs live on their own options interfaces.
|
|
40
|
+
*/
|
|
41
|
+
interface DensityContourLayerBaseOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Required. Id of the `GraphLayer` whose node positions feed the density
|
|
44
|
+
* estimate. Per canvas architecture: cross-layer deps are declared
|
|
45
|
+
* explicitly, never inferred.
|
|
46
|
+
*/
|
|
47
|
+
graphLayerId: string;
|
|
48
|
+
/**
|
|
49
|
+
* Kernel bandwidth in world units. Larger = smoother / broader blobs.
|
|
50
|
+
* Defaults to `20` (d3's own default).
|
|
51
|
+
*/
|
|
52
|
+
bandwidth?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Either a count of iso-bands or an explicit array of iso-values. Defaults
|
|
55
|
+
* to `10`. With a number, d3-contour picks evenly-spaced thresholds across
|
|
56
|
+
* the value range.
|
|
57
|
+
*/
|
|
58
|
+
thresholds?: number | number[];
|
|
59
|
+
/**
|
|
60
|
+
* Grid cell size in world units. Smaller = sharper bands but quadratically
|
|
61
|
+
* more compute. d3 requires a power of two (1, 2, 4, 8, 16). Defaults to `4`.
|
|
62
|
+
*/
|
|
63
|
+
cellSize?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Padding added around the node bounding box before building the grid, so
|
|
66
|
+
* bands at the edge of the cluster aren't clipped against the grid border.
|
|
67
|
+
* World units. Defaults to `50`.
|
|
68
|
+
*/
|
|
69
|
+
padding?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Recompute trigger:
|
|
72
|
+
* - `'auto'` (default) — subscribe to the source layer's `data:changed`
|
|
73
|
+
* and recompute on a debounce.
|
|
74
|
+
* - `'manual'` — caller drives recompute via `layer.recompute()`.
|
|
75
|
+
*/
|
|
76
|
+
recompute?: 'auto' | 'manual';
|
|
77
|
+
/** Debounce window for `auto` recomputes. Default `120` ms. */
|
|
78
|
+
recomputeDebounceMs?: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The palette-resolution chain shared by both layers. The fill layer
|
|
82
|
+
* consumes it to colour bands; the stroke layer consumes it when
|
|
83
|
+
* `strokeColor: 'palette'`. Resolution order (most specific wins):
|
|
84
|
+
* `fillColor` callback (fill layer only) > `paletteFn(t)` >
|
|
85
|
+
* `paletteRangeStart`/`paletteRangeEnd` (only when BOTH set) > `palette`
|
|
86
|
+
* (name or stop array) > default `'blues'`.
|
|
87
|
+
*/
|
|
88
|
+
interface DensityContourPaletteOptions {
|
|
89
|
+
palette?: DensityContourPaletteName | number[];
|
|
90
|
+
paletteRangeStart?: number;
|
|
91
|
+
paletteRangeEnd?: number;
|
|
92
|
+
paletteFn?: (t: number) => number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Options for {@link DensityContourFillLayer}. Paints filled iso-bands and
|
|
96
|
+
* nothing else — no stroke. For an outline-only look use
|
|
97
|
+
* {@link DensityContourStrokeLayer}; compose both layers (same
|
|
98
|
+
* `graphLayerId`, different `zIndex`) for fill + outline together.
|
|
99
|
+
*/
|
|
100
|
+
interface DensityContourFillLayerOptions extends DensityContourLayerBaseOptions, DensityContourPaletteOptions {
|
|
101
|
+
/**
|
|
102
|
+
* Fully-custom fill colour per band. Receives `(value, index, total)` and
|
|
103
|
+
* returns a `0xRRGGBB` integer. `value` is the iso-value (density);
|
|
104
|
+
* `index` runs 0..total-1 from low-density to high-density. Wins over
|
|
105
|
+
* every other palette option.
|
|
106
|
+
*/
|
|
107
|
+
fillColor?: (value: number, index: number, total: number) => number;
|
|
108
|
+
/** Fill alpha 0..1. Defaults to `0.4`. */
|
|
109
|
+
fillOpacity?: number;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Options for {@link DensityContourStrokeLayer}. Paints iso-line outlines
|
|
113
|
+
* and nothing else — no fill. Defaults match Observable's
|
|
114
|
+
* [`@d3/density-contours`](https://observablehq.com/@d3/density-contours):
|
|
115
|
+
* steelblue strokes, every 5th band stroked at 1 unit, the rest at 0.25
|
|
116
|
+
* (the topographic "index contour" pattern).
|
|
117
|
+
*/
|
|
118
|
+
interface DensityContourStrokeLayerOptions extends DensityContourLayerBaseOptions, DensityContourPaletteOptions {
|
|
119
|
+
/**
|
|
120
|
+
* Band outline colour.
|
|
121
|
+
*
|
|
122
|
+
* - `0xRRGGBB` → constant colour for every band. Default `0x4682b4`
|
|
123
|
+
* (steelblue, Observable's default).
|
|
124
|
+
* - `'palette'` → resolved per band through the palette chain above
|
|
125
|
+
* ({@link paletteFn} > range > {@link palette}). Use this for the
|
|
126
|
+
* "rainbow iso-lines" look.
|
|
127
|
+
*/
|
|
128
|
+
strokeColor?: number | 'palette';
|
|
129
|
+
/**
|
|
130
|
+
* Band outline width. Either a constant or a per-band function that
|
|
131
|
+
* receives `(index, total, value)` and returns a width in world units.
|
|
132
|
+
*
|
|
133
|
+
* The function form is the most general — useful for any pattern where
|
|
134
|
+
* width depends on `index`. For the canonical topo-map "every Nth line
|
|
135
|
+
* heavy" look, prefer the declarative {@link indexEvery}/
|
|
136
|
+
* {@link indexMajorWidth}/{@link indexMinorWidth} sugar below.
|
|
137
|
+
*
|
|
138
|
+
* Default `0.5`.
|
|
139
|
+
*/
|
|
140
|
+
strokeWidth?: number | ((index: number, total: number, value: number) => number);
|
|
141
|
+
/**
|
|
142
|
+
* Index-contour sugar — every `indexEvery`-th band (counting from
|
|
143
|
+
* low-density at `i=0`) is stroked with {@link indexMajorWidth}, all
|
|
144
|
+
* others with {@link indexMinorWidth}. Reproduces the topographic
|
|
145
|
+
* "index contour every N lines" pattern used by Observable's
|
|
146
|
+
* `@d3/density-contours`.
|
|
147
|
+
*
|
|
148
|
+
* Precedence: function-form {@link strokeWidth} wins over the sugar (so
|
|
149
|
+
* callers can opt fully out by setting `strokeWidth` to a callback). The
|
|
150
|
+
* sugar wins over numeric {@link strokeWidth}. All three sugar fields
|
|
151
|
+
* must be set together; partial setups fall back to {@link strokeWidth}.
|
|
152
|
+
*
|
|
153
|
+
* Defaults reproduce Observable's example: `indexEvery: 5`,
|
|
154
|
+
* `indexMajorWidth: 1`, `indexMinorWidth: 0.25`.
|
|
155
|
+
*/
|
|
156
|
+
indexEvery?: number;
|
|
157
|
+
indexMajorWidth?: number;
|
|
158
|
+
indexMinorWidth?: number;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Reserved. Neither layer currently projects user-mutated state — the
|
|
162
|
+
* computed contour data is held as a private field, not in `Layer.state`,
|
|
163
|
+
* because it's bulk geometry that's rebuilt wholesale on each recompute
|
|
164
|
+
* rather than diffed.
|
|
165
|
+
*/
|
|
166
|
+
interface DensityContourLayerState {
|
|
167
|
+
readonly _placeholder?: never;
|
|
168
|
+
}
|
|
169
|
+
interface DensityContourLayerEvents extends EventMap {
|
|
170
|
+
/** Fired after each recompute completes, before paint. */
|
|
171
|
+
recompute: {
|
|
172
|
+
thresholds: number;
|
|
173
|
+
points: number;
|
|
174
|
+
durationMs: number;
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* `DensityContourLayerBase` — abstract `WorldLayer` that owns the
|
|
180
|
+
* d3-contour density compute and recompute lifecycle. Concrete subclasses
|
|
181
|
+
* decide *how* the resulting iso-bands are painted: filled
|
|
182
|
+
* ({@link DensityContourFillLayer}) or stroked
|
|
183
|
+
* ({@link DensityContourStrokeLayer}).
|
|
184
|
+
*
|
|
185
|
+
* The compute lives in world space so iso-bands track the source graph as
|
|
186
|
+
* the camera pans and zooms. Recompute is debounced (default 120 ms) and
|
|
187
|
+
* triggered by the source `GraphLayer`'s `data:changed`. `contourDensity`
|
|
188
|
+
* is O(n · grid²), so per-frame recompute during drag would tank perf —
|
|
189
|
+
* set `recompute: 'manual'` and call `layer.recompute()` from a drag
|
|
190
|
+
* behaviour if you need that.
|
|
191
|
+
*
|
|
192
|
+
* Subclasses implement {@link paintDensity} to render the
|
|
193
|
+
* `ContourMultiPolygon[]` into the layer's `Graphics`. All shared state
|
|
194
|
+
* (subscription, debounce timer, bounds math) is owned here.
|
|
195
|
+
*/
|
|
196
|
+
|
|
197
|
+
declare abstract class DensityContourLayerBase<TOpt extends DensityContourLayerBaseOptions, TEvt extends DensityContourLayerEvents = DensityContourLayerEvents> extends WorldLayer<TOpt, DensityContourLayerState, TEvt, never, WorldLayerHit> {
|
|
198
|
+
private readonly graphLayerId;
|
|
199
|
+
private graph;
|
|
200
|
+
protected gfx: Graphics | null;
|
|
201
|
+
private readonly subs;
|
|
202
|
+
private debounceTimer;
|
|
203
|
+
constructor(opts: LayerOptions<TOpt>);
|
|
204
|
+
protected createState(): DensityContourLayerState;
|
|
205
|
+
protected onMount(ctx: CanvasContext): void;
|
|
206
|
+
protected onUnmount(): void;
|
|
207
|
+
/**
|
|
208
|
+
* Force an immediate recompute. Useful in `recompute: 'manual'` mode, or
|
|
209
|
+
* to refresh the overlay after externally mutating options that don't
|
|
210
|
+
* have setters yet.
|
|
211
|
+
*/
|
|
212
|
+
recompute(): void;
|
|
213
|
+
hitTest(_worldX: number, _worldY: number): WorldLayerHit | null;
|
|
214
|
+
private scheduleRecompute;
|
|
215
|
+
private computeAndPaint;
|
|
216
|
+
/**
|
|
217
|
+
* Render the iso-bands into `g`. The bands are ordered low-density →
|
|
218
|
+
* high-density; subclasses typically paint in that order so denser bands
|
|
219
|
+
* sit on top. `offsetX`/`offsetY` are the world-space origin of the
|
|
220
|
+
* compute grid — add them to each polygon point.
|
|
221
|
+
*/
|
|
222
|
+
protected abstract paintDensity(g: Graphics, density: ContourMultiPolygon[], offsetX: number, offsetY: number): void;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* `DensityContourFillLayer` — paints filled iso-bands from a
|
|
227
|
+
* d3-contour density estimate over a source `GraphLayer`'s node positions.
|
|
228
|
+
* No outline. For the stroked / Observable-style look, use
|
|
229
|
+
* {@link DensityContourStrokeLayer}; compose both layers (same
|
|
230
|
+
* `graphLayerId`, different `zIndex`) for fill + outline together.
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
declare class DensityContourFillLayer extends DensityContourLayerBase<DensityContourFillLayerOptions> {
|
|
234
|
+
protected paintDensity(g: Graphics, density: ContourMultiPolygon[], offsetX: number, offsetY: number): void;
|
|
235
|
+
/**
|
|
236
|
+
* Resolve the palette chain into a per-band colour function. Order
|
|
237
|
+
* (most specific wins): {@link DensityContourFillLayerOptions.fillColor}
|
|
238
|
+
* > `paletteFn(t)` > `paletteRangeStart`/`paletteRangeEnd` (both set) >
|
|
239
|
+
* `palette` > default `'blues'`.
|
|
240
|
+
*/
|
|
241
|
+
private resolveFillColor;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* `DensityContourStrokeLayer` — paints stroked iso-lines from a
|
|
246
|
+
* d3-contour density estimate over a source `GraphLayer`'s node positions.
|
|
247
|
+
* No fill. Defaults reproduce Observable's
|
|
248
|
+
* [`@d3/density-contours`](https://observablehq.com/@d3/density-contours):
|
|
249
|
+
* steelblue strokes with the topographic "index contour" pattern (every 5th
|
|
250
|
+
* band heavy at 1 unit, the rest hair-thin at 0.25).
|
|
251
|
+
*
|
|
252
|
+
* For filled iso-bands use {@link DensityContourFillLayer}; compose both
|
|
253
|
+
* layers (same `graphLayerId`, different `zIndex`) for fill + outline
|
|
254
|
+
* together.
|
|
255
|
+
*/
|
|
256
|
+
|
|
257
|
+
declare class DensityContourStrokeLayer extends DensityContourLayerBase<DensityContourStrokeLayerOptions> {
|
|
258
|
+
protected paintDensity(g: Graphics, density: ContourMultiPolygon[], offsetX: number, offsetY: number): void;
|
|
259
|
+
/**
|
|
260
|
+
* Resolve the per-band stroke-width function. Precedence:
|
|
261
|
+
* 1. `strokeWidth` is a function → use it directly.
|
|
262
|
+
* 2. All three index-contour sugar fields set → build
|
|
263
|
+
* `(i) => i % every === 0 ? major : minor`.
|
|
264
|
+
* 3. `strokeWidth` is a number → constant.
|
|
265
|
+
* 4. Default {@link STROKE_DEFAULTS.strokeWidth}.
|
|
266
|
+
*/
|
|
267
|
+
private resolveWidth;
|
|
268
|
+
/**
|
|
269
|
+
* Resolve the per-band stroke-colour function. When `strokeColor` is
|
|
270
|
+
* `'palette'`, walk the palette chain (`paletteFn` > range > `palette`);
|
|
271
|
+
* otherwise return a constant colour function. Default is steelblue.
|
|
272
|
+
*/
|
|
273
|
+
private resolveStrokeColor;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export { DENSITY_CONTOUR_PALETTES, DENSITY_CONTOUR_PALETTE_NAMES, DensityContourFillLayer, type DensityContourFillLayerOptions, DensityContourLayerBase, type DensityContourLayerBaseOptions, type DensityContourLayerEvents, type DensityContourLayerState, type DensityContourPaletteName, type DensityContourPaletteOptions, DensityContourStrokeLayer, type DensityContourStrokeLayerOptions, lerpColor, sampleStops };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { WorldLayer } from '@invana/canvas';
|
|
2
|
+
import { contourDensity } from 'd3-contour';
|
|
3
|
+
|
|
4
|
+
// src/DensityContourLayerBase.ts
|
|
5
|
+
var DENSITY_CONTOUR_BASE_DEFAULTS = {
|
|
6
|
+
bandwidth: 20,
|
|
7
|
+
thresholds: 10,
|
|
8
|
+
cellSize: 4,
|
|
9
|
+
padding: 50,
|
|
10
|
+
recompute: "auto",
|
|
11
|
+
recomputeDebounceMs: 120
|
|
12
|
+
};
|
|
13
|
+
var DensityContourLayerBase = class extends WorldLayer {
|
|
14
|
+
graphLayerId;
|
|
15
|
+
graph = null;
|
|
16
|
+
gfx = null;
|
|
17
|
+
subs = [];
|
|
18
|
+
// Browser `setTimeout` returns `number`; using `ReturnType<typeof setTimeout>`
|
|
19
|
+
// would resolve to NodeJS.Timeout in dual-typed environments and break the
|
|
20
|
+
// `window.clearTimeout(...)` call site.
|
|
21
|
+
debounceTimer = null;
|
|
22
|
+
constructor(opts) {
|
|
23
|
+
super({
|
|
24
|
+
...opts,
|
|
25
|
+
// Density bands extend past node centres by `bandwidth + padding`, so
|
|
26
|
+
// viewport culling against the bare node AABB would clip them.
|
|
27
|
+
cullable: opts.cullable ?? false,
|
|
28
|
+
// Passive overlay — clicks fall through to the graph below.
|
|
29
|
+
hittable: opts.hittable ?? false
|
|
30
|
+
});
|
|
31
|
+
this.graphLayerId = opts.options.graphLayerId;
|
|
32
|
+
}
|
|
33
|
+
createState() {
|
|
34
|
+
return {};
|
|
35
|
+
}
|
|
36
|
+
onMount(ctx) {
|
|
37
|
+
const graph = ctx.layers.get(this.graphLayerId);
|
|
38
|
+
if (!graph) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`${this.constructor.name} "${this.id}": graph layer "${this.graphLayerId}" not found. Add the GraphLayer before this contour layer.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
this.graph = graph;
|
|
44
|
+
this.gfx = this.createGraphics("density-bands");
|
|
45
|
+
const recompute = this.options.recompute ?? DENSITY_CONTOUR_BASE_DEFAULTS.recompute;
|
|
46
|
+
if (recompute === "auto") {
|
|
47
|
+
this.subs.push(graph.events.on("data:changed", () => this.scheduleRecompute()));
|
|
48
|
+
}
|
|
49
|
+
this.scheduleRecompute();
|
|
50
|
+
}
|
|
51
|
+
onUnmount() {
|
|
52
|
+
for (const off of this.subs) off();
|
|
53
|
+
this.subs.length = 0;
|
|
54
|
+
if (this.debounceTimer !== null) {
|
|
55
|
+
window.clearTimeout(this.debounceTimer);
|
|
56
|
+
this.debounceTimer = null;
|
|
57
|
+
}
|
|
58
|
+
this.gfx = null;
|
|
59
|
+
this.graph = null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Force an immediate recompute. Useful in `recompute: 'manual'` mode, or
|
|
63
|
+
* to refresh the overlay after externally mutating options that don't
|
|
64
|
+
* have setters yet.
|
|
65
|
+
*/
|
|
66
|
+
recompute() {
|
|
67
|
+
this.computeAndPaint();
|
|
68
|
+
}
|
|
69
|
+
hitTest(_worldX, _worldY) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// ─── Internals ─────────────────────────────────────────────────────────────
|
|
73
|
+
scheduleRecompute() {
|
|
74
|
+
if (this.debounceTimer !== null) window.clearTimeout(this.debounceTimer);
|
|
75
|
+
const wait = this.options.recomputeDebounceMs ?? DENSITY_CONTOUR_BASE_DEFAULTS.recomputeDebounceMs;
|
|
76
|
+
this.debounceTimer = window.setTimeout(() => {
|
|
77
|
+
this.debounceTimer = null;
|
|
78
|
+
this.computeAndPaint();
|
|
79
|
+
}, wait);
|
|
80
|
+
}
|
|
81
|
+
computeAndPaint() {
|
|
82
|
+
const g = this.gfx;
|
|
83
|
+
const graph = this.graph;
|
|
84
|
+
if (!g || !graph) return;
|
|
85
|
+
const t0 = performance.now();
|
|
86
|
+
const points = [];
|
|
87
|
+
for (const node of graph.store.nodes()) {
|
|
88
|
+
const p = node.position;
|
|
89
|
+
if (!p) continue;
|
|
90
|
+
points.push({ x: p.x, y: p.y });
|
|
91
|
+
}
|
|
92
|
+
g.clear();
|
|
93
|
+
if (points.length === 0) return;
|
|
94
|
+
const pad = this.options.padding ?? DENSITY_CONTOUR_BASE_DEFAULTS.padding;
|
|
95
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
96
|
+
for (const p of points) {
|
|
97
|
+
if (p.x < minX) minX = p.x;
|
|
98
|
+
if (p.y < minY) minY = p.y;
|
|
99
|
+
if (p.x > maxX) maxX = p.x;
|
|
100
|
+
if (p.y > maxY) maxY = p.y;
|
|
101
|
+
}
|
|
102
|
+
minX -= pad;
|
|
103
|
+
minY -= pad;
|
|
104
|
+
maxX += pad;
|
|
105
|
+
maxY += pad;
|
|
106
|
+
const width = Math.max(1, Math.ceil(maxX - minX));
|
|
107
|
+
const height = Math.max(1, Math.ceil(maxY - minY));
|
|
108
|
+
const density = contourDensity().x((d) => d.x - minX).y((d) => d.y - minY).size([width, height]).bandwidth(this.options.bandwidth ?? DENSITY_CONTOUR_BASE_DEFAULTS.bandwidth).thresholds(this.options.thresholds ?? DENSITY_CONTOUR_BASE_DEFAULTS.thresholds).cellSize(this.options.cellSize ?? DENSITY_CONTOUR_BASE_DEFAULTS.cellSize)(points);
|
|
109
|
+
this.paintDensity(g, density, minX, minY);
|
|
110
|
+
this.events.emit(
|
|
111
|
+
"recompute",
|
|
112
|
+
{
|
|
113
|
+
thresholds: density.length,
|
|
114
|
+
points: points.length,
|
|
115
|
+
durationMs: performance.now() - t0
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/palettes.ts
|
|
122
|
+
var DENSITY_CONTOUR_PALETTES = {
|
|
123
|
+
blues: [
|
|
124
|
+
16251903,
|
|
125
|
+
14609399,
|
|
126
|
+
13032431,
|
|
127
|
+
10406625,
|
|
128
|
+
7057110,
|
|
129
|
+
4362950,
|
|
130
|
+
2191797,
|
|
131
|
+
545180,
|
|
132
|
+
536683
|
|
133
|
+
],
|
|
134
|
+
greens: [
|
|
135
|
+
16252149,
|
|
136
|
+
15070688,
|
|
137
|
+
13101504,
|
|
138
|
+
10607003,
|
|
139
|
+
7652470,
|
|
140
|
+
4303709,
|
|
141
|
+
2329413,
|
|
142
|
+
27948,
|
|
143
|
+
17435
|
|
144
|
+
],
|
|
145
|
+
oranges: [
|
|
146
|
+
16774635,
|
|
147
|
+
16705230,
|
|
148
|
+
16634018,
|
|
149
|
+
16625259,
|
|
150
|
+
16616764,
|
|
151
|
+
15821075,
|
|
152
|
+
14239745,
|
|
153
|
+
10892803,
|
|
154
|
+
8333060
|
|
155
|
+
],
|
|
156
|
+
purples: [
|
|
157
|
+
16579581,
|
|
158
|
+
15724021,
|
|
159
|
+
14342891,
|
|
160
|
+
12369372,
|
|
161
|
+
10394312,
|
|
162
|
+
8420794,
|
|
163
|
+
6967715,
|
|
164
|
+
5515151,
|
|
165
|
+
4128893
|
|
166
|
+
],
|
|
167
|
+
reds: [
|
|
168
|
+
16774640,
|
|
169
|
+
16703698,
|
|
170
|
+
16563105,
|
|
171
|
+
16552562,
|
|
172
|
+
16476746,
|
|
173
|
+
15678252,
|
|
174
|
+
13309981,
|
|
175
|
+
10817301,
|
|
176
|
+
6750221
|
|
177
|
+
],
|
|
178
|
+
viridis: [
|
|
179
|
+
4456788,
|
|
180
|
+
4728952,
|
|
181
|
+
4082057,
|
|
182
|
+
3238030,
|
|
183
|
+
2523790,
|
|
184
|
+
2072201,
|
|
185
|
+
3520377,
|
|
186
|
+
7261784,
|
|
187
|
+
11918891,
|
|
188
|
+
16639781
|
|
189
|
+
],
|
|
190
|
+
plasma: [
|
|
191
|
+
854151,
|
|
192
|
+
4588447,
|
|
193
|
+
7471528,
|
|
194
|
+
10229662,
|
|
195
|
+
12400518,
|
|
196
|
+
14178155,
|
|
197
|
+
15563091,
|
|
198
|
+
16424507,
|
|
199
|
+
16632358,
|
|
200
|
+
15792417
|
|
201
|
+
],
|
|
202
|
+
magma: [
|
|
203
|
+
4,
|
|
204
|
+
1576765,
|
|
205
|
+
4460406,
|
|
206
|
+
7479169,
|
|
207
|
+
10366847,
|
|
208
|
+
13451377,
|
|
209
|
+
15818845,
|
|
210
|
+
16619112,
|
|
211
|
+
16697997,
|
|
212
|
+
16580031
|
|
213
|
+
],
|
|
214
|
+
inferno: [
|
|
215
|
+
4,
|
|
216
|
+
1772609,
|
|
217
|
+
4852843,
|
|
218
|
+
7871597,
|
|
219
|
+
10824800,
|
|
220
|
+
13583430,
|
|
221
|
+
15558949,
|
|
222
|
+
16488966,
|
|
223
|
+
16240957,
|
|
224
|
+
16580516
|
|
225
|
+
],
|
|
226
|
+
warm: [16772512, 16701814, 16691788, 16616764, 16535082, 14883356, 11599910],
|
|
227
|
+
cool: [14740724, 12571622, 10403034, 9213638, 9202609, 8929693, 7209323]
|
|
228
|
+
};
|
|
229
|
+
var DENSITY_CONTOUR_PALETTE_NAMES = Object.keys(
|
|
230
|
+
DENSITY_CONTOUR_PALETTES
|
|
231
|
+
);
|
|
232
|
+
function lerpColor(a, b, t) {
|
|
233
|
+
const u = t <= 0 ? 0 : t >= 1 ? 1 : t;
|
|
234
|
+
const ar = a >> 16 & 255;
|
|
235
|
+
const ag = a >> 8 & 255;
|
|
236
|
+
const ab = a & 255;
|
|
237
|
+
const br = b >> 16 & 255;
|
|
238
|
+
const bg = b >> 8 & 255;
|
|
239
|
+
const bb = b & 255;
|
|
240
|
+
const r = Math.round(ar + (br - ar) * u);
|
|
241
|
+
const g = Math.round(ag + (bg - ag) * u);
|
|
242
|
+
const bl = Math.round(ab + (bb - ab) * u);
|
|
243
|
+
return r << 16 | g << 8 | bl;
|
|
244
|
+
}
|
|
245
|
+
function sampleStops(stops, index, total) {
|
|
246
|
+
if (stops.length === 0) return 0;
|
|
247
|
+
if (total <= 1 || stops.length === 1) return stops[stops.length - 1];
|
|
248
|
+
const t = index / (total - 1);
|
|
249
|
+
const pos = Math.max(0, Math.min(1, t)) * (stops.length - 1);
|
|
250
|
+
const lo = Math.floor(pos);
|
|
251
|
+
const hi = Math.min(stops.length - 1, lo + 1);
|
|
252
|
+
return lerpColor(stops[lo], stops[hi], pos - lo);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/DensityContourFillLayer.ts
|
|
256
|
+
var FILL_DEFAULTS = {
|
|
257
|
+
fillOpacity: 0.4,
|
|
258
|
+
palette: "blues"
|
|
259
|
+
};
|
|
260
|
+
function resolveStops(palette) {
|
|
261
|
+
if (Array.isArray(palette)) return palette;
|
|
262
|
+
const name = palette ?? FILL_DEFAULTS.palette;
|
|
263
|
+
return DENSITY_CONTOUR_PALETTES[name] ?? DENSITY_CONTOUR_PALETTES[FILL_DEFAULTS.palette];
|
|
264
|
+
}
|
|
265
|
+
var DensityContourFillLayer = class extends DensityContourLayerBase {
|
|
266
|
+
paintDensity(g, density, offsetX, offsetY) {
|
|
267
|
+
const opacity = this.options.fillOpacity ?? FILL_DEFAULTS.fillOpacity;
|
|
268
|
+
const total = density.length;
|
|
269
|
+
const colorAt = this.resolveFillColor();
|
|
270
|
+
density.forEach((band, i) => {
|
|
271
|
+
const fillColor = colorAt(band.value, i, total);
|
|
272
|
+
for (const polygon of band.coordinates) {
|
|
273
|
+
const outer = polygon[0];
|
|
274
|
+
if (!outer || outer.length < 3) continue;
|
|
275
|
+
const flat = new Array(outer.length * 2);
|
|
276
|
+
for (let k = 0; k < outer.length; k++) {
|
|
277
|
+
const pt = outer[k];
|
|
278
|
+
flat[k * 2] = (pt[0] ?? 0) + offsetX;
|
|
279
|
+
flat[k * 2 + 1] = (pt[1] ?? 0) + offsetY;
|
|
280
|
+
}
|
|
281
|
+
g.poly(flat);
|
|
282
|
+
}
|
|
283
|
+
g.fill({ color: fillColor, alpha: opacity });
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Resolve the palette chain into a per-band colour function. Order
|
|
288
|
+
* (most specific wins): {@link DensityContourFillLayerOptions.fillColor}
|
|
289
|
+
* > `paletteFn(t)` > `paletteRangeStart`/`paletteRangeEnd` (both set) >
|
|
290
|
+
* `palette` > default `'blues'`.
|
|
291
|
+
*/
|
|
292
|
+
resolveFillColor() {
|
|
293
|
+
const o = this.options;
|
|
294
|
+
if (o.fillColor) return o.fillColor;
|
|
295
|
+
if (o.paletteFn) {
|
|
296
|
+
const fn = o.paletteFn;
|
|
297
|
+
return (_v, i, n) => fn(n > 1 ? i / (n - 1) : 0);
|
|
298
|
+
}
|
|
299
|
+
if (o.paletteRangeStart !== void 0 && o.paletteRangeEnd !== void 0) {
|
|
300
|
+
const a = o.paletteRangeStart;
|
|
301
|
+
const b = o.paletteRangeEnd;
|
|
302
|
+
return (_v, i, n) => lerpColor(a, b, n > 1 ? i / (n - 1) : 0);
|
|
303
|
+
}
|
|
304
|
+
const stops = resolveStops(o.palette);
|
|
305
|
+
return (_v, i, n) => sampleStops(stops, i, n);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// src/DensityContourStrokeLayer.ts
|
|
310
|
+
var STROKE_DEFAULTS = {
|
|
311
|
+
strokeColor: 4620980,
|
|
312
|
+
// steelblue — Observable's default
|
|
313
|
+
strokeWidth: 0.5,
|
|
314
|
+
palette: "blues"
|
|
315
|
+
};
|
|
316
|
+
function resolveStops2(palette) {
|
|
317
|
+
if (Array.isArray(palette)) return palette;
|
|
318
|
+
const name = palette ?? STROKE_DEFAULTS.palette;
|
|
319
|
+
return DENSITY_CONTOUR_PALETTES[name] ?? DENSITY_CONTOUR_PALETTES[STROKE_DEFAULTS.palette];
|
|
320
|
+
}
|
|
321
|
+
var DensityContourStrokeLayer = class extends DensityContourLayerBase {
|
|
322
|
+
paintDensity(g, density, offsetX, offsetY) {
|
|
323
|
+
const total = density.length;
|
|
324
|
+
const widthAt = this.resolveWidth();
|
|
325
|
+
const strokeAt = this.resolveStrokeColor();
|
|
326
|
+
density.forEach((band, i) => {
|
|
327
|
+
const w = widthAt(i, total, band.value);
|
|
328
|
+
if (w <= 0) return;
|
|
329
|
+
for (const polygon of band.coordinates) {
|
|
330
|
+
const outer = polygon[0];
|
|
331
|
+
if (!outer || outer.length < 3) continue;
|
|
332
|
+
const flat = new Array(outer.length * 2);
|
|
333
|
+
for (let k = 0; k < outer.length; k++) {
|
|
334
|
+
const pt = outer[k];
|
|
335
|
+
flat[k * 2] = (pt[0] ?? 0) + offsetX;
|
|
336
|
+
flat[k * 2 + 1] = (pt[1] ?? 0) + offsetY;
|
|
337
|
+
}
|
|
338
|
+
g.poly(flat);
|
|
339
|
+
}
|
|
340
|
+
g.stroke({ color: strokeAt(band.value, i, total), width: w });
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Resolve the per-band stroke-width function. Precedence:
|
|
345
|
+
* 1. `strokeWidth` is a function → use it directly.
|
|
346
|
+
* 2. All three index-contour sugar fields set → build
|
|
347
|
+
* `(i) => i % every === 0 ? major : minor`.
|
|
348
|
+
* 3. `strokeWidth` is a number → constant.
|
|
349
|
+
* 4. Default {@link STROKE_DEFAULTS.strokeWidth}.
|
|
350
|
+
*/
|
|
351
|
+
resolveWidth() {
|
|
352
|
+
const o = this.options;
|
|
353
|
+
if (typeof o.strokeWidth === "function") return o.strokeWidth;
|
|
354
|
+
if (o.indexEvery !== void 0 && o.indexMajorWidth !== void 0 && o.indexMinorWidth !== void 0) {
|
|
355
|
+
const every = o.indexEvery;
|
|
356
|
+
const major = o.indexMajorWidth;
|
|
357
|
+
const minor = o.indexMinorWidth;
|
|
358
|
+
return (i) => i % every === 0 ? major : minor;
|
|
359
|
+
}
|
|
360
|
+
const w = typeof o.strokeWidth === "number" ? o.strokeWidth : STROKE_DEFAULTS.strokeWidth;
|
|
361
|
+
return () => w;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Resolve the per-band stroke-colour function. When `strokeColor` is
|
|
365
|
+
* `'palette'`, walk the palette chain (`paletteFn` > range > `palette`);
|
|
366
|
+
* otherwise return a constant colour function. Default is steelblue.
|
|
367
|
+
*/
|
|
368
|
+
resolveStrokeColor() {
|
|
369
|
+
const o = this.options;
|
|
370
|
+
const sc = o.strokeColor;
|
|
371
|
+
if (sc === "palette") {
|
|
372
|
+
if (o.paletteFn) {
|
|
373
|
+
const fn = o.paletteFn;
|
|
374
|
+
return (_v, i, n) => fn(n > 1 ? i / (n - 1) : 0);
|
|
375
|
+
}
|
|
376
|
+
if (o.paletteRangeStart !== void 0 && o.paletteRangeEnd !== void 0) {
|
|
377
|
+
const a = o.paletteRangeStart;
|
|
378
|
+
const b = o.paletteRangeEnd;
|
|
379
|
+
return (_v, i, n) => lerpColor(a, b, n > 1 ? i / (n - 1) : 0);
|
|
380
|
+
}
|
|
381
|
+
const stops = resolveStops2(o.palette);
|
|
382
|
+
return (_v, i, n) => sampleStops(stops, i, n);
|
|
383
|
+
}
|
|
384
|
+
const c = typeof sc === "number" ? sc : STROKE_DEFAULTS.strokeColor;
|
|
385
|
+
return () => c;
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export { DENSITY_CONTOUR_PALETTES, DENSITY_CONTOUR_PALETTE_NAMES, DensityContourFillLayer, DensityContourLayerBase, DensityContourStrokeLayer, lerpColor, sampleStops };
|
|
390
|
+
//# sourceMappingURL=index.js.map
|
|
391
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/DensityContourLayerBase.ts","../src/palettes.ts","../src/DensityContourFillLayer.ts","../src/DensityContourStrokeLayer.ts"],"names":["resolveStops"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * `DensityContourLayerBase` — abstract `WorldLayer` that owns the\n * d3-contour density compute and recompute lifecycle. Concrete subclasses\n * decide *how* the resulting iso-bands are painted: filled\n * ({@link DensityContourFillLayer}) or stroked\n * ({@link DensityContourStrokeLayer}).\n *\n * The compute lives in world space so iso-bands track the source graph as\n * the camera pans and zooms. Recompute is debounced (default 120 ms) and\n * triggered by the source `GraphLayer`'s `data:changed`. `contourDensity`\n * is O(n · grid²), so per-frame recompute during drag would tank perf —\n * set `recompute: 'manual'` and call `layer.recompute()` from a drag\n * behaviour if you need that.\n *\n * Subclasses implement {@link paintDensity} to render the\n * `ContourMultiPolygon[]` into the layer's `Graphics`. All shared state\n * (subscription, debounce timer, bounds math) is owned here.\n */\n\nimport { WorldLayer } from '@invana/canvas';\nimport type { CanvasContext, LayerOptions, WorldLayerHit } from '@invana/canvas';\nimport { GraphLayer } from '@invana/graph';\nimport { contourDensity, type ContourMultiPolygon } from 'd3-contour';\nimport type { Graphics } from 'pixi.js';\n\nimport type {\n DensityContourLayerBaseOptions,\n DensityContourLayerEvents,\n DensityContourLayerState,\n} from './types';\n\nexport const DENSITY_CONTOUR_BASE_DEFAULTS = {\n bandwidth: 20,\n thresholds: 10 as number | number[],\n cellSize: 4,\n padding: 50,\n recompute: 'auto' as 'auto' | 'manual',\n recomputeDebounceMs: 120,\n} as const;\n\nexport abstract class DensityContourLayerBase<\n TOpt extends DensityContourLayerBaseOptions,\n TEvt extends DensityContourLayerEvents = DensityContourLayerEvents,\n> extends WorldLayer<TOpt, DensityContourLayerState, TEvt, never, WorldLayerHit> {\n private readonly graphLayerId: string;\n\n private graph: GraphLayer | null = null;\n protected gfx: Graphics | null = null;\n private readonly subs: Array<() => void> = [];\n\n // Browser `setTimeout` returns `number`; using `ReturnType<typeof setTimeout>`\n // would resolve to NodeJS.Timeout in dual-typed environments and break the\n // `window.clearTimeout(...)` call site.\n private debounceTimer: number | null = null;\n\n constructor(opts: LayerOptions<TOpt>) {\n super({\n ...opts,\n // Density bands extend past node centres by `bandwidth + padding`, so\n // viewport culling against the bare node AABB would clip them.\n cullable: opts.cullable ?? false,\n // Passive overlay — clicks fall through to the graph below.\n hittable: opts.hittable ?? false,\n });\n this.graphLayerId = opts.options.graphLayerId;\n }\n\n protected createState(): DensityContourLayerState {\n return {};\n }\n\n protected override onMount(ctx: CanvasContext): void {\n const graph = ctx.layers.get<GraphLayer>(this.graphLayerId);\n if (!graph) {\n throw new Error(\n `${this.constructor.name} \"${this.id}\": graph layer \"${this.graphLayerId}\" not found. ` +\n `Add the GraphLayer before this contour layer.`,\n );\n }\n this.graph = graph;\n this.gfx = this.createGraphics('density-bands');\n\n const recompute = this.options.recompute ?? DENSITY_CONTOUR_BASE_DEFAULTS.recompute;\n if (recompute === 'auto') {\n this.subs.push(graph.events.on('data:changed', () => this.scheduleRecompute()));\n }\n\n // Initial paint — the graph may already hold data when we mount.\n this.scheduleRecompute();\n }\n\n protected override onUnmount(): void {\n for (const off of this.subs) off();\n this.subs.length = 0;\n if (this.debounceTimer !== null) {\n window.clearTimeout(this.debounceTimer);\n this.debounceTimer = null;\n }\n this.gfx = null;\n this.graph = null;\n }\n\n /**\n * Force an immediate recompute. Useful in `recompute: 'manual'` mode, or\n * to refresh the overlay after externally mutating options that don't\n * have setters yet.\n */\n recompute(): void {\n this.computeAndPaint();\n }\n\n hitTest(_worldX: number, _worldY: number): WorldLayerHit | null {\n return null;\n }\n\n // ─── Internals ─────────────────────────────────────────────────────────────\n\n private scheduleRecompute(): void {\n if (this.debounceTimer !== null) window.clearTimeout(this.debounceTimer);\n const wait =\n this.options.recomputeDebounceMs ?? DENSITY_CONTOUR_BASE_DEFAULTS.recomputeDebounceMs;\n this.debounceTimer = window.setTimeout(() => {\n this.debounceTimer = null;\n this.computeAndPaint();\n }, wait);\n }\n\n private computeAndPaint(): void {\n const g = this.gfx;\n const graph = this.graph;\n if (!g || !graph) return;\n\n const t0 = performance.now();\n\n // Collect node positions in world coords. d3-contour wants its grid in\n // a local positive-quadrant space, so we offset by (minX, minY) before\n // feeding points in and add the offset back when painting.\n const points: Array<{ x: number; y: number }> = [];\n for (const node of graph.store.nodes()) {\n const p = node.position;\n if (!p) continue;\n points.push({ x: p.x, y: p.y });\n }\n\n g.clear();\n\n if (points.length === 0) return;\n\n const pad = this.options.padding ?? DENSITY_CONTOUR_BASE_DEFAULTS.padding;\n let minX = Infinity,\n minY = Infinity,\n maxX = -Infinity,\n maxY = -Infinity;\n for (const p of points) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxX) maxX = p.x;\n if (p.y > maxY) maxY = p.y;\n }\n minX -= pad;\n minY -= pad;\n maxX += pad;\n maxY += pad;\n const width = Math.max(1, Math.ceil(maxX - minX));\n const height = Math.max(1, Math.ceil(maxY - minY));\n\n const density: ContourMultiPolygon[] = contourDensity<{ x: number; y: number }>()\n .x((d) => d.x - minX)\n .y((d) => d.y - minY)\n .size([width, height])\n .bandwidth(this.options.bandwidth ?? DENSITY_CONTOUR_BASE_DEFAULTS.bandwidth)\n .thresholds(this.options.thresholds ?? DENSITY_CONTOUR_BASE_DEFAULTS.thresholds)\n .cellSize(this.options.cellSize ?? DENSITY_CONTOUR_BASE_DEFAULTS.cellSize)(points);\n\n this.paintDensity(g, density, minX, minY);\n\n // `recompute` is part of the base event contract — subclass event maps\n // must extend `DensityContourLayerEvents`, so this emit is always\n // type-correct. The cast bridges the generic TEvt to the literal\n // event name.\n (this.events.emit as (k: 'recompute', p: DensityContourLayerEvents['recompute']) => void)(\n 'recompute',\n {\n thresholds: density.length,\n points: points.length,\n durationMs: performance.now() - t0,\n },\n );\n }\n\n /**\n * Render the iso-bands into `g`. The bands are ordered low-density →\n * high-density; subclasses typically paint in that order so denser bands\n * sit on top. `offsetX`/`offsetY` are the world-space origin of the\n * compute grid — add them to each polygon point.\n */\n protected abstract paintDensity(\n g: Graphics,\n density: ContourMultiPolygon[],\n offsetX: number,\n offsetY: number,\n ): void;\n}\n","/**\n * Built-in colour ramps for {@link DensityContourLayer}.\n *\n * Each palette is an ordered array of `0xRRGGBB` stops from low-density to\n * high-density. The layer interpolates between adjacent stops so any band\n * count (3, 10, 30...) lands on a perceptually-smooth colour.\n *\n * Sequential single-hue palettes (`blues`, `greens`, ...) are drawn from\n * ColorBrewer; perceptual ramps (`viridis`, `plasma`, `magma`, `inferno`)\n * are 10-stop quantizations of matplotlib's perceptual colour maps. `warm`\n * and `cool` are ColorBrewer YlOrRd / BuPu equivalents.\n */\n\nexport type DensityContourPaletteName =\n | 'blues'\n | 'greens'\n | 'oranges'\n | 'purples'\n | 'reds'\n | 'viridis'\n | 'plasma'\n | 'magma'\n | 'inferno'\n | 'warm'\n | 'cool';\n\nexport const DENSITY_CONTOUR_PALETTES: Record<DensityContourPaletteName, number[]> = {\n blues: [\n 0xf7fbff, 0xdeebf7, 0xc6dbef, 0x9ecae1, 0x6baed6, 0x4292c6, 0x2171b5, 0x08519c, 0x08306b,\n ],\n greens: [\n 0xf7fcf5, 0xe5f5e0, 0xc7e9c0, 0xa1d99b, 0x74c476, 0x41ab5d, 0x238b45, 0x006d2c, 0x00441b,\n ],\n oranges: [\n 0xfff5eb, 0xfee6ce, 0xfdd0a2, 0xfdae6b, 0xfd8d3c, 0xf16913, 0xd94801, 0xa63603, 0x7f2704,\n ],\n purples: [\n 0xfcfbfd, 0xefedf5, 0xdadaeb, 0xbcbddc, 0x9e9ac8, 0x807dba, 0x6a51a3, 0x54278f, 0x3f007d,\n ],\n reds: [\n 0xfff5f0, 0xfee0d2, 0xfcbba1, 0xfc9272, 0xfb6a4a, 0xef3b2c, 0xcb181d, 0xa50f15, 0x67000d,\n ],\n viridis: [\n 0x440154, 0x482878, 0x3e4989, 0x31688e, 0x26828e, 0x1f9e89, 0x35b779, 0x6ece58, 0xb5de2b,\n 0xfde725,\n ],\n plasma: [\n 0x0d0887, 0x46039f, 0x7201a8, 0x9c179e, 0xbd3786, 0xd8576b, 0xed7953, 0xfa9e3b, 0xfdca26,\n 0xf0f921,\n ],\n magma: [\n 0x000004, 0x180f3d, 0x440f76, 0x721f81, 0x9e2f7f, 0xcd4071, 0xf1605d, 0xfd9668, 0xfeca8d,\n 0xfcfdbf,\n ],\n inferno: [\n 0x000004, 0x1b0c41, 0x4a0c6b, 0x781c6d, 0xa52c60, 0xcf4446, 0xed6925, 0xfb9a06, 0xf7d13d,\n 0xfcffa4,\n ],\n warm: [0xffeda0, 0xfed976, 0xfeb24c, 0xfd8d3c, 0xfc4e2a, 0xe31a1c, 0xb10026],\n cool: [0xe0ecf4, 0xbfd3e6, 0x9ebcda, 0x8c96c6, 0x8c6bb1, 0x88419d, 0x6e016b],\n};\n\n/** All built-in palette names in declaration order. Useful for GUI menus. */\nexport const DENSITY_CONTOUR_PALETTE_NAMES: readonly DensityContourPaletteName[] = Object.keys(\n DENSITY_CONTOUR_PALETTES,\n) as DensityContourPaletteName[];\n\n/**\n * Linear interpolation between two `0xRRGGBB` colours in sRGB space.\n * `t` is clamped to `[0, 1]`. sRGB-linear is \"good enough\" for adjacent\n * stops in a smooth ramp; for perceptually-uniform mixing across distant\n * hues, supply a function via `paletteFn`.\n */\nexport function lerpColor(a: number, b: number, t: number): number {\n const u = t <= 0 ? 0 : t >= 1 ? 1 : t;\n const ar = (a >> 16) & 0xff;\n const ag = (a >> 8) & 0xff;\n const ab = a & 0xff;\n const br = (b >> 16) & 0xff;\n const bg = (b >> 8) & 0xff;\n const bb = b & 0xff;\n const r = Math.round(ar + (br - ar) * u);\n const g = Math.round(ag + (bg - ag) * u);\n const bl = Math.round(ab + (bb - ab) * u);\n return (r << 16) | (g << 8) | bl;\n}\n\n/**\n * Linearly interpolate a `0xRRGGBB` colour from a stop array based on the\n * band's position `(index / (total - 1))`. Returns the last stop if there's\n * only one band or one stop.\n */\nexport function sampleStops(stops: number[], index: number, total: number): number {\n if (stops.length === 0) return 0x000000;\n if (total <= 1 || stops.length === 1) return stops[stops.length - 1]!;\n const t = index / (total - 1);\n const pos = Math.max(0, Math.min(1, t)) * (stops.length - 1);\n const lo = Math.floor(pos);\n const hi = Math.min(stops.length - 1, lo + 1);\n return lerpColor(stops[lo]!, stops[hi]!, pos - lo);\n}\n","/**\n * `DensityContourFillLayer` — paints filled iso-bands from a\n * d3-contour density estimate over a source `GraphLayer`'s node positions.\n * No outline. For the stroked / Observable-style look, use\n * {@link DensityContourStrokeLayer}; compose both layers (same\n * `graphLayerId`, different `zIndex`) for fill + outline together.\n */\n\nimport type { ContourMultiPolygon } from 'd3-contour';\nimport type { Graphics } from 'pixi.js';\n\nimport { DensityContourLayerBase } from './DensityContourLayerBase';\nimport { DENSITY_CONTOUR_PALETTES, lerpColor, sampleStops } from './palettes';\nimport type { DensityContourFillLayerOptions } from './types';\n\nconst FILL_DEFAULTS = {\n fillOpacity: 0.4,\n palette: 'blues' as const,\n};\n\nfunction resolveStops(\n palette: DensityContourFillLayerOptions['palette'] | undefined,\n): number[] {\n if (Array.isArray(palette)) return palette;\n const name = palette ?? FILL_DEFAULTS.palette;\n return DENSITY_CONTOUR_PALETTES[name] ?? DENSITY_CONTOUR_PALETTES[FILL_DEFAULTS.palette];\n}\n\nexport class DensityContourFillLayer extends DensityContourLayerBase<DensityContourFillLayerOptions> {\n protected paintDensity(\n g: Graphics,\n density: ContourMultiPolygon[],\n offsetX: number,\n offsetY: number,\n ): void {\n const opacity = this.options.fillOpacity ?? FILL_DEFAULTS.fillOpacity;\n const total = density.length;\n const colorAt = this.resolveFillColor();\n\n // d3 returns bands low-density → high-density; paint in that order so\n // denser bands sit on top.\n density.forEach((band, i) => {\n const fillColor = colorAt(band.value, i, total);\n // Each band is a MultiPolygon: an array of polygons; each polygon is\n // an array of rings (outer + holes). For density bands the outer ring\n // is what we want filled; holes are rare and visually subtle, so we\n // paint the outer ring only (PixiJS `Graphics` doesn't expose ring\n // subtraction in v8's path builder).\n for (const polygon of band.coordinates) {\n const outer = polygon[0];\n if (!outer || outer.length < 3) continue;\n const flat: number[] = new Array(outer.length * 2);\n for (let k = 0; k < outer.length; k++) {\n const pt = outer[k]!;\n flat[k * 2] = (pt[0] ?? 0) + offsetX;\n flat[k * 2 + 1] = (pt[1] ?? 0) + offsetY;\n }\n g.poly(flat);\n }\n g.fill({ color: fillColor, alpha: opacity });\n });\n }\n\n /**\n * Resolve the palette chain into a per-band colour function. Order\n * (most specific wins): {@link DensityContourFillLayerOptions.fillColor}\n * > `paletteFn(t)` > `paletteRangeStart`/`paletteRangeEnd` (both set) >\n * `palette` > default `'blues'`.\n */\n private resolveFillColor(): (value: number, index: number, total: number) => number {\n const o = this.options;\n if (o.fillColor) return o.fillColor;\n if (o.paletteFn) {\n const fn = o.paletteFn;\n return (_v, i, n) => fn(n > 1 ? i / (n - 1) : 0);\n }\n if (o.paletteRangeStart !== undefined && o.paletteRangeEnd !== undefined) {\n const a = o.paletteRangeStart;\n const b = o.paletteRangeEnd;\n return (_v, i, n) => lerpColor(a, b, n > 1 ? i / (n - 1) : 0);\n }\n const stops = resolveStops(o.palette);\n return (_v, i, n) => sampleStops(stops, i, n);\n }\n}\n","/**\n * `DensityContourStrokeLayer` — paints stroked iso-lines from a\n * d3-contour density estimate over a source `GraphLayer`'s node positions.\n * No fill. Defaults reproduce Observable's\n * [`@d3/density-contours`](https://observablehq.com/@d3/density-contours):\n * steelblue strokes with the topographic \"index contour\" pattern (every 5th\n * band heavy at 1 unit, the rest hair-thin at 0.25).\n *\n * For filled iso-bands use {@link DensityContourFillLayer}; compose both\n * layers (same `graphLayerId`, different `zIndex`) for fill + outline\n * together.\n */\n\nimport type { ContourMultiPolygon } from 'd3-contour';\nimport type { Graphics } from 'pixi.js';\n\nimport { DensityContourLayerBase } from './DensityContourLayerBase';\nimport { DENSITY_CONTOUR_PALETTES, lerpColor, sampleStops } from './palettes';\nimport type { DensityContourStrokeLayerOptions } from './types';\n\nconst STROKE_DEFAULTS = {\n strokeColor: 0x4682b4, // steelblue — Observable's default\n strokeWidth: 0.5,\n palette: 'blues' as const,\n};\n\nfunction resolveStops(\n palette: DensityContourStrokeLayerOptions['palette'] | undefined,\n): number[] {\n if (Array.isArray(palette)) return palette;\n const name = palette ?? STROKE_DEFAULTS.palette;\n return DENSITY_CONTOUR_PALETTES[name] ?? DENSITY_CONTOUR_PALETTES[STROKE_DEFAULTS.palette];\n}\n\nexport class DensityContourStrokeLayer extends DensityContourLayerBase<DensityContourStrokeLayerOptions> {\n protected paintDensity(\n g: Graphics,\n density: ContourMultiPolygon[],\n offsetX: number,\n offsetY: number,\n ): void {\n const total = density.length;\n const widthAt = this.resolveWidth();\n const strokeAt = this.resolveStrokeColor();\n\n density.forEach((band, i) => {\n const w = widthAt(i, total, band.value);\n if (w <= 0) return; // skip; nothing to paint for this band\n\n for (const polygon of band.coordinates) {\n const outer = polygon[0];\n if (!outer || outer.length < 3) continue;\n const flat: number[] = new Array(outer.length * 2);\n for (let k = 0; k < outer.length; k++) {\n const pt = outer[k]!;\n flat[k * 2] = (pt[0] ?? 0) + offsetX;\n flat[k * 2 + 1] = (pt[1] ?? 0) + offsetY;\n }\n g.poly(flat);\n }\n g.stroke({ color: strokeAt(band.value, i, total), width: w });\n });\n }\n\n /**\n * Resolve the per-band stroke-width function. Precedence:\n * 1. `strokeWidth` is a function → use it directly.\n * 2. All three index-contour sugar fields set → build\n * `(i) => i % every === 0 ? major : minor`.\n * 3. `strokeWidth` is a number → constant.\n * 4. Default {@link STROKE_DEFAULTS.strokeWidth}.\n */\n private resolveWidth(): (index: number, total: number, value: number) => number {\n const o = this.options;\n if (typeof o.strokeWidth === 'function') return o.strokeWidth;\n\n if (\n o.indexEvery !== undefined &&\n o.indexMajorWidth !== undefined &&\n o.indexMinorWidth !== undefined\n ) {\n const every = o.indexEvery;\n const major = o.indexMajorWidth;\n const minor = o.indexMinorWidth;\n return (i) => (i % every === 0 ? major : minor);\n }\n\n const w = typeof o.strokeWidth === 'number' ? o.strokeWidth : STROKE_DEFAULTS.strokeWidth;\n return () => w;\n }\n\n /**\n * Resolve the per-band stroke-colour function. When `strokeColor` is\n * `'palette'`, walk the palette chain (`paletteFn` > range > `palette`);\n * otherwise return a constant colour function. Default is steelblue.\n */\n private resolveStrokeColor(): (value: number, index: number, total: number) => number {\n const o = this.options;\n const sc = o.strokeColor;\n\n if (sc === 'palette') {\n if (o.paletteFn) {\n const fn = o.paletteFn;\n return (_v, i, n) => fn(n > 1 ? i / (n - 1) : 0);\n }\n if (o.paletteRangeStart !== undefined && o.paletteRangeEnd !== undefined) {\n const a = o.paletteRangeStart;\n const b = o.paletteRangeEnd;\n return (_v, i, n) => lerpColor(a, b, n > 1 ? i / (n - 1) : 0);\n }\n const stops = resolveStops(o.palette);\n return (_v, i, n) => sampleStops(stops, i, n);\n }\n\n const c = typeof sc === 'number' ? sc : STROKE_DEFAULTS.strokeColor;\n return () => c;\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@invana/graph-layer-d3-contour",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Density-contour overlay Layer for @invana/graph, backed by d3-contour.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"typedocOptions": {
|
|
9
|
+
"entryPoints": [
|
|
10
|
+
"src/index.ts"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"d3-contour": "^4.0.2"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"pixi.js": "^8.18.1",
|
|
18
|
+
"@invana/canvas": "0.0.1",
|
|
19
|
+
"@invana/graph": "0.0.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/d3-contour": "^3.0.6",
|
|
23
|
+
"pixi.js": "^8.18.1",
|
|
24
|
+
"tsup": "^8.3.5",
|
|
25
|
+
"typescript": "5.9.2",
|
|
26
|
+
"@invana/canvas": "0.0.1",
|
|
27
|
+
"@invana/graph": "0.0.1",
|
|
28
|
+
"@repo/typescript-config": "0.0.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"canvas",
|
|
32
|
+
"graph",
|
|
33
|
+
"layer",
|
|
34
|
+
"d3",
|
|
35
|
+
"d3-contour",
|
|
36
|
+
"density",
|
|
37
|
+
"contour"
|
|
38
|
+
],
|
|
39
|
+
"license": "Apache-2.0",
|
|
40
|
+
"module": "./dist/index.js",
|
|
41
|
+
"exports": {
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"import": "./dist/index.js",
|
|
45
|
+
"default": "./dist/index.js"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"dist"
|
|
50
|
+
],
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"build": "tsup",
|
|
56
|
+
"dev": "tsup --watch",
|
|
57
|
+
"lint": "eslint src/",
|
|
58
|
+
"check-types": "tsc --noEmit",
|
|
59
|
+
"clean": "rm -rf dist"
|
|
60
|
+
}
|
|
61
|
+
}
|