svelteplot 0.14.0 → 0.14.1-pr-549.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/marks/Density.svelte +139 -32
- package/dist/marks/Density.svelte.d.ts +23 -2
- package/dist/marks/Link.svelte +1 -1
- package/dist/marks/RegressionX.svelte +1 -1
- package/dist/marks/RegressionY.svelte +1 -1
- package/dist/marks/helpers/GeoPathCanvas.svelte +15 -3
- package/dist/marks/helpers/GeoPathCanvas.svelte.d.ts +2 -0
- package/dist/marks/helpers/GeoPathGroup.svelte +25 -6
- package/dist/marks/helpers/GeoPathGroup.svelte.d.ts +11 -0
- package/dist/marks/helpers/Regression.svelte +20 -7
- package/package.json +27 -26
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
density through the plot's color scale. Defaults: `fill="none"`,
|
|
13
13
|
`stroke="currentColor"`.
|
|
14
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
|
+
|
|
15
21
|
Supports faceting via `fx`/`fy`.
|
|
16
22
|
-->
|
|
17
23
|
<script lang="ts" generics="Datum extends DataRecord">
|
|
@@ -24,6 +30,13 @@
|
|
|
24
30
|
y?: ChannelAccessor<Datum>;
|
|
25
31
|
/** Optional weight channel; defaults to 1 for each point. */
|
|
26
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>;
|
|
27
40
|
/**
|
|
28
41
|
* Gaussian kernel bandwidth in screen pixels (default 20).
|
|
29
42
|
* Larger values produce smoother, more blurred density estimates.
|
|
@@ -40,14 +53,22 @@
|
|
|
40
53
|
/**
|
|
41
54
|
* Fill color for density bands. Use `"density"` to map each band's
|
|
42
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.
|
|
43
60
|
*/
|
|
44
|
-
fill?: string;
|
|
61
|
+
fill?: ChannelAccessor<Datum> | string;
|
|
45
62
|
/**
|
|
46
63
|
* Stroke color for density isolines. Use `"density"` to map each
|
|
47
64
|
* isoline's estimated density through the plot's color scale.
|
|
48
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.
|
|
49
70
|
*/
|
|
50
|
-
stroke?: string;
|
|
71
|
+
stroke?: ChannelAccessor<Datum> | string;
|
|
51
72
|
strokeWidth?: number;
|
|
52
73
|
strokeOpacity?: number;
|
|
53
74
|
fillOpacity?: number;
|
|
@@ -79,6 +100,7 @@
|
|
|
79
100
|
import { RAW_VALUE } from '../transforms/recordize.js';
|
|
80
101
|
import { ORIGINAL_NAME_KEYS } from '../constants.js';
|
|
81
102
|
import { getPlotDefaults } from '../hooks/plotDefaults.js';
|
|
103
|
+
import { isColorOrNull } from '../helpers/typeChecks.js';
|
|
82
104
|
|
|
83
105
|
// Per-band fake-datum symbols — used to attach the density geometry,
|
|
84
106
|
// extent, and pre-resolved facet values to the synthetic records passed
|
|
@@ -86,6 +108,7 @@
|
|
|
86
108
|
const GEOM = Symbol('density_geom');
|
|
87
109
|
const FX_VAL = Symbol('density_fx');
|
|
88
110
|
const FY_VAL = Symbol('density_fy');
|
|
111
|
+
const Z_VAL = Symbol('density_z');
|
|
89
112
|
const X1_VAL = Symbol('density_x1');
|
|
90
113
|
const X2_VAL = Symbol('density_x2');
|
|
91
114
|
const Y1_VAL = Symbol('density_y1');
|
|
@@ -108,10 +131,11 @@
|
|
|
108
131
|
data,
|
|
109
132
|
x: xAcc,
|
|
110
133
|
y: yAcc,
|
|
134
|
+
z: zAcc,
|
|
111
135
|
weight: weightAcc,
|
|
112
136
|
bandwidth = 20,
|
|
113
137
|
thresholds: thresholdSpec = 20,
|
|
114
|
-
fill = 'none',
|
|
138
|
+
fill: rawFill = 'none',
|
|
115
139
|
stroke: rawStroke,
|
|
116
140
|
strokeWidth,
|
|
117
141
|
strokeOpacity,
|
|
@@ -128,13 +152,61 @@
|
|
|
128
152
|
|
|
129
153
|
const plot = usePlot();
|
|
130
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
|
+
// Descriptor form { value, scale } — recurse into the inner value.
|
|
163
|
+
if (typeof v === 'object' && 'value' in (v as object))
|
|
164
|
+
return isDensityAccessor((v as { value: any }).value);
|
|
165
|
+
if (typeof v !== 'string') return false;
|
|
166
|
+
const lower = v.toLowerCase();
|
|
167
|
+
if (
|
|
168
|
+
lower === 'none' ||
|
|
169
|
+
lower === 'density' ||
|
|
170
|
+
lower === 'inherit' ||
|
|
171
|
+
lower === 'currentcolor'
|
|
172
|
+
)
|
|
173
|
+
return false;
|
|
174
|
+
return !isColorOrNull(v);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Whether fill/stroke are data accessors (trigger z-grouping + color scale)
|
|
178
|
+
const fillIsAccessor = $derived(isDensityAccessor(rawFill));
|
|
179
|
+
const strokeIsAccessor = $derived(isDensityAccessor(rawStroke));
|
|
180
|
+
|
|
181
|
+
// Effective grouping accessor: explicit z > fill (if accessor) > stroke (if accessor)
|
|
182
|
+
const zGroupAcc = $derived<ChannelAccessor<Datum> | null>(
|
|
183
|
+
zAcc != null
|
|
184
|
+
? zAcc
|
|
185
|
+
: fillIsAccessor
|
|
186
|
+
? (rawFill as ChannelAccessor<Datum>)
|
|
187
|
+
: strokeIsAccessor
|
|
188
|
+
? (rawStroke as ChannelAccessor<Datum>)
|
|
189
|
+
: null
|
|
190
|
+
);
|
|
191
|
+
const isZGrouped = $derived(zGroupAcc != null);
|
|
192
|
+
|
|
193
|
+
// Resolved static fill/stroke strings (after extracting accessor info)
|
|
194
|
+
const fill = $derived<string>(fillIsAccessor ? 'none' : ((rawFill as string) ?? 'none'));
|
|
195
|
+
|
|
131
196
|
const fillDensity = $derived(/^density$/i.test(fill ?? ''));
|
|
132
197
|
|
|
133
198
|
/**
|
|
134
199
|
* When fill is active (not "none" or "density"), stroke defaults to "none";
|
|
135
200
|
* otherwise it defaults to "currentColor".
|
|
201
|
+
* fillIsAccessor also counts as "fill active" even though the static fill
|
|
202
|
+
* prop is forced to "none" for GeoPathGroup — per-path fills flow via d.fill.
|
|
136
203
|
*/
|
|
137
|
-
const effectiveStroke = $derived(
|
|
204
|
+
const effectiveStroke = $derived<string>(
|
|
205
|
+
strokeIsAccessor
|
|
206
|
+
? 'currentColor'
|
|
207
|
+
: ((rawStroke as string | undefined) ??
|
|
208
|
+
(fill !== 'none' || fillIsAccessor ? 'none' : 'currentColor'))
|
|
209
|
+
);
|
|
138
210
|
|
|
139
211
|
const strokeDensity = $derived(/^density$/i.test(effectiveStroke ?? ''));
|
|
140
212
|
|
|
@@ -145,9 +217,12 @@
|
|
|
145
217
|
const markUsesColorScale = $derived(fillDensity || strokeDensity);
|
|
146
218
|
|
|
147
219
|
/** Resolve a channel accessor against a single datum. */
|
|
148
|
-
function resolveAcc(acc: ChannelAccessor<any> | undefined, d: any): any {
|
|
220
|
+
function resolveAcc(acc: ChannelAccessor<any> | undefined | null, d: any): any {
|
|
149
221
|
if (acc == null) return undefined;
|
|
150
222
|
if (typeof acc === 'function') return (acc as (d: any) => any)(d);
|
|
223
|
+
// Descriptor form { value, scale } — resolve the inner value.
|
|
224
|
+
if (typeof acc === 'object' && 'value' in (acc as object))
|
|
225
|
+
return resolveAcc((acc as { value: any }).value, d);
|
|
151
226
|
return (d as any)[acc as string];
|
|
152
227
|
}
|
|
153
228
|
|
|
@@ -174,6 +249,8 @@
|
|
|
174
249
|
fxVal?: RawValue;
|
|
175
250
|
/** pre-resolved fy channel value for facet filtering */
|
|
176
251
|
fyVal?: RawValue;
|
|
252
|
+
/** pre-resolved z group value for per-group coloring */
|
|
253
|
+
zVal?: RawValue;
|
|
177
254
|
};
|
|
178
255
|
|
|
179
256
|
/**
|
|
@@ -183,7 +260,8 @@
|
|
|
183
260
|
* Phase 2: derive thresholds from global max (for consistent color scale).
|
|
184
261
|
* Phase 3: compute contour geometries at those thresholds.
|
|
185
262
|
*
|
|
186
|
-
* Geometries are tagged with fxVal/fyVal so <Mark> can filter per
|
|
263
|
+
* Geometries are tagged with fxVal/fyVal/zVal so <Mark> can filter per
|
|
264
|
+
* panel and apply per-group colors.
|
|
187
265
|
*/
|
|
188
266
|
const densityResult = $derived.by((): DensityGeometry[] | null => {
|
|
189
267
|
const xFn = plot.scales.x?.fn;
|
|
@@ -193,13 +271,17 @@
|
|
|
193
271
|
const { bx1, by1, w, h } = getBounds();
|
|
194
272
|
const isFaceted = fxAcc != null || fyAcc != null;
|
|
195
273
|
|
|
196
|
-
// Group data by (fxVal, fyVal) — a single group when not faceted.
|
|
197
|
-
const groups = new SvelteMap<
|
|
274
|
+
// Group data by (fxVal, fyVal, zVal) — a single group when not faceted/z-grouped.
|
|
275
|
+
const groups = new SvelteMap<
|
|
276
|
+
string,
|
|
277
|
+
{ fxVal: RawValue; fyVal: RawValue; zVal: RawValue; items: any[] }
|
|
278
|
+
>();
|
|
198
279
|
for (const d of data as any[]) {
|
|
199
|
-
const fxVal =
|
|
200
|
-
const fyVal =
|
|
201
|
-
const
|
|
202
|
-
|
|
280
|
+
const fxVal = fxAcc != null ? resolveAcc(fxAcc, d) : undefined;
|
|
281
|
+
const fyVal = fyAcc != null ? resolveAcc(fyAcc, d) : undefined;
|
|
282
|
+
const zVal = isZGrouped ? resolveAcc(zGroupAcc, d) : undefined;
|
|
283
|
+
const key = `${fxVal}\0${fyVal}\0${zVal}`;
|
|
284
|
+
if (!groups.has(key)) groups.set(key, { fxVal, fyVal, zVal, items: [] });
|
|
203
285
|
groups.get(key)!.items.push(d);
|
|
204
286
|
}
|
|
205
287
|
|
|
@@ -207,6 +289,7 @@
|
|
|
207
289
|
type GroupEntry = {
|
|
208
290
|
fxVal: RawValue;
|
|
209
291
|
fyVal: RawValue;
|
|
292
|
+
zVal: RawValue;
|
|
210
293
|
/** function returned by kde.contours(data); call it with a threshold */
|
|
211
294
|
contourFn: (t: number) => DensityGeometry;
|
|
212
295
|
maxVal: number;
|
|
@@ -214,7 +297,7 @@
|
|
|
214
297
|
const groupEntries: GroupEntry[] = [];
|
|
215
298
|
let globalMax = 0;
|
|
216
299
|
|
|
217
|
-
for (const { fxVal, fyVal, items } of groups.values()) {
|
|
300
|
+
for (const { fxVal, fyVal, zVal, items } of groups.values()) {
|
|
218
301
|
if (!items.length) continue;
|
|
219
302
|
|
|
220
303
|
const kde = contourDensity<any>()
|
|
@@ -231,7 +314,7 @@
|
|
|
231
314
|
};
|
|
232
315
|
const maxVal = contourFn.max;
|
|
233
316
|
if (maxVal > globalMax) globalMax = maxVal;
|
|
234
|
-
groupEntries.push({ fxVal, fyVal, contourFn, maxVal });
|
|
317
|
+
groupEntries.push({ fxVal, fyVal, zVal, contourFn, maxVal });
|
|
235
318
|
}
|
|
236
319
|
|
|
237
320
|
if (!groupEntries.length || globalMax === 0) return null;
|
|
@@ -250,7 +333,7 @@
|
|
|
250
333
|
// Phase 3: compute contour geometries for each group at each threshold.
|
|
251
334
|
const allGeoms: DensityGeometry[] = [];
|
|
252
335
|
|
|
253
|
-
for (const { fxVal, fyVal, contourFn } of groupEntries) {
|
|
336
|
+
for (const { fxVal, fyVal, zVal, contourFn } of groupEntries) {
|
|
254
337
|
for (const t of T) {
|
|
255
338
|
// contourFn expects actual density values; t is k-scaled.
|
|
256
339
|
const geom = contourFn(t / k);
|
|
@@ -268,6 +351,7 @@
|
|
|
268
351
|
geom.value = t; // k-scaled, used for color mapping
|
|
269
352
|
if (isFaceted && fxVal !== undefined) geom.fxVal = fxVal;
|
|
270
353
|
if (isFaceted && fyVal !== undefined) geom.fyVal = fyVal;
|
|
354
|
+
if (isZGrouped && zVal !== undefined) geom.zVal = zVal;
|
|
271
355
|
allGeoms.push(geom);
|
|
272
356
|
}
|
|
273
357
|
}
|
|
@@ -277,7 +361,7 @@
|
|
|
277
361
|
// no record ever has an undefined fx/fy value, which would otherwise
|
|
278
362
|
// introduce a spurious null facet panel.
|
|
279
363
|
if (markUsesColorScale) {
|
|
280
|
-
for (const { fxVal, fyVal } of groupEntries) {
|
|
364
|
+
for (const { fxVal, fyVal, zVal } of groupEntries) {
|
|
281
365
|
const anchor: DensityGeometry = {
|
|
282
366
|
type: 'MultiPolygon',
|
|
283
367
|
coordinates: [],
|
|
@@ -285,6 +369,7 @@
|
|
|
285
369
|
};
|
|
286
370
|
if (isFaceted && fxVal !== undefined) anchor.fxVal = fxVal;
|
|
287
371
|
if (isFaceted && fyVal !== undefined) anchor.fyVal = fyVal;
|
|
372
|
+
if (isZGrouped && zVal !== undefined) anchor.zVal = zVal;
|
|
288
373
|
allGeoms.push(anchor);
|
|
289
374
|
}
|
|
290
375
|
}
|
|
@@ -348,7 +433,8 @@
|
|
|
348
433
|
* computed (scatter-style circular dependency with x/y scales). Each
|
|
349
434
|
* per-band fake datum carries:
|
|
350
435
|
* [X1_VAL]..[Y2_VAL] data-space extent → x/y scale domain
|
|
351
|
-
* [RAW_VALUE] k-scaled density → color scale domain
|
|
436
|
+
* [RAW_VALUE] k-scaled density → color scale domain (density mode)
|
|
437
|
+
* [Z_VAL] z group value → fill/stroke scale domain (z-group mode)
|
|
352
438
|
* [FX_VAL]/[FY_VAL] facet values → Mark facet filtering
|
|
353
439
|
* [GEOM] geometry → rendered by children snippet
|
|
354
440
|
*/
|
|
@@ -356,17 +442,18 @@
|
|
|
356
442
|
const ext = extent;
|
|
357
443
|
const records: any[] = [];
|
|
358
444
|
// Bootstrap extent record(s) so x/y scales are available for density
|
|
359
|
-
// computation on the first render pass. When faceted, emit
|
|
360
|
-
// per unique (fxVal, fyVal) combination so no record
|
|
361
|
-
// undefined fx/fy value (which would create a spurious null facet).
|
|
445
|
+
// computation on the first render pass. When faceted or z-grouped, emit
|
|
446
|
+
// one record per unique (fxVal, fyVal, zVal) combination so no record
|
|
447
|
+
// carries an undefined fx/fy value (which would create a spurious null facet).
|
|
362
448
|
if (ext && !densityResult) {
|
|
363
449
|
const isFaceted = fxAcc != null || fyAcc != null;
|
|
364
|
-
if (isFaceted && data?.length) {
|
|
450
|
+
if ((isFaceted || isZGrouped) && data?.length) {
|
|
365
451
|
const seen = new SvelteSet<string>();
|
|
366
452
|
for (const d of data as any[]) {
|
|
367
|
-
const fxVal = resolveAcc(fxAcc, d);
|
|
368
|
-
const fyVal = resolveAcc(fyAcc, d);
|
|
369
|
-
const
|
|
453
|
+
const fxVal = fxAcc != null ? resolveAcc(fxAcc, d) : undefined;
|
|
454
|
+
const fyVal = fyAcc != null ? resolveAcc(fyAcc, d) : undefined;
|
|
455
|
+
const zVal = isZGrouped ? resolveAcc(zGroupAcc, d) : undefined;
|
|
456
|
+
const key = `${fxVal}\0${fyVal}\0${zVal}`;
|
|
370
457
|
if (!seen.has(key)) {
|
|
371
458
|
seen.add(key);
|
|
372
459
|
records.push({
|
|
@@ -375,7 +462,8 @@
|
|
|
375
462
|
[Y1_VAL]: ext.y1,
|
|
376
463
|
[Y2_VAL]: ext.y2,
|
|
377
464
|
[FX_VAL]: fxVal,
|
|
378
|
-
[FY_VAL]: fyVal
|
|
465
|
+
[FY_VAL]: fyVal,
|
|
466
|
+
[Z_VAL]: zVal
|
|
379
467
|
});
|
|
380
468
|
}
|
|
381
469
|
}
|
|
@@ -399,6 +487,7 @@
|
|
|
399
487
|
[RAW_VALUE]: geom.value,
|
|
400
488
|
[FX_VAL]: geom.fxVal,
|
|
401
489
|
[FY_VAL]: geom.fyVal,
|
|
490
|
+
[Z_VAL]: geom.zVal,
|
|
402
491
|
[GEOM]: geom
|
|
403
492
|
});
|
|
404
493
|
}
|
|
@@ -407,13 +496,24 @@
|
|
|
407
496
|
return records as DataRecord[];
|
|
408
497
|
});
|
|
409
498
|
|
|
410
|
-
const markChannels = $derived(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
499
|
+
const markChannels = $derived.by(() => {
|
|
500
|
+
const base = ['x1', 'x2', 'y1', 'y2'] as const;
|
|
501
|
+
if (markUsesColorScale || fillIsAccessor) return [...base, 'fill'] as const;
|
|
502
|
+
if (strokeIsAccessor) return [...base, 'stroke'] as const;
|
|
503
|
+
return base;
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
// fill channel accessor:
|
|
507
|
+
// density color scale (fill or stroke="density") → RAW_VALUE (registers domain)
|
|
508
|
+
// fill accessor (z-grouping) → Z_VAL
|
|
509
|
+
// else → undefined
|
|
510
|
+
const markFill = $derived(
|
|
511
|
+
markUsesColorScale ? (RAW_VALUE as any) : fillIsAccessor ? (Z_VAL as any) : undefined
|
|
414
512
|
);
|
|
415
513
|
|
|
416
|
-
|
|
514
|
+
// stroke channel accessor: only set when stroke is a z-group accessor
|
|
515
|
+
const markStroke = $derived(strokeIsAccessor ? (Z_VAL as any) : undefined);
|
|
516
|
+
|
|
417
517
|
const markFx = $derived(fxAcc != null ? FX_VAL : undefined);
|
|
418
518
|
const markFy = $derived(fyAcc != null ? FY_VAL : undefined);
|
|
419
519
|
|
|
@@ -423,11 +523,16 @@
|
|
|
423
523
|
y1: Y1_VAL as any,
|
|
424
524
|
y2: Y2_VAL as any,
|
|
425
525
|
fill: markFill as any,
|
|
526
|
+
stroke: markStroke as any,
|
|
426
527
|
fx: markFx as any,
|
|
427
528
|
fy: markFy as any,
|
|
428
529
|
...(typeof xAcc === 'string' && { [ORIGINAL_NAME_KEYS.x]: xAcc }),
|
|
429
530
|
...(typeof yAcc === 'string' && { [ORIGINAL_NAME_KEYS.y]: yAcc }),
|
|
430
|
-
...(markUsesColorScale && { [ORIGINAL_NAME_KEYS.fill]: 'Density' })
|
|
531
|
+
...(markUsesColorScale && { [ORIGINAL_NAME_KEYS.fill]: 'Density' }),
|
|
532
|
+
...(fillIsAccessor &&
|
|
533
|
+
typeof rawFill === 'string' && { [ORIGINAL_NAME_KEYS.fill]: rawFill }),
|
|
534
|
+
...(strokeIsAccessor &&
|
|
535
|
+
typeof rawStroke === 'string' && { [ORIGINAL_NAME_KEYS.stroke]: rawStroke })
|
|
431
536
|
});
|
|
432
537
|
|
|
433
538
|
const path = geoPath();
|
|
@@ -456,6 +561,8 @@
|
|
|
456
561
|
className={className || undefined}
|
|
457
562
|
ariaLabel="density"
|
|
458
563
|
{canvas}
|
|
459
|
-
{plot}
|
|
564
|
+
{plot}
|
|
565
|
+
usePerPathFill={fillIsAccessor}
|
|
566
|
+
usePerPathStroke={strokeIsAccessor} />
|
|
460
567
|
{/snippet}
|
|
461
568
|
</Mark>
|
|
@@ -9,6 +9,13 @@ declare function $$render<Datum extends DataRecord>(): {
|
|
|
9
9
|
y?: ChannelAccessor<Datum>;
|
|
10
10
|
/** Optional weight channel; defaults to 1 for each point. */
|
|
11
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>;
|
|
12
19
|
/**
|
|
13
20
|
* Gaussian kernel bandwidth in screen pixels (default 20).
|
|
14
21
|
* Larger values produce smoother, more blurred density estimates.
|
|
@@ -25,14 +32,22 @@ declare function $$render<Datum extends DataRecord>(): {
|
|
|
25
32
|
/**
|
|
26
33
|
* Fill color for density bands. Use `"density"` to map each band's
|
|
27
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.
|
|
28
39
|
*/
|
|
29
|
-
fill?: string;
|
|
40
|
+
fill?: ChannelAccessor<Datum> | string;
|
|
30
41
|
/**
|
|
31
42
|
* Stroke color for density isolines. Use `"density"` to map each
|
|
32
43
|
* isoline's estimated density through the plot's color scale.
|
|
33
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.
|
|
34
49
|
*/
|
|
35
|
-
stroke?: string;
|
|
50
|
+
stroke?: ChannelAccessor<Datum> | string;
|
|
36
51
|
strokeWidth?: number;
|
|
37
52
|
strokeOpacity?: number;
|
|
38
53
|
fillOpacity?: number;
|
|
@@ -80,6 +95,12 @@ interface $$IsomorphicComponent {
|
|
|
80
95
|
* density through the plot's color scale. Defaults: `fill="none"`,
|
|
81
96
|
* `stroke="currentColor"`.
|
|
82
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
|
+
*
|
|
83
104
|
* Supports faceting via `fx`/`fy`.
|
|
84
105
|
*/
|
|
85
106
|
declare const Density: $$IsomorphicComponent;
|
package/dist/marks/Link.svelte
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
strokeOpacity,
|
|
20
20
|
fillOpacity,
|
|
21
21
|
opacity,
|
|
22
|
-
strokeMiterlimit
|
|
22
|
+
strokeMiterlimit,
|
|
23
|
+
usePerPathFill = false,
|
|
24
|
+
usePerPathStroke = false
|
|
23
25
|
}: {
|
|
24
26
|
scaledData: ScaledDataRecord[];
|
|
25
27
|
path: GeoPath;
|
|
@@ -38,6 +40,8 @@
|
|
|
38
40
|
fillOpacity?: number;
|
|
39
41
|
opacity?: number;
|
|
40
42
|
strokeMiterlimit?: number;
|
|
43
|
+
usePerPathFill?: boolean;
|
|
44
|
+
usePerPathStroke?: boolean;
|
|
41
45
|
} = $props();
|
|
42
46
|
|
|
43
47
|
const plot = usePlot();
|
|
@@ -86,8 +90,16 @@
|
|
|
86
90
|
if (!geom?.coordinates?.length) continue;
|
|
87
91
|
|
|
88
92
|
const thresholdValue = (d.datum[RAW_VALUE as any] as number) ?? 0;
|
|
89
|
-
const fillColor = resolveCanvasColor(
|
|
90
|
-
|
|
93
|
+
const fillColor = resolveCanvasColor(
|
|
94
|
+
usePerPathFill && d.fill
|
|
95
|
+
? (d.fill as string)
|
|
96
|
+
: resolveColorProp(fill, thresholdValue)
|
|
97
|
+
);
|
|
98
|
+
const strokeColor = resolveCanvasColor(
|
|
99
|
+
usePerPathStroke && d.stroke
|
|
100
|
+
? (d.stroke as string)
|
|
101
|
+
: resolveColorProp(stroke, thresholdValue)
|
|
102
|
+
);
|
|
91
103
|
|
|
92
104
|
context.beginPath();
|
|
93
105
|
path(geom);
|
|
@@ -18,6 +18,8 @@ type $$ComponentProps = {
|
|
|
18
18
|
fillOpacity?: number;
|
|
19
19
|
opacity?: number;
|
|
20
20
|
strokeMiterlimit?: number;
|
|
21
|
+
usePerPathFill?: boolean;
|
|
22
|
+
usePerPathStroke?: boolean;
|
|
21
23
|
};
|
|
22
24
|
declare const GeoPathCanvas: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
23
25
|
type GeoPathCanvas = ReturnType<typeof GeoPathCanvas>;
|
|
@@ -26,7 +26,9 @@
|
|
|
26
26
|
className,
|
|
27
27
|
ariaLabel,
|
|
28
28
|
canvas = false,
|
|
29
|
-
plot
|
|
29
|
+
plot,
|
|
30
|
+
usePerPathFill = false,
|
|
31
|
+
usePerPathStroke = false
|
|
30
32
|
}: {
|
|
31
33
|
scaledData: ScaledDataRecord[];
|
|
32
34
|
/** d3 geoPath renderer (must NOT have a canvas context set). */
|
|
@@ -52,6 +54,17 @@
|
|
|
52
54
|
/** Render using a canvas element instead of SVG paths. */
|
|
53
55
|
canvas?: boolean;
|
|
54
56
|
plot: PlotState;
|
|
57
|
+
/**
|
|
58
|
+
* When true, per-path fill color is read from `d.fill` in scaledData
|
|
59
|
+
* (z-group coloring). Must be set explicitly; avoids accidental fill
|
|
60
|
+
* when fill channel is only registered for color-scale domain purposes.
|
|
61
|
+
*/
|
|
62
|
+
usePerPathFill?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* When true, per-path stroke color is read from `d.stroke` in scaledData
|
|
65
|
+
* (z-group coloring).
|
|
66
|
+
*/
|
|
67
|
+
usePerPathStroke?: boolean;
|
|
55
68
|
} = $props();
|
|
56
69
|
|
|
57
70
|
/** Resolve a fill/stroke prop that may be the colorKeyword. */
|
|
@@ -63,10 +76,14 @@
|
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
/** Build the inline style string for a single contour/density path. */
|
|
66
|
-
function buildStyle(value: number): string {
|
|
79
|
+
function buildStyle(d: ScaledDataRecord, value: number): string {
|
|
67
80
|
const parts: string[] = [];
|
|
68
|
-
parts.push(
|
|
69
|
-
|
|
81
|
+
parts.push(
|
|
82
|
+
`fill:${usePerPathFill && d.fill ? (d.fill as string) : resolveColor(fill, value)}`
|
|
83
|
+
);
|
|
84
|
+
parts.push(
|
|
85
|
+
`stroke:${usePerPathStroke && d.stroke ? (d.stroke as string) : resolveColor(stroke, value)}`
|
|
86
|
+
);
|
|
70
87
|
if (strokeWidth != null) parts.push(`stroke-width:${strokeWidth}`);
|
|
71
88
|
if (strokeOpacity != null) parts.push(`stroke-opacity:${strokeOpacity}`);
|
|
72
89
|
if (fillOpacity != null) parts.push(`fill-opacity:${fillOpacity}`);
|
|
@@ -88,7 +105,9 @@
|
|
|
88
105
|
{strokeOpacity}
|
|
89
106
|
{fillOpacity}
|
|
90
107
|
{opacity}
|
|
91
|
-
{strokeMiterlimit}
|
|
108
|
+
{strokeMiterlimit}
|
|
109
|
+
{usePerPathFill}
|
|
110
|
+
{usePerPathStroke} />
|
|
92
111
|
{:else}
|
|
93
112
|
<g clip-path={clipPath} class={className || null} aria-label={ariaLabel}>
|
|
94
113
|
{#each scaledData as d, i (i)}
|
|
@@ -96,7 +115,7 @@
|
|
|
96
115
|
{#if geom?.coordinates?.length}
|
|
97
116
|
<path
|
|
98
117
|
d={path(geom)}
|
|
99
|
-
style={buildStyle((d.datum[RAW_VALUE as any] as number) ?? 0)} />
|
|
118
|
+
style={buildStyle(d, (d.datum[RAW_VALUE as any] as number) ?? 0)} />
|
|
100
119
|
{/if}
|
|
101
120
|
{/each}
|
|
102
121
|
</g>
|
|
@@ -25,6 +25,17 @@ type $$ComponentProps = {
|
|
|
25
25
|
/** Render using a canvas element instead of SVG paths. */
|
|
26
26
|
canvas?: boolean;
|
|
27
27
|
plot: PlotState;
|
|
28
|
+
/**
|
|
29
|
+
* When true, per-path fill color is read from `d.fill` in scaledData
|
|
30
|
+
* (z-group coloring). Must be set explicitly; avoids accidental fill
|
|
31
|
+
* when fill channel is only registered for color-scale domain purposes.
|
|
32
|
+
*/
|
|
33
|
+
usePerPathFill?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* When true, per-path stroke color is read from `d.stroke` in scaledData
|
|
36
|
+
* (z-group coloring).
|
|
37
|
+
*/
|
|
38
|
+
usePerPathStroke?: boolean;
|
|
28
39
|
};
|
|
29
40
|
/**
|
|
30
41
|
* Renders GeoJSON geometries as SVG <path> elements (or via canvas when
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
regressionLoess
|
|
44
44
|
} from '../../regression/index.js';
|
|
45
45
|
import { resolveChannel } from '../../helpers/resolve.js';
|
|
46
|
-
import { isTemporalScale } from '../../helpers/typeChecks.js';
|
|
47
46
|
import { confidenceInterval } from '../../helpers/math.js';
|
|
48
47
|
import callWithProps from '../../helpers/callWithProps.js';
|
|
49
48
|
import type { DataRecord, FacetContext, RawValue } from '../../types/index.js';
|
|
@@ -93,8 +92,12 @@
|
|
|
93
92
|
}
|
|
94
93
|
|
|
95
94
|
// Convert generated points back to Date for time scales so downstream marks render correctly.
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
// Takes a boolean instead of the scale type to avoid a circular dependency: if we used
|
|
96
|
+
// plot.scales[independent].type, the Line mark would register numeric __x values during the
|
|
97
|
+
// first render pass (before the scale type is resolved), causing scale inference to see a mix
|
|
98
|
+
// of Dates and numbers and fall back to 'linear' permanently.
|
|
99
|
+
function toOutputX(value: number, isTemporal: boolean): RawValue {
|
|
100
|
+
return isTemporal ? new Date(value) : value;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
function makeTicks(domain: [number, number], count = 40): number[] {
|
|
@@ -129,6 +132,16 @@
|
|
|
129
132
|
|
|
130
133
|
const regressionFn = $derived(maybeRegression(type));
|
|
131
134
|
|
|
135
|
+
// Detect temporality from the raw data rather than from the computed scale type to avoid a
|
|
136
|
+
// circular dependency that would cause the scale type to be permanently inferred as 'linear'.
|
|
137
|
+
// Scan for the first row that resolves to a non-null independent value — checking only
|
|
138
|
+
// filteredData[0] would give false negatives when the leading row has a missing/null value.
|
|
139
|
+
const independentIsDate = $derived(
|
|
140
|
+
filteredData.some(
|
|
141
|
+
(d) => resolveChannel(independent, d, options as any) instanceof Date
|
|
142
|
+
)
|
|
143
|
+
);
|
|
144
|
+
|
|
132
145
|
// Build a clean numeric input set for regression fitting, dropping invalid rows early.
|
|
133
146
|
const regressionInput = $derived(
|
|
134
147
|
filteredData
|
|
@@ -193,18 +206,18 @@
|
|
|
193
206
|
// Prefer batch prediction when supported, then per-point predict, then raw curve output.
|
|
194
207
|
if (typeof regression.predictMany === 'function') {
|
|
195
208
|
return regression.predictMany(regrPoints).map((__y, i) => ({
|
|
196
|
-
__x: toOutputX(regrPoints[i],
|
|
209
|
+
__x: toOutputX(regrPoints[i], independentIsDate),
|
|
197
210
|
__y
|
|
198
211
|
}));
|
|
199
212
|
}
|
|
200
213
|
if (typeof regression.predict === 'function') {
|
|
201
214
|
return regrPoints.map((point) => ({
|
|
202
|
-
__x: toOutputX(point,
|
|
215
|
+
__x: toOutputX(point, independentIsDate),
|
|
203
216
|
__y: regression.predict!(point)
|
|
204
217
|
}));
|
|
205
218
|
}
|
|
206
219
|
return regression.map(([__x, __y]) => ({
|
|
207
|
-
__x: toOutputX(__x,
|
|
220
|
+
__x: toOutputX(__x, independentIsDate),
|
|
208
221
|
__y
|
|
209
222
|
}));
|
|
210
223
|
});
|
|
@@ -230,7 +243,7 @@
|
|
|
230
243
|
return regrPoints.map((x) => {
|
|
231
244
|
const { x: __x, left, right } = confBandGen(x);
|
|
232
245
|
return {
|
|
233
|
-
__x: toOutputX(__x,
|
|
246
|
+
__x: toOutputX(__x, independentIsDate),
|
|
234
247
|
__y1: left,
|
|
235
248
|
__y2: right
|
|
236
249
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelteplot",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1-pr-549.1",
|
|
4
4
|
"description": "A Svelte-native data visualization framework based on the layered grammar of graphics principles.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"svelte",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"d3-scale-chromatic": "^3.1.0",
|
|
95
95
|
"d3-shape": "^3.2.0",
|
|
96
96
|
"d3-time": "^3.1.0",
|
|
97
|
-
"es-toolkit": "^1.
|
|
97
|
+
"es-toolkit": "^1.45.1",
|
|
98
98
|
"fast-equals": "^6.0.0",
|
|
99
99
|
"interval-tree-1d": "^1.0.4",
|
|
100
100
|
"merge-deep": "^3.0.3"
|
|
@@ -105,13 +105,13 @@
|
|
|
105
105
|
"@shikijs/twoslash": "^3.22.0",
|
|
106
106
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
107
107
|
"@sveltejs/adapter-static": "^3.0.10",
|
|
108
|
-
"@sveltejs/enhanced-img": "^0.10.
|
|
108
|
+
"@sveltejs/enhanced-img": "^0.10.4",
|
|
109
109
|
"@sveltejs/eslint-config": "^8.3.5",
|
|
110
|
-
"@sveltejs/kit": "^2.
|
|
110
|
+
"@sveltejs/kit": "^2.57.1",
|
|
111
111
|
"@sveltejs/package": "^2.5.7",
|
|
112
112
|
"@sveltejs/vite-plugin-svelte": "6.2.4",
|
|
113
|
-
"@sveltepress/twoslash": "^1.3.
|
|
114
|
-
"@sveltepress/vite": "^1.3.
|
|
113
|
+
"@sveltepress/twoslash": "^1.3.10",
|
|
114
|
+
"@sveltepress/vite": "^1.3.10",
|
|
115
115
|
"@testing-library/svelte": "^5.3.1",
|
|
116
116
|
"@testing-library/user-event": "^14.6.1",
|
|
117
117
|
"@types/d3-array": "^3.2.2",
|
|
@@ -128,11 +128,12 @@
|
|
|
128
128
|
"@types/d3-scale-chromatic": "^3.1.0",
|
|
129
129
|
"@types/d3-shape": "^3.1.8",
|
|
130
130
|
"@types/geojson": "^7946.0.16",
|
|
131
|
+
"@types/node": "^25.6.0",
|
|
131
132
|
"@types/topojson": "^3.2.6",
|
|
132
133
|
"@types/topojson-client": "^3.1.5",
|
|
133
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
134
|
-
"@typescript-eslint/parser": "^8.
|
|
135
|
-
"@unocss/extractor-svelte": "^66.6.
|
|
134
|
+
"@typescript-eslint/eslint-plugin": "^8.58.1",
|
|
135
|
+
"@typescript-eslint/parser": "^8.58.1",
|
|
136
|
+
"@unocss/extractor-svelte": "^66.6.8",
|
|
136
137
|
"@vite-pwa/sveltekit": "^1.1.0",
|
|
137
138
|
"csstype": "^3.2.3",
|
|
138
139
|
"d3-dsv": "^3.0.1",
|
|
@@ -142,50 +143,50 @@
|
|
|
142
143
|
"eslint-config-prettier": "^10.1.8",
|
|
143
144
|
"eslint-plugin-package-json": "^0.88.3",
|
|
144
145
|
"eslint-plugin-regexp": "^2.10.0",
|
|
145
|
-
"eslint-plugin-svelte": "3.
|
|
146
|
+
"eslint-plugin-svelte": "3.17.0",
|
|
146
147
|
"jqmath": "^0.4.9",
|
|
147
148
|
"jsdom": "^27.4.0",
|
|
148
149
|
"log-update": "^7.1.0",
|
|
149
|
-
"lru-cache": "^11.
|
|
150
|
+
"lru-cache": "^11.3.3",
|
|
150
151
|
"magic-string": "^0.30.21",
|
|
151
|
-
"mdast-util-from-markdown": "^2.0.
|
|
152
|
+
"mdast-util-from-markdown": "^2.0.3",
|
|
152
153
|
"mdast-util-gfm": "^3.1.0",
|
|
153
|
-
"oxlint": "^1.
|
|
154
|
+
"oxlint": "^1.59.0",
|
|
154
155
|
"oxlint-tsgolint": "^0.14.0",
|
|
155
156
|
"pixelmatch": "^7.1.0",
|
|
156
157
|
"pngjs": "^7.0.0",
|
|
157
|
-
"prettier": "^3.8.
|
|
158
|
-
"prettier-plugin-svelte": "^3.5.
|
|
159
|
-
"puppeteer": "^24.
|
|
158
|
+
"prettier": "^3.8.2",
|
|
159
|
+
"prettier-plugin-svelte": "^3.5.1",
|
|
160
|
+
"puppeteer": "^24.40.0",
|
|
160
161
|
"remark-code-extra": "^1.0.1",
|
|
161
162
|
"remark-code-frontmatter": "^1.0.0",
|
|
162
163
|
"remark-math": "^6.0.0",
|
|
163
164
|
"resize-observer-polyfill": "^1.5.1",
|
|
164
|
-
"sass": "^1.
|
|
165
|
+
"sass": "^1.99.0",
|
|
165
166
|
"shiki": "^3.22.0",
|
|
166
167
|
"svelte": "5",
|
|
167
|
-
"svelte-check": "^4.4.
|
|
168
|
-
"svelte-eslint-parser": "1.
|
|
168
|
+
"svelte-check": "^4.4.6",
|
|
169
|
+
"svelte-eslint-parser": "1.6.0",
|
|
169
170
|
"svelte-highlight": "^7.9.0",
|
|
170
171
|
"svg-path-parser": "^1.1.0",
|
|
171
|
-
"temml": "^0.13.
|
|
172
|
+
"temml": "^0.13.2",
|
|
172
173
|
"topojson-client": "^3.1.0",
|
|
173
174
|
"ts-essentials": "^10.1.1",
|
|
174
175
|
"tslib": "^2.8.1",
|
|
175
|
-
"typedoc": "^0.28.
|
|
176
|
-
"typedoc-plugin-markdown": "^4.
|
|
176
|
+
"typedoc": "^0.28.18",
|
|
177
|
+
"typedoc-plugin-markdown": "^4.11.0",
|
|
177
178
|
"typescript": "^5.9.3",
|
|
178
179
|
"uid": "^2.0.2",
|
|
179
180
|
"unist-util-visit": "^5.1.0",
|
|
180
|
-
"unocss": "^66.6.
|
|
181
|
+
"unocss": "^66.6.8",
|
|
181
182
|
"vite": "^7.3.1",
|
|
182
|
-
"vitest": "^4.
|
|
183
|
+
"vitest": "^4.1.4",
|
|
183
184
|
"vitest-matchmedia-mock": "^2.0.3",
|
|
184
|
-
"wx-svelte-grid": "^2.
|
|
185
|
+
"wx-svelte-grid": "^2.6.1",
|
|
185
186
|
"yoctocolors": "^2.1.2"
|
|
186
187
|
},
|
|
187
188
|
"peerDependencies": {
|
|
188
189
|
"svelte": "^5.43.0"
|
|
189
190
|
},
|
|
190
|
-
"packageManager": "pnpm@10.
|
|
191
|
+
"packageManager": "pnpm@10.33.0"
|
|
191
192
|
}
|