@genome-spy/core 0.19.1 → 0.22.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 (42) hide show
  1. package/dist/index.js +42 -42
  2. package/dist/schema.json +213 -21
  3. package/package.json +3 -3
  4. package/src/genomeSpy.js +16 -11
  5. package/src/gl/dataToVertices.js +43 -46
  6. package/src/gl/includes/common.glsl +12 -12
  7. package/src/gl/includes/picking.fragment.glsl +0 -2
  8. package/src/gl/includes/picking.vertex.glsl +0 -2
  9. package/src/marks/link.js +32 -39
  10. package/src/marks/mark.js +168 -95
  11. package/src/marks/pointMark.js +28 -59
  12. package/src/marks/rectMark.js +38 -33
  13. package/src/marks/rule.js +31 -21
  14. package/src/marks/text.js +18 -14
  15. package/src/spec/mark.d.ts +0 -3
  16. package/src/spec/title.d.ts +102 -0
  17. package/src/spec/view.d.ts +6 -4
  18. package/src/tooltip/dataTooltipHandler.js +3 -2
  19. package/src/utils/binnedIndex.js +147 -0
  20. package/src/utils/binnedIndex.test.js +73 -0
  21. package/src/utils/layout/flexLayout.js +35 -3
  22. package/src/utils/layout/flexLayout.test.js +14 -0
  23. package/src/utils/layout/grid.js +95 -0
  24. package/src/utils/layout/grid.test.js +71 -0
  25. package/src/utils/layout/padding.js +13 -0
  26. package/src/utils/layout/rectangle.js +6 -0
  27. package/src/view/axisView.js +3 -5
  28. package/src/view/concatView.js +24 -275
  29. package/src/view/containerView.js +18 -0
  30. package/src/view/gridView.js +774 -0
  31. package/src/view/implicitRootView.js +14 -0
  32. package/src/view/layerView.js +15 -1
  33. package/src/view/renderingContext/deferredViewRenderingContext.js +3 -1
  34. package/src/view/renderingContext/simpleViewRenderingContext.js +3 -1
  35. package/src/view/title.js +165 -0
  36. package/src/view/unitView.js +9 -5
  37. package/src/view/view.js +35 -14
  38. package/src/view/viewContext.d.ts +6 -1
  39. package/src/view/viewUtils.js +1 -93
  40. package/src/view/zoom.js +89 -0
  41. package/src/utils/binnedRangeIndex.js +0 -83
  42. 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
  }
@@ -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
  /**
@@ -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
@@ -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
+ }
@@ -1,83 +0,0 @@
1
- import clamp from "./clamp";
2
-
3
- const MAX_INTEGER = 2 ** 31 - 1;
4
-
5
- /**
6
- * @callback Lookup
7
- * @param {number} start
8
- * @param {number} end
9
- * @returns {[number, number]}
10
- */
11
-
12
- /**
13
- * A binned index for (overlapping) ranges that are sorted by their start position.
14
- * Allows for indexing vertices of mark instances.
15
- *
16
- * @param {number} size Number of bins
17
- * @param {[number, number]} domain
18
- */
19
- export default function createBinningRangeIndexer(size, domain) {
20
- const startIndices = new Int32Array(size);
21
- startIndices.fill(MAX_INTEGER);
22
-
23
- const endIndices = new Int32Array(size);
24
-
25
- const start = domain[0];
26
- const domainLength = domain[1] - domain[0];
27
- const divisor = domainLength / size;
28
-
29
- /** @param {number} pos */
30
- const getBin = (pos) =>
31
- clamp(Math.floor((pos - start) / divisor), 0, size - 1);
32
-
33
- /**
34
- *
35
- * @param {number} start
36
- * @param {number} end
37
- * @param {number} startIndex
38
- * @param {number} endIndex
39
- */
40
- const indexer = (start, end, startIndex, endIndex) => {
41
- const startBin = getBin(start);
42
- const endBin = getBin(end);
43
-
44
- // TODO: This loop could probably be done as a more efficient post processing
45
- // step.
46
- for (let bin = startBin; bin <= endBin; bin++) {
47
- if (startIndices[bin] > startIndex) {
48
- startIndices[bin] = startIndex;
49
- }
50
-
51
- if (endIndices[bin] < endIndex) {
52
- endIndices[bin] = endIndex;
53
- }
54
- }
55
- };
56
-
57
- /**
58
- * @type {Lookup}
59
- */
60
- const lookup = (start, end) => [
61
- startIndices[getBin(start)],
62
- endIndices[getBin(end)],
63
- ];
64
-
65
- const getIndex = () => {
66
- for (let i = 1; i < endIndices.length; i++) {
67
- if (endIndices[i] < endIndices[i - 1]) {
68
- endIndices[i] = endIndices[i - 1];
69
- }
70
- }
71
- for (let i = endIndices.length - 1; i > 0; i--) {
72
- if (endIndices[i - 1] > endIndices[i]) {
73
- endIndices[i - 1] = endIndices[i];
74
- }
75
- }
76
-
77
- return lookup;
78
- };
79
-
80
- indexer.getIndex = getIndex;
81
-
82
- return indexer;
83
- }