svelteplot 0.2.0 → 0.2.2
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/Mark.svelte +12 -1
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.js +1 -0
- package/dist/helpers/resolve.d.ts +1 -1
- package/dist/helpers/resolve.js +6 -5
- package/dist/helpers/scales.d.ts +2 -2
- package/dist/helpers/scales.js +5 -4
- package/dist/helpers/typeChecks.js +14 -10
- package/dist/index.d.ts +3 -1
- package/dist/index.js +4 -2
- package/dist/marks/BarX.svelte +11 -37
- package/dist/marks/BarY.svelte +27 -58
- package/dist/marks/BarY.svelte.d.ts +2 -8
- package/dist/marks/Cell.svelte +12 -36
- package/dist/marks/ColorLegend.svelte +6 -10
- package/dist/marks/Dot.svelte +2 -2
- package/dist/marks/Geo.svelte +50 -41
- package/dist/marks/Geo.svelte.d.ts +3 -1
- package/dist/marks/GridX.svelte +2 -2
- package/dist/marks/GridY.svelte +2 -2
- package/dist/marks/Line.svelte +98 -80
- package/dist/marks/Line.svelte.d.ts +5 -3
- package/dist/marks/Pointer.svelte +2 -1
- package/dist/marks/Rect.svelte +10 -24
- package/dist/marks/helpers/CanvasLayer.svelte +10 -16
- package/dist/marks/helpers/CanvasLayer.svelte.d.ts +2 -6
- package/dist/marks/helpers/DotCanvas.svelte +72 -159
- package/dist/marks/helpers/DotCanvas.svelte.d.ts +2 -4
- package/dist/marks/helpers/GeoCanvas.svelte +95 -145
- package/dist/marks/helpers/GeoCanvas.svelte.d.ts +3 -5
- package/dist/marks/helpers/LineCanvas.svelte +116 -0
- package/dist/marks/helpers/LineCanvas.svelte.d.ts +12 -0
- package/dist/marks/helpers/LinearGradientX.svelte +27 -0
- package/dist/marks/helpers/LinearGradientX.svelte.d.ts +11 -0
- package/dist/marks/helpers/LinearGradientY.svelte +27 -0
- package/dist/marks/helpers/LinearGradientY.svelte.d.ts +11 -0
- package/dist/marks/helpers/RectPath.svelte +129 -0
- package/dist/marks/helpers/RectPath.svelte.d.ts +27 -0
- package/dist/marks/helpers/canvas.d.ts +1 -0
- package/dist/marks/helpers/canvas.js +34 -0
- package/dist/transforms/recordize.d.ts +1 -0
- package/dist/transforms/recordize.js +16 -5
- package/dist/transforms/stack.js +10 -7
- package/dist/types.d.ts +12 -6
- package/package.json +19 -17
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
PlotState,
|
|
4
|
+
Mark,
|
|
5
|
+
BaseMarkProps,
|
|
6
|
+
ScaledDataRecord,
|
|
7
|
+
PlotContext
|
|
8
|
+
} from '../../types.js';
|
|
3
9
|
import { CSS_VAR } from '../../constants.js';
|
|
4
|
-
import {
|
|
5
|
-
import { resolveChannel, resolveProp, resolveScaledStyleProps } from '../../helpers/resolve.js';
|
|
6
|
-
import { projectXY } from '../../helpers/scales.js';
|
|
10
|
+
import { resolveProp } from '../../helpers/resolve.js';
|
|
7
11
|
import { maybeSymbol } from '../../helpers/symbols.js';
|
|
8
12
|
import { symbol as d3Symbol } from 'd3-shape';
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
13
|
+
import type { Attachment } from 'svelte/attachments';
|
|
14
|
+
import CanvasLayer from './CanvasLayer.svelte';
|
|
15
|
+
import { getContext } from 'svelte';
|
|
16
|
+
import { devicePixelRatio } from 'svelte/reactivity/window';
|
|
17
|
+
import { resolveColor } from './canvas';
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
const { getPlotState } = getContext<PlotContext>('svelteplot');
|
|
20
|
+
const plot = $derived(getPlotState());
|
|
14
21
|
|
|
15
22
|
let {
|
|
16
23
|
mark,
|
|
17
|
-
|
|
18
|
-
data,
|
|
19
|
-
testFacet,
|
|
20
|
-
usedScales
|
|
24
|
+
data
|
|
21
25
|
}: {
|
|
22
26
|
mark: Mark<BaseMarkProps>;
|
|
23
27
|
plot: PlotState;
|
|
24
|
-
data:
|
|
25
|
-
testFacet: any;
|
|
26
|
-
usedScales: any;
|
|
28
|
+
data: ScaledDataRecord[];
|
|
27
29
|
} = $props();
|
|
28
30
|
|
|
29
31
|
function drawSymbolPath(symbolType: string, size: number, context) {
|
|
@@ -31,154 +33,65 @@
|
|
|
31
33
|
return d3Symbol(maybeSymbol(symbolType), size).context(context)();
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
function scaleHash(scale) {
|
|
35
|
-
return { domain: scale.domain, type: scale.type, range: scale.range };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let _plotSize = $state([plot.width, plot.height]);
|
|
39
|
-
let _usedScales = $state(usedScales);
|
|
40
36
|
let _markOptions = $state(mark.options);
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const rScale = $derived(scaleHash(plot.scales.r));
|
|
44
|
-
let _xScale = $state(xScale);
|
|
45
|
-
let _yScale = $state(yScale);
|
|
46
|
-
let _rScale = $state(rScale);
|
|
47
|
-
|
|
48
|
-
const filteredData = $derived(
|
|
49
|
-
data.filter((datum) => testFilter(datum, _markOptions) && testFacet(datum, _markOptions))
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
let _filteredData: DataRecord[] = $state([]);
|
|
53
|
-
|
|
54
|
-
$effect(() => {
|
|
55
|
-
// update _usedScales only if changed
|
|
56
|
-
if (!isEqual(usedScales, _usedScales)) _usedScales = usedScales;
|
|
57
|
-
if (!isEqual(mark.options, _markOptions)) _markOptions = mark.options;
|
|
58
|
-
|
|
59
|
-
const plotSize = [plot.width, plot.height];
|
|
60
|
-
if (!isEqual(plotSize, _plotSize)) _plotSize = plotSize;
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
_markOptions.filter
|
|
64
|
-
? !isEqual(filteredData, _filteredData)
|
|
65
|
-
: filteredData.length !== _filteredData.length
|
|
66
|
-
) {
|
|
67
|
-
_filteredData = filteredData;
|
|
68
|
-
}
|
|
69
|
-
if (!isEqual(xScale, _xScale)) _xScale = xScale;
|
|
70
|
-
if (!isEqual(yScale, _yScale)) _yScale = yScale;
|
|
71
|
-
if (!isEqual(rScale, _rScale)) _rScale = rScale;
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
$effect(() => {
|
|
75
|
-
// track plot size, since we're untracking the scales
|
|
76
|
-
_plotSize;
|
|
77
|
-
_markOptions;
|
|
78
|
-
_xScale;
|
|
79
|
-
_yScale;
|
|
80
|
-
_rScale;
|
|
81
|
-
const plotScales = untrack(() => plot.scales);
|
|
37
|
+
|
|
38
|
+
const renderDots: Attachment = (canvas: HTMLCanvasElement) => {
|
|
82
39
|
const context = canvas.getContext('2d');
|
|
83
|
-
if (context === null) return;
|
|
84
|
-
// this will re-run whenever `color` or `size` change
|
|
85
|
-
context.resetTransform();
|
|
86
|
-
context.scale(devicePixelRatio, devicePixelRatio);
|
|
87
|
-
|
|
88
|
-
for (const datum of _filteredData) {
|
|
89
|
-
// untrack the filter test to avoid redrawing when not necessary
|
|
90
|
-
const x = resolveChannel('x', datum, _markOptions);
|
|
91
|
-
const y = resolveChannel('y', datum, _markOptions);
|
|
92
|
-
const r = resolveChannel('r', datum, _markOptions) || 2;
|
|
93
|
-
const symbol_ = resolveChannel('symbol', datum, {
|
|
94
|
-
symbol: 'circle',
|
|
95
|
-
..._markOptions
|
|
96
|
-
});
|
|
97
|
-
const symbol = _usedScales.symbol ? plotScales.symbol.fn(symbol_) : symbol_;
|
|
98
|
-
|
|
99
|
-
if (isValid(x) && isValid(y) && isValid(r)) {
|
|
100
|
-
const [px, py] = projectXY(plotScales, x, y, true, true);
|
|
101
|
-
|
|
102
|
-
const r_ = _usedScales.r ? plotScales.r.fn(r) : r;
|
|
103
|
-
const size = r_ * r_ * Math.PI * devicePixelRatio;
|
|
104
|
-
let { stroke, strokeOpacity, fillOpacity, fill, opacity } = resolveScaledStyleProps(
|
|
105
|
-
datum,
|
|
106
|
-
_markOptions,
|
|
107
|
-
_usedScales,
|
|
108
|
-
untrack(() => plot),
|
|
109
|
-
'stroke'
|
|
110
|
-
);
|
|
111
40
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
41
|
+
$effect(() => {
|
|
42
|
+
if (context) {
|
|
43
|
+
context.resetTransform();
|
|
44
|
+
context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
|
|
45
|
+
|
|
46
|
+
for (const datum of data) {
|
|
47
|
+
if (datum.valid) {
|
|
48
|
+
let { fill, stroke } = datum;
|
|
49
|
+
|
|
50
|
+
fill = resolveColor(fill, canvas);
|
|
51
|
+
stroke = resolveColor(stroke, canvas);
|
|
52
|
+
|
|
53
|
+
if (stroke && stroke !== 'none') {
|
|
54
|
+
const strokeWidth = resolveProp(
|
|
55
|
+
_markOptions.strokeWidth,
|
|
56
|
+
datum.datum,
|
|
57
|
+
1.6
|
|
58
|
+
);
|
|
59
|
+
context.lineWidth = strokeWidth;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
context.fillStyle = fill ? fill : 'none';
|
|
63
|
+
context.strokeStyle = stroke ? stroke : 'none';
|
|
64
|
+
context.translate(datum.x, datum.y);
|
|
65
|
+
|
|
66
|
+
const size = datum.r * datum.r * Math.PI;
|
|
67
|
+
|
|
68
|
+
context.beginPath();
|
|
69
|
+
drawSymbolPath(datum.symbol, size, context);
|
|
70
|
+
context.closePath();
|
|
71
|
+
|
|
72
|
+
const { opacity = 1, fillOpacity = 1, strokeOpacity = 1 } = datum;
|
|
73
|
+
|
|
74
|
+
if (opacity != null) context.globalAlpha = opacity ?? 1;
|
|
75
|
+
if (fillOpacity != null) context.globalAlpha = (opacity ?? 1) * fillOpacity;
|
|
76
|
+
if (fill && fill !== 'none') context.fill();
|
|
77
|
+
if (strokeOpacity != null)
|
|
78
|
+
context.globalAlpha = (opacity ?? 1) * strokeOpacity;
|
|
79
|
+
if (stroke && stroke !== 'none') context.stroke();
|
|
80
|
+
context.translate(-datum.x, -datum.y);
|
|
81
|
+
}
|
|
128
82
|
}
|
|
129
|
-
context.fillStyle = fill ? fill : 'none';
|
|
130
|
-
context.strokeStyle = stroke ? stroke : 'none';
|
|
131
|
-
context.translate(px, py);
|
|
132
|
-
|
|
133
|
-
context.beginPath();
|
|
134
|
-
drawSymbolPath(symbol, size, context);
|
|
135
|
-
context.closePath();
|
|
136
|
-
|
|
137
|
-
if (opacity != null) context.globalAlpha = opacity ?? 1;
|
|
138
|
-
if (fillOpacity != null) context.globalAlpha = (opacity ?? 1) * fillOpacity;
|
|
139
|
-
if (fill && fill !== 'none') context.fill();
|
|
140
|
-
if (strokeOpacity != null) context.globalAlpha = (opacity ?? 1) * strokeOpacity;
|
|
141
|
-
if (stroke && stroke !== 'none') context.stroke();
|
|
142
|
-
context.translate(-px, -py);
|
|
143
83
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
remove();
|
|
156
|
-
}
|
|
157
|
-
const mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
|
|
158
|
-
const media = matchMedia(mqString);
|
|
159
|
-
media.addEventListener('change', updatePixelRatio);
|
|
160
|
-
remove = () => {
|
|
161
|
-
media.removeEventListener('change', updatePixelRatio);
|
|
162
|
-
};
|
|
163
|
-
devicePixelRatio = window.devicePixelRatio;
|
|
164
|
-
}
|
|
165
|
-
$effect(() => {
|
|
166
|
-
updatePixelRatio();
|
|
167
|
-
});
|
|
84
|
+
|
|
85
|
+
return () => {
|
|
86
|
+
context?.clearRect(
|
|
87
|
+
0,
|
|
88
|
+
0,
|
|
89
|
+
plot.width * (devicePixelRatio.current ?? 1),
|
|
90
|
+
plot.height * (devicePixelRatio.current ?? 1)
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
};
|
|
168
95
|
</script>
|
|
169
96
|
|
|
170
|
-
<
|
|
171
|
-
<canvas
|
|
172
|
-
xmlns="http://www.w3.org/1999/xhtml"
|
|
173
|
-
bind:this={canvas}
|
|
174
|
-
width={plot.width * devicePixelRatio}
|
|
175
|
-
height={plot.height * devicePixelRatio}
|
|
176
|
-
style="width: {plot.width}px; height: {plot.height}px;"></canvas>
|
|
177
|
-
</foreignObject>
|
|
178
|
-
|
|
179
|
-
<style>
|
|
180
|
-
foreignObject,
|
|
181
|
-
canvas {
|
|
182
|
-
color: currentColor;
|
|
183
|
-
}
|
|
184
|
-
</style>
|
|
97
|
+
<CanvasLayer {@attach renderDots} />
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import type { PlotState, Mark,
|
|
1
|
+
import type { PlotState, Mark, BaseMarkProps, ScaledDataRecord } from '../../types.js';
|
|
2
2
|
type $$ComponentProps = {
|
|
3
3
|
mark: Mark<BaseMarkProps>;
|
|
4
4
|
plot: PlotState;
|
|
5
|
-
data:
|
|
6
|
-
testFacet: any;
|
|
7
|
-
usedScales: any;
|
|
5
|
+
data: ScaledDataRecord[];
|
|
8
6
|
};
|
|
9
7
|
declare const DotCanvas: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
10
8
|
type DotCanvas = ReturnType<typeof DotCanvas>;
|
|
@@ -1,165 +1,115 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Mark,
|
|
4
|
+
BaseMarkProps,
|
|
5
|
+
PlotContext,
|
|
6
|
+
ScaledDataRecord,
|
|
7
|
+
UsedScales
|
|
8
|
+
} from '../../types.js';
|
|
3
9
|
import { CSS_VAR } from '../../constants.js';
|
|
4
|
-
import { testFilter } from '../../helpers/index.js';
|
|
5
10
|
import { resolveProp, resolveScaledStyleProps } from '../../helpers/resolve.js';
|
|
6
|
-
import { untrack } from 'svelte';
|
|
7
|
-
import { isEqual } from 'es-toolkit';
|
|
11
|
+
import { getContext, untrack } from 'svelte';
|
|
8
12
|
import { type GeoPath } from 'd3-geo';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
import CanvasLayer from './CanvasLayer.svelte';
|
|
14
|
+
import type { Attachment } from 'svelte/attachments';
|
|
15
|
+
import { devicePixelRatio } from 'svelte/reactivity/window';
|
|
16
|
+
import { GEOJSON_PREFER_STROKE } from '../../helpers/index.js';
|
|
12
17
|
|
|
13
18
|
let {
|
|
14
19
|
mark,
|
|
15
|
-
plot,
|
|
16
20
|
data,
|
|
17
|
-
|
|
18
|
-
usedScales
|
|
19
|
-
path
|
|
21
|
+
path,
|
|
22
|
+
usedScales
|
|
20
23
|
}: {
|
|
21
24
|
mark: Mark<BaseMarkProps>;
|
|
22
|
-
|
|
23
|
-
data: DataRecord[];
|
|
24
|
-
testFacet: any;
|
|
25
|
-
usedScales: any;
|
|
25
|
+
data: ScaledDataRecord[];
|
|
26
26
|
path: GeoPath;
|
|
27
|
+
usedScales: UsedScales;
|
|
27
28
|
} = $props();
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
let _plotSize = $state([plot.width, plot.height]);
|
|
34
|
-
let _usedScales = $state(usedScales);
|
|
35
|
-
let _markOptions = $state(mark.options);
|
|
36
|
-
|
|
37
|
-
const filteredData = $derived(
|
|
38
|
-
data.filter((datum) => testFilter(datum, _markOptions) && testFacet(datum, _markOptions))
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
let _filteredData: DataRecord[] = $state([]);
|
|
42
|
-
|
|
43
|
-
$effect(() => {
|
|
44
|
-
// update _usedScales only if changed
|
|
45
|
-
if (!isEqual(usedScales, _usedScales)) _usedScales = usedScales;
|
|
46
|
-
if (!isEqual(mark.options, _markOptions)) _markOptions = mark.options;
|
|
30
|
+
const { getPlotState } = getContext<PlotContext>('svelteplot');
|
|
31
|
+
const plot = $derived(getPlotState());
|
|
47
32
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (
|
|
52
|
-
_markOptions.filter
|
|
53
|
-
? !isEqual(filteredData, _filteredData)
|
|
54
|
-
: filteredData.length !== _filteredData.length
|
|
55
|
-
) {
|
|
56
|
-
_filteredData = filteredData;
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
$effect(() => {
|
|
61
|
-
// track plot size, since we're untracking the scales
|
|
62
|
-
_plotSize;
|
|
63
|
-
_markOptions;
|
|
33
|
+
function maybeOpacity(value) {
|
|
34
|
+
return value == null ? 1 : +value;
|
|
35
|
+
}
|
|
64
36
|
|
|
65
|
-
|
|
37
|
+
const render: Attachment = (canvas: HTMLCanvasElement) => {
|
|
66
38
|
const context = canvas.getContext('2d');
|
|
67
|
-
if (context === null) return;
|
|
68
|
-
// this will re-run whenever `color` or `size` change
|
|
69
|
-
context.resetTransform();
|
|
70
|
-
context.scale(devicePixelRatio, devicePixelRatio);
|
|
71
|
-
|
|
72
|
-
let currentColor;
|
|
73
|
-
|
|
74
|
-
path.context(context);
|
|
75
|
-
|
|
76
|
-
const plot_ = untrack(() => plot);
|
|
77
39
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
40
|
+
$effect(() => {
|
|
41
|
+
path.context(context);
|
|
42
|
+
if (context) {
|
|
43
|
+
context.resetTransform();
|
|
44
|
+
context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
|
|
45
|
+
let currentColor;
|
|
46
|
+
|
|
47
|
+
for (const d of data) {
|
|
48
|
+
if (!d.valid) continue;
|
|
49
|
+
const geometry = resolveProp(mark.options.geometry, d.datum, d.datum);
|
|
50
|
+
// untrack the filter test to avoid redrawing when not necessary
|
|
51
|
+
let { stroke, fill, ...restStyles } = resolveScaledStyleProps(
|
|
52
|
+
d.datum,
|
|
53
|
+
mark.options,
|
|
54
|
+
usedScales,
|
|
55
|
+
plot,
|
|
56
|
+
GEOJSON_PREFER_STROKE.has(geometry.type) ? 'stroke' : 'fill'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const opacity = maybeOpacity(restStyles['opacity']);
|
|
60
|
+
const fillOpacity = maybeOpacity(restStyles['fill-opacity']);
|
|
61
|
+
const strokeOpacity = maybeOpacity(restStyles['stroke-opacity']);
|
|
62
|
+
|
|
63
|
+
if (`${fill}`.toLowerCase() === 'currentcolor')
|
|
64
|
+
fill =
|
|
65
|
+
currentColor ||
|
|
66
|
+
(currentColor = getComputedStyle(
|
|
67
|
+
canvas?.parentElement?.parentElement
|
|
68
|
+
).getPropertyValue('color'));
|
|
69
|
+
if (`${stroke}`.toLowerCase() === 'currentcolor')
|
|
70
|
+
stroke =
|
|
71
|
+
currentColor ||
|
|
72
|
+
(currentColor = getComputedStyle(
|
|
73
|
+
canvas?.parentElement?.parentElement
|
|
74
|
+
).getPropertyValue('color'));
|
|
75
|
+
if (CSS_VAR.test(fill))
|
|
76
|
+
fill = getComputedStyle(canvas).getPropertyValue(fill.slice(4, -1));
|
|
77
|
+
if (CSS_VAR.test(stroke))
|
|
78
|
+
stroke = getComputedStyle(canvas).getPropertyValue(stroke.slice(4, -1));
|
|
79
|
+
|
|
80
|
+
if (stroke && stroke !== 'none') {
|
|
81
|
+
const strokeWidth = resolveProp(mark.options.strokeWidth, d.datum, 1);
|
|
82
|
+
context.lineWidth = strokeWidth ?? 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
context.fillStyle = fill ? fill : 'none';
|
|
86
|
+
context.strokeStyle = stroke ? stroke : 'none';
|
|
87
|
+
context.lineJoin = 'round';
|
|
88
|
+
context.beginPath();
|
|
89
|
+
|
|
90
|
+
path(geometry);
|
|
91
|
+
context.closePath();
|
|
92
|
+
|
|
93
|
+
if (opacity != null) context.globalAlpha = opacity;
|
|
94
|
+
if (fillOpacity != null) context.globalAlpha = opacity * fillOpacity;
|
|
95
|
+
|
|
96
|
+
if (fill && fill !== 'none') context.fill();
|
|
97
|
+
if (strokeOpacity != null) context.globalAlpha = opacity * strokeOpacity;
|
|
98
|
+
if (stroke && stroke !== 'none') context.stroke();
|
|
99
|
+
}
|
|
111
100
|
}
|
|
112
|
-
context
|
|
113
|
-
context
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (stroke && stroke !== 'none') context.stroke();
|
|
125
|
-
}
|
|
126
|
-
return () => {
|
|
127
|
-
canvas?.getContext('2d')?.clearRect(0, 0, canvas?.width, canvas?.height);
|
|
128
|
-
};
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// code from https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
|
|
132
|
-
let remove: null | (() => void) = null;
|
|
133
|
-
|
|
134
|
-
function updatePixelRatio() {
|
|
135
|
-
if (remove != null) {
|
|
136
|
-
remove();
|
|
137
|
-
}
|
|
138
|
-
const mqString = `(resolution: ${window.devicePixelRatio}dppx)`;
|
|
139
|
-
const media = matchMedia(mqString);
|
|
140
|
-
media.addEventListener('change', updatePixelRatio);
|
|
141
|
-
remove = () => {
|
|
142
|
-
media.removeEventListener('change', updatePixelRatio);
|
|
143
|
-
};
|
|
144
|
-
devicePixelRatio = window.devicePixelRatio;
|
|
145
|
-
}
|
|
146
|
-
$effect(() => {
|
|
147
|
-
updatePixelRatio();
|
|
148
|
-
});
|
|
101
|
+
// reset path context in case we switch back to SVG
|
|
102
|
+
path.context(null);
|
|
103
|
+
return () => {
|
|
104
|
+
context?.clearRect(
|
|
105
|
+
0,
|
|
106
|
+
0,
|
|
107
|
+
plot.width * (devicePixelRatio.current ?? 1),
|
|
108
|
+
plot.height * (devicePixelRatio.current ?? 1)
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
};
|
|
149
113
|
</script>
|
|
150
114
|
|
|
151
|
-
<
|
|
152
|
-
<canvas
|
|
153
|
-
xmlns="http://www.w3.org/1999/xhtml"
|
|
154
|
-
bind:this={canvas}
|
|
155
|
-
width={plot.width * devicePixelRatio}
|
|
156
|
-
height={plot.height * devicePixelRatio}
|
|
157
|
-
style="width: {plot.width}px; height: {plot.height}px;"></canvas>
|
|
158
|
-
</foreignObject>
|
|
159
|
-
|
|
160
|
-
<style>
|
|
161
|
-
foreignObject,
|
|
162
|
-
canvas {
|
|
163
|
-
color: currentColor;
|
|
164
|
-
}
|
|
165
|
-
</style>
|
|
115
|
+
<CanvasLayer {@attach render} />
|
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Mark, BaseMarkProps, ScaledDataRecord, UsedScales } from '../../types.js';
|
|
2
2
|
import { type GeoPath } from 'd3-geo';
|
|
3
3
|
type $$ComponentProps = {
|
|
4
4
|
mark: Mark<BaseMarkProps>;
|
|
5
|
-
|
|
6
|
-
data: DataRecord[];
|
|
7
|
-
testFacet: any;
|
|
8
|
-
usedScales: any;
|
|
5
|
+
data: ScaledDataRecord[];
|
|
9
6
|
path: GeoPath;
|
|
7
|
+
usedScales: UsedScales;
|
|
10
8
|
};
|
|
11
9
|
declare const GeoCanvas: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
12
10
|
type GeoCanvas = ReturnType<typeof GeoCanvas>;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type {
|
|
3
|
+
Mark,
|
|
4
|
+
BaseMarkProps,
|
|
5
|
+
PlotContext,
|
|
6
|
+
ScaledDataRecord,
|
|
7
|
+
UsedScales
|
|
8
|
+
} from '../../types.js';
|
|
9
|
+
import { resolveProp, resolveScaledStyleProps } from '../../helpers/resolve.js';
|
|
10
|
+
import { getContext } from 'svelte';
|
|
11
|
+
import { type Line } from 'd3-shape';
|
|
12
|
+
import CanvasLayer from './CanvasLayer.svelte';
|
|
13
|
+
import type { Attachment } from 'svelte/attachments';
|
|
14
|
+
import { devicePixelRatio } from 'svelte/reactivity/window';
|
|
15
|
+
import { resolveColor } from './canvas';
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
mark,
|
|
19
|
+
groupedLineData,
|
|
20
|
+
usedScales,
|
|
21
|
+
linePath
|
|
22
|
+
}: {
|
|
23
|
+
mark: Mark<BaseMarkProps>;
|
|
24
|
+
groupedLineData: ScaledDataRecord[][];
|
|
25
|
+
usedScales: UsedScales;
|
|
26
|
+
linePath: Line<ScaledDataRecord>;
|
|
27
|
+
groupByKey?: unknown;
|
|
28
|
+
} = $props();
|
|
29
|
+
|
|
30
|
+
const { getPlotState } = getContext<PlotContext>('svelteplot');
|
|
31
|
+
const plot = $derived(getPlotState());
|
|
32
|
+
|
|
33
|
+
function maybeOpacity(value: unknown) {
|
|
34
|
+
return value == null ? 1 : +value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const render = ((canvas: HTMLCanvasElement) => {
|
|
38
|
+
const context = canvas.getContext('2d');
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (context) {
|
|
42
|
+
linePath.context(context);
|
|
43
|
+
context.resetTransform();
|
|
44
|
+
context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
|
|
45
|
+
context.lineJoin = 'round';
|
|
46
|
+
context.lineCap = 'round';
|
|
47
|
+
|
|
48
|
+
for (const group of groupedLineData) {
|
|
49
|
+
if (group.length < 2) continue;
|
|
50
|
+
|
|
51
|
+
// Get the first point to determine line styles
|
|
52
|
+
const firstPoint = group[0];
|
|
53
|
+
if (!firstPoint || !firstPoint.valid) continue;
|
|
54
|
+
|
|
55
|
+
let { stroke, ...restStyles } = resolveScaledStyleProps(
|
|
56
|
+
firstPoint.datum,
|
|
57
|
+
mark.options,
|
|
58
|
+
usedScales,
|
|
59
|
+
plot,
|
|
60
|
+
'stroke'
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const opacity = maybeOpacity(restStyles['opacity']);
|
|
64
|
+
const strokeOpacity = maybeOpacity(restStyles['stroke-opacity']);
|
|
65
|
+
|
|
66
|
+
const strokeWidth = resolveProp(
|
|
67
|
+
mark.options.strokeWidth,
|
|
68
|
+
firstPoint.datum,
|
|
69
|
+
1.4
|
|
70
|
+
) as number;
|
|
71
|
+
|
|
72
|
+
if (mark.options.outlineStroke) {
|
|
73
|
+
// draw stroke outline first
|
|
74
|
+
const outlineStroke = resolveColor(mark.options.outlineStroke, canvas);
|
|
75
|
+
const outlineStrokeWidth =
|
|
76
|
+
mark.options.outlineStrokeWidth ?? strokeWidth + 2;
|
|
77
|
+
const outlineStrokeOpacity = mark.options.outlineStrokeOpacity ?? 1;
|
|
78
|
+
|
|
79
|
+
context.lineWidth = outlineStrokeWidth;
|
|
80
|
+
context.strokeStyle = outlineStroke;
|
|
81
|
+
context.globalAlpha = opacity * outlineStrokeOpacity;
|
|
82
|
+
context.beginPath();
|
|
83
|
+
linePath(group);
|
|
84
|
+
context.stroke();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
stroke = resolveColor(stroke, canvas);
|
|
88
|
+
|
|
89
|
+
if (stroke && stroke !== 'none') {
|
|
90
|
+
context.lineWidth = strokeWidth ?? 1.4;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
context.strokeStyle = stroke ? stroke : 'currentColor';
|
|
94
|
+
context.globalAlpha = opacity * strokeOpacity;
|
|
95
|
+
|
|
96
|
+
// Start drawing the line
|
|
97
|
+
context.beginPath();
|
|
98
|
+
linePath(group);
|
|
99
|
+
context.stroke();
|
|
100
|
+
}
|
|
101
|
+
linePath.context(null);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return () => {
|
|
105
|
+
context?.clearRect(
|
|
106
|
+
0,
|
|
107
|
+
0,
|
|
108
|
+
plot.width * (devicePixelRatio.current ?? 1),
|
|
109
|
+
plot.height * (devicePixelRatio.current ?? 1)
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}) as Attachment;
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<CanvasLayer {@attach render} />
|