svelteplot 0.12.0 → 0.14.0-pr-545.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/core/Plot.svelte +3 -6
  2. package/dist/helpers/group.d.ts +1 -1
  3. package/dist/helpers/group.js +3 -3
  4. package/dist/helpers/scales.js +8 -0
  5. package/dist/helpers/vectorShapes.d.ts +13 -0
  6. package/dist/helpers/vectorShapes.js +57 -0
  7. package/dist/marks/Arrow.svelte +70 -59
  8. package/dist/marks/Arrow.svelte.d.ts +2 -0
  9. package/dist/marks/ColorLegend.svelte +7 -3
  10. package/dist/marks/Contour.svelte +684 -0
  11. package/dist/marks/Contour.svelte.d.ts +152 -0
  12. package/dist/marks/DelaunayLink.svelte +127 -0
  13. package/dist/marks/DelaunayLink.svelte.d.ts +175 -0
  14. package/dist/marks/DelaunayMesh.svelte +102 -0
  15. package/dist/marks/DelaunayMesh.svelte.d.ts +172 -0
  16. package/dist/marks/Density.svelte +552 -0
  17. package/dist/marks/Density.svelte.d.ts +108 -0
  18. package/dist/marks/Hull.svelte +103 -0
  19. package/dist/marks/Hull.svelte.d.ts +175 -0
  20. package/dist/marks/Image.svelte +37 -27
  21. package/dist/marks/Image.svelte.d.ts +2 -0
  22. package/dist/marks/Link.svelte +68 -50
  23. package/dist/marks/Link.svelte.d.ts +2 -0
  24. package/dist/marks/Raster.svelte +6 -1
  25. package/dist/marks/Vector.svelte +12 -81
  26. package/dist/marks/Vector.svelte.d.ts +2 -4
  27. package/dist/marks/Voronoi.svelte +118 -0
  28. package/dist/marks/Voronoi.svelte.d.ts +172 -0
  29. package/dist/marks/VoronoiMesh.svelte +109 -0
  30. package/dist/marks/VoronoiMesh.svelte.d.ts +172 -0
  31. package/dist/marks/helpers/ArrowCanvas.svelte +132 -0
  32. package/dist/marks/helpers/ArrowCanvas.svelte.d.ts +39 -0
  33. package/dist/marks/helpers/BaseAxisX.svelte +5 -7
  34. package/dist/marks/helpers/DensityCanvas.svelte +118 -0
  35. package/dist/marks/helpers/DensityCanvas.svelte.d.ts +18 -0
  36. package/dist/marks/helpers/GeoPathCanvas.svelte +130 -0
  37. package/dist/marks/helpers/GeoPathCanvas.svelte.d.ts +24 -0
  38. package/dist/marks/helpers/GeoPathGroup.svelte +104 -0
  39. package/dist/marks/helpers/GeoPathGroup.svelte.d.ts +37 -0
  40. package/dist/marks/helpers/ImageCanvas.svelte +126 -0
  41. package/dist/marks/helpers/ImageCanvas.svelte.d.ts +34 -0
  42. package/dist/marks/helpers/LinkCanvas.svelte +103 -0
  43. package/dist/marks/helpers/LinkCanvas.svelte.d.ts +32 -0
  44. package/dist/marks/helpers/PathGroup.svelte +100 -0
  45. package/dist/marks/helpers/PathGroup.svelte.d.ts +16 -0
  46. package/dist/marks/helpers/PathItems.svelte +112 -0
  47. package/dist/marks/helpers/PathItems.svelte.d.ts +16 -0
  48. package/dist/marks/helpers/VectorCanvas.svelte +127 -0
  49. package/dist/marks/helpers/VectorCanvas.svelte.d.ts +36 -0
  50. package/dist/marks/index.d.ts +7 -0
  51. package/dist/marks/index.js +7 -0
  52. package/dist/types/mark.d.ts +1 -1
  53. package/dist/types/plot.d.ts +33 -1
  54. package/package.json +185 -181
@@ -0,0 +1,552 @@
1
+ <!-- @component
2
+ Renders two-dimensional kernel density estimation as filled or stroked
3
+ contour paths.
4
+
5
+ Data points with `x` and `y` channels are projected into pixel space and
6
+ passed to d3's `contourDensity` estimator, which uses a Gaussian kernel to
7
+ produce a density grid. Iso-density contour bands are then drawn using the
8
+ marching-squares algorithm.
9
+
10
+ Styling: `fill` and `stroke` accept ordinary CSS color strings **or** the
11
+ special keyword `"density"`, which maps each contour level's estimated
12
+ density through the plot's color scale. Defaults: `fill="none"`,
13
+ `stroke="currentColor"`.
14
+
15
+ Grouping: when a `z` channel is specified (or when `fill`/`stroke` is a
16
+ field name or function rather than a CSS color), the mark computes a
17
+ separate density estimate per group and renders each group independently.
18
+ Using `fill` or `stroke` as a data accessor also maps the group value
19
+ through the plot's color scale.
20
+
21
+ Supports faceting via `fx`/`fy`.
22
+ -->
23
+ <script lang="ts" generics="Datum extends DataRecord">
24
+ interface DensityMarkProps {
25
+ /** Input data — an array of records with x/y positions. */
26
+ data?: Datum[] | null;
27
+ /** x position channel (data space). */
28
+ x?: ChannelAccessor<Datum>;
29
+ /** y position channel (data space). */
30
+ y?: ChannelAccessor<Datum>;
31
+ /** Optional weight channel; defaults to 1 for each point. */
32
+ weight?: ChannelAccessor<Datum>;
33
+ /**
34
+ * Grouping channel. When specified the mark computes a separate
35
+ * density estimate per unique z value and renders the groups
36
+ * independently. Unlike `fill`/`stroke` as accessors, `z` does not
37
+ * automatically add a color channel.
38
+ */
39
+ z?: ChannelAccessor<Datum>;
40
+ /**
41
+ * Gaussian kernel bandwidth in screen pixels (default 20).
42
+ * Larger values produce smoother, more blurred density estimates.
43
+ */
44
+ bandwidth?: number;
45
+ /**
46
+ * Density threshold levels. Can be:
47
+ * - a **count** (number): that many evenly-spaced levels from 0 to the
48
+ * maximum density (default 20)
49
+ * - an explicit **array** of threshold values in k-scaled density units
50
+ * (where k = 100; values from 0 to roughly 100× the peak density)
51
+ */
52
+ thresholds?: number | number[];
53
+ /**
54
+ * Fill color for density bands. Use `"density"` to map each band's
55
+ * estimated density through the plot's color scale. Default `"none"`.
56
+ *
57
+ * When a field name or accessor function is provided (instead of a CSS
58
+ * color), the mark groups by the fill channel and maps group values
59
+ * through the plot's fill color scale.
60
+ */
61
+ fill?: ChannelAccessor<Datum> | string;
62
+ /**
63
+ * Stroke color for density isolines. Use `"density"` to map each
64
+ * isoline's estimated density through the plot's color scale.
65
+ * Default `"currentColor"` when fill is `"none"`, otherwise `"none"`.
66
+ *
67
+ * When a field name or accessor function is provided (instead of a CSS
68
+ * color), the mark groups by the stroke channel and maps group values
69
+ * through the plot's stroke color scale.
70
+ */
71
+ stroke?: ChannelAccessor<Datum> | string;
72
+ strokeWidth?: number;
73
+ strokeOpacity?: number;
74
+ fillOpacity?: number;
75
+ opacity?: number;
76
+ strokeMiterlimit?: number;
77
+ clipPath?: string;
78
+ class?: string;
79
+ /** Render using a canvas element instead of SVG paths. */
80
+ canvas?: boolean;
81
+ /** the horizontal facet channel */
82
+ fx?: ChannelAccessor<Datum>;
83
+ /** the vertical facet channel */
84
+ fy?: ChannelAccessor<Datum>;
85
+ }
86
+
87
+ import type {
88
+ DataRecord,
89
+ ChannelAccessor,
90
+ ScaledDataRecord,
91
+ MarkType,
92
+ RawValue
93
+ } from '../types/index.js';
94
+ import GeoPathGroup from './helpers/GeoPathGroup.svelte';
95
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
96
+ import { contourDensity } from 'd3-contour';
97
+ import { geoPath } from 'd3-geo';
98
+ import Mark from '../Mark.svelte';
99
+ import { usePlot } from '../hooks/usePlot.svelte.js';
100
+ import { RAW_VALUE } from '../transforms/recordize.js';
101
+ import { ORIGINAL_NAME_KEYS } from '../constants.js';
102
+ import { getPlotDefaults } from '../hooks/plotDefaults.js';
103
+ import { isColorOrNull } from '../helpers/typeChecks.js';
104
+
105
+ // Per-band fake-datum symbols — used to attach the density geometry,
106
+ // extent, and pre-resolved facet values to the synthetic records passed
107
+ // to <Mark> for scale-domain registration and facet filtering.
108
+ const GEOM = Symbol('density_geom');
109
+ const FX_VAL = Symbol('density_fx');
110
+ const FY_VAL = Symbol('density_fy');
111
+ const Z_VAL = Symbol('density_z');
112
+ const X1_VAL = Symbol('density_x1');
113
+ const X2_VAL = Symbol('density_x2');
114
+ const Y1_VAL = Symbol('density_y1');
115
+ const Y2_VAL = Symbol('density_y2');
116
+
117
+ /**
118
+ * Arbitrary scale factor matching Observable Plot's density mark.
119
+ * Multiplying raw density values (in 1/px²) by k makes thresholds
120
+ * human-readable integers rather than tiny floating-point numbers.
121
+ */
122
+ const k = 100;
123
+
124
+ const DEFAULTS = {
125
+ ...getPlotDefaults().density
126
+ };
127
+
128
+ let markProps: DensityMarkProps = $props();
129
+
130
+ const {
131
+ data,
132
+ x: xAcc,
133
+ y: yAcc,
134
+ z: zAcc,
135
+ weight: weightAcc,
136
+ bandwidth = 20,
137
+ thresholds: thresholdSpec = 20,
138
+ fill: rawFill = 'none',
139
+ stroke: rawStroke,
140
+ strokeWidth,
141
+ strokeOpacity,
142
+ fillOpacity,
143
+ opacity,
144
+ strokeMiterlimit = 1,
145
+ clipPath,
146
+ class: className = '',
147
+ canvas = false,
148
+ fx: fxAcc,
149
+ fy: fyAcc,
150
+ ...options
151
+ }: DensityMarkProps = $derived({ ...DEFAULTS, ...markProps });
152
+
153
+ const plot = usePlot();
154
+
155
+ /**
156
+ * Returns true when a fill/stroke value should be treated as a data
157
+ * accessor (field name or function) rather than a constant color.
158
+ */
159
+ function isDensityAccessor(v: ChannelAccessor<Datum> | string | undefined): boolean {
160
+ if (v == null) return false;
161
+ if (typeof v === 'function') return true;
162
+ if (typeof v !== 'string') return false;
163
+ const lower = v.toLowerCase();
164
+ if (lower === 'none' || lower === 'density' || lower === 'inherit' || lower === 'currentcolor')
165
+ return false;
166
+ return !isColorOrNull(v);
167
+ }
168
+
169
+ // Whether fill/stroke are data accessors (trigger z-grouping + color scale)
170
+ const fillIsAccessor = $derived(isDensityAccessor(rawFill));
171
+ const strokeIsAccessor = $derived(isDensityAccessor(rawStroke));
172
+
173
+ // Effective grouping accessor: explicit z > fill (if accessor) > stroke (if accessor)
174
+ const zGroupAcc = $derived<ChannelAccessor<Datum> | null>(
175
+ zAcc != null
176
+ ? zAcc
177
+ : fillIsAccessor
178
+ ? (rawFill as ChannelAccessor<Datum>)
179
+ : strokeIsAccessor
180
+ ? (rawStroke as ChannelAccessor<Datum>)
181
+ : null
182
+ );
183
+ const isZGrouped = $derived(zGroupAcc != null);
184
+
185
+ // Resolved static fill/stroke strings (after extracting accessor info)
186
+ const fill = $derived<string>(fillIsAccessor ? 'none' : (rawFill as string) ?? 'none');
187
+
188
+ const fillDensity = $derived(/^density$/i.test(fill ?? ''));
189
+
190
+ /**
191
+ * When fill is active (not "none" or "density"), stroke defaults to "none";
192
+ * otherwise it defaults to "currentColor".
193
+ */
194
+ const effectiveStroke = $derived<string>(
195
+ strokeIsAccessor
196
+ ? 'currentColor'
197
+ : ((rawStroke as string | undefined) ?? (fill !== 'none' ? 'none' : 'currentColor'))
198
+ );
199
+
200
+ const strokeDensity = $derived(/^density$/i.test(effectiveStroke ?? ''));
201
+
202
+ /**
203
+ * True when fill or stroke uses the `"density"` keyword — the color scale
204
+ * is used to encode density values.
205
+ */
206
+ const markUsesColorScale = $derived(fillDensity || strokeDensity);
207
+
208
+ /** Resolve a channel accessor against a single datum. */
209
+ function resolveAcc(acc: ChannelAccessor<any> | undefined | null, d: any): any {
210
+ if (acc == null) return undefined;
211
+ if (typeof acc === 'function') return (acc as (d: any) => any)(d);
212
+ return (d as any)[acc as string];
213
+ }
214
+
215
+ /** Pixel-space bounds of the current facet (or full plot if not faceted). */
216
+ function getBounds() {
217
+ const facetWidth = plot.facetWidth ?? 100;
218
+ const facetHeight = plot.facetHeight ?? 100;
219
+ const marginLeft = plot.options.marginLeft ?? 0;
220
+ const marginTop = plot.options.marginTop ?? 0;
221
+ return {
222
+ bx1: marginLeft,
223
+ by1: marginTop,
224
+ w: Math.max(1, Math.round(facetWidth)),
225
+ h: Math.max(1, Math.round(facetHeight))
226
+ };
227
+ }
228
+
229
+ type DensityGeometry = {
230
+ type: 'MultiPolygon';
231
+ coordinates: number[][][][];
232
+ /** k-scaled density threshold (value × k); used for color mapping. */
233
+ value: number;
234
+ /** pre-resolved fx channel value for facet filtering */
235
+ fxVal?: RawValue;
236
+ /** pre-resolved fy channel value for facet filtering */
237
+ fyVal?: RawValue;
238
+ /** pre-resolved z group value for per-group coloring */
239
+ zVal?: RawValue;
240
+ };
241
+
242
+ /**
243
+ * All density contour bands across every facet group.
244
+ *
245
+ * Phase 1: compute density grids per facet group, find global max density.
246
+ * Phase 2: derive thresholds from global max (for consistent color scale).
247
+ * Phase 3: compute contour geometries at those thresholds.
248
+ *
249
+ * Geometries are tagged with fxVal/fyVal/zVal so <Mark> can filter per
250
+ * panel and apply per-group colors.
251
+ */
252
+ const densityResult = $derived.by((): DensityGeometry[] | null => {
253
+ const xFn = plot.scales.x?.fn;
254
+ const yFn = plot.scales.y?.fn;
255
+ if (!xFn || !yFn || !data?.length) return null;
256
+
257
+ const { bx1, by1, w, h } = getBounds();
258
+ const isFaceted = fxAcc != null || fyAcc != null;
259
+
260
+ // Group data by (fxVal, fyVal, zVal) — a single group when not faceted/z-grouped.
261
+ const groups = new SvelteMap<
262
+ string,
263
+ { fxVal: RawValue; fyVal: RawValue; zVal: RawValue; items: any[] }
264
+ >();
265
+ for (const d of data as any[]) {
266
+ const fxVal = fxAcc != null ? resolveAcc(fxAcc, d) : undefined;
267
+ const fyVal = fyAcc != null ? resolveAcc(fyAcc, d) : undefined;
268
+ const zVal = isZGrouped ? resolveAcc(zGroupAcc, d) : undefined;
269
+ const key = `${fxVal}\0${fyVal}\0${zVal}`;
270
+ if (!groups.has(key)) groups.set(key, { fxVal, fyVal, zVal, items: [] });
271
+ groups.get(key)!.items.push(d);
272
+ }
273
+
274
+ // Phase 1: build a density estimator per group, record max density.
275
+ type GroupEntry = {
276
+ fxVal: RawValue;
277
+ fyVal: RawValue;
278
+ zVal: RawValue;
279
+ /** function returned by kde.contours(data); call it with a threshold */
280
+ contourFn: (t: number) => DensityGeometry;
281
+ maxVal: number;
282
+ };
283
+ const groupEntries: GroupEntry[] = [];
284
+ let globalMax = 0;
285
+
286
+ for (const { fxVal, fyVal, zVal, items } of groups.values()) {
287
+ if (!items.length) continue;
288
+
289
+ const kde = contourDensity<any>()
290
+ .x((d) => (xFn(resolveAcc(xAcc, d)) as number) - bx1)
291
+ .y((d) => (yFn(resolveAcc(yAcc, d)) as number) - by1)
292
+ .weight(weightAcc != null ? (d) => +(resolveAcc(weightAcc, d) ?? 1) : () => 1)
293
+ .size([w, h])
294
+ .bandwidth(bandwidth);
295
+
296
+ // .contours() is in d3-contour v4 but not typed in @types/d3-contour
297
+ const contourFn = (kde as any).contours(items) as {
298
+ (t: number): DensityGeometry;
299
+ max: number;
300
+ };
301
+ const maxVal = contourFn.max;
302
+ if (maxVal > globalMax) globalMax = maxVal;
303
+ groupEntries.push({ fxVal, fyVal, zVal, contourFn, maxVal });
304
+ }
305
+
306
+ if (!groupEntries.length || globalMax === 0) return null;
307
+
308
+ // Phase 2: derive thresholds from global max density.
309
+ let T: number[];
310
+ if (Array.isArray(thresholdSpec)) {
311
+ T = thresholdSpec as number[];
312
+ } else {
313
+ const n = thresholdSpec as number;
314
+ // Generate n-1 levels evenly spaced over (0, globalMax × k]
315
+ T = Array.from({ length: n - 1 }, (_, i) => (globalMax * k * (i + 1)) / n);
316
+ }
317
+ if (!T.length) return null;
318
+
319
+ // Phase 3: compute contour geometries for each group at each threshold.
320
+ const allGeoms: DensityGeometry[] = [];
321
+
322
+ for (const { fxVal, fyVal, zVal, contourFn } of groupEntries) {
323
+ for (const t of T) {
324
+ // contourFn expects actual density values; t is k-scaled.
325
+ const geom = contourFn(t / k);
326
+
327
+ // Translate from facet-local [0, w]×[0, h] to SVG coordinates.
328
+ for (const rings of geom.coordinates) {
329
+ for (const ring of rings) {
330
+ for (const point of ring) {
331
+ point[0] += bx1;
332
+ point[1] += by1;
333
+ }
334
+ }
335
+ }
336
+
337
+ geom.value = t; // k-scaled, used for color mapping
338
+ if (isFaceted && fxVal !== undefined) geom.fxVal = fxVal;
339
+ if (isFaceted && fyVal !== undefined) geom.fyVal = fyVal;
340
+ if (isZGrouped && zVal !== undefined) geom.zVal = zVal;
341
+ allGeoms.push(geom);
342
+ }
343
+ }
344
+
345
+ // Anchor the color scale at zero so the first density band is
346
+ // distinguishable from the background. One anchor per facet group so
347
+ // no record ever has an undefined fx/fy value, which would otherwise
348
+ // introduce a spurious null facet panel.
349
+ if (markUsesColorScale) {
350
+ for (const { fxVal, fyVal, zVal } of groupEntries) {
351
+ const anchor: DensityGeometry = {
352
+ type: 'MultiPolygon',
353
+ coordinates: [],
354
+ value: 0
355
+ };
356
+ if (isFaceted && fxVal !== undefined) anchor.fxVal = fxVal;
357
+ if (isFaceted && fyVal !== undefined) anchor.fyVal = fyVal;
358
+ if (isZGrouped && zVal !== undefined) anchor.zVal = zVal;
359
+ allGeoms.push(anchor);
360
+ }
361
+ }
362
+
363
+ return allGeoms.length > 0 ? allGeoms : null;
364
+ });
365
+
366
+ /**
367
+ * Data-space extent used to bootstrap the x/y scale domains before
368
+ * densityResult is available (density computation needs the scale fns,
369
+ * which need the domain — this breaks the circular dependency).
370
+ */
371
+ const extent = $derived.by(() => {
372
+ if (!data?.length) return null;
373
+ let xMin = Infinity,
374
+ xMax = -Infinity,
375
+ yMin = Infinity,
376
+ yMax = -Infinity;
377
+ let xUsesDate = false,
378
+ yUsesDate = false;
379
+ for (const d of data as any[]) {
380
+ const xv = resolveAcc(xAcc, d);
381
+ const yv = resolveAcc(yAcc, d);
382
+ if (xv instanceof Date) {
383
+ xUsesDate = true;
384
+ const ms = xv.getTime();
385
+ if (isFinite(ms)) {
386
+ if (ms < xMin) xMin = ms;
387
+ if (ms > xMax) xMax = ms;
388
+ }
389
+ } else if (typeof xv === 'number' && isFinite(xv)) {
390
+ if (xv < xMin) xMin = xv;
391
+ if (xv > xMax) xMax = xv;
392
+ }
393
+ if (yv instanceof Date) {
394
+ yUsesDate = true;
395
+ const ms = yv.getTime();
396
+ if (isFinite(ms)) {
397
+ if (ms < yMin) yMin = ms;
398
+ if (ms > yMax) yMax = ms;
399
+ }
400
+ } else if (typeof yv === 'number' && isFinite(yv)) {
401
+ if (yv < yMin) yMin = yv;
402
+ if (yv > yMax) yMax = yv;
403
+ }
404
+ }
405
+ if (!isFinite(xMin) || !isFinite(xMax) || !isFinite(yMin) || !isFinite(yMax)) return null;
406
+ return {
407
+ x1: xUsesDate ? new Date(xMin) : xMin,
408
+ x2: xUsesDate ? new Date(xMax) : xMax,
409
+ y1: yUsesDate ? new Date(yMin) : yMin,
410
+ y2: yUsesDate ? new Date(yMax) : yMax
411
+ };
412
+ });
413
+
414
+ /**
415
+ * Unified mark data passed to <Mark> for scale-domain registration and
416
+ * facet filtering.
417
+ *
418
+ * A bootstrap extent record is always included before densityResult is
419
+ * computed (scatter-style circular dependency with x/y scales). Each
420
+ * per-band fake datum carries:
421
+ * [X1_VAL]..[Y2_VAL] data-space extent → x/y scale domain
422
+ * [RAW_VALUE] k-scaled density → color scale domain (density mode)
423
+ * [Z_VAL] z group value → fill/stroke scale domain (z-group mode)
424
+ * [FX_VAL]/[FY_VAL] facet values → Mark facet filtering
425
+ * [GEOM] geometry → rendered by children snippet
426
+ */
427
+ const markData = $derived.by((): DataRecord[] => {
428
+ const ext = extent;
429
+ const records: any[] = [];
430
+ // Bootstrap extent record(s) so x/y scales are available for density
431
+ // computation on the first render pass. When faceted or z-grouped, emit
432
+ // one record per unique (fxVal, fyVal, zVal) combination so no record
433
+ // carries an undefined fx/fy value (which would create a spurious null facet).
434
+ if (ext && !densityResult) {
435
+ const isFaceted = fxAcc != null || fyAcc != null;
436
+ if ((isFaceted || isZGrouped) && data?.length) {
437
+ const seen = new SvelteSet<string>();
438
+ for (const d of data as any[]) {
439
+ const fxVal = fxAcc != null ? resolveAcc(fxAcc, d) : undefined;
440
+ const fyVal = fyAcc != null ? resolveAcc(fyAcc, d) : undefined;
441
+ const zVal = isZGrouped ? resolveAcc(zGroupAcc, d) : undefined;
442
+ const key = `${fxVal}\0${fyVal}\0${zVal}`;
443
+ if (!seen.has(key)) {
444
+ seen.add(key);
445
+ records.push({
446
+ [X1_VAL]: ext.x1,
447
+ [X2_VAL]: ext.x2,
448
+ [Y1_VAL]: ext.y1,
449
+ [Y2_VAL]: ext.y2,
450
+ [FX_VAL]: fxVal,
451
+ [FY_VAL]: fyVal,
452
+ [Z_VAL]: zVal
453
+ });
454
+ }
455
+ }
456
+ } else {
457
+ records.push({
458
+ [X1_VAL]: ext.x1,
459
+ [X2_VAL]: ext.x2,
460
+ [Y1_VAL]: ext.y1,
461
+ [Y2_VAL]: ext.y2
462
+ });
463
+ }
464
+ }
465
+
466
+ if (densityResult) {
467
+ for (const geom of densityResult) {
468
+ records.push({
469
+ [X1_VAL]: ext?.x1,
470
+ [X2_VAL]: ext?.x2,
471
+ [Y1_VAL]: ext?.y1,
472
+ [Y2_VAL]: ext?.y2,
473
+ [RAW_VALUE]: geom.value,
474
+ [FX_VAL]: geom.fxVal,
475
+ [FY_VAL]: geom.fyVal,
476
+ [Z_VAL]: geom.zVal,
477
+ [GEOM]: geom
478
+ });
479
+ }
480
+ }
481
+
482
+ return records as DataRecord[];
483
+ });
484
+
485
+ const markChannels = $derived.by(() => {
486
+ const base = ['x1', 'x2', 'y1', 'y2'] as const;
487
+ if (markUsesColorScale || fillIsAccessor) return [...base, 'fill'] as const;
488
+ if (strokeIsAccessor) return [...base, 'stroke'] as const;
489
+ return base;
490
+ });
491
+
492
+ // fill channel accessor:
493
+ // density color scale (fill or stroke="density") → RAW_VALUE (registers domain)
494
+ // fill accessor (z-grouping) → Z_VAL
495
+ // else → undefined
496
+ const markFill = $derived(
497
+ markUsesColorScale ? (RAW_VALUE as any) : fillIsAccessor ? (Z_VAL as any) : undefined
498
+ );
499
+
500
+ // stroke channel accessor: only set when stroke is a z-group accessor
501
+ const markStroke = $derived(strokeIsAccessor ? (Z_VAL as any) : undefined);
502
+
503
+ const markFx = $derived(fxAcc != null ? FX_VAL : undefined);
504
+ const markFy = $derived(fyAcc != null ? FY_VAL : undefined);
505
+
506
+ const markChannelProps = $derived({
507
+ x1: X1_VAL as any,
508
+ x2: X2_VAL as any,
509
+ y1: Y1_VAL as any,
510
+ y2: Y2_VAL as any,
511
+ fill: markFill as any,
512
+ stroke: markStroke as any,
513
+ fx: markFx as any,
514
+ fy: markFy as any,
515
+ ...(typeof xAcc === 'string' && { [ORIGINAL_NAME_KEYS.x]: xAcc }),
516
+ ...(typeof yAcc === 'string' && { [ORIGINAL_NAME_KEYS.y]: yAcc }),
517
+ ...(markUsesColorScale && { [ORIGINAL_NAME_KEYS.fill]: 'Density' }),
518
+ ...(fillIsAccessor &&
519
+ typeof rawFill === 'string' && { [ORIGINAL_NAME_KEYS.fill]: rawFill }),
520
+ ...(strokeIsAccessor &&
521
+ typeof rawStroke === 'string' && { [ORIGINAL_NAME_KEYS.stroke]: rawStroke })
522
+ });
523
+
524
+ const path = geoPath();
525
+ </script>
526
+
527
+ <Mark
528
+ type={'density' as MarkType}
529
+ data={markData}
530
+ channels={markChannels as any}
531
+ {...options}
532
+ {...markChannelProps}>
533
+ {#snippet children({ scaledData }: { scaledData: ScaledDataRecord[] })}
534
+ <GeoPathGroup
535
+ {scaledData}
536
+ {path}
537
+ geomKey={GEOM}
538
+ colorKeyword="density"
539
+ {fill}
540
+ stroke={effectiveStroke}
541
+ {strokeWidth}
542
+ {strokeOpacity}
543
+ {fillOpacity}
544
+ {opacity}
545
+ {strokeMiterlimit}
546
+ {clipPath}
547
+ className={className || undefined}
548
+ ariaLabel="density"
549
+ {canvas}
550
+ {plot} />
551
+ {/snippet}
552
+ </Mark>
@@ -0,0 +1,108 @@
1
+ import type { DataRecord, ChannelAccessor } from '../types/index.js';
2
+ declare function $$render<Datum extends DataRecord>(): {
3
+ props: {
4
+ /** Input data — an array of records with x/y positions. */
5
+ data?: Datum[] | null;
6
+ /** x position channel (data space). */
7
+ x?: ChannelAccessor<Datum>;
8
+ /** y position channel (data space). */
9
+ y?: ChannelAccessor<Datum>;
10
+ /** Optional weight channel; defaults to 1 for each point. */
11
+ weight?: ChannelAccessor<Datum>;
12
+ /**
13
+ * Grouping channel. When specified the mark computes a separate
14
+ * density estimate per unique z value and renders the groups
15
+ * independently. Unlike `fill`/`stroke` as accessors, `z` does not
16
+ * automatically add a color channel.
17
+ */
18
+ z?: ChannelAccessor<Datum>;
19
+ /**
20
+ * Gaussian kernel bandwidth in screen pixels (default 20).
21
+ * Larger values produce smoother, more blurred density estimates.
22
+ */
23
+ bandwidth?: number;
24
+ /**
25
+ * Density threshold levels. Can be:
26
+ * - a **count** (number): that many evenly-spaced levels from 0 to the
27
+ * maximum density (default 20)
28
+ * - an explicit **array** of threshold values in k-scaled density units
29
+ * (where k = 100; values from 0 to roughly 100× the peak density)
30
+ */
31
+ thresholds?: number | number[];
32
+ /**
33
+ * Fill color for density bands. Use `"density"` to map each band's
34
+ * estimated density through the plot's color scale. Default `"none"`.
35
+ *
36
+ * When a field name or accessor function is provided (instead of a CSS
37
+ * color), the mark groups by the fill channel and maps group values
38
+ * through the plot's fill color scale.
39
+ */
40
+ fill?: ChannelAccessor<Datum> | string;
41
+ /**
42
+ * Stroke color for density isolines. Use `"density"` to map each
43
+ * isoline's estimated density through the plot's color scale.
44
+ * Default `"currentColor"` when fill is `"none"`, otherwise `"none"`.
45
+ *
46
+ * When a field name or accessor function is provided (instead of a CSS
47
+ * color), the mark groups by the stroke channel and maps group values
48
+ * through the plot's stroke color scale.
49
+ */
50
+ stroke?: ChannelAccessor<Datum> | string;
51
+ strokeWidth?: number;
52
+ strokeOpacity?: number;
53
+ fillOpacity?: number;
54
+ opacity?: number;
55
+ strokeMiterlimit?: number;
56
+ clipPath?: string;
57
+ class?: string;
58
+ /** Render using a canvas element instead of SVG paths. */
59
+ canvas?: boolean;
60
+ /** the horizontal facet channel */
61
+ fx?: ChannelAccessor<Datum>;
62
+ /** the vertical facet channel */
63
+ fy?: ChannelAccessor<Datum>;
64
+ };
65
+ exports: {};
66
+ bindings: "";
67
+ slots: {};
68
+ events: {};
69
+ };
70
+ declare class __sveltets_Render<Datum extends DataRecord> {
71
+ props(): ReturnType<typeof $$render<Datum>>['props'];
72
+ events(): ReturnType<typeof $$render<Datum>>['events'];
73
+ slots(): ReturnType<typeof $$render<Datum>>['slots'];
74
+ bindings(): "";
75
+ exports(): {};
76
+ }
77
+ interface $$IsomorphicComponent {
78
+ new <Datum extends DataRecord>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Datum>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Datum>['props']>, ReturnType<__sveltets_Render<Datum>['events']>, ReturnType<__sveltets_Render<Datum>['slots']>> & {
79
+ $$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
80
+ } & ReturnType<__sveltets_Render<Datum>['exports']>;
81
+ <Datum extends DataRecord>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
82
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
83
+ }
84
+ /**
85
+ * Renders two-dimensional kernel density estimation as filled or stroked
86
+ * contour paths.
87
+ *
88
+ * Data points with `x` and `y` channels are projected into pixel space and
89
+ * passed to d3's `contourDensity` estimator, which uses a Gaussian kernel to
90
+ * produce a density grid. Iso-density contour bands are then drawn using the
91
+ * marching-squares algorithm.
92
+ *
93
+ * Styling: `fill` and `stroke` accept ordinary CSS color strings **or** the
94
+ * special keyword `"density"`, which maps each contour level's estimated
95
+ * density through the plot's color scale. Defaults: `fill="none"`,
96
+ * `stroke="currentColor"`.
97
+ *
98
+ * Grouping: when a `z` channel is specified (or when `fill`/`stroke` is a
99
+ * field name or function rather than a CSS color), the mark computes a
100
+ * separate density estimate per group and renders each group independently.
101
+ * Using `fill` or `stroke` as a data accessor also maps the group value
102
+ * through the plot's color scale.
103
+ *
104
+ * Supports faceting via `fx`/`fy`.
105
+ */
106
+ declare const Density: $$IsomorphicComponent;
107
+ type Density<Datum extends DataRecord> = InstanceType<typeof Density<Datum>>;
108
+ export default Density;