@genome-spy/core 0.19.0 → 0.21.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 (101) hide show
  1. package/dist/index.js +46 -119
  2. package/dist/schema.json +213 -25
  3. package/package.json +4 -3
  4. package/src/data/collector.test.js +2 -0
  5. package/src/data/dataFlow.test.js +2 -0
  6. package/src/data/flow.test.js +1 -0
  7. package/src/data/flowNode.test.js +1 -0
  8. package/src/data/flowOptimizer.test.js +1 -0
  9. package/src/data/formats/fasta.test.js +1 -0
  10. package/src/data/sources/inlineSource.test.js +1 -0
  11. package/src/data/sources/sequenceSource.test.js +1 -0
  12. package/src/data/transforms/clone.test.js +1 -0
  13. package/src/data/transforms/coverage.test.js +1 -0
  14. package/src/data/transforms/filter.test.js +1 -0
  15. package/src/data/transforms/flattenDelimited.test.js +1 -0
  16. package/src/data/transforms/flattenSequence.test.js +1 -0
  17. package/src/data/transforms/formula.test.js +1 -0
  18. package/src/data/transforms/identifier.test.js +1 -0
  19. package/src/data/transforms/pileup.test.js +1 -0
  20. package/src/data/transforms/project.test.js +1 -0
  21. package/src/data/transforms/regexExtract.test.js +1 -0
  22. package/src/data/transforms/regexFold.test.js +1 -0
  23. package/src/data/transforms/sample.test.js +1 -0
  24. package/src/data/transforms/stack.test.js +1 -0
  25. package/src/encoder/accessor.test.js +1 -0
  26. package/src/encoder/encoder.test.js +1 -0
  27. package/src/genome/genome.test.js +1 -0
  28. package/src/genome/scaleIndex.js +3 -2
  29. package/src/genome/scaleIndex.test.js +23 -6
  30. package/src/genome/scaleLocus.test.js +1 -0
  31. package/src/genomeSpy.js +16 -11
  32. package/src/gl/dataToVertices.js +52 -52
  33. package/src/gl/includes/common.glsl +12 -12
  34. package/src/gl/includes/picking.fragment.glsl +0 -2
  35. package/src/gl/includes/picking.vertex.glsl +0 -2
  36. package/src/gl/includes/scales.glsl +33 -2
  37. package/src/gl/point.vertex.glsl +0 -2
  38. package/src/gl/rule.vertex.glsl +1 -1
  39. package/src/gl/webGLHelper.js +0 -3
  40. package/src/marks/link.js +32 -39
  41. package/src/marks/mark.js +176 -106
  42. package/src/marks/pointMark.js +28 -59
  43. package/src/marks/rectMark.js +38 -33
  44. package/src/marks/rule.js +31 -21
  45. package/src/marks/text.js +18 -14
  46. package/src/scale/glslScaleGenerator.js +56 -17
  47. package/src/scale/scale.test.js +1 -0
  48. package/src/scale/ticks.test.js +1 -0
  49. package/src/spec/mark.d.ts +0 -3
  50. package/src/spec/scale.d.ts +0 -9
  51. package/src/spec/title.d.ts +102 -0
  52. package/src/spec/view.d.ts +6 -4
  53. package/src/tooltip/dataTooltipHandler.js +3 -2
  54. package/src/utils/addBaseUrl.test.js +1 -0
  55. package/src/utils/binnedIndex.js +147 -0
  56. package/src/utils/binnedIndex.test.js +73 -0
  57. package/src/utils/cloner.test.js +1 -0
  58. package/src/utils/coalesce.test.js +1 -0
  59. package/src/utils/concatIterables.test.js +1 -0
  60. package/src/utils/domainArray.test.js +1 -0
  61. package/src/utils/indexer.test.js +1 -0
  62. package/src/utils/iterateNestedMaps.test.js +1 -0
  63. package/src/utils/kWayMerge.test.js +1 -0
  64. package/src/utils/layout/flexLayout.js +35 -3
  65. package/src/utils/layout/flexLayout.test.js +15 -0
  66. package/src/utils/layout/grid.js +95 -0
  67. package/src/utils/layout/grid.test.js +71 -0
  68. package/src/utils/layout/padding.js +13 -0
  69. package/src/utils/layout/rectangle.js +6 -0
  70. package/src/utils/layout/rectangle.test.js +1 -0
  71. package/src/utils/mergeObjects.test.js +1 -0
  72. package/src/utils/numberExtractor.test.js +1 -0
  73. package/src/utils/propertyCacher.test.js +1 -0
  74. package/src/utils/propertyCoalescer.test.js +1 -0
  75. package/src/utils/reservationMap.test.js +1 -0
  76. package/src/utils/topK.test.js +1 -0
  77. package/src/utils/variableTools.test.js +1 -0
  78. package/src/view/axisResolution.test.js +1 -0
  79. package/src/view/axisView.js +3 -5
  80. package/src/view/concatView.js +24 -275
  81. package/src/view/flowBuilder.test.js +1 -0
  82. package/src/view/gridView.js +774 -0
  83. package/src/view/implicitRootView.js +14 -0
  84. package/src/view/layerView.js +15 -1
  85. package/src/view/renderingContext/deferredViewRenderingContext.js +3 -1
  86. package/src/view/renderingContext/simpleViewRenderingContext.js +3 -1
  87. package/src/view/scaleResolution.js +5 -11
  88. package/src/view/scaleResolution.test.js +1 -0
  89. package/src/view/title.js +165 -0
  90. package/src/view/unitView.js +9 -5
  91. package/src/view/view.js +35 -14
  92. package/src/view/view.test.js +1 -0
  93. package/src/view/viewContext.d.ts +6 -1
  94. package/src/view/viewFactory.test.js +1 -0
  95. package/src/view/viewUtils.js +1 -93
  96. package/src/view/zoom.js +89 -0
  97. package/src/gl/includes/fp64-arithmetic.glsl +0 -187
  98. package/src/gl/includes/fp64-utils.js +0 -142
  99. package/src/gl/includes/scales_fp64.glsl +0 -30
  100. package/src/utils/binnedRangeIndex.js +0 -83
  101. package/src/view/decoratorView.js +0 -513
@@ -0,0 +1,14 @@
1
+ import GridView from "./gridView";
2
+
3
+ export default class ImplicitRootView extends GridView {
4
+ /**
5
+ * @param {import("./viewUtils").ViewContext} context
6
+ * @param {import("./view").default} view
7
+ */
8
+ constructor(context, view) {
9
+ super({ vconcat: [] }, context, undefined, "implicitRoot", 1);
10
+
11
+ view.parent = this;
12
+ this.appendChild(view);
13
+ }
14
+ }
@@ -48,7 +48,6 @@ export default class LayerView extends ContainerView {
48
48
  return;
49
49
  }
50
50
 
51
- coords = coords.shrink(this.getPadding());
52
51
  context.pushView(this, coords);
53
52
 
54
53
  for (const child of this.children) {
@@ -57,4 +56,19 @@ export default class LayerView extends ContainerView {
57
56
 
58
57
  context.popView(this);
59
58
  }
59
+
60
+ /**
61
+ * @param {import("../utils/interactionEvent").default} event
62
+ */
63
+ propagateInteractionEvent(event) {
64
+ this.handleInteractionEvent(undefined, event, true);
65
+ if (this.children.length) {
66
+ // Propagate to the top layer
67
+ this.children.at(-1).propagateInteractionEvent(event);
68
+ }
69
+ if (event.stopped) {
70
+ return;
71
+ }
72
+ this.handleInteractionEvent(undefined, event, false);
73
+ }
60
74
  }
@@ -146,7 +146,9 @@ export default class DeferredViewRenderingContext extends ViewRenderingContext {
146
146
  });
147
147
  // Change program, set common uniforms (mark properties, shared domains)
148
148
  this.batch.push(
149
- ifEnabled(() => mark.prepareRender(this.globalOptions))
149
+ ...mark
150
+ .prepareRender(this.globalOptions)
151
+ .map((op) => ifEnabled(op))
150
152
  );
151
153
 
152
154
  /** @type {import("../../utils/layout/rectangle").default} */
@@ -55,7 +55,9 @@ export default class SimpleViewRenderingContext extends ViewRenderingContext {
55
55
  return;
56
56
  }
57
57
 
58
- mark.prepareRender(this.globalOptions);
58
+ for (const op of mark.prepareRender(this.globalOptions)) {
59
+ op();
60
+ }
59
61
  mark.setViewport(this.coords, options.clipRect);
60
62
  mark.render(options)();
61
63
  }
@@ -228,11 +228,6 @@ export default class ScaleResolution {
228
228
  props.domain = [props.domainMin ?? 0, props.domainMax ?? 1];
229
229
  }
230
230
 
231
- // Genomic coordinates need higher precision
232
- if (props.type == LOCUS && !("fp64" in props)) {
233
- props.fp64 = true;
234
- }
235
-
236
231
  // Reverse discrete y axis
237
232
  if (
238
233
  this.channel == "y" &&
@@ -331,11 +326,6 @@ export default class ScaleResolution {
331
326
  scale.genome(this.getGenome());
332
327
  }
333
328
 
334
- // Tag the scale and inform encoders and shaders that emulated
335
- // 64bit floats should be used.
336
- // N.B. the tag is lost upon scale.clone().
337
- scale.fp64 = !!props.fp64;
338
-
339
329
  if (isContinuous(scale.type)) {
340
330
  this._zoomExtent = this._getZoomExtent();
341
331
  }
@@ -405,12 +395,16 @@ export default class ScaleResolution {
405
395
  let newDomain = [...oldDomain];
406
396
 
407
397
  // @ts-expect-error
408
- const anchor = scale.invert(scaleAnchor);
398
+ let anchor = scale.invert(scaleAnchor);
409
399
 
410
400
  if (this.getScaleProps().reverse) {
411
401
  pan = -pan;
412
402
  }
413
403
 
404
+ if ("align" in scale) {
405
+ anchor += scale.align();
406
+ }
407
+
414
408
  // TODO: symlog
415
409
  switch (scale.type) {
416
410
  case "linear":
@@ -1,3 +1,4 @@
1
+ import { describe, expect, test } from "vitest";
1
2
  import { createAndInitialize } from "./testUtils";
2
3
  import createDomain, { toRegularArray as r } from "../utils/domainArray";
3
4
  import LayerView from "./layerView";
@@ -0,0 +1,165 @@
1
+ import { isString } from "vega-util";
2
+
3
+ /** @type {Omit<Required<import("../spec/title").Title>, "text" | "style">} */
4
+ const BASE_TITLE_STYLE = {
5
+ anchor: "middle",
6
+ frame: "group",
7
+ offset: 10,
8
+ orient: "top",
9
+ align: undefined,
10
+ angle: 0,
11
+ baseline: "alphabetic",
12
+ dx: 0,
13
+ dy: 0,
14
+ color: undefined,
15
+ font: undefined,
16
+ fontSize: 12,
17
+ fontStyle: "normal",
18
+ fontWeight: "normal",
19
+ };
20
+
21
+ /** @type {Partial<import("../spec/title").Title>} */
22
+ const TRACK_TITLE_STYLE = {
23
+ orient: "left",
24
+ anchor: "middle",
25
+ align: "right",
26
+ baseline: "middle",
27
+ angle: 0,
28
+ fontSize: 12,
29
+ };
30
+
31
+ /** @type {Partial<import("../spec/title").Title>} */
32
+ const OVERLAY_TITLE_STYLE = {
33
+ orient: "top",
34
+ anchor: "start",
35
+ align: "left",
36
+ baseline: "top",
37
+ offset: -10,
38
+ dx: 10,
39
+ fontSize: 12,
40
+ };
41
+
42
+ /** @type {Record<import("../spec/title").TitleAnchor, number>} */
43
+ const ANCHORS = {
44
+ start: 0,
45
+ middle: 0.5,
46
+ end: 1,
47
+ };
48
+
49
+ /** @type {Record<import("../spec/title").TitleAnchor, import("../spec/font").Align>} */
50
+ const ANCHOR_TO_ALIGN = {
51
+ start: "left",
52
+ middle: "center",
53
+ end: "right",
54
+ };
55
+
56
+ /**
57
+ * @param {string | import("../spec/title").Title} title
58
+ * @returns {import("../spec/view").UnitSpec}
59
+ */
60
+ export default function createTitle(title) {
61
+ if (!title) {
62
+ return;
63
+ }
64
+
65
+ /** @type {import("../spec/title").Title} */
66
+ const titleSpec = isString(title) ? { text: title } : title;
67
+
68
+ if (!titleSpec.text || titleSpec.orient == "none") {
69
+ return;
70
+ }
71
+
72
+ // TODO: Make these configurable
73
+ /** @type {Partial<import("../spec/title").Title>} */
74
+ const config =
75
+ {
76
+ "track-title": TRACK_TITLE_STYLE,
77
+ overlay: OVERLAY_TITLE_STYLE,
78
+ }[titleSpec.style] ?? {};
79
+
80
+ // TODO: frame prop
81
+
82
+ /** @type {import("../spec/title").Title} */
83
+ const preliminarySpec = {
84
+ ...BASE_TITLE_STYLE,
85
+ ...config,
86
+ ...titleSpec,
87
+ };
88
+
89
+ /** @type {Partial<import("../spec/title").Title>} */
90
+ let orientConfig = {};
91
+ let xy = { x: 0, y: 0 };
92
+
93
+ const anchorPos = ANCHORS[preliminarySpec.anchor ?? "middle"];
94
+
95
+ switch (preliminarySpec.orient) {
96
+ case "top":
97
+ xy = { x: anchorPos, y: 1 };
98
+ orientConfig = { baseline: "alphabetic", angle: 0 };
99
+ break;
100
+ case "right":
101
+ xy = { x: 1, y: 1 - anchorPos };
102
+ orientConfig = { baseline: "alphabetic", angle: 90 };
103
+ break;
104
+ case "bottom":
105
+ xy = { x: anchorPos, y: 0 };
106
+ orientConfig = { baseline: "top", angle: 0 };
107
+ break;
108
+ case "left":
109
+ xy = { x: 0, y: anchorPos };
110
+ orientConfig = { baseline: "alphabetic", angle: -90 };
111
+ break;
112
+ default:
113
+ }
114
+
115
+ /** @type {import("../spec/title").Title} */
116
+ const spec = {
117
+ ...BASE_TITLE_STYLE,
118
+ ...orientConfig,
119
+ ...config,
120
+ ...titleSpec,
121
+ };
122
+
123
+ const offsets = { xOffset: 0, yOffset: 0 };
124
+ switch (preliminarySpec.orient) {
125
+ case "top":
126
+ offsets.yOffset = -spec.offset;
127
+ break;
128
+ case "right":
129
+ offsets.xOffset = spec.offset;
130
+ break;
131
+ case "bottom":
132
+ offsets.yOffset = spec.offset;
133
+ break;
134
+ case "left":
135
+ offsets.xOffset = -spec.offset;
136
+ break;
137
+ default:
138
+ }
139
+
140
+ return {
141
+ configurableVisibility: false,
142
+ data: { values: [{}] },
143
+ mark: {
144
+ type: "text",
145
+ tooltip: null,
146
+ clip: false,
147
+
148
+ ...xy,
149
+ ...offsets,
150
+
151
+ text: spec.text,
152
+
153
+ align: spec.align ?? ANCHOR_TO_ALIGN[spec.anchor],
154
+ angle: spec.angle,
155
+ baseline: spec.baseline,
156
+ dx: spec.dx,
157
+ dy: spec.dy,
158
+ color: spec.color,
159
+ font: spec.font,
160
+ size: spec.fontSize,
161
+ fontStyle: spec.fontStyle,
162
+ fontWeight: spec.fontWeight,
163
+ },
164
+ };
165
+ }
@@ -40,7 +40,6 @@ export const markTypes = {
40
40
  * @typedef {import("../encoder/accessor").Accessor} Accessor
41
41
  * @typedef {import("../utils/layout/flexLayout").SizeDef} SizeDef
42
42
  * @typedef {import("../spec/view").ResolutionTarget} ResolutionTarget
43
- * @typedef {import("./decoratorView").default} DecoratorView
44
43
  *
45
44
  */
46
45
  export default class UnitView extends ContainerView {
@@ -64,13 +63,13 @@ export default class UnitView extends ContainerView {
64
63
  throw new Error(`No such mark: ${this.getMarkType()}`);
65
64
  }
66
65
 
67
- /** @type {(UnitView | LayerView | DecoratorView)[]} */
66
+ /** @type {(UnitView | LayerView)[]} */
68
67
  this.sampleAggregateViews = [];
69
68
  this._initializeAggregateViews();
70
69
 
71
70
  /**
72
71
  * Not nice! Inconsistent when faceting!
73
- * TODO: Something. Perhaps a Map that has coords for each facet or something...
72
+ * TODO: Something. Maybe store only width/height
74
73
  * @type {import("../utils/layout/rectangle").default}
75
74
  */
76
75
  this.coords = undefined;
@@ -108,8 +107,6 @@ export default class UnitView extends ContainerView {
108
107
  return;
109
108
  }
110
109
 
111
- coords = coords.shrink(this.getPadding());
112
-
113
110
  this.coords = coords;
114
111
 
115
112
  context.pushView(this, coords);
@@ -366,6 +363,13 @@ export default class UnitView extends ContainerView {
366
363
  }
367
364
  }
368
365
 
366
+ /**
367
+ * @param {import("../utils/interactionEvent").default} event
368
+ */
369
+ propagateInteractionEvent(event) {
370
+ event.target = this;
371
+ }
372
+
369
373
  /**
370
374
  * @param {string} channel
371
375
  * @param {import("./containerView").ResolutionTarget} resolutionType
package/src/view/view.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  initPropertyCache,
10
10
  invalidatePrefix,
11
11
  } from "../utils/propertyCacher";
12
- import { isNumber, span } from "vega-util";
12
+ import { isNumber, isString, span } from "vega-util";
13
13
  import { scaleLog } from "d3-scale";
14
14
  import { isFieldDef, getPrimaryChannel } from "../encoder/encoder";
15
15
  import { appendToBaseUrl } from "../utils/url";
@@ -81,12 +81,12 @@ export default class View {
81
81
  this.resolutions = {
82
82
  /**
83
83
  * Channel-specific scale resolutions
84
- * @type {Record<string, import("./scaleResolution").default>}
84
+ * @type {Partial<Record<Channel, import("./scaleResolution").default>>}
85
85
  */
86
86
  scale: {},
87
87
  /**
88
88
  * Channel-specific axis resolutions
89
- * @type {Record<string, import("./axisResolution").default>}
89
+ * @type {Partial<Record<import("../spec/channel").PrimaryPositionalChannel, import("./axisResolution").default>>}
90
90
  */
91
91
  axis: {},
92
92
  };
@@ -103,6 +103,12 @@ export default class View {
103
103
 
104
104
  /** @type {function(number):number} */
105
105
  this.opacityFunction = defaultOpacityFunction;
106
+
107
+ /**
108
+ * Don't inherit encodings from parent.
109
+ * TODO: Make configurable through spec. Allow more fine-grained control.
110
+ */
111
+ this.blockEncodingInheritance = false;
106
112
  }
107
113
 
108
114
  getPadding() {
@@ -112,15 +118,12 @@ export default class View {
112
118
  }
113
119
 
114
120
  /**
115
- * Returns a computed, "effective" padding between the plot area and view's
116
- * bounding box. The padding may include the configured padding, axes,
117
- * peripheral views, etc.
121
+ * Returns a padding that indicates how much axes and titles extend over the plot area.
118
122
  *
119
- * Effective padding allows for aligning views so that their content and
120
- * axes line up properly.
123
+ * @returns {Padding}
121
124
  */
122
- getEffectivePadding() {
123
- return this.getPadding();
125
+ getOverhang() {
126
+ return Padding.zero();
124
127
  }
125
128
 
126
129
  /**
@@ -367,12 +370,13 @@ export default class View {
367
370
  * encodings. However, this does not contain any defaults or inferred/adjusted/fixed
368
371
  * encodings. Those are available in Mark's encoding property.
369
372
  *
370
- * @param {View} [whoIsAsking] Passed to the immediate parent. Allows for
371
- * selectively breaking the inheritance.
372
373
  * @return {import("../spec/channel").Encoding}
373
374
  */
374
- getEncoding(whoIsAsking) {
375
- const pe = this.parent ? this.parent.getEncoding(this) : {};
375
+ getEncoding() {
376
+ const pe =
377
+ this.parent && !this.blockEncodingInheritance
378
+ ? this.parent.getEncoding()
379
+ : {};
376
380
  const te = this.spec.encoding || {};
377
381
 
378
382
  /** @type {import("../spec/channel").Encoding} */
@@ -490,6 +494,13 @@ export default class View {
490
494
  return true;
491
495
  }
492
496
 
497
+ getTitleText() {
498
+ const title = this.spec.title;
499
+ if (title) {
500
+ return isString(title) ? title : title.text;
501
+ }
502
+ }
503
+
493
504
  /**
494
505
  * @param {any} key string
495
506
  * @param {function(key?):T} callable A function that produces a value to be cached
@@ -525,6 +536,16 @@ export default class View {
525
536
  invalidateSizeCache() {
526
537
  this._invalidateCacheByPrefix("size/", "ancestors");
527
538
  }
539
+
540
+ /**
541
+ * Broadcasts a message to views that include the given (x, y) point.
542
+ * This is mainly intended for mouse events.
543
+ *
544
+ * @param {import("../utils/interactionEvent").default} event
545
+ */
546
+ propagateInteractionEvent(event) {
547
+ // Subclasses must implement proper handling
548
+ }
528
549
  }
529
550
 
530
551
  /**
@@ -1,3 +1,4 @@
1
+ import { describe, expect, test } from "vitest";
1
2
  import UnitView from "./unitView";
2
3
  import LayerView from "./layerView";
3
4
 
@@ -11,6 +11,11 @@ import { Datum } from "../data/flowNode";
11
11
  import { ViewSpec } from "../spec/view";
12
12
  import ContainerView from "./containerView";
13
13
 
14
+ export interface Hover {
15
+ mark: Mark;
16
+ datum: Datum;
17
+ }
18
+
14
19
  /**
15
20
  * ViewContext provides essential data and interfaces to View classes.
16
21
  */
@@ -29,7 +34,7 @@ export default interface ViewContext {
29
34
  converter?: (datum: T) => Promise<TemplateResult>
30
35
  ) => void;
31
36
 
32
- getCurrentHover: () => { mark: Mark; datum: Datum };
37
+ getCurrentHover: () => Hover;
33
38
 
34
39
  /**
35
40
  * Adds a keyboard event listener to the document. Cleanup is performed automatically
@@ -1,3 +1,4 @@
1
+ import { expect, test } from "vitest";
1
2
  import { ViewFactory } from "./viewFactory";
2
3
 
3
4
  test("isViewSpec", () => {
@@ -3,19 +3,11 @@ import { loader as vegaLoader } from "vega-loader";
3
3
 
4
4
  import UnitView from "./unitView";
5
5
  import ImportView from "./importView";
6
- import LayerView from "./layerView";
7
- import DecoratorView from "./decoratorView";
8
6
  // eslint-disable-next-line no-unused-vars
9
7
  import View, { VISIT_SKIP, VISIT_STOP } from "./view";
10
8
  import { buildDataFlow } from "./flowBuilder";
11
9
  import { optimizeDataFlow } from "../data/flowOptimizer";
12
- import {
13
- isFieldDef,
14
- isValueDef,
15
- primaryPositionalChannels,
16
- } from "../encoder/encoder";
17
- import ContainerView from "./containerView";
18
- import { peek } from "../utils/arrayUtils";
10
+ import { isFieldDef, primaryPositionalChannels } from "../encoder/encoder";
19
11
  import { rollup } from "d3-array";
20
12
 
21
13
  /**
@@ -145,90 +137,6 @@ export function setImplicitScaleNames(root) {
145
137
  }
146
138
  }
147
139
 
148
- /**
149
- * @param {View} root
150
- */
151
- export function addDecorators(root) {
152
- let newRoot = root; // If the root is wrapped...
153
-
154
- /** @param {ChannelDef} channelDef */
155
- const hasDomain = (channelDef) => channelDef && !isValueDef(channelDef);
156
-
157
- root.visit((view) => {
158
- if (view instanceof LayerView || view instanceof UnitView) {
159
- const encoding = view.getEncoding();
160
- if (
161
- view instanceof UnitView &&
162
- !hasDomain(encoding.x) &&
163
- !hasDomain(encoding.y)
164
- ) {
165
- // Don't wrap views that have no positional channels
166
- // TODO: However, in future, views with borders or backgrounds should be wrapped always
167
- // TODO: Also, views with "axis: null" need no wrapping.
168
- // TODO: Handle LayerViews, they may have children with positional domains
169
- return VISIT_SKIP;
170
- }
171
-
172
- const originalParent = view.parent;
173
- const decorator = new DecoratorView(view.context, originalParent);
174
- view.parent = decorator;
175
- decorator.child = view;
176
- decorator.name = view.name + "_decorator";
177
-
178
- if (originalParent) {
179
- if (originalParent instanceof ContainerView) {
180
- originalParent.replaceChild(view, decorator);
181
- } else {
182
- // The situation is likely related to summaries of SampleView and the
183
- // hierarchy is inconsistent. Let's try to find the SampleView.
184
-
185
- /** @type {view} */
186
- let parent;
187
- root.visit(
188
- stackifyVisitor((needle, stack) => {
189
- if (needle === view) {
190
- parent = peek(stack);
191
- return VISIT_STOP;
192
- }
193
- })
194
- );
195
-
196
- if (parent instanceof ContainerView) {
197
- parent.replaceChild(view, decorator);
198
- } else {
199
- throw new Error(
200
- "Cannot find parent while decorating: " +
201
- view.getPathString()
202
- );
203
- }
204
- }
205
- }
206
-
207
- decorator.resolutions = view.resolutions;
208
- view.resolutions = { scale: {}, axis: {} };
209
-
210
- decorator.spec.height = view.spec.height;
211
- view.spec.height = "container";
212
-
213
- decorator.spec.width = view.spec.width;
214
- view.spec.width = "container";
215
-
216
- decorator.spec.padding = view.spec.padding;
217
- view.spec.padding = undefined;
218
-
219
- if (view === root) {
220
- newRoot = decorator;
221
- }
222
-
223
- decorator.initialize();
224
-
225
- return VISIT_SKIP;
226
- }
227
- });
228
-
229
- return newRoot;
230
- }
231
-
232
140
  /**
233
141
  * @param {View} root
234
142
  * @param {import("../data/dataFlow").default<View>} [existingFlow] Add data flow
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @typedef {object} ZoomEvent
3
+ * @prop {number} x
4
+ * @prop {number} y
5
+ * @prop {number} xDelta
6
+ * @prop {number} yDelta
7
+ * @prop {number} zDelta
8
+ */
9
+
10
+ /**
11
+ * @param {import("../utils/interactionEvent").default} event
12
+ * @param {import("./renderingContext/layoutRecorderViewRenderingContext").Rectangle} coords The plot area
13
+ * @param {(zoomEvent: ZoomEvent) => void} handleZoom
14
+ * @param {import("./viewContext").Hover} [hover]
15
+ */
16
+ export default function interactionToZoom(event, coords, handleZoom, hover) {
17
+ if (event.type == "wheel") {
18
+ event.uiEvent.preventDefault(); // TODO: Only if there was something zoomable
19
+
20
+ const wheelEvent = /** @type {WheelEvent} */ (event.uiEvent);
21
+ const wheelMultiplier = wheelEvent.deltaMode ? 120 : 1;
22
+
23
+ let { x, y } = event.point;
24
+
25
+ // Snapping to the hovered item:
26
+ // We find the currently hovered object and move the pointed coordinates
27
+ // to its center if the mark has only primary positional channels.
28
+ // This allows the user to rapidly zoom closer without having to
29
+ // continuously adjust the cursor position.
30
+
31
+ if (hover) {
32
+ const e = hover.mark.encoders;
33
+ if (e.x && !e.x2) {
34
+ x = +e.x(hover.datum) * coords.width + coords.x;
35
+ }
36
+ if (e.y && !e.y2) {
37
+ y = (1 - +e.y(hover.datum)) * coords.height + coords.y;
38
+ }
39
+ }
40
+
41
+ if (Math.abs(wheelEvent.deltaX) < Math.abs(wheelEvent.deltaY)) {
42
+ handleZoom({
43
+ x,
44
+ y,
45
+ xDelta: 0,
46
+ yDelta: 0,
47
+ zDelta: (wheelEvent.deltaY * wheelMultiplier) / 300,
48
+ });
49
+ } else {
50
+ handleZoom({
51
+ x,
52
+ y,
53
+ xDelta: -wheelEvent.deltaX * wheelMultiplier,
54
+ yDelta: 0,
55
+ zDelta: 0,
56
+ });
57
+ }
58
+ } else if (
59
+ event.type == "mousedown" &&
60
+ /** @type {MouseEvent} */ (event.uiEvent).button === 0
61
+ ) {
62
+ const mouseEvent = /** @type {MouseEvent} */ (event.uiEvent);
63
+ mouseEvent.preventDefault();
64
+
65
+ let prevMouseEvent = mouseEvent;
66
+
67
+ const onMousemove = /** @param {MouseEvent} moveEvent */ (
68
+ moveEvent
69
+ ) => {
70
+ handleZoom({
71
+ x: prevMouseEvent.clientX,
72
+ y: prevMouseEvent.clientY,
73
+ xDelta: moveEvent.clientX - prevMouseEvent.clientX,
74
+ yDelta: moveEvent.clientY - prevMouseEvent.clientY,
75
+ zDelta: 0,
76
+ });
77
+
78
+ prevMouseEvent = moveEvent;
79
+ };
80
+
81
+ const onMouseup = /** @param {MouseEvent} upEvent */ (upEvent) => {
82
+ document.removeEventListener("mousemove", onMousemove);
83
+ document.removeEventListener("mouseup", onMouseup);
84
+ };
85
+
86
+ document.addEventListener("mouseup", onMouseup, false);
87
+ document.addEventListener("mousemove", onMousemove, false);
88
+ }
89
+ }