@genome-spy/core 0.72.0 → 0.73.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.
Files changed (103) hide show
  1. package/LICENSE +1 -1
  2. package/dist/bundle/index.es.js +6779 -5393
  3. package/dist/bundle/index.js +133 -121
  4. package/dist/schema.json +281 -17
  5. package/dist/src/data/formats/bed.d.ts +8 -0
  6. package/dist/src/data/formats/bed.d.ts.map +1 -0
  7. package/dist/src/data/formats/bed.js +53 -0
  8. package/dist/src/data/formats/bedpe.d.ts +8 -0
  9. package/dist/src/data/formats/bedpe.d.ts.map +1 -0
  10. package/dist/src/data/formats/bedpe.js +160 -0
  11. package/dist/src/data/sources/dataUtils.d.ts +16 -0
  12. package/dist/src/data/sources/dataUtils.d.ts.map +1 -1
  13. package/dist/src/data/sources/dataUtils.js +53 -3
  14. package/dist/src/data/sources/urlSource.d.ts +4 -0
  15. package/dist/src/data/sources/urlSource.d.ts.map +1 -1
  16. package/dist/src/data/sources/urlSource.js +133 -14
  17. package/dist/src/genome/assemblyPreflight.d.ts +31 -0
  18. package/dist/src/genome/assemblyPreflight.d.ts.map +1 -0
  19. package/dist/src/genome/assemblyPreflight.js +99 -0
  20. package/dist/src/genome/genome.d.ts +2 -2
  21. package/dist/src/genome/genome.d.ts.map +1 -1
  22. package/dist/src/genome/genome.js +4 -0
  23. package/dist/src/genome/genomeStore.d.ts +34 -3
  24. package/dist/src/genome/genomeStore.d.ts.map +1 -1
  25. package/dist/src/genome/genomeStore.js +409 -18
  26. package/dist/src/genome/rootGenomeConfig.d.ts +26 -0
  27. package/dist/src/genome/rootGenomeConfig.d.ts.map +1 -0
  28. package/dist/src/genome/rootGenomeConfig.js +94 -0
  29. package/dist/src/genomeSpy/interactionController.d.ts +5 -1
  30. package/dist/src/genomeSpy/interactionController.d.ts.map +1 -1
  31. package/dist/src/genomeSpy/interactionController.js +244 -29
  32. package/dist/src/genomeSpy/renderCoordinator.js +1 -1
  33. package/dist/src/genomeSpy.d.ts +13 -3
  34. package/dist/src/genomeSpy.d.ts.map +1 -1
  35. package/dist/src/genomeSpy.js +81 -7
  36. package/dist/src/gl/canvasSizeHelper.d.ts +74 -0
  37. package/dist/src/gl/canvasSizeHelper.d.ts.map +1 -0
  38. package/dist/src/gl/canvasSizeHelper.js +203 -0
  39. package/dist/src/gl/webGLHelper.d.ts +25 -11
  40. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  41. package/dist/src/gl/webGLHelper.js +59 -33
  42. package/dist/src/index.d.ts.map +1 -1
  43. package/dist/src/index.js +5 -2
  44. package/dist/src/marks/link.d.ts.map +1 -1
  45. package/dist/src/marks/link.js +5 -3
  46. package/dist/src/marks/mark.d.ts.map +1 -1
  47. package/dist/src/marks/mark.js +6 -1
  48. package/dist/src/scales/domainPlanner.d.ts +34 -3
  49. package/dist/src/scales/domainPlanner.d.ts.map +1 -1
  50. package/dist/src/scales/domainPlanner.js +247 -26
  51. package/dist/src/scales/scaleInstanceManager.d.ts +2 -1
  52. package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -1
  53. package/dist/src/scales/scaleInstanceManager.js +10 -11
  54. package/dist/src/scales/scaleInteractionController.d.ts.map +1 -1
  55. package/dist/src/scales/scaleInteractionController.js +16 -14
  56. package/dist/src/scales/scaleResolution.d.ts +16 -0
  57. package/dist/src/scales/scaleResolution.d.ts.map +1 -1
  58. package/dist/src/scales/scaleResolution.js +314 -54
  59. package/dist/src/scales/scaleResolutionTestUtils.d.ts +21 -0
  60. package/dist/src/scales/scaleResolutionTestUtils.d.ts.map +1 -0
  61. package/dist/src/scales/scaleResolutionTestUtils.js +33 -0
  62. package/dist/src/scales/selectionDomainUtils.d.ts +22 -0
  63. package/dist/src/scales/selectionDomainUtils.d.ts.map +1 -0
  64. package/dist/src/scales/selectionDomainUtils.js +79 -0
  65. package/dist/src/scales/zoomDomainUtils.d.ts +18 -0
  66. package/dist/src/scales/zoomDomainUtils.d.ts.map +1 -0
  67. package/dist/src/scales/zoomDomainUtils.js +69 -0
  68. package/dist/src/screenshotHarness.d.ts +16 -0
  69. package/dist/src/screenshotHarness.d.ts.map +1 -0
  70. package/dist/src/screenshotHarness.js +242 -0
  71. package/dist/src/singlePageApp.js +1 -1
  72. package/dist/src/spec/data.d.ts +23 -3
  73. package/dist/src/spec/genome.d.ts +22 -2
  74. package/dist/src/spec/parameter.d.ts +39 -2
  75. package/dist/src/spec/root.d.ts +20 -1
  76. package/dist/src/spec/scale.d.ts +41 -5
  77. package/dist/src/styles/genome-spy.css +8 -0
  78. package/dist/src/styles/genome-spy.css.d.ts +1 -1
  79. package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
  80. package/dist/src/styles/genome-spy.css.js +8 -0
  81. package/dist/src/tooltip/dataTooltipHandler.js +59 -10
  82. package/dist/src/types/embedApi.d.ts +19 -0
  83. package/dist/src/utils/inferSpecBaseUrl.d.ts +14 -0
  84. package/dist/src/utils/inferSpecBaseUrl.d.ts.map +1 -0
  85. package/dist/src/utils/inferSpecBaseUrl.js +73 -0
  86. package/dist/src/utils/interactionEvent.d.ts +53 -3
  87. package/dist/src/utils/interactionEvent.d.ts.map +1 -1
  88. package/dist/src/utils/interactionEvent.js +62 -1
  89. package/dist/src/view/containerMutationHelper.d.ts.map +1 -1
  90. package/dist/src/view/containerMutationHelper.js +8 -0
  91. package/dist/src/view/dataReadiness.d.ts +2 -2
  92. package/dist/src/view/dataReadiness.d.ts.map +1 -1
  93. package/dist/src/view/dataReadiness.js +63 -58
  94. package/dist/src/view/facetView.js +1 -1
  95. package/dist/src/view/gridView/gridChild.d.ts +7 -0
  96. package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
  97. package/dist/src/view/gridView/gridChild.js +180 -11
  98. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  99. package/dist/src/view/gridView/gridView.js +60 -17
  100. package/dist/src/view/zoom.d.ts +14 -2
  101. package/dist/src/view/zoom.d.ts.map +1 -1
  102. package/dist/src/view/zoom.js +373 -76
  103. package/package.json +4 -2
@@ -9,13 +9,27 @@
9
9
 
10
10
  import { makeLerpSmoother } from "../utils/animator.js";
11
11
  import RingBuffer from "../utils/ringBuffer.js";
12
+ import { isTouchGestureEvent } from "../utils/interactionEvent.js";
12
13
  import Point from "./layout/point.js";
13
14
 
14
- /** @type {ReturnType<typeof makeLerpSmoother>} */
15
- let smoother;
15
+ /**
16
+ * @typedef {object} ZoomInteractionState
17
+ * @prop {ReturnType<typeof makeLerpSmoother>} smoother
18
+ * @prop {RingBuffer<{point: Point, timestamp: number}>} touchPanEventBuffer
19
+ * @prop {Point | undefined} touchPanLastPoint
20
+ * @prop {0 | 1 | 2} touchPanPointerCount
21
+ */
16
22
 
17
23
  let lastTimestamp = 0;
18
24
 
25
+ /** @type {WeakMap<import("../utils/animator.js").default, ZoomInteractionState>} */
26
+ const zoomInteractionStates = new WeakMap();
27
+
28
+ /** @type {ZoomInteractionState} */
29
+ const fallbackInteractionState = createInteractionState();
30
+
31
+ const MIN_LINK_ENDPOINT_SNAP_DISTANCE = 6;
32
+
19
33
  export function markZoomActivity() {
20
34
  lastTimestamp = performance.now();
21
35
  }
@@ -35,26 +49,25 @@ function recordTimeStamp(fn) {
35
49
  // @ts-ignore
36
50
  return function (...args) {
37
51
  markZoomActivity();
38
- fn(...args);
52
+ return fn(...args);
39
53
  };
40
54
  }
41
55
 
42
56
  /**
43
57
  * @param {import("../utils/interactionEvent.js").default} event
44
58
  * @param {import("./layout/rectangle.js").default} coords
45
- * @param {(zoomEvent: ZoomEvent) => void} handleZoom
59
+ * @param {(zoomEvent: ZoomEvent) => boolean | void} handleZoom
46
60
  * @param {import("../types/viewContext.js").Hover} [hover]
47
61
  * @param {import("../utils/animator.js").default} [animator]
48
62
  */
49
63
  export function interactionToZoom(event, coords, handleZoom, hover, animator) {
50
64
  handleZoom = recordTimeStamp(handleZoom);
65
+ const interactionState = getInteractionState(animator);
51
66
 
52
67
  if (event.type == "wheel") {
53
68
  // TODO: Wheel-zoom inertia should probably be moved here and the faked wheel
54
69
  // events in genomeSpy.js and inertia.js should be retired.
55
70
 
56
- event.uiEvent.preventDefault(); // TODO: Only if there was something zoomable
57
-
58
71
  const wheelEvent = /** @type {WheelEvent} */ (event.uiEvent);
59
72
  const wheelMultiplier = wheelEvent.deltaMode ? 120 : 1;
60
73
 
@@ -63,46 +76,73 @@ export function interactionToZoom(event, coords, handleZoom, hover, animator) {
63
76
  }
64
77
 
65
78
  // Stop drag-to-pan inertia
66
- smoother?.stop();
79
+ interactionState.smoother?.stop();
67
80
 
68
81
  let { x, y } = event.point;
69
82
 
70
83
  // Snapping to the hovered item:
71
- // We find the currently hovered object and move the pointed coordinates
72
- // to its center if the mark has only primary positional channels.
73
- // This allows the user to rapidly zoom closer without having to
74
- // continuously adjust the cursor position.
84
+ // - Link marks: snap to the nearest endpoint when cursor is near one.
85
+ // - Other marks: snap to the center if only primary positional channels exist.
86
+ // This allows rapid zooming without constantly adjusting cursor position.
75
87
 
76
88
  if (hover) {
77
- const e = hover.mark.encoders;
78
- if (e.x && !e.x2 && !e.x.constant) {
79
- x = +e.x(hover.datum) * coords.width + coords.x;
80
- }
81
- if (e.y && !e.y2 && !e.y.constant) {
82
- y = (1 - +e.y(hover.datum)) * coords.height + coords.y;
89
+ const linkEndpoint = getLinkEndpointSnapPoint(
90
+ event.point,
91
+ coords,
92
+ hover
93
+ );
94
+
95
+ if (linkEndpoint) {
96
+ if (linkEndpoint.x !== undefined) {
97
+ x = linkEndpoint.x;
98
+ }
99
+ if (linkEndpoint.y !== undefined) {
100
+ y = linkEndpoint.y;
101
+ }
102
+ } else {
103
+ const e = hover.mark.encoders;
104
+ if (e.x && !e.x2 && !e.x.constant) {
105
+ x =
106
+ getEncoderUnitPosition(e.x, hover.datum) *
107
+ coords.width +
108
+ coords.x;
109
+ }
110
+ if (e.y && !e.y2 && !e.y.constant) {
111
+ y =
112
+ (1 - getEncoderUnitPosition(e.y, hover.datum)) *
113
+ coords.height +
114
+ coords.y;
115
+ }
83
116
  }
84
117
  }
85
118
 
119
+ let handled = false;
86
120
  if (Math.abs(wheelEvent.deltaX) < Math.abs(wheelEvent.deltaY)) {
87
- handleZoom({
88
- x,
89
- y,
90
- xDelta: 0,
91
- yDelta: 0,
92
- zDelta: (wheelEvent.deltaY * wheelMultiplier) / 300,
93
- });
121
+ handled =
122
+ handleZoom({
123
+ x,
124
+ y,
125
+ xDelta: 0,
126
+ yDelta: 0,
127
+ zDelta: (wheelEvent.deltaY * wheelMultiplier) / 300,
128
+ }) === true;
94
129
  } else {
95
- handleZoom({
96
- x,
97
- y,
98
- xDelta: -wheelEvent.deltaX * wheelMultiplier,
99
- yDelta: 0,
100
- zDelta: 0,
101
- });
130
+ handled =
131
+ handleZoom({
132
+ x,
133
+ y,
134
+ xDelta: -wheelEvent.deltaX * wheelMultiplier,
135
+ yDelta: 0,
136
+ zDelta: 0,
137
+ }) === true;
138
+ }
139
+
140
+ if (handled) {
141
+ wheelEvent.preventDefault();
102
142
  }
103
143
  } else if (event.type == "mousedown" && event.mouseEvent.button === 0) {
104
- if (smoother) {
105
- smoother.stop();
144
+ if (interactionState.smoother) {
145
+ interactionState.smoother.stop();
106
146
  }
107
147
 
108
148
  /** @type {RingBuffer<{point: Point, timestamp: number}>} */
@@ -132,63 +172,320 @@ export function interactionToZoom(event, coords, handleZoom, hover, animator) {
132
172
  prevPoint = point;
133
173
  };
134
174
 
135
- const animateInertia = () => {
136
- const lastMillisToInclude = 160;
175
+ const onMouseup = () => {
176
+ document.removeEventListener("mousemove", onMousemove);
177
+ document.removeEventListener("mouseup", onMouseup);
178
+ startPanInertia(
179
+ interactionState,
180
+ eventBuffer,
181
+ prevPoint,
182
+ handleZoom,
183
+ animator,
184
+ { minSampleCount: 5 }
185
+ );
186
+ };
137
187
 
138
- const now = performance.now();
139
- const arr = eventBuffer
140
- .get()
141
- .filter((p) => now - p.timestamp < lastMillisToInclude);
188
+ document.addEventListener("mouseup", onMouseup, false);
189
+ document.addEventListener("mousemove", onMousemove, false);
190
+ } else if (event.type == "touchgesture") {
191
+ if (!isTouchGestureEvent(event.uiEvent)) {
192
+ return;
193
+ }
142
194
 
143
- if (arr.length < 5 || !animator || isDecelerating(arr)) {
144
- return;
195
+ const touchGesture = event.uiEvent;
196
+ const { xDelta, yDelta, zDelta } = touchGesture;
197
+
198
+ if (touchGesture.phase === "end") {
199
+ if (touchGesture.pointerCount === 1) {
200
+ startPanInertia(
201
+ interactionState,
202
+ interactionState.touchPanEventBuffer,
203
+ interactionState.touchPanLastPoint,
204
+ handleZoom,
205
+ animator,
206
+ {
207
+ minSampleCount: 2,
208
+ minVelocityPxPerMs: 0.03,
209
+ }
210
+ );
145
211
  }
212
+ resetTouchPanState(interactionState);
213
+ return;
214
+ }
215
+
216
+ if (
217
+ interactionState.touchPanPointerCount !== touchGesture.pointerCount
218
+ ) {
219
+ resetTouchPanState(interactionState);
220
+ interactionState.touchPanPointerCount = touchGesture.pointerCount;
221
+ }
146
222
 
147
- const a = arr.at(-1);
148
- const b = arr[0];
223
+ const currentPoint = new Point(
224
+ event.point.x + xDelta,
225
+ event.point.y + yDelta
226
+ );
227
+ interactionState.touchPanLastPoint = currentPoint;
149
228
 
150
- const v = a.point
151
- .subtract(b.point)
152
- .multiply(1 / (a.timestamp - b.timestamp));
229
+ if (touchGesture.pointerCount === 1 && (xDelta !== 0 || yDelta !== 0)) {
230
+ interactionState.touchPanEventBuffer.push({
231
+ point: currentPoint,
232
+ timestamp: performance.now(),
233
+ });
234
+ }
153
235
 
154
- let x = prevPoint.x;
155
- let y = prevPoint.y;
236
+ if (xDelta === 0 && yDelta === 0 && zDelta === 0) {
237
+ return;
238
+ }
156
239
 
157
- smoother = makeLerpSmoother(
158
- animator,
159
- (p) => {
160
- handleZoom({
161
- x: p.x,
162
- y: p.y,
163
- xDelta: x - p.x,
164
- yDelta: y - p.y,
165
- zDelta: 0,
166
- });
167
- x = p.x;
168
- y = p.y;
169
- },
170
- 150,
171
- 0.5,
172
- { x, y }
173
- );
240
+ // Stop drag-to-pan inertia when touch gestures take over.
241
+ interactionState.smoother?.stop();
174
242
 
175
- smoother({
176
- x: prevPoint.x - v.x * 250,
177
- y: prevPoint.y - v.y * 250,
178
- });
179
- };
243
+ handleZoom({
244
+ x: event.point.x,
245
+ y: event.point.y,
246
+ xDelta,
247
+ yDelta,
248
+ zDelta,
249
+ });
250
+ }
251
+ }
180
252
 
181
- const onMouseup = () => {
182
- document.removeEventListener("mousemove", onMousemove);
183
- document.removeEventListener("mouseup", onMouseup);
184
- animateInertia();
253
+ /**
254
+ * @param {Point} point
255
+ * @param {import("./layout/rectangle.js").default} coords
256
+ * @param {import("../types/viewContext.js").Hover} hover
257
+ */
258
+ function getLinkEndpointSnapPoint(point, coords, hover) {
259
+ if (hover.mark.getType() !== "link") {
260
+ return undefined;
261
+ }
262
+
263
+ const e = hover.mark.encoders;
264
+ if (!(e.x && e.y && e.x2 && e.y2)) {
265
+ return undefined;
266
+ }
267
+
268
+ const snapX = !e.x.constant && !e.x2.constant;
269
+ const snapY = !e.y.constant && !e.y2.constant;
270
+
271
+ if (!snapX && !snapY) {
272
+ return undefined;
273
+ }
274
+
275
+ const x1 =
276
+ getEncoderUnitPosition(e.x, hover.datum) * coords.width + coords.x;
277
+ const y1 =
278
+ (1 - getEncoderUnitPosition(e.y, hover.datum)) * coords.height +
279
+ coords.y;
280
+ const x2 =
281
+ getEncoderUnitPosition(e.x2, hover.datum) * coords.width + coords.x;
282
+ const y2 =
283
+ (1 - getEncoderUnitPosition(e.y2, hover.datum)) * coords.height +
284
+ coords.y;
285
+
286
+ let d1Squared = 0;
287
+ let d2Squared = 0;
288
+
289
+ if (snapX) {
290
+ d1Squared += (point.x - x1) ** 2;
291
+ d2Squared += (point.x - x2) ** 2;
292
+ }
293
+
294
+ if (snapY) {
295
+ d1Squared += (point.y - y1) ** 2;
296
+ d2Squared += (point.y - y2) ** 2;
297
+ }
298
+
299
+ const size = e.size ? +e.size(hover.datum) : 0;
300
+ const snapDistance = Number.isFinite(size)
301
+ ? Math.max(size, MIN_LINK_ENDPOINT_SNAP_DISTANCE)
302
+ : MIN_LINK_ENDPOINT_SNAP_DISTANCE;
303
+ const snapDistanceSquared = snapDistance * snapDistance;
304
+
305
+ if (Math.min(d1Squared, d2Squared) > snapDistanceSquared) {
306
+ return undefined;
307
+ }
308
+
309
+ if (d1Squared <= d2Squared) {
310
+ return {
311
+ x: snapX ? x1 : undefined,
312
+ y: snapY ? y1 : undefined,
313
+ };
314
+ } else {
315
+ return {
316
+ x: snapX ? x2 : undefined,
317
+ y: snapY ? y2 : undefined,
185
318
  };
319
+ }
320
+ }
186
321
 
187
- document.addEventListener("mouseup", onMouseup, false);
188
- document.addEventListener("mousemove", onMousemove, false);
322
+ /**
323
+ * Returns channel position in unit coordinates using the same band placement
324
+ * convention as mark rendering.
325
+ *
326
+ * @param {import("../types/encoder.js").Encoder} encoder
327
+ * @param {import("../data/flowNode.js").Datum} datum
328
+ */
329
+ function getEncoderUnitPosition(encoder, datum) {
330
+ const basePosition = +encoder(datum);
331
+ const scale = encoder.scale;
332
+
333
+ if (!scale) {
334
+ return basePosition;
335
+ }
336
+
337
+ const band = resolveBandPosition(encoder.channelDef);
338
+
339
+ if (scale.type === "band" || scale.type === "point") {
340
+ if (!Number.isFinite(band)) {
341
+ return basePosition;
342
+ }
343
+
344
+ const typedScale = /** @type {{ bandwidth: () => number }} */ (
345
+ /** @type {any} */ (scale)
346
+ );
347
+ return basePosition + typedScale.bandwidth() * band;
348
+ } else if (scale.type === "index" || scale.type === "locus") {
349
+ if (!Number.isFinite(band)) {
350
+ return basePosition;
351
+ }
352
+
353
+ const typedScale =
354
+ /** @type {{ step: () => number, align: () => number }} */ (
355
+ /** @type {any} */ (scale)
356
+ );
357
+ return basePosition + typedScale.step() * (band - typedScale.align());
358
+ } else {
359
+ return basePosition;
189
360
  }
190
361
  }
191
362
 
363
+ /**
364
+ * @param {import("../spec/channel.js").ChannelDef} channelDef
365
+ */
366
+ function resolveBandPosition(channelDef) {
367
+ if (channelDef && "band" in channelDef) {
368
+ return channelDef.band ?? 0.5;
369
+ } else {
370
+ return 0.5;
371
+ }
372
+ }
373
+
374
+ /**
375
+ * @returns {ZoomInteractionState}
376
+ */
377
+ function createInteractionState() {
378
+ return {
379
+ smoother: undefined,
380
+ touchPanEventBuffer: new RingBuffer(30),
381
+ touchPanLastPoint: undefined,
382
+ touchPanPointerCount: 0,
383
+ };
384
+ }
385
+
386
+ /**
387
+ * @param {import("../utils/animator.js").default} [animator]
388
+ */
389
+ function getInteractionState(animator) {
390
+ if (!animator) {
391
+ return fallbackInteractionState;
392
+ }
393
+
394
+ let state = zoomInteractionStates.get(animator);
395
+ if (!state) {
396
+ state = createInteractionState();
397
+ zoomInteractionStates.set(animator, state);
398
+ }
399
+
400
+ return state;
401
+ }
402
+
403
+ /**
404
+ * @param {ZoomInteractionState} interactionState
405
+ */
406
+ function resetTouchPanState(interactionState) {
407
+ interactionState.touchPanEventBuffer = new RingBuffer(30);
408
+ interactionState.touchPanLastPoint = undefined;
409
+ interactionState.touchPanPointerCount = 0;
410
+ }
411
+
412
+ /**
413
+ * @param {ZoomInteractionState} interactionState
414
+ * @param {RingBuffer<{point: Point, timestamp: number}>} eventBuffer
415
+ * @param {Point | undefined} lastPoint
416
+ * @param {(zoomEvent: ZoomEvent) => void} handleZoom
417
+ * @param {import("../utils/animator.js").default} [animator]
418
+ * @param {{minSampleCount?: number, minVelocityPxPerMs?: number}} [options]
419
+ */
420
+ function startPanInertia(
421
+ interactionState,
422
+ eventBuffer,
423
+ lastPoint,
424
+ handleZoom,
425
+ animator,
426
+ options = {}
427
+ ) {
428
+ if (!animator || !lastPoint) {
429
+ return;
430
+ }
431
+
432
+ const minSampleCount = options.minSampleCount ?? 5;
433
+ const minVelocityPxPerMs = options.minVelocityPxPerMs ?? 0;
434
+ const lastMillisToInclude = 160;
435
+ const now = performance.now();
436
+ const arr = eventBuffer
437
+ .get()
438
+ .filter((point) => now - point.timestamp < lastMillisToInclude);
439
+
440
+ if (arr.length < minSampleCount) {
441
+ return;
442
+ }
443
+
444
+ if (arr.length >= 5 && isDecelerating(arr)) {
445
+ return;
446
+ }
447
+
448
+ const a = arr.at(-1);
449
+ const b = arr[0];
450
+ const v = a.point
451
+ .subtract(b.point)
452
+ .multiply(1 / (a.timestamp - b.timestamp));
453
+
454
+ if (!Number.isFinite(v.x) || !Number.isFinite(v.y)) {
455
+ return;
456
+ }
457
+
458
+ if (v.length < minVelocityPxPerMs) {
459
+ return;
460
+ }
461
+
462
+ let x = lastPoint.x;
463
+ let y = lastPoint.y;
464
+
465
+ interactionState.smoother = makeLerpSmoother(
466
+ animator,
467
+ (point) => {
468
+ handleZoom({
469
+ x: point.x,
470
+ y: point.y,
471
+ xDelta: x - point.x,
472
+ yDelta: y - point.y,
473
+ zDelta: 0,
474
+ });
475
+ x = point.x;
476
+ y = point.y;
477
+ },
478
+ 150,
479
+ 0.5,
480
+ { x, y }
481
+ );
482
+
483
+ interactionState.smoother({
484
+ x: lastPoint.x - v.x * 250,
485
+ y: lastPoint.y - v.y * 250,
486
+ });
487
+ }
488
+
192
489
  /**
193
490
  * Split the array into two vectors and compare their lengths to find out if
194
491
  * the mouse movement is decelerating.
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "contributors": [],
9
9
  "license": "MIT",
10
- "version": "0.72.0",
10
+ "version": "0.73.0",
11
11
  "jsdelivr": "dist/bundle/index.js",
12
12
  "unpkg": "dist/bundle/index.js",
13
13
  "browser": "dist/bundle/index.js",
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "scripts": {
29
29
  "dev": "node dev-server.mjs",
30
+ "capture:screenshots": "node scripts/captureScreenshots.mjs",
30
31
  "build": "rm -rf dist && mkdir -p dist && node scripts/build.mjs && vite build && npm run build:schema && npm run build:typings",
31
32
  "prepublishOnly": "npm run build",
32
33
  "test:tsc": "tsc -p tsconfig.json --noEmit",
@@ -50,6 +51,7 @@
50
51
  "@types/d3-scale": "^4.0.9",
51
52
  "d3-array": "^3.2.4",
52
53
  "d3-color": "^3.1.0",
54
+ "d3-dsv": "^3.0.1",
53
55
  "d3-ease": "^3.0.1",
54
56
  "d3-format": "^3.1.0",
55
57
  "events": "^3.3.0",
@@ -68,5 +70,5 @@
68
70
  "devDependencies": {
69
71
  "@types/long": "^4.0.1"
70
72
  },
71
- "gitHead": "504e1d0f7c45c2324bb6b0ff7b9230829d60518e"
73
+ "gitHead": "925d6b740d4bbb3d28a8e6f4a869cfca60c58449"
72
74
  }