@genome-spy/core 0.20.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.
package/src/marks/rule.js CHANGED
@@ -139,6 +139,20 @@ export default class RuleMark extends Mark {
139
139
  this.createAndLinkShaders(VERTEX_SHADER, FRAGMENT_SHADER);
140
140
  }
141
141
 
142
+ finalizeGraphicsInitialization() {
143
+ super.finalizeGraphicsInitialization();
144
+
145
+ this.gl.useProgram(this.programInfo.program);
146
+
147
+ const props = this.properties;
148
+
149
+ setUniforms(this.programInfo, {
150
+ uMinLength: props.minLength,
151
+ uDashTextureSize: this.dashTextureSize,
152
+ uStrokeCap: ["butt", "square", "round"].indexOf(props.strokeCap),
153
+ });
154
+ }
155
+
142
156
  updateGraphicsData() {
143
157
  const collector = this.unitView.getCollector();
144
158
  const itemCount = collector.getItemCount();
@@ -147,13 +161,12 @@ export default class RuleMark extends Mark {
147
161
  encoders: this.encoders,
148
162
  attributes: this.getAttributes(),
149
163
  numItems: Math.max(itemCount, this.properties.minBufferSize || 0),
150
- buildXIndex: this.properties.buildIndex,
151
164
  });
152
165
 
153
166
  builder.addBatches(collector.facetBatches);
154
167
 
155
168
  const vertexData = builder.toArrays();
156
- this.rangeMap = vertexData.rangeMap;
169
+ this.rangeMap.migrateEntries(vertexData.rangeMap);
157
170
 
158
171
  this.updateBufferInfo(vertexData);
159
172
  }
@@ -162,28 +175,26 @@ export default class RuleMark extends Mark {
162
175
  * @param {import("../view/rendering").GlobalRenderingOptions} options
163
176
  */
164
177
  prepareRender(options) {
165
- super.prepareRender(options);
166
-
167
- setUniforms(this.programInfo, {
168
- uMinLength: this.properties.minLength,
169
- uDashTextureSize: this.dashTextureSize,
170
- uStrokeCap: ["butt", "square", "round"].indexOf(
171
- this.properties.strokeCap
172
- ),
173
- });
178
+ const ops = super.prepareRender(options);
174
179
 
175
180
  if (this.dashTexture) {
176
- setUniforms(this.programInfo, {
177
- uDashTexture: this.dashTexture,
178
- uStrokeDashOffset: this.properties.strokeDashOffset,
179
- });
181
+ ops.push(() =>
182
+ setUniforms(this.programInfo, {
183
+ uDashTexture: this.dashTexture,
184
+ uStrokeDashOffset: this.properties.strokeDashOffset,
185
+ })
186
+ );
180
187
  }
181
188
 
182
- setBuffersAndAttributes(
183
- this.gl,
184
- this.programInfo,
185
- this.vertexArrayInfo
189
+ ops.push(() =>
190
+ setBuffersAndAttributes(
191
+ this.gl,
192
+ this.programInfo,
193
+ this.vertexArrayInfo
194
+ )
186
195
  );
196
+
197
+ return ops;
187
198
  }
188
199
 
189
200
  /**
@@ -201,8 +212,7 @@ export default class RuleMark extends Mark {
201
212
  count,
202
213
  offset
203
214
  ),
204
- options,
205
- () => this.rangeMap
215
+ options
206
216
  );
207
217
  }
208
218
  }
package/src/marks/text.js CHANGED
@@ -214,13 +214,12 @@ export default class TextMark extends Mark {
214
214
  charCount,
215
215
  this.properties.minBufferSize || 0
216
216
  ),
217
- buildXIndex: this.properties.buildIndex,
218
217
  });
219
218
 
220
219
  builder.addBatches(collector.facetBatches);
221
220
 
222
221
  const vertexData = builder.toArrays();
223
- this.rangeMap = vertexData.rangeMap;
222
+ this.rangeMap.migrateEntries(vertexData.rangeMap);
224
223
 
225
224
  this.updateBufferInfo(vertexData);
226
225
  }
@@ -229,7 +228,7 @@ export default class TextMark extends Mark {
229
228
  * @param {import("../view/rendering").GlobalRenderingOptions} options
230
229
  */
231
230
  prepareRender(options) {
232
- super.prepareRender(options);
231
+ const ops = super.prepareRender(options);
233
232
 
234
233
  let q = 0.35; // TODO: Ensure that this makes sense. Now chosen by trial & error
235
234
  if (this.properties.logoLetters) {
@@ -239,17 +238,23 @@ export default class TextMark extends Mark {
239
238
  q /= 2;
240
239
  }
241
240
 
242
- setUniforms(this.programInfo, {
243
- uTexture: this.font.texture,
244
- uSdfNumerator:
245
- this.font.metrics.common.base / (this.glHelper.dpr / q),
246
- });
241
+ ops.push(() =>
242
+ setUniforms(this.programInfo, {
243
+ uTexture: this.font.texture,
244
+ uSdfNumerator:
245
+ this.font.metrics.common.base / (this.glHelper.dpr / q),
246
+ })
247
+ );
247
248
 
248
- setBuffersAndAttributes(
249
- this.gl,
250
- this.programInfo,
251
- this.vertexArrayInfo
249
+ ops.push(() =>
250
+ setBuffersAndAttributes(
251
+ this.gl,
252
+ this.programInfo,
253
+ this.vertexArrayInfo
254
+ )
252
255
  );
256
+
257
+ return ops;
253
258
  }
254
259
 
255
260
  /**
@@ -267,8 +272,7 @@ export default class TextMark extends Mark {
267
272
  count,
268
273
  offset
269
274
  ),
270
- options,
271
- () => this.rangeMap
275
+ options
272
276
  );
273
277
  }
274
278
  }
@@ -407,9 +407,6 @@ export interface MarkConfig
407
407
  */
408
408
  strokeWidth?: number;
409
409
 
410
- // TODO: get rid of this
411
- dynamicData?: boolean;
412
-
413
410
  /**
414
411
  * Minimum size for WebGL buffers (number of data items).
415
412
  * Allows for using `bufferSubData()` to update graphics.
@@ -0,0 +1,147 @@
1
+ import clamp from "./clamp";
2
+
3
+ const MAX_INTEGER = 2 ** 31 - 1;
4
+ const MIN_INTEGER = -(2 ** 31);
5
+
6
+ /**
7
+ * @callback Lookup
8
+ * @param {number} start
9
+ * @param {number} end
10
+ * @param {[number, number]} [arr] Store the result into this array (and return it)
11
+ * @returns {[number, number]}
12
+ */
13
+
14
+ /**
15
+ * A binned index for (overlapping) ranges that are sorted by their start position.
16
+ *
17
+ * @param {number} size Number of bins
18
+ * @param {[number, number]} domain
19
+ * @param {(datum: T) => number} accessor
20
+ * @param {(datum: T) => number} [accessor2]
21
+ * @template T
22
+ */
23
+ export function createBinningRangeIndexer(
24
+ size,
25
+ domain,
26
+ accessor,
27
+ accessor2 = accessor
28
+ ) {
29
+ const startIndices = new Int32Array(size);
30
+ startIndices.fill(MAX_INTEGER);
31
+
32
+ let lastIndex = MIN_INTEGER;
33
+ let unordered = false;
34
+
35
+ const endIndices = new Int32Array(size);
36
+
37
+ const start = domain[0];
38
+ const domainLength = domain[1] - domain[0];
39
+ const divisor = domainLength / size;
40
+
41
+ /** @param {number} pos */
42
+ const getBin = (pos) =>
43
+ clamp(Math.floor((pos - start) / divisor), 0, size - 1);
44
+
45
+ /**
46
+ *
47
+ * @param {T} datum
48
+ * @param {number} startIndex
49
+ * @param {number} endIndex
50
+ */
51
+ function binningIndexer(datum, startIndex, endIndex) {
52
+ if (startIndex > lastIndex) {
53
+ lastIndex = startIndex;
54
+ } else if (!unordered) {
55
+ unordered = true;
56
+ // TODO: Contextual info like view path
57
+ console.debug(
58
+ "Items are not ordered properly. Disabling binned index."
59
+ );
60
+ }
61
+
62
+ const value = accessor(datum);
63
+ const bin = getBin(value);
64
+
65
+ if (startIndices[bin] > startIndex) {
66
+ startIndices[bin] = startIndex;
67
+ }
68
+
69
+ if (endIndices[bin] < endIndex) {
70
+ endIndices[bin] = endIndex;
71
+ }
72
+ }
73
+
74
+ /**
75
+ *
76
+ * @param {T} datum
77
+ * @param {number} startIndex
78
+ * @param {number} endIndex
79
+ */
80
+ function binningRangeIndexer(datum, startIndex, endIndex) {
81
+ if (startIndex > lastIndex) {
82
+ lastIndex = startIndex;
83
+ } else if (!unordered) {
84
+ unordered = true;
85
+ // TODO: Contextual info like view path
86
+ console.debug(
87
+ "Items are not ordered properly. Disabling binned index."
88
+ );
89
+ }
90
+
91
+ const start = accessor(datum);
92
+ const end = accessor2(datum);
93
+ const startBin = getBin(start);
94
+ const endBin = getBin(end);
95
+
96
+ // TODO: This loop could probably be done as a more efficient post processing
97
+ // step.
98
+ for (let bin = startBin; bin <= endBin; bin++) {
99
+ if (startIndices[bin] > startIndex) {
100
+ startIndices[bin] = startIndex;
101
+ }
102
+
103
+ if (endIndices[bin] < endIndex) {
104
+ endIndices[bin] = endIndex;
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * @type {Lookup}
111
+ */
112
+ const lookup = (start, end, arr = [0, 0]) => {
113
+ arr[0] = startIndices[getBin(start)];
114
+ arr[1] = Math.max(endIndices[getBin(end)], arr[0]);
115
+ return arr;
116
+ };
117
+
118
+ const getIndex = () => {
119
+ for (let i = 1; i < endIndices.length; i++) {
120
+ if (endIndices[i] < endIndices[i - 1]) {
121
+ endIndices[i] = endIndices[i - 1];
122
+ }
123
+ }
124
+
125
+ let tail = true;
126
+
127
+ for (let i = startIndices.length - 1; i > 0; i--) {
128
+ if (tail && startIndices[i] == MAX_INTEGER) {
129
+ startIndices[i] = endIndices[i];
130
+ tail = false;
131
+ } else if (startIndices[i - 1] > startIndices[i]) {
132
+ startIndices[i - 1] = startIndices[i];
133
+ }
134
+ }
135
+
136
+ return lookup;
137
+ };
138
+
139
+ binningIndexer.getIndex = getIndex;
140
+ binningRangeIndexer.getIndex = getIndex;
141
+
142
+ if (unordered) {
143
+ return undefined;
144
+ } else {
145
+ return accessor == accessor2 ? binningIndexer : binningRangeIndexer;
146
+ }
147
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { createBinningRangeIndexer } from "./binnedIndex.js";
3
+
4
+ describe("Binning Indexer", () => {
5
+ test("Points are binned correctly", () => {
6
+ const items = [0, 1, 4, 10, 35, 36, 80];
7
+ const indexer = createBinningRangeIndexer(10, [0, 100], (x) => x);
8
+
9
+ items.forEach((i) => indexer(i, i, i + 1));
10
+
11
+ const index = indexer.getIndex();
12
+
13
+ expect(index(0, 1)).toEqual([0, 5]);
14
+ expect(index(1, 2)).toEqual([0, 5]);
15
+ expect(index(1, 15)).toEqual([0, 11]);
16
+ expect(index(10, 15)).toEqual([10, 11]);
17
+ expect(index(11, 38)).toEqual([10, 37]);
18
+ expect(index(11, 45)).toEqual([10, 37]);
19
+ expect(index(40, 85)).toEqual([80, 81]);
20
+ expect(index(90, 100)).toEqual([81, 81]);
21
+ });
22
+
23
+ test("Non-overlapping ranges are binned correctly", () => {
24
+ const items = [
25
+ [0, 5],
26
+ [25, 50],
27
+ [50, 55],
28
+ ];
29
+ const indexer = createBinningRangeIndexer(
30
+ 10,
31
+ [0, 100],
32
+ (x) => x[0],
33
+ (x) => x[1]
34
+ );
35
+
36
+ items.forEach((x) => indexer(x, x[0], x[1]));
37
+
38
+ const index = indexer.getIndex();
39
+
40
+ // TODO: More tests. Doesn't work 100%
41
+
42
+ expect(index(0, 1)).toEqual([0, 5]);
43
+ expect(index(3, 40)).toEqual([0, 50]);
44
+ expect(index(6, 40)).toEqual([0, 50]);
45
+ // fails: expect(index(50, 57)).toEqual([50, 55]);
46
+ });
47
+
48
+ test("Overlapping ranges are binned correctly", () => {
49
+ const items = [
50
+ [10, 30],
51
+ [25, 50],
52
+ ];
53
+ const indexer = createBinningRangeIndexer(
54
+ 10,
55
+ [0, 100],
56
+ (x) => x[0],
57
+ (x) => x[1]
58
+ );
59
+
60
+ items.forEach((x) => indexer(x, x[0], x[1]));
61
+
62
+ const index = indexer.getIndex();
63
+
64
+ // TODO: More tests. Doesn't work 100%
65
+
66
+ expect(index(0, 5)).toEqual([10, 10]);
67
+ expect(index(0, 15)).toEqual([10, 30]);
68
+ expect(index(27, 40)).toEqual([10, 50]);
69
+ expect(index(40, 50)).toEqual([25, 50]);
70
+ expect(index(40, 80)).toEqual([25, 50]);
71
+ expect(index(10, 29)).toEqual([10, 50]);
72
+ });
73
+ });
@@ -429,7 +429,6 @@ function createAxis(axisProps) {
429
429
  size: ap.labelFontSize,
430
430
  color: ap.labelColor,
431
431
  minBufferSize: 1500, // to prevent GPU buffer reallocation when zooming
432
- dynamicData: true,
433
432
  },
434
433
  encoding: {
435
434
  [main]: { field: "value", type: "quantitative" },
@@ -450,7 +449,6 @@ function createAxis(axisProps) {
450
449
  color: ap.tickColor,
451
450
  size: ap.tickWidth,
452
451
  minBufferSize: 300,
453
- dynamicData: true,
454
452
  },
455
453
  encoding: {
456
454
  [secondary]: { value: anchor },
@@ -576,7 +574,6 @@ export function createGenomeAxis(axisProps) {
576
574
  anchor - (ap.chromTickSize / ap.extent) * (anchor ? 1 : -1),
577
575
  color: axisProps.chromTickColor,
578
576
  size: ap.chromTickWidth,
579
- dynamicData: true,
580
577
  },
581
578
  });
582
579
 
@@ -649,7 +646,6 @@ export function createGenomeAxis(axisProps) {
649
646
  align: axisProps.chromLabelAlign,
650
647
  baseline: "alphabetic",
651
648
  clip: false,
652
- dynamicData: true,
653
649
  ...chromLabelMarkProps,
654
650
  },
655
651
  encoding: {
@@ -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
  }
@@ -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
- }