svelteplot 0.3.10 → 0.3.11

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.
@@ -64,6 +64,14 @@
64
64
  transform="translate({useFacetX ? facetXScale(facetX) : 0}, {useFacetY
65
65
  ? facetYScale(facetY)
66
66
  : 0})">
67
+ <!-- facets need invisible rect -->
68
+ <rect
69
+ x={plot.options.marginLeft}
70
+ y={plot.options.marginTop}
71
+ width={facetWidth}
72
+ height={facetHeight}
73
+ fill="transparent"
74
+ pointer-events="all" />
67
75
  <Facet
68
76
  fx={facetX}
69
77
  fy={facetY}
@@ -1,14 +1,21 @@
1
1
  <script lang="ts" generics="Datum extends DataRow">
2
2
  interface PointerMarkProps {
3
3
  data: Datum[];
4
- children: Snippet<[{ data: Datum[] }]>;
4
+ children?: Snippet<[{ data: Datum[] }]>;
5
5
  x?: ChannelAccessor<Datum>;
6
6
  y?: ChannelAccessor<Datum>;
7
7
  z?: ChannelAccessor<Datum>;
8
+ fx?: ChannelAccessor<Datum>;
9
+ fy?: ChannelAccessor<Datum>;
8
10
  /**
9
11
  * maximum cursor distance to select data points
10
12
  */
11
13
  maxDistance?: number;
14
+ /**
15
+ * tolerance for considering points as "the same" when sharing x or y values
16
+ * defaults to 0 pixel
17
+ */
18
+ tolerance?: number;
12
19
  /**
13
20
  * called whenever the selection changes
14
21
  * @param data
@@ -24,10 +31,14 @@
24
31
  import { projectXY } from '../helpers/scales.js';
25
32
  import isDataRecord from '../helpers/isDataRecord.js';
26
33
  import { RAW_VALUE } from '../transforms/recordize.js';
34
+ import { groupFacetsAndZ } from '../helpers/group.js';
27
35
 
28
36
  const { getPlotState } = getContext<PlotContext>('svelteplot');
29
37
  const plot = $derived(getPlotState());
30
38
 
39
+ const POINTER_X = Symbol('pointerX');
40
+ const POINTER_Y = Symbol('pointerY');
41
+
31
42
  let markProps: PointerMarkProps = $props();
32
43
 
33
44
  const DEFAULTS = {
@@ -40,7 +51,10 @@
40
51
  x,
41
52
  y,
42
53
  z,
54
+ fx,
55
+ fy,
43
56
  maxDistance = 15,
57
+ tolerance = Number.NEGATIVE_INFINITY,
44
58
  onupdate = null
45
59
  }: PointerMarkProps = $derived({
46
60
  ...DEFAULTS,
@@ -49,27 +63,21 @@
49
63
 
50
64
  let selectedData = $state([]);
51
65
 
52
- function onMouseMove(evt: MouseEvent) {
53
- updateSelection(evt.layerX, evt.layerY);
54
- }
55
-
56
- function onTouchMove(evt: TouchEvent) {
57
- if (evt.touches) {
58
- const rect = (evt.target as HTMLElement).getBoundingClientRect();
59
- const pageTop = window.scrollY || document.documentElement.scrollTop;
60
- const ox = rect.left;
61
- const oy = rect.top + pageTop;
62
-
63
- const touch = evt.touches[0] || evt.changedTouches[0];
64
- if (touch) {
65
- const ex = touch.pageX - ox;
66
- const ey = touch.pageY - oy;
67
- updateSelection(ex, ey);
68
- }
66
+ function onPointerMove(evt: MouseEvent) {
67
+ let facetEl = evt.target as SVGElement;
68
+ while (facetEl && !facetEl.classList.contains('facet')) {
69
+ facetEl = facetEl.parentElement;
69
70
  }
71
+ const facetRect = (facetEl?.firstChild ?? plot.body).getBoundingClientRect();
72
+
73
+ const relativeX = evt.clientX - facetRect.left + (plot.options.marginLeft ?? 0);
74
+ const relativeY = evt.clientY - facetRect.top + (plot.options.marginTop ?? 0);
75
+
76
+ // console.log({ relativeX, relativeY }, evt);
77
+ updateSelection(relativeX, relativeY);
70
78
  }
71
79
 
72
- function onMouseLeave() {
80
+ function onPointerLeave() {
73
81
  selectedData = [];
74
82
  if (onupdate) onupdate(selectedData);
75
83
  }
@@ -79,31 +87,44 @@
79
87
  const points = trees.map((tree) =>
80
88
  tree.find(x != null ? ex : 0, y != null ? ey : 0, maxDistance)
81
89
  );
82
- selectedData = points.filter((d) => d != null);
90
+ // also include other points that share the same x or y value
91
+ const otherPoints = trees.flatMap((tree, i) => {
92
+ return tree
93
+ .data()
94
+ .filter((d) => d !== points[i])
95
+ .filter(
96
+ (d) =>
97
+ (!isFinite(d[POINTER_X]) ||
98
+ Math.abs(d[POINTER_X] - points[i]?.[POINTER_X]) < tolerance) &&
99
+ (!isFinite(d[POINTER_Y]) ||
100
+ Math.abs(d[POINTER_Y] - points[i]?.[POINTER_Y]) < tolerance)
101
+ );
102
+ });
103
+ selectedData = [...points, ...otherPoints].filter((d) => d != null);
83
104
  if (onupdate) onupdate(selectedData);
84
105
  }
85
106
 
86
107
  $effect(() => {
87
- plot.body?.addEventListener('mousemove', onMouseMove);
88
- plot.body?.addEventListener('mouseleave', onMouseLeave);
89
- plot.body?.addEventListener('touchmove', onTouchMove);
108
+ plot.body?.addEventListener('pointermove', onPointerMove);
109
+ plot.body?.addEventListener('pointerleave', onPointerLeave);
90
110
 
91
111
  return () => {
92
- plot.body?.removeEventListener('mousemove', onMouseMove);
93
- plot.body?.removeEventListener('mouseleave', onMouseLeave);
94
- plot.body?.removeEventListener('touchmove', onTouchMove);
112
+ plot.body?.removeEventListener('pointermove', onPointerMove);
113
+ plot.body?.removeEventListener('pointerleave', onPointerLeave);
95
114
  };
96
115
  });
97
116
 
98
- const groups = $derived(
99
- z != null ? d3Groups(data, (d) => resolveChannel('z', d, { x, z })) : [[null, data]]
100
- );
117
+ const groups = $derived.by(() => {
118
+ const groups = [];
119
+ groupFacetsAndZ(data, { x, y, z, fx, fy }, (d) => groups.push(d));
120
+ return groups;
121
+ });
101
122
 
102
123
  const trees = $derived(
103
- groups.map(([, items]) =>
124
+ groups.map((items) =>
104
125
  quadtree()
105
- .x(x != null ? (d) => d.__pointerX : () => 0)
106
- .y(y != null ? (d) => d.__pointerY : () => 0)
126
+ .x(x != null ? (d) => d[POINTER_X] : () => 0)
127
+ .y(y != null ? (d) => d[POINTER_Y] : () => 0)
107
128
  .addAll(
108
129
  items?.map((d) => {
109
130
  const [px, py] = projectXY(
@@ -115,8 +136,8 @@
115
136
  );
116
137
  return {
117
138
  ...(isDataRecord(d) ? d : { [RAW_VALUE]: d }),
118
- __pointerX: px,
119
- __pointerY: py
139
+ [POINTER_X]: px,
140
+ [POINTER_Y]: py
120
141
  };
121
142
  }) ?? []
122
143
  )
@@ -3,16 +3,23 @@ import type { ChannelAccessor, DataRow } from '../types/index.js';
3
3
  declare class __sveltets_Render<Datum extends DataRow> {
4
4
  props(): {
5
5
  data: Datum[];
6
- children: Snippet<[{
6
+ children?: Snippet<[{
7
7
  data: Datum[];
8
- }]>;
8
+ }]> | undefined;
9
9
  x?: ChannelAccessor<Datum>;
10
10
  y?: ChannelAccessor<Datum>;
11
11
  z?: ChannelAccessor<Datum>;
12
+ fx?: ChannelAccessor<Datum>;
13
+ fy?: ChannelAccessor<Datum>;
12
14
  /**
13
15
  * maximum cursor distance to select data points
14
16
  */
15
17
  maxDistance?: number;
18
+ /**
19
+ * tolerance for considering points as "the same" when sharing x or y values
20
+ * defaults to 0 pixel
21
+ */
22
+ tolerance?: number;
16
23
  /**
17
24
  * called whenever the selection changes
18
25
  * @param data
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteplot",
3
- "version": "0.3.10",
3
+ "version": "0.3.11",
4
4
  "license": "ISC",
5
5
  "author": {
6
6
  "name": "Gregor Aisch",