@genome-spy/core 0.23.0 → 0.24.2

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/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "contributors": [],
9
9
  "license": "BSD-2-Clause",
10
- "version": "0.23.0",
10
+ "version": "0.24.2",
11
11
  "main": "dist/index.js",
12
12
  "module": "src/index.js",
13
13
  "exports": {
@@ -53,5 +53,5 @@
53
53
  "vega-scale": "^7.1.1",
54
54
  "vega-util": "^1.16.0"
55
55
  },
56
- "gitHead": "7420b51a4344e39985db59396ddf873eb3002143"
56
+ "gitHead": "587ef91161457b17a039f82111d15df339e1e9f5"
57
57
  }
@@ -131,13 +131,18 @@ export class GeometryBuilder {
131
131
  * @param {number} [hi]
132
132
  */
133
133
  prepareXIndexer(data, lo = 0, hi = lo + data.length) {
134
- if (!data.length || hi - lo < 0) {
134
+ const disable = () => {
135
135
  /**
136
136
  * @param {import("../data/flowNode").Datum} datum
137
137
  */
138
138
  this.addToXIndex = (datum) => {
139
139
  // nop
140
140
  };
141
+ this.xIndexer = undefined;
142
+ };
143
+
144
+ if (!data.length || hi - lo < 0) {
145
+ disable();
141
146
  return;
142
147
  }
143
148
 
@@ -152,31 +157,33 @@ export class GeometryBuilder {
152
157
  const xa = xe.accessor;
153
158
  const x2a = x2e ? x2e.accessor : xa;
154
159
 
155
- this.xIndexer = createBinningRangeIndexer(
156
- 50,
157
- [xa(data[lo]), x2a(data[hi - 1])],
158
- xa,
159
- x2a
160
- );
161
-
162
- let lastVertexCount = this.variableBuilder.vertexCount;
163
-
164
- /**
165
- * @param {any} datum
166
- */
167
- this.addToXIndex = (datum) => {
168
- let currentVertexCount = this.variableBuilder.vertexCount;
169
- this.xIndexer(datum, lastVertexCount, currentVertexCount);
170
- lastVertexCount = currentVertexCount;
171
- };
160
+ /** @type {[number, number]} */
161
+ const dataDomain = [xa(data[lo]), x2a(data[hi - 1])];
162
+
163
+ // No indexer for point domains that have zero extent
164
+ if (dataDomain[1] > dataDomain[0]) {
165
+ this.xIndexer = createBinningRangeIndexer(
166
+ 50,
167
+ dataDomain,
168
+ xa,
169
+ x2a
170
+ );
171
+
172
+ let lastVertexCount = this.variableBuilder.vertexCount;
173
+
174
+ /**
175
+ * @param {any} datum
176
+ */
177
+ this.addToXIndex = (datum) => {
178
+ let currentVertexCount = this.variableBuilder.vertexCount;
179
+ this.xIndexer(datum, lastVertexCount, currentVertexCount);
180
+ lastVertexCount = currentVertexCount;
181
+ };
182
+ } else {
183
+ disable();
184
+ }
172
185
  } else {
173
- this.xIndexer = undefined;
174
- /**
175
- * @param {import("../data/flowNode").Datum} datum
176
- */
177
- this.addToXIndex = (datum) => {
178
- // nop
179
- };
186
+ disable();
180
187
  }
181
188
  }
182
189
 
package/src/marks/link.js CHANGED
@@ -140,7 +140,11 @@ export default class LinkMark extends Mark {
140
140
  this.bufferInfo.attribs
141
141
  )) {
142
142
  const [attribute, attribInfo] = attribInfoObject;
143
- if (attribInfo.buffer && this.arrays[attribute].numComponents) {
143
+ if (
144
+ attribInfo.buffer &&
145
+ attribInfo.numComponents &&
146
+ attribInfo.divisor
147
+ ) {
144
148
  attribInfo.offset =
145
149
  offset * this.arrays[attribute].numComponents * 4; // gl.FLOAT in bytes
146
150
  }
@@ -0,0 +1 @@
1
+ {"version":"0.23.1","results":[["/genome/genome.test.js",{"duration":17,"failed":false}],["/scale/scale.test.js",{"duration":27,"failed":false}],["/utils/layout/flexLayout.test.js",{"duration":41,"failed":false}],["/data/flowOptimizer.test.js",{"duration":12,"failed":false}],["/utils/layout/rectangle.test.js",{"duration":10,"failed":false}],["/utils/domainArray.test.js",{"duration":11,"failed":false}],["/data/transforms/regexFold.test.js",{"duration":9,"failed":false}],["/view/scaleResolution.test.js",{"duration":169,"failed":false}],["/view/view.test.js",{"duration":161,"failed":false}],["/view/axisResolution.test.js",{"duration":22,"failed":false}],["/view/flowBuilder.test.js",{"duration":11,"failed":false}],["/encoder/encoder.test.js",{"duration":15,"failed":false}],["/data/transforms/stack.test.js",{"duration":11,"failed":false}],["/data/transforms/identifier.test.js",{"duration":84,"failed":false}],["/data/transforms/coverage.test.js",{"duration":10,"failed":false}],["/data/transforms/flattenDelimited.test.js",{"duration":16,"failed":false}],["/genome/scaleIndex.test.js",{"duration":11,"failed":false}],["/utils/layout/grid.test.js",{"duration":7,"failed":false}],["/utils/topK.test.js",{"duration":95,"failed":false}],["/utils/binnedIndex.test.js",{"duration":6,"failed":false}],["/data/collector.test.js",{"duration":11,"failed":false}],["/data/flow.test.js",{"duration":11,"failed":false}],["/utils/propertyCacher.test.js",{"duration":8,"failed":false}],["/data/transforms/regexExtract.test.js",{"duration":10,"failed":false}],["/data/transforms/pileup.test.js",{"duration":7,"failed":false}],["/utils/indexer.test.js",{"duration":6,"failed":false}],["/encoder/accessor.test.js",{"duration":10,"failed":false}],["/data/sources/sequenceSource.test.js",{"duration":8,"failed":false}],["/data/sources/inlineSource.test.js",{"duration":10,"failed":false}],["/utils/mergeObjects.test.js",{"duration":13,"failed":false}],["/data/flowNode.test.js",{"duration":6,"failed":false}],["/scale/ticks.test.js",{"duration":10,"failed":false}],["/data/formats/fasta.test.js",{"duration":10,"failed":false}],["/data/transforms/flattenSequence.test.js",{"duration":10,"failed":false}],["/utils/addBaseUrl.test.js",{"duration":5,"failed":false}],["/data/transforms/project.test.js",{"duration":10,"failed":false}],["/utils/iterateNestedMaps.test.js",{"duration":5,"failed":false}],["/utils/reservationMap.test.js",{"duration":6,"failed":false}],["/utils/kWayMerge.test.js",{"duration":5,"failed":false}],["/data/transforms/sample.test.js",{"duration":326,"failed":false}],["/utils/propertyCoalescer.test.js",{"duration":5,"failed":false}],["/utils/coalesce.test.js",{"duration":4,"failed":false}],["/data/transforms/filter.test.js",{"duration":6,"failed":false}],["/utils/cloner.test.js",{"duration":6,"failed":false}],["/utils/variableTools.test.js",{"duration":6,"failed":false}],["/view/viewFactory.test.js",{"duration":4,"failed":false}],["/data/transforms/formula.test.js",{"duration":8,"failed":false}],["/utils/concatIterables.test.js",{"duration":18,"failed":false}],["/utils/numberExtractor.test.js",{"duration":30,"failed":false}],["/data/transforms/clone.test.js",{"duration":8,"failed":false}],["/genome/scaleLocus.test.js",{"duration":0,"failed":false}],["/data/dataFlow.test.js",{"duration":0,"failed":false}]]}
@@ -13,11 +13,12 @@ const MIN_INTEGER = -(2 ** 31);
13
13
 
14
14
  /**
15
15
  * A binned index for (overlapping) ranges that are sorted by their start position.
16
+ * Each indexed range is associated with respective vertex indices.
16
17
  *
17
18
  * @param {number} size Number of bins
18
- * @param {[number, number]} domain
19
- * @param {(datum: T) => number} accessor
20
- * @param {(datum: T) => number} [accessor2]
19
+ * @param {[number, number]} domain Domain of positions
20
+ * @param {(datum: T) => number} accessor Accessor for range's start position
21
+ * @param {(datum: T) => number} [accessor2] Accessor for range's end position
21
22
  * @template T
22
23
  */
23
24
  export function createBinningRangeIndexer(
@@ -38,19 +39,32 @@ export function createBinningRangeIndexer(
38
39
  const domainLength = domain[1] - domain[0];
39
40
  const divisor = domainLength / size;
40
41
 
41
- /** @param {number} pos */
42
- const getBin = (pos) =>
43
- clamp(Math.floor((pos - start) / divisor), 0, size - 1);
42
+ /**
43
+ * @param {number} pos
44
+ * @param {boolean} end
45
+ */
46
+ const getBin = (pos, end) => {
47
+ const unfloored = (pos - start) / divisor;
48
+ const floored = Math.floor(unfloored);
49
+
50
+ // Special handling for the end coordinate because we are using half-open ranges.
51
+ return clamp(
52
+ end && floored == unfloored ? floored - 1 : floored,
53
+ 0,
54
+ size - 1
55
+ );
56
+ };
44
57
 
45
58
  /**
59
+ * Indexer for point items. Those have just a single coordinate.
46
60
  *
47
61
  * @param {T} datum
48
- * @param {number} startIndex
49
- * @param {number} endIndex
62
+ * @param {number} startVertexIndex
63
+ * @param {number} endVertexIndex
50
64
  */
51
- function binningIndexer(datum, startIndex, endIndex) {
52
- if (startIndex > lastIndex) {
53
- lastIndex = startIndex;
65
+ function binningIndexer(datum, startVertexIndex, endVertexIndex) {
66
+ if (startVertexIndex > lastIndex) {
67
+ lastIndex = startVertexIndex;
54
68
  } else if (!unordered) {
55
69
  unordered = true;
56
70
  // TODO: Contextual info like view path
@@ -60,26 +74,27 @@ export function createBinningRangeIndexer(
60
74
  }
61
75
 
62
76
  const value = accessor(datum);
63
- const bin = getBin(value);
77
+ const bin = getBin(value, false);
64
78
 
65
- if (startIndices[bin] > startIndex) {
66
- startIndices[bin] = startIndex;
79
+ if (startIndices[bin] > startVertexIndex) {
80
+ startIndices[bin] = startVertexIndex;
67
81
  }
68
82
 
69
- if (endIndices[bin] < endIndex) {
70
- endIndices[bin] = endIndex;
83
+ if (endIndices[bin] < endVertexIndex) {
84
+ endIndices[bin] = endVertexIndex;
71
85
  }
72
86
  }
73
87
 
74
88
  /**
89
+ * Indexer for ranged items. Those have both start and end coordinates.
75
90
  *
76
91
  * @param {T} datum
77
- * @param {number} startIndex
78
- * @param {number} endIndex
92
+ * @param {number} startVertexIndex
93
+ * @param {number} endVertexIndex
79
94
  */
80
- function binningRangeIndexer(datum, startIndex, endIndex) {
81
- if (startIndex > lastIndex) {
82
- lastIndex = startIndex;
95
+ function binningRangeIndexer(datum, startVertexIndex, endVertexIndex) {
96
+ if (startVertexIndex > lastIndex) {
97
+ lastIndex = startVertexIndex;
83
98
  } else if (!unordered) {
84
99
  unordered = true;
85
100
  // TODO: Contextual info like view path
@@ -90,18 +105,18 @@ export function createBinningRangeIndexer(
90
105
 
91
106
  const start = accessor(datum);
92
107
  const end = accessor2(datum);
93
- const startBin = getBin(start);
94
- const endBin = getBin(end);
108
+ const startBin = getBin(start, false);
109
+ const endBin = getBin(end, true);
95
110
 
96
111
  // TODO: This loop could probably be done as a more efficient post processing
97
112
  // step.
98
113
  for (let bin = startBin; bin <= endBin; bin++) {
99
- if (startIndices[bin] > startIndex) {
100
- startIndices[bin] = startIndex;
114
+ if (startIndices[bin] > startVertexIndex) {
115
+ startIndices[bin] = startVertexIndex;
101
116
  }
102
117
 
103
- if (endIndices[bin] < endIndex) {
104
- endIndices[bin] = endIndex;
118
+ if (endIndices[bin] < endVertexIndex) {
119
+ endIndices[bin] = endVertexIndex;
105
120
  }
106
121
  }
107
122
  }
@@ -110,8 +125,13 @@ export function createBinningRangeIndexer(
110
125
  * @type {Lookup}
111
126
  */
112
127
  const lookup = (start, end, arr = [0, 0]) => {
113
- arr[0] = startIndices[getBin(start)];
114
- arr[1] = Math.max(endIndices[getBin(end)], arr[0]);
128
+ const startBin = getBin(start, false);
129
+ const endBin = getBin(end, true);
130
+ const startIndex = startIndices[startBin];
131
+ const endIndex = Math.max(endIndices[endBin], startIndex);
132
+
133
+ arr[0] = startIndex;
134
+ arr[1] = endIndex;
115
135
  return arr;
116
136
  };
117
137
 
@@ -2,29 +2,60 @@ import { describe, expect, test } from "vitest";
2
2
  import { createBinningRangeIndexer } from "./binnedIndex.js";
3
3
 
4
4
  describe("Binning Indexer", () => {
5
- test("Points are binned correctly", () => {
6
- const items = [0, 1, 4, 10, 35, 36, 80];
5
+ test("Single point is binned correctly", () => {
6
+ const items = [25];
7
7
  const indexer = createBinningRangeIndexer(10, [0, 100], (x) => x);
8
8
 
9
- items.forEach((i) => indexer(i, i, i + 1));
9
+ // Each item uses two vertices
10
+ items.forEach((x, i) => indexer(x, i * 2, (i + 1) * 2));
10
11
 
11
12
  const index = indexer.getIndex();
12
13
 
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]);
14
+ expect(index(1, 4)).toEqual([0, 0]);
15
+ expect(index(23, 27)).toEqual([0, 2]);
16
+ expect(index(13, 37)).toEqual([0, 2]);
17
+ // TODO: MAX_INT could be replaced with the actual maximum vertex number
18
+ expect(index(40, 42)).toEqual([2147483647, 2147483647]);
19
+ });
20
+
21
+ test("Multiple points are binned correctly", () => {
22
+ const items = [0, 1, 4, 10, 35, 35, 36, 80];
23
+ const indexer = createBinningRangeIndexer(10, [0, 100], (x) => x);
24
+
25
+ // Each item uses two vertices
26
+ items.forEach((x, i) => indexer(x, i * 2, (i + 1) * 2));
27
+
28
+ const index = indexer.getIndex();
29
+
30
+ expect(index(0, 0)).toEqual([0, 6]);
31
+ expect(index(0, 1)).toEqual([0, 6]);
32
+ expect(index(1, 2)).toEqual([0, 6]);
33
+ expect(index(1, 15)).toEqual([0, 8]);
34
+ expect(index(3, 6)).toEqual([0, 6]);
35
+ expect(index(10, 15)).toEqual([6, 8]);
36
+ expect(index(11, 38)).toEqual([6, 14]);
37
+ expect(index(11, 45)).toEqual([6, 14]);
38
+ expect(index(34, 36)).toEqual([8, 14]);
39
+ expect(index(35.5, 36.5)).toEqual([8, 14]);
40
+ expect(index(40, 50)).toEqual([14, 14]);
41
+ expect(index(40, 85)).toEqual([14, 16]);
42
+ expect(index(90, 100)).toEqual([16, 16]);
43
+
44
+ expect(index(0, 100)).toEqual([0, 16]);
45
+ expect(index(-1, 100)).toEqual([0, 16]);
46
+ expect(index(0, 101)).toEqual([0, 16]);
21
47
  });
22
48
 
23
49
  test("Non-overlapping ranges are binned correctly", () => {
24
50
  const items = [
25
51
  [0, 5],
26
- [25, 50],
52
+ [25, 48],
27
53
  [50, 55],
54
+ [64, 67],
55
+ [72, 75],
56
+ [75, 78],
57
+ [86, 90],
58
+ [90, 93],
28
59
  ];
29
60
  const indexer = createBinningRangeIndexer(
30
61
  10,
@@ -33,41 +64,92 @@ describe("Binning Indexer", () => {
33
64
  (x) => x[1]
34
65
  );
35
66
 
36
- items.forEach((x) => indexer(x, x[0], x[1]));
67
+ // Each item uses two vertices
68
+ items.forEach((x, i) => indexer(x, i * 2, (i + 1) * 2));
69
+
70
+ const index = indexer.getIndex();
71
+
72
+ expect(index(0, 1)).toEqual([0, 2]);
73
+ expect(index(3, 40)).toEqual([0, 4]);
74
+ expect(index(6, 40)).toEqual([0, 4]);
75
+ expect(index(15, 30)).toEqual([2, 4]);
76
+ expect(index(50, 57)).toEqual([4, 6]);
77
+ expect(index(62, 69)).toEqual([6, 8]);
78
+ expect(index(69, 71)).toEqual([6, 12]);
79
+ expect(index(69, 79)).toEqual([6, 12]);
80
+
81
+ expect(index(80, 90)).toEqual([12, 14]);
82
+ expect(index(90, 100)).toEqual([14, 16]);
83
+
84
+ expect(index(0, 99)).toEqual([0, 16]);
85
+ expect(index(0, 100)).toEqual([0, 16]);
86
+ });
87
+
88
+ test("Overlapping ranges with the same start coordinate are binned correctly", () => {
89
+ const items = [
90
+ // Increasing lengths
91
+ [0, 5],
92
+ [0, 64],
93
+ [0, 80],
94
+ // Decreasing lengths
95
+ [100, 191],
96
+ [100, 167],
97
+ [100, 123],
98
+ ];
99
+ const indexer = createBinningRangeIndexer(
100
+ 100,
101
+ [0, 1000],
102
+ (x) => x[0],
103
+ (x) => x[1]
104
+ );
105
+
106
+ // Each item uses two vertices
107
+ items.forEach((x, i) => indexer(x, i * 2, (i + 1) * 2));
37
108
 
38
109
  const index = indexer.getIndex();
39
110
 
40
- // TODO: More tests. Doesn't work 100%
111
+ expect(index(0, 1)).toEqual([0, 6]);
112
+ expect(index(3, 40)).toEqual([0, 6]);
113
+ expect(index(0, 100)).toEqual([0, 6]);
114
+ expect(index(77, 78)).toEqual([4, 6]);
41
115
 
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]);
116
+ expect(index(90, 205)).toEqual([6, 12]);
117
+ expect(index(111, 115)).toEqual([6, 12]);
118
+ // Not optimal. Should be [6, 8], but [6, 12] is not wrong
119
+ expect(index(180, 190)).toEqual([6, 12]);
46
120
  });
47
121
 
48
122
  test("Overlapping ranges are binned correctly", () => {
49
123
  const items = [
50
124
  [10, 30],
51
125
  [25, 50],
126
+
127
+ [102, 129],
128
+ [112, 139],
129
+ [121, 149],
52
130
  ];
53
131
  const indexer = createBinningRangeIndexer(
54
- 10,
55
- [0, 100],
132
+ 100,
133
+ [0, 1000],
56
134
  (x) => x[0],
57
135
  (x) => x[1]
58
136
  );
59
137
 
60
- items.forEach((x) => indexer(x, x[0], x[1]));
138
+ items.forEach((x, i) => indexer(x, i * 2, (i + 1) * 2));
61
139
 
62
140
  const index = indexer.getIndex();
63
141
 
64
- // TODO: More tests. Doesn't work 100%
142
+ // TODO: More tests
143
+
144
+ expect(index(0, 5)).toEqual([0, 0]);
145
+ expect(index(0, 15)).toEqual([0, 2]);
146
+ expect(index(27, 40)).toEqual([0, 4]);
147
+ expect(index(40, 50)).toEqual([2, 4]);
148
+ expect(index(40, 80)).toEqual([2, 4]);
149
+ expect(index(10, 29)).toEqual([0, 4]);
65
150
 
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]);
151
+ expect(index(90, 160)).toEqual([4, 10]);
152
+ expect(index(115, 116)).toEqual([4, 8]);
153
+ expect(index(135, 145)).toEqual([6, 10]);
72
154
  });
73
155
  });