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.
- 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 +461 -0
- package/dist/marks/Density.svelte.d.ts +87 -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 +125 -0
- package/dist/marks/helpers/GeoPathCanvas.svelte.d.ts +24 -0
- package/dist/marks/helpers/GeoPathGroup.svelte +103 -0
- package/dist/marks/helpers/GeoPathGroup.svelte.d.ts +37 -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,109 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Renders the full Voronoi diagram as a single SVG path.
|
|
3
|
+
-->
|
|
4
|
+
<script lang="ts" generics="Datum = DataRecord">
|
|
5
|
+
interface VoronoiMeshMarkProps 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 diagrams 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().voronoiMesh
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let markProps: VoronoiMeshMarkProps = $props();
|
|
38
|
+
|
|
39
|
+
const {
|
|
40
|
+
data = [] as Datum[],
|
|
41
|
+
class: className = 'voronoi-mesh',
|
|
42
|
+
canvas = false,
|
|
43
|
+
...options
|
|
44
|
+
}: VoronoiMeshMarkProps = $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 x0 = plot.options.marginLeft;
|
|
57
|
+
const y0 = plot.options.marginTop;
|
|
58
|
+
const x1 = x0 + plot.facetWidth;
|
|
59
|
+
const y1 = y0 + plot.facetHeight;
|
|
60
|
+
if (!(x1 > x0) || !(y1 > y0)) return [];
|
|
61
|
+
|
|
62
|
+
const scaledByDatum = new Map(scaledData.map((d) => [d.datum, d]));
|
|
63
|
+
const meshes: { 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
|
+
const delaunay = Delaunay.from(
|
|
80
|
+
groupScaled,
|
|
81
|
+
(d) => d.x as number,
|
|
82
|
+
(d) => d.y as number
|
|
83
|
+
);
|
|
84
|
+
const voronoi = delaunay.voronoi([x0, y0, x1, y1]);
|
|
85
|
+
const path = voronoi.render();
|
|
86
|
+
if (path) meshes.push({ path, datum: groupScaled[0] });
|
|
87
|
+
},
|
|
88
|
+
false
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
return meshes;
|
|
92
|
+
}
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<Mark
|
|
96
|
+
type={'voronoiMesh' as MarkType}
|
|
97
|
+
channels={['x', 'y', 'fill', 'stroke', 'strokeOpacity', 'fillOpacity', 'opacity']}
|
|
98
|
+
defaults={{ fill: 'none', stroke: 'currentColor' }}
|
|
99
|
+
{...args}>
|
|
100
|
+
{#snippet children({ scaledData, usedScales })}
|
|
101
|
+
<PathGroup
|
|
102
|
+
paths={computeMeshPaths(scaledData)}
|
|
103
|
+
{args}
|
|
104
|
+
{className}
|
|
105
|
+
{usedScales}
|
|
106
|
+
{plot}
|
|
107
|
+
{canvas} />
|
|
108
|
+
{/snippet}
|
|
109
|
+
</Mark>
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { DataRecord, ChannelAccessor } 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
|
+
}> & {
|
|
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 diagrams 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
|
+
/** Renders the full Voronoi diagram as a single SVG path. */
|
|
170
|
+
declare const VoronoiMesh: $$IsomorphicComponent;
|
|
171
|
+
type VoronoiMesh<Datum = DataRecord> = InstanceType<typeof VoronoiMesh<Datum>>;
|
|
172
|
+
export default VoronoiMesh;
|
|
@@ -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;
|
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
colorKeyword,
|
|
16
|
+
fill,
|
|
17
|
+
stroke,
|
|
18
|
+
strokeWidth,
|
|
19
|
+
strokeOpacity,
|
|
20
|
+
fillOpacity,
|
|
21
|
+
opacity,
|
|
22
|
+
strokeMiterlimit
|
|
23
|
+
}: {
|
|
24
|
+
scaledData: ScaledDataRecord[];
|
|
25
|
+
path: GeoPath;
|
|
26
|
+
/** Symbol key used to retrieve the GeoJSON geometry from each datum. */
|
|
27
|
+
geomKey: symbol;
|
|
28
|
+
/**
|
|
29
|
+
* The color keyword that, when used as fill/stroke, maps the threshold
|
|
30
|
+
* value through the plot's color scale. E.g. "value" for Contour,
|
|
31
|
+
* "density" for Density.
|
|
32
|
+
*/
|
|
33
|
+
colorKeyword: string;
|
|
34
|
+
fill: string;
|
|
35
|
+
stroke: string;
|
|
36
|
+
strokeWidth?: number;
|
|
37
|
+
strokeOpacity?: number;
|
|
38
|
+
fillOpacity?: number;
|
|
39
|
+
opacity?: number;
|
|
40
|
+
strokeMiterlimit?: number;
|
|
41
|
+
} = $props();
|
|
42
|
+
|
|
43
|
+
const plot = usePlot();
|
|
44
|
+
|
|
45
|
+
/** Resolve a fill/stroke string that may be the colorKeyword. */
|
|
46
|
+
function resolveColorProp(prop: string, value: number): string {
|
|
47
|
+
if (prop.toLowerCase() === colorKeyword.toLowerCase()) {
|
|
48
|
+
return (plot.scales.color?.fn(value) as string) ?? 'currentColor';
|
|
49
|
+
}
|
|
50
|
+
return prop;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const render: Attachment = (canvasEl: Element) => {
|
|
54
|
+
const canvas = canvasEl as HTMLCanvasElement;
|
|
55
|
+
const context = canvas.getContext('2d');
|
|
56
|
+
|
|
57
|
+
$effect(() => {
|
|
58
|
+
if (!context) return;
|
|
59
|
+
|
|
60
|
+
path.context(context);
|
|
61
|
+
context.resetTransform();
|
|
62
|
+
context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
|
|
63
|
+
|
|
64
|
+
let currentColor: string | undefined;
|
|
65
|
+
|
|
66
|
+
const resolveCanvasColor = (color: string): string => {
|
|
67
|
+
if (color.toLowerCase() === 'currentcolor') {
|
|
68
|
+
return (
|
|
69
|
+
currentColor ||
|
|
70
|
+
(currentColor = getComputedStyle(
|
|
71
|
+
canvas.parentElement?.parentElement ?? canvas
|
|
72
|
+
).getPropertyValue('color'))
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (CSS_VAR.test(color)) {
|
|
76
|
+
return getComputedStyle(canvas).getPropertyValue(color.slice(4, -1));
|
|
77
|
+
}
|
|
78
|
+
return color;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const globalOpacity = opacity ?? 1;
|
|
82
|
+
if (strokeMiterlimit != null) context.miterLimit = strokeMiterlimit;
|
|
83
|
+
|
|
84
|
+
for (const d of scaledData) {
|
|
85
|
+
const geom = d.datum[geomKey as any] as any;
|
|
86
|
+
if (!geom?.coordinates?.length) continue;
|
|
87
|
+
|
|
88
|
+
const thresholdValue = (d.datum[RAW_VALUE as any] as number) ?? 0;
|
|
89
|
+
const fillColor = resolveCanvasColor(resolveColorProp(fill, thresholdValue));
|
|
90
|
+
const strokeColor = resolveCanvasColor(resolveColorProp(stroke, thresholdValue));
|
|
91
|
+
|
|
92
|
+
context.beginPath();
|
|
93
|
+
path(geom);
|
|
94
|
+
context.closePath();
|
|
95
|
+
|
|
96
|
+
if (fillColor && fillColor !== 'none') {
|
|
97
|
+
context.fillStyle = fillColor;
|
|
98
|
+
context.globalAlpha = globalOpacity * (fillOpacity ?? 1);
|
|
99
|
+
context.fill();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (strokeColor && strokeColor !== 'none') {
|
|
103
|
+
context.strokeStyle = strokeColor;
|
|
104
|
+
context.lineWidth = strokeWidth ?? 1;
|
|
105
|
+
context.globalAlpha = globalOpacity * (strokeOpacity ?? 1);
|
|
106
|
+
context.stroke();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Reset path context in case we switch back to SVG rendering.
|
|
111
|
+
path.context(null);
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
context.clearRect(
|
|
115
|
+
0,
|
|
116
|
+
0,
|
|
117
|
+
plot.width * (devicePixelRatio.current ?? 1),
|
|
118
|
+
plot.height * (devicePixelRatio.current ?? 1)
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<CanvasLayer {@attach render} />
|
|
@@ -0,0 +1,24 @@
|
|
|
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 GeoJSON geometry from each datum. */
|
|
7
|
+
geomKey: symbol;
|
|
8
|
+
/**
|
|
9
|
+
* The color keyword that, when used as fill/stroke, maps the threshold
|
|
10
|
+
* value through the plot's color scale. E.g. "value" for Contour,
|
|
11
|
+
* "density" for Density.
|
|
12
|
+
*/
|
|
13
|
+
colorKeyword: string;
|
|
14
|
+
fill: string;
|
|
15
|
+
stroke: string;
|
|
16
|
+
strokeWidth?: number;
|
|
17
|
+
strokeOpacity?: number;
|
|
18
|
+
fillOpacity?: number;
|
|
19
|
+
opacity?: number;
|
|
20
|
+
strokeMiterlimit?: number;
|
|
21
|
+
};
|
|
22
|
+
declare const GeoPathCanvas: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
23
|
+
type GeoPathCanvas = ReturnType<typeof GeoPathCanvas>;
|
|
24
|
+
export default GeoPathCanvas;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
Renders GeoJSON geometries as SVG <path> elements (or via canvas when
|
|
3
|
+
canvas=true). Used by the Contour and Density marks. Each path's style is
|
|
4
|
+
resolved per-threshold using a color keyword (e.g. "value" or "density") that
|
|
5
|
+
maps the RAW_VALUE attached to each fake datum through the plot's color scale.
|
|
6
|
+
-->
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
import type { ScaledDataRecord, PlotState } from '../../types/index.js';
|
|
9
|
+
import type { GeoPath } from 'd3-geo';
|
|
10
|
+
import { RAW_VALUE } from '../../transforms/recordize.js';
|
|
11
|
+
import GeoPathCanvas from './GeoPathCanvas.svelte';
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
scaledData,
|
|
15
|
+
path,
|
|
16
|
+
geomKey,
|
|
17
|
+
colorKeyword,
|
|
18
|
+
fill,
|
|
19
|
+
stroke,
|
|
20
|
+
strokeWidth,
|
|
21
|
+
strokeOpacity,
|
|
22
|
+
fillOpacity,
|
|
23
|
+
opacity,
|
|
24
|
+
strokeMiterlimit,
|
|
25
|
+
clipPath,
|
|
26
|
+
className,
|
|
27
|
+
ariaLabel,
|
|
28
|
+
canvas = false,
|
|
29
|
+
plot
|
|
30
|
+
}: {
|
|
31
|
+
scaledData: ScaledDataRecord[];
|
|
32
|
+
/** d3 geoPath renderer (must NOT have a canvas context set). */
|
|
33
|
+
path: GeoPath;
|
|
34
|
+
/** Symbol key used to retrieve the GeoJSON geometry from each datum. */
|
|
35
|
+
geomKey: symbol;
|
|
36
|
+
/**
|
|
37
|
+
* The color keyword that, when used as fill/stroke, maps the threshold
|
|
38
|
+
* value through the plot's color scale. E.g. "value" for Contour,
|
|
39
|
+
* "density" for Density.
|
|
40
|
+
*/
|
|
41
|
+
colorKeyword: string;
|
|
42
|
+
fill: string;
|
|
43
|
+
stroke: string;
|
|
44
|
+
strokeWidth?: number;
|
|
45
|
+
strokeOpacity?: number;
|
|
46
|
+
fillOpacity?: number;
|
|
47
|
+
opacity?: number;
|
|
48
|
+
strokeMiterlimit?: number;
|
|
49
|
+
clipPath?: string;
|
|
50
|
+
className?: string;
|
|
51
|
+
ariaLabel?: string;
|
|
52
|
+
/** Render using a canvas element instead of SVG paths. */
|
|
53
|
+
canvas?: boolean;
|
|
54
|
+
plot: PlotState;
|
|
55
|
+
} = $props();
|
|
56
|
+
|
|
57
|
+
/** Resolve a fill/stroke prop that may be the colorKeyword. */
|
|
58
|
+
function resolveColor(prop: string | undefined, value: number): string {
|
|
59
|
+
if (prop != null && prop.toLowerCase() === colorKeyword.toLowerCase()) {
|
|
60
|
+
return (plot.scales.color?.fn(value) as string) ?? 'currentColor';
|
|
61
|
+
}
|
|
62
|
+
return prop ?? 'none';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Build the inline style string for a single contour/density path. */
|
|
66
|
+
function buildStyle(value: number): string {
|
|
67
|
+
const parts: string[] = [];
|
|
68
|
+
parts.push(`fill:${resolveColor(fill, value)}`);
|
|
69
|
+
parts.push(`stroke:${resolveColor(stroke, value)}`);
|
|
70
|
+
if (strokeWidth != null) parts.push(`stroke-width:${strokeWidth}`);
|
|
71
|
+
if (strokeOpacity != null) parts.push(`stroke-opacity:${strokeOpacity}`);
|
|
72
|
+
if (fillOpacity != null) parts.push(`fill-opacity:${fillOpacity}`);
|
|
73
|
+
if (opacity != null) parts.push(`opacity:${opacity}`);
|
|
74
|
+
if (strokeMiterlimit != null) parts.push(`stroke-miterlimit:${strokeMiterlimit}`);
|
|
75
|
+
return parts.join(';');
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
{#if canvas}
|
|
80
|
+
<GeoPathCanvas
|
|
81
|
+
{scaledData}
|
|
82
|
+
{path}
|
|
83
|
+
{geomKey}
|
|
84
|
+
{colorKeyword}
|
|
85
|
+
{fill}
|
|
86
|
+
{stroke}
|
|
87
|
+
{strokeWidth}
|
|
88
|
+
{strokeOpacity}
|
|
89
|
+
{fillOpacity}
|
|
90
|
+
{opacity}
|
|
91
|
+
{strokeMiterlimit} />
|
|
92
|
+
{:else}
|
|
93
|
+
<g clip-path={clipPath} class={className || null} aria-label={ariaLabel}>
|
|
94
|
+
{#each scaledData as d, i (i)}
|
|
95
|
+
{@const geom = d.datum[geomKey as any] as any}
|
|
96
|
+
{#if geom?.coordinates?.length}
|
|
97
|
+
<path
|
|
98
|
+
d={path(geom)}
|
|
99
|
+
style={buildStyle((d.datum[RAW_VALUE as any] as number) ?? 0)} />
|
|
100
|
+
{/if}
|
|
101
|
+
{/each}
|
|
102
|
+
</g>
|
|
103
|
+
{/if}
|