@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
@@ -1,513 +0,0 @@
1
- import ContainerView from "./containerView";
2
- import AxisView from "./axisView";
3
- import { getFlattenedViews } from "./viewUtils";
4
- import Padding from "../utils/layout/padding";
5
- import UnitView from "./unitView";
6
- import { ZERO_FLEXDIMENSIONS } from "../utils/layout/flexLayout";
7
-
8
- /**
9
- * @typedef {import("../spec/channel").PrimaryPositionalChannel} PositionalChannel
10
- * @typedef {import("../spec/view").GeometricDimension} GeometricDimension
11
- */
12
-
13
- /** @type {Record<PositionalChannel, AxisOrient[]>} */
14
- const CHANNEL_ORIENTS = {
15
- x: ["bottom", "top"],
16
- y: ["left", "right"],
17
- };
18
-
19
- /**
20
- * An internal view that wraps a unit or layer view and takes care of the axes.
21
- *
22
- * @typedef {import("../spec/view").LayerSpec} LayerSpec
23
- * @typedef {import("./view").default} View
24
- * @typedef {import("../spec/axis").Axis} Axis
25
- * @typedef {import("../spec/axis").GenomeAxis} GenomeAxis
26
- * @typedef {import("../spec/axis").AxisOrient} AxisOrient
27
- *
28
- * @typedef {Axis & { extent: number }} AugmentedAxis
29
- *
30
- * @typedef {object} ZoomEvent
31
- * @prop {number} x
32
- * @prop {number} y
33
- * @prop {number} xDelta
34
- * @prop {number} yDelta
35
- * @prop {number} zDelta
36
- */
37
- export default class DecoratorView extends ContainerView {
38
- /**
39
- * @param {import("./viewUtils").ViewContext} context
40
- * @param {import("./containerView").default} parent
41
- */
42
- constructor(context, parent) {
43
- super({}, context, parent, "decorator");
44
-
45
- /** @type { import("./layerView").default | import("./unitView").default } */
46
- this.child = undefined;
47
-
48
- /** @type {UnitView} */
49
- this.backgroundView = undefined;
50
-
51
- /** @type {Record<AxisOrient, AxisView>} */
52
- this.axisViews = {
53
- top: undefined,
54
- right: undefined,
55
- bottom: undefined,
56
- left: undefined,
57
- };
58
-
59
- ["mousedown", "wheel"].forEach((type) =>
60
- this.addInteractionEventListener(
61
- type,
62
- this.handleMouseEvent.bind(this)
63
- )
64
- );
65
- }
66
-
67
- /**
68
- * Creates the axis views
69
- *
70
- * TODO: Perhaps views need a common initialization method?
71
- */
72
- initialize() {
73
- Object.entries(CHANNEL_ORIENTS).forEach(([channel, orients]) =>
74
- this._initializeAxes(channel, orients)
75
- );
76
- this._invalidateCacheByPrefix("size/", "ancestors");
77
-
78
- // TODO: Merge viewConfig from all descendants (when there are layers)
79
- // TODO: Implement styles
80
-
81
- const viewConfig = this.child.spec?.view;
82
- if (viewConfig?.fill || viewConfig?.stroke) {
83
- this.backgroundView = new UnitView(
84
- createBackground(viewConfig),
85
- this.context,
86
- this,
87
- "background"
88
- );
89
- }
90
- }
91
-
92
- /**
93
- * @param {View} [whoIsAsking] Passed to the immediate parent. Allows for
94
- * selectively breaking the inheritance.
95
- */
96
- getEncoding(whoIsAsking) {
97
- if (
98
- Object.values(this.axisViews).find(
99
- (view) => whoIsAsking === view
100
- ) ||
101
- whoIsAsking == this.backgroundView
102
- ) {
103
- // Prevent the axis views from inheriting any encodings
104
- return {};
105
- }
106
-
107
- return super.getEncoding();
108
- }
109
-
110
- /**
111
- * @param {View} [whoIsAsking] Passed to the immediate parent. Allows for
112
- * selectively breaking the inheritance.
113
- * @return {function(object):any}
114
- */
115
- getFacetAccessor(whoIsAsking) {
116
- if (whoIsAsking != this.child) {
117
- // Axes have no facets
118
- return;
119
- }
120
-
121
- if (this.parent) {
122
- return this.parent.getFacetAccessor(this);
123
- }
124
- }
125
-
126
- /**
127
- * @returns {IterableIterator<View>}
128
- */
129
- *[Symbol.iterator]() {
130
- yield this.child;
131
- if (this.backgroundView) {
132
- yield this.backgroundView;
133
- }
134
- for (const view of Object.values(this.axisViews)) {
135
- if (view) {
136
- yield view;
137
- }
138
- }
139
- }
140
-
141
- _getAxisExtents() {
142
- return this._cache("size/axisExtents", () => {
143
- /** @type {Record<AxisOrient, number>} */
144
- // @ts-ignore
145
- const paddings = {};
146
- for (const view of Object.values(this.axisViews)) {
147
- if (view) {
148
- paddings[view.getOrient()] = view.getPerpendicularSize();
149
- }
150
- }
151
- return Padding.createFromRecord(paddings);
152
- });
153
- }
154
-
155
- _getAxisOffsets() {
156
- return this._cache("size/axisOffsets", () => {
157
- /** @type {Record<AxisOrient, number>} */
158
- // @ts-ignore
159
- const paddings = {};
160
- for (const view of Object.values(this.axisViews)) {
161
- if (view) {
162
- paddings[view.getOrient()] = view.axisProps.offset;
163
- }
164
- }
165
- return Padding.createFromRecord(paddings);
166
- });
167
- }
168
-
169
- getEffectivePadding() {
170
- // TODO: Handle negative axis extents
171
- return this._cache("size/effectivePadding", () =>
172
- this.getPadding().add(this._getAxisExtents())
173
- );
174
- }
175
-
176
- getSize() {
177
- return this._cache("size/size", () =>
178
- this.child.isVisible()
179
- ? this.getSizeFromSpec()
180
- .addPadding(this.getPadding())
181
- .addPadding(this.getAxisSizes())
182
- : ZERO_FLEXDIMENSIONS
183
- );
184
- }
185
-
186
- /**
187
- * Returns the amount of extra space the axes need on the plot edges.
188
- * The calculation takes axis offsets into account.
189
- *
190
- * @returns {Padding}
191
- */
192
- getAxisSizes() {
193
- // TODO: Clamp negative sizes (if axes are positioned entirely onto the plots)
194
- return this._cache("size/axisSizes", () =>
195
- this._getAxisExtents().add(this._getAxisOffsets())
196
- );
197
- }
198
-
199
- /**
200
- * @param {import("./renderingContext/viewRenderingContext").default} context
201
- * @param {import("../utils/layout/rectangle").default} coords
202
- * @param {import("./view").RenderingOptions} [options]
203
- */
204
- render(context, coords, options = {}) {
205
- if (!this.isVisible() || !this.child.isVisible()) {
206
- return;
207
- }
208
-
209
- coords = coords.shrink(this.getPadding());
210
- context.pushView(this, coords);
211
-
212
- const extents = this._getAxisExtents();
213
- const childCoords = coords.shrink(extents.add(this._getAxisOffsets()));
214
-
215
- this._childCoords = childCoords;
216
-
217
- if (this.backgroundView) {
218
- this.backgroundView.render(context, childCoords, options);
219
- }
220
-
221
- this.child.render(context, childCoords, options);
222
-
223
- const entries = this._cache("axisViewEntries", () =>
224
- Object.entries(this.axisViews).filter((e) => !!e[1])
225
- );
226
-
227
- for (const [orient, view] of entries) {
228
- const props = view.axisProps;
229
-
230
- /** @type {import("../utils/layout/rectangle").default} */
231
- let axisCoords;
232
-
233
- if (orient == "bottom") {
234
- axisCoords = childCoords
235
- .translate(0, childCoords.height + props.offset)
236
- .modify({ height: extents.bottom });
237
- } else if (orient == "top") {
238
- axisCoords = childCoords
239
- .translate(0, -extents.top - props.offset)
240
- .modify({ height: extents.top });
241
- } else if (orient == "left") {
242
- axisCoords = childCoords
243
- .translate(-extents.left - props.offset, 0)
244
- .modify({ width: extents.left });
245
- } else if (orient == "right") {
246
- axisCoords = childCoords
247
- .translate(childCoords.width + props.offset, 0)
248
- .modify({ width: extents.right });
249
- }
250
-
251
- // Axes have no faceted data, thus, pass undefined facetId
252
- view.render(context, axisCoords);
253
- }
254
-
255
- context.popView(this);
256
- }
257
-
258
- /**
259
- * Returns the views that should be scanned for resolutions: all view's ancestors and children.
260
- * Axis views are not included.
261
- */
262
- _getResolutionParticipants() {
263
- return [...this.getAncestors(), ...getFlattenedViews(this.child)];
264
- }
265
-
266
- /**
267
- * @param {string} channel
268
- * @param {AxisOrient[]} orients
269
- */
270
- _initializeAxes(channel, orients) {
271
- const resolutions = this._getResolutionParticipants()
272
- .map((view) => view.resolutions.axis[channel])
273
- .filter((resolution) => resolution);
274
-
275
- // First, fill the preferred slots
276
- for (const r of resolutions) {
277
- const axisProps = r.getAxisProps();
278
- if (axisProps && axisProps.orient) {
279
- if (!orients.includes(axisProps.orient)) {
280
- throw new Error(
281
- `Invalid axis orientation for '${channel}' channel: ${axisProps.orient}`
282
- );
283
- }
284
- if (this.axisViews[axisProps.orient]) {
285
- throw new Error(
286
- `The slot for ${axisProps.orient} axis is already reserved!`
287
- );
288
- }
289
- this.axisViews[axisProps.orient] = new AxisView(
290
- {
291
- ...axisProps,
292
- title: r.getTitle(),
293
- },
294
- r.scaleResolution.type,
295
- this.context,
296
- this
297
- );
298
- }
299
- }
300
-
301
- // Next, fill the slots in the preferred order
302
- // eslint-disable-next-line no-labels
303
- resolutionLoop: for (const r of resolutions) {
304
- const axisProps = r.getAxisProps();
305
- if (axisProps && !axisProps.orient) {
306
- for (const slot of orients) {
307
- if (!this.axisViews[slot]) {
308
- axisProps.orient = /** @type {AxisOrient} */ (slot);
309
- this.axisViews[slot] = new AxisView(
310
- {
311
- ...axisProps,
312
- title: r.getTitle(),
313
- },
314
- r.scaleResolution.type,
315
- this.context,
316
- this
317
- );
318
- // eslint-disable-next-line no-labels
319
- continue resolutionLoop;
320
- }
321
- }
322
- throw new Error(
323
- "No room for axes. All slots are already reserved."
324
- );
325
- }
326
- }
327
- }
328
-
329
- /**
330
- * @param {import("../utils/layout/rectangle").default} coords
331
- * Coordinates of the view
332
- * @param {import("../utils/interactionEvent").default} event
333
- */
334
- handleMouseEvent(coords, event) {
335
- if (!this.isZoomable()) {
336
- return;
337
- }
338
-
339
- // TODO: Extract a class or something
340
-
341
- if (event.type == "wheel") {
342
- event.uiEvent.preventDefault(); // TODO: Only if there was something to zoom
343
-
344
- const wheelEvent = /** @type {WheelEvent} */ (event.uiEvent);
345
- const wheelMultiplier = wheelEvent.deltaMode ? 120 : 1;
346
-
347
- let { x, y } = event.point;
348
-
349
- // Snapping to the hovered item:
350
- // We find the currently hovered object and move the pointed coordinates
351
- // to its center if the mark has only primary positional channels.
352
- // This allows the user to rapidly zoom closer without having to
353
- // continuously adjust the cursor position.
354
-
355
- const hover = this.context.getCurrentHover();
356
- if (hover) {
357
- const viewCoords = coords.shrink(this.getEffectivePadding());
358
-
359
- const e = hover.mark.encoders;
360
- if (e.x && !e.x2) {
361
- x = +e.x(hover.datum) * viewCoords.width + viewCoords.x;
362
- }
363
- if (e.y && !e.y2) {
364
- y =
365
- (1 - +e.y(hover.datum)) * viewCoords.height +
366
- viewCoords.y;
367
- }
368
- }
369
-
370
- if (Math.abs(wheelEvent.deltaX) < Math.abs(wheelEvent.deltaY)) {
371
- this._handleZoom(coords, {
372
- x,
373
- y,
374
- xDelta: 0,
375
- yDelta: 0,
376
- zDelta: (wheelEvent.deltaY * wheelMultiplier) / 300,
377
- });
378
- } else {
379
- this._handleZoom(coords, {
380
- x,
381
- y,
382
- xDelta: -wheelEvent.deltaX * wheelMultiplier,
383
- yDelta: 0,
384
- zDelta: 0,
385
- });
386
- }
387
- } else if (
388
- event.type == "mousedown" &&
389
- /** @type {MouseEvent} */ (event.uiEvent).button === 0
390
- ) {
391
- const mouseEvent = /** @type {MouseEvent} */ (event.uiEvent);
392
- mouseEvent.preventDefault();
393
-
394
- let prevMouseEvent = mouseEvent;
395
-
396
- const onMousemove = /** @param {MouseEvent} moveEvent */ (
397
- moveEvent
398
- ) => {
399
- this._handleZoom(coords, {
400
- x: prevMouseEvent.clientX,
401
- y: prevMouseEvent.clientY,
402
- xDelta: moveEvent.clientX - prevMouseEvent.clientX,
403
- yDelta: moveEvent.clientY - prevMouseEvent.clientY,
404
- zDelta: 0,
405
- });
406
-
407
- prevMouseEvent = moveEvent;
408
- };
409
-
410
- const onMouseup = /** @param {MouseEvent} upEvent */ (upEvent) => {
411
- document.removeEventListener("mousemove", onMousemove);
412
- document.removeEventListener("mouseup", onMouseup);
413
- };
414
-
415
- document.addEventListener("mouseup", onMouseup, false);
416
- document.addEventListener("mousemove", onMousemove, false);
417
- }
418
- }
419
-
420
- isZoomable() {
421
- return this._cache("zoomable", () =>
422
- Object.values(this._getZoomableResolutions()).some(
423
- (set) => set.size
424
- )
425
- );
426
- }
427
-
428
- _getZoomableResolutions() {
429
- return this._cache("zoomableResolutions", () => {
430
- /** @type {Record<import("../spec/channel").PrimaryPositionalChannel, Set<import("./scaleResolution").default>>} */
431
- const resolutions = {
432
- x: new Set(),
433
- y: new Set(),
434
- };
435
-
436
- // Find all resolutions (scales) that are candidates for zooming
437
- this.child.visit((v) => {
438
- for (const [channel, resolutionSet] of Object.entries(
439
- resolutions
440
- )) {
441
- const resolution = v.getScaleResolution(channel);
442
- if (resolution && resolution.isZoomable()) {
443
- resolutionSet.add(resolution);
444
- }
445
- }
446
- });
447
-
448
- return resolutions;
449
- });
450
- }
451
-
452
- /**
453
- *
454
- * @param {import("../utils/layout/rectangle").default} coords Coordinates
455
- * @param {ZoomEvent} zoomEvent
456
- */
457
- _handleZoom(coords, zoomEvent) {
458
- for (const [channel, resolutionSet] of Object.entries(
459
- this._getZoomableResolutions()
460
- )) {
461
- if (resolutionSet.size <= 0) {
462
- continue;
463
- }
464
-
465
- const extents = this._getAxisExtents();
466
- const childCoords = coords.shrink(
467
- extents.add(this._getAxisOffsets())
468
- );
469
-
470
- const p = childCoords.normalizePoint(zoomEvent.x, zoomEvent.y);
471
- const tp = childCoords.normalizePoint(
472
- zoomEvent.x + zoomEvent.xDelta,
473
- zoomEvent.y + zoomEvent.yDelta
474
- );
475
-
476
- const delta = {
477
- x: tp.x - p.x,
478
- y: tp.y - p.y,
479
- };
480
-
481
- for (const resolution of resolutionSet) {
482
- resolution.zoom(
483
- 2 ** zoomEvent.zDelta,
484
- channel == "y"
485
- ? 1 - p[/** @type {PositionalChannel} */ (channel)]
486
- : p[/** @type {PositionalChannel} */ (channel)],
487
- channel == "x" ? delta.x : -delta.y
488
- );
489
- }
490
- }
491
-
492
- this.context.animator.requestRender();
493
- }
494
- }
495
-
496
- /**
497
- * @param {import("../spec/view").ViewConfig} viewConfig
498
- * @returns {import("../spec/view").UnitSpec}
499
- */
500
- function createBackground(viewConfig) {
501
- return {
502
- configurableVisibility: false,
503
- data: { values: [{}] },
504
- mark: {
505
- fill: null,
506
- strokeWidth: 1.0,
507
- ...viewConfig,
508
- type: "rect",
509
- clip: false, // Shouldn't be needed
510
- tooltip: null,
511
- },
512
- };
513
- }