svelteplot 0.13.0 → 0.14.0-pr-545.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/helpers/group.d.ts +1 -1
- package/dist/helpers/group.js +3 -3
- package/dist/marks/ColorLegend.svelte +5 -1
- package/dist/marks/Contour.svelte +21 -30
- package/dist/marks/Contour.svelte.d.ts +2 -0
- package/dist/marks/DelaunayLink.svelte +127 -0
- package/dist/marks/DelaunayLink.svelte.d.ts +175 -0
- package/dist/marks/DelaunayMesh.svelte +102 -0
- package/dist/marks/DelaunayMesh.svelte.d.ts +172 -0
- package/dist/marks/Density.svelte +554 -0
- package/dist/marks/Density.svelte.d.ts +108 -0
- package/dist/marks/Hull.svelte +103 -0
- package/dist/marks/Hull.svelte.d.ts +175 -0
- package/dist/marks/Voronoi.svelte +118 -0
- package/dist/marks/Voronoi.svelte.d.ts +172 -0
- package/dist/marks/VoronoiMesh.svelte +109 -0
- package/dist/marks/VoronoiMesh.svelte.d.ts +172 -0
- package/dist/marks/helpers/DensityCanvas.svelte +118 -0
- package/dist/marks/helpers/DensityCanvas.svelte.d.ts +18 -0
- package/dist/marks/helpers/GeoPathCanvas.svelte +137 -0
- package/dist/marks/helpers/GeoPathCanvas.svelte.d.ts +26 -0
- package/dist/marks/helpers/GeoPathGroup.svelte +122 -0
- package/dist/marks/helpers/GeoPathGroup.svelte.d.ts +48 -0
- package/dist/marks/helpers/PathGroup.svelte +100 -0
- package/dist/marks/helpers/PathGroup.svelte.d.ts +16 -0
- package/dist/marks/helpers/PathItems.svelte +112 -0
- package/dist/marks/helpers/PathItems.svelte.d.ts +16 -0
- package/dist/marks/index.d.ts +7 -1
- package/dist/marks/index.js +7 -1
- package/dist/types/mark.d.ts +1 -1
- package/dist/types/plot.d.ts +25 -1
- package/package.json +1 -1
|
@@ -0,0 +1,554 @@
|
|
|
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
|
+
usePerPathFill={fillIsAccessor}
|
|
552
|
+
usePerPathStroke={strokeIsAccessor} />
|
|
553
|
+
{/snippet}
|
|
554
|
+
</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;
|