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: number | Date = Infinity,
299
- xMax: number | Date = -Infinity,
300
- yMin: number | Date = Infinity,
301
- yMax: number | Date = -Infinity;
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 ((typeof xv === 'number' && isFinite(xv)) || xv instanceof Date) {
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 ((typeof yv === 'number' && isFinite(yv)) || yv instanceof Date) {
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
- return (Number.isFinite(xMin as number) || xMin instanceof Date) &&
315
- (Number.isFinite(xMax as number) || xMax instanceof Date) &&
316
- (Number.isFinite(yMin as number) || yMin instanceof Date) &&
317
- (Number.isFinite(yMax as number) || yMax instanceof Date)
318
- ? { x1: xMin, x2: xMax, y1: yMin, y2: yMax }
319
- : null;
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
- {#each scaledData as d, i (i)}
446
- {#if d.datum[GEOM] && (d.datum[GEOM] as DensityGeometry).coordinates.length > 0}
447
- <path
448
- d={path(d.datum[GEOM] as any)}
449
- style={densityStyle(d.datum[RAW_VALUE] as number)} />
450
- {/if}
451
- {/each}
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteplot",
3
- "version": "0.12.0-pr-532.3",
3
+ "version": "0.12.0-pr-532.5",
4
4
  "description": "A Svelte-native data visualization framework based on the layered grammar of graphics principles.",
5
5
  "keywords": [
6
6
  "svelte",