@genome-spy/core 0.46.1 → 0.48.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 (51) hide show
  1. package/dist/bundle/index.es.js +5310 -5113
  2. package/dist/bundle/index.js +105 -94
  3. package/dist/schema.json +38 -0
  4. package/dist/src/genomeSpy.d.ts.map +1 -1
  5. package/dist/src/genomeSpy.js +31 -16
  6. package/dist/src/gl/webGLHelper.d.ts +2 -1
  7. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  8. package/dist/src/gl/webGLHelper.js +8 -1
  9. package/dist/src/marks/mark.d.ts +24 -12
  10. package/dist/src/marks/mark.d.ts.map +1 -1
  11. package/dist/src/marks/mark.js +27 -13
  12. package/dist/src/marks/point.d.ts +0 -1
  13. package/dist/src/marks/point.d.ts.map +1 -1
  14. package/dist/src/marks/point.js +6 -2
  15. package/dist/src/marks/text.d.ts.map +1 -1
  16. package/dist/src/marks/text.js +4 -1
  17. package/dist/src/scale/scale.js +2 -0
  18. package/dist/src/spec/parameter.d.ts +20 -1
  19. package/dist/src/types/embedApi.d.ts +7 -0
  20. package/dist/src/types/rendering.d.ts +6 -0
  21. package/dist/src/utils/animator.d.ts +17 -0
  22. package/dist/src/utils/animator.d.ts.map +1 -1
  23. package/dist/src/utils/animator.js +72 -0
  24. package/dist/src/utils/inertia.d.ts +6 -15
  25. package/dist/src/utils/inertia.d.ts.map +1 -1
  26. package/dist/src/utils/inertia.js +28 -63
  27. package/dist/src/utils/inputBinding.d.ts.map +1 -1
  28. package/dist/src/utils/inputBinding.js +26 -2
  29. package/dist/src/utils/ringBuffer.d.ts +19 -0
  30. package/dist/src/utils/ringBuffer.d.ts.map +1 -0
  31. package/dist/src/utils/ringBuffer.js +43 -0
  32. package/dist/src/utils/ringBuffer.test.js +39 -0
  33. package/dist/src/view/gridView.d.ts +6 -6
  34. package/dist/src/view/gridView.d.ts.map +1 -1
  35. package/dist/src/view/gridView.js +48 -29
  36. package/dist/src/view/layout/point.d.ts +17 -1
  37. package/dist/src/view/layout/point.d.ts.map +1 -1
  38. package/dist/src/view/layout/point.js +36 -1
  39. package/dist/src/view/unitView.d.ts +3 -14
  40. package/dist/src/view/unitView.d.ts.map +1 -1
  41. package/dist/src/view/unitView.js +26 -8
  42. package/dist/src/view/view.d.ts +14 -5
  43. package/dist/src/view/view.d.ts.map +1 -1
  44. package/dist/src/view/view.js +26 -7
  45. package/dist/src/view/zoom.d.ts +4 -10
  46. package/dist/src/view/zoom.d.ts.map +1 -1
  47. package/dist/src/view/zoom.js +126 -9
  48. package/package.json +2 -2
  49. package/dist/src/view/renderingContext/layoutRecorderViewRenderingContext.d.ts +0 -60
  50. package/dist/src/view/renderingContext/layoutRecorderViewRenderingContext.d.ts.map +0 -1
  51. package/dist/src/view/renderingContext/layoutRecorderViewRenderingContext.js +0 -128
@@ -4,7 +4,6 @@ import RuleMark from "../marks/rule.js";
4
4
  import LinkMark from "../marks/link.js";
5
5
  import TextMark from "../marks/text.js";
6
6
 
7
- import ContainerView from "./containerView.js";
8
7
  import ScaleResolution from "./scaleResolution.js";
9
8
  import {
10
9
  isSecondaryChannel,
@@ -18,6 +17,7 @@ import {
18
17
  } from "../encoder/encoder.js";
19
18
  import createDomain from "../utils/domainArray.js";
20
19
  import AxisResolution from "./axisResolution.js";
20
+ import View from "./view.js";
21
21
 
22
22
  /**
23
23
  *
@@ -32,15 +32,20 @@ export const markTypes = {
32
32
  text: TextMark,
33
33
  };
34
34
 
35
- export default class UnitView extends ContainerView {
35
+ export default class UnitView extends View {
36
36
  /**
37
37
  * @typedef {import("../spec/channel.js").Channel} Channel
38
- * @typedef {import("./view.js").default} View
39
- * @typedef {import("./layerView.js").default} LayerView
40
38
  * @typedef {import("../utils/domainArray.js").DomainArray} DomainArray
41
39
  * @typedef {import("../spec/view.js").ResolutionTarget} ResolutionTarget
42
40
  *
43
41
  */
42
+
43
+ /**
44
+ * Sets the zoom level parameter.
45
+ * @type {(zoomLevel: number) => void}
46
+ */
47
+ #zoomLevelSetter;
48
+
44
49
  /**
45
50
  *
46
51
  * @param {import("../spec/view.js").UnitSpec} spec
@@ -65,6 +70,19 @@ export default class UnitView extends ContainerView {
65
70
 
66
71
  this.resolve();
67
72
 
73
+ this.#zoomLevelSetter = this.paramMediator.allocateSetter(
74
+ "zoomLevel",
75
+ 1.0
76
+ );
77
+ /** @type {import("../spec/channel.js").ChannelWithScale[]} */ ([
78
+ "x",
79
+ "y",
80
+ ]).forEach((channel) =>
81
+ this.getScaleResolution(channel)?.addEventListener("domain", () =>
82
+ this.#zoomLevelSetter(Math.sqrt(this.getZoomLevel()))
83
+ )
84
+ );
85
+
68
86
  this.needsAxes = { x: true, y: true };
69
87
  }
70
88
 
@@ -129,7 +147,7 @@ export default class UnitView extends ContainerView {
129
147
  while (
130
148
  (view.getConfiguredOrDefaultResolution(targetChannel, type) ==
131
149
  "forced" ||
132
- (view.dataParent instanceof ContainerView &&
150
+ (view.dataParent &&
133
151
  ["shared", "excluded", "forced"].includes(
134
152
  view.dataParent.getConfiguredOrDefaultResolution(
135
153
  targetChannel,
@@ -220,7 +238,7 @@ export default class UnitView extends ContainerView {
220
238
  /**
221
239
  * @param {Channel} channel A primary channel
222
240
  */
223
- _validateDomainQuery(channel) {
241
+ #validateDomainQuery(channel) {
224
242
  if (isSecondaryChannel(channel)) {
225
243
  throw new Error(
226
244
  `getDomain(${channel}), must only be called for primary channels!`
@@ -243,7 +261,7 @@ export default class UnitView extends ContainerView {
243
261
  * @returns {DomainArray}
244
262
  */
245
263
  getConfiguredDomain(channel) {
246
- const channelDef = this._validateDomainQuery(channel);
264
+ const channelDef = this.#validateDomainQuery(channel);
247
265
 
248
266
  const specDomain =
249
267
  channelDef && channelDef.scale && channelDef.scale.domain;
@@ -273,7 +291,7 @@ export default class UnitView extends ContainerView {
273
291
  * @returns {DomainArray}
274
292
  */
275
293
  extractDataDomain(channel) {
276
- const channelDef = this._validateDomainQuery(channel);
294
+ const channelDef = this.#validateDomainQuery(channel);
277
295
  const type = channelDef.type ?? "nominal"; // TODO: Should check that this is a channel without scale
278
296
 
279
297
  /** @param {Channel} channel */
@@ -47,11 +47,12 @@ export default class View {
47
47
  */
48
48
  opacityFunction: (arg0: number) => number;
49
49
  /**
50
- * Not nice! Inconsistent when faceting!
51
- * TODO: Something. Maybe store only width/height
52
- * @type {import("./layout/rectangle.js").default}
50
+ * Coords of the view for each facet, recorded during the last layout rendering pass.
51
+ * Most views have only one facet, so the map is usually of size 1.
52
+ *
53
+ * @type {Map<any, import("./layout/rectangle.js").default>}
53
54
  */
54
- coords: import("./layout/rectangle.js").default;
55
+ facetCoords: Map<any, import("./layout/rectangle.js").default>;
55
56
  context: import("../types/viewContext.js").default;
56
57
  layoutParent: import("./containerView.js").default;
57
58
  dataParent: View;
@@ -86,6 +87,13 @@ export default class View {
86
87
  needsAxes: Record<import("../spec/channel.js").PrimaryPositionalChannel, boolean>;
87
88
  /** @type {ParamMediator} */
88
89
  paramMediator: ParamMediator;
90
+ /**
91
+ * Returns the coords of the view. If view has been faceted, returns the coords
92
+ * of an arbitrary facet. If all or specific facet coords are needed, use `facetCoords`.
93
+ *
94
+ * @returns {import("./layout/rectangle.js").default}
95
+ */
96
+ get coords(): import("./layout/rectangle.js").default;
89
97
  getPadding(): Padding;
90
98
  /**
91
99
  * Returns a padding that indicates how much axes and titles extend over the plot area.
@@ -160,8 +168,9 @@ export default class View {
160
168
  * Coordinates of the view
161
169
  * @param {import("../utils/interactionEvent.js").default} event
162
170
  * @param {boolean} capturing
171
+ * @protected
163
172
  */
164
- handleInteractionEvent(coords: import("./layout/rectangle.js").default, event: import("../utils/interactionEvent.js").default, capturing: boolean): void;
173
+ protected handleInteractionEvent(coords: import("./layout/rectangle.js").default, event: import("../utils/interactionEvent.js").default, capturing: boolean): void;
165
174
  /**
166
175
  * Add an "interaction" event listener that mimics DOM's event model inside
167
176
  * the view hierarchy.
@@ -1 +1 @@
1
- {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../../src/view/view.js"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../../src/view/view.js"],"names":[],"mappings":"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"}
@@ -17,6 +17,7 @@ import { isDiscrete, bandSpace } from "vega-scale";
17
17
  import { peek } from "../utils/arrayUtils.js";
18
18
  import ViewError from "./viewError.js";
19
19
  import ParamMediator, { isExprRef } from "./paramMediator.js";
20
+ import { InternMap } from "internmap";
20
21
 
21
22
  // TODO: View classes have too many responsibilities. Come up with a way
22
23
  // to separate the concerns. However, most concerns are tightly tied to
@@ -75,11 +76,12 @@ export default class View {
75
76
  opacityFunction = defaultOpacityFunction;
76
77
 
77
78
  /**
78
- * Not nice! Inconsistent when faceting!
79
- * TODO: Something. Maybe store only width/height
80
- * @type {import("./layout/rectangle.js").default}
79
+ * Coords of the view for each facet, recorded during the last layout rendering pass.
80
+ * Most views have only one facet, so the map is usually of size 1.
81
+ *
82
+ * @type {Map<any, import("./layout/rectangle.js").default>}
81
83
  */
82
- coords;
84
+ facetCoords = new InternMap([], JSON.stringify);
83
85
 
84
86
  /**
85
87
  *
@@ -141,6 +143,16 @@ export default class View {
141
143
  }
142
144
  }
143
145
 
146
+ /**
147
+ * Returns the coords of the view. If view has been faceted, returns the coords
148
+ * of an arbitrary facet. If all or specific facet coords are needed, use `facetCoords`.
149
+ *
150
+ * @returns {import("./layout/rectangle.js").default}
151
+ */
152
+ get coords() {
153
+ return this.facetCoords.values().next().value;
154
+ }
155
+
144
156
  getPadding() {
145
157
  return this._cache("size/padding", () =>
146
158
  Padding.createFromConfig(this.spec.padding)
@@ -379,6 +391,7 @@ export default class View {
379
391
  * Coordinates of the view
380
392
  * @param {import("../utils/interactionEvent.js").default} event
381
393
  * @param {boolean} capturing
394
+ * @protected
382
395
  */
383
396
  handleInteractionEvent(coords, event, capturing) {
384
397
  const listenersByType = capturing
@@ -480,9 +493,15 @@ export default class View {
480
493
  * @type {import("../types/rendering.js").RenderMethod}
481
494
  */
482
495
  render(context, coords, options = {}) {
483
- this.coords = options.clipRect
484
- ? coords.intersect(options.clipRect)
485
- : coords;
496
+ // TODO: When using sample faceting, all facets have the same coords.
497
+ // It would be better to save only single coords with an `undefined` facetId.
498
+ if (options.firstFacet) {
499
+ this.facetCoords.clear();
500
+ }
501
+ this.facetCoords.set(
502
+ options.facetId,
503
+ options.clipRect ? coords.intersect(options.clipRect) : coords
504
+ );
486
505
 
487
506
  // override
488
507
  }
@@ -1,18 +1,12 @@
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
- */
1
+ export function isStillZooming(): boolean;
9
2
  /**
10
3
  * @param {import("../utils/interactionEvent.js").default} event
11
- * @param {import("./renderingContext/layoutRecorderViewRenderingContext.js").Rectangle} coords The plot area
4
+ * @param {import("./layout/rectangle.js").default} coords
12
5
  * @param {(zoomEvent: ZoomEvent) => void} handleZoom
13
6
  * @param {import("../types/viewContext.js").Hover} [hover]
7
+ * @param {import("../utils/animator.js").default} [animator]
14
8
  */
15
- export default function interactionToZoom(event: import("../utils/interactionEvent.js").default, coords: import("./renderingContext/layoutRecorderViewRenderingContext.js").Rectangle, handleZoom: (zoomEvent: ZoomEvent) => void, hover?: import("../types/viewContext.js").Hover): void;
9
+ 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;
16
10
  export type ZoomEvent = {
17
11
  x: number;
18
12
  y: number;
@@ -1 +1 @@
1
- {"version":3,"file":"zoom.d.ts","sourceRoot":"","sources":["../../../src/view/zoom.js"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;GAKG;AACH,iDALW,OAAO,8BAA8B,EAAE,OAAO,UAC9C,OAAO,0DAA0D,EAAE,SAAS,0BAChE,SAAS,KAAK,IAAI,UAC9B,OAAO,yBAAyB,EAAE,KAAK,QA2EjD;;OAtFS,MAAM;OACN,MAAM;YACN,MAAM;YACN,MAAM;YACN,MAAM"}
1
+ {"version":3,"file":"zoom.d.ts","sourceRoot":"","sources":["../../../src/view/zoom.js"],"names":[],"mappings":"AAkBA,0CAGC;AAgBD;;;;;;GAMG;AACH,yCANW,OAAO,8BAA8B,EAAE,OAAO,UAC9C,OAAO,uBAAuB,EAAE,OAAO,0BAC3B,SAAS,KAAK,IAAI,UAC9B,OAAO,yBAAyB,EAAE,KAAK,aACvC,OAAO,sBAAsB,EAAE,OAAO,QA0IhD;;OAlLS,MAAM;OACN,MAAM;YACN,MAAM;YACN,MAAM;YACN,MAAM"}
@@ -7,19 +7,54 @@
7
7
  * @prop {number} zDelta
8
8
  */
9
9
 
10
+ import { makeLerpSmoother } from "../utils/animator.js";
11
+ import RingBuffer from "../utils/ringBuffer.js";
12
+ import Point from "./layout/point.js";
13
+
14
+ /** @type {ReturnType<typeof makeLerpSmoother>} */
15
+ let smoother;
16
+
17
+ let lastTimestamp = 0;
18
+
19
+ export function isStillZooming() {
20
+ const delta = performance.now() - lastTimestamp;
21
+ return delta < 50;
22
+ }
23
+
24
+ /**
25
+ *
26
+ * @param {T} fn
27
+ * @returns {T}
28
+ * @template {Function} T
29
+ */
30
+ function recordTimeStamp(fn) {
31
+ // @ts-ignore
32
+ return function (...args) {
33
+ lastTimestamp = performance.now();
34
+ fn(...args);
35
+ };
36
+ }
37
+
10
38
  /**
11
39
  * @param {import("../utils/interactionEvent.js").default} event
12
- * @param {import("./renderingContext/layoutRecorderViewRenderingContext.js").Rectangle} coords The plot area
40
+ * @param {import("./layout/rectangle.js").default} coords
13
41
  * @param {(zoomEvent: ZoomEvent) => void} handleZoom
14
42
  * @param {import("../types/viewContext.js").Hover} [hover]
43
+ * @param {import("../utils/animator.js").default} [animator]
15
44
  */
16
- export default function interactionToZoom(event, coords, handleZoom, hover) {
45
+ export function interactionToZoom(event, coords, handleZoom, hover, animator) {
46
+ handleZoom = recordTimeStamp(handleZoom);
47
+
17
48
  if (event.type == "wheel") {
18
49
  event.uiEvent.preventDefault(); // TODO: Only if there was something zoomable
19
50
 
20
51
  const wheelEvent = /** @type {WheelEvent} */ (event.uiEvent);
21
52
  const wheelMultiplier = wheelEvent.deltaMode ? 120 : 1;
22
53
 
54
+ if (wheelEvent.deltaX === 0 && wheelEvent.deltaY === 0) {
55
+ return;
56
+ }
57
+
23
58
  let { x, y } = event.point;
24
59
 
25
60
  // Snapping to the hovered item:
@@ -28,6 +63,9 @@ export default function interactionToZoom(event, coords, handleZoom, hover) {
28
63
  // This allows the user to rapidly zoom closer without having to
29
64
  // continuously adjust the cursor position.
30
65
 
66
+ // Stop drag-to-pan inertia
67
+ smoother?.stop();
68
+
31
69
  if (hover) {
32
70
  const e = hover.mark.encoders;
33
71
  if (e.x && !e.x2 && !e.x.constantValue) {
@@ -59,31 +97,110 @@ export default function interactionToZoom(event, coords, handleZoom, hover) {
59
97
  event.type == "mousedown" &&
60
98
  /** @type {MouseEvent} */ (event.uiEvent).button === 0
61
99
  ) {
100
+ if (smoother) {
101
+ smoother.stop();
102
+ }
103
+
104
+ /** @type {RingBuffer<{point: Point, timestamp: number}>} */
105
+ const buffer = new RingBuffer(30);
106
+
62
107
  const mouseEvent = /** @type {MouseEvent} */ (event.uiEvent);
63
108
  mouseEvent.preventDefault();
64
109
 
65
- let prevMouseEvent = mouseEvent;
110
+ let prevPoint = Point.fromMouseEvent(mouseEvent);
66
111
 
67
112
  const onMousemove = /** @param {MouseEvent} moveEvent */ (
68
113
  moveEvent
69
114
  ) => {
115
+ const point = Point.fromMouseEvent(moveEvent);
116
+ buffer.push({ point, timestamp: performance.now() });
117
+
118
+ const delta = point.subtract(prevPoint);
119
+
70
120
  handleZoom({
71
- x: prevMouseEvent.clientX,
72
- y: prevMouseEvent.clientY,
73
- xDelta: moveEvent.clientX - prevMouseEvent.clientX,
74
- yDelta: moveEvent.clientY - prevMouseEvent.clientY,
121
+ x: prevPoint.x,
122
+ y: prevPoint.y,
123
+ xDelta: delta.x,
124
+ yDelta: delta.y,
75
125
  zDelta: 0,
76
126
  });
77
127
 
78
- prevMouseEvent = moveEvent;
128
+ prevPoint = point;
79
129
  };
80
130
 
81
- const onMouseup = /** @param {MouseEvent} upEvent */ (upEvent) => {
131
+ const animateInertia = () => {
132
+ const lastMillisToInclude = 160;
133
+
134
+ const now = performance.now();
135
+ const arr = buffer
136
+ .get()
137
+ .filter((p) => now - p.timestamp < lastMillisToInclude);
138
+
139
+ if (arr.length < 5 || !animator || isDecelerating(arr)) {
140
+ return;
141
+ }
142
+
143
+ const a = arr.at(-1);
144
+ const b = arr[0];
145
+
146
+ const v = a.point
147
+ .subtract(b.point)
148
+ .multiply(1 / (a.timestamp - b.timestamp));
149
+
150
+ let x = prevPoint.x;
151
+
152
+ smoother = makeLerpSmoother(
153
+ animator,
154
+ (a) => {
155
+ handleZoom({
156
+ x: a,
157
+ y: prevPoint.y,
158
+ xDelta: x - a,
159
+ yDelta: 0,
160
+ zDelta: 0,
161
+ });
162
+ x = a;
163
+ },
164
+ 150,
165
+ 0.5,
166
+ x
167
+ );
168
+
169
+ smoother(prevPoint.x - v.x * 250);
170
+ };
171
+
172
+ const onMouseup = () => {
82
173
  document.removeEventListener("mousemove", onMousemove);
83
174
  document.removeEventListener("mouseup", onMouseup);
175
+ animateInertia();
84
176
  };
85
177
 
86
178
  document.addEventListener("mouseup", onMouseup, false);
87
179
  document.addEventListener("mousemove", onMousemove, false);
88
180
  }
89
181
  }
182
+
183
+ /**
184
+ * Split the array into two vectors and compare their lengths to find out if
185
+ * the mouse movement is decelerating.
186
+ * @param {{point: Point, timestamp: number}[]} arr
187
+ */
188
+ function isDecelerating(arr) {
189
+ const mid = arr[Math.floor(arr.length / 2)];
190
+
191
+ const ap = mid.point
192
+ .subtract(arr[0].point)
193
+ .multiply(mid.timestamp - arr[0].timestamp);
194
+ const bp = arr
195
+ .at(-1)
196
+ .point.subtract(mid.point)
197
+ .multiply(arr.at(-1).timestamp - mid.timestamp);
198
+
199
+ const a = ap.length;
200
+ const b = bp.length;
201
+
202
+ // Found by trial and error
203
+ const maxRatio = 0.4;
204
+
205
+ return b / a < maxRatio;
206
+ }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "contributors": [],
9
9
  "license": "MIT",
10
- "version": "0.46.1",
10
+ "version": "0.48.0",
11
11
  "jsdelivr": "dist/bundle/index.js",
12
12
  "unpkg": "dist/bundle/index.js",
13
13
  "browser": "dist/bundle/index.js",
@@ -64,5 +64,5 @@
64
64
  "vega-scale": "^7.3.1",
65
65
  "vega-util": "^1.17.2"
66
66
  },
67
- "gitHead": "9e43f979e60032c4a6a3780b1eba429358917bd7"
67
+ "gitHead": "64cd3335607b2608db3c85c74a4fb571302e77d0"
68
68
  }
@@ -1,60 +0,0 @@
1
- /**
2
- * A Rendering context that doesn't render anything. It creates a hierarchy
3
- * of view coordinates, including faceted views that are repeated multiple times.
4
- * The coordinates can be used for mouse events / interactions, for example.
5
- *
6
- * @typedef {import("../view.js").default} View
7
- * @typedef {import("../layout/rectangle.js").default} Rectangle
8
- *
9
- */
10
- export default class LayoutRecorderViewRenderingContext extends ViewRenderingContext {
11
- /** @type {ViewCoords} */
12
- root: ViewCoords;
13
- /** @type {ViewCoords[]} */
14
- stack: ViewCoords[];
15
- /** @type {ViewCoords} */
16
- lastAddition: ViewCoords;
17
- getLayout(): ViewCoords;
18
- }
19
- /**
20
- * A Rendering context that doesn't render anything. It creates a hierarchy
21
- * of view coordinates, including faceted views that are repeated multiple times.
22
- * The coordinates can be used for mouse events / interactions, for example.
23
- */
24
- export type View = import("../view.js").default;
25
- /**
26
- * A Rendering context that doesn't render anything. It creates a hierarchy
27
- * of view coordinates, including faceted views that are repeated multiple times.
28
- * The coordinates can be used for mouse events / interactions, for example.
29
- */
30
- export type Rectangle = import("../layout/rectangle.js").default;
31
- import ViewRenderingContext from "./viewRenderingContext.js";
32
- /**
33
- * Represents coordinates of view instances. Faceted views objects may have
34
- * been rendered at multiple locations.
35
- */
36
- declare class ViewCoords {
37
- /**
38
- * @param {View} view
39
- * @param {Rectangle} coords
40
- */
41
- constructor(view: View, coords: Rectangle);
42
- view: import("../view.js").default;
43
- coords: import("../layout/rectangle.js").default;
44
- /** @type {ViewCoords[]} */
45
- children: ViewCoords[];
46
- /**
47
- *
48
- * @param {ViewCoords} viewCoords
49
- */
50
- addChild(viewCoords: ViewCoords): void;
51
- /**
52
- * Broadcasts a message to views that include the given (x, y) point.
53
- * This is mainly intended for mouse events.
54
- *
55
- * @param {import("../../utils/interactionEvent.js").default} event
56
- */
57
- dispatchInteractionEvent(event: import("../../utils/interactionEvent.js").default): void;
58
- }
59
- export {};
60
- //# sourceMappingURL=layoutRecorderViewRenderingContext.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"layoutRecorderViewRenderingContext.d.ts","sourceRoot":"","sources":["../../../../src/view/renderingContext/layoutRecorderViewRenderingContext.js"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH;IAOQ,yBAAyB;IACzB,MADW,UAAU,CACA;IAErB,2BAA2B;IAC3B,OADW,UAAU,EAAE,CACR;IAEf,yBAAyB;IACzB,cADW,UAAU,CACQ;IAgCjC,wBAEC;CACJ;;;;;;mBArDY,OAAO,YAAY,EAAE,OAAO;;;;;;wBAC5B,OAAO,wBAAwB,EAAE,OAAO;iCARpB,2BAA2B;AA8D5D;;;GAGG;AACH;IACI;;;OAGG;IACH,kBAHW,IAAI,UACJ,SAAS,EAOnB;IAJG,mCAAgB;IAChB,iDAAoB;IACpB,2BAA2B;IAC3B,UADW,UAAU,EAAE,CACL;IAGtB;;;OAGG;IACH,qBAFW,UAAU,QAcpB;IAED;;;;;OAKG;IACH,gCAFW,OAAO,iCAAiC,EAAE,OAAO,QAyB3D;CACJ"}
@@ -1,128 +0,0 @@
1
- import { peek } from "../../utils/arrayUtils.js";
2
- import ViewRenderingContext from "./viewRenderingContext.js";
3
-
4
- /**
5
- * A Rendering context that doesn't render anything. It creates a hierarchy
6
- * of view coordinates, including faceted views that are repeated multiple times.
7
- * The coordinates can be used for mouse events / interactions, for example.
8
- *
9
- * @typedef {import("../view.js").default} View
10
- * @typedef {import("../layout/rectangle.js").default} Rectangle
11
- *
12
- */
13
- export default class LayoutRecorderViewRenderingContext extends ViewRenderingContext {
14
- /**
15
- * @param {import("../../types/rendering.js").GlobalRenderingOptions} globalOptions
16
- */
17
- constructor(globalOptions) {
18
- super(globalOptions);
19
-
20
- /** @type {ViewCoords} */
21
- this.root = undefined;
22
-
23
- /** @type {ViewCoords[]} */
24
- this.stack = [];
25
-
26
- /** @type {ViewCoords} */
27
- this.lastAddition = undefined;
28
- }
29
-
30
- /**
31
- * Must be called when a view's render() method is entered
32
- *
33
- * @param {View} view
34
- * @param {Rectangle} coords View coordinates
35
- * inside the padding.
36
- */
37
- pushView(view, coords) {
38
- // TODO: Facet id
39
-
40
- const viewCoords = new ViewCoords(view, coords);
41
-
42
- if (!this.root) {
43
- this.root = viewCoords;
44
- } else {
45
- peek(this.stack).addChild(viewCoords);
46
- }
47
- this.stack.push(viewCoords);
48
- }
49
-
50
- /**
51
- * Must be called when a view's render() method is being exited
52
- *
53
- * @param {View} view
54
- */
55
- popView(view) {
56
- this.stack.pop();
57
- }
58
-
59
- getLayout() {
60
- return this.root;
61
- }
62
- }
63
-
64
- /**
65
- * Represents coordinates of view instances. Faceted views objects may have
66
- * been rendered at multiple locations.
67
- */
68
- class ViewCoords {
69
- /**
70
- * @param {View} view
71
- * @param {Rectangle} coords
72
- */
73
- constructor(view, coords) {
74
- this.view = view;
75
- this.coords = coords;
76
- /** @type {ViewCoords[]} */
77
- this.children = [];
78
- }
79
-
80
- /**
81
- *
82
- * @param {ViewCoords} viewCoords
83
- */
84
- addChild(viewCoords) {
85
- const last = peek(this.children);
86
- if (
87
- last &&
88
- viewCoords.view === last.view &&
89
- viewCoords.coords.equals(last.coords)
90
- ) {
91
- // Skip extra copies of sample facets. They all have the same coords.
92
- return;
93
- }
94
-
95
- this.children.push(viewCoords);
96
- }
97
-
98
- /**
99
- * Broadcasts a message to views that include the given (x, y) point.
100
- * This is mainly intended for mouse events.
101
- *
102
- * @param {import("../../utils/interactionEvent.js").default} event
103
- */
104
- dispatchInteractionEvent(event) {
105
- if (this.coords.containsPoint(event.point.x, event.point.y)) {
106
- this.view.handleInteractionEvent(this.coords, event, true);
107
- if (event.stopped) {
108
- return;
109
- }
110
-
111
- if (this.children.length == 0) {
112
- event.target = this.view;
113
- } else {
114
- for (const child of this.children) {
115
- child.dispatchInteractionEvent(event);
116
- if (event.target) {
117
- break;
118
- }
119
- if (event.stopped) {
120
- return;
121
- }
122
- }
123
- }
124
-
125
- this.view.handleInteractionEvent(this.coords, event, false);
126
- }
127
- }
128
- }