svelteplot 0.13.0 → 0.14.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 (32) hide show
  1. package/dist/helpers/group.d.ts +1 -1
  2. package/dist/helpers/group.js +3 -3
  3. package/dist/marks/ColorLegend.svelte +5 -1
  4. package/dist/marks/Contour.svelte +21 -30
  5. package/dist/marks/Contour.svelte.d.ts +2 -0
  6. package/dist/marks/DelaunayLink.svelte +127 -0
  7. package/dist/marks/DelaunayLink.svelte.d.ts +175 -0
  8. package/dist/marks/DelaunayMesh.svelte +102 -0
  9. package/dist/marks/DelaunayMesh.svelte.d.ts +172 -0
  10. package/dist/marks/Density.svelte +461 -0
  11. package/dist/marks/Density.svelte.d.ts +87 -0
  12. package/dist/marks/Hull.svelte +103 -0
  13. package/dist/marks/Hull.svelte.d.ts +175 -0
  14. package/dist/marks/Voronoi.svelte +118 -0
  15. package/dist/marks/Voronoi.svelte.d.ts +172 -0
  16. package/dist/marks/VoronoiMesh.svelte +109 -0
  17. package/dist/marks/VoronoiMesh.svelte.d.ts +172 -0
  18. package/dist/marks/helpers/DensityCanvas.svelte +118 -0
  19. package/dist/marks/helpers/DensityCanvas.svelte.d.ts +18 -0
  20. package/dist/marks/helpers/GeoPathCanvas.svelte +125 -0
  21. package/dist/marks/helpers/GeoPathCanvas.svelte.d.ts +24 -0
  22. package/dist/marks/helpers/GeoPathGroup.svelte +103 -0
  23. package/dist/marks/helpers/GeoPathGroup.svelte.d.ts +37 -0
  24. package/dist/marks/helpers/PathGroup.svelte +100 -0
  25. package/dist/marks/helpers/PathGroup.svelte.d.ts +16 -0
  26. package/dist/marks/helpers/PathItems.svelte +112 -0
  27. package/dist/marks/helpers/PathItems.svelte.d.ts +16 -0
  28. package/dist/marks/index.d.ts +7 -1
  29. package/dist/marks/index.js +7 -1
  30. package/dist/types/mark.d.ts +1 -1
  31. package/dist/types/plot.d.ts +25 -1
  32. package/package.json +1 -1
@@ -3,4 +3,4 @@ import type { Channels } from '../types/index.js';
3
3
  * Groups the data by the fx, fy and z channels and calls the reduce function
4
4
  * for each group. Returns the new channels to be added in the transform.
5
5
  */
6
- export declare function groupFacetsAndZ<T>(items: T[], channels: Channels<T>, reduce: (items: T[], groupProps?: Record<string, unknown>) => any): any;
6
+ export declare function groupFacetsAndZ<T>(items: T[], channels: Channels<T>, reduce: (items: T[], groupProps?: Record<string, unknown>) => any, implicitColorZ?: boolean): any;
@@ -4,15 +4,15 @@ import { groups as d3Groups } from 'd3-array';
4
4
  * Groups the data by the fx, fy and z channels and calls the reduce function
5
5
  * for each group. Returns the new channels to be added in the transform.
6
6
  */
7
- export function groupFacetsAndZ(items, channels, reduce) {
7
+ export function groupFacetsAndZ(items, channels, reduce, implicitColorZ = true) {
8
8
  const groupBy = ['fx', 'fy', 'z'].map((groupChannel) => {
9
9
  let groupByChannel = null;
10
10
  if (groupChannel === 'z') {
11
11
  if (channels.z)
12
12
  groupByChannel = 'z';
13
- else if (channels.fill)
13
+ else if (channels.fill && implicitColorZ)
14
14
  groupByChannel = 'fill';
15
- else if (channels.stroke)
15
+ else if (channels.stroke && implicitColorZ)
16
16
  groupByChannel = 'stroke';
17
17
  }
18
18
  else if (channels[groupChannel]) {
@@ -19,8 +19,12 @@
19
19
 
20
20
  const DEFAULTS = getPlotDefaults();
21
21
 
22
- const legendTitle = $derived(plot.options.color.label ?? plot.scales.color?.autoTitle);
23
22
  const scaleType = $derived(plot.scales.color.type);
23
+ const legendTitle = $derived(
24
+ scaleType === 'categorical'
25
+ ? (plot.options.color.label ?? '')
26
+ : (plot.options.color.label ?? plot.scales.color?.autoTitle)
27
+ );
24
28
  const tickFormat = $derived(
25
29
  typeof plot.options.color?.tickFormat === 'function'
26
30
  ? plot.options.color.tickFormat
@@ -117,6 +117,8 @@
117
117
  strokeMiterlimit?: number;
118
118
  clipPath?: string;
119
119
  class?: string;
120
+ /** Render using a canvas element instead of SVG paths. */
121
+ canvas?: boolean;
120
122
  /** the horizontal facet channel */
121
123
  fx?: ChannelAccessor<Datum>;
122
124
  /** the vertical facet channel */
@@ -136,6 +138,7 @@
136
138
  import { contours } from 'd3-contour';
137
139
  import { geoPath } from 'd3-geo';
138
140
  import Mark from '../Mark.svelte';
141
+ import GeoPathGroup from './helpers/GeoPathGroup.svelte';
139
142
  import { usePlot } from '../hooks/usePlot.svelte.js';
140
143
  import {
141
144
  interpolateNone,
@@ -195,6 +198,7 @@
195
198
  strokeMiterlimit = 1,
196
199
  clipPath,
197
200
  class: className = '',
201
+ canvas = false,
198
202
  ...options
199
203
  }: ContourMarkProps = $derived({ ...DEFAULTS, ...markProps });
200
204
 
@@ -491,27 +495,6 @@
491
495
  return tz;
492
496
  }
493
497
 
494
- /** Resolve a fill or stroke prop for a given contour level. */
495
- function resolveColor(prop: string | undefined, threshold: number): string {
496
- if (prop === 'value') {
497
- return (plot.scales.color?.fn(threshold) as string) ?? 'currentColor';
498
- }
499
- return prop ?? 'none';
500
- }
501
-
502
- /** Build the inline style string for a single contour path. */
503
- function contourStyle(threshold: number): string {
504
- const parts: string[] = [];
505
- parts.push(`fill:${resolveColor(fill, threshold)}`);
506
- parts.push(`stroke:${resolveColor(effectiveStroke, threshold)}`);
507
- if (strokeWidth != null) parts.push(`stroke-width:${strokeWidth}`);
508
- if (strokeOpacity != null) parts.push(`stroke-opacity:${strokeOpacity}`);
509
- if (fillOpacity != null) parts.push(`fill-opacity:${fillOpacity}`);
510
- if (opacity != null) parts.push(`opacity:${opacity}`);
511
- if (strokeMiterlimit != null) parts.push(`stroke-miterlimit:${strokeMiterlimit}`);
512
- return parts.join(';');
513
- }
514
-
515
498
  const path = geoPath();
516
499
 
517
500
  /**
@@ -680,14 +663,22 @@
680
663
  {...options}
681
664
  {...markChannelProps}>
682
665
  {#snippet children({ scaledData }: { scaledData: ScaledDataRecord[] })}
683
- <g clip-path={clipPath} class={className || null} aria-label="contour">
684
- {#each scaledData as d, i (i)}
685
- {#if d.datum[GEOM]}
686
- <path
687
- d={path(d.datum[GEOM] as any)}
688
- style={contourStyle(d.datum[RAW_VALUE] as number)} />
689
- {/if}
690
- {/each}
691
- </g>
666
+ <GeoPathGroup
667
+ {scaledData}
668
+ {path}
669
+ geomKey={GEOM}
670
+ colorKeyword="value"
671
+ {fill}
672
+ stroke={effectiveStroke}
673
+ {strokeWidth}
674
+ {strokeOpacity}
675
+ {fillOpacity}
676
+ {opacity}
677
+ {strokeMiterlimit}
678
+ {clipPath}
679
+ className={className || undefined}
680
+ ariaLabel="contour"
681
+ {canvas}
682
+ {plot} />
692
683
  {/snippet}
693
684
  </Mark>
@@ -100,6 +100,8 @@ declare function $$render<Datum extends DataRow>(): {
100
100
  strokeMiterlimit?: number;
101
101
  clipPath?: string;
102
102
  class?: string;
103
+ /** Render using a canvas element instead of SVG paths. */
104
+ canvas?: boolean;
103
105
  /** the horizontal facet channel */
104
106
  fx?: ChannelAccessor<Datum>;
105
107
  /** the vertical facet channel */
@@ -0,0 +1,127 @@
1
+ <!-- @component
2
+ Renders individual Delaunay triangulation edges as separate paths,
3
+ allowing per-edge styling based on the source data point.
4
+ -->
5
+ <script lang="ts" generics="Datum = DataRecord">
6
+ interface DelaunayLinkMarkProps extends BaseMarkProps<Datum>, LinkableMarkProps<Datum> {
7
+ /** the input data array */
8
+ data?: Datum[];
9
+ /** the horizontal position channel */
10
+ x?: ChannelAccessor<Datum>;
11
+ /** the vertical position channel */
12
+ y?: ChannelAccessor<Datum>;
13
+ /** the grouping channel; separate triangulations per group */
14
+ z?: ChannelAccessor<Datum>;
15
+ /** Render using a canvas element instead of SVG paths. */
16
+ canvas?: boolean;
17
+ }
18
+
19
+ import { Delaunay } from 'd3-delaunay';
20
+ import type {
21
+ DataRecord,
22
+ BaseMarkProps,
23
+ ChannelAccessor,
24
+ LinkableMarkProps,
25
+ MarkType,
26
+ ScaledDataRecord
27
+ } from '../types/index.js';
28
+ import { groupFacetsAndZ } from '../helpers/group.js';
29
+ import { recordizeXY } from '../transforms/recordize.js';
30
+ import { sort } from '../index.js';
31
+ import Mark from '../Mark.svelte';
32
+ import PathItems from './helpers/PathItems.svelte';
33
+ import { usePlot } from '../hooks/usePlot.svelte.js';
34
+ import { getPlotDefaults } from '../hooks/plotDefaults.js';
35
+ import { SvelteSet } from 'svelte/reactivity';
36
+
37
+ const DEFAULTS = {
38
+ ...getPlotDefaults().delaunayLink
39
+ };
40
+
41
+ let markProps: DelaunayLinkMarkProps = $props();
42
+
43
+ const {
44
+ data = [] as Datum[],
45
+ class: className = 'delaunay-link',
46
+ canvas = false,
47
+ ...options
48
+ }: DelaunayLinkMarkProps = $derived({ ...DEFAULTS, ...markProps });
49
+
50
+ const args = $derived(
51
+ sort(
52
+ recordizeXY({
53
+ data: data as any[],
54
+ ...options
55
+ })
56
+ )
57
+ );
58
+
59
+ const plot = usePlot();
60
+
61
+ function computeEdges(scaledData: ScaledDataRecord[]) {
62
+ const scaledByDatum = new Map(scaledData.map((d) => [d.datum, d]));
63
+ const results: { path: string; datum: ScaledDataRecord }[] = [];
64
+
65
+ groupFacetsAndZ(
66
+ scaledData.map((d) => d.datum),
67
+ args,
68
+ (groupItems) => {
69
+ const groupScaled = groupItems
70
+ .map((d) => scaledByDatum.get(d))
71
+ .filter(
72
+ (d): d is ScaledDataRecord =>
73
+ d !== undefined &&
74
+ d.valid &&
75
+ Number.isFinite(d.x as number) &&
76
+ Number.isFinite(d.y as number)
77
+ );
78
+ if (groupScaled.length < 2) return;
79
+
80
+ const delaunay = Delaunay.from(
81
+ groupScaled,
82
+ (d) => d.x as number,
83
+ (d) => d.y as number
84
+ );
85
+ const { halfedges, triangles } = delaunay;
86
+ const seen = new SvelteSet<string>();
87
+
88
+ for (let i = 0; i < halfedges.length; i++) {
89
+ const j = halfedges[i];
90
+ if (j < i && j !== -1) continue;
91
+ const a = triangles[i];
92
+ const b = triangles[i % 3 === 2 ? i - 2 : i + 1];
93
+ const key = a < b ? `${a},${b}` : `${b},${a}`;
94
+ if (seen.has(key)) continue;
95
+ seen.add(key);
96
+
97
+ const p = groupScaled[a];
98
+ const q = groupScaled[b];
99
+ results.push({
100
+ datum: p,
101
+ path: `M${p.x},${p.y}L${q.x},${q.y}`
102
+ });
103
+ }
104
+ },
105
+ false
106
+ );
107
+
108
+ return results;
109
+ }
110
+ </script>
111
+
112
+ <Mark
113
+ type={'delaunayLink' as MarkType}
114
+ channels={['x', 'y', 'fill', 'opacity', 'stroke', 'fillOpacity', 'strokeOpacity']}
115
+ defaults={{ fill: 'none', stroke: 'currentColor' }}
116
+ {...args}>
117
+ {#snippet children({ mark, usedScales, scaledData })}
118
+ <PathItems
119
+ paths={computeEdges(scaledData)}
120
+ {args}
121
+ {options}
122
+ {className}
123
+ {usedScales}
124
+ {plot}
125
+ {canvas} />
126
+ {/snippet}
127
+ </Mark>
@@ -0,0 +1,175 @@
1
+ import type { DataRecord, ChannelAccessor, LinkableMarkProps } from '../types/index.js';
2
+ declare function $$render<Datum = DataRecord>(): {
3
+ props: Partial<{
4
+ filter: import("../types/index.js").ConstantAccessor<boolean, Datum>;
5
+ facet: "auto" | "include" | "exclude";
6
+ fx: ChannelAccessor<Datum>;
7
+ fy: ChannelAccessor<Datum>;
8
+ dx: import("../types/index.js").ConstantAccessor<number, Datum>;
9
+ dy: import("../types/index.js").ConstantAccessor<number, Datum>;
10
+ dodgeX: import("../transforms/dodge.js").DodgeXOptions;
11
+ dodgeY: import("../transforms/dodge.js").DodgeYOptions;
12
+ fill: ChannelAccessor<Datum>;
13
+ fillOpacity: import("../types/index.js").ConstantAccessor<number, Datum>;
14
+ fontFamily: import("../types/index.js").ConstantAccessor<import("csstype").Property.FontFamily, Datum>;
15
+ fontSize: import("../types/index.js").ConstantAccessor<import("csstype").Property.FontSize<number>, Datum>;
16
+ fontStyle: import("../types/index.js").ConstantAccessor<import("csstype").Property.FontStyle, Datum>;
17
+ fontVariant: import("../types/index.js").ConstantAccessor<import("csstype").Property.FontVariant, Datum>;
18
+ fontWeight: import("../types/index.js").ConstantAccessor<import("csstype").Property.FontWeight, Datum>;
19
+ letterSpacing: import("../types/index.js").ConstantAccessor<import("csstype").Property.LetterSpacing<0 | (string & {})>, Datum>;
20
+ wordSpacing: import("../types/index.js").ConstantAccessor<import("csstype").Property.WordSpacing<0 | (string & {})>, Datum>;
21
+ textAnchor: import("../types/index.js").ConstantAccessor<import("csstype").Property.TextAnchor, Datum>;
22
+ textTransform: import("../types/index.js").ConstantAccessor<import("csstype").Property.TextTransform, Datum>;
23
+ textDecoration: import("../types/index.js").ConstantAccessor<import("csstype").Property.TextDecoration<0 | (string & {})>, Datum>;
24
+ sort: ((a: import("../types/data.js").RawValue, b: import("../types/data.js").RawValue) => number) | {
25
+ channel: string;
26
+ order?: "ascending" | "descending";
27
+ } | import("../types/index.js").ConstantAccessor<import("../types/data.js").RawValue, Datum>;
28
+ stroke: ChannelAccessor<Datum>;
29
+ strokeWidth: import("../types/index.js").ConstantAccessor<number, Datum>;
30
+ strokeOpacity: import("../types/index.js").ConstantAccessor<number, Datum>;
31
+ strokeLinejoin: import("../types/index.js").ConstantAccessor<import("csstype").Property.StrokeLinejoin, Datum>;
32
+ strokeLinecap: import("../types/index.js").ConstantAccessor<import("csstype").Property.StrokeLinecap, Datum>;
33
+ strokeMiterlimit: import("../types/index.js").ConstantAccessor<number, Datum>;
34
+ opacity: ChannelAccessor<Datum>;
35
+ strokeDasharray: import("../types/index.js").ConstantAccessor<string, Datum>;
36
+ strokeDashoffset: import("../types/index.js").ConstantAccessor<number, Datum>;
37
+ blend: import("../types/index.js").ConstantAccessor<import("csstype").Property.MixBlendMode, Datum>;
38
+ mixBlendMode: import("../types/index.js").ConstantAccessor<import("csstype").Property.MixBlendMode, Datum>;
39
+ clipPath: string;
40
+ mask: string;
41
+ imageFilter: import("../types/index.js").ConstantAccessor<string, Datum>;
42
+ shapeRendering: import("../types/index.js").ConstantAccessor<import("csstype").Property.ShapeRendering, Datum>;
43
+ paintOrder: import("../types/index.js").ConstantAccessor<string, Datum>;
44
+ onclick: (event: Event & {
45
+ currentTarget: SVGPathElement;
46
+ }, datum: Datum, index: number) => void;
47
+ ondblclick: (event: Event & {
48
+ currentTarget: SVGPathElement;
49
+ }, datum: Datum, index: number) => void;
50
+ onmouseup: (event: Event & {
51
+ currentTarget: SVGPathElement;
52
+ }, datum: Datum, index: number) => void;
53
+ onmousedown: (event: Event & {
54
+ currentTarget: SVGPathElement;
55
+ }, datum: Datum, index: number) => void;
56
+ onmouseenter: (event: Event & {
57
+ currentTarget: SVGPathElement;
58
+ }, datum: Datum, index: number) => void;
59
+ onmousemove: (event: Event & {
60
+ currentTarget: SVGPathElement;
61
+ }, datum: Datum, index: number) => void;
62
+ onmouseleave: (event: Event & {
63
+ currentTarget: SVGPathElement;
64
+ }, datum: Datum, index: number) => void;
65
+ onmouseout: (event: Event & {
66
+ currentTarget: SVGPathElement;
67
+ }, datum: Datum, index: number) => void;
68
+ onmouseover: (event: Event & {
69
+ currentTarget: SVGPathElement;
70
+ }, datum: Datum, index: number) => void;
71
+ onpointercancel: (event: Event & {
72
+ currentTarget: SVGPathElement;
73
+ }, datum: Datum, index: number) => void;
74
+ onpointerdown: (event: Event & {
75
+ currentTarget: SVGPathElement;
76
+ }, datum: Datum, index: number) => void;
77
+ onpointerup: (event: Event & {
78
+ currentTarget: SVGPathElement;
79
+ }, datum: Datum, index: number) => void;
80
+ onpointerenter: (event: Event & {
81
+ currentTarget: SVGPathElement;
82
+ }, datum: Datum, index: number) => void;
83
+ onpointerleave: (event: Event & {
84
+ currentTarget: SVGPathElement;
85
+ }, datum: Datum, index: number) => void;
86
+ onpointermove: (event: Event & {
87
+ currentTarget: SVGPathElement;
88
+ }, datum: Datum, index: number) => void;
89
+ onpointerover: (event: Event & {
90
+ currentTarget: SVGPathElement;
91
+ }, datum: Datum, index: number) => void;
92
+ onpointerout: (event: Event & {
93
+ currentTarget: SVGPathElement;
94
+ }, datum: Datum, index: number) => void;
95
+ ondrag: (event: Event & {
96
+ currentTarget: SVGPathElement;
97
+ }, datum: Datum, index: number) => void;
98
+ ondrop: (event: Event & {
99
+ currentTarget: SVGPathElement;
100
+ }, datum: Datum, index: number) => void;
101
+ ondragstart: (event: Event & {
102
+ currentTarget: SVGPathElement;
103
+ }, datum: Datum, index: number) => void;
104
+ ondragenter: (event: Event & {
105
+ currentTarget: SVGPathElement;
106
+ }, datum: Datum, index: number) => void;
107
+ ondragleave: (event: Event & {
108
+ currentTarget: SVGPathElement;
109
+ }, datum: Datum, index: number) => void;
110
+ ondragover: (event: Event & {
111
+ currentTarget: SVGPathElement;
112
+ }, datum: Datum, index: number) => void;
113
+ ondragend: (event: Event & {
114
+ currentTarget: SVGPathElement;
115
+ }, datum: Datum, index: number) => void;
116
+ ontouchstart: (event: Event & {
117
+ currentTarget: SVGPathElement;
118
+ }, datum: Datum, index: number) => void;
119
+ ontouchmove: (event: Event & {
120
+ currentTarget: SVGPathElement;
121
+ }, datum: Datum, index: number) => void;
122
+ ontouchend: (event: Event & {
123
+ currentTarget: SVGPathElement;
124
+ }, datum: Datum, index: number) => void;
125
+ ontouchcancel: (event: Event & {
126
+ currentTarget: SVGPathElement;
127
+ }, datum: Datum, index: number) => void;
128
+ oncontextmenu: (event: Event & {
129
+ currentTarget: SVGPathElement;
130
+ }, datum: Datum, index: number) => void;
131
+ onwheel: (event: Event & {
132
+ currentTarget: SVGPathElement;
133
+ }, datum: Datum, index: number) => void;
134
+ class: string;
135
+ style: string;
136
+ cursor: import("../types/index.js").ConstantAccessor<import("csstype").Property.Cursor, Datum>;
137
+ title: import("../types/index.js").ConstantAccessor<string, Datum>;
138
+ }> & LinkableMarkProps<Datum> & {
139
+ /** the input data array */
140
+ data?: Datum[];
141
+ /** the horizontal position channel */
142
+ x?: ChannelAccessor<Datum>;
143
+ /** the vertical position channel */
144
+ y?: ChannelAccessor<Datum>;
145
+ /** the grouping channel; separate triangulations per group */
146
+ z?: ChannelAccessor<Datum>;
147
+ /** Render using a canvas element instead of SVG paths. */
148
+ canvas?: boolean;
149
+ };
150
+ exports: {};
151
+ bindings: "";
152
+ slots: {};
153
+ events: {};
154
+ };
155
+ declare class __sveltets_Render<Datum = DataRecord> {
156
+ props(): ReturnType<typeof $$render<Datum>>['props'];
157
+ events(): ReturnType<typeof $$render<Datum>>['events'];
158
+ slots(): ReturnType<typeof $$render<Datum>>['slots'];
159
+ bindings(): "";
160
+ exports(): {};
161
+ }
162
+ interface $$IsomorphicComponent {
163
+ new <Datum = 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']>> & {
164
+ $$bindings?: ReturnType<__sveltets_Render<Datum>['bindings']>;
165
+ } & ReturnType<__sveltets_Render<Datum>['exports']>;
166
+ <Datum = DataRecord>(internal: unknown, props: ReturnType<__sveltets_Render<Datum>['props']> & {}): ReturnType<__sveltets_Render<Datum>['exports']>;
167
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
168
+ }
169
+ /**
170
+ * Renders individual Delaunay triangulation edges as separate paths,
171
+ * allowing per-edge styling based on the source data point.
172
+ */
173
+ declare const DelaunayLink: $$IsomorphicComponent;
174
+ type DelaunayLink<Datum = DataRecord> = InstanceType<typeof DelaunayLink<Datum>>;
175
+ export default DelaunayLink;
@@ -0,0 +1,102 @@
1
+ <!-- @component
2
+ Renders the full Delaunay triangulation as a single SVG path.
3
+ -->
4
+ <script lang="ts" generics="Datum = DataRecord">
5
+ interface DelaunayMeshMarkProps extends BaseMarkProps<Datum> {
6
+ /** the input data array */
7
+ data?: Datum[];
8
+ /** the horizontal position channel */
9
+ x?: ChannelAccessor<Datum>;
10
+ /** the vertical position channel */
11
+ y?: ChannelAccessor<Datum>;
12
+ /** the grouping channel; separate triangulations per group */
13
+ z?: ChannelAccessor<Datum>;
14
+ /** Render using a canvas element instead of SVG paths. */
15
+ canvas?: boolean;
16
+ }
17
+
18
+ import { Delaunay } from 'd3-delaunay';
19
+ import type {
20
+ DataRecord,
21
+ BaseMarkProps,
22
+ ChannelAccessor,
23
+ MarkType,
24
+ ScaledDataRecord
25
+ } from '../types/index.js';
26
+ import { groupFacetsAndZ } from '../helpers/group.js';
27
+ import { recordizeXY } from '../transforms/recordize.js';
28
+ import Mark from '../Mark.svelte';
29
+ import PathGroup from './helpers/PathGroup.svelte';
30
+ import { usePlot } from '../hooks/usePlot.svelte.js';
31
+ import { getPlotDefaults } from '../hooks/plotDefaults.js';
32
+
33
+ const DEFAULTS = {
34
+ ...getPlotDefaults().delaunayMesh
35
+ };
36
+
37
+ let markProps: DelaunayMeshMarkProps = $props();
38
+
39
+ const {
40
+ data = [] as Datum[],
41
+ class: className = 'delaunay-mesh',
42
+ canvas = false,
43
+ ...options
44
+ }: DelaunayMeshMarkProps = $derived({ ...DEFAULTS, ...markProps });
45
+
46
+ const args = $derived(
47
+ recordizeXY({
48
+ data: data as any[],
49
+ ...options
50
+ })
51
+ );
52
+
53
+ const plot = usePlot();
54
+
55
+ function computeMeshPaths(scaledData: ScaledDataRecord[]) {
56
+ const scaledByDatum = new Map(scaledData.map((d) => [d.datum, d]));
57
+ const meshes: { path: string; datum: ScaledDataRecord }[] = [];
58
+
59
+ groupFacetsAndZ(
60
+ scaledData.map((d) => d.datum),
61
+ args,
62
+ (groupItems) => {
63
+ const groupScaled = groupItems
64
+ .map((d) => scaledByDatum.get(d))
65
+ .filter(
66
+ (d): d is ScaledDataRecord =>
67
+ d !== undefined &&
68
+ d.valid &&
69
+ Number.isFinite(d.x as number) &&
70
+ Number.isFinite(d.y as number)
71
+ );
72
+ if (groupScaled.length < 2) return;
73
+ const delaunay = Delaunay.from(
74
+ groupScaled,
75
+ (d) => d.x as number,
76
+ (d) => d.y as number
77
+ );
78
+ const path = delaunay.render();
79
+ if (path) meshes.push({ path, datum: groupScaled[0] });
80
+ },
81
+ false
82
+ );
83
+
84
+ return meshes;
85
+ }
86
+ </script>
87
+
88
+ <Mark
89
+ type={'delaunayMesh' as MarkType}
90
+ channels={['x', 'y', 'fill', 'stroke', 'strokeOpacity', 'fillOpacity', 'opacity']}
91
+ defaults={{ fill: 'none', stroke: 'currentColor' }}
92
+ {...args}>
93
+ {#snippet children({ scaledData, usedScales })}
94
+ <PathGroup
95
+ paths={computeMeshPaths(scaledData)}
96
+ {args}
97
+ {className}
98
+ {usedScales}
99
+ {plot}
100
+ {canvas} />
101
+ {/snippet}
102
+ </Mark>