@genome-spy/core 0.43.1 → 0.43.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.
@@ -97,7 +97,8 @@ export default class BigBedSource extends SingleAxisWindowedSource {
97
97
  * This parser avoids generating piles of garbage to be collected by the GC.
98
98
  * We don't split the line into an array of strings, but instead parse the
99
99
  * integer fields directly from the original string.
100
- * This parser doesn't support arrays, etc. at the moment.
100
+ * This parser doesn't support arrays, etc. at the moment. This could, however,
101
+ * be extended into a fully-featured parser.
101
102
  *
102
103
  * @param {import("@gmod/bed").default} bed
103
104
  */
@@ -171,6 +172,30 @@ function makeFastParser(bed) {
171
172
  }
172
173
  });
173
174
 
175
+ const templateFields = fields.map(
176
+ (field) => `"${field.name}": ${field.isNumeric ? "0" : "emptyString"}`
177
+ );
178
+
179
+ /**
180
+ * Make a template object with all fields to avoid the JavaScript VM's
181
+ * hidden class to be changed after each property assignment. Transitions
182
+ * between hidden classes generate plenty of garbage to be collected.
183
+ *
184
+ * Ideally, the parsed values would be assigned directly in this function,
185
+ * but for some reason, it results in abysmally slow performance on Chrome,
186
+ * but not on Firefox, where it would be super fast.
187
+ */
188
+ const makeTemplate = new Function(`
189
+ const emptyString = "";
190
+ return function makeTemplate(chrom, chromStart, chromEnd) {
191
+ return {
192
+ chrom,
193
+ chromStart,
194
+ chromEnd,
195
+ ${templateFields.join(",\n")}
196
+ }
197
+ };`)();
198
+
174
199
  /**
175
200
  * @param {string} line
176
201
  */
@@ -189,11 +214,7 @@ function makeFastParser(bed) {
189
214
  function parseLine(chrom, chromStart, chromEnd, rest) {
190
215
  setLine(rest);
191
216
 
192
- currentObject = {
193
- chrom,
194
- chromStart,
195
- chromEnd,
196
- };
217
+ currentObject = makeTemplate(chrom, chromStart, chromEnd);
197
218
 
198
219
  for (let j = 0, n = fieldParsers.length; j < n; j++) {
199
220
  fieldParsers[j]();
@@ -2,7 +2,10 @@
2
2
  /**
3
3
  * Computes coverage for sorted segments
4
4
  *
5
- * TODO: Binned coverage
5
+ * TODO: Binned coverage, e.g., don't emit a new segment for every
6
+ * coverage change, but only every n bases or so. The most straightforward
7
+ * way to implement it is a separate transform that bins the coverage
8
+ * segments and calculates weighted averages.
6
9
  */
7
10
  export default class CoverageTransform extends FlowNode {
8
11
  /**
@@ -23,7 +26,12 @@ export default class CoverageTransform extends FlowNode {
23
26
  chrom: string;
24
27
  };
25
28
  createSegment: Function;
26
- ends: FlatQueue<any>;
29
+ /**
30
+ * End pos as priority, weight as value
31
+ *
32
+ * @type {FlatQueue<number>}
33
+ */
34
+ ends: FlatQueue<number>;
27
35
  }
28
36
  import FlowNode from "../flowNode.js";
29
37
  import FlatQueue from "flatqueue";
@@ -1 +1 @@
1
- {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../../../src/data/transforms/coverage.js"],"names":[],"mappings":";AAKA;;;;GAIG;AACH;IAKI;;OAEG;IACH,oBAFW,OAAO,yBAAyB,EAAE,cAAc,EA4C1D;IAxCG,yDAAoB;IAEpB,mDAAwC;IACxC,iDAAoC;IAEpC,mCAAmC;IACnC,sBADoB,GAAG,KAAE,MAAM,CAGT;IACtB,mCAAmC;IACnC,uBADoB,GAAG,KAAE,MAAM,CACsC;IAErE;;;;;MAKC;IAGD,wBAgBC;IAGD,qBAA2B;CA2HlC;qBAnLyC,gBAAgB;sBAHpC,WAAW"}
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../../../src/data/transforms/coverage.js"],"names":[],"mappings":";AAKA;;;;;;;GAOG;AACH;IAKI;;OAEG;IACH,oBAFW,OAAO,yBAAyB,EAAE,cAAc,EAgD1D;IA5CG,yDAAoB;IAEpB,mDAAwC;IACxC,iDAAoC;IAEpC,mCAAmC;IACnC,sBADoB,GAAG,KAAE,MAAM,CAGT;IACtB,mCAAmC;IACnC,uBADoB,GAAG,KAAE,MAAM,CACsC;IAErE;;;;;MAKC;IAGD,wBAgBC;IAED;;;;OAIG;IACH,MAFU,UAAU,MAAM,CAAC,CAEA;CAiIlC;qBAhMyC,gBAAgB;sBAHpC,WAAW"}
@@ -6,7 +6,10 @@ import FlowNode, { BEHAVIOR_CLONES } from "../flowNode.js";
6
6
  /**
7
7
  * Computes coverage for sorted segments
8
8
  *
9
- * TODO: Binned coverage
9
+ * TODO: Binned coverage, e.g., don't emit a new segment for every
10
+ * coverage change, but only every n bases or so. The most straightforward
11
+ * way to implement it is a separate transform that bins the coverage
12
+ * segments and calculates weighted averages.
10
13
  */
11
14
  export default class CoverageTransform extends FlowNode {
12
15
  get behavior() {
@@ -56,7 +59,11 @@ export default class CoverageTransform extends FlowNode {
56
59
  )
57
60
  );
58
61
 
59
- // End pos as priority, weight as value
62
+ /**
63
+ * End pos as priority, weight as value
64
+ *
65
+ * @type {FlatQueue<number>}
66
+ */
60
67
  this.ends = new FlatQueue();
61
68
  }
62
69
 
@@ -75,7 +82,7 @@ export default class CoverageTransform extends FlowNode {
75
82
  const chromAccessor = this.chromAccessor;
76
83
  const weightAccessor = this.weightAccessor;
77
84
 
78
- /** @type {Record<string, number|string>} used for merging adjacent segment */
85
+ /** @type {import("../flowNode.js").Datum} used for merging adjacent segment */
79
86
  let bufferedSegment;
80
87
 
81
88
  /** @type {string} */
@@ -91,7 +98,7 @@ export default class CoverageTransform extends FlowNode {
91
98
  /** @type {number} */
92
99
  let prevEdge;
93
100
 
94
- // End pos as priority, weight as value
101
+ /** End pos as priority, weight as value */
95
102
  const ends = this.ends;
96
103
  ends.clear();
97
104
 
@@ -127,9 +134,7 @@ export default class CoverageTransform extends FlowNode {
127
134
  };
128
135
 
129
136
  const flushQueue = () => {
130
- // Flush queue
131
- /** @type {number} */
132
- let edge;
137
+ let edge = 0;
133
138
  while ((edge = ends.peekValue()) !== undefined) {
134
139
  pushSegment(prevEdge, edge, coverage);
135
140
  prevEdge = edge;
@@ -147,8 +152,7 @@ export default class CoverageTransform extends FlowNode {
147
152
  this.handle = (datum) => {
148
153
  const start = startAccessor(datum);
149
154
 
150
- /** @type {number} */
151
- let edge;
155
+ let edge = 0;
152
156
  while ((edge = ends.peekValue()) !== undefined && edge < start) {
153
157
  pushSegment(prevEdge, edge, coverage);
154
158
  prevEdge = edge;
@@ -179,5 +183,14 @@ export default class CoverageTransform extends FlowNode {
179
183
  flushQueue();
180
184
  super.complete();
181
185
  };
186
+
187
+ /**
188
+ * @param {import("../../types/flowBatch.js").FlowBatch} flowBatch
189
+ */
190
+ this.beginBatch = (flowBatch) => {
191
+ flushQueue();
192
+ prevChrom = undefined;
193
+ super.beginBatch(flowBatch);
194
+ };
182
195
  }
183
196
  }
@@ -1,4 +1,4 @@
1
- import { expect, test } from "vitest";
1
+ import { describe, expect, test } from "vitest";
2
2
  import CoverageTransform from "./coverage.js";
3
3
  import { processData } from "../flowTestUtils.js";
4
4
 
@@ -16,108 +16,223 @@ function transform(params, data) {
16
16
  return processData(t, data);
17
17
  }
18
18
 
19
- test("Coverage transform produces correct coverage segments", () => {
20
- const reads = [
21
- [0, 4],
22
- [1, 3],
23
- [2, 6],
24
- [4, 8],
25
- [8, 10],
26
- [11, 14],
27
- [11, 13],
28
- [11, 12],
29
- [15, 18],
30
- [16, 18],
31
- [17, 18],
32
- ].map((d) => ({
33
- start: d[0],
34
- end: d[1],
35
- }));
36
-
37
- const coverageSegments = [
38
- [0, 1, 1],
39
- [1, 2, 2],
40
- [2, 3, 3],
41
- [3, 6, 2],
42
- [6, 10, 1],
43
- [11, 12, 3],
44
- [12, 13, 2],
45
- [13, 14, 1],
46
- [15, 16, 1],
47
- [16, 17, 2],
48
- [17, 18, 3],
49
- ].map((d) => ({
50
- start: d[0],
51
- end: d[1],
52
- coverage: d[2],
53
- }));
54
-
19
+ /**
20
+ *
21
+ * @param {[number, number][]} reads Start, end
22
+ * @param {[number, number, number][]} coverageSegments Start, end, coverage
23
+ */
24
+ function testSimpleCoverage(reads, coverageSegments) {
55
25
  /** @type {CoverageParams} */
56
26
  const coverageConfig = {
57
27
  type: "coverage",
58
28
  start: "start",
59
29
  end: "end",
60
30
  };
61
- expect(transform(coverageConfig, reads)).toEqual(coverageSegments);
62
- });
63
-
64
- test("Coverage transform handles chromosomes", () => {
65
- const reads = [
66
- { chrom: "chr1", start: 0, end: 1 },
67
- { chrom: "chr2", start: 0, end: 1 },
68
- { chrom: "chr3", start: 1, end: 3 },
69
- ];
70
-
71
- const coverageSegments = [
72
- { chrom: "chr1", start: 0, end: 1, coverage: 1 },
73
- { chrom: "chr2", start: 0, end: 1, coverage: 1 },
74
- { chrom: "chr3", start: 1, end: 3, coverage: 1 },
75
- ];
31
+ expect(
32
+ transform(
33
+ coverageConfig,
34
+ reads.map((d) => ({
35
+ start: d[0],
36
+ end: d[1],
37
+ }))
38
+ )
39
+ ).toEqual(
40
+ coverageSegments.map((d) => ({
41
+ start: d[0],
42
+ end: d[1],
43
+ coverage: d[2],
44
+ }))
45
+ );
46
+ }
76
47
 
48
+ /**
49
+ *
50
+ * @param {[number, number, number][]} reads Start, end, weight
51
+ * @param {[number, number, number][]} coverageSemgments Start, end, coverage
52
+ */
53
+ function testWeightedCoverage(reads, coverageSegments) {
77
54
  /** @type {CoverageParams} */
78
55
  const coverageConfig = {
79
56
  type: "coverage",
80
- chrom: "chrom",
81
57
  start: "start",
82
58
  end: "end",
59
+ weight: "weight",
83
60
  };
61
+ expect(
62
+ transform(
63
+ coverageConfig,
64
+ reads.map((d) => ({
65
+ start: d[0],
66
+ end: d[1],
67
+ weight: d[2],
68
+ }))
69
+ )
70
+ ).toEqual(
71
+ coverageSegments.map((d) => ({
72
+ start: d[0],
73
+ end: d[1],
74
+ coverage: d[2],
75
+ }))
76
+ );
77
+ }
84
78
 
85
- expect(transform(coverageConfig, reads)).toEqual(coverageSegments);
86
- });
79
+ describe("Coverage transform", () => {
80
+ test("Typical case", () =>
81
+ testSimpleCoverage(
82
+ [
83
+ [0, 4],
84
+ [1, 3],
85
+ [2, 6],
86
+ [4, 8],
87
+ [8, 10],
88
+ [11, 14],
89
+ [11, 13],
90
+ [11, 12],
91
+ [15, 18],
92
+ [16, 18],
93
+ [17, 18],
94
+ ],
95
+ [
96
+ [0, 1, 1],
97
+ [1, 2, 2],
98
+ [2, 3, 3],
99
+ [3, 6, 2],
100
+ [6, 10, 1],
101
+ [11, 12, 3],
102
+ [12, 13, 2],
103
+ [13, 14, 1],
104
+ [15, 16, 1],
105
+ [16, 17, 2],
106
+ [17, 18, 3],
107
+ ]
108
+ ));
87
109
 
88
- test("Coverage transform handles weights", () => {
89
- const reads = [
90
- [0, 4, 1],
91
- [1, 3, 2],
92
- [2, 6, 3],
93
- [8, 10, -1],
94
- ].map((d) => ({
95
- start: d[0],
96
- end: d[1],
97
- weight: d[2],
98
- }));
110
+ test("Multiple identical overlapping segments", () =>
111
+ testSimpleCoverage(
112
+ [
113
+ [1, 2],
114
+ [3, 4],
115
+ [3, 4],
116
+ [5, 6],
117
+ [5, 6],
118
+ [5, 6],
119
+ ],
120
+ [
121
+ [1, 2, 1],
122
+ [3, 4, 2],
123
+ [5, 6, 3],
124
+ ]
125
+ ));
99
126
 
100
- const coverageSegments = [
101
- [0, 1, 1],
102
- [1, 2, 3],
103
- [2, 3, 6],
104
- [3, 4, 4],
105
- [4, 6, 3],
106
- [8, 10, -1],
107
- ].map((d) => ({
108
- start: d[0],
109
- end: d[1],
110
- coverage: d[2],
111
- }));
127
+ test("Adjacent segments with equal coverage are merged", () =>
128
+ testSimpleCoverage(
129
+ [
130
+ [1, 2],
131
+ [2, 3],
132
+ [3, 4],
133
+ [5, 6],
134
+ [6, 7],
135
+ [7, 8],
136
+ ],
137
+ [
138
+ [1, 4, 1],
139
+ [5, 8, 1],
140
+ ]
141
+ ));
112
142
 
113
- /** @type {CoverageParams} */
114
- const coverageConfig = {
115
- type: "coverage",
116
- chrom: "chrom",
117
- start: "start",
118
- end: "end",
119
- weight: "weight",
120
- };
143
+ test("Chromosomes pass through", () => {
144
+ const reads = [
145
+ { chrom: "chr1", start: 0, end: 1 },
146
+ { chrom: "chr2", start: 0, end: 1 },
147
+ { chrom: "chr3", start: 1, end: 3 },
148
+ ];
149
+
150
+ const coverageSegments = [
151
+ { chrom: "chr1", start: 0, end: 1, coverage: 1 },
152
+ { chrom: "chr2", start: 0, end: 1, coverage: 1 },
153
+ { chrom: "chr3", start: 1, end: 3, coverage: 1 },
154
+ ];
155
+
156
+ /** @type {CoverageParams} */
157
+ const coverageConfig = {
158
+ type: "coverage",
159
+ chrom: "chrom",
160
+ start: "start",
161
+ end: "end",
162
+ };
163
+
164
+ expect(transform(coverageConfig, reads)).toEqual(coverageSegments);
165
+ });
166
+
167
+ test("Typical weighted coverage", () =>
168
+ testWeightedCoverage(
169
+ [
170
+ [0, 4, 1],
171
+ [1, 3, 2],
172
+ [2, 6, 3],
173
+ [8, 10, -1],
174
+ ],
175
+ [
176
+ [0, 1, 1],
177
+ [1, 2, 3],
178
+ [2, 3, 6],
179
+ [3, 4, 4],
180
+ [4, 6, 3],
181
+ [8, 10, -1],
182
+ ]
183
+ ));
184
+
185
+ test("Multiple weights at a single locus", () =>
186
+ testWeightedCoverage(
187
+ [
188
+ // -- Locus 1
189
+ [1, 2, 1],
190
+ [1, 2, 2],
191
+ [1, 2, 3],
192
+ [1, 2, 4],
193
+ [1, 2, 5],
194
+ // -- Locus 2
195
+ [2, 3, 2],
196
+ [2, 3, 3],
197
+ [2, 3, 4],
198
+ [2, 3, 5],
199
+ [2, 3, 6],
200
+ ],
201
+ [
202
+ // -- Locus 1
203
+ [1, 2, 15],
204
+ // -- Locus 2
205
+ [2, 3, 20],
206
+ ]
207
+ ));
121
208
 
122
- expect(transform(coverageConfig, reads)).toEqual(coverageSegments);
209
+ test("Adjacent segments with different weights produce separated segments", () =>
210
+ testWeightedCoverage(
211
+ [
212
+ // -- Cluster 1
213
+ [1, 2, 2],
214
+ [2, 3, 1],
215
+ [3, 4, 1],
216
+ // -- Cluster 2
217
+ [5, 6, 1],
218
+ [6, 7, 2],
219
+ [7, 8, 1],
220
+ // -- Cluster 3
221
+ [9, 10, 1],
222
+ [10, 11, 1],
223
+ [11, 12, 2],
224
+ ],
225
+ [
226
+ // -- Cluster 1
227
+ [1, 2, 2],
228
+ [2, 4, 1],
229
+ // -- Cluster 2
230
+ [5, 6, 1],
231
+ [6, 7, 2],
232
+ [7, 8, 1],
233
+ // -- Cluster 3
234
+ [9, 11, 1],
235
+ [11, 12, 2],
236
+ ]
237
+ ));
123
238
  });
@@ -1,9 +1,10 @@
1
1
  /**
2
- * Returns an iterator that merges multiple sorted arrays.
2
+ * Merges multiple sorted arrays.
3
3
  *
4
4
  * @param {T[][]} arrays
5
+ * @param {function(T):void} handler a function that will be called for each element
5
6
  * @param {function(T):number} [accessor]
6
7
  * @template T
7
8
  */
8
- export default function kWayMerge<T>(arrays: T[][], accessor?: (arg0: T) => number): Generator<T, void, unknown>;
9
+ export default function kWayMerge<T>(arrays: T[][], handler: (arg0: T) => void, accessor?: (arg0: T) => number): void;
9
10
  //# sourceMappingURL=kWayMerge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"kWayMerge.d.ts","sourceRoot":"","sources":["../../../src/utils/kWayMerge.js"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,4EAHuB,MAAM,+BAmC5B"}
1
+ {"version":3,"file":"kWayMerge.d.ts","sourceRoot":"","sources":["../../../src/utils/kWayMerge.js"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,0EAJuB,IAAI,0BACJ,MAAM,QAmC5B"}
@@ -1,13 +1,14 @@
1
1
  import FlatQueue from "flatqueue";
2
2
 
3
3
  /**
4
- * Returns an iterator that merges multiple sorted arrays.
4
+ * Merges multiple sorted arrays.
5
5
  *
6
6
  * @param {T[][]} arrays
7
+ * @param {function(T):void} handler a function that will be called for each element
7
8
  * @param {function(T):number} [accessor]
8
9
  * @template T
9
10
  */
10
- export default function* kWayMerge(arrays, accessor = (x) => +x) {
11
+ export default function kWayMerge(arrays, handler, accessor = (x) => +x) {
11
12
  // https://www.wikiwand.com/en/K-way_merge_algorithm
12
13
 
13
14
  // This could be optimized by implementing a tournament tree or
@@ -31,7 +32,7 @@ export default function* kWayMerge(arrays, accessor = (x) => +x) {
31
32
  let pointer = pointers[i];
32
33
  const element = array[pointer++];
33
34
 
34
- yield element;
35
+ handler(element);
35
36
 
36
37
  if (pointer < array.length) {
37
38
  const newValue = accessor(array[pointer]);
@@ -22,5 +22,9 @@ test("k-way merge merges multiple sorted arrays", () => {
22
22
  /** @type {function(any):number} */
23
23
  const accessor = (d) => d.a;
24
24
 
25
- expect([...kWayMerge(arrays, accessor)]).toEqual(sorted);
25
+ /** @type {{a: number}[]} */
26
+ const result = [];
27
+ kWayMerge(arrays, (d) => result.push(d), accessor);
28
+
29
+ expect(result).toEqual(sorted);
26
30
  });
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "contributors": [],
9
9
  "license": "MIT",
10
- "version": "0.43.1",
10
+ "version": "0.43.2",
11
11
  "jsdelivr": "dist/bundle/index.js",
12
12
  "unpkg": "dist/bundle/index.js",
13
13
  "browser": "dist/bundle/index.js",
@@ -65,5 +65,5 @@
65
65
  "vega-scale": "^7.1.1",
66
66
  "vega-util": "^1.16.0"
67
67
  },
68
- "gitHead": "e47791753d5467ab83b5ef073976eb2f2484000b"
68
+ "gitHead": "12ff70326672ed7ca34ef081c01fc25f19cf1ede"
69
69
  }