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.
Files changed (45) hide show
  1. package/dist/Mark.svelte +12 -1
  2. package/dist/helpers/index.d.ts +1 -0
  3. package/dist/helpers/index.js +1 -0
  4. package/dist/helpers/resolve.d.ts +1 -1
  5. package/dist/helpers/resolve.js +6 -5
  6. package/dist/helpers/scales.d.ts +2 -2
  7. package/dist/helpers/scales.js +5 -4
  8. package/dist/helpers/typeChecks.js +14 -10
  9. package/dist/index.d.ts +3 -1
  10. package/dist/index.js +4 -2
  11. package/dist/marks/BarX.svelte +11 -37
  12. package/dist/marks/BarY.svelte +27 -58
  13. package/dist/marks/BarY.svelte.d.ts +2 -8
  14. package/dist/marks/Cell.svelte +12 -36
  15. package/dist/marks/ColorLegend.svelte +6 -10
  16. package/dist/marks/Dot.svelte +2 -2
  17. package/dist/marks/Geo.svelte +50 -41
  18. package/dist/marks/Geo.svelte.d.ts +3 -1
  19. package/dist/marks/GridX.svelte +2 -2
  20. package/dist/marks/GridY.svelte +2 -2
  21. package/dist/marks/Line.svelte +98 -80
  22. package/dist/marks/Line.svelte.d.ts +5 -3
  23. package/dist/marks/Pointer.svelte +2 -1
  24. package/dist/marks/Rect.svelte +10 -24
  25. package/dist/marks/helpers/CanvasLayer.svelte +10 -16
  26. package/dist/marks/helpers/CanvasLayer.svelte.d.ts +2 -6
  27. package/dist/marks/helpers/DotCanvas.svelte +72 -159
  28. package/dist/marks/helpers/DotCanvas.svelte.d.ts +2 -4
  29. package/dist/marks/helpers/GeoCanvas.svelte +95 -145
  30. package/dist/marks/helpers/GeoCanvas.svelte.d.ts +3 -5
  31. package/dist/marks/helpers/LineCanvas.svelte +116 -0
  32. package/dist/marks/helpers/LineCanvas.svelte.d.ts +12 -0
  33. package/dist/marks/helpers/LinearGradientX.svelte +27 -0
  34. package/dist/marks/helpers/LinearGradientX.svelte.d.ts +11 -0
  35. package/dist/marks/helpers/LinearGradientY.svelte +27 -0
  36. package/dist/marks/helpers/LinearGradientY.svelte.d.ts +11 -0
  37. package/dist/marks/helpers/RectPath.svelte +129 -0
  38. package/dist/marks/helpers/RectPath.svelte.d.ts +27 -0
  39. package/dist/marks/helpers/canvas.d.ts +1 -0
  40. package/dist/marks/helpers/canvas.js +34 -0
  41. package/dist/transforms/recordize.d.ts +1 -0
  42. package/dist/transforms/recordize.js +16 -5
  43. package/dist/transforms/stack.js +10 -7
  44. package/dist/types.d.ts +12 -6
  45. package/package.json +19 -17
@@ -1,29 +1,31 @@
1
1
  <script lang="ts">
2
- import type { PlotState, Mark, DataRecord, BaseMarkProps } from '../../types.js';
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 { isValid, testFilter } from '../../helpers/index.js';
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 { untrack } from 'svelte';
10
- import { isEqual } from 'es-toolkit';
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
- let canvas: HTMLCanvasElement | undefined = $state();
13
- let devicePixelRatio = $state(1);
19
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
20
+ const plot = $derived(getPlotState());
14
21
 
15
22
  let {
16
23
  mark,
17
- plot,
18
- data,
19
- testFacet,
20
- usedScales
24
+ data
21
25
  }: {
22
26
  mark: Mark<BaseMarkProps>;
23
27
  plot: PlotState;
24
- data: DataRecord[];
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
- const xScale = $derived(scaleHash(plot.scales.x));
42
- const yScale = $derived(scaleHash(plot.scales.y));
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
- if (`${fill}`.toLowerCase() === 'currentcolor')
113
- fill = getComputedStyle(canvas.parentElement.parentElement).getPropertyValue(
114
- 'color'
115
- );
116
- if (`${stroke}`.toLowerCase() === 'currentcolor')
117
- stroke = getComputedStyle(canvas.parentElement.parentElement).getPropertyValue(
118
- 'color'
119
- );
120
- if (CSS_VAR.test(fill))
121
- fill = getComputedStyle(canvas).getPropertyValue(fill.slice(4, -1));
122
- if (CSS_VAR.test(stroke))
123
- stroke = getComputedStyle(canvas).getPropertyValue(stroke.slice(4, -1));
124
-
125
- if (stroke && stroke !== 'none') {
126
- const strokeWidth = resolveProp(_markOptions.strokeWidth, datum, 1.6);
127
- context.lineWidth = strokeWidth;
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
- return () => {
146
- canvas?.getContext('2d')?.clearRect(0, 0, canvas?.width, canvas?.height);
147
- };
148
- });
149
-
150
- // code from https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio
151
- let remove: null | (() => void) = null;
152
-
153
- function updatePixelRatio() {
154
- if (remove != null) {
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
- <foreignObject x="0" y="0" width={plot.width} height={plot.height}>
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, DataRecord, BaseMarkProps } from '../../types.js';
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: DataRecord[];
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 { PlotState, Mark, DataRecord, BaseMarkProps } from '../../types.js';
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
- let canvas: HTMLCanvasElement | undefined = $state();
11
- let devicePixelRatio = $state(1);
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
- testFacet,
18
- usedScales,
19
- path
21
+ path,
22
+ usedScales
20
23
  }: {
21
24
  mark: Mark<BaseMarkProps>;
22
- plot: PlotState;
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
- function scaleHash(scale) {
30
- return { domain: scale.domain, type: scale.type, range: scale.range };
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
- const plotSize = [plot.width, plot.height];
49
- if (!isEqual(plotSize, _plotSize)) _plotSize = plotSize;
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
- const plotScales = untrack(() => plot.scales);
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
- for (const datum of _filteredData) {
79
- // untrack the filter test to avoid redrawing when not necessary
80
- let { stroke, fill, opacity, ...restStyles } = resolveScaledStyleProps(
81
- datum,
82
- _markOptions,
83
- _usedScales,
84
- plot_,
85
- 'fill'
86
- );
87
-
88
- const fillOpacity = restStyles['fill-opacity'];
89
- const strokeOpacity = restStyles['stroke-opacity'];
90
-
91
- if (`${fill}`.toLowerCase() === 'currentcolor')
92
- fill =
93
- currentColor ||
94
- (currentColor = getComputedStyle(
95
- canvas?.parentElement?.parentElement
96
- ).getPropertyValue('color'));
97
- if (`${stroke}`.toLowerCase() === 'currentcolor')
98
- stroke =
99
- currentColor ||
100
- (currentColor = getComputedStyle(
101
- canvas?.parentElement?.parentElement
102
- ).getPropertyValue('color'));
103
- if (CSS_VAR.test(fill))
104
- fill = getComputedStyle(canvas).getPropertyValue(fill.slice(4, -1));
105
- if (CSS_VAR.test(stroke))
106
- stroke = getComputedStyle(canvas).getPropertyValue(stroke.slice(4, -1));
107
-
108
- if (stroke && stroke !== 'none') {
109
- const strokeWidth = resolveProp(_markOptions.strokeWidth, datum, 1.6);
110
- context.lineWidth = strokeWidth;
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.fillStyle = fill ? fill : 'none';
113
- context.strokeStyle = stroke ? stroke : 'none';
114
-
115
- context.beginPath();
116
- path(datum);
117
- context.closePath();
118
-
119
- if (opacity != null) context.globalAlpha = opacity ?? 1;
120
- if (fillOpacity != null) context.globalAlpha = (opacity ?? 1) * fillOpacity;
121
-
122
- if (fill && fill !== 'none') context.fill();
123
- if (strokeOpacity != null) context.globalAlpha = (opacity ?? 1) * strokeOpacity;
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
- <foreignObject x="0" y="0" width={plot.width} height={plot.height}>
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 { PlotState, Mark, DataRecord, BaseMarkProps } from '../../types.js';
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
- plot: PlotState;
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} />