svelteplot 0.12.0-pr-532.3 → 0.12.0-pr-532.5
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.
|
@@ -55,6 +55,8 @@
|
|
|
55
55
|
strokeMiterlimit?: number;
|
|
56
56
|
clipPath?: string;
|
|
57
57
|
class?: string;
|
|
58
|
+
/** Render using a canvas element instead of SVG paths. */
|
|
59
|
+
canvas?: boolean;
|
|
58
60
|
/** the horizontal facet channel */
|
|
59
61
|
fx?: ChannelAccessor<Datum>;
|
|
60
62
|
/** the vertical facet channel */
|
|
@@ -68,6 +70,7 @@
|
|
|
68
70
|
MarkType,
|
|
69
71
|
RawValue
|
|
70
72
|
} from '../types/index.js';
|
|
73
|
+
import DensityCanvas from './helpers/DensityCanvas.svelte';
|
|
71
74
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
72
75
|
import { contourDensity } from 'd3-contour';
|
|
73
76
|
import { geoPath } from 'd3-geo';
|
|
@@ -117,6 +120,7 @@
|
|
|
117
120
|
strokeMiterlimit = 1,
|
|
118
121
|
clipPath,
|
|
119
122
|
class: className = '',
|
|
123
|
+
canvas = false,
|
|
120
124
|
fx: fxAcc,
|
|
121
125
|
fy: fyAcc,
|
|
122
126
|
...options
|
|
@@ -295,28 +299,45 @@
|
|
|
295
299
|
*/
|
|
296
300
|
const extent = $derived.by(() => {
|
|
297
301
|
if (!data?.length) return null;
|
|
298
|
-
let xMin
|
|
299
|
-
xMax
|
|
300
|
-
yMin
|
|
301
|
-
yMax
|
|
302
|
+
let xMin = Infinity,
|
|
303
|
+
xMax = -Infinity,
|
|
304
|
+
yMin = Infinity,
|
|
305
|
+
yMax = -Infinity;
|
|
306
|
+
let xUsesDate = false,
|
|
307
|
+
yUsesDate = false;
|
|
302
308
|
for (const d of data as any[]) {
|
|
303
309
|
const xv = resolveAcc(xAcc, d);
|
|
304
310
|
const yv = resolveAcc(yAcc, d);
|
|
305
|
-
if (
|
|
311
|
+
if (xv instanceof Date) {
|
|
312
|
+
xUsesDate = true;
|
|
313
|
+
const ms = xv.getTime();
|
|
314
|
+
if (isFinite(ms)) {
|
|
315
|
+
if (ms < xMin) xMin = ms;
|
|
316
|
+
if (ms > xMax) xMax = ms;
|
|
317
|
+
}
|
|
318
|
+
} else if (typeof xv === 'number' && isFinite(xv)) {
|
|
306
319
|
if (xv < xMin) xMin = xv;
|
|
307
320
|
if (xv > xMax) xMax = xv;
|
|
308
321
|
}
|
|
309
|
-
if (
|
|
322
|
+
if (yv instanceof Date) {
|
|
323
|
+
yUsesDate = true;
|
|
324
|
+
const ms = yv.getTime();
|
|
325
|
+
if (isFinite(ms)) {
|
|
326
|
+
if (ms < yMin) yMin = ms;
|
|
327
|
+
if (ms > yMax) yMax = ms;
|
|
328
|
+
}
|
|
329
|
+
} else if (typeof yv === 'number' && isFinite(yv)) {
|
|
310
330
|
if (yv < yMin) yMin = yv;
|
|
311
331
|
if (yv > yMax) yMax = yv;
|
|
312
332
|
}
|
|
313
333
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
:
|
|
334
|
+
if (!isFinite(xMin) || !isFinite(xMax) || !isFinite(yMin) || !isFinite(yMax)) return null;
|
|
335
|
+
return {
|
|
336
|
+
x1: xUsesDate ? new Date(xMin) : xMin,
|
|
337
|
+
x2: xUsesDate ? new Date(xMax) : xMax,
|
|
338
|
+
y1: yUsesDate ? new Date(yMin) : yMin,
|
|
339
|
+
y2: yUsesDate ? new Date(yMax) : yMax
|
|
340
|
+
};
|
|
320
341
|
});
|
|
321
342
|
|
|
322
343
|
/**
|
|
@@ -334,7 +355,6 @@
|
|
|
334
355
|
const markData = $derived.by((): DataRecord[] => {
|
|
335
356
|
const ext = extent;
|
|
336
357
|
const records: any[] = [];
|
|
337
|
-
|
|
338
358
|
// Bootstrap extent record(s) so x/y scales are available for density
|
|
339
359
|
// computation on the first render pass. When faceted, emit one record
|
|
340
360
|
// per unique (fxVal, fyVal) combination so no record carries an
|
|
@@ -442,13 +462,27 @@
|
|
|
442
462
|
{...markChannelProps}>
|
|
443
463
|
{#snippet children({ scaledData }: { scaledData: ScaledDataRecord[] })}
|
|
444
464
|
<g clip-path={clipPath} class={className || null} aria-label="density">
|
|
445
|
-
{#
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
465
|
+
{#if canvas}
|
|
466
|
+
<DensityCanvas
|
|
467
|
+
{scaledData}
|
|
468
|
+
{path}
|
|
469
|
+
geomKey={GEOM}
|
|
470
|
+
{fill}
|
|
471
|
+
stroke={effectiveStroke}
|
|
472
|
+
{strokeWidth}
|
|
473
|
+
{strokeOpacity}
|
|
474
|
+
{fillOpacity}
|
|
475
|
+
{opacity}
|
|
476
|
+
{strokeMiterlimit} />
|
|
477
|
+
{:else}
|
|
478
|
+
{#each scaledData as d, i (i)}
|
|
479
|
+
{#if d.datum[GEOM] && (d.datum[GEOM] as DensityGeometry).coordinates.length > 0}
|
|
480
|
+
<path
|
|
481
|
+
d={path(d.datum[GEOM] as any)}
|
|
482
|
+
style={densityStyle(d.datum[RAW_VALUE] as number)} />
|
|
483
|
+
{/if}
|
|
484
|
+
{/each}
|
|
485
|
+
{/if}
|
|
452
486
|
</g>
|
|
453
487
|
{/snippet}
|
|
454
488
|
</Mark>
|
|
@@ -40,6 +40,8 @@ declare function $$render<Datum extends DataRecord>(): {
|
|
|
40
40
|
strokeMiterlimit?: number;
|
|
41
41
|
clipPath?: string;
|
|
42
42
|
class?: string;
|
|
43
|
+
/** Render using a canvas element instead of SVG paths. */
|
|
44
|
+
canvas?: boolean;
|
|
43
45
|
/** the horizontal facet channel */
|
|
44
46
|
fx?: ChannelAccessor<Datum>;
|
|
45
47
|
/** the vertical facet channel */
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ScaledDataRecord } from '../../types/index.js';
|
|
3
|
+
import type { GeoPath } from 'd3-geo';
|
|
4
|
+
import type { Attachment } from 'svelte/attachments';
|
|
5
|
+
import { devicePixelRatio } from 'svelte/reactivity/window';
|
|
6
|
+
import { CSS_VAR } from '../../constants.js';
|
|
7
|
+
import CanvasLayer from './CanvasLayer.svelte';
|
|
8
|
+
import { usePlot } from '../../hooks/usePlot.svelte.js';
|
|
9
|
+
import { RAW_VALUE } from '../../transforms/recordize.js';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
scaledData,
|
|
13
|
+
path,
|
|
14
|
+
geomKey,
|
|
15
|
+
fill,
|
|
16
|
+
stroke,
|
|
17
|
+
strokeWidth,
|
|
18
|
+
strokeOpacity,
|
|
19
|
+
fillOpacity,
|
|
20
|
+
opacity,
|
|
21
|
+
strokeMiterlimit
|
|
22
|
+
}: {
|
|
23
|
+
scaledData: ScaledDataRecord[];
|
|
24
|
+
path: GeoPath;
|
|
25
|
+
/** Symbol key used to retrieve the DensityGeometry from each datum. */
|
|
26
|
+
geomKey: symbol;
|
|
27
|
+
fill: string;
|
|
28
|
+
stroke: string;
|
|
29
|
+
strokeWidth?: number;
|
|
30
|
+
strokeOpacity?: number;
|
|
31
|
+
fillOpacity?: number;
|
|
32
|
+
opacity?: number;
|
|
33
|
+
strokeMiterlimit?: number;
|
|
34
|
+
} = $props();
|
|
35
|
+
|
|
36
|
+
const plot = usePlot();
|
|
37
|
+
|
|
38
|
+
/** Resolve a fill/stroke string that may be the "density" keyword. */
|
|
39
|
+
function resolveColorProp(prop: string, densityValue: number): string {
|
|
40
|
+
if (/^density$/i.test(prop)) {
|
|
41
|
+
return (plot.scales.color?.fn(densityValue) as string) ?? 'currentColor';
|
|
42
|
+
}
|
|
43
|
+
return prop;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const render: Attachment = (canvasEl: Element) => {
|
|
47
|
+
const canvas = canvasEl as HTMLCanvasElement;
|
|
48
|
+
const context = canvas.getContext('2d');
|
|
49
|
+
|
|
50
|
+
$effect(() => {
|
|
51
|
+
if (!context) return;
|
|
52
|
+
|
|
53
|
+
path.context(context);
|
|
54
|
+
context.resetTransform();
|
|
55
|
+
context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
|
|
56
|
+
|
|
57
|
+
let currentColor: string | undefined;
|
|
58
|
+
|
|
59
|
+
const resolveCanvasColor = (color: string): string => {
|
|
60
|
+
if (color.toLowerCase() === 'currentcolor') {
|
|
61
|
+
return (
|
|
62
|
+
currentColor ||
|
|
63
|
+
(currentColor = getComputedStyle(
|
|
64
|
+
canvas.parentElement?.parentElement ?? canvas
|
|
65
|
+
).getPropertyValue('color'))
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (CSS_VAR.test(color)) {
|
|
69
|
+
return getComputedStyle(canvas).getPropertyValue(color.slice(4, -1));
|
|
70
|
+
}
|
|
71
|
+
return color;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const globalOpacity = opacity ?? 1;
|
|
75
|
+
if (strokeMiterlimit != null) context.miterLimit = strokeMiterlimit;
|
|
76
|
+
|
|
77
|
+
for (const d of scaledData) {
|
|
78
|
+
const geom = d.datum[geomKey as any] as any;
|
|
79
|
+
if (!geom?.coordinates?.length) continue;
|
|
80
|
+
|
|
81
|
+
const densityValue = (d.datum[RAW_VALUE as any] as number) ?? 0;
|
|
82
|
+
const fillColor = resolveCanvasColor(resolveColorProp(fill, densityValue));
|
|
83
|
+
const strokeColor = resolveCanvasColor(resolveColorProp(stroke, densityValue));
|
|
84
|
+
|
|
85
|
+
context.beginPath();
|
|
86
|
+
path(geom);
|
|
87
|
+
context.closePath();
|
|
88
|
+
|
|
89
|
+
if (fillColor && fillColor !== 'none') {
|
|
90
|
+
context.fillStyle = fillColor;
|
|
91
|
+
context.globalAlpha = globalOpacity * (fillOpacity ?? 1);
|
|
92
|
+
context.fill();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (strokeColor && strokeColor !== 'none') {
|
|
96
|
+
context.strokeStyle = strokeColor;
|
|
97
|
+
context.lineWidth = strokeWidth ?? 1;
|
|
98
|
+
context.globalAlpha = globalOpacity * (strokeOpacity ?? 1);
|
|
99
|
+
context.stroke();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Reset path context in case we switch back to SVG rendering.
|
|
104
|
+
path.context(null);
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
context.clearRect(
|
|
108
|
+
0,
|
|
109
|
+
0,
|
|
110
|
+
plot.width * (devicePixelRatio.current ?? 1),
|
|
111
|
+
plot.height * (devicePixelRatio.current ?? 1)
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<CanvasLayer {@attach render} />
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ScaledDataRecord } from '../../types/index.js';
|
|
2
|
+
import type { GeoPath } from 'd3-geo';
|
|
3
|
+
type $$ComponentProps = {
|
|
4
|
+
scaledData: ScaledDataRecord[];
|
|
5
|
+
path: GeoPath;
|
|
6
|
+
/** Symbol key used to retrieve the DensityGeometry from each datum. */
|
|
7
|
+
geomKey: symbol;
|
|
8
|
+
fill: string;
|
|
9
|
+
stroke: string;
|
|
10
|
+
strokeWidth?: number;
|
|
11
|
+
strokeOpacity?: number;
|
|
12
|
+
fillOpacity?: number;
|
|
13
|
+
opacity?: number;
|
|
14
|
+
strokeMiterlimit?: number;
|
|
15
|
+
};
|
|
16
|
+
declare const DensityCanvas: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
17
|
+
type DensityCanvas = ReturnType<typeof DensityCanvas>;
|
|
18
|
+
export default DensityCanvas;
|