@genome-spy/core 0.71.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 (121) hide show
  1. package/LICENSE +1 -1
  2. package/dist/bundle/index.es.js +6842 -5365
  3. package/dist/bundle/index.js +159 -140
  4. package/dist/bundle/parquetRead-BnAGCa4_.js +1663 -0
  5. package/dist/schema.json +281 -17
  6. package/dist/src/data/formats/bed.d.ts +8 -0
  7. package/dist/src/data/formats/bed.d.ts.map +1 -0
  8. package/dist/src/data/formats/bed.js +53 -0
  9. package/dist/src/data/formats/bedpe.d.ts +8 -0
  10. package/dist/src/data/formats/bedpe.d.ts.map +1 -0
  11. package/dist/src/data/formats/bedpe.js +160 -0
  12. package/dist/src/data/formats/parquet.d.ts +12 -0
  13. package/dist/src/data/formats/parquet.d.ts.map +1 -0
  14. package/dist/src/data/formats/parquet.js +29 -0
  15. package/dist/src/data/formats/parquetRead.d.ts +18 -0
  16. package/dist/src/data/formats/parquetRead.d.ts.map +1 -0
  17. package/dist/src/data/formats/parquetRead.js +326 -0
  18. package/dist/src/data/sources/dataUtils.d.ts +16 -0
  19. package/dist/src/data/sources/dataUtils.d.ts.map +1 -1
  20. package/dist/src/data/sources/dataUtils.js +53 -3
  21. package/dist/src/data/sources/urlSource.d.ts +4 -0
  22. package/dist/src/data/sources/urlSource.d.ts.map +1 -1
  23. package/dist/src/data/sources/urlSource.js +141 -17
  24. package/dist/src/encoder/encoder.d.ts +2 -2
  25. package/dist/src/fonts/bmFontManager.d.ts +1 -1
  26. package/dist/src/genome/assemblyPreflight.d.ts +31 -0
  27. package/dist/src/genome/assemblyPreflight.d.ts.map +1 -0
  28. package/dist/src/genome/assemblyPreflight.js +99 -0
  29. package/dist/src/genome/genome.d.ts +2 -2
  30. package/dist/src/genome/genome.d.ts.map +1 -1
  31. package/dist/src/genome/genome.js +4 -0
  32. package/dist/src/genome/genomeStore.d.ts +34 -3
  33. package/dist/src/genome/genomeStore.d.ts.map +1 -1
  34. package/dist/src/genome/genomeStore.js +409 -18
  35. package/dist/src/genome/rootGenomeConfig.d.ts +26 -0
  36. package/dist/src/genome/rootGenomeConfig.d.ts.map +1 -0
  37. package/dist/src/genome/rootGenomeConfig.js +94 -0
  38. package/dist/src/genomeSpy/interactionController.d.ts +5 -1
  39. package/dist/src/genomeSpy/interactionController.d.ts.map +1 -1
  40. package/dist/src/genomeSpy/interactionController.js +244 -29
  41. package/dist/src/genomeSpy/renderCoordinator.js +1 -1
  42. package/dist/src/genomeSpy.d.ts +13 -3
  43. package/dist/src/genomeSpy.d.ts.map +1 -1
  44. package/dist/src/genomeSpy.js +83 -7
  45. package/dist/src/gl/canvasSizeHelper.d.ts +74 -0
  46. package/dist/src/gl/canvasSizeHelper.d.ts.map +1 -0
  47. package/dist/src/gl/canvasSizeHelper.js +203 -0
  48. package/dist/src/gl/hashTable.d.ts +78 -0
  49. package/dist/src/gl/hashTable.d.ts.map +1 -0
  50. package/dist/src/gl/hashTable.js +164 -0
  51. package/dist/src/gl/includes/common.glsl.js +1 -1
  52. package/dist/src/gl/webGLHelper.d.ts +25 -11
  53. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  54. package/dist/src/gl/webGLHelper.js +71 -39
  55. package/dist/src/index.d.ts.map +1 -1
  56. package/dist/src/index.js +5 -2
  57. package/dist/src/marks/link.d.ts.map +1 -1
  58. package/dist/src/marks/link.js +5 -3
  59. package/dist/src/marks/mark.d.ts +1 -1
  60. package/dist/src/marks/mark.d.ts.map +1 -1
  61. package/dist/src/marks/mark.js +8 -4
  62. package/dist/src/scales/domainPlanner.d.ts +34 -3
  63. package/dist/src/scales/domainPlanner.d.ts.map +1 -1
  64. package/dist/src/scales/domainPlanner.js +247 -26
  65. package/dist/src/scales/scaleInstanceManager.d.ts +2 -1
  66. package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -1
  67. package/dist/src/scales/scaleInstanceManager.js +10 -11
  68. package/dist/src/scales/scaleInteractionController.d.ts.map +1 -1
  69. package/dist/src/scales/scaleInteractionController.js +16 -14
  70. package/dist/src/scales/scaleResolution.d.ts +16 -0
  71. package/dist/src/scales/scaleResolution.d.ts.map +1 -1
  72. package/dist/src/scales/scaleResolution.js +314 -54
  73. package/dist/src/scales/scaleResolutionTestUtils.d.ts +21 -0
  74. package/dist/src/scales/scaleResolutionTestUtils.d.ts.map +1 -0
  75. package/dist/src/scales/scaleResolutionTestUtils.js +33 -0
  76. package/dist/src/scales/selectionDomainUtils.d.ts +22 -0
  77. package/dist/src/scales/selectionDomainUtils.d.ts.map +1 -0
  78. package/dist/src/scales/selectionDomainUtils.js +79 -0
  79. package/dist/src/scales/zoomDomainUtils.d.ts +18 -0
  80. package/dist/src/scales/zoomDomainUtils.d.ts.map +1 -0
  81. package/dist/src/scales/zoomDomainUtils.js +69 -0
  82. package/dist/src/screenshotHarness.d.ts +16 -0
  83. package/dist/src/screenshotHarness.d.ts.map +1 -0
  84. package/dist/src/screenshotHarness.js +242 -0
  85. package/dist/src/singlePageApp.js +1 -1
  86. package/dist/src/spec/data.d.ts +23 -3
  87. package/dist/src/spec/genome.d.ts +22 -2
  88. package/dist/src/spec/parameter.d.ts +39 -2
  89. package/dist/src/spec/root.d.ts +20 -1
  90. package/dist/src/spec/scale.d.ts +41 -5
  91. package/dist/src/styles/genome-spy.css +8 -0
  92. package/dist/src/styles/genome-spy.css.d.ts +1 -1
  93. package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
  94. package/dist/src/styles/genome-spy.css.js +8 -0
  95. package/dist/src/tooltip/dataTooltipHandler.js +59 -10
  96. package/dist/src/types/embedApi.d.ts +19 -0
  97. package/dist/src/utils/inferSpecBaseUrl.d.ts +14 -0
  98. package/dist/src/utils/inferSpecBaseUrl.d.ts.map +1 -0
  99. package/dist/src/utils/inferSpecBaseUrl.js +73 -0
  100. package/dist/src/utils/interactionEvent.d.ts +53 -3
  101. package/dist/src/utils/interactionEvent.d.ts.map +1 -1
  102. package/dist/src/utils/interactionEvent.js +62 -1
  103. package/dist/src/utils/radixSort.d.ts.map +1 -1
  104. package/dist/src/utils/radixSort.js +26 -1
  105. package/dist/src/view/containerMutationHelper.d.ts.map +1 -1
  106. package/dist/src/view/containerMutationHelper.js +8 -0
  107. package/dist/src/view/dataReadiness.d.ts +2 -2
  108. package/dist/src/view/dataReadiness.d.ts.map +1 -1
  109. package/dist/src/view/dataReadiness.js +63 -58
  110. package/dist/src/view/facetView.d.ts +1 -1
  111. package/dist/src/view/facetView.js +1 -1
  112. package/dist/src/view/gridView/gridChild.d.ts +7 -0
  113. package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
  114. package/dist/src/view/gridView/gridChild.js +180 -11
  115. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  116. package/dist/src/view/gridView/gridView.js +60 -17
  117. package/dist/src/view/unitView.d.ts +1 -1
  118. package/dist/src/view/zoom.d.ts +14 -2
  119. package/dist/src/view/zoom.d.ts.map +1 -1
  120. package/dist/src/view/zoom.js +373 -76
  121. package/package.json +5 -2
@@ -1,4 +1,5 @@
1
1
  import DataSource from "../data/sources/dataSource.js";
2
+ import SingleAxisLazySource from "../data/sources/lazy/singleAxisLazySource.js";
2
3
  import UnitView from "./unitView.js";
3
4
 
4
5
  /**
@@ -95,54 +96,14 @@ export function isSubtreeReady(subtreeRoot, readinessRequest, viewFilter) {
95
96
  * @returns {boolean}
96
97
  */
97
98
  export function isSubtreeLazyReady(subtreeRoot, readinessRequest, viewFilter) {
98
- const shouldConsiderView =
99
- viewFilter ??
100
- ((/** @type {View} */ view) => view.isConfiguredVisible());
101
-
102
- /** @type {Set<DataSource>} */
103
- const dataSources = new Set();
104
-
105
- subtreeRoot.visit((view) => {
106
- if (!(view instanceof UnitView)) {
107
- return;
108
- }
109
- if (!shouldConsiderView(view)) {
110
- return;
111
- }
112
-
113
- /** @type {View | null} */
114
- let current = view;
115
- while (current) {
116
- if (current.flowHandle && current.flowHandle.dataSource) {
117
- break;
118
- }
119
- current = current.dataParent;
120
- }
121
-
122
- if (!current || !current.flowHandle) {
123
- return;
124
- }
125
- const dataSource = current.flowHandle.dataSource;
126
- if (!("isDataReadyForDomain" in dataSource)) {
127
- return;
128
- }
129
- dataSources.add(dataSource);
130
- });
99
+ const dataSources = collectLazyDataSources(subtreeRoot, viewFilter);
131
100
 
132
101
  if (!dataSources.size) {
133
102
  return true;
134
103
  }
135
104
 
136
- if (!readinessRequest) {
137
- return false;
138
- }
139
-
140
105
  for (const dataSource of dataSources) {
141
- const checkReady =
142
- /** @type {import("../data/sources/lazy/singleAxisLazySource.js").DataReadinessCheckable["isDataReadyForDomain"]} */ (
143
- /** @type {any} */ (dataSource).isDataReadyForDomain
144
- );
145
- if (!checkReady.call(dataSource, readinessRequest)) {
106
+ if (!isLazySourceReady(dataSource, readinessRequest)) {
146
107
  return false;
147
108
  }
148
109
  }
@@ -156,7 +117,7 @@ export function isSubtreeLazyReady(subtreeRoot, readinessRequest, viewFilter) {
156
117
  *
157
118
  * @param {import("../types/viewContext.js").default} context
158
119
  * @param {View} subtreeRoot
159
- * @param {DataReadinessRequest} readinessRequest
120
+ * @param {DataReadinessRequest | undefined} readinessRequest
160
121
  * @param {AbortSignal} [signal]
161
122
  * @param {(view: View) => boolean} [viewFilter]
162
123
  * @returns {Promise<void>}
@@ -172,21 +133,6 @@ export function awaitSubtreeLazyReady(
172
133
  viewFilter ??
173
134
  ((/** @type {View} */ view) => view.isConfiguredVisible());
174
135
 
175
- if (!readinessRequest) {
176
- if (
177
- isSubtreeLazyReady(
178
- subtreeRoot,
179
- readinessRequest,
180
- shouldConsiderView
181
- )
182
- ) {
183
- return Promise.resolve();
184
- }
185
- return Promise.reject(
186
- new Error("Lazy subtree readiness requires a readiness request.")
187
- );
188
- }
189
-
190
136
  return new Promise((resolve, reject) => {
191
137
  /** @type {Set<() => void>} */
192
138
  const unregisters = new Set();
@@ -265,3 +211,62 @@ export function awaitSubtreeLazyReady(
265
211
  }
266
212
  });
267
213
  }
214
+
215
+ /**
216
+ * @param {View} subtreeRoot
217
+ * @param {(view: View) => boolean} [viewFilter]
218
+ * @returns {Set<SingleAxisLazySource>}
219
+ */
220
+ function collectLazyDataSources(subtreeRoot, viewFilter) {
221
+ const shouldConsiderView =
222
+ viewFilter ??
223
+ ((/** @type {View} */ view) => view.isConfiguredVisible());
224
+
225
+ /** @type {Set<SingleAxisLazySource>} */
226
+ const dataSources = new Set();
227
+
228
+ subtreeRoot.visit((view) => {
229
+ if (!(view instanceof UnitView)) {
230
+ return;
231
+ }
232
+ if (!shouldConsiderView(view)) {
233
+ return;
234
+ }
235
+
236
+ /** @type {View | null} */
237
+ let current = view;
238
+ while (current) {
239
+ if (current.flowHandle && current.flowHandle.dataSource) {
240
+ break;
241
+ }
242
+ current = current.dataParent;
243
+ }
244
+
245
+ if (!current || !current.flowHandle) {
246
+ return;
247
+ }
248
+
249
+ const dataSource = current.flowHandle.dataSource;
250
+ if (!(dataSource instanceof SingleAxisLazySource)) {
251
+ return;
252
+ }
253
+
254
+ dataSources.add(dataSource);
255
+ });
256
+
257
+ return dataSources;
258
+ }
259
+
260
+ /**
261
+ * @param {SingleAxisLazySource} dataSource
262
+ * @param {DataReadinessRequest | undefined} readinessRequest
263
+ */
264
+ function isLazySourceReady(dataSource, readinessRequest) {
265
+ const request =
266
+ readinessRequest ??
267
+ ({
268
+ [dataSource.channel]: Array.from(dataSource.scaleResolution.getDomain()),
269
+ });
270
+
271
+ return dataSource.isDataReadyForDomain(request);
272
+ }
@@ -48,7 +48,7 @@ export default class FacetView extends ContainerView<import("../spec/view.js").C
48
48
  getAccessor(channel: "row" | "column"): any;
49
49
  updateFacets(): void;
50
50
  updateLabels(): void;
51
- getFacetGroups(): string[] | number[] | boolean[];
51
+ getFacetGroups(): number[] | boolean[] | string[];
52
52
  /**
53
53
  * @param {import("./renderingContext/viewRenderingContext.js").default} context
54
54
  * @param {import("./layout/rectangle.js").default} coords
@@ -354,7 +354,7 @@ export default class FacetView extends ContainerView {
354
354
  coords[channel == "column" ? "width" : "height"],
355
355
  {
356
356
  spacing,
357
- devicePixelRatio: window.devicePixelRatio,
357
+ devicePixelRatio: context.getDevicePixelRatio(),
358
358
  }
359
359
  );
360
360
  };
@@ -1,3 +1,10 @@
1
+ /**
2
+ * @param {import("../../spec/parameter.js").IntervalSelectionConfig["zoom"]} zoom
3
+ * @param {boolean} hasZoomableChannel
4
+ * @param {string} paramName
5
+ * @returns {import("../../spec/parameter.js").EventConfig | undefined}
6
+ */
7
+ export function resolveIntervalZoomEventConfig(zoom: import("../../spec/parameter.js").IntervalSelectionConfig["zoom"], hasZoomableChannel: boolean, paramName: string): import("../../spec/parameter.js").EventConfig | undefined;
1
8
  /**
2
9
  * @param {import("../../spec/view.js").ViewBackground} viewBackground
3
10
  * @returns {import("../../spec/view.js").UnitSpec}
@@ -1 +1 @@
1
- {"version":3,"file":"gridChild.d.ts","sourceRoot":"","sources":["../../../../src/view/gridView/gridChild.js"],"names":[],"mappings":"AAysBA;;;GAGG;AACH,iDAHW,OAAO,oBAAoB,EAAE,cAAc,GACzC,OAAO,oBAAoB,EAAE,QAAQ,CA6BjD;AAED;;;GAGG;AACH,uDAHW,OAAO,oBAAoB,EAAE,cAAc,GACzC,OAAO,oBAAoB,EAAE,QAAQ,CA4CjD;AApwBD;IACI;;;;;;OAMG;IAEH;;;;OAIG;IACH,kBAJW,OAAO,YAAY,EAAE,OAAO,gBAC5B,OAAO,qBAAqB,EAAE,OAAO,UACrC,MAAM,EAgGhB;IA7FG,gGAAgC;IAChC,0EAAgB;IAChB,eAAoB;IAEpB,uBAAuB;IACvB,YADW,QAAQ,CACQ;IAE3B,uBAAuB;IACvB,kBADW,QAAQ,CACc;IAEjC,sFAAsF;IACtF,MADW,OAAO,CAAC,MAAM,CAAC,OAAO,oBAAoB,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAC/D;IAEd,+FAA+F;IAC/F,WADW,OAAO,CAAC,MAAM,CAAC,OAAO,oBAAoB,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAC9D;IAEnB,mFAAmF;IACnF,YADW,OAAO,CAAC,MAAM,CAAC,OAAO,gBAAgB,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC,CAC3D;IAEpB,4BAA4B;IAC5B,eADW,aAAa,CACM;IAE9B,uBAAuB;IACvB,OADW,QAAQ,CACG;IAEtB,wBAAwB;IACxB,QADW,SAAS,CACQ;IAgahC,6GAiBC;IAED;;OAEG;IACH,4BAwKC;IAED;;OAEG;IACH,yBAWC;IAED,uBAqBC;IAED,iCAEC;;CACJ;qBAxrBoB,gBAAgB;qBANK,gBAAgB;yBADjC,oBAAoB;sBASvB,gBAAgB;0BACZ,oBAAoB;sBALxB,wBAAwB;oBAF1B,sBAAsB"}
1
+ {"version":3,"file":"gridChild.d.ts","sourceRoot":"","sources":["../../../../src/view/gridView/gridChild.js"],"names":[],"mappings":"AAu0BA;;;;;GAKG;AACH,qDALW,OAAO,yBAAyB,EAAE,uBAAuB,CAAC,MAAM,CAAC,sBACjE,OAAO,aACP,MAAM,GACJ,OAAO,yBAAyB,EAAE,WAAW,GAAG,SAAS,CAyBrE;AAcD;;;GAGG;AACH,iDAHW,OAAO,oBAAoB,EAAE,cAAc,GACzC,OAAO,oBAAoB,EAAE,QAAQ,CA6BjD;AAED;;;GAGG;AACH,uDAHW,OAAO,oBAAoB,EAAE,cAAc,GACzC,OAAO,oBAAoB,EAAE,QAAQ,CA4CjD;AAx6BD;IACI;;;;;;OAMG;IAEH;;;;OAIG;IACH,kBAJW,OAAO,YAAY,EAAE,OAAO,gBAC5B,OAAO,qBAAqB,EAAE,OAAO,UACrC,MAAM,EAgGhB;IA7FG,gGAAgC;IAChC,0EAAgB;IAChB,eAAoB;IAEpB,uBAAuB;IACvB,YADW,QAAQ,CACQ;IAE3B,uBAAuB;IACvB,kBADW,QAAQ,CACc;IAEjC,sFAAsF;IACtF,MADW,OAAO,CAAC,MAAM,CAAC,OAAO,oBAAoB,EAAE,UAAU,EAAE,QAAQ,CAAC,CAAC,CAC/D;IAEd,+FAA+F;IAC/F,WADW,OAAO,CAAC,MAAM,CAAC,OAAO,oBAAoB,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAC9D;IAEnB,mFAAmF;IACnF,YADW,OAAO,CAAC,MAAM,CAAC,OAAO,gBAAgB,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC,CAC3D;IAEpB,4BAA4B;IAC5B,eADW,aAAa,CACM;IAE9B,uBAAuB;IACvB,OADW,QAAQ,CACG;IAEtB,wBAAwB;IACxB,QADW,SAAS,CACQ;IAyhBhC,6GAiBC;IAED;;OAEG;IACH,4BAwKC;IAED;;OAEG;IACH,yBAWC;IAED,uBAqBC;IAED,iCAEC;;CACJ;qBApzBoB,gBAAgB;qBANK,gBAAgB;yBADjC,oBAAoB;sBASvB,gBAAgB;0BACZ,oBAAoB;sBALxB,wBAAwB;oBAF1B,sBAAsB"}
@@ -1,11 +1,13 @@
1
1
  import { isContinuous } from "vega-scale";
2
2
  import {
3
+ asEventConfig,
3
4
  asSelectionConfig,
4
5
  createIntervalSelection,
5
6
  isActiveIntervalSelection,
6
7
  isIntervalSelectionConfig,
7
8
  selectionContainsPoint,
8
9
  } from "../../selection/selection.js";
10
+ import { createPrimitiveEventProxy } from "../../utils/interactionEvent.js";
9
11
  import AxisGridView from "../axisGridView.js";
10
12
  import AxisView, { CHANNEL_ORIENTS } from "../axisView.js";
11
13
  import LayerView from "../layerView.js";
@@ -17,6 +19,9 @@ import UnitView from "../unitView.js";
17
19
  import { markViewAsNonAddressable } from "../viewSelectors.js";
18
20
  import Scrollbar from "./scrollbar.js";
19
21
  import SelectionRect from "./selectionRect.js";
22
+ import { normalizeIntervalForSelection } from "../../scales/selectionDomainUtils.js";
23
+ import { zoomDomainByScaleType } from "../../scales/zoomDomainUtils.js";
24
+ import { createEventFilterFunction } from "../../utils/expression.js";
20
25
 
21
26
  export default class GridChild {
22
27
  /**
@@ -164,6 +169,42 @@ export default class GridChild {
164
169
  })
165
170
  );
166
171
 
172
+ const requiresShiftToBrush = channels.some((channel) =>
173
+ scaleResolutions[channel].isZoomable()
174
+ );
175
+
176
+ const eventConfig =
177
+ /** @type {import("../../spec/parameter.js").EventConfig} */ (
178
+ select.on ??
179
+ (requiresShiftToBrush
180
+ ? {
181
+ type: "mousedown",
182
+ filter: "event.shiftKey",
183
+ }
184
+ : {
185
+ type: "mousedown",
186
+ })
187
+ );
188
+
189
+ if (eventConfig.type !== "mousedown") {
190
+ throw new Error(
191
+ `Interval selection param "${name}" currently supports only "mousedown" in "on".`
192
+ );
193
+ }
194
+
195
+ const eventPredicate = eventConfig.filter
196
+ ? createEventFilterFunction(eventConfig.filter)
197
+ : () => true;
198
+
199
+ const zoomEventConfig = resolveIntervalZoomEventConfig(
200
+ select.zoom,
201
+ requiresShiftToBrush,
202
+ name
203
+ );
204
+ const zoomEventPredicate = zoomEventConfig?.filter
205
+ ? createEventFilterFunction(zoomEventConfig.filter)
206
+ : () => true;
207
+
167
208
  if (this.selectionRect) {
168
209
  throw new Error(
169
210
  "Only one interval selection per container is currently allowed!"
@@ -304,7 +345,9 @@ export default class GridChild {
304
345
  preventNextClickPropagation = true;
305
346
  }
306
347
 
307
- const startSelection = event.mouseEvent.shiftKey;
348
+ const startSelection = eventPredicate(
349
+ event.proxiedMouseEvent
350
+ );
308
351
 
309
352
  if (startSelection) {
310
353
  clearSelection();
@@ -373,15 +416,9 @@ export default class GridChild {
373
416
 
374
417
  for (const channel of channels) {
375
418
  const scaleResolution = scaleResolutions[channel];
376
- const { zoomExtent, scale } = scaleResolution;
419
+ const { zoomExtent } = scaleResolution;
377
420
  const interval = intervals[channel];
378
421
 
379
- if (["index", "locus"].includes(scale.type)) {
380
- // These scales use integer values. Need to round them.
381
- interval[0] = Math.ceil(interval[0]);
382
- interval[1] = Math.ceil(interval[1]);
383
- }
384
-
385
422
  if (translatedRectangle) {
386
423
  // When dragging, clamp the interval so that the size stays the same and the interval doesn't exceed zoomExtent
387
424
  const size = interval[1] - interval[0];
@@ -398,11 +435,20 @@ export default class GridChild {
398
435
  interval[1] = max;
399
436
  interval[0] = max - size;
400
437
  }
438
+ }
439
+
440
+ const normalized = normalizeIntervalForChannel(
441
+ scaleResolution,
442
+ interval
443
+ );
444
+
445
+ if (!normalized) {
446
+ interval[0] = zoomExtent[0];
447
+ interval[1] = zoomExtent[0];
401
448
  } else {
402
- interval[0] = Math.max(zoomExtent[0], interval[0]);
403
- interval[1] = Math.min(zoomExtent[1], interval[1]);
449
+ interval[0] = normalized[0];
450
+ interval[1] = normalized[1];
404
451
  }
405
- interval[1] = Math.min(zoomExtent[1], interval[1]);
406
452
  }
407
453
 
408
454
  setter({ type: "interval", intervals });
@@ -454,6 +500,86 @@ export default class GridChild {
454
500
  true
455
501
  );
456
502
 
503
+ view.addInteractionEventListener("wheel", (coords, event) => {
504
+ const wheelEvent = event.uiEvent;
505
+ if (!(wheelEvent instanceof WheelEvent)) {
506
+ return;
507
+ }
508
+
509
+ if (
510
+ !zoomEventConfig ||
511
+ !zoomEventPredicate(createPrimitiveEventProxy(wheelEvent))
512
+ ) {
513
+ return;
514
+ }
515
+
516
+ if (
517
+ Math.abs(wheelEvent.deltaX) >= Math.abs(wheelEvent.deltaY)
518
+ ) {
519
+ return;
520
+ }
521
+ if (!isPointInsideSelection(event.point)) {
522
+ return;
523
+ }
524
+
525
+ const selection = selectionExpr();
526
+ if (!isActiveIntervalSelection(selection)) {
527
+ return;
528
+ }
529
+
530
+ const wheelMultiplier = wheelEvent.deltaMode ? 120 : 1;
531
+ const scaleFactor =
532
+ 2 ** ((wheelEvent.deltaY * wheelMultiplier) / 300);
533
+
534
+ const anchor = invertPoint(event.point);
535
+ /** @type {typeof selection.intervals} */
536
+ const intervals = { ...selection.intervals };
537
+ let changed = false;
538
+
539
+ for (const channel of channels) {
540
+ const currentInterval = intervals[channel];
541
+ if (!currentInterval || currentInterval.length !== 2) {
542
+ continue;
543
+ }
544
+
545
+ const scaleResolution = scaleResolutions[channel];
546
+ const scale = scaleResolution.getScale();
547
+ const zoomed = zoomDomainByScaleType(
548
+ scale,
549
+ /** @type {[number, number]} */ ([...currentInterval]),
550
+ anchor[channel],
551
+ scaleFactor,
552
+ { onUnsupported: "identity" }
553
+ );
554
+
555
+ const normalized = normalizeIntervalForChannel(
556
+ scaleResolution,
557
+ zoomed
558
+ );
559
+ if (!normalized) {
560
+ continue;
561
+ }
562
+
563
+ if (
564
+ normalized[0] !== currentInterval[0] ||
565
+ normalized[1] !== currentInterval[1]
566
+ ) {
567
+ intervals[channel] = normalized;
568
+ changed = true;
569
+ }
570
+ }
571
+
572
+ if (changed) {
573
+ setter({
574
+ ...selection,
575
+ type: "interval",
576
+ intervals,
577
+ });
578
+ wheelEvent.preventDefault();
579
+ event.stopPropagation();
580
+ }
581
+ });
582
+
457
583
  // Handle mouse cursor changes
458
584
  view.addInteractionEventListener("mousemove", (coords, event) => {
459
585
  if (isPointInsideSelection(event.point)) {
@@ -711,6 +837,49 @@ export default class GridChild {
711
837
  }
712
838
  }
713
839
 
840
+ /**
841
+ * @param {import("../../spec/parameter.js").IntervalSelectionConfig["zoom"]} zoom
842
+ * @param {boolean} hasZoomableChannel
843
+ * @param {string} paramName
844
+ * @returns {import("../../spec/parameter.js").EventConfig | undefined}
845
+ */
846
+ export function resolveIntervalZoomEventConfig(
847
+ zoom,
848
+ hasZoomableChannel,
849
+ paramName
850
+ ) {
851
+ const defaultEnabled = !hasZoomableChannel;
852
+ const resolved = zoom === undefined ? defaultEnabled : zoom;
853
+ if (resolved === false) {
854
+ return;
855
+ }
856
+
857
+ if (resolved === true) {
858
+ return { type: "wheel" };
859
+ }
860
+
861
+ const eventConfig = asEventConfig(resolved);
862
+ if (eventConfig.type !== "wheel") {
863
+ throw new Error(
864
+ `Interval selection param "${paramName}" currently supports only "wheel" in "zoom".`
865
+ );
866
+ }
867
+
868
+ return eventConfig;
869
+ }
870
+
871
+ /**
872
+ * @param {import("../../scales/scaleResolution.js").default} scaleResolution
873
+ * @param {[number, number]} interval
874
+ * @returns {[number, number] | undefined}
875
+ */
876
+ function normalizeIntervalForChannel(scaleResolution, interval) {
877
+ const scale = scaleResolution.getScale();
878
+ return normalizeIntervalForSelection(interval, scaleResolution.zoomExtent, {
879
+ roundToIntegers: scale.type === "index" || scale.type === "locus",
880
+ });
881
+ }
882
+
714
883
  /**
715
884
  * @param {import("../../spec/view.js").ViewBackground} viewBackground
716
885
  * @returns {import("../../spec/view.js").UnitSpec}
@@ -1 +1 @@
1
- {"version":3,"file":"gridView.d.ts","sourceRoot":"","sources":["../../../../src/view/gridView/gridView.js"],"names":[],"mappings":"AAu8BA;;GAEG;AACH,sHAUC;AAmBD;;;;;GAKG;AACH,4CAJW,OAAO,wBAAwB,EAAE,OAAO,UACxC,OAAO,oBAAoB,EAAE,UAAU,YACvC,QAAQ,aAmBlB;AAx+BD;;;;;;;;;;;;;;;;;;GAkBG;AACH,8BAH2D,KAAK,SAAnD,OAAQ,oBAAoB,EAAE,aAAc;IA6CrD;;;;;;;;;OASG;IACH,kBARW,KAAK,WACL,OAAO,4BAA4B,EAAE,OAAO,gBAC5C,aAAa,yFAEb,MAAM,WACN,MAAM,YACN,OAAO,YAAY,EAAE,WAAW,EAyC1C;IAtBG,uBAA0B;IAwB9B;;OAEG;IACH,6FAEC;IAED;;;;;;OAMG;IACH,4FAFa,SAAS,CAIrB;IAED;;;;;;;OAOG;IACH,oGAHW,MAAM,GACJ,SAAS,CASrB;IAED;;;;;OAKG;IACH,iGAQC;IAED;;;;;OAKG;IACH,qBAFW,MAAM,QAUhB;IAeD;;OAEG;IACH,mBAFW,qEAAM,QAWhB;IAYD;;OAEG;IACH,sFAEC;IAED,yBAEC;IAED;;OAEG;IACH,sCAKC;IAED;;;;OAIG;IACH,gCAqCC;;CAwpBJ;qBAz7B0D,gBAAgB;sBADrD,wBAAwB;0BAEpB,qBAAqB;sBAIzB,gBAAgB"}
1
+ {"version":3,"file":"gridView.d.ts","sourceRoot":"","sources":["../../../../src/view/gridView/gridView.js"],"names":[],"mappings":"AAi+BA;;GAEG;AACH,sHAUC;AAoCD;;;;;GAKG;AACH,4CAJW,OAAO,wBAAwB,EAAE,OAAO,UACxC,OAAO,oBAAoB,EAAE,UAAU,YACvC,QAAQ,aAmBlB;AAnhCD;;;;;;;;;;;;;;;;;;GAkBG;AACH,8BAH2D,KAAK,SAAnD,OAAQ,oBAAoB,EAAE,aAAc;IA6CrD;;;;;;;;;OASG;IACH,kBARW,KAAK,WACL,OAAO,4BAA4B,EAAE,OAAO,gBAC5C,aAAa,yFAEb,MAAM,WACN,MAAM,YACN,OAAO,YAAY,EAAE,WAAW,EAyC1C;IAtBG,uBAA0B;IAwB9B;;OAEG;IACH,6FAEC;IAED;;;;;;OAMG;IACH,4FAFa,SAAS,CAIrB;IAED;;;;;;;OAOG;IACH,oGAHW,MAAM,GACJ,SAAS,CASrB;IAED;;;;;OAKG;IACH,iGAQC;IAED;;;;;OAKG;IACH,qBAFW,MAAM,QAUhB;IAeD;;OAEG;IACH,mBAFW,qEAAM,QAWhB;IAYD;;OAEG;IACH,sFAEC;IAED,yBAEC;IAED;;OAEG;IACH,sCAKC;IAED;;;;OAIG;IACH,gCAqCC;;CAkrBJ;qBAn9B0D,gBAAgB;sBADrD,wBAAwB;0BAEpB,qBAAqB;sBAIzB,gBAAgB"}
@@ -871,6 +871,26 @@ export default class GridView extends ContainerView {
871
871
  const pointedChild = this.#visibleChildren.find((gridChild) =>
872
872
  gridChild.coords.containsPoint(event.point.x, event.point.y)
873
873
  );
874
+ const pointedView = pointedChild?.view;
875
+
876
+ if (event.type === "wheelclaimprobe") {
877
+ // Probe path: claim wheel ownership without executing regular wheel
878
+ // behavior. InteractionController uses this to decide whether native
879
+ // wheel should be preventDefault()'ed before inertia kicks in.
880
+ if (!pointedView) {
881
+ return;
882
+ }
883
+
884
+ if (isZoomInteractionView(pointedView)) {
885
+ if (hasZoomableResolutions(pointedView)) {
886
+ event.claimWheel();
887
+ }
888
+ } else {
889
+ pointedView.propagateInteractionEvent(event);
890
+ }
891
+ return;
892
+ }
893
+
874
894
  this.#keyboardZoomController?.handlePointerEvent(pointedChild, event);
875
895
 
876
896
  for (const scrollbar of Object.values(pointedChild?.scrollbars ?? {})) {
@@ -882,7 +902,6 @@ export default class GridView extends ContainerView {
882
902
  }
883
903
  }
884
904
 
885
- const pointedView = pointedChild?.view;
886
905
  if (pointedView) {
887
906
  pointedView.propagateInteractionEvent(event);
888
907
 
@@ -892,10 +911,7 @@ export default class GridView extends ContainerView {
892
911
 
893
912
  // Hmm, maybe this should be registered when needed and not include
894
913
  // as a hardcoded interaction?
895
- if (
896
- pointedView instanceof UnitView ||
897
- pointedView instanceof LayerView
898
- ) {
914
+ if (isZoomInteractionView(pointedView)) {
899
915
  interactionToZoom(
900
916
  event,
901
917
  pointedChild.coords,
@@ -923,8 +939,22 @@ export default class GridView extends ContainerView {
923
939
  * @param {import("../layout/rectangle.js").default} coords Coordinates
924
940
  * @param {View} view
925
941
  * @param {import("../zoom.js").ZoomEvent} zoomEvent
942
+ * @returns {boolean} `true` when there was at least one zoomable resolution
926
943
  */
927
944
  #handleZoom(coords, view, zoomEvent) {
945
+ let zoomable = false;
946
+ let changed = false;
947
+
948
+ const p = coords.normalizePoint(zoomEvent.x, zoomEvent.y);
949
+ const tp = coords.normalizePoint(
950
+ zoomEvent.x + zoomEvent.xDelta,
951
+ zoomEvent.y + zoomEvent.yDelta
952
+ );
953
+ const delta = {
954
+ x: tp.x - p.x,
955
+ y: tp.y - p.y,
956
+ };
957
+
928
958
  for (const [channel, resolutionSet] of Object.entries(
929
959
  getZoomableResolutions(view)
930
960
  )) {
@@ -932,27 +962,23 @@ export default class GridView extends ContainerView {
932
962
  continue;
933
963
  }
934
964
 
935
- const p = coords.normalizePoint(zoomEvent.x, zoomEvent.y);
936
- const tp = coords.normalizePoint(
937
- zoomEvent.x + zoomEvent.xDelta,
938
- zoomEvent.y + zoomEvent.yDelta
939
- );
940
-
941
- const delta = {
942
- x: tp.x - p.x,
943
- y: tp.y - p.y,
944
- };
965
+ zoomable = true;
945
966
 
946
967
  for (const resolution of resolutionSet) {
947
- resolution.zoom(
968
+ const resolutionChanged = resolution.zoom(
948
969
  2 ** zoomEvent.zDelta,
949
970
  channel == "y" ? 1 - p[channel] : p[channel],
950
971
  channel == "x" ? delta.x : -delta.y
951
972
  );
973
+ changed = resolutionChanged || changed;
952
974
  }
953
975
  }
954
976
 
955
- this.context.animator.requestRender();
977
+ if (changed) {
978
+ this.context.animator.requestRender();
979
+ }
980
+
981
+ return zoomable;
956
982
  }
957
983
 
958
984
  /**
@@ -980,6 +1006,23 @@ export function isClippedChildren(view) {
980
1006
  return clipped;
981
1007
  }
982
1008
 
1009
+ /**
1010
+ * @param {View} view
1011
+ * @returns {boolean}
1012
+ */
1013
+ function hasZoomableResolutions(view) {
1014
+ const zoomableResolutions = getZoomableResolutions(view);
1015
+ return zoomableResolutions.x.size > 0 || zoomableResolutions.y.size > 0;
1016
+ }
1017
+
1018
+ /**
1019
+ * @param {View} view
1020
+ * @returns {view is UnitView | LayerView}
1021
+ */
1022
+ function isZoomInteractionView(view) {
1023
+ return view instanceof UnitView || view instanceof LayerView;
1024
+ }
1025
+
983
1026
  /**
984
1027
  * @param {import("../../spec/view.js").AnyConcatSpec} spec
985
1028
  * @returns {("horizontal" | "vertical")[]}
@@ -21,7 +21,7 @@ export default class UnitView<TSpec extends import("../spec/view.js").UnitSpec =
21
21
  constructor(spec: TSpec, context: import("../types/viewContext.js").default, layoutParent: import("./containerView.js").default, dataParent: import("./view.js").default, name: string, options?: import("./view.js").ViewOptions);
22
22
  /** @type {import("../marks/mark.js").default} */
23
23
  mark: import("../marks/mark.js").default;
24
- getMarkType(): "link" | "rect" | "text" | "point" | "rule";
24
+ getMarkType(): "link" | "point" | "text" | "rect" | "rule";
25
25
  /**
26
26
  * Pulls scales and axes up in the view hierarcy according to the resolution rules, using dataParents.
27
27
  * TODO: legends
@@ -3,11 +3,11 @@ export function isStillZooming(): boolean;
3
3
  /**
4
4
  * @param {import("../utils/interactionEvent.js").default} event
5
5
  * @param {import("./layout/rectangle.js").default} coords
6
- * @param {(zoomEvent: ZoomEvent) => void} handleZoom
6
+ * @param {(zoomEvent: ZoomEvent) => boolean | void} handleZoom
7
7
  * @param {import("../types/viewContext.js").Hover} [hover]
8
8
  * @param {import("../utils/animator.js").default} [animator]
9
9
  */
10
- export function interactionToZoom(event: import("../utils/interactionEvent.js").default, coords: import("./layout/rectangle.js").default, handleZoom: (zoomEvent: ZoomEvent) => void, hover?: import("../types/viewContext.js").Hover, animator?: import("../utils/animator.js").default): void;
10
+ export function interactionToZoom(event: import("../utils/interactionEvent.js").default, coords: import("./layout/rectangle.js").default, handleZoom: (zoomEvent: ZoomEvent) => boolean | void, hover?: import("../types/viewContext.js").Hover, animator?: import("../utils/animator.js").default): void;
11
11
  export type ZoomEvent = {
12
12
  x: number;
13
13
  y: number;
@@ -15,4 +15,16 @@ export type ZoomEvent = {
15
15
  yDelta: number;
16
16
  zDelta: number;
17
17
  };
18
+ export type ZoomInteractionState = {
19
+ smoother: ReturnType<typeof makeLerpSmoother>;
20
+ touchPanEventBuffer: RingBuffer<{
21
+ point: Point;
22
+ timestamp: number;
23
+ }>;
24
+ touchPanLastPoint: Point | undefined;
25
+ touchPanPointerCount: 0 | 1 | 2;
26
+ };
27
+ import { makeLerpSmoother } from "../utils/animator.js";
28
+ import RingBuffer from "../utils/ringBuffer.js";
29
+ import Point from "./layout/point.js";
18
30
  //# sourceMappingURL=zoom.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"zoom.d.ts","sourceRoot":"","sources":["../../../src/view/zoom.js"],"names":[],"mappings":"AAkBA,yCAEC;AAED,0CAGC;AAgBD;;;;;;GAMG;AACH,yCANW,OAAO,8BAA8B,EAAE,OAAO,UAC9C,OAAO,uBAAuB,EAAE,OAAO,cACvC,CAAC,SAAS,EAAE,SAAS,KAAK,IAAI,UAC9B,OAAO,yBAAyB,EAAE,KAAK,aACvC,OAAO,sBAAsB,EAAE,OAAO,QA+IhD;;OA3LS,MAAM;OACN,MAAM;YACN,MAAM;YACN,MAAM;YACN,MAAM"}
1
+ {"version":3,"file":"zoom.d.ts","sourceRoot":"","sources":["../../../src/view/zoom.js"],"names":[],"mappings":"AAgCA,yCAEC;AAED,0CAGC;AAgBD;;;;;;GAMG;AACH,yCANW,OAAO,8BAA8B,EAAE,OAAO,UAC9C,OAAO,uBAAuB,EAAE,OAAO,cACvC,CAAC,SAAS,EAAE,SAAS,KAAK,OAAO,GAAG,IAAI,UACxC,OAAO,yBAAyB,EAAE,KAAK,aACvC,OAAO,sBAAsB,EAAE,OAAO,QA8LhD;;OAxPS,MAAM;OACN,MAAM;YACN,MAAM;YACN,MAAM;YACN,MAAM;;;cAUN,UAAU,CAAC,OAAO,gBAAgB,CAAC;yBACnC,UAAU,CAAC;QAAC,KAAK,EAAE,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,CAAC;uBAC7C,KAAK,GAAG,SAAS;0BACjB,CAAC,GAAG,CAAC,GAAG,CAAC;;iCAVc,sBAAsB;uBAChC,wBAAwB;kBAE7B,mBAAmB"}