@genome-spy/core 0.19.1 → 0.20.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.
@@ -133,7 +133,7 @@ export function mapToPixelCoords(
133
133
  /**
134
134
  * Returns the minimum size (the sum of pixels sizes) for the flex items
135
135
  *
136
- * @param {SizeDef[]} items
136
+ * @param {Iterable<SizeDef>} items
137
137
  * @param {FlexOptions} [options]
138
138
  */
139
139
  export function getMinimumSize(items, { spacing } = { spacing: 0 }) {
@@ -144,6 +144,21 @@ export function getMinimumSize(items, { spacing } = { spacing: 0 }) {
144
144
  return Math.max(0, minimumSize - spacing);
145
145
  }
146
146
 
147
+ /**
148
+ * @param {Iterable<SizeDef>} items
149
+ * @returns {SizeDef}
150
+ */
151
+ export function getLargestSize(items) {
152
+ let px = 0;
153
+ let grow = 0;
154
+ for (const s of items) {
155
+ px = Math.max(px, s.px ?? 0);
156
+ grow = Math.max(grow, s.grow ?? 0);
157
+ }
158
+
159
+ return { px, grow };
160
+ }
161
+
147
162
  /**
148
163
  * Returns true if relative (stretching) elements are present
149
164
  * @param {SizeDef[]} items
@@ -172,13 +187,30 @@ export class FlexDimensions {
172
187
  * @param {import("./padding").default} padding
173
188
  */
174
189
  addPadding(padding) {
190
+ return this.#addPx(padding.width, padding.height);
191
+ }
192
+
193
+ /**
194
+ * Subtracts padding from absolute (px) dimensions
195
+ *
196
+ * @param {import("./padding").default} padding
197
+ */
198
+ subtractPadding(padding) {
199
+ return this.#addPx(-padding.width, -padding.height);
200
+ }
201
+
202
+ /**
203
+ * @param {number} width
204
+ * @param {number} height
205
+ */
206
+ #addPx(width, height) {
175
207
  return new FlexDimensions(
176
208
  {
177
- px: (this.width.px || 0) + padding.width,
209
+ px: (this.width.px ?? 0) + width,
178
210
  grow: this.width.grow,
179
211
  },
180
212
  {
181
- px: (this.height.px || 0) + padding.height,
213
+ px: (this.height.px ?? 0) + height,
182
214
  grow: this.height.grow,
183
215
  }
184
216
  );
@@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest";
2
2
  import {
3
3
  mapToPixelCoords,
4
4
  getMinimumSize,
5
+ getLargestSize,
5
6
  isStretching,
6
7
  parseSizeDef,
7
8
  } from "./flexLayout";
@@ -290,6 +291,19 @@ describe("Utility fuctions", () => {
290
291
  expect(getMinimumSize(items, { spacing: 10 })).toEqual(330);
291
292
  });
292
293
 
294
+ test("getLargestSize", () => {
295
+ const items = [
296
+ { px: 100 },
297
+ { px: 0, grow: 0 },
298
+ { grow: 1 },
299
+ { grow: 9 },
300
+ { px: 200 },
301
+ { px: 50 },
302
+ ];
303
+
304
+ expect(getLargestSize(items)).toEqual({ px: 200, grow: 9 });
305
+ });
306
+
293
307
  test("isStretching", () => {
294
308
  expect(isStretching([{ grow: 1 }])).toBeTruthy();
295
309
  expect(isStretching([{ px: 1 }])).toBeFalsy();
@@ -0,0 +1,95 @@
1
+ /**
2
+ * An utility class for indexing cells in a wrapping grid layout
3
+ */
4
+ export default class Grid {
5
+ /**
6
+ *
7
+ * @param {number} nChildren
8
+ * @param {number} [maxCols]
9
+ */
10
+ constructor(nChildren, maxCols) {
11
+ this.n = nChildren;
12
+ this.maxCols = maxCols ?? Infinity;
13
+ }
14
+
15
+ get nRows() {
16
+ return this.maxCols == Infinity ? 1 : Math.ceil(this.n / this.maxCols);
17
+ }
18
+
19
+ get nCols() {
20
+ return Math.min(this.n, this.maxCols);
21
+ }
22
+
23
+ get rowIndices() {
24
+ /** @type {number[][]} */
25
+ const rows = [];
26
+
27
+ const nCols = this.nCols;
28
+ const nRows = this.nRows;
29
+
30
+ for (let row = 0; row < nRows; row++) {
31
+ /** @type {number[]} */
32
+ const arr = [];
33
+ rows.push(arr);
34
+ for (let col = 0; col < nCols; col++) {
35
+ const i = row * nCols + col;
36
+ if (i < this.n) {
37
+ arr.push(i);
38
+ }
39
+ }
40
+ }
41
+ return rows;
42
+ }
43
+
44
+ get colIndices() {
45
+ /** @type {number[][]} */
46
+ const cols = [];
47
+
48
+ const nCols = this.nCols;
49
+ const nRows = this.nRows;
50
+
51
+ for (let col = 0; col < nCols; col++) {
52
+ /** @type {number[]} */
53
+ const arr = [];
54
+ cols.push(arr);
55
+ for (let row = 0; row < nRows; row++) {
56
+ const i = row * nCols + col;
57
+ if (i < this.n) {
58
+ arr.push(i);
59
+ }
60
+ }
61
+ }
62
+ return cols;
63
+ }
64
+
65
+ /**
66
+ * @param {number} col
67
+ * @param {number} row
68
+ */
69
+ getCellIndex(col, row) {
70
+ let i = 0;
71
+
72
+ if (this.maxCols == Infinity) {
73
+ i = row == 0 ? col : undefined;
74
+ } else if (col >= this.maxCols) {
75
+ return undefined;
76
+ } else {
77
+ i = row * this.nCols + col;
78
+ }
79
+
80
+ return i < this.n ? i : undefined;
81
+ }
82
+
83
+ /**
84
+ *
85
+ * @param {number} index
86
+ * @returns {[number, number]}
87
+ */
88
+ getCellCoords(index) {
89
+ if (index < 0 || index >= this.n) {
90
+ return undefined;
91
+ }
92
+
93
+ return [index % this.nCols, Math.floor(index / this.nCols)];
94
+ }
95
+ }
@@ -0,0 +1,71 @@
1
+ import { expect, test, describe } from "vitest";
2
+
3
+ import Grid from "./grid";
4
+
5
+ describe("Grid indexing", () => {
6
+ test("Single row", () => {
7
+ const g = new Grid(3);
8
+
9
+ expect(g.maxCols).toEqual(Infinity);
10
+ expect(g.nCols).toEqual(3);
11
+ expect(g.nRows).toEqual(1);
12
+ expect(g.colIndices).toEqual([[0], [1], [2]]);
13
+ expect(g.rowIndices).toEqual([[0, 1, 2]]);
14
+ expect(g.getCellIndex(1, 0)).toEqual(1);
15
+ expect(g.getCellIndex(1, 1)).toBeUndefined();
16
+ expect(g.getCellCoords(1)).toEqual([1, 0]);
17
+ expect(g.getCellCoords(-1)).toBeUndefined();
18
+ expect(g.getCellCoords(3)).toBeUndefined();
19
+ });
20
+
21
+ test("Single column", () => {
22
+ const g = new Grid(3, 1);
23
+
24
+ expect(g.maxCols).toEqual(1);
25
+ expect(g.nCols).toEqual(1);
26
+ expect(g.nRows).toEqual(3);
27
+ expect(g.colIndices).toEqual([[0, 1, 2]]);
28
+ expect(g.rowIndices).toEqual([[0], [1], [2]]);
29
+ expect(g.getCellIndex(0, 1)).toEqual(1);
30
+ expect(g.getCellIndex(1, 1)).toBeUndefined();
31
+ expect(g.getCellCoords(1)).toEqual([0, 1]);
32
+ });
33
+
34
+ test("Two columns", () => {
35
+ const g = new Grid(6, 2);
36
+
37
+ expect(g.maxCols).toEqual(2);
38
+ expect(g.nCols).toEqual(2);
39
+ expect(g.nRows).toEqual(3);
40
+ expect(g.colIndices).toEqual([
41
+ [0, 2, 4],
42
+ [1, 3, 5],
43
+ ]);
44
+ expect(g.rowIndices).toEqual([
45
+ [0, 1],
46
+ [2, 3],
47
+ [4, 5],
48
+ ]);
49
+ expect(g.getCellIndex(1, 0)).toEqual(1);
50
+ expect(g.getCellIndex(0, 1)).toEqual(2);
51
+ expect(g.getCellIndex(1, 1)).toEqual(3);
52
+ expect(g.getCellCoords(3)).toEqual([1, 1]);
53
+ });
54
+
55
+ test("Two columns, second is partial", () => {
56
+ const g = new Grid(5, 2);
57
+
58
+ expect(g.maxCols).toEqual(2);
59
+ expect(g.nCols).toEqual(2);
60
+ expect(g.nRows).toEqual(3);
61
+ expect(g.colIndices).toEqual([
62
+ [0, 2, 4],
63
+ [1, 3],
64
+ ]);
65
+ expect(g.rowIndices).toEqual([[0, 1], [2, 3], [4]]);
66
+ expect(g.getCellIndex(1, 0)).toEqual(1);
67
+ expect(g.getCellIndex(0, 1)).toEqual(2);
68
+ expect(g.getCellIndex(1, 2)).toBeUndefined();
69
+ expect(g.getCellCoords(3)).toEqual([1, 1]);
70
+ });
71
+ });
@@ -61,6 +61,19 @@ export default class Padding {
61
61
  );
62
62
  }
63
63
 
64
+ /**
65
+ *
66
+ * @param {Padding} padding padding to subtract
67
+ */
68
+ subtract(padding) {
69
+ return new Padding(
70
+ this.top - padding.top,
71
+ this.right - padding.right,
72
+ this.bottom - padding.bottom,
73
+ this.left - padding.left
74
+ );
75
+ }
76
+
64
77
  /**
65
78
  *
66
79
  * @param {PaddingConfig} config
@@ -31,6 +31,8 @@ export default class Rectangle {
31
31
  );
32
32
  }
33
33
 
34
+ static ZERO = Rectangle.create(0, 0, 0, 0);
35
+
34
36
  /**
35
37
  * @param {Prop} prop
36
38
  * @param {number | function():number} value
@@ -279,4 +281,8 @@ export default class Rectangle {
279
281
  y: (y - this.y) / this.height,
280
282
  };
281
283
  }
284
+
285
+ toString() {
286
+ return `Rectangle: x: ${this.x}, y: ${this.y}, width: ${this.width}, height: ${this.height}`;
287
+ }
282
288
  }
@@ -31,7 +31,7 @@ function getPerpendicularChannel(channel) {
31
31
  }
32
32
 
33
33
  /** @type {Record<PositionalChannel, AxisOrient[]>} */
34
- const CHANNEL_ORIENTS = {
34
+ export const CHANNEL_ORIENTS = {
35
35
  x: ["bottom", "top"],
36
36
  y: ["left", "right"],
37
37
  };
@@ -120,6 +120,8 @@ export default class AxisView extends LayerView {
120
120
  this.findChildByName(CHROM_LAYER_NAME).getDynamicDataSource = () =>
121
121
  new DynamicCallbackSource(() => genome.chromosomes);
122
122
  }
123
+
124
+ this.blockEncodingInheritance = true;
123
125
  }
124
126
 
125
127
  getOrient() {
@@ -1,22 +1,10 @@
1
- import { isHConcatSpec, isVConcatSpec } from "./viewFactory";
2
- import ContainerView from "./containerView";
3
- import {
4
- mapToPixelCoords,
5
- getMinimumSize,
6
- parseSizeDef,
7
- FlexDimensions,
8
- } from "../utils/layout/flexLayout";
9
- import Rectangle from "../utils/layout/rectangle";
10
- import Padding from "../utils/layout/padding";
11
- import { peek } from "../utils/arrayUtils";
1
+ import { isConcatSpec, isVConcatSpec } from "./viewFactory";
2
+ import GridView from "./gridView";
12
3
 
13
4
  /**
14
5
  * Creates a vertically or horizontally concatenated layout for children.
15
- *
16
- * @typedef {import("./view").default} View
17
- * @typedef { import("../utils/layout/flexLayout").SizeDef} SizeDef
18
6
  */
19
- export default class ConcatView extends ContainerView {
7
+ export default class ConcatView extends GridView {
20
8
  /**
21
9
  *
22
10
  * @param {import("./viewUtils").AnyConcatSpec} spec
@@ -25,272 +13,33 @@ export default class ConcatView extends ContainerView {
25
13
  * @param {string} name
26
14
  */
27
15
  constructor(spec, context, parent, name) {
28
- super(spec, context, parent, name);
16
+ super(
17
+ spec,
18
+ context,
19
+ parent,
20
+ name,
21
+ isConcatSpec(spec)
22
+ ? spec.columns
23
+ : isVConcatSpec(spec)
24
+ ? 1
25
+ : Infinity
26
+ );
29
27
 
30
28
  this.spec = spec;
29
+ }
31
30
 
32
- if (!("spacing" in this.spec)) {
33
- this.spec.spacing = 10; // TODO: Provide a global configuration (theme!)
34
- }
35
-
36
- /** @type {import("../spec/view").GeometricDimension } */
37
- this.mainDimension = isHConcatSpec(spec) ? "width" : "height";
38
- /** @type {import("../spec/view").GeometricDimension } */
39
- this.secondaryDimension =
40
- this.mainDimension == "width" ? "height" : "width";
41
-
42
- const childSpecs = isHConcatSpec(spec)
43
- ? spec.hconcat
31
+ _createChildren() {
32
+ const spec = this.spec;
33
+ const childSpecs = isConcatSpec(spec)
34
+ ? spec.concat
44
35
  : isVConcatSpec(spec)
45
36
  ? spec.vconcat
46
- : spec.concat;
37
+ : spec.hconcat;
47
38
 
48
- /** @type { View[] } */
49
- this.children = childSpecs.map((childSpec, i) =>
50
- context.createView(childSpec, this, "concat" + i)
39
+ this.setChildren(
40
+ childSpecs.map((childSpec, i) =>
41
+ this.context.createView(childSpec, this, "grid" + i)
42
+ )
51
43
  );
52
44
  }
53
-
54
- /*
55
- _getEffectiveChildPaddings() {
56
- return this.children
57
- .map((view) => view.getEffectivePadding())
58
- .map((padding) =>
59
- this.mainDimension == "height"
60
- ? [padding.left, padding.right]
61
- : [padding.top, padding.bottom]
62
- )
63
- }
64
- */
65
-
66
- getEffectivePadding() {
67
- return this._cache("size/effectivePadding", () => {
68
- const visibleChildren = this.children.filter((view) =>
69
- view.isVisible()
70
- );
71
-
72
- if (!visibleChildren.length) {
73
- return this.getPadding();
74
- }
75
-
76
- // Max paddings along the secondary dimension
77
-
78
- const paddings = visibleChildren
79
- .map((view) => view.getEffectivePadding())
80
- .map((padding) =>
81
- this.mainDimension == "height"
82
- ? [padding.left, padding.right]
83
- : [padding.top, padding.bottom]
84
- );
85
-
86
- const maxPaddings = getMaxEffectivePaddings(paddings);
87
-
88
- const effectiveChildPadding =
89
- this.mainDimension == "height"
90
- ? new Padding(
91
- visibleChildren[0].getEffectivePadding().top,
92
- maxPaddings[1],
93
- peek(visibleChildren).getEffectivePadding().bottom,
94
- maxPaddings[0]
95
- )
96
- : new Padding(
97
- maxPaddings[0],
98
- visibleChildren[0].getEffectivePadding().left,
99
- maxPaddings[1],
100
- peek(visibleChildren).getEffectivePadding().right
101
- );
102
-
103
- return this.getPadding().add(effectiveChildPadding);
104
- });
105
- }
106
-
107
- getSize() {
108
- return this._cache("size", () => {
109
- /** @type {SizeDef} */
110
- let mainSizeDef;
111
- if (this.spec[this.mainDimension]) {
112
- mainSizeDef = parseSizeDef(this.spec[this.mainDimension]);
113
- } else {
114
- const childMainSizeDefs = this.children
115
- .filter((view) => view.isVisible())
116
- .map((view) => view.getSize()[this.mainDimension]);
117
-
118
- mainSizeDef = {
119
- // Grows are summed to support sensible nesting of concatViews
120
- grow: childMainSizeDefs
121
- .map((sizeDef) => +sizeDef.grow)
122
- .reduce((a, b) => a + b, 0),
123
- px: getMinimumSize(childMainSizeDefs, {
124
- spacing: this.spec.spacing,
125
- }),
126
- };
127
- }
128
-
129
- const secondarySizeDef = (this.spec[this.secondaryDimension] &&
130
- parseSizeDef(this.spec[this.secondaryDimension])) || {
131
- grow: 1,
132
- };
133
-
134
- return (
135
- this.mainDimension == "height"
136
- ? new FlexDimensions(secondarySizeDef, mainSizeDef)
137
- : new FlexDimensions(mainSizeDef, secondarySizeDef)
138
- ).addPadding(this.getPadding());
139
- });
140
- }
141
-
142
- /**
143
- * @param {import("./renderingContext/viewRenderingContext").default} context
144
- * @param {import("../utils/layout/rectangle").default} coords
145
- * @param {import("./view").RenderingOptions} [options]
146
- */
147
- render(context, coords, options = {}) {
148
- if (!this.isVisible()) {
149
- return;
150
- }
151
-
152
- coords = coords.shrink(this.getPadding());
153
- context.pushView(this, coords);
154
-
155
- // This method produces piles of garbage when used with sample faceting.
156
- // TODO: Figure out something. Perhaps the rectangles could be cached because
157
- // they are identical for each sample facet.
158
-
159
- const visibleChildren = this.children.filter((view) =>
160
- view.isVisible()
161
- );
162
- const childSizeDefs = visibleChildren.map(
163
- (view) => view.getSize()[this.mainDimension]
164
- );
165
-
166
- const mappedCoords = mapToPixelCoords(
167
- childSizeDefs,
168
- coords[this.mainDimension],
169
- {
170
- spacing: this.spec.spacing,
171
- devicePixelRatio: this.context.glHelper.dpr,
172
- }
173
- );
174
-
175
- // Align the views.
176
- const paddings = visibleChildren
177
- .map((view) => view.getEffectivePadding())
178
- .map((padding) =>
179
- this.mainDimension == "height"
180
- ? [padding.left, padding.right]
181
- : [padding.top, padding.bottom]
182
- );
183
-
184
- const maxPaddings = getMaxEffectivePaddings(paddings);
185
-
186
- for (let i = 0; i < visibleChildren.length; i++) {
187
- const view = visibleChildren[i];
188
- const flexCoords = mappedCoords[i];
189
-
190
- const pa = maxPaddings[0] - paddings[i][0];
191
- const pb = maxPaddings[1] - paddings[i][1];
192
-
193
- const secondarySize = coords[this.secondaryDimension] - pa - pb;
194
-
195
- const childCoords =
196
- this.mainDimension == "height"
197
- ? new Rectangle(
198
- () => coords.x + pa,
199
- () => coords.y + flexCoords.location,
200
- () => secondarySize,
201
- () => flexCoords.size
202
- )
203
- : new Rectangle(
204
- () => coords.x + flexCoords.location,
205
- () => coords.y + pa,
206
- () => flexCoords.size,
207
- () => secondarySize
208
- );
209
-
210
- view.render(context, childCoords, options);
211
- }
212
-
213
- context.popView(this);
214
- }
215
-
216
- /**
217
- * @returns {IterableIterator<View>}
218
- */
219
- *[Symbol.iterator]() {
220
- for (const child of this.children) {
221
- yield child;
222
- }
223
- }
224
-
225
- /**
226
- * @param {import("./view").default} child
227
- * @param {import("./view").default} replacement
228
- */
229
- replaceChild(child, replacement) {
230
- const i = this.children.indexOf(child);
231
- if (i >= 0) {
232
- this.children[i] = replacement;
233
- } else {
234
- throw new Error("Not my child view!");
235
- }
236
- }
237
-
238
- /**
239
- * Adds a child using a spec. Does NOT perform any initializations.
240
- * Returns the newly added view instance.
241
- *
242
- * @param {import("./viewUtils").ViewSpec} viewSpec
243
- */
244
- addChildBySpec(viewSpec) {
245
- // TODO: Move to containerView
246
-
247
- // TODO: More robust solution. Will break in future when views can be removed
248
- const i = this.children.length;
249
-
250
- const view = this.context.createView(viewSpec, this, "concat" + i);
251
- this.children.push(view);
252
-
253
- return view;
254
- }
255
-
256
- /**
257
- * Adds a child. Does NOT perform any initializations.
258
- * Returns the newly added view instance.
259
- *
260
- * @param {View} view
261
- */
262
- addChild(view) {
263
- // TODO: Move to containerView
264
-
265
- // TODO: More robust solution. Will break in future when views can be removed
266
- const i = this.children.length;
267
-
268
- if (!view.name) {
269
- view.name = "concat" + i;
270
- }
271
- view.parent = this;
272
- this.children.push(view);
273
-
274
- return view;
275
- }
276
-
277
- /**
278
- * @param {string} channel
279
- * @param {import("./containerView").ResolutionTarget} resolutionType
280
- * @returns {import("../spec/view").ResolutionBehavior}
281
- */
282
- getDefaultResolution(channel, resolutionType) {
283
- // TODO: Default to shared when working with genomic coordinates
284
- return "independent";
285
- }
286
- }
287
-
288
- /**
289
- *
290
- * @param {number[][]} paddings
291
- */
292
- function getMaxEffectivePaddings(paddings) {
293
- return [0, 1].map((i) =>
294
- paddings.map((p) => p[i]).reduce((a, c) => Math.max(a, c), 0)
295
- );
296
45
  }