svelteplot 0.1.3-next.9 → 0.2.1

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 (51) hide show
  1. package/README.md +4 -2
  2. package/dist/Mark.svelte +18 -2
  3. package/dist/Plot.svelte +45 -29
  4. package/dist/helpers/index.d.ts +2 -1
  5. package/dist/helpers/index.js +1 -0
  6. package/dist/helpers/resolve.js +7 -6
  7. package/dist/helpers/scales.d.ts +2 -2
  8. package/dist/helpers/scales.js +8 -5
  9. package/dist/helpers/typeChecks.js +14 -10
  10. package/dist/index.d.ts +3 -0
  11. package/dist/index.js +3 -0
  12. package/dist/marks/BarX.svelte +15 -5
  13. package/dist/marks/BarY.svelte +20 -12
  14. package/dist/marks/BarY.svelte.d.ts +22 -1
  15. package/dist/marks/Brush.svelte +364 -0
  16. package/dist/marks/Brush.svelte.d.ts +32 -0
  17. package/dist/marks/BrushX.svelte +7 -0
  18. package/dist/marks/BrushX.svelte.d.ts +4 -0
  19. package/dist/marks/BrushY.svelte +7 -0
  20. package/dist/marks/BrushY.svelte.d.ts +4 -0
  21. package/dist/marks/Cell.svelte +0 -7
  22. package/dist/marks/ColorLegend.svelte +6 -10
  23. package/dist/marks/Dot.svelte +11 -20
  24. package/dist/marks/Dot.svelte.d.ts +8 -8
  25. package/dist/marks/Frame.svelte +10 -5
  26. package/dist/marks/Frame.svelte.d.ts +6 -1
  27. package/dist/marks/Geo.svelte +50 -41
  28. package/dist/marks/Geo.svelte.d.ts +3 -1
  29. package/dist/marks/GridX.svelte +17 -8
  30. package/dist/marks/GridY.svelte +17 -8
  31. package/dist/marks/Pointer.svelte +4 -3
  32. package/dist/marks/Pointer.svelte.d.ts +2 -2
  33. package/dist/marks/Rect.svelte +12 -19
  34. package/dist/marks/Sphere.svelte.d.ts +14 -4
  35. package/dist/marks/Text.svelte +2 -2
  36. package/dist/marks/Text.svelte.d.ts +2 -2
  37. package/dist/marks/helpers/CanvasLayer.svelte +10 -16
  38. package/dist/marks/helpers/CanvasLayer.svelte.d.ts +2 -6
  39. package/dist/marks/helpers/DotCanvas.svelte +82 -159
  40. package/dist/marks/helpers/DotCanvas.svelte.d.ts +2 -4
  41. package/dist/marks/helpers/GeoCanvas.svelte +95 -145
  42. package/dist/marks/helpers/GeoCanvas.svelte.d.ts +3 -5
  43. package/dist/marks/helpers/events.d.ts +13 -0
  44. package/dist/marks/helpers/events.js +32 -3
  45. package/dist/transforms/bin.d.ts +7 -7
  46. package/dist/transforms/recordize.d.ts +2 -0
  47. package/dist/transforms/recordize.js +20 -10
  48. package/dist/transforms/stack.js +10 -7
  49. package/dist/transforms/window.d.ts +2 -0
  50. package/dist/types.d.ts +34 -13
  51. package/package.json +23 -20
@@ -1,29 +1,30 @@
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';
11
17
 
12
- let canvas: HTMLCanvasElement | undefined = $state();
13
- let devicePixelRatio = $state(1);
18
+ const { getPlotState } = getContext<PlotContext>('svelteplot');
19
+ const plot = $derived(getPlotState());
14
20
 
15
21
  let {
16
22
  mark,
17
- plot,
18
- data,
19
- testFacet,
20
- usedScales
23
+ data
21
24
  }: {
22
25
  mark: Mark<BaseMarkProps>;
23
26
  plot: PlotState;
24
- data: DataRecord[];
25
- testFacet: any;
26
- usedScales: any;
27
+ data: ScaledDataRecord[];
27
28
  } = $props();
28
29
 
29
30
  function drawSymbolPath(symbolType: string, size: number, context) {
@@ -31,154 +32,76 @@
31
32
  return d3Symbol(maybeSymbol(symbolType), size).context(context)();
32
33
  }
33
34
 
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
35
  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);
36
+
37
+ const renderDots: Attachment = (canvas: HTMLCanvasElement) => {
82
38
  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
39
 
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;
40
+ $effect(() => {
41
+ if (context) {
42
+ context.resetTransform();
43
+ context.scale(devicePixelRatio.current ?? 1, devicePixelRatio.current ?? 1);
44
+
45
+ for (const datum of data) {
46
+ if (datum.valid) {
47
+ let { fill, stroke } = datum;
48
+
49
+ if (`${fill}`.toLowerCase() === 'currentcolor')
50
+ fill = getComputedStyle(
51
+ canvas?.parentElement?.parentElement
52
+ ).getPropertyValue('color');
53
+ if (`${stroke}`.toLowerCase() === 'currentcolor')
54
+ stroke = getComputedStyle(
55
+ canvas?.parentElement?.parentElement
56
+ ).getPropertyValue('color');
57
+
58
+ if (CSS_VAR.test(fill))
59
+ fill = getComputedStyle(canvas).getPropertyValue(fill.slice(4, -1));
60
+ if (CSS_VAR.test(stroke))
61
+ stroke = getComputedStyle(canvas).getPropertyValue(stroke.slice(4, -1));
62
+
63
+ if (stroke && stroke !== 'none') {
64
+ const strokeWidth = resolveProp(
65
+ _markOptions.strokeWidth,
66
+ datum.datum,
67
+ 1.6
68
+ );
69
+ context.lineWidth = strokeWidth;
70
+ }
71
+
72
+ context.fillStyle = fill ? fill : 'none';
73
+ context.strokeStyle = stroke ? stroke : 'none';
74
+ context.translate(datum.x, datum.y);
75
+
76
+ const size = datum.r * datum.r * Math.PI;
77
+
78
+ context.beginPath();
79
+ drawSymbolPath(datum.symbol, size, context);
80
+ context.closePath();
81
+
82
+ const { opacity = 1, fillOpacity = 1, strokeOpacity = 1 } = datum;
83
+
84
+ if (opacity != null) context.globalAlpha = opacity ?? 1;
85
+ if (fillOpacity != null) context.globalAlpha = (opacity ?? 1) * fillOpacity;
86
+ if (fill && fill !== 'none') context.fill();
87
+ if (strokeOpacity != null)
88
+ context.globalAlpha = (opacity ?? 1) * strokeOpacity;
89
+ if (stroke && stroke !== 'none') context.stroke();
90
+ context.translate(-datum.x, -datum.y);
91
+ }
128
92
  }
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
93
  }
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
- });
94
+
95
+ return () => {
96
+ context?.clearRect(
97
+ 0,
98
+ 0,
99
+ plot.width * (devicePixelRatio.current ?? 1),
100
+ plot.height * (devicePixelRatio.current ?? 1)
101
+ );
102
+ };
103
+ });
104
+ };
168
105
  </script>
169
106
 
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>
107
+ <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>;
@@ -1,4 +1,17 @@
1
1
  import type { BaseMarkProps, DataRecord, PlotState } from '../../types.js';
2
+ declare global {
3
+ interface MouseEvent {
4
+ layerX?: number;
5
+ layerY?: number;
6
+ dataX?: number | string | Date;
7
+ dataY?: number | string | Date;
8
+ }
9
+ }
10
+ /**
11
+ * Translates client coordinates (clientX, clientY) to the layer coordinates
12
+ * of the plot frame, regardless of which element triggered the event
13
+ */
14
+ export declare function clientToLayerCoordinates(event: MouseEvent, plotBody: HTMLElement | null | undefined): [number, number];
2
15
  export declare function addEventHandlers(node: SVGElement, { options, datum, getPlotState }: {
3
16
  options: BaseMarkProps;
4
17
  datum: DataRecord;
@@ -1,6 +1,27 @@
1
1
  import { invert, pick } from 'es-toolkit';
2
2
  import { RAW_VALUE } from '../../transforms/recordize.js';
3
3
  import { INDEX } from '../../constants.js';
4
+ /**
5
+ * Translates client coordinates (clientX, clientY) to the layer coordinates
6
+ * of the plot frame, regardless of which element triggered the event
7
+ */
8
+ export function clientToLayerCoordinates(event, plotBody) {
9
+ // If layerX/Y already exist and the target is the plot frame (rect element),
10
+ // we can use them directly
11
+ // if (event.layerX !== undefined && (event.target as SVGElement).tagName === 'rect') {
12
+ // return [event.layerX, event.layerY];
13
+ // }
14
+ // Otherwise, transform from client coordinates to layer coordinates
15
+ // by getting the bounds of the plot body element and calculating the offset
16
+ if (!plotBody)
17
+ return [0, 0];
18
+ const plotBodyRect = plotBody.getBoundingClientRect();
19
+ // Calculate the coordinates relative to the plot body
20
+ return [
21
+ event.clientX - plotBodyRect.left,
22
+ event.clientY - plotBodyRect.top
23
+ ];
24
+ }
4
25
  export function addEventHandlers(node, { options, datum, getPlotState }) {
5
26
  const events = pick(options, [
6
27
  'onclick',
@@ -18,11 +39,20 @@ export function addEventHandlers(node, { options, datum, getPlotState }) {
18
39
  'onmouseleave',
19
40
  'onmousemove',
20
41
  'onmouseout',
42
+ 'onmouseover',
21
43
  'onmouseup',
22
- 'onwheel',
44
+ 'onpointercancel',
45
+ 'onpointerdown',
46
+ 'onpointerenter',
47
+ 'onpointerleave',
48
+ 'onpointermove',
49
+ 'onpointerout',
50
+ 'onpointerover',
51
+ 'onpointerup',
23
52
  'ontouchcancel',
24
53
  'ontouchend',
25
- 'ontouchmove'
54
+ 'ontouchmove',
55
+ 'onwheel',
26
56
  ]);
27
57
  const listeners = new Map();
28
58
  // attach event handlers
@@ -66,7 +96,6 @@ function invertScale(scale, position) {
66
96
  if (scale.type === 'band') {
67
97
  // invert band scale since scaleBand doesn't have an invert function
68
98
  const eachBand = scale.fn.step();
69
- console.log({ eachBand, position });
70
99
  const index = Math.floor(position / eachBand);
71
100
  return scale.fn.domain()[index];
72
101
  }
@@ -1,7 +1,7 @@
1
1
  import type { DataRecord, RawValue } from '../types.js';
2
2
  import type { TransformArg } from '../types.js';
3
3
  import { type ThresholdCountGenerator } from 'd3-array';
4
- import { Reducer, type ReducerName } from '../helpers/reduce.js';
4
+ import { type ReducerName } from '../helpers/reduce.js';
5
5
  type NamedThresholdsGenerator = 'auto' | 'scott' | 'sturges' | 'freedman-diaconis';
6
6
  type BinBaseOptions = {
7
7
  domain?: [number, number];
@@ -20,14 +20,14 @@ type AdditionalOutputChannels = Partial<{
20
20
  strokeOpacity: ReducerOption;
21
21
  }>;
22
22
  export type BinXOptions = BinBaseOptions & AdditionalOutputChannels & Partial<{
23
- y: typeof Reducer;
24
- y1: typeof Reducer;
25
- y2: typeof Reducer;
23
+ y: ReducerOption;
24
+ y1: ReducerOption;
25
+ y2: ReducerOption;
26
26
  }>;
27
27
  export type BinYOptions = BinBaseOptions & AdditionalOutputChannels & Partial<{
28
- x: typeof Reducer;
29
- x1: typeof Reducer;
30
- x2: typeof Reducer;
28
+ x: ReducerOption;
29
+ x1: ReducerOption;
30
+ x2: ReducerOption;
31
31
  }>;
32
32
  type BinOptions = BinBaseOptions & AdditionalOutputChannels;
33
33
  /**