@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.
Files changed (45) hide show
  1. package/dist/chunks/{chunk-MP7LXLWV.js → chunk-3C5UXR2U.js} +1 -1
  2. package/dist/chunks/{chunk-T4SQ5QGM.js → chunk-AMS3MAFP.js} +1 -1
  3. package/dist/chunks/{chunk-DRA7733Q.js → chunk-BPRXQ2SS.js} +8 -3
  4. package/dist/chunks/{chunk-TEDQJPMI.js → chunk-CWFNEKST.js} +22 -7
  5. package/dist/chunks/chunk-INJ2UVOT.js +370 -0
  6. package/dist/chunks/{chunk-KCRL5JB2.js → chunk-LLP3FXPM.js} +31 -10
  7. package/dist/chunks/{chunk-SZNZ2T6D.js → chunk-MAUEPAMG.js} +37 -9
  8. package/dist/chunks/{chunk-V3MD3IZB.js → chunk-MIW3HYPE.js} +13 -4
  9. package/dist/chunks/{chunk-DYYIHN3P.js → chunk-TLQ7F2ZT.js} +4 -4
  10. package/dist/chunks/{chunk-O2ANJQVH.js → chunk-WDTXW7T2.js} +1 -1
  11. package/dist/chunks/{chunk-OWFPAIJI.js → chunk-WEDZ76AK.js} +1 -1
  12. package/dist/chunks/{chunk-3PYY7OIP.js → chunk-WNUZP3WQ.js} +3 -3
  13. package/dist/chunks/{chunk-733L2TS6.js → chunk-XXT2JCAC.js} +10 -7
  14. package/dist/chunks/{chunk-GQ2AOHU4.js → chunk-YOYLCLIG.js} +7 -29
  15. package/dist/cli/figure.js +9 -9
  16. package/dist/figure.js +6 -6
  17. package/dist/index.js +19 -17
  18. package/dist/layout/tubeMapLayout.js +12 -6
  19. package/dist/layout/walkRowDraw.js +1 -1
  20. package/dist/layout/walkRowLayout.js +2 -2
  21. package/dist/layout/walkRows.js +1 -1
  22. package/dist/layout/walkStrip.js +2 -2
  23. package/dist/layoutModes.js +5 -5
  24. package/dist/tubeMap/axis.js +1 -1
  25. package/dist/tubeMap/coarsen.js +11 -23
  26. package/dist/tubeMap/connectors.js +1 -4
  27. package/dist/tubeMap/deviations.js +15 -19
  28. package/dist/tubeMap/draw.js +9 -5
  29. package/dist/tubeMap/genes.js +2 -2
  30. package/dist/tubeMap/mismatches.js +1 -1
  31. package/dist/tubeMap/reads.js +1 -1
  32. package/dist/types/index.d.ts +5 -5
  33. package/dist/types/layout/tubeMapLayout.d.ts +15 -2
  34. package/dist/types/layout/walkRowDraw.d.ts +1 -1
  35. package/dist/types/layout/walkRows.d.ts +5 -0
  36. package/dist/types/layoutModes.d.ts +5 -5
  37. package/dist/types/tubeMap/axis.d.ts +6 -3
  38. package/dist/types/tubeMap/connectors.d.ts +2 -11
  39. package/dist/types/tubeMap/deviations.d.ts +2 -3
  40. package/dist/types/tubeMap/draw.d.ts +4 -1
  41. package/dist/types/tubeMap/mismatches.d.ts +1 -1
  42. package/dist/types/version.d.ts +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +1 -1
  45. package/dist/chunks/chunk-W2I4DOL4.js +0 -230
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  walkRows
3
- } from "./chunk-SZNZ2T6D.js";
3
+ } from "./chunk-MAUEPAMG.js";
4
4
  import {
5
5
  ROW_HEIGHT_PX
6
6
  } from "./chunk-CPMHA4W5.js";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var version = "8.1.0";
2
+ var version = "9.0.0";
3
3
 
4
4
  export {
5
5
  version
@@ -6,17 +6,22 @@ import {
6
6
  } from "./chunk-FYQWPEEY.js";
7
7
 
8
8
  // src/tubeMap/axis.ts
9
- function referenceBoxes(graph, layout) {
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 + node.sequenceLength,
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
- for (let s = 0; s < starts.length; s++) {
35
- const start = starts[s];
36
- const length = lengths[s];
37
- if (start !== void 0 && length !== void 0 && start <= pos2 && (pos2 < start + length || atEnd && pos2 === start + length)) {
38
- return { s, local: pos2 - start, room: start + length - pos2 };
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 = at ? Math.min(left, at.room) : left;
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
- mapping_quality: record.mappingQuality === 255 ? 0 : record.mappingQuality,
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-733L2TS6.js";
3
+ } from "./chunk-XXT2JCAC.js";
4
4
  import {
5
5
  readColor
6
- } from "./chunk-TEDQJPMI.js";
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, track) {
56
+ function shapeOf(d, color, id, haplotype) {
57
57
  const commands = parsePath(d);
58
- return { commands, color, track, ...extentOf(commands) };
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
- track: trackOf(r),
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), trackOf(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), trackOf(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.track !== void 0 && frame.tubeColors?.[item.track] || item.color;
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((id, i) => {
61
- const s = typeof id === "string" ? span.get(id) : void 0;
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 < 0 || i1 < 0) {
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 ids = steps.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
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: i0 < i1 ? ids : ids.reverse(),
79
- complete: true,
80
- from: i0,
81
- to: i1
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
- return `${kb(bp)}${units(bp, unit)}${against}${outside}${complete ? "" : " \xB7 partial walk"}${called}`;
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;