@genome-spy/core 0.58.1 → 0.60.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 (117) hide show
  1. package/dist/bundle/{index-DwLfOHEk.js → index-5ajWdKly.js} +1 -1
  2. package/dist/bundle/{index-vgGDWUPz.js → index-B03-Om4z.js} +1 -1
  3. package/dist/bundle/index-Bg7C4Xat.js +2750 -0
  4. package/dist/bundle/{index-CalimFw3.js → index-C3QR8Lv6.js} +79 -79
  5. package/dist/bundle/{index-DKe9Bhvi.js → index-g8iXgW0W.js} +1 -1
  6. package/dist/bundle/index.es.js +6554 -6011
  7. package/dist/bundle/index.js +189 -164
  8. package/dist/bundle/{long-BviWyoZx.js → long-B-FASCSo.js} +45 -45
  9. package/dist/schema.json +312 -25
  10. package/dist/src/data/collector.d.ts.map +1 -1
  11. package/dist/src/data/collector.js +1 -0
  12. package/dist/src/data/flowNode.d.ts.map +1 -1
  13. package/dist/src/data/sources/dataSource.d.ts.map +1 -1
  14. package/dist/src/data/sources/dataUtils.d.ts +2 -1
  15. package/dist/src/data/sources/dataUtils.d.ts.map +1 -1
  16. package/dist/src/data/sources/dataUtils.js +3 -4
  17. package/dist/src/data/sources/inlineSource.d.ts +8 -0
  18. package/dist/src/data/sources/inlineSource.d.ts.map +1 -1
  19. package/dist/src/data/sources/inlineSource.js +17 -1
  20. package/dist/src/data/sources/urlSource.d.ts +1 -0
  21. package/dist/src/data/sources/urlSource.d.ts.map +1 -1
  22. package/dist/src/data/sources/urlSource.js +33 -4
  23. package/dist/src/data/transforms/identifier.d.ts.map +1 -1
  24. package/dist/src/data/transforms/measureText.js +1 -1
  25. package/dist/src/data/transforms/regexFold.d.ts.map +1 -1
  26. package/dist/src/data/transforms/regexFold.js +10 -0
  27. package/dist/src/data/transforms/regexFold.test.js +13 -0
  28. package/dist/src/encoder/encoder.d.ts +1 -1
  29. package/dist/src/fonts/bmFontManager.js +2 -2
  30. package/dist/src/fonts/bmFontMetrics.d.ts.map +1 -1
  31. package/dist/src/genomeSpy.d.ts.map +1 -1
  32. package/dist/src/genomeSpy.js +39 -19
  33. package/dist/src/gl/arrayBuilder.d.ts.map +1 -1
  34. package/dist/src/gl/colorUtils.d.ts +4 -0
  35. package/dist/src/gl/colorUtils.d.ts.map +1 -1
  36. package/dist/src/gl/colorUtils.js +8 -0
  37. package/dist/src/gl/glslScaleGenerator.d.ts +1 -1
  38. package/dist/src/gl/glslScaleGenerator.d.ts.map +1 -1
  39. package/dist/src/gl/glslScaleGenerator.js +1 -9
  40. package/dist/src/gl/includes/common.glsl.js +1 -1
  41. package/dist/src/gl/webGLHelper.d.ts +1 -1
  42. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  43. package/dist/src/marks/link.d.ts.map +1 -1
  44. package/dist/src/marks/link.js +9 -1
  45. package/dist/src/marks/mark.d.ts +8 -0
  46. package/dist/src/marks/mark.d.ts.map +1 -1
  47. package/dist/src/marks/mark.js +101 -3
  48. package/dist/src/marks/point.d.ts +1 -1
  49. package/dist/src/marks/point.d.ts.map +1 -1
  50. package/dist/src/marks/point.fragment.glsl.js +1 -1
  51. package/dist/src/marks/point.vertex.glsl.js +1 -1
  52. package/dist/src/marks/rect.common.glsl.js +1 -1
  53. package/dist/src/marks/rect.d.ts.map +1 -1
  54. package/dist/src/marks/rect.fragment.glsl.js +1 -1
  55. package/dist/src/marks/rect.js +41 -0
  56. package/dist/src/marks/rect.vertex.glsl.js +1 -1
  57. package/dist/src/selection/selection.d.ts +27 -2
  58. package/dist/src/selection/selection.d.ts.map +1 -1
  59. package/dist/src/selection/selection.js +53 -3
  60. package/dist/src/spec/data.d.ts +18 -1
  61. package/dist/src/spec/mark.d.ts +58 -1
  62. package/dist/src/spec/parameter.d.ts +71 -31
  63. package/dist/src/spec/sampleView.d.ts +12 -1
  64. package/dist/src/spec/view.d.ts +9 -2
  65. package/dist/src/styles/genome-spy.css.d.ts +1 -1
  66. package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
  67. package/dist/src/styles/genome-spy.css.js +12 -1
  68. package/dist/src/styles/genome-spy.scss +19 -1
  69. package/dist/src/types/selectionTypes.d.ts +4 -7
  70. package/dist/src/types/viewContext.d.ts +0 -15
  71. package/dist/src/utils/expression.d.ts.map +1 -1
  72. package/dist/src/utils/expression.js +4 -0
  73. package/dist/src/utils/indexer.d.ts +0 -2
  74. package/dist/src/utils/indexer.d.ts.map +1 -1
  75. package/dist/src/utils/reservationMap.d.ts +4 -4
  76. package/dist/src/utils/reservationMap.d.ts.map +1 -1
  77. package/dist/src/utils/scaleNull.d.ts +0 -2
  78. package/dist/src/utils/scaleNull.d.ts.map +1 -1
  79. package/dist/src/utils/trees.d.ts +2 -2
  80. package/dist/src/utils/ui/tooltip.d.ts +6 -10
  81. package/dist/src/utils/ui/tooltip.d.ts.map +1 -1
  82. package/dist/src/utils/ui/tooltip.js +74 -42
  83. package/dist/src/view/concatView.d.ts +1 -1
  84. package/dist/src/view/concatView.d.ts.map +1 -1
  85. package/dist/src/view/concatView.js +1 -1
  86. package/dist/src/view/gridView/gridChild.d.ts +53 -0
  87. package/dist/src/view/gridView/gridChild.d.ts.map +1 -0
  88. package/dist/src/view/gridView/gridChild.js +753 -0
  89. package/dist/src/view/gridView/gridView.d.ts +64 -0
  90. package/dist/src/view/gridView/gridView.d.ts.map +1 -0
  91. package/dist/src/view/{gridView.js → gridView/gridView.js} +40 -595
  92. package/dist/src/view/gridView/scrollbar.d.ts +32 -0
  93. package/dist/src/view/gridView/scrollbar.d.ts.map +1 -0
  94. package/dist/src/view/gridView/scrollbar.js +186 -0
  95. package/dist/src/view/gridView/selectionRect.d.ts +10 -0
  96. package/dist/src/view/gridView/selectionRect.d.ts.map +1 -0
  97. package/dist/src/view/gridView/selectionRect.js +182 -0
  98. package/dist/src/view/layout/rectangle.d.ts +11 -1
  99. package/dist/src/view/layout/rectangle.d.ts.map +1 -1
  100. package/dist/src/view/layout/rectangle.js +22 -2
  101. package/dist/src/view/layout/rectangle.test.js +12 -0
  102. package/dist/src/view/paramMediator.d.ts.map +1 -1
  103. package/dist/src/view/paramMediator.js +11 -2
  104. package/dist/src/view/scaleResolution.d.ts +1 -0
  105. package/dist/src/view/scaleResolution.d.ts.map +1 -1
  106. package/dist/src/view/scaleResolution.js +43 -33
  107. package/dist/src/view/testUtils.d.ts.map +1 -1
  108. package/dist/src/view/testUtils.js +0 -4
  109. package/dist/src/view/view.d.ts +6 -0
  110. package/dist/src/view/view.d.ts.map +1 -1
  111. package/dist/src/view/view.js +19 -0
  112. package/dist/src/view/viewFactory.d.ts.map +1 -1
  113. package/dist/src/view/viewFactory.js +13 -1
  114. package/package.json +2 -2
  115. package/dist/bundle/index-DS2hvLgl.js +0 -3425
  116. package/dist/src/view/gridView.d.ts +0 -135
  117. package/dist/src/view/gridView.d.ts.map +0 -1
@@ -0,0 +1,753 @@
1
+ import { isContinuous } from "vega-scale";
2
+ import {
3
+ asSelectionConfig,
4
+ createIntervalSelection,
5
+ isActiveIntervalSelection,
6
+ isIntervalSelectionConfig,
7
+ selectionContainsPoint,
8
+ } from "../../selection/selection.js";
9
+ import AxisGridView from "../axisGridView.js";
10
+ import AxisView, { CHANNEL_ORIENTS } from "../axisView.js";
11
+ import LayerView from "../layerView.js";
12
+ import Padding from "../layout/padding.js";
13
+ import Point from "../layout/point.js";
14
+ import Rectangle from "../layout/rectangle.js";
15
+ import createTitle from "../title.js";
16
+ import UnitView from "../unitView.js";
17
+ import Scrollbar from "./scrollbar.js";
18
+ import SelectionRect from "./selectionRect.js";
19
+
20
+ export default class GridChild {
21
+ /**
22
+ * @param {import("../view.js").default} view
23
+ * @param {import("../containerView.js").default} layoutParent
24
+ * @param {number} serial
25
+ */
26
+ constructor(view, layoutParent, serial) {
27
+ this.layoutParent = layoutParent;
28
+ this.view = view;
29
+ this.serial = serial;
30
+
31
+ /** @type {UnitView} */
32
+ this.background = undefined;
33
+
34
+ /** @type {UnitView} */
35
+ this.backgroundStroke = undefined;
36
+
37
+ /** @type {Partial<Record<import("../../spec/axis.js").AxisOrient, AxisView>>} axes */
38
+ this.axes = {};
39
+
40
+ /** @type {Partial<Record<import("../../spec/axis.js").AxisOrient, AxisGridView>>} gridLines */
41
+ this.gridLines = {};
42
+
43
+ /** @type {Partial<Record<import("./scrollbar.js").ScrollDirection, Scrollbar>>} */
44
+ this.scrollbars = {};
45
+
46
+ /** @type {SelectionRect} */
47
+ this.selectionRect = undefined;
48
+
49
+ /** @type {UnitView} */
50
+ this.title = undefined;
51
+
52
+ /** @type {Rectangle} */
53
+ this.coords = Rectangle.ZERO;
54
+
55
+ if (view.needsAxes.x || view.needsAxes.y) {
56
+ const spec = view.spec;
57
+ const viewBackground = "view" in spec ? spec?.view : undefined;
58
+
59
+ const backgroundSpec = createBackground(viewBackground);
60
+ if (backgroundSpec) {
61
+ this.background = new UnitView(
62
+ backgroundSpec,
63
+ layoutParent.context,
64
+ layoutParent,
65
+ view,
66
+ "background" + serial,
67
+ {
68
+ blockEncodingInheritance: true,
69
+ }
70
+ );
71
+ }
72
+
73
+ const backgroundStrokeSpec = createBackgroundStroke(viewBackground);
74
+ if (backgroundStrokeSpec) {
75
+ this.backgroundStroke = new UnitView(
76
+ backgroundStrokeSpec,
77
+ layoutParent.context,
78
+ layoutParent,
79
+ view,
80
+ "backgroundStroke" + serial,
81
+ {
82
+ blockEncodingInheritance: true,
83
+ }
84
+ );
85
+ }
86
+
87
+ const title = createTitle(view.spec.title);
88
+ if (title) {
89
+ const unitView = new UnitView(
90
+ title,
91
+ layoutParent.context,
92
+ layoutParent,
93
+ view,
94
+ "title" + serial,
95
+ {
96
+ blockEncodingInheritance: true,
97
+ }
98
+ );
99
+ this.title = unitView;
100
+ }
101
+ }
102
+
103
+ // TODO: More specific getter for this
104
+ if (view.spec.viewportWidth != null) {
105
+ this.scrollbars.horizontal = new Scrollbar(this, "horizontal");
106
+ }
107
+
108
+ if (view.spec.viewportHeight != null) {
109
+ this.scrollbars.vertical = new Scrollbar(this, "vertical");
110
+ }
111
+
112
+ this.#setupIntervalSelection();
113
+ }
114
+
115
+ #setupIntervalSelection() {
116
+ const view = this.view;
117
+
118
+ // TODO: Move to context
119
+ const setCursor = (/** @type {string} */ cursor) => {
120
+ this.view.context.glHelper.canvas.style.cursor = cursor;
121
+ };
122
+
123
+ // TODO: If the child is a LayerView, selection params should be pulled from its children as well
124
+ for (const [name, param] of view.paramMediator.paramConfigs) {
125
+ if (!("select" in param)) {
126
+ continue;
127
+ }
128
+
129
+ const select = asSelectionConfig(param.select);
130
+
131
+ if (!isIntervalSelectionConfig(select)) {
132
+ continue;
133
+ }
134
+
135
+ const channels = select.encodings;
136
+
137
+ const scaleResolutions = Object.fromEntries(
138
+ channels.map((channel) => {
139
+ const resolution = this.view.getScaleResolution(channel);
140
+ const scale = resolution?.scale;
141
+
142
+ if (!scale || !isContinuous(scale.type)) {
143
+ throw new Error(
144
+ `No continuous scale found for interval selection param "${name}" on channel "${channel}"! Scale type is "${scale?.type ?? "none"}".`
145
+ );
146
+ }
147
+ return [channel, resolution];
148
+ })
149
+ );
150
+
151
+ if (this.selectionRect) {
152
+ throw new Error(
153
+ "Only one interval selection per container is currently allowed!"
154
+ );
155
+ }
156
+
157
+ // --- Validation and early exits done ---
158
+
159
+ let mouseOver = false;
160
+ let preventNextClickPropagation = false;
161
+ let nowBrushing = false;
162
+
163
+ /**
164
+ * Selection rectangle in screen coordinates. Used when translating
165
+ * an existing selection.
166
+ * @type {Rectangle}
167
+ */
168
+ let translatedRectangle = null;
169
+
170
+ /**
171
+ * @param {{x: number, y: number}} a
172
+ * @param {{x: number, y: number}} b
173
+ * @return {Partial<Record<import("../../spec/channel.js").PrimaryPositionalChannel, [number, number]>>}
174
+ */
175
+ const pointsToIntervals = (a, b) =>
176
+ Object.fromEntries(
177
+ channels.map((channel) => [
178
+ channel,
179
+ [
180
+ Math.min(a[channel], b[channel]),
181
+ Math.max(a[channel], b[channel]),
182
+ ],
183
+ ])
184
+ );
185
+
186
+ const selectionExpr = view.paramMediator.createExpression(name);
187
+ const setter = view.paramMediator.getSetter(name);
188
+
189
+ if (param.value) {
190
+ setter({ type: "interval", intervals: param.value });
191
+ }
192
+
193
+ const clearSelection = () => {
194
+ setter(createIntervalSelection(channels));
195
+ setCursor(null);
196
+ };
197
+
198
+ this.selectionRect = new SelectionRect(
199
+ this,
200
+ selectionExpr,
201
+ select.mark
202
+ );
203
+
204
+ // WARNING! The following is an async method! Seems to work (by chance).
205
+ // TODO: Should be called and awaited in a sensible place. Maybe provide some
206
+ // registration logic for such post-creation initializations?
207
+ this.selectionRect.initializeChildren();
208
+
209
+ const invertPoint = (
210
+ /** @type {import("../layout/point.js").default} */ point
211
+ ) => {
212
+ const inverted = { x: 0, y: 0 };
213
+
214
+ const np = view.coords.normalizePoint(point.x, point.y, true);
215
+
216
+ for (const channel of channels) {
217
+ const scale = scaleResolutions[channel].scale;
218
+ // @ts-ignore
219
+ const val = scale.invert(channel == "x" ? np.x : np.y);
220
+ inverted[channel] =
221
+ val +
222
+ (["index", "locus"].includes(scale.type) ? 0.5 : 0);
223
+ }
224
+
225
+ return inverted;
226
+ };
227
+
228
+ /**
229
+ * Converts the current selection intervals (in scale domain) to a rectangle
230
+ * in screen coordinates.
231
+ * @param {import("../../types/selectionTypes.js").IntervalSelection} selection
232
+ */
233
+ const selectionToRect = (selection) => {
234
+ const { intervals } = selection;
235
+
236
+ const mapCorner = (
237
+ /** @type {number} */ xVal,
238
+ /** @type {number} */ yVal,
239
+ /** @type {number} */ i
240
+ ) => {
241
+ const getCoord = (
242
+ /** @type {import("../../spec/channel.js").PrimaryPositionalChannel} */ channel,
243
+ /** @type {number} */ val
244
+ ) => {
245
+ if (val == null) return null;
246
+ return scaleResolutions[channel].scale(val);
247
+ };
248
+ const px = getCoord("x", xVal) ?? i;
249
+ const py = getCoord("y", yVal) ?? i;
250
+ return view.coords.denormalizePoint(px, py, true);
251
+ };
252
+
253
+ const a = mapCorner(intervals.x?.[0], intervals.y?.[0], 0);
254
+ const b = mapCorner(intervals.x?.[1], intervals.y?.[1], 1);
255
+
256
+ return Rectangle.create(a.x, a.y, b.x - a.x, b.y - a.y);
257
+ };
258
+
259
+ view.addInteractionEventListener("mousedown", (coords, event) => {
260
+ if (/** @type {MouseEvent} */ (event.uiEvent).button != 0) {
261
+ return;
262
+ }
263
+
264
+ // Coordinates of the selection rectangle, if it exists.
265
+ // Must be operated in the view's coordinate system, not in data domain,
266
+ // as non-linear scales may be used.
267
+ translatedRectangle = mouseOver
268
+ ? selectionToRect(selectionExpr())
269
+ : null;
270
+
271
+ if (translatedRectangle) {
272
+ // Started dragging an existing selection
273
+ setCursor("grabbing");
274
+ // Start of dragging should prevent click propagation so that
275
+ // no other selections or events are triggered.
276
+ preventNextClickPropagation = true;
277
+ } else {
278
+ const mouseDownPoint = event.point;
279
+ if (isActiveIntervalSelection(selectionExpr())) {
280
+ // If there's a selection, prevent the next click from propagating.
281
+ // The first click will clear the selection, not trigger
282
+ // any other possible selections.
283
+ preventNextClickPropagation = true;
284
+ }
285
+
286
+ if (/** @type {MouseEvent} */ (event.uiEvent).shiftKey) {
287
+ // Start brushing a new selection, clear the existing selection
288
+ clearSelection();
289
+ nowBrushing = true;
290
+ } else if (isActiveIntervalSelection(selectionExpr())) {
291
+ // If mouse button is released and there was a selection,
292
+ // it should be cleared unless the viewport was panned by dragging.
293
+ /** @type {import("../view.js").InteractionEventListener} */
294
+ const listener = (coords, event) => {
295
+ view.removeInteractionEventListener(
296
+ "mouseup",
297
+ listener
298
+ );
299
+ const mouseUpPoint = event.point;
300
+
301
+ // Retain selection if the viewport is panned by dragging
302
+ const movementThreshold = 2; // pixels
303
+ if (
304
+ mouseDownPoint.subtract(mouseUpPoint).length <
305
+ movementThreshold
306
+ ) {
307
+ clearSelection();
308
+ }
309
+ };
310
+ view.addInteractionEventListener("mouseup", listener);
311
+ return;
312
+ }
313
+ }
314
+
315
+ // Prevent panning interaction
316
+ event.stopPropagation();
317
+
318
+ const start = event.point;
319
+ const viewOffset = Point.fromMouseEvent(
320
+ /** @type {MouseEvent} */ (event.uiEvent)
321
+ ).subtract(start);
322
+
323
+ const mouseMoveListener = (/** @type {MouseEvent} */ event) => {
324
+ // This listener is added to the document so that events are captured even if the mouse leaves the view.
325
+ // Thus, coordinates need to be adjusted to the view's coordinate system.
326
+ const current =
327
+ Point.fromMouseEvent(event).subtract(viewOffset);
328
+
329
+ /** @type {ReturnType<typeof pointsToIntervals>} */
330
+ let intervals;
331
+
332
+ if (translatedRectangle) {
333
+ const offset = current.subtract(start);
334
+ const newRect = translatedRectangle.translate(
335
+ offset.x,
336
+ offset.y
337
+ );
338
+
339
+ intervals = pointsToIntervals(
340
+ invertPoint(new Point(newRect.x, newRect.y)),
341
+ invertPoint(new Point(newRect.x2, newRect.y2))
342
+ );
343
+ } else {
344
+ intervals = pointsToIntervals(
345
+ invertPoint(start),
346
+ invertPoint(current)
347
+ );
348
+ }
349
+
350
+ for (const channel of channels) {
351
+ const scaleResolution = scaleResolutions[channel];
352
+ const { zoomExtent, scale } = scaleResolution;
353
+ const interval = intervals[channel];
354
+
355
+ if (["index", "locus"].includes(scale.type)) {
356
+ // These scales use integer values. Need to round them.
357
+ interval[0] = Math.ceil(interval[0]);
358
+ interval[1] = Math.ceil(interval[1]);
359
+ }
360
+
361
+ if (translatedRectangle) {
362
+ // When dragging, clamp the interval so that the size stays the same and the interval doesn't exceed zoomExtent
363
+ const size = interval[1] - interval[0];
364
+ const min = zoomExtent[0];
365
+ const max = zoomExtent[1];
366
+
367
+ // Clamp the start and end so the interval stays within bounds
368
+ // Note: Only works reliably with linear scales. TODO: Handle other scales.
369
+ if (interval[0] < min) {
370
+ interval[0] = min;
371
+ interval[1] = min + size;
372
+ }
373
+ if (interval[1] > max) {
374
+ interval[1] = max;
375
+ interval[0] = max - size;
376
+ }
377
+ } else {
378
+ interval[0] = Math.max(zoomExtent[0], interval[0]);
379
+ interval[1] = Math.min(zoomExtent[1], interval[1]);
380
+ }
381
+ interval[1] = Math.min(zoomExtent[1], interval[1]);
382
+ }
383
+
384
+ setter({ type: "interval", intervals });
385
+ };
386
+
387
+ const mouseUpListener = () => {
388
+ document.removeEventListener(
389
+ "mousemove",
390
+ mouseMoveListener
391
+ );
392
+ document.removeEventListener("mouseup", mouseUpListener);
393
+
394
+ nowBrushing = false;
395
+ if (translatedRectangle) {
396
+ setCursor("move");
397
+ translatedRectangle = null;
398
+ }
399
+ };
400
+ document.addEventListener("mousemove", mouseMoveListener);
401
+
402
+ document.addEventListener("mouseup", mouseUpListener);
403
+ });
404
+
405
+ view.addInteractionEventListener(
406
+ "click",
407
+ (coords, event) => {
408
+ if (/** @type {MouseEvent} */ (event.uiEvent).button == 0) {
409
+ if (preventNextClickPropagation) {
410
+ event.stopPropagation();
411
+ preventNextClickPropagation = false;
412
+ }
413
+ }
414
+ },
415
+ true
416
+ );
417
+
418
+ const isPointInsideSelection = (/** @type {Point} */ point) =>
419
+ selectionContainsPoint(selectionExpr(), invertPoint(point));
420
+
421
+ // TODO: Make behavior configurable
422
+ view.addInteractionEventListener(
423
+ "dblclick",
424
+ (coords, event) => {
425
+ if (isPointInsideSelection(event.point)) {
426
+ clearSelection();
427
+ event.stopPropagation();
428
+ }
429
+ },
430
+ true
431
+ );
432
+
433
+ // Handle mouse cursor changes
434
+ view.addInteractionEventListener("mousemove", (coords, event) => {
435
+ if (isPointInsideSelection(event.point)) {
436
+ // Brushing and translating the existing brush are different actions.
437
+ if (!nowBrushing) {
438
+ mouseOver = true;
439
+ // When translation is active, the cursor shows a grabbing hand.
440
+ if (!translatedRectangle) {
441
+ setCursor("move");
442
+ }
443
+ }
444
+ } else {
445
+ mouseOver = false;
446
+ if (!translatedRectangle) {
447
+ setCursor(null);
448
+ }
449
+ }
450
+ });
451
+ }
452
+ }
453
+
454
+ *getChildren() {
455
+ if (this.background) {
456
+ yield this.background;
457
+ }
458
+ if (this.backgroundStroke) {
459
+ yield this.backgroundStroke;
460
+ }
461
+ if (this.title) {
462
+ yield this.title;
463
+ }
464
+ yield* Object.values(this.axes);
465
+ yield* Object.values(this.gridLines);
466
+ yield this.view;
467
+ yield* Object.values(this.scrollbars);
468
+ if (this.selectionRect) {
469
+ yield this.selectionRect;
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Create view decorations, grid lines, axes, etc.
475
+ */
476
+ async createAxes() {
477
+ const { view, axes, gridLines } = this;
478
+
479
+ /**
480
+ * @param {import("../axisResolution.js").default} r
481
+ * @param {import("../../spec/channel.js").PrimaryPositionalChannel} channel
482
+ */
483
+ const getAxisPropsWithDefaults = (r, channel) => {
484
+ const propsWithoutDefaults = r.getAxisProps();
485
+ if (propsWithoutDefaults === null) {
486
+ return;
487
+ }
488
+
489
+ const props = propsWithoutDefaults
490
+ ? { ...propsWithoutDefaults }
491
+ : {};
492
+
493
+ // Pick a default orient based on what is available.
494
+ // This logic is needed for layer views that have independent axes.
495
+ if (!props.orient) {
496
+ for (const orient of CHANNEL_ORIENTS[channel]) {
497
+ if (!axes[orient]) {
498
+ props.orient = orient;
499
+ break;
500
+ }
501
+ }
502
+ if (!props.orient) {
503
+ throw new Error(
504
+ "No slots available for an axis! Perhaps a LayerView has more than two children?"
505
+ );
506
+ }
507
+ }
508
+
509
+ props.title ??= r.getTitle();
510
+
511
+ if (!CHANNEL_ORIENTS[channel].includes(props.orient)) {
512
+ throw new Error(
513
+ `Invalid axis orientation "${props.orient}" on channel "${channel}"!`
514
+ );
515
+ }
516
+
517
+ return props;
518
+ };
519
+
520
+ /**
521
+ * @param {import("../axisResolution.js").default} r
522
+ * @param {import("../../spec/channel.js").PrimaryPositionalChannel} channel
523
+ * @param {import("../view.js").default} axisParent
524
+ */
525
+ const createAxis = async (r, channel, axisParent) => {
526
+ const props = getAxisPropsWithDefaults(r, channel);
527
+
528
+ if (props) {
529
+ if (axes[props.orient]) {
530
+ throw new Error(
531
+ `An axis with the orient "${props.orient}" already exists!`
532
+ );
533
+ }
534
+
535
+ const axisView = new AxisView(
536
+ props,
537
+ r.scaleResolution.type,
538
+ this.layoutParent.context,
539
+ this.layoutParent,
540
+ axisParent
541
+ );
542
+ axes[props.orient] = axisView;
543
+ await axisView.initializeChildren();
544
+ }
545
+ };
546
+
547
+ /**
548
+ * @param {import("../axisResolution.js").default} r
549
+ * @param {import("../../spec/channel.js").PrimaryPositionalChannel} channel
550
+ * @param {import("../view.js").default} axisParent
551
+ */
552
+ const createAxisGrid = async (r, channel, axisParent) => {
553
+ const props = getAxisPropsWithDefaults(r, channel);
554
+
555
+ if (props && (props.grid || props.chromGrid)) {
556
+ const axisGridView = new AxisGridView(
557
+ props,
558
+ r.scaleResolution.type,
559
+ this.layoutParent.context,
560
+ this.layoutParent,
561
+ axisParent
562
+ );
563
+ gridLines[props.orient] = axisGridView;
564
+ await axisGridView.initializeChildren();
565
+ }
566
+ };
567
+
568
+ // Handle children that have caught axis resolutions. Create axes for them.
569
+ for (const channel of /** @type {import("../../spec/channel.js").PrimaryPositionalChannel[]} */ ([
570
+ "x",
571
+ "y",
572
+ ])) {
573
+ if (view.needsAxes[channel]) {
574
+ const r = view.resolutions.axis[channel];
575
+ if (!r) {
576
+ continue;
577
+ }
578
+
579
+ await createAxis(r, channel, view);
580
+ }
581
+ }
582
+
583
+ // Handle gridlines of children. Note: children's axis resolution may be caught by
584
+ // this view or some of this view's ancestors.
585
+ for (const channel of /** @type {import("../../spec/channel.js").PrimaryPositionalChannel[]} */ ([
586
+ "x",
587
+ "y",
588
+ ])) {
589
+ if (
590
+ view.needsAxes[channel] &&
591
+ // Handle a special case where the child view has an excluded resolution
592
+ // but no scale or axis, e.g., when only values are used on a channel.
593
+ view.getConfiguredOrDefaultResolution(channel, "axis") !=
594
+ "excluded"
595
+ ) {
596
+ const r = view.getAxisResolution(channel);
597
+ if (!r) {
598
+ continue;
599
+ }
600
+
601
+ await createAxisGrid(r, channel, view);
602
+ }
603
+ }
604
+
605
+ // Handle LayerView's possible independent axes
606
+ if (view instanceof LayerView) {
607
+ // First create axes that have an orient preference
608
+ for (const layerChild of view) {
609
+ for (const [channel, r] of Object.entries(
610
+ layerChild.resolutions.axis
611
+ )) {
612
+ const props = r.getAxisProps();
613
+ if (props && props.orient) {
614
+ await createAxis(r, channel, layerChild);
615
+ }
616
+ }
617
+ }
618
+
619
+ // Then create axes in a priority order
620
+ for (const layerChild of view) {
621
+ for (const [channel, r] of Object.entries(
622
+ layerChild.resolutions.axis
623
+ )) {
624
+ const props = r.getAxisProps();
625
+ if (props && !props.orient) {
626
+ await createAxis(r, channel, layerChild);
627
+ }
628
+ }
629
+ }
630
+
631
+ // TODO: Axis grid
632
+ }
633
+
634
+ // Axes are created after scales are resolved, so we need to resolve possible new scales here
635
+ [...Object.values(axes), ...Object.values(gridLines)].forEach((v) =>
636
+ v.visit((view) => {
637
+ if (view instanceof UnitView) {
638
+ view.resolve("scale");
639
+ }
640
+ })
641
+ );
642
+ }
643
+
644
+ getOverhang() {
645
+ const calculate = (
646
+ /** @type {import("../../spec/axis.js").AxisOrient} */ orient
647
+ ) => {
648
+ const axisView = this.axes[orient];
649
+ return axisView
650
+ ? Math.max(
651
+ axisView.getPerpendicularSize() +
652
+ (axisView.axisProps.offset ?? 0),
653
+ 0
654
+ )
655
+ : 0;
656
+ };
657
+
658
+ // Axes and overhang should be mutually exclusive, so we can just add them together
659
+ return new Padding(
660
+ calculate("top"),
661
+ calculate("right"),
662
+ calculate("bottom"),
663
+ calculate("left")
664
+ ).add(this.view.getOverhang());
665
+ }
666
+
667
+ getOverhangAndPadding() {
668
+ return this.getOverhang().add(this.view.getPadding());
669
+ }
670
+ }
671
+
672
+ /**
673
+ * @param {import("../../spec/view.js").ViewBackground} viewBackground
674
+ * @returns {import("../../spec/view.js").UnitSpec}
675
+ */
676
+ export function createBackground(viewBackground) {
677
+ const required =
678
+ viewBackground?.fill ||
679
+ viewBackground?.fillOpacity ||
680
+ viewBackground?.shadowOpacity;
681
+ if (!required) {
682
+ return;
683
+ }
684
+
685
+ return {
686
+ configurableVisibility: false,
687
+ data: { values: [{}] },
688
+ mark: {
689
+ color: viewBackground.fill,
690
+ opacity:
691
+ viewBackground.fillOpacity ?? (viewBackground.fill ? 1.0 : 0.0),
692
+ type: "rect",
693
+ clip: false, // Shouldn't be needed
694
+ tooltip: null,
695
+ minHeight: 1,
696
+ minOpacity: 0,
697
+ shadowBlur: viewBackground.shadowBlur,
698
+ shadowColor: viewBackground.shadowColor,
699
+ shadowOffsetX: viewBackground.shadowOffsetX,
700
+ shadowOffsetY: viewBackground.shadowOffsetY,
701
+ shadowOpacity: viewBackground.shadowOpacity,
702
+ },
703
+ };
704
+ }
705
+
706
+ /**
707
+ * @param {import("../../spec/view.js").ViewBackground} viewBackground
708
+ * @returns {import("../../spec/view.js").UnitSpec}
709
+ */
710
+ export function createBackgroundStroke(viewBackground) {
711
+ if (
712
+ !viewBackground ||
713
+ !viewBackground.stroke ||
714
+ viewBackground.strokeWidth === 0 ||
715
+ viewBackground.strokeOpacity === 0
716
+ ) {
717
+ return;
718
+ }
719
+
720
+ // Using rules to draw a non-filled rectangle.
721
+ // We are not using a rect mark because it is not optimized for outlines.
722
+ // TODO: Implement "hollow" mesh for non-filled rectangles
723
+ return {
724
+ configurableVisibility: false,
725
+ resolve: {
726
+ scale: { x: "excluded", y: "excluded" },
727
+ axis: { x: "excluded", y: "excluded" },
728
+ },
729
+ data: {
730
+ values: [
731
+ { x: 0, y: 0, x2: 1, y2: 0 },
732
+ { x: 1, y: 0, x2: 1, y2: 1 },
733
+ { x: 1, y: 1, x2: 0, y2: 1 },
734
+ { x: 0, y: 1, x2: 0, y2: 0 },
735
+ ],
736
+ },
737
+ mark: {
738
+ size: viewBackground.strokeWidth ?? 1.0,
739
+ color: viewBackground.stroke ?? "lightgray",
740
+ strokeCap: "square",
741
+ opacity: viewBackground.strokeOpacity ?? 1.0,
742
+ type: "rule",
743
+ clip: false,
744
+ tooltip: null,
745
+ },
746
+ encoding: {
747
+ x: { field: "x", type: "quantitative", scale: null },
748
+ y: { field: "y", type: "quantitative", scale: null },
749
+ x2: { field: "x2" },
750
+ y2: { field: "y2" },
751
+ },
752
+ };
753
+ }