@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.
@@ -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
+ }