@jbrowse/bandage-core 8.1.0 → 9.0.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/chunks/{chunk-MP7LXLWV.js → chunk-3C5UXR2U.js} +1 -1
- package/dist/chunks/{chunk-T4SQ5QGM.js → chunk-AMS3MAFP.js} +1 -1
- 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-INJ2UVOT.js +370 -0
- package/dist/chunks/{chunk-KCRL5JB2.js → chunk-LLP3FXPM.js} +31 -10
- package/dist/chunks/{chunk-SZNZ2T6D.js → chunk-MAUEPAMG.js} +37 -9
- package/dist/chunks/{chunk-V3MD3IZB.js → chunk-MIW3HYPE.js} +13 -4
- package/dist/chunks/{chunk-DYYIHN3P.js → chunk-TLQ7F2ZT.js} +4 -4
- package/dist/chunks/{chunk-O2ANJQVH.js → chunk-WDTXW7T2.js} +1 -1
- package/dist/chunks/{chunk-OWFPAIJI.js → chunk-WEDZ76AK.js} +1 -1
- package/dist/chunks/{chunk-3PYY7OIP.js → chunk-WNUZP3WQ.js} +3 -3
- package/dist/chunks/{chunk-733L2TS6.js → chunk-XXT2JCAC.js} +10 -7
- package/dist/chunks/{chunk-GQ2AOHU4.js → chunk-YOYLCLIG.js} +7 -29
- package/dist/cli/figure.js +9 -9
- package/dist/figure.js +6 -6
- package/dist/index.js +19 -17
- package/dist/layout/tubeMapLayout.js +12 -6
- package/dist/layout/walkRowDraw.js +1 -1
- package/dist/layout/walkRowLayout.js +2 -2
- package/dist/layout/walkRows.js +1 -1
- package/dist/layout/walkStrip.js +2 -2
- package/dist/layoutModes.js +5 -5
- package/dist/tubeMap/axis.js +1 -1
- package/dist/tubeMap/coarsen.js +11 -23
- package/dist/tubeMap/connectors.js +1 -4
- package/dist/tubeMap/deviations.js +15 -19
- package/dist/tubeMap/draw.js +9 -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/index.d.ts +5 -5
- package/dist/types/layout/tubeMapLayout.d.ts +15 -2
- package/dist/types/layout/walkRowDraw.d.ts +1 -1
- package/dist/types/layout/walkRows.d.ts +5 -0
- package/dist/types/layoutModes.d.ts +5 -5
- package/dist/types/tubeMap/axis.d.ts +6 -3
- package/dist/types/tubeMap/connectors.d.ts +2 -11
- package/dist/types/tubeMap/deviations.d.ts +2 -3
- package/dist/types/tubeMap/draw.d.ts +4 -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/package.json +1 -1
- package/dist/chunks/chunk-W2I4DOL4.js +0 -230
|
@@ -6,17 +6,22 @@ import {
|
|
|
6
6
|
} from "./chunk-FYQWPEEY.js";
|
|
7
7
|
|
|
8
8
|
// src/tubeMap/axis.ts
|
|
9
|
-
function referenceBoxes(
|
|
9
|
+
function referenceBoxes({
|
|
10
|
+
graph,
|
|
11
|
+
layout
|
|
12
|
+
}) {
|
|
10
13
|
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
11
14
|
const byRefName = /* @__PURE__ */ new Map();
|
|
12
|
-
layout.nodes.forEach((node) => {
|
|
15
|
+
layout.nodes.forEach((node, index) => {
|
|
13
16
|
const graphNode = nodeById.get(node.name);
|
|
14
17
|
if (node.order >= 0 && graphNode && isBackbone(graphNode)) {
|
|
15
18
|
const { refName, start } = graphNode.stable;
|
|
16
19
|
const boxes = byRefName.get(refName) ?? byRefName.set(refName, []).get(refName);
|
|
17
20
|
boxes.push({
|
|
21
|
+
name: node.name,
|
|
22
|
+
index,
|
|
18
23
|
bp0: start,
|
|
19
|
-
bp1: start +
|
|
24
|
+
bp1: start + graphNode.length,
|
|
20
25
|
x0: node.x,
|
|
21
26
|
x1: node.x + node.pixelWidth
|
|
22
27
|
});
|
|
@@ -30,16 +30,30 @@ function mismatchesByStep(record, lengths) {
|
|
|
30
30
|
return byStep;
|
|
31
31
|
}
|
|
32
32
|
const starts = stepStarts(lengths, record.pathLength);
|
|
33
|
+
const known = starts.flatMap((start, s) => start === void 0 ? [] : [s]);
|
|
34
|
+
const startOf = (k) => starts[known[k]];
|
|
35
|
+
const endOf = (k) => startOf(k) + lengths[known[k]];
|
|
36
|
+
let first = 0;
|
|
33
37
|
const stepAt = (pos2, atEnd = false) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
while (first < known.length && endOf(first) < pos2) {
|
|
39
|
+
first++;
|
|
40
|
+
}
|
|
41
|
+
for (let k = first; k < known.length && startOf(k) <= pos2; k++) {
|
|
42
|
+
const end = endOf(k);
|
|
43
|
+
if (pos2 < end || atEnd && pos2 === end) {
|
|
44
|
+
return { s: known[k], local: pos2 - startOf(k), room: end - pos2 };
|
|
39
45
|
}
|
|
40
46
|
}
|
|
41
47
|
return void 0;
|
|
42
48
|
};
|
|
49
|
+
const unplacedFrom = (pos2) => {
|
|
50
|
+
for (let k = first; k < known.length; k++) {
|
|
51
|
+
if (startOf(k) > pos2) {
|
|
52
|
+
return startOf(k) - pos2;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Infinity;
|
|
56
|
+
};
|
|
43
57
|
const add = (s, mismatch) => {
|
|
44
58
|
const list = byStep[s];
|
|
45
59
|
const last = list.at(-1);
|
|
@@ -68,7 +82,7 @@ function mismatchesByStep(record, lengths) {
|
|
|
68
82
|
let left = op.length;
|
|
69
83
|
while (left > 0) {
|
|
70
84
|
const at = stepAt(pos);
|
|
71
|
-
const take =
|
|
85
|
+
const take = Math.min(left, at ? at.room : unplacedFrom(pos));
|
|
72
86
|
if (at) {
|
|
73
87
|
add(at.s, { type: "deletion", pos: at.local, length: take });
|
|
74
88
|
}
|
|
@@ -106,7 +120,8 @@ function tubeMapReads(graph, records, firstId) {
|
|
|
106
120
|
})),
|
|
107
121
|
firstNodeOffset: first === 0 ? record.pathStart : 0,
|
|
108
122
|
finalNodeCoverLength: last === record.path.length - 1 ? lastLength - (record.pathLength - record.pathEnd) : lastLength,
|
|
109
|
-
|
|
123
|
+
// 255 is GAF's missing MAPQ
|
|
124
|
+
mapping_quality: record.mappingQuality === 255 ? void 0 : record.mappingQuality,
|
|
110
125
|
is_secondary: record.secondary,
|
|
111
126
|
is_reverse: record.strand === "-"
|
|
112
127
|
});
|
|
@@ -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
|
+
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
tubeMapMismatches
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-XXT2JCAC.js";
|
|
4
4
|
import {
|
|
5
5
|
readColor
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-CWFNEKST.js";
|
|
7
7
|
|
|
8
8
|
// src/tubeMap/draw.ts
|
|
9
9
|
import { curvePaths, nodeOutlinePath } from "@jbrowse/tubemap-core";
|
|
@@ -53,9 +53,9 @@ function extentOf(commands) {
|
|
|
53
53
|
}
|
|
54
54
|
return { x0, x1 };
|
|
55
55
|
}
|
|
56
|
-
function shapeOf(d, color,
|
|
56
|
+
function shapeOf(d, color, id, haplotype) {
|
|
57
57
|
const commands = parsePath(d);
|
|
58
|
-
return { commands, color,
|
|
58
|
+
return { commands, color, id, haplotype, ...extentOf(commands) };
|
|
59
59
|
}
|
|
60
60
|
function trackColors(layout, pathColors) {
|
|
61
61
|
return new Map(
|
|
@@ -67,20 +67,21 @@ function trackColors(layout, pathColors) {
|
|
|
67
67
|
}
|
|
68
68
|
function layerOf(layout, type, colors) {
|
|
69
69
|
const { shapes } = layout;
|
|
70
|
+
const haplotype = type === "haplotype";
|
|
70
71
|
const colorOf = (s) => colors.get(s.id);
|
|
71
|
-
const trackOf = (s) => type === "haplotype" ? s.id : void 0;
|
|
72
72
|
const rects = [...shapes.rectangles, ...shapes.verticalRectangles].filter((r) => r.type === type).map((r) => ({
|
|
73
73
|
color: colorOf(r),
|
|
74
|
-
|
|
74
|
+
id: r.id,
|
|
75
|
+
haplotype,
|
|
75
76
|
x0: r.xStart,
|
|
76
77
|
x1: r.xEnd + 1,
|
|
77
78
|
y0: r.yStart,
|
|
78
79
|
y1: r.yEnd + 1
|
|
79
80
|
}));
|
|
80
81
|
const curves = curvePaths(shapes.curves, type).map(
|
|
81
|
-
(c) => shapeOf(c.path, colorOf(c),
|
|
82
|
+
(c) => shapeOf(c.path, colorOf(c), c.id, haplotype)
|
|
82
83
|
);
|
|
83
|
-
const corners = shapes.corners.filter((c) => c.type === type).map((c) => shapeOf(c.path, colorOf(c),
|
|
84
|
+
const corners = shapes.corners.filter((c) => c.type === type).map((c) => shapeOf(c.path, colorOf(c), c.id, haplotype));
|
|
84
85
|
return { rects, shapes: [...curves, ...corners] };
|
|
85
86
|
}
|
|
86
87
|
function tubeMapPicture({
|
|
@@ -177,7 +178,7 @@ var NODE_TINT_ALPHA = 0.55;
|
|
|
177
178
|
function drawTubeMap(ctx, picture, frame) {
|
|
178
179
|
const { x, y, width } = frame;
|
|
179
180
|
const visible = (item) => x(item.x1) >= 0 && x(item.x0) <= width;
|
|
180
|
-
const colorOf = (item) => item.
|
|
181
|
+
const colorOf = (item) => item.haplotype && frame.tubeColors?.[item.id] || item.color;
|
|
181
182
|
ctx.globalAlpha = 1;
|
|
182
183
|
for (const layer of picture.layers) {
|
|
183
184
|
fillByColor(ctx, layer.rects, visible, colorOf, (r) => {
|
|
@@ -235,6 +236,7 @@ function drawMismatches(ctx, marks, frame) {
|
|
|
235
236
|
}
|
|
236
237
|
ctx.fill();
|
|
237
238
|
ctx.font = `${fontPx}px monospace`;
|
|
239
|
+
ctx.textAlign = "left";
|
|
238
240
|
ctx.textBaseline = "alphabetic";
|
|
239
241
|
ctx.fillStyle = darkMode ? "#ffffff" : "#000000";
|
|
240
242
|
for (const m of marks) {
|
|
@@ -248,6 +250,23 @@ function drawMismatches(ctx, marks, frame) {
|
|
|
248
250
|
}
|
|
249
251
|
}
|
|
250
252
|
}
|
|
253
|
+
function tubeMapTrackAt(picture, { x, y }, sx, sy) {
|
|
254
|
+
for (let l = picture.layers.length - 1; l >= 0; l--) {
|
|
255
|
+
for (const r of picture.layers[l].rects) {
|
|
256
|
+
if (x(r.x0) <= sx && sx <= x(r.x1) && y(r.y0) <= sy && sy <= y(r.y1)) {
|
|
257
|
+
return r.id;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
var INSERTION_SLOP_PX = 4;
|
|
264
|
+
function tubeMapMismatchAt(picture, frame, sx, sy) {
|
|
265
|
+
const { x, y, yScale } = frame;
|
|
266
|
+
return mismatchesLegible(yScale) ? picture.mismatches.find(
|
|
267
|
+
(m) => y(m.y) <= sy && sy <= y(m.y + m.height) && (m.kind === "insertion" ? Math.abs(x(m.x) - sx) <= INSERTION_SLOP_PX : x(m.x0) <= sx && sx <= x(m.x1))
|
|
268
|
+
) : void 0;
|
|
269
|
+
}
|
|
251
270
|
|
|
252
271
|
export {
|
|
253
272
|
parsePath,
|
|
@@ -255,5 +274,7 @@ export {
|
|
|
255
274
|
tubeMapPicture,
|
|
256
275
|
drawTubeMap,
|
|
257
276
|
mismatchesLegible,
|
|
258
|
-
mismatchOnScreen
|
|
277
|
+
mismatchOnScreen,
|
|
278
|
+
tubeMapTrackAt,
|
|
279
|
+
tubeMapMismatchAt
|
|
259
280
|
};
|
|
@@ -53,12 +53,16 @@ function sliceBetween(steps, span, region, flanked = true) {
|
|
|
53
53
|
if (!region || !flanked) {
|
|
54
54
|
return { ids: steps, complete: true, from: -1, to: -1 };
|
|
55
55
|
}
|
|
56
|
+
const spanAt = (i) => {
|
|
57
|
+
const id = steps[i];
|
|
58
|
+
return typeof id === "string" ? span.get(id) : void 0;
|
|
59
|
+
};
|
|
56
60
|
let i0 = -1;
|
|
57
61
|
let i1 = -1;
|
|
58
62
|
let bestEnd = -Infinity;
|
|
59
63
|
let bestStart = Infinity;
|
|
60
|
-
steps.forEach((
|
|
61
|
-
const s =
|
|
64
|
+
steps.forEach((_, i) => {
|
|
65
|
+
const s = spanAt(i);
|
|
62
66
|
if (s) {
|
|
63
67
|
if (s.end <= region.start && s.end > bestEnd) {
|
|
64
68
|
bestEnd = s.end;
|
|
@@ -70,15 +74,38 @@ function sliceBetween(steps, span, region, flanked = true) {
|
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
});
|
|
73
|
-
if (i0
|
|
77
|
+
if (i0 >= 0 && i1 >= 0) {
|
|
78
|
+
const ids2 = steps.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
|
|
79
|
+
return {
|
|
80
|
+
ids: i0 < i1 ? ids2 : ids2.reverse(),
|
|
81
|
+
complete: true,
|
|
82
|
+
from: i0,
|
|
83
|
+
to: i1
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const flank = i0 >= 0 ? i0 : i1;
|
|
87
|
+
let near = -1;
|
|
88
|
+
for (let d = 1; flank >= 0 && near < 0 && d < steps.length; d++) {
|
|
89
|
+
near = [flank - d, flank + d].find((j) => spanAt(j) !== void 0) ?? -1;
|
|
90
|
+
}
|
|
91
|
+
if (near < 0) {
|
|
74
92
|
return { ids: steps, complete: false, from: -1, to: -1 };
|
|
75
93
|
}
|
|
76
|
-
const
|
|
94
|
+
const forward = near > flank === spanAt(near).start > spanAt(flank).start;
|
|
95
|
+
const after = forward === (flank === i0);
|
|
96
|
+
const ids = after ? steps.slice(flank + 1) : steps.slice(0, flank).reverse();
|
|
97
|
+
const last = ids.length > 0 ? ids.length - 1 : -1;
|
|
98
|
+
const end = last < 0 ? spanAt(flank) : spanAt(after ? flank + 1 + last : 0);
|
|
99
|
+
const stop = {
|
|
100
|
+
contigEnds: end !== void 0,
|
|
101
|
+
shortBp: !end ? 0 : Math.max(0, region.start - end.end, end.start - region.end)
|
|
102
|
+
};
|
|
77
103
|
return {
|
|
78
|
-
ids
|
|
79
|
-
complete:
|
|
80
|
-
from:
|
|
81
|
-
to:
|
|
104
|
+
ids,
|
|
105
|
+
complete: false,
|
|
106
|
+
from: flank,
|
|
107
|
+
to: after ? steps.length : -1,
|
|
108
|
+
stop
|
|
82
109
|
};
|
|
83
110
|
}
|
|
84
111
|
function filterSamples(rows, samples) {
|
|
@@ -117,7 +144,7 @@ function walkRows(graph, region, unit) {
|
|
|
117
144
|
const rowOf = (pieces) => {
|
|
118
145
|
const path = pieces[0];
|
|
119
146
|
const ordered = [...pieces].sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
|
|
120
|
-
const { ids, complete, from, to } = sliceBetween(
|
|
147
|
+
const { ids, complete, from, to, stop } = sliceBetween(
|
|
121
148
|
stepsOf(ordered, lengthOf),
|
|
122
149
|
span,
|
|
123
150
|
cut,
|
|
@@ -177,6 +204,7 @@ function walkRows(graph, region, unit) {
|
|
|
177
204
|
offReferenceBp,
|
|
178
205
|
gapBp,
|
|
179
206
|
complete,
|
|
207
|
+
stop,
|
|
180
208
|
runs,
|
|
181
209
|
axis
|
|
182
210
|
};
|
|
@@ -34,15 +34,24 @@ function units(bp, unit) {
|
|
|
34
34
|
return unit ? ` \u2248 ${Math.round(bp / unit)} units` : "";
|
|
35
35
|
}
|
|
36
36
|
function walkRowReadout(row, reference, unit, call) {
|
|
37
|
-
const { bp, gapBp, complete } = row;
|
|
37
|
+
const { bp, gapBp, complete, stop } = row;
|
|
38
38
|
if (!reference) {
|
|
39
39
|
return `${kb(bp)}${units(bp, unit)}`;
|
|
40
40
|
}
|
|
41
|
-
const delta = bp - reference.bp;
|
|
42
|
-
const against = delta === 0 ? "" : ` (${delta > 0 ? "+" : "\u2212"}${kb(Math.abs(delta))})`;
|
|
43
41
|
const outside = gapBp > 0 ? ` \xB7 ${kb(gapBp)} outside the cut` : "";
|
|
44
42
|
const called = call ? ` \xB7 called ${kb(call.bp)}${call.spanningReads === 0 ? " \xB7 no spanning read" : ""}` : "";
|
|
45
|
-
|
|
43
|
+
if (complete) {
|
|
44
|
+
const delta = bp - reference.bp;
|
|
45
|
+
const against = delta === 0 ? "" : ` (${delta > 0 ? "+" : "\u2212"}${kb(Math.abs(delta))})`;
|
|
46
|
+
return `${kb(bp)}${units(bp, unit)}${against}${outside}${called}`;
|
|
47
|
+
}
|
|
48
|
+
const region = unit ? "repeat" : "window";
|
|
49
|
+
if (stop?.contigEnds && bp === 0) {
|
|
50
|
+
const where = stop.shortBp > 0 ? `${kb(stop.shortBp)} before` : "at";
|
|
51
|
+
return `contig ends ${where} the ${region}${called}`;
|
|
52
|
+
}
|
|
53
|
+
const why = !stop ? "partial walk" : stop.contigEnds ? `contig ends inside the ${region}` : "does not rejoin the reference within the cut";
|
|
54
|
+
return `\u2265 ${kb(bp)}${units(bp, unit)} \xB7 ${why}${outside}${called}`;
|
|
46
55
|
}
|
|
47
56
|
function readoutPlacement(text, endX, width) {
|
|
48
57
|
const x = endX + 6;
|