@jbrowse/bandage-core 8.2.0 → 9.1.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/dist/bubbles/bubbleHalos.js +9 -5
- package/dist/bubbles/bubbleLine.js +3 -1
- package/dist/bubbles/bubblesFromGraph.js +8 -4
- package/dist/bubbles/classifyBubble.js +2 -2
- package/dist/bubbles/superbubbles.js +21 -0
- package/dist/chunks/{chunk-ISEI2FH3.js → chunk-2AWUWGAD.js} +10 -4
- package/dist/chunks/{chunk-GQ2AOHU4.js → chunk-2ZT7ZF7A.js} +13 -31
- package/dist/chunks/{chunk-DE4UFDUZ.js → chunk-3TGUKQWQ.js} +4 -4
- package/dist/chunks/{chunk-KCRL5JB2.js → chunk-75HR3756.js} +55 -17
- package/dist/chunks/{chunk-DRA7733Q.js → chunk-BPRXQ2SS.js} +8 -3
- package/dist/chunks/{chunk-TEDQJPMI.js → chunk-CWFNEKST.js} +22 -7
- package/dist/chunks/chunk-FUVI4WQP.js +273 -0
- package/dist/chunks/{chunk-MFKORG3F.js → chunk-FX4RDMDN.js} +5 -1
- package/dist/chunks/{chunk-4S47XRYA.js → chunk-GOZYKNQ2.js} +16 -11
- package/dist/chunks/chunk-INJ2UVOT.js +370 -0
- package/dist/chunks/{chunk-TL4B342F.js → chunk-MXDQGA7A.js} +1 -1
- package/dist/chunks/{chunk-SKKYLBRA.js → chunk-RRZMMYBG.js} +1 -1
- package/dist/chunks/{chunk-3FXPFVKJ.js → chunk-UID3QBWH.js} +1 -1
- package/dist/chunks/{chunk-2WNBVB55.js → chunk-WBCOREDZ.js} +121 -74
- package/dist/chunks/{chunk-OWFPAIJI.js → chunk-WEDZ76AK.js} +1 -1
- package/dist/chunks/{chunk-CF62U3QS.js → chunk-WNUZP3WQ.js} +2 -2
- package/dist/chunks/{chunk-WOO7QYKB.js → chunk-XKWD2RY3.js} +8 -2
- package/dist/chunks/{chunk-6VBDMZBZ.js → chunk-XLCTQHBE.js} +1 -1
- package/dist/chunks/{chunk-733L2TS6.js → chunk-XXT2JCAC.js} +10 -7
- package/dist/cli/figure.js +9 -9
- package/dist/figure.js +6 -6
- package/dist/figureSettings.js +3 -3
- package/dist/index.js +45 -33
- package/dist/layout/forceCache.js +3 -3
- package/dist/layout/tubeMapLayout.js +12 -6
- package/dist/layoutModes.js +3 -3
- package/dist/pipeline.js +2 -2
- package/dist/renderer/Canvas2DRenderer.js +1 -1
- package/dist/renderer/recordingCanvas.js +12 -1
- package/dist/tubeMap/axis.js +1 -1
- package/dist/tubeMap/coarsen.js +11 -23
- package/dist/tubeMap/connectors.js +3 -4
- package/dist/tubeMap/deviations.js +15 -19
- package/dist/tubeMap/draw.js +11 -5
- package/dist/tubeMap/genes.js +2 -2
- package/dist/tubeMap/mismatches.js +1 -1
- package/dist/tubeMap/reads.js +1 -1
- package/dist/types/bubbles/bubbleHalos.d.ts +2 -0
- package/dist/types/bubbles/bubbleLine.d.ts +3 -0
- package/dist/types/bubbles/bubblesFromGraph.d.ts +12 -2
- package/dist/types/bubbles/superbubbles.d.ts +5 -0
- package/dist/types/index.d.ts +5 -4
- package/dist/types/layout/tubeMapLayout.d.ts +15 -2
- package/dist/types/layoutModes.d.ts +5 -5
- package/dist/types/renderer/Canvas2DRenderer.d.ts +6 -3
- package/dist/types/tubeMap/axis.d.ts +6 -3
- package/dist/types/tubeMap/connectors.d.ts +3 -11
- package/dist/types/tubeMap/deviations.d.ts +2 -3
- package/dist/types/tubeMap/draw.d.ts +5 -1
- package/dist/types/tubeMap/mismatches.d.ts +1 -1
- package/dist/types/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewport.js +3 -3
- package/package.json +1 -1
- package/dist/chunks/chunk-W2I4DOL4.js +0 -230
- package/dist/chunks/{chunk-5OIVTEF7.js → chunk-OOXIMURX.js} +3 -3
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bubbleSubgraph
|
|
3
|
+
} from "./chunk-VHW7FDHU.js";
|
|
4
|
+
import {
|
|
5
|
+
coversGraph
|
|
6
|
+
} from "./chunk-2AWUWGAD.js";
|
|
7
|
+
import {
|
|
8
|
+
bubblesFromGraph,
|
|
9
|
+
walkIndexOf,
|
|
10
|
+
walkRoutes
|
|
11
|
+
} from "./chunk-GOZYKNQ2.js";
|
|
12
|
+
import {
|
|
13
|
+
bubbleSegmentIds
|
|
14
|
+
} from "./chunk-XKWD2RY3.js";
|
|
15
|
+
import {
|
|
16
|
+
SATURATED_PATH_COUNT
|
|
17
|
+
} from "./chunk-FX4RDMDN.js";
|
|
18
|
+
import {
|
|
19
|
+
strandSides
|
|
20
|
+
} from "./chunk-2CVHXFK7.js";
|
|
21
|
+
import {
|
|
22
|
+
isBackbone
|
|
23
|
+
} from "./chunk-FYQWPEEY.js";
|
|
24
|
+
|
|
25
|
+
// src/bubbles/superbubbles.ts
|
|
26
|
+
var MAX_SUPERBUBBLES = 50;
|
|
27
|
+
function slotGraph(graph, index) {
|
|
28
|
+
const slots = graph.nodes.length * 2;
|
|
29
|
+
const seen = /* @__PURE__ */ new Set();
|
|
30
|
+
const pairs = [];
|
|
31
|
+
const degree = new Int32Array(slots);
|
|
32
|
+
for (const e of graph.edges) {
|
|
33
|
+
const a = index.get(e.from);
|
|
34
|
+
const b = index.get(e.to);
|
|
35
|
+
if (a === void 0 || b === void 0) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const sides = strandSides(e);
|
|
39
|
+
const ka = a * 2 + (sides.from === "end" ? 1 : 0);
|
|
40
|
+
const kb = b * 2 + (sides.to === "end" ? 1 : 0);
|
|
41
|
+
const id = Math.min(ka, kb) * slots + Math.max(ka, kb);
|
|
42
|
+
if (seen.has(id)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
seen.add(id);
|
|
46
|
+
pairs.push(ka, kb);
|
|
47
|
+
degree[ka]++;
|
|
48
|
+
if (ka !== kb) {
|
|
49
|
+
degree[kb]++;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const offset = new Int32Array(slots + 1);
|
|
53
|
+
for (let k = 0; k < slots; k++) {
|
|
54
|
+
offset[k + 1] = offset[k] + degree[k];
|
|
55
|
+
}
|
|
56
|
+
const next = new Int32Array(offset[slots]);
|
|
57
|
+
const fill = offset.slice(0, slots);
|
|
58
|
+
for (let i = 0; i < pairs.length; i += 2) {
|
|
59
|
+
const ka = pairs[i];
|
|
60
|
+
const kb = pairs[i + 1];
|
|
61
|
+
next[fill[ka]++] = kb;
|
|
62
|
+
if (ka !== kb) {
|
|
63
|
+
next[fill[kb]++] = ka;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { offset, next };
|
|
67
|
+
}
|
|
68
|
+
function searcher(nodes, offset, next) {
|
|
69
|
+
const visited = new Int32Array(nodes);
|
|
70
|
+
const pending = new Uint8Array(nodes * 2);
|
|
71
|
+
const stack = new Int32Array(nodes * 2);
|
|
72
|
+
const inside = new Int32Array(nodes);
|
|
73
|
+
const touched = [];
|
|
74
|
+
let stamp = 0;
|
|
75
|
+
return (start) => {
|
|
76
|
+
const s = start >> 1;
|
|
77
|
+
stamp++;
|
|
78
|
+
touched.length = 0;
|
|
79
|
+
let open = 0;
|
|
80
|
+
let top = 0;
|
|
81
|
+
let count = 0;
|
|
82
|
+
let found;
|
|
83
|
+
const expect = (k) => {
|
|
84
|
+
if (!pending[k]) {
|
|
85
|
+
pending[k] = 1;
|
|
86
|
+
open++;
|
|
87
|
+
touched.push(k);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
expect(start);
|
|
91
|
+
stack[top++] = start;
|
|
92
|
+
search: while (top > 0) {
|
|
93
|
+
const k = stack[--top];
|
|
94
|
+
const v = k >> 1;
|
|
95
|
+
visited[v] = stamp;
|
|
96
|
+
if (v !== s) {
|
|
97
|
+
inside[count++] = k;
|
|
98
|
+
}
|
|
99
|
+
if (pending[k]) {
|
|
100
|
+
pending[k] = 0;
|
|
101
|
+
open--;
|
|
102
|
+
}
|
|
103
|
+
if (offset[k] === offset[k + 1]) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
for (let p = offset[k]; p < offset[k + 1]; p++) {
|
|
107
|
+
const entry = next[p];
|
|
108
|
+
const u = entry >> 1;
|
|
109
|
+
if (u === s) {
|
|
110
|
+
break search;
|
|
111
|
+
}
|
|
112
|
+
expect(entry ^ 1);
|
|
113
|
+
if (visited[u] === stamp) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let ready = true;
|
|
117
|
+
for (let q = offset[entry]; q < offset[entry + 1]; q++) {
|
|
118
|
+
if (visited[next[q] >> 1] !== stamp) {
|
|
119
|
+
ready = false;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (ready) {
|
|
124
|
+
stack[top++] = entry ^ 1;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (top === 1 && open === 1) {
|
|
128
|
+
if (count > 0) {
|
|
129
|
+
found = {
|
|
130
|
+
source: start,
|
|
131
|
+
sink: stack[0],
|
|
132
|
+
inside: inside.slice(0, count)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
for (const k of touched) {
|
|
139
|
+
pending[k] = 0;
|
|
140
|
+
}
|
|
141
|
+
return found;
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function reversed(f) {
|
|
145
|
+
return {
|
|
146
|
+
source: f.sink ^ 1,
|
|
147
|
+
sink: f.source ^ 1,
|
|
148
|
+
inside: f.inside.map((k) => k ^ 1).reverse()
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
var collator = new Intl.Collator(void 0, { numeric: true });
|
|
152
|
+
function onReference(a, b) {
|
|
153
|
+
return isBackbone(a) && isBackbone(b) && a.stable.refName === b.stable.refName;
|
|
154
|
+
}
|
|
155
|
+
function forward(f, nodes) {
|
|
156
|
+
const a = nodes[f.source >> 1];
|
|
157
|
+
const b = nodes[f.sink >> 1];
|
|
158
|
+
const byRef = onReference(a, b) ? a.stable.start - b.stable.start : 0;
|
|
159
|
+
const bySide = ((f.sink ^ 1) & 1) - (f.source & 1);
|
|
160
|
+
return (byRef || bySide || collator.compare(a.name, b.name)) < 0;
|
|
161
|
+
}
|
|
162
|
+
function nodesOf(f) {
|
|
163
|
+
return [f.source >> 1, ...Array.from(f.inside, (k) => k >> 1), f.sink >> 1];
|
|
164
|
+
}
|
|
165
|
+
function allSuperbubbles(graph) {
|
|
166
|
+
const { nodes } = graph;
|
|
167
|
+
const index = new Map(nodes.map((n, i) => [n.id, i]));
|
|
168
|
+
const { offset, next } = slotGraph(graph, index);
|
|
169
|
+
const find = searcher(nodes.length, offset, next);
|
|
170
|
+
const byEnds = /* @__PURE__ */ new Map();
|
|
171
|
+
for (let k = 0; k < nodes.length * 2; k++) {
|
|
172
|
+
const hit = find(k);
|
|
173
|
+
if (!hit) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const a = hit.source >> 1;
|
|
177
|
+
const b = hit.sink >> 1;
|
|
178
|
+
const id = Math.min(a, b) * nodes.length + Math.max(a, b);
|
|
179
|
+
if (forward(hit, nodes) || !byEnds.has(id)) {
|
|
180
|
+
byEnds.set(id, hit);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const found = [...byEnds.values()].map(
|
|
184
|
+
(f) => forward(f, nodes) ? f : reversed(f)
|
|
185
|
+
);
|
|
186
|
+
return { found, offset, next };
|
|
187
|
+
}
|
|
188
|
+
function routesThrough(nodes, offset, next, f) {
|
|
189
|
+
const sink = f.sink >> 1;
|
|
190
|
+
const best = /* @__PURE__ */ new Map([
|
|
191
|
+
[f.source >> 1, { min: 0, max: 0, n: 1 }]
|
|
192
|
+
]);
|
|
193
|
+
for (const k of [f.source, ...f.inside]) {
|
|
194
|
+
const cur = best.get(k >> 1);
|
|
195
|
+
if (!cur) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
for (let p = offset[k]; p < offset[k + 1]; p++) {
|
|
199
|
+
const u = next[p] >> 1;
|
|
200
|
+
const add = u === sink ? 0 : nodes[u].length;
|
|
201
|
+
const prev = best.get(u);
|
|
202
|
+
best.set(u, {
|
|
203
|
+
min: Math.min(prev?.min ?? Infinity, cur.min + add),
|
|
204
|
+
max: Math.max(prev?.max ?? -Infinity, cur.max + add),
|
|
205
|
+
n: Math.min(SATURATED_PATH_COUNT, cur.n + (prev?.n ?? 0))
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return best.get(sink) ?? { min: 0, max: 0, n: 0 };
|
|
210
|
+
}
|
|
211
|
+
function superbubblesFromGraph(graph, span) {
|
|
212
|
+
const { nodes } = graph;
|
|
213
|
+
const { found, offset, next } = allSuperbubbles(graph);
|
|
214
|
+
const covers = found.map((f) => coversGraph(f.inside.length, nodes.length));
|
|
215
|
+
const members = found.map(nodesOf);
|
|
216
|
+
const holders = /* @__PURE__ */ new Map();
|
|
217
|
+
members.forEach((vs, i) => {
|
|
218
|
+
for (const v of vs) {
|
|
219
|
+
;
|
|
220
|
+
(holders.get(v) ?? holders.set(v, []).get(v)).push(i);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
const nested = (f, i) => holders.get(f.inside[0] >> 1).some(
|
|
224
|
+
(j) => j !== i && !covers[j] && members[j].length > members[i].length && members[i].every((v) => holders.get(v).includes(j))
|
|
225
|
+
);
|
|
226
|
+
const placed = (f) => span !== void 0 || onReference(nodes[f.source >> 1], nodes[f.sink >> 1]);
|
|
227
|
+
const outermost = found.filter((f, i) => !covers[i] && placed(f) && !nested(f, i)).sort((a, b) => b.inside.length - a.inside.length).slice(0, MAX_SUPERBUBBLES);
|
|
228
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
229
|
+
const walkIndex = walkIndexOf(graph);
|
|
230
|
+
return outermost.map((f) => {
|
|
231
|
+
const source = nodes[f.source >> 1];
|
|
232
|
+
const sink = nodes[f.sink >> 1];
|
|
233
|
+
const walked = walkIndex && walkRoutes(graph, byId, walkIndex, source.id, sink.id);
|
|
234
|
+
const crossed = walked?.n ? walked : void 0;
|
|
235
|
+
const routes = crossed ?? routesThrough(nodes, offset, next, f);
|
|
236
|
+
const placedOn = onReference(source, sink);
|
|
237
|
+
return {
|
|
238
|
+
refName: placedOn ? source.stable.refName : span.refName,
|
|
239
|
+
start: placedOn ? source.stable.start + source.length : span.start,
|
|
240
|
+
end: placedOn ? sink.stable.start : span.end,
|
|
241
|
+
segmentCount: f.inside.length + 2,
|
|
242
|
+
pathCount: routes.n,
|
|
243
|
+
inversion: false,
|
|
244
|
+
shortestAlleleLength: routes.min,
|
|
245
|
+
longestAlleleLength: routes.max,
|
|
246
|
+
segments: nodesOf(f).map((v) => nodes[v].name).join(","),
|
|
247
|
+
shortestAllele: void 0,
|
|
248
|
+
longestAllele: void 0,
|
|
249
|
+
partial: walked !== void 0 && walked.left > 0,
|
|
250
|
+
routes: crossed?.routes,
|
|
251
|
+
key: `${source.name}>${sink.name}`,
|
|
252
|
+
...placedOn ? {} : { offReference: true }
|
|
253
|
+
};
|
|
254
|
+
}).sort(
|
|
255
|
+
(a, b) => a.start - b.start || a.end - b.end || collator.compare(a.key, b.key)
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
function graphBubbles(graph, span) {
|
|
259
|
+
const n = graph.nodes.length;
|
|
260
|
+
const reach = bubblesFromGraph(graph);
|
|
261
|
+
return reach.length === 0 ? superbubblesFromGraph(graph, span) : reach.flatMap(
|
|
262
|
+
(b) => coversGraph(b.segmentCount - 2, n) ? superbubblesFromGraph(
|
|
263
|
+
b.segmentCount === n ? graph : bubbleSubgraph(graph, bubbleSegmentIds(b)),
|
|
264
|
+
b
|
|
265
|
+
) : [b]
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export {
|
|
270
|
+
MAX_SUPERBUBBLES,
|
|
271
|
+
superbubblesFromGraph,
|
|
272
|
+
graphBubbles
|
|
273
|
+
};
|
|
@@ -19,7 +19,10 @@ function parseBubbleLine(line) {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
function sameBubble(a, b) {
|
|
22
|
-
return a.refName === b.refName && a.start === b.start && a.end === b.end;
|
|
22
|
+
return a.key !== void 0 || b.key !== void 0 ? a.key === b.key : a.refName === b.refName && a.start === b.start && a.end === b.end;
|
|
23
|
+
}
|
|
24
|
+
function bubbleKey(b) {
|
|
25
|
+
return b.key ?? `${b.refName}:${b.start}-${b.end}`;
|
|
23
26
|
}
|
|
24
27
|
function bubbleLabel(bubble) {
|
|
25
28
|
const { shortestAlleleLength: min, longestAlleleLength: max } = bubble;
|
|
@@ -38,6 +41,7 @@ function bubbleDescription(bubble) {
|
|
|
38
41
|
export {
|
|
39
42
|
parseBubbleLine,
|
|
40
43
|
sameBubble,
|
|
44
|
+
bubbleKey,
|
|
41
45
|
bubbleLabel,
|
|
42
46
|
SATURATED_PATH_COUNT,
|
|
43
47
|
bubbleDescription
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SATURATED_PATH_COUNT
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-FX4RDMDN.js";
|
|
4
4
|
import {
|
|
5
5
|
referenceOrder
|
|
6
6
|
} from "./chunk-HNQPEMYE.js";
|
|
@@ -38,15 +38,7 @@ function bubblesFromGraph(graph) {
|
|
|
38
38
|
const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
|
|
39
39
|
const ends = /* @__PURE__ */ new Set([backbone[0].id, backbone.at(-1).id]);
|
|
40
40
|
const boundaries = backbone.map((node) => [node, indexOf.get(node.id)]).filter(([node, i]) => ends.has(node.id) || reach[i] === 0).sort(([a, ia], [b, ib]) => ia - ib || a.stable.start - b.stable.start);
|
|
41
|
-
const walkIndex = graph
|
|
42
|
-
const at = /* @__PURE__ */ new Map();
|
|
43
|
-
p.nodeIds.forEach((id, i) => {
|
|
44
|
-
if (!at.has(id)) {
|
|
45
|
-
at.set(id, i);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
return at;
|
|
49
|
-
});
|
|
41
|
+
const walkIndex = walkIndexOf(graph);
|
|
50
42
|
const bubbles = [];
|
|
51
43
|
for (let i = 0; i + 1 < boundaries.length; i++) {
|
|
52
44
|
const [start, i0] = boundaries[i];
|
|
@@ -112,6 +104,17 @@ function bubblesFromGraph(graph) {
|
|
|
112
104
|
}
|
|
113
105
|
return bubbles;
|
|
114
106
|
}
|
|
107
|
+
function walkIndexOf(graph) {
|
|
108
|
+
return graph.paths?.map((p) => {
|
|
109
|
+
const at = /* @__PURE__ */ new Map();
|
|
110
|
+
p.nodeIds.forEach((id, i) => {
|
|
111
|
+
if (!at.has(id)) {
|
|
112
|
+
at.set(id, i);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
return at;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
115
118
|
function walkRoutes(graph, byId, walkIndex, startId, endId) {
|
|
116
119
|
const seen = /* @__PURE__ */ new Map();
|
|
117
120
|
let min = Infinity;
|
|
@@ -145,5 +148,7 @@ function walkRoutes(graph, byId, walkIndex, startId, endId) {
|
|
|
145
148
|
}
|
|
146
149
|
|
|
147
150
|
export {
|
|
148
|
-
bubblesFromGraph
|
|
151
|
+
bubblesFromGraph,
|
|
152
|
+
walkIndexOf,
|
|
153
|
+
walkRoutes
|
|
149
154
|
};
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canonicalStrand,
|
|
3
|
+
stepName,
|
|
4
|
+
tubeMapReads
|
|
5
|
+
} from "./chunk-CWFNEKST.js";
|
|
6
|
+
import {
|
|
7
|
+
pathCssColor,
|
|
8
|
+
pathGreyCssColor
|
|
9
|
+
} from "./chunk-7GBL7SJY.js";
|
|
10
|
+
import {
|
|
11
|
+
pathOrigin
|
|
12
|
+
} from "./chunk-ST2IBUAN.js";
|
|
13
|
+
import {
|
|
14
|
+
ownStrand,
|
|
15
|
+
strandSides
|
|
16
|
+
} from "./chunk-2CVHXFK7.js";
|
|
17
|
+
import {
|
|
18
|
+
isBackbone
|
|
19
|
+
} from "./chunk-FYQWPEEY.js";
|
|
20
|
+
|
|
21
|
+
// src/layout/tubeMapLayout.ts
|
|
22
|
+
import { forward, isReverse, layoutTubeMap } from "@jbrowse/tubemap-core";
|
|
23
|
+
function pathSteps(graph) {
|
|
24
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
25
|
+
const strands = /* @__PURE__ */ new Map();
|
|
26
|
+
for (const [segment, visits] of graph.pathVisits ?? []) {
|
|
27
|
+
for (const visit of visits) {
|
|
28
|
+
const key = `${visit.path} ${segment}`;
|
|
29
|
+
const list = strands.get(key);
|
|
30
|
+
if (list) {
|
|
31
|
+
list.push(visit.strand);
|
|
32
|
+
} else {
|
|
33
|
+
strands.set(key, [visit.strand]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const seen = /* @__PURE__ */ new Map();
|
|
38
|
+
return (graph.paths ?? []).map((path) => {
|
|
39
|
+
const walk = pathOrigin(path.name).name;
|
|
40
|
+
return path.nodeIds.flatMap((id) => {
|
|
41
|
+
const node = nodeById.get(id);
|
|
42
|
+
if (!node) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
const key = `${walk} ${node.name}`;
|
|
46
|
+
const k = seen.get(key) ?? 0;
|
|
47
|
+
seen.set(key, k + 1);
|
|
48
|
+
return [{ node, strand: strands.get(key)?.[k] ?? canonicalStrand(node) }];
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function tubeMapTracks(graph) {
|
|
53
|
+
const steps = pathSteps(graph);
|
|
54
|
+
const tracks = (graph.paths ?? []).flatMap((path, index) => {
|
|
55
|
+
const sequence = steps[index].map((s) => stepName(s.node, s.strand));
|
|
56
|
+
return sequence.length > 0 ? [
|
|
57
|
+
{
|
|
58
|
+
id: index,
|
|
59
|
+
name: path.name,
|
|
60
|
+
sequence,
|
|
61
|
+
sourceTrackID: 0,
|
|
62
|
+
type: "haplotype"
|
|
63
|
+
}
|
|
64
|
+
] : [];
|
|
65
|
+
});
|
|
66
|
+
const reference = tracks.findIndex(
|
|
67
|
+
(t) => pathOrigin(t.name).name === graph.referencePath
|
|
68
|
+
);
|
|
69
|
+
if (reference > 0) {
|
|
70
|
+
tracks.unshift(...tracks.splice(reference, 1));
|
|
71
|
+
}
|
|
72
|
+
return tracks;
|
|
73
|
+
}
|
|
74
|
+
function tubeMapNodes(graph) {
|
|
75
|
+
return graph.nodes.map((n) => ({
|
|
76
|
+
name: n.id,
|
|
77
|
+
seq: "",
|
|
78
|
+
sequenceLength: Math.max(1, n.length)
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
function hasTubeMapPaths(graph) {
|
|
82
|
+
return (graph.paths?.length ?? 0) > 0;
|
|
83
|
+
}
|
|
84
|
+
var STACK_PX = 600;
|
|
85
|
+
var MIN_TUBE_PX = 3;
|
|
86
|
+
var MAX_TUBE_PX = 15;
|
|
87
|
+
function tubeWidth(trackCount) {
|
|
88
|
+
return Math.max(
|
|
89
|
+
MIN_TUBE_PX,
|
|
90
|
+
Math.min(MAX_TUBE_PX, Math.floor(STACK_PX / Math.max(1, trackCount)))
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
function straightened(first) {
|
|
94
|
+
const flipped = /* @__PURE__ */ new Set();
|
|
95
|
+
const forwards = /* @__PURE__ */ new Set();
|
|
96
|
+
for (const step of first?.sequence ?? []) {
|
|
97
|
+
if (!isReverse(step)) {
|
|
98
|
+
forwards.add(step);
|
|
99
|
+
} else if (!forwards.has(forward(step))) {
|
|
100
|
+
flipped.add(forward(step));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return flipped;
|
|
104
|
+
}
|
|
105
|
+
function mergedRuns(graph, layout, walks) {
|
|
106
|
+
const flipped = straightened(walks[0]);
|
|
107
|
+
const before = /* @__PURE__ */ new Map();
|
|
108
|
+
for (const { sequence, is_secondary } of walks) {
|
|
109
|
+
if (!is_secondary) {
|
|
110
|
+
sequence.forEach((step, i) => {
|
|
111
|
+
const id = forward(step);
|
|
112
|
+
const against = isReverse(step) !== flipped.has(id);
|
|
113
|
+
const neighbour = against ? sequence[i + 1] : sequence[i - 1];
|
|
114
|
+
if (!layout.nodeMap.has(id) && neighbour !== void 0) {
|
|
115
|
+
before.set(id, forward(neighbour));
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const lengthOf = new Map(graph.nodes.map((n) => [n.id, Math.max(1, n.length)]));
|
|
121
|
+
const after = new Map([...before].map(([node, prev]) => [prev, node]));
|
|
122
|
+
const members = /* @__PURE__ */ new Map();
|
|
123
|
+
for (const head of after.keys()) {
|
|
124
|
+
const index = layout.nodeMap.get(head);
|
|
125
|
+
if (index !== void 0) {
|
|
126
|
+
const run = [head];
|
|
127
|
+
for (let n = after.get(head); n !== void 0; n = after.get(n)) {
|
|
128
|
+
run.push(n);
|
|
129
|
+
}
|
|
130
|
+
const bp = run.reduce((sum, id) => sum + (lengthOf.get(id) ?? 0), 0);
|
|
131
|
+
if (bp === layout.nodes[index].sequenceLength) {
|
|
132
|
+
members.set(head, run);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { members, flipped };
|
|
137
|
+
}
|
|
138
|
+
var opposite = { start: "end", end: "start" };
|
|
139
|
+
var otherStrand = { "+": "-", "-": "+" };
|
|
140
|
+
function boxGraph(graph, { members, flipped }) {
|
|
141
|
+
if (members.size === 0) {
|
|
142
|
+
return graph;
|
|
143
|
+
}
|
|
144
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
145
|
+
const boxOf = /* @__PURE__ */ new Map();
|
|
146
|
+
const inner = /* @__PURE__ */ new Set();
|
|
147
|
+
const drawn = (id, side) => flipped.has(id) ? opposite[side] : side;
|
|
148
|
+
for (const [box, run] of members) {
|
|
149
|
+
run.forEach((id, i) => {
|
|
150
|
+
boxOf.set(id, box);
|
|
151
|
+
const next = run[i + 1];
|
|
152
|
+
if (next !== void 0) {
|
|
153
|
+
inner.add(`${id}:${drawn(id, "end")} ${next}:${drawn(next, "start")}`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const absorbed = new Set(
|
|
158
|
+
[...boxOf].filter(([id, box]) => id !== box).map(([id]) => id)
|
|
159
|
+
);
|
|
160
|
+
const nodes = graph.nodes.flatMap((node) => {
|
|
161
|
+
const run = members.get(node.id);
|
|
162
|
+
if (!run) {
|
|
163
|
+
return absorbed.has(node.id) ? [] : [node];
|
|
164
|
+
}
|
|
165
|
+
const parts = run.map((id) => nodeById.get(id));
|
|
166
|
+
const starts = parts.flatMap((p) => p.stable ? [p.stable.start] : []);
|
|
167
|
+
return [
|
|
168
|
+
{
|
|
169
|
+
...node,
|
|
170
|
+
length: parts.reduce((sum, p) => sum + p.length, 0),
|
|
171
|
+
stable: node.stable && { ...node.stable, start: Math.min(...starts) }
|
|
172
|
+
}
|
|
173
|
+
];
|
|
174
|
+
});
|
|
175
|
+
const leaving = (id, box, side) => drawn(box, drawn(id, side)) === "end" ? ownStrand(box) : otherStrand[ownStrand(box)];
|
|
176
|
+
const edges = graph.edges.flatMap((edge) => {
|
|
177
|
+
const sides = strandSides(edge);
|
|
178
|
+
const from = `${edge.from}:${sides.from}`;
|
|
179
|
+
const to = `${edge.to}:${sides.to}`;
|
|
180
|
+
if (inner.has(`${from} ${to}`) || inner.has(`${to} ${from}`)) {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
const fromBox = boxOf.get(edge.from);
|
|
184
|
+
const toBox = boxOf.get(edge.to);
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
...edge,
|
|
188
|
+
...fromBox === void 0 ? {} : {
|
|
189
|
+
from: fromBox,
|
|
190
|
+
fromStrand: leaving(edge.from, fromBox, sides.from)
|
|
191
|
+
},
|
|
192
|
+
...toBox === void 0 ? {} : {
|
|
193
|
+
to: toBox,
|
|
194
|
+
toStrand: otherStrand[leaving(edge.to, toBox, sides.to)]
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
];
|
|
198
|
+
});
|
|
199
|
+
const absorbedNames = new Set(
|
|
200
|
+
[...absorbed].map((id) => nodeById.get(id)?.name ?? id)
|
|
201
|
+
);
|
|
202
|
+
return {
|
|
203
|
+
...graph,
|
|
204
|
+
nodes,
|
|
205
|
+
edges,
|
|
206
|
+
paths: graph.paths?.map((path) => ({
|
|
207
|
+
...path,
|
|
208
|
+
nodeIds: path.nodeIds.filter((id) => !absorbed.has(id))
|
|
209
|
+
})),
|
|
210
|
+
pathVisits: graph.pathVisits && new Map(
|
|
211
|
+
[...graph.pathVisits].filter(([name]) => !absorbedNames.has(name))
|
|
212
|
+
)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function runTubeMap(graph) {
|
|
216
|
+
const paths = graph.paths ?? [];
|
|
217
|
+
const tracks = tubeMapTracks(graph);
|
|
218
|
+
const reads = graph.reads ? tubeMapReads(graph, graph.reads, paths.length) : [];
|
|
219
|
+
const tubeColor = reads.length > 0 ? pathGreyCssColor : pathCssColor;
|
|
220
|
+
const pathColors = paths.map((_, i) => tubeColor(i, paths.length));
|
|
221
|
+
const layout = layoutTubeMap(tubeMapNodes(graph), tracks, reads, {
|
|
222
|
+
nodeWidthOption: "compressed",
|
|
223
|
+
trackWidth: tubeWidth(tracks.length)
|
|
224
|
+
});
|
|
225
|
+
if (!layout) {
|
|
226
|
+
return void 0;
|
|
227
|
+
}
|
|
228
|
+
const runs = mergedRuns(graph, layout, [...tracks, ...reads]);
|
|
229
|
+
return {
|
|
230
|
+
layout,
|
|
231
|
+
pathColors,
|
|
232
|
+
members: runs.members,
|
|
233
|
+
graph: boxGraph(graph, runs)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function drawnNodes(layout) {
|
|
237
|
+
const nodes = [];
|
|
238
|
+
layout.nodes.forEach((node) => {
|
|
239
|
+
if (node.order >= 0) {
|
|
240
|
+
nodes.push(node);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
return nodes;
|
|
244
|
+
}
|
|
245
|
+
function centreline(node, yOffset, x0, x1) {
|
|
246
|
+
const y = node.y + node.contentHeight / 2 - yOffset;
|
|
247
|
+
return [
|
|
248
|
+
{ x: x0, y },
|
|
249
|
+
{ x: x1, y }
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
function extentOf(layout, yOffset) {
|
|
253
|
+
return {
|
|
254
|
+
minY: layout.bounds.minY - yOffset,
|
|
255
|
+
maxY: layout.bounds.maxY - yOffset
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function tubeMapLayout(graph) {
|
|
259
|
+
const run = hasTubeMapPaths(graph) ? runTubeMap(graph) : void 0;
|
|
260
|
+
if (!run) {
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
const { layout, ...drawing } = run;
|
|
264
|
+
const yOffset = layout.bounds.minY;
|
|
265
|
+
const nodePositions = {};
|
|
266
|
+
for (const node of drawnNodes(layout)) {
|
|
267
|
+
nodePositions[node.name] = centreline(
|
|
268
|
+
node,
|
|
269
|
+
yOffset,
|
|
270
|
+
node.x,
|
|
271
|
+
node.x + node.pixelWidth
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const { minY, maxY } = extentOf(layout, yOffset);
|
|
275
|
+
return {
|
|
276
|
+
nodePositions,
|
|
277
|
+
tubeMap: { layout, yOffset, ...drawing },
|
|
278
|
+
extent: {
|
|
279
|
+
minX: layout.bounds.minX,
|
|
280
|
+
maxX: layout.bounds.maxX,
|
|
281
|
+
minY,
|
|
282
|
+
maxY
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function tubeMapColumns(graph, layout) {
|
|
287
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
288
|
+
const byOrder = /* @__PURE__ */ new Map();
|
|
289
|
+
for (const node of drawnNodes(layout)) {
|
|
290
|
+
const list = byOrder.get(node.order);
|
|
291
|
+
if (list) {
|
|
292
|
+
list.push(node);
|
|
293
|
+
} else {
|
|
294
|
+
byOrder.set(node.order, [node]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const columns = [];
|
|
298
|
+
let end = -Infinity;
|
|
299
|
+
for (const order of [...byOrder.keys()].sort((a, b) => a - b)) {
|
|
300
|
+
const nodes = byOrder.get(order);
|
|
301
|
+
let bp0 = Infinity;
|
|
302
|
+
let bp1 = -Infinity;
|
|
303
|
+
for (const node of nodes) {
|
|
304
|
+
const graphNode = nodeById.get(node.name);
|
|
305
|
+
if (graphNode && isBackbone(graphNode)) {
|
|
306
|
+
bp0 = Math.min(bp0, graphNode.stable.start);
|
|
307
|
+
bp1 = Math.max(bp1, graphNode.stable.start + graphNode.length);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (bp0 === Infinity) {
|
|
311
|
+
bp0 = bp1 = end;
|
|
312
|
+
}
|
|
313
|
+
if (bp0 < end) {
|
|
314
|
+
bp0 = end;
|
|
315
|
+
bp1 = Math.max(end, bp1);
|
|
316
|
+
}
|
|
317
|
+
end = Math.max(end, bp1);
|
|
318
|
+
columns.push({
|
|
319
|
+
order,
|
|
320
|
+
x0: Math.min(...nodes.map((n) => n.x)),
|
|
321
|
+
x1: Math.max(...nodes.map((n) => n.x + n.pixelWidth)),
|
|
322
|
+
bp0,
|
|
323
|
+
bp1
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const first = columns.find((c) => c.bp0 !== -Infinity);
|
|
327
|
+
for (const column of columns) {
|
|
328
|
+
if (column.bp0 === -Infinity) {
|
|
329
|
+
column.bp0 = column.bp1 = first?.bp0 ?? 0;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return columns;
|
|
333
|
+
}
|
|
334
|
+
function hasTubeMapBackbone(graph) {
|
|
335
|
+
return hasTubeMapPaths(graph) && graph.nodes.some(isBackbone);
|
|
336
|
+
}
|
|
337
|
+
function tubeMapReferenceLayout(graph) {
|
|
338
|
+
const run = hasTubeMapBackbone(graph) ? runTubeMap(graph) : void 0;
|
|
339
|
+
if (!run) {
|
|
340
|
+
return void 0;
|
|
341
|
+
}
|
|
342
|
+
const { layout, ...drawing } = run;
|
|
343
|
+
const columns = tubeMapColumns(drawing.graph, layout);
|
|
344
|
+
const byOrder = new Map(columns.map((c) => [c.order, c]));
|
|
345
|
+
const yOffset = layout.bounds.minY;
|
|
346
|
+
const nodePositions = {};
|
|
347
|
+
for (const node of drawnNodes(layout)) {
|
|
348
|
+
const column = byOrder.get(node.order);
|
|
349
|
+
nodePositions[node.name] = centreline(node, yOffset, column.bp0, column.bp1);
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
nodePositions,
|
|
353
|
+
tubeMap: { layout, yOffset, columns, ...drawing },
|
|
354
|
+
referenceAxis: true,
|
|
355
|
+
pixelRows: true,
|
|
356
|
+
extent: extentOf(layout, yOffset)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export {
|
|
361
|
+
pathSteps,
|
|
362
|
+
tubeMapTracks,
|
|
363
|
+
hasTubeMapPaths,
|
|
364
|
+
tubeWidth,
|
|
365
|
+
mergedRuns,
|
|
366
|
+
boxGraph,
|
|
367
|
+
tubeMapLayout,
|
|
368
|
+
hasTubeMapBackbone,
|
|
369
|
+
tubeMapReferenceLayout
|
|
370
|
+
};
|