@jbrowse/bandage-core 4.2.0 → 4.3.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.
@@ -69,6 +69,13 @@ function layoutScaling(graph, spread = PROPORTIONAL_SPREAD) {
69
69
  }
70
70
  };
71
71
  }
72
+ function drawnLengthOf(graph, spread = PROPORTIONAL_SPREAD) {
73
+ if (isProportional(spread.law)) {
74
+ const opts = bandageAutoScale(graph, spread.minNodeLength);
75
+ return (bp) => drawnNodeLength(opts, bp);
76
+ }
77
+ return drawnLengthFor(graph, spread.law);
78
+ }
72
79
 
73
80
  // src/bubbleSpreads.ts
74
81
  var BUBBLE_SPREADS = [
@@ -110,26 +117,8 @@ function spreadFor(spread) {
110
117
  };
111
118
  }
112
119
 
113
- // src/pansn.ts
114
- var SEP = "#";
115
- function panSNSample(refName) {
116
- return refName.split(SEP)[0];
117
- }
118
- var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
119
- function isGenericSample(sample) {
120
- return GENERIC_SAMPLES.has(sample);
121
- }
122
- function panSNHaplotype(refName) {
123
- const parts = refName.split(SEP);
124
- return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : void 0;
125
- }
126
- function panSNContig(refName) {
127
- const parts = refName.split(SEP);
128
- return parts.length >= 3 ? parts.slice(2).join(SEP) : parts.length === 2 ? parts[1] : refName;
129
- }
130
- function panSNMatchesPrefix(refName, prefix) {
131
- return prefix !== void 0 && (refName === prefix || refName.startsWith(prefix + SEP));
132
- }
120
+ // src/layout/rowSpacing.ts
121
+ var ROW_HEIGHT_PX = 20;
133
122
 
134
123
  // src/anchoredNodes.ts
135
124
  var REFERENCE_RANK = 0;
@@ -184,1366 +173,1561 @@ function referenceSpan(backbone, region) {
184
173
  return region && region.end > region.start ? region.end - region.start : backboneSpan(backbone);
185
174
  }
186
175
 
187
- // src/gfa-core/gfaParser.ts
188
- function parseTag(tag, tags) {
189
- const nameEnd = tag.indexOf(":");
190
- const typeEnd = tag.indexOf(":", nameEnd + 1);
191
- if (nameEnd !== -1 && typeEnd !== -1) {
192
- const name = tag.slice(0, nameEnd);
193
- const type = tag.slice(nameEnd + 1, typeEnd);
194
- const val = tag.slice(typeEnd + 1);
195
- if (type === "i" || type === "f") {
196
- tags[name] = +val;
197
- } else if (type === "Z") {
198
- tags[name] = val;
199
- }
200
- }
176
+ // src/util/geometry.ts
177
+ function svgPath(points) {
178
+ const round = (v) => Math.round(v * 100) / 100;
179
+ return points.map((p, i) => `${i ? "L" : "M"}${round(p.x)},${round(p.y)}`).join("");
201
180
  }
202
- function parseTags(fields) {
203
- const tags = {};
204
- for (const field of fields) {
205
- parseTag(field, tags);
181
+ function projectLine(x1, y1, x2, y2, distance) {
182
+ const d = Math.hypot(y2 - y1, x2 - x1);
183
+ if (d === 0) {
184
+ return [x2, y2];
206
185
  }
207
- return tags;
186
+ const vx = (x2 - x1) / d;
187
+ const vy = (y2 - y1) / d;
188
+ return [x2 + distance * vx, y2 + distance * vy];
208
189
  }
209
- function stableCoordinate(node) {
210
- const { SN, SO, SR } = node.tags;
211
- return typeof SN === "string" && typeof SO === "number" && typeof SR === "number" ? { refName: SN, start: SO, rank: SR } : void 0;
190
+ function curvePointAt(c, t) {
191
+ const u = 1 - t;
192
+ const a = u * u * u;
193
+ const b = 3 * u * u * t;
194
+ const d = 3 * u * t * t;
195
+ const e = t * t * t;
196
+ return {
197
+ x: a * c.x0 + b * c.cx0 + d * c.cx1 + e * c.x1,
198
+ y: a * c.y0 + b * c.cy0 + d * c.cy1 + e * c.y1
199
+ };
212
200
  }
213
- var GT = 62;
214
- var LT = 60;
215
- function parseWalkBody(body) {
216
- const segments = [];
217
- let strand = "+";
218
- let start = 0;
219
- function push(end) {
220
- if (end > start) {
221
- segments.push({ id: body.slice(start, end), strand });
222
- }
223
- }
224
- for (let i = 0; i < body.length; i++) {
225
- const ch = body.charCodeAt(i);
226
- if (ch === GT || ch === LT) {
227
- push(i);
228
- strand = ch === GT ? "+" : "-";
229
- start = i + 1;
230
- }
201
+ function curveMidpoint(curves) {
202
+ const mid = curves[Math.floor(curves.length / 2)];
203
+ return mid ? curvePointAt(mid, 0.5) : void 0;
204
+ }
205
+ function curveBounds(curves) {
206
+ let minX = Infinity;
207
+ let minY = Infinity;
208
+ let maxX = -Infinity;
209
+ let maxY = -Infinity;
210
+ for (const c of curves) {
211
+ minX = Math.min(minX, c.x0, c.cx0, c.cx1, c.x1);
212
+ maxX = Math.max(maxX, c.x0, c.cx0, c.cx1, c.x1);
213
+ minY = Math.min(minY, c.y0, c.cy0, c.cy1, c.y1);
214
+ maxY = Math.max(maxY, c.y0, c.cy0, c.cy1, c.y1);
231
215
  }
232
- push(body.length);
233
- return segments;
216
+ return { minX, minY, maxX, maxY };
234
217
  }
235
- function parseGFA(file) {
236
- const graph = {
237
- nodes: [],
238
- links: [],
239
- paths: [],
240
- walks: [],
241
- header: [],
242
- id: ""
218
+ function subCurve(c, t0, t1) {
219
+ const p0 = curvePointAt(c, t0);
220
+ const p1 = curvePointAt(c, t1);
221
+ const dt = (t1 - t0) / 3;
222
+ const d0 = curveTangentAt(c, t0);
223
+ const d1 = curveTangentAt(c, t1);
224
+ return {
225
+ x0: p0.x,
226
+ y0: p0.y,
227
+ cx0: p0.x + d0.x * dt,
228
+ cy0: p0.y + d0.y * dt,
229
+ cx1: p1.x - d1.x * dt,
230
+ cy1: p1.y - d1.y * dt,
231
+ x1: p1.x,
232
+ y1: p1.y
243
233
  };
244
- for (const rawLine of file.split("\n")) {
245
- const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
246
- if (line.startsWith("H")) {
247
- const [, ...rest] = line.split(" ");
248
- graph.header.push(parseTags(rest));
249
- } else if (line.startsWith("S")) {
250
- const [, name, ...rest] = line.split(" ");
251
- let len;
252
- let seq;
253
- let tagfields;
254
- let gfa1 = false;
255
- const first = rest[0] ?? "*";
256
- if (/^\d+$/.test(first)) {
257
- len = +first;
258
- seq = rest[1] ?? "*";
259
- tagfields = rest.slice(2);
260
- } else {
261
- gfa1 = true;
262
- seq = first;
263
- len = seq === "*" ? 0 : seq.length;
264
- tagfields = rest.slice(1);
265
- }
266
- const tags = parseTags(tagfields);
267
- if (gfa1 && "LN" in tags) {
268
- len = +tags.LN;
269
- }
270
- graph.nodes.push({ id: name, length: len, sequence: seq, tags });
271
- } else if (line.startsWith("E")) {
272
- const [, , source, target, , , , , cigar, ...rest] = line.split(" ");
273
- if (source && target) {
274
- graph.links.push({
275
- source: source.slice(0, -1),
276
- target: target.slice(0, -1),
277
- strand1: source.at(-1),
278
- strand2: target.at(-1),
279
- cigar: cigar ?? "*",
280
- tags: parseTags(rest)
281
- });
282
- }
283
- } else if (line.startsWith("L")) {
284
- const [, source, strand1, target, strand2, cigar, ...rest] = line.split(" ");
285
- if (source && target && strand2) {
286
- graph.links.push({
287
- source,
288
- target,
289
- strand1,
290
- strand2,
291
- cigar: cigar ?? "*",
292
- tags: parseTags(rest)
293
- });
294
- }
295
- } else if (line.startsWith("W")) {
296
- const [, sample, hap, contig, start, end, body, ...rest] = line.split(" ");
297
- if (sample && hap && contig && start && end && body) {
298
- graph.walks.push({
299
- sample,
300
- haplotype: +hap,
301
- contig,
302
- start: start === "*" ? -1 : +start,
303
- end: end === "*" ? -1 : +end,
304
- segments: parseWalkBody(body),
305
- tags: parseTags(rest)
306
- });
307
- }
308
- } else if (line.startsWith("P")) {
309
- const [, name, path, ...rest] = line.split(" ");
310
- if (name && path) {
311
- graph.paths.push({ name, path, rest });
312
- }
313
- }
314
- }
315
- return graph;
316
234
  }
317
-
318
- // src/pathAnchoring.ts
319
- var RANGE_SUFFIX = /:(\d+)-\d+$/;
320
- function pathOrigin(pathName) {
321
- const match = RANGE_SUFFIX.exec(pathName);
322
- return match ? { name: pathName.slice(0, match.index), start: +match[1] } : { name: pathName, start: 0 };
235
+ function curveTangentAt(c, t) {
236
+ const u = 1 - t;
237
+ const a = 3 * u * u;
238
+ const b = 6 * u * t;
239
+ const d = 3 * t * t;
240
+ return {
241
+ x: a * (c.cx0 - c.x0) + b * (c.cx1 - c.cx0) + d * (c.x1 - c.cx1),
242
+ y: a * (c.cy0 - c.y0) + b * (c.cy1 - c.cy0) + d * (c.y1 - c.cy1)
243
+ };
323
244
  }
324
- function surveyPaths(paths, lengthOf) {
325
- const anchorPaths = [];
326
- const pathVisits = /* @__PURE__ */ new Map();
327
- for (const path of paths) {
328
- const sample = panSNSample(path.name);
329
- let pos = path.start;
330
- for (const step of path.steps) {
331
- const visit = {
332
- path: path.name,
333
- sample,
334
- start: pos,
335
- strand: step.strand
336
- };
337
- const existing = pathVisits.get(step.id);
338
- if (existing) {
339
- existing.push(visit);
340
- } else {
341
- pathVisits.set(step.id, [visit]);
245
+ function dashCurves(curves, dashWorld) {
246
+ const lengths = curves.map(
247
+ (c) => Math.hypot(c.cx0 - c.x0, c.cy0 - c.y0) + Math.hypot(c.cx1 - c.cx0, c.cy1 - c.cy0) + Math.hypot(c.x1 - c.cx1, c.y1 - c.cy1)
248
+ );
249
+ const total = lengths.reduce((a, b) => a + b, 0);
250
+ const spans = Math.max(3, Math.round(total / dashWorld) | 1);
251
+ const out = [];
252
+ for (let i = 0; i < spans; i += 2) {
253
+ const from = i / spans * total;
254
+ const to = (i + 1) / spans * total;
255
+ const dash = [];
256
+ let start = 0;
257
+ curves.forEach((c, k) => {
258
+ const length = lengths[k];
259
+ const end = start + length;
260
+ if (length > 0 && end > from && start < to) {
261
+ dash.push(
262
+ subCurve(
263
+ c,
264
+ Math.max(0, (from - start) / length),
265
+ Math.min(1, (to - start) / length)
266
+ )
267
+ );
342
268
  }
343
- pos += lengthOf(step.id);
344
- }
345
- anchorPaths.push({
346
- name: path.name,
347
- sample,
348
- start: path.start,
349
- length: pos - path.start
269
+ start = end;
350
270
  });
271
+ if (dash.length) {
272
+ out.push(dash);
273
+ }
351
274
  }
352
- return { anchorPaths, pathVisits };
353
- }
354
- function chooseReferencePath(anchorPaths, preferred) {
355
- const named = anchorPaths.find(
356
- (p) => p.sample === preferred || p.name === preferred
357
- );
358
- return named ?? anchorPaths[0];
275
+ return out;
359
276
  }
360
- var OFF_REFERENCE_RANK = 1;
361
- function anchorNode(node, visits, reference) {
362
- let anchored = node;
363
- if (visits && visits.length > 0) {
364
- const anchor = visits.find((v) => v.path === reference) ?? visits[0];
365
- anchored = {
366
- ...node,
367
- stable: {
368
- refName: anchor.path,
369
- start: anchor.start,
370
- rank: anchor.path === reference ? REFERENCE_RANK : OFF_REFERENCE_RANK,
371
- strand: anchor.strand
372
- },
373
- samples: [...new Set(visits.map((v) => v.sample))].sort()
277
+ function translateCurves(curves, dx, dy) {
278
+ const out = new Array(curves.length);
279
+ for (let i = 0; i < curves.length; i++) {
280
+ const c = curves[i];
281
+ out[i] = {
282
+ x0: c.x0 + dx,
283
+ y0: c.y0 + dy,
284
+ cx0: c.cx0 + dx,
285
+ cy0: c.cy0 + dy,
286
+ cx1: c.cx1 + dx,
287
+ cy1: c.cy1 + dy,
288
+ x1: c.x1 + dx,
289
+ y1: c.y1 + dy
374
290
  };
375
291
  }
376
- return anchored;
377
- }
378
- function anchorFromPaths(graph, preferred) {
379
- const { anchorPaths, pathVisits } = graph;
380
- const reference = anchorPaths && pathVisits ? chooseReferencePath(anchorPaths, preferred) : void 0;
381
- return reference && pathVisits ? {
382
- ...graph,
383
- nodes: graph.nodes.map(
384
- (node) => anchorNode(node, pathVisits.get(node.name), reference.name)
385
- ),
386
- anchoredBy: "paths",
387
- referencePath: reference.name
388
- } : graph;
292
+ return out;
389
293
  }
390
- function anchorGraph(graph, preferred) {
391
- return graph.anchoredBy === "tags" ? graph : anchorFromPaths(graph, preferred);
294
+ function attachment(segments, side) {
295
+ const last = segments.length - 1;
296
+ return side === "end" ? { at: segments[last], inward: segments[last - 1] } : { at: segments[0], inward: segments[1] };
392
297
  }
393
- function trimOrigins(graph, kept) {
394
- if (!graph.anchorPaths) {
395
- return void 0;
298
+ var TANGENT_SPAN_FACTOR = 0.5;
299
+ var MAX_TANGENT = 80;
300
+ function spanOf(segments) {
301
+ let min = Infinity;
302
+ let max = -Infinity;
303
+ for (const s of segments) {
304
+ min = Math.min(min, s.x);
305
+ max = Math.max(max, s.x);
396
306
  }
397
- const lengthOf = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
398
- const bp = (ids, end = ids.length) => {
399
- let sum = 0;
400
- for (let i = 0; i < end; i++) {
401
- sum += lengthOf.get(ids[i]) ?? 0;
402
- }
403
- return sum;
307
+ return { min, max };
308
+ }
309
+ function distanceToSpan(x, span) {
310
+ return Math.max(0, span.min - x, x - span.max);
311
+ }
312
+ function facingSide(segments, other, tieBreak) {
313
+ const startGap = distanceToSpan(segments[0].x, other);
314
+ const endGap = distanceToSpan(segments[segments.length - 1].x, other);
315
+ return startGap === endGap ? tieBreak : startGap < endGap ? "start" : "end";
316
+ }
317
+ function facingSides(fromSegments, toSegments) {
318
+ return {
319
+ from: facingSide(fromSegments, spanOf(toSegments), "end"),
320
+ to: facingSide(toSegments, spanOf(fromSegments), "start")
404
321
  };
405
- const byOrigin = /* @__PURE__ */ new Map();
406
- for (const path of graph.paths ?? []) {
407
- const origin = pathOrigin(path.name).name;
408
- byOrigin.set(origin, [...byOrigin.get(origin) ?? [], path]);
409
- }
410
- return graph.anchorPaths.map((o) => {
411
- const pieces = byOrigin.get(o.name);
412
- const path = pieces?.length === 1 ? pieces[0] : void 0;
413
- const stretch = path && kept.get(path.name);
414
- return path && stretch ? {
415
- ...o,
416
- start: o.start + bp(path.nodeIds, stretch.first),
417
- length: bp(stretch.nodeIds)
418
- } : o;
419
- });
420
322
  }
421
-
422
- // src/gfa/gfaConverter.ts
423
- function surveySegments(gfaGraph) {
424
- const canonical = /* @__PURE__ */ new Map();
425
- const traversals = /* @__PURE__ */ new Map();
426
- function claim(id, strand) {
427
- if (!canonical.has(id)) {
428
- canonical.set(id, strand);
429
- }
430
- }
431
- function traverse(id, strand) {
432
- claim(id, strand);
433
- traversals.set(id, (traversals.get(id) ?? 0) + 1);
434
- }
435
- for (const link of gfaGraph.links) {
436
- claim(link.source, link.strand1 === "-" ? "-" : "+");
437
- claim(link.target, link.strand2 === "-" ? "-" : "+");
438
- }
439
- for (const gfaPath of gfaGraph.paths) {
440
- for (const segment of gfaPath.path.split(",")) {
441
- traverse(segment.slice(0, -1), segment.endsWith("-") ? "-" : "+");
442
- }
443
- }
444
- for (const walk of gfaGraph.walks) {
445
- for (const seg of walk.segments) {
446
- traverse(seg.id, seg.strand === "-" ? "-" : "+");
323
+ var APEX_FRACTION = 0.75;
324
+ var BOW_CLEARANCE = 1.15;
325
+ var ALONG_FRACTION = 0.35;
326
+ var MAX_ROW_BOW_PX = 3 * ROW_HEIGHT_PX;
327
+ function bowAround(p1x, p1y, p2x, p2y, toward, maxBow) {
328
+ const chord = Math.hypot(p2x - p1x, p2y - p1y);
329
+ const ux = chord === 0 ? 1 : (p2x - p1x) / chord;
330
+ const uy = chord === 0 ? 0 : (p2y - p1y) / chord;
331
+ let reach = 0;
332
+ let minAlong = Infinity;
333
+ let maxAlong = -Infinity;
334
+ for (const p of toward) {
335
+ const dx = p.x - p1x;
336
+ const dy = p.y - p1y;
337
+ const perp = dx * -uy + dy * ux;
338
+ if (Math.abs(perp) > Math.abs(reach)) {
339
+ reach = perp;
447
340
  }
341
+ const along = dx * ux + dy * uy;
342
+ minAlong = Math.min(minAlong, along);
343
+ maxAlong = Math.max(maxAlong, along);
448
344
  }
449
- return { canonical, traversals };
345
+ const span = toward.length ? maxAlong - minAlong : 0;
346
+ const uncapped = Math.max(
347
+ Math.abs(reach) * BOW_CLEARANCE / APEX_FRACTION,
348
+ span * ALONG_FRACTION
349
+ );
350
+ const size = maxBow === void 0 ? uncapped : Math.min(uncapped, maxBow);
351
+ return reach < 0 ? -size : size;
450
352
  }
451
- var walkStart = (w) => w.start === -1 ? 0 : w.start;
452
- function anchorablePaths(gfaGraph) {
453
- return [
454
- ...gfaGraph.paths.map((p) => ({
455
- ...pathOrigin(p.name),
456
- steps: p.path.split(",").map((segment) => ({
457
- id: segment.slice(0, -1),
458
- strand: segment.endsWith("-") ? "-" : "+"
459
- }))
460
- })),
461
- ...gfaGraph.walks.map((w) => ({
462
- name: `${w.sample}#${w.haplotype}#${w.contig}`,
463
- start: walkStart(w),
464
- steps: w.segments.map((s) => ({
465
- id: s.id,
466
- strand: s.strand === "-" ? "-" : "+"
467
- }))
468
- }))
469
- ];
353
+ function yToXOf({ scaleX, scaleY }) {
354
+ return scaleY / scaleX;
470
355
  }
471
- function carriedSamples(gfaNode) {
472
- const sm = gfaNode.tags.SM;
473
- if (typeof sm !== "string") {
474
- return void 0;
356
+ var PATH_RIBBON_SPACING = 3;
357
+ function drawnDirection(a, b, yToX) {
358
+ const dx = b.x - a.x;
359
+ const dy = (b.y - a.y) * yToX;
360
+ const len = Math.hypot(dx, dy);
361
+ return len === 0 ? void 0 : { x: dx / len, y: dy / len };
362
+ }
363
+ function pathRibbonOffsets(fromSegments, toSegments, count, axis, spacing = PATH_RIBBON_SPACING) {
364
+ const yToX = yToXOf(axis);
365
+ const dir = drawnDirection(
366
+ fromSegments[fromSegments.length - 1],
367
+ toSegments[0],
368
+ yToX
369
+ ) ?? drawnDirection(fromSegments[0], fromSegments.at(-1), yToX) ?? drawnDirection(toSegments[0], toSegments.at(-1), yToX) ?? { x: 1, y: 0 };
370
+ const offsets = [];
371
+ for (let i = 0; i < count; i++) {
372
+ const distance = (i - (count - 1) / 2) * spacing;
373
+ offsets.push({ x: -dir.y * distance, y: dir.x * distance / yToX });
475
374
  }
476
- const samples = sm.split(",").map((s) => s.trim()).filter(Boolean);
477
- return samples.length > 0 ? samples : void 0;
375
+ return offsets;
478
376
  }
479
- var positive = (v) => typeof v === "number" && v > 0 ? v : void 0;
480
- function statedDepth({ tags, length }) {
481
- const count = positive(tags.RC) ?? positive(tags.FC) ?? positive(tags.KC);
482
- return positive(tags.dp) ?? positive(tags.DP) ?? (count !== void 0 && length > 0 ? count / length : void 0);
377
+ function scaleYOf(points, yToX) {
378
+ return points.map((p) => ({ x: p.x, y: p.y * yToX }));
483
379
  }
484
- function makeNode(gfaNode, strand, depth) {
485
- return {
486
- id: `${gfaNode.id}${strand}`,
487
- name: gfaNode.id,
488
- length: gfaNode.length,
489
- depth,
490
- stable: stableCoordinate(gfaNode),
491
- samples: carriedSamples(gfaNode)
492
- };
380
+ function unscaleYOf(curves, yToX) {
381
+ for (const c of curves) {
382
+ c.y0 /= yToX;
383
+ c.cy0 /= yToX;
384
+ c.cy1 /= yToX;
385
+ c.y1 /= yToX;
386
+ }
387
+ return curves;
493
388
  }
494
- function convertGFAToGraph(gfaGraph, name = "Imported GFA") {
495
- const nodes = [];
496
- const edges = [];
497
- const { canonical, traversals } = surveySegments(gfaGraph);
498
- const nodeId = (segmentId) => `${segmentId}${canonical.get(segmentId) ?? "+"}`;
499
- for (const gfaNode of gfaGraph.nodes) {
500
- const depth = statedDepth(gfaNode) ?? Math.max(traversals.get(gfaNode.id) ?? 1, 1);
501
- nodes.push(makeNode(gfaNode, canonical.get(gfaNode.id) ?? "+", depth));
389
+ var SELF_LOOP_MIN_PX = 12;
390
+ var SELF_LOOP_MAX_PX = 40;
391
+ function selfLoopReach(segments, scale) {
392
+ let length = 0;
393
+ for (let i = 1; i < segments.length; i++) {
394
+ length += Math.hypot(
395
+ segments[i].x - segments[i - 1].x,
396
+ segments[i].y - segments[i - 1].y
397
+ );
502
398
  }
503
- for (const link of gfaGraph.links) {
504
- edges.push({
505
- from: nodeId(link.source),
506
- to: nodeId(link.target),
507
- fromStrand: link.strand1 === "-" ? "-" : "+",
508
- toStrand: link.strand2 === "-" ? "-" : "+"
509
- });
399
+ const nodePx = length * scale;
400
+ const reachPx = Math.min(
401
+ SELF_LOOP_MAX_PX,
402
+ Math.max(SELF_LOOP_MIN_PX, nodePx / 2)
403
+ );
404
+ return reachPx / Math.max(scale, Number.EPSILON);
405
+ }
406
+ function selfLinkOf(edge) {
407
+ if (edge.from !== edge.to) {
408
+ return false;
510
409
  }
511
- const paths = [];
512
- const edgeToPathsMap = /* @__PURE__ */ new Map();
513
- const edgeKey = (a, b) => [a, b].sort().join("--");
514
- function recordPathEdges(nodeIds, name2) {
515
- for (let i = 0; i < nodeIds.length - 1; i++) {
516
- const key = edgeKey(nodeIds[i], nodeIds[i + 1]);
517
- if (!edgeToPathsMap.has(key)) {
518
- edgeToPathsMap.set(key, /* @__PURE__ */ new Set());
519
- }
520
- edgeToPathsMap.get(key).add(name2);
521
- }
522
- }
523
- for (const gfaPath of gfaGraph.paths) {
524
- const nodeIds = gfaPath.path.split(",").map((segment) => nodeId(segment.slice(0, -1)));
525
- paths.push({
526
- name: gfaPath.name,
527
- nodeIds,
528
- start: pathOrigin(gfaPath.name).start
529
- });
530
- recordPathEdges(nodeIds, gfaPath.name);
410
+ return edge.fromStrand && edge.toStrand && edge.fromStrand !== edge.toStrand ? edge.fromStrand === "+" ? "end" : "start" : true;
411
+ }
412
+ function hairpinCurves(attach, reach, offsetX, offsetY) {
413
+ const { at, inward } = attach;
414
+ const dx = inward ? at.x - inward.x : 1;
415
+ const dy = inward ? at.y - inward.y : 0;
416
+ const len = Math.hypot(dx, dy) || 1;
417
+ const ux = dx / len;
418
+ const uy = dy / len;
419
+ const px = at.x + offsetX;
420
+ const py = at.y + offsetY;
421
+ const nearX = px + ux * reach / 3;
422
+ const nearY = py + uy * reach / 3;
423
+ const apexX = px + ux * reach;
424
+ const apexY = py + uy * reach;
425
+ const sideX = -uy * reach / 2;
426
+ const sideY = ux * reach / 2;
427
+ return [
428
+ {
429
+ x0: px,
430
+ y0: py,
431
+ cx0: nearX,
432
+ cy0: nearY,
433
+ cx1: apexX + sideX,
434
+ cy1: apexY + sideY,
435
+ x1: apexX,
436
+ y1: apexY
437
+ },
438
+ {
439
+ x0: apexX,
440
+ y0: apexY,
441
+ cx0: apexX - sideX,
442
+ cy0: apexY - sideY,
443
+ cx1: nearX,
444
+ cy1: nearY,
445
+ x1: px,
446
+ y1: py
447
+ }
448
+ ];
449
+ }
450
+ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY, axis, bypassed = [], maxBowIn) {
451
+ const scale = axis.scaleX;
452
+ const yToX = yToXOf(axis);
453
+ const maxBow = maxBowIn ?? (axis.pixelRows ? MAX_ROW_BOW_PX : void 0);
454
+ if (yToX !== 1) {
455
+ return unscaleYOf(
456
+ computeEdgeCurves(
457
+ scaleYOf(fromSegments, yToX),
458
+ scaleYOf(toSegments, yToX),
459
+ selfLink,
460
+ offsetX,
461
+ offsetY * yToX,
462
+ { scaleX: scale, scaleY: scale },
463
+ scaleYOf(bypassed, yToX),
464
+ // the cap is a y measure and the body works in x units, so it converts
465
+ // with everything else that crosses this boundary
466
+ maxBow === void 0 ? void 0 : maxBow * yToX
467
+ ),
468
+ yToX
469
+ );
531
470
  }
532
- for (const walk of gfaGraph.walks) {
533
- const nodeIds = walk.segments.map((seg) => nodeId(seg.id));
534
- const name2 = `${walk.sample}#${walk.haplotype}#${walk.contig}`;
535
- paths.push({
536
- name: name2,
537
- nodeIds,
538
- start: walkStart(walk),
539
- sample: walk.sample,
540
- haplotype: walk.haplotype,
541
- contig: walk.contig
542
- });
543
- recordPathEdges(nodeIds, name2);
471
+ if (typeof selfLink === "string") {
472
+ return hairpinCurves(
473
+ attachment(fromSegments, selfLink),
474
+ selfLoopReach(fromSegments, scale),
475
+ offsetX,
476
+ offsetY
477
+ );
544
478
  }
545
- for (const edge of edges) {
546
- const pathIds = edgeToPathsMap.get(edgeKey(edge.from, edge.to));
547
- if (pathIds && pathIds.size > 0) {
548
- edge.pathIds = [...pathIds];
479
+ const sides = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
480
+ const fromAttach = attachment(fromSegments, sides.from);
481
+ const toAttach = attachment(toSegments, sides.to);
482
+ const fromEnd = fromAttach.at;
483
+ const toStart = toAttach.at;
484
+ const p1x = fromEnd.x + offsetX;
485
+ const p1y = fromEnd.y + offsetY;
486
+ const p2x = toStart.x + offsetX;
487
+ const p2y = toStart.y + offsetY;
488
+ if (selfLink) {
489
+ let segDirX = 1;
490
+ let segDirY = 0;
491
+ if (fromAttach.inward) {
492
+ const dx = fromEnd.x - fromAttach.inward.x;
493
+ const dy = fromEnd.y - fromAttach.inward.y;
494
+ const len = Math.hypot(dx, dy);
495
+ if (len > 0) {
496
+ segDirX = dx / len;
497
+ segDirY = dy / len;
498
+ }
549
499
  }
500
+ const ext = selfLoopReach(fromSegments, scale);
501
+ const perpX = -segDirY;
502
+ const perpY = segDirX;
503
+ const midX = (fromEnd.x + toStart.x) / 2 + offsetX + perpX * ext;
504
+ const midY = (fromEnd.y + toStart.y) / 2 + offsetY + perpY * ext;
505
+ const cp1x = p1x + segDirX * ext;
506
+ const cp1y = p1y + segDirY * ext;
507
+ const cp2x = p2x - segDirX * ext;
508
+ const cp2y = p2y - segDirY * ext;
509
+ return [
510
+ {
511
+ x0: p1x,
512
+ y0: p1y,
513
+ cx0: cp1x,
514
+ cy0: cp1y,
515
+ cx1: cp1x + perpX * ext,
516
+ cy1: cp1y + perpY * ext,
517
+ x1: midX,
518
+ y1: midY
519
+ },
520
+ {
521
+ x0: midX,
522
+ y0: midY,
523
+ cx0: cp2x + perpX * ext,
524
+ cy0: cp2y + perpY * ext,
525
+ cx1: cp2x,
526
+ cy1: cp2y,
527
+ x1: p2x,
528
+ y1: p2y
529
+ }
530
+ ];
531
+ } else {
532
+ const fromPrev = fromAttach.inward ?? {
533
+ x: fromEnd.x - (toStart.x - fromEnd.x),
534
+ y: fromEnd.y - (toStart.y - fromEnd.y)
535
+ };
536
+ const toNext = toAttach.inward ?? {
537
+ x: toStart.x - (fromEnd.x - toStart.x),
538
+ y: toStart.y - (fromEnd.y - toStart.y)
539
+ };
540
+ const dist = Math.hypot(p2x - p1x, p2y - p1y);
541
+ const tangentProj = (prev, at, towardX, towardY) => {
542
+ const dx = at.x - prev.x;
543
+ const dy = at.y - prev.y;
544
+ const len = Math.hypot(dx, dy);
545
+ const along = len === 0 ? 0 : (towardX * dx + towardY * dy) / len;
546
+ return Math.max(
547
+ 0,
548
+ Math.min(dist * TANGENT_SPAN_FACTOR, MAX_TANGENT, along * 0.5)
549
+ );
550
+ };
551
+ const towardToX = toStart.x - fromEnd.x;
552
+ const towardToY = toStart.y - fromEnd.y;
553
+ const [cx1, cy1] = projectLine(
554
+ fromPrev.x,
555
+ fromPrev.y,
556
+ fromEnd.x,
557
+ fromEnd.y,
558
+ tangentProj(fromPrev, fromEnd, towardToX, towardToY)
559
+ );
560
+ const [cx2, cy2] = projectLine(
561
+ toNext.x,
562
+ toNext.y,
563
+ toStart.x,
564
+ toStart.y,
565
+ tangentProj(toNext, toStart, -towardToX, -towardToY)
566
+ );
567
+ const bulge = bypassed.length ? bowAround(p1x, p1y, p2x, p2y, bypassed, maxBow) : 0;
568
+ const chordLen = dist === 0 ? 1 : dist;
569
+ const ux = (p2x - p1x) / chordLen;
570
+ const uy = (p2y - p1y) / chordLen;
571
+ const bulgeX = -uy * bulge;
572
+ const bulgeY = ux * bulge;
573
+ const spreadX = ux * Math.abs(bulge);
574
+ const spreadY = uy * Math.abs(bulge);
575
+ return [
576
+ {
577
+ x0: p1x,
578
+ y0: p1y,
579
+ cx0: cx1 + offsetX + bulgeX - spreadX,
580
+ cy0: cy1 + offsetY + bulgeY - spreadY,
581
+ cx1: cx2 + offsetX + bulgeX + spreadX,
582
+ cy1: cy2 + offsetY + bulgeY + spreadY,
583
+ x1: p2x,
584
+ y1: p2y
585
+ }
586
+ ];
550
587
  }
551
- const lengths = new Map(gfaGraph.nodes.map((n2) => [n2.id, n2.length]));
552
- const anchoring = gfaGraph.paths.length > 0 || gfaGraph.walks.length > 0 ? surveyPaths(anchorablePaths(gfaGraph), (id) => lengths.get(id) ?? 0) : void 0;
553
- return {
554
- name,
555
- nodes,
556
- edges,
557
- paths: paths.length > 0 ? paths : void 0,
558
- anchorPaths: anchoring?.anchorPaths,
559
- pathVisits: anchoring?.pathVisits,
560
- // rGFA's own tags win wherever they are present; pathAnchoring only ever
561
- // fills in for a graph that carries none.
562
- anchoredBy: nodes.some((n2) => n2.stable) ? "tags" : void 0
563
- };
564
- }
565
-
566
- // src/layout/mergeRuns.ts
567
- function ownStrand(id) {
568
- return id.endsWith("-") ? "-" : "+";
569
- }
570
- function flip(strand) {
571
- return strand === "+" ? "-" : "+";
572
588
  }
573
- function sidesOf(edge) {
574
- const from = (edge.fromStrand ?? ownStrand(edge.from)) === ownStrand(edge.from) ? "end" : "start";
575
- const to = (edge.toStrand ?? ownStrand(edge.to)) === ownStrand(edge.to) ? "start" : "end";
576
- return { from, to };
589
+ function nearerEnd(segments, to) {
590
+ const start = segments[0];
591
+ const end = segments.at(-1);
592
+ return Math.hypot(start.x - to.x, start.y - to.y) < Math.hypot(end.x - to.x, end.y - to.y) ? "start" : "end";
577
593
  }
578
- function mergeRuns(graph) {
579
- const sides = graph.edges.map(sidesOf);
580
- const at = /* @__PURE__ */ new Map();
581
- const attach = (id, side, ei) => {
582
- const key = `${id}|${side}`;
583
- const list = at.get(key);
584
- if (list) {
585
- list.push(ei);
586
- } else {
587
- at.set(key, [ei]);
594
+ function routedEdgeCurves(fromSegments, toSegments, route) {
595
+ const fromAttach = attachment(
596
+ fromSegments,
597
+ nearerEnd(fromSegments, route[0])
598
+ );
599
+ const toAttach = attachment(toSegments, nearerEnd(toSegments, route.at(-1)));
600
+ const points = [fromAttach.at, ...route, toAttach.at];
601
+ const tangent = (i) => {
602
+ const p = points[i];
603
+ const span = (q) => Math.hypot(q.x - p.x, q.y - p.y);
604
+ const along = (inward, next) => {
605
+ const len = inward ? span(inward) : 0;
606
+ const reach = span(next);
607
+ return len === 0 ? { x: next.x - p.x, y: next.y - p.y } : {
608
+ x: (p.x - inward.x) / len * reach,
609
+ y: (p.y - inward.y) / len * reach
610
+ };
611
+ };
612
+ if (i === 0) {
613
+ return along(fromAttach.inward, points[1]);
588
614
  }
589
- };
590
- graph.edges.forEach((edge, ei) => {
591
- attach(edge.from, sides[ei].from, ei);
592
- attach(edge.to, sides[ei].to, ei);
593
- });
594
- const joinAfter = (a) => {
595
- const out = at.get(`${a}|end`);
596
- if (out?.length !== 1) {
597
- return void 0;
615
+ if (i === points.length - 1) {
616
+ const out = along(toAttach.inward, points[i - 1]);
617
+ return { x: -out.x, y: -out.y };
598
618
  }
599
- const ei = out[0];
600
- const edge = graph.edges[ei];
601
- return edge.from === a && edge.to !== a && sides[ei].from === "end" && sides[ei].to === "start" && at.get(`${edge.to}|start`)?.length === 1 ? ei : void 0;
619
+ return {
620
+ x: (points[i + 1].x - points[i - 1].x) / 2,
621
+ y: (points[i + 1].y - points[i - 1].y) / 2
622
+ };
602
623
  };
603
- const joined = /* @__PURE__ */ new Set();
604
- const runOf = /* @__PURE__ */ new Map();
605
- const runs = /* @__PURE__ */ new Map();
606
- for (const node of graph.nodes) {
607
- const into = at.get(`${node.id}|start`);
608
- const pred = into?.length === 1 ? graph.edges[into[0]].from : void 0;
609
- if (runOf.has(node.id) || pred !== void 0 && joinAfter(pred) === into[0]) {
610
- continue;
624
+ const curves = [];
625
+ for (let i = 0; i < points.length - 1; i++) {
626
+ const a = points[i];
627
+ const b = points[i + 1];
628
+ const ta = tangent(i);
629
+ const tb = tangent(i + 1);
630
+ curves.push({
631
+ x0: a.x,
632
+ y0: a.y,
633
+ cx0: a.x + ta.x / 3,
634
+ cy0: a.y + ta.y / 3,
635
+ cx1: b.x - tb.x / 3,
636
+ cy1: b.y - tb.y / 3,
637
+ x1: b.x,
638
+ y1: b.y
639
+ });
640
+ }
641
+ return curves;
642
+ }
643
+
644
+ // src/deletionEdges.ts
645
+ var MIN_DELETION_BP = 1;
646
+ function bypassedPoints(nodePositions, bypassed) {
647
+ return bypassed.flatMap((id) => nodePositions[id] ?? []);
648
+ }
649
+ function deletionArcCurves(nodePositions, deletion, axis, route) {
650
+ const from = nodePositions[deletion.from];
651
+ const to = nodePositions[deletion.to];
652
+ if (!from?.length || !to?.length) {
653
+ return void 0;
654
+ }
655
+ return route ? routedEdgeCurves(from, to, route) : computeEdgeCurves(
656
+ from,
657
+ to,
658
+ deletion.from === deletion.to,
659
+ 0,
660
+ 0,
661
+ axis,
662
+ bypassedPoints(nodePositions, deletion.bypassed)
663
+ );
664
+ }
665
+ function backboneBySequence(graph) {
666
+ const bySequence = /* @__PURE__ */ new Map();
667
+ for (const node of graph.nodes) {
668
+ if (isBackbone(node)) {
669
+ const list = bySequence.get(node.stable.refName);
670
+ if (list) {
671
+ list.push(node);
672
+ } else {
673
+ bySequence.set(node.stable.refName, [node]);
674
+ }
611
675
  }
612
- const run = [node.id];
613
- for (; ; ) {
614
- const ei = joinAfter(run.at(-1));
615
- const next = ei === void 0 ? void 0 : graph.edges[ei].to;
616
- if (next === void 0 || runOf.has(next)) {
676
+ }
677
+ for (const list of bySequence.values()) {
678
+ list.sort((a, b) => a.stable.start - b.stable.start);
679
+ }
680
+ return bySequence;
681
+ }
682
+ function bypassedNodes(nodes, start, end) {
683
+ const bypassed = [];
684
+ if (nodes) {
685
+ for (let i = firstNodeAtOrAfter(nodes, start); i < nodes.length; i++) {
686
+ const node = nodes[i];
687
+ if (node.stable.start >= end) {
617
688
  break;
618
689
  }
619
- joined.add(ei);
620
- run.push(next);
621
- }
622
- for (const id of run) {
623
- runOf.set(id, node.id);
624
- }
625
- if (run.length > 1) {
626
- runs.set(node.id, run);
690
+ if (node.stable.start + node.length <= end) {
691
+ bypassed.push(node.id);
692
+ }
627
693
  }
628
694
  }
629
- for (const node of graph.nodes) {
630
- if (!runOf.has(node.id)) {
631
- runOf.set(node.id, node.id);
632
- }
695
+ return bypassed;
696
+ }
697
+ function skipsBetween(edge, from, to, forwards) {
698
+ const { fromStrand, toStrand } = edge;
699
+ if (fromStrand === void 0 || toStrand === void 0) {
700
+ return true;
633
701
  }
702
+ const leavesRight = fromStrand === (from.stable.strand ?? "+");
703
+ const arrivesLeft = toStrand === (to.stable.strand ?? "+");
704
+ return forwards ? leavesRight && arrivesLeft : !leavesRight && !arrivesLeft;
705
+ }
706
+ function deletionEdges(graph) {
634
707
  const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
635
- const nodes = [];
636
- for (const node of graph.nodes) {
637
- if (runOf.get(node.id) !== node.id) {
638
- continue;
639
- }
640
- const run = runs.get(node.id);
641
- if (!run) {
642
- nodes.push(node);
643
- continue;
708
+ const backbone = backboneBySequence(graph);
709
+ const found = [];
710
+ for (let edgeIndex = 0; edgeIndex < graph.edges.length; edgeIndex++) {
711
+ const edge = graph.edges[edgeIndex];
712
+ const from = byId.get(edge.from);
713
+ const to = byId.get(edge.to);
714
+ if (from !== void 0 && to !== void 0 && isBackbone(from) && isBackbone(to) && from.stable.refName === to.stable.refName) {
715
+ const forwards = from.stable.start <= to.stable.start;
716
+ const [left, right] = forwards ? [from, to] : [to, from];
717
+ const start = left.stable.start + left.length;
718
+ const end = right.stable.start;
719
+ const bp = end - start;
720
+ if (bp >= MIN_DELETION_BP && skipsBetween(edge, from, to, forwards)) {
721
+ const refName = left.stable.refName;
722
+ found.push({
723
+ edgeIndex,
724
+ from: edge.from,
725
+ to: edge.to,
726
+ refName,
727
+ start,
728
+ end,
729
+ bp,
730
+ bypassed: bypassedNodes(backbone.get(refName), start, end)
731
+ });
732
+ }
644
733
  }
645
- const members = run.map((id) => byId.get(id));
646
- const backbone = members.filter(isBackbone);
647
- const first = backbone.length ? backbone.reduce((a, b) => a.stable.start <= b.stable.start ? a : b) : members[0];
648
- nodes.push({
649
- id: node.id,
650
- name: node.name,
651
- length: members.reduce((sum, n2) => sum + n2.length, 0),
652
- depth: node.depth,
653
- stable: first.stable
654
- });
655
734
  }
656
- const seen = /* @__PURE__ */ new Set();
657
- const edges = [];
658
- graph.edges.forEach((edge, ei) => {
659
- const from = runOf.get(edge.from);
660
- const to = runOf.get(edge.to);
661
- const side = sides[ei];
662
- const key = `${from}|${side.from}>${to}|${side.to}`;
663
- if (joined.has(ei) || seen.has(key)) {
664
- return;
665
- }
666
- seen.add(key);
667
- edges.push({
668
- from,
669
- to,
670
- ...side.from === "start" ? { fromStrand: flip(ownStrand(from)) } : {},
671
- ...side.to === "end" ? { toStrand: flip(ownStrand(to)) } : {}
672
- });
673
- });
674
- return {
675
- graph: { ...graph, nodes, edges, paths: void 0, pathVisits: void 0 },
676
- runs
677
- };
735
+ return found;
678
736
  }
679
- function splitRuns(positions, runs, weightOf) {
680
- const out = { ...positions };
681
- for (const [id, members] of runs) {
682
- const line = positions[id];
683
- if (!line?.length) {
684
- continue;
685
- }
686
- delete out[id];
687
- const arc = arcPrefix(line);
688
- const total = arc.at(-1);
689
- const weights = members.map((m) => Math.max(weightOf(m), 0));
690
- const weightSum = weights.reduce((a, b) => a + b, 0) || members.length;
691
- let from = 0;
692
- members.forEach((member, k) => {
693
- const share = weightSum ? weights[k] / weightSum * total : total / members.length;
694
- const to = k === members.length - 1 ? total : from + share;
695
- out[member] = slice(line, arc, from, to);
696
- from = to;
697
- });
698
- }
699
- return out;
737
+
738
+ // src/pansn.ts
739
+ var SEP = "#";
740
+ function panSNSample(refName) {
741
+ return refName.split(SEP)[0];
700
742
  }
701
- function polylineSlice(line, from, to) {
702
- if (line.length === 0) {
703
- return [];
704
- }
705
- const arc = arcPrefix(line);
706
- const total = arc.at(-1);
707
- return slice(line, arc, from * total, to * total);
743
+ var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
744
+ function isGenericSample(sample) {
745
+ return GENERIC_SAMPLES.has(sample);
708
746
  }
709
- function polylineMidpoint(line) {
710
- const arc = arcPrefix(line);
711
- return pointAt(line, arc, arc.at(-1) / 2);
747
+ function panSNHaplotype(refName) {
748
+ const parts = refName.split(SEP);
749
+ return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : void 0;
712
750
  }
713
- function arcPrefix(line) {
714
- const arc = [0];
715
- for (let i = 1; i < line.length; i++) {
716
- arc.push(
717
- arc[i - 1] + Math.hypot(line[i].x - line[i - 1].x, line[i].y - line[i - 1].y)
718
- );
719
- }
720
- return arc;
751
+ function panSNContig(refName) {
752
+ const parts = refName.split(SEP);
753
+ return parts.length >= 3 ? parts.slice(2).join(SEP) : parts.length === 2 ? parts[1] : refName;
721
754
  }
722
- function pointAt(line, arc, s) {
723
- if (s <= 0) {
724
- return { ...line[0] };
725
- }
726
- let i = 1;
727
- while (i < arc.length && arc[i] < s) {
728
- i++;
729
- }
730
- if (i >= arc.length) {
731
- return { ...line.at(-1) };
732
- }
733
- const span = arc[i] - arc[i - 1];
734
- const t = span > 0 ? (s - arc[i - 1]) / span : 0;
735
- const a = line[i - 1];
736
- const b = line[i];
737
- return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
755
+ function panSNMatchesPrefix(refName, prefix) {
756
+ return prefix !== void 0 && (refName === prefix || refName.startsWith(prefix + SEP));
738
757
  }
739
- function slice(line, arc, from, to) {
740
- const points = [pointAt(line, arc, from)];
741
- for (let i = 0; i < line.length; i++) {
742
- if (arc[i] > from && arc[i] < to) {
743
- points.push({ ...line[i] });
758
+
759
+ // src/gfa-core/gfaParser.ts
760
+ function parseTag(tag, tags) {
761
+ const nameEnd = tag.indexOf(":");
762
+ const typeEnd = tag.indexOf(":", nameEnd + 1);
763
+ if (nameEnd !== -1 && typeEnd !== -1) {
764
+ const name = tag.slice(0, nameEnd);
765
+ const type = tag.slice(nameEnd + 1, typeEnd);
766
+ const val = tag.slice(typeEnd + 1);
767
+ if (type === "i" || type === "f") {
768
+ tags[name] = +val;
769
+ } else if (type === "Z") {
770
+ tags[name] = val;
744
771
  }
745
772
  }
746
- points.push(pointAt(line, arc, to));
747
- return points;
748
773
  }
749
-
750
- // src/layout/orientToReference.ts
751
- function orientToReference(graph, positions) {
752
- const backbone = graph.nodes.filter(
753
- (node) => isBackbone(node) && (positions[node.id]?.length ?? 0) > 0
754
- );
755
- if (backbone.length < 2) {
756
- return positions;
774
+ function parseTags(fields) {
775
+ const tags = {};
776
+ for (const field of fields) {
777
+ parseTag(field, tags);
757
778
  }
758
- const points = backbone.map((node) => {
759
- const segments = positions[node.id];
760
- const mid = segments[Math.floor(segments.length / 2)];
761
- return {
762
- x: mid.x,
763
- y: mid.y,
764
- r: node.stable.start + node.length / 2,
765
- w: node.length
766
- };
767
- });
768
- const weight = points.reduce((sum, p) => sum + p.w, 0);
769
- const cx = points.reduce((sum, p) => sum + p.x * p.w, 0) / weight;
770
- const cy = points.reduce((sum, p) => sum + p.y * p.w, 0) / weight;
771
- const cr = points.reduce((sum, p) => sum + p.r * p.w, 0) / weight;
772
- let sxr = 0;
773
- let syr = 0;
774
- for (const p of points) {
775
- sxr += p.w * (p.x - cx) * (p.r - cr);
776
- syr += p.w * (p.y - cy) * (p.r - cr);
777
- }
778
- const angle = -Math.atan2(syr, sxr);
779
- const c = Math.cos(angle);
780
- const s = Math.sin(angle);
781
- const out = {};
782
- for (const [id, segments] of Object.entries(positions)) {
783
- out[id] = segments.map((p) => {
784
- const x = p.x - cx;
785
- const y = p.y - cy;
786
- return { x: cx + c * x - s * y, y: cy + s * x + c * y };
787
- });
779
+ return tags;
780
+ }
781
+ function stableCoordinate(node) {
782
+ const { SN, SO, SR } = node.tags;
783
+ return typeof SN === "string" && typeof SO === "number" && typeof SR === "number" ? { refName: SN, start: SO, rank: SR } : void 0;
784
+ }
785
+ var GT = 62;
786
+ var LT = 60;
787
+ function parseWalkBody(body) {
788
+ const segments = [];
789
+ let strand = "+";
790
+ let start = 0;
791
+ function push(end) {
792
+ if (end > start) {
793
+ segments.push({ id: body.slice(start, end), strand });
794
+ }
788
795
  }
789
- let below = 0;
790
- let above = 0;
791
- for (const node of graph.nodes) {
792
- if (isBackbone(node)) {
793
- continue;
796
+ for (let i = 0; i < body.length; i++) {
797
+ const ch = body.charCodeAt(i);
798
+ if (ch === GT || ch === LT) {
799
+ push(i);
800
+ strand = ch === GT ? "+" : "-";
801
+ start = i + 1;
794
802
  }
795
- for (const p of out[node.id] ?? []) {
796
- if (p.y > cy) {
797
- below++;
803
+ }
804
+ push(body.length);
805
+ return segments;
806
+ }
807
+ function parseGFA(file) {
808
+ const graph = {
809
+ nodes: [],
810
+ links: [],
811
+ paths: [],
812
+ walks: [],
813
+ header: [],
814
+ id: ""
815
+ };
816
+ for (const rawLine of file.split("\n")) {
817
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
818
+ if (line.startsWith("H")) {
819
+ const [, ...rest] = line.split(" ");
820
+ graph.header.push(parseTags(rest));
821
+ } else if (line.startsWith("S")) {
822
+ const [, name, ...rest] = line.split(" ");
823
+ let len;
824
+ let seq;
825
+ let tagfields;
826
+ let gfa1 = false;
827
+ const first = rest[0] ?? "*";
828
+ if (/^\d+$/.test(first)) {
829
+ len = +first;
830
+ seq = rest[1] ?? "*";
831
+ tagfields = rest.slice(2);
798
832
  } else {
799
- above++;
833
+ gfa1 = true;
834
+ seq = first;
835
+ len = seq === "*" ? 0 : seq.length;
836
+ tagfields = rest.slice(1);
800
837
  }
801
- }
802
- }
803
- if (above > below) {
804
- for (const segments of Object.values(out)) {
805
- for (const p of segments) {
806
- p.y = 2 * cy - p.y;
838
+ const tags = parseTags(tagfields);
839
+ if (gfa1 && "LN" in tags) {
840
+ len = +tags.LN;
841
+ }
842
+ graph.nodes.push({ id: name, length: len, sequence: seq, tags });
843
+ } else if (line.startsWith("E")) {
844
+ const [, , source, target, , , , , cigar, ...rest] = line.split(" ");
845
+ if (source && target) {
846
+ graph.links.push({
847
+ source: source.slice(0, -1),
848
+ target: target.slice(0, -1),
849
+ strand1: source.at(-1),
850
+ strand2: target.at(-1),
851
+ cigar: cigar ?? "*",
852
+ tags: parseTags(rest)
853
+ });
854
+ }
855
+ } else if (line.startsWith("L")) {
856
+ const [, source, strand1, target, strand2, cigar, ...rest] = line.split(" ");
857
+ if (source && target && strand2) {
858
+ graph.links.push({
859
+ source,
860
+ target,
861
+ strand1,
862
+ strand2,
863
+ cigar: cigar ?? "*",
864
+ tags: parseTags(rest)
865
+ });
866
+ }
867
+ } else if (line.startsWith("W")) {
868
+ const [, sample, hap, contig, start, end, body, ...rest] = line.split(" ");
869
+ if (sample && hap && contig && start && end && body) {
870
+ graph.walks.push({
871
+ sample,
872
+ haplotype: +hap,
873
+ contig,
874
+ start: start === "*" ? -1 : +start,
875
+ end: end === "*" ? -1 : +end,
876
+ segments: parseWalkBody(body),
877
+ tags: parseTags(rest)
878
+ });
879
+ }
880
+ } else if (line.startsWith("P")) {
881
+ const [, name, path, ...rest] = line.split(" ");
882
+ if (name && path) {
883
+ graph.paths.push({ name, path, rest });
807
884
  }
808
885
  }
809
886
  }
810
- return out;
887
+ return graph;
811
888
  }
812
889
 
813
- // src/referenceSpan.ts
814
- var MAX_BACKBONE_HOPS = 4;
815
- function buildNeighbors(graph) {
816
- const neighbors = /* @__PURE__ */ new Map();
817
- function add(from, to) {
818
- const list = neighbors.get(from);
819
- if (list) {
820
- list.push(to);
821
- } else {
822
- neighbors.set(from, [to]);
823
- }
824
- }
825
- for (const edge of graph.edges) {
826
- add(edge.from, edge.to);
827
- add(edge.to, edge.from);
828
- }
829
- return neighbors;
890
+ // src/pathAnchoring.ts
891
+ var RANGE_SUFFIX = /:(\d+)-\d+$/;
892
+ function pathOrigin(pathName) {
893
+ const match = RANGE_SUFFIX.exec(pathName);
894
+ return match ? { name: pathName.slice(0, match.index), start: +match[1] } : { name: pathName, start: 0 };
830
895
  }
831
- function flankingSpan({
832
- nodeId,
833
- nodeById,
834
- neighbors
835
- }) {
836
- const seen = /* @__PURE__ */ new Set([nodeId]);
837
- let frontier = [nodeId];
838
- let minEnd = Infinity;
839
- let maxStart = -Infinity;
840
- for (let hop = 0; hop < MAX_BACKBONE_HOPS && frontier.length > 0; hop++) {
841
- const next = [];
842
- for (const id of frontier) {
843
- const adjacent = neighbors.get(id);
844
- if (adjacent) {
845
- for (const neighborId of adjacent) {
846
- if (!seen.has(neighborId)) {
847
- seen.add(neighborId);
848
- const neighbor = nodeById.get(neighborId);
849
- if (neighbor && isBackbone(neighbor)) {
850
- minEnd = Math.min(minEnd, neighbor.stable.start + neighbor.length);
851
- maxStart = Math.max(maxStart, neighbor.stable.start);
852
- } else {
853
- next.push(neighborId);
854
- }
855
- }
856
- }
896
+ function surveyPaths(paths, lengthOf) {
897
+ const anchorPaths = [];
898
+ const pathVisits = /* @__PURE__ */ new Map();
899
+ for (const path of paths) {
900
+ const sample = panSNSample(path.name);
901
+ let pos = path.start;
902
+ for (const step of path.steps) {
903
+ const visit = {
904
+ path: path.name,
905
+ sample,
906
+ start: pos,
907
+ strand: step.strand
908
+ };
909
+ const existing = pathVisits.get(step.id);
910
+ if (existing) {
911
+ existing.push(visit);
912
+ } else {
913
+ pathVisits.set(step.id, [visit]);
857
914
  }
915
+ pos += lengthOf(step.id);
858
916
  }
859
- frontier = next;
917
+ anchorPaths.push({
918
+ name: path.name,
919
+ sample,
920
+ start: path.start,
921
+ length: pos - path.start
922
+ });
860
923
  }
861
- return minEnd === Infinity ? void 0 : {
862
- start: Math.min(minEnd, maxStart),
863
- end: Math.max(minEnd, maxStart)
864
- };
924
+ return { anchorPaths, pathVisits };
865
925
  }
866
- function nodeReferenceSpan({
867
- nodeId,
868
- nodeById,
869
- neighbors
870
- }) {
871
- const node = nodeById.get(nodeId);
872
- return node === void 0 ? void 0 : isBackbone(node) ? { start: node.stable.start, end: node.stable.start + node.length } : flankingSpan({ nodeId, nodeById, neighbors });
926
+ function chooseReferencePath(anchorPaths, preferred) {
927
+ const named = anchorPaths.find(
928
+ (p) => p.sample === preferred || p.name === preferred
929
+ );
930
+ return named ?? anchorPaths[0];
873
931
  }
874
- function referenceMidpoints(graph) {
875
- const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
876
- const neighbors = buildNeighbors(graph);
877
- const midpoints = /* @__PURE__ */ new Map();
878
- for (const node of graph.nodes) {
879
- const span = nodeReferenceSpan({ nodeId: node.id, nodeById, neighbors });
880
- if (span) {
881
- midpoints.set(node.id, (span.start + span.end) / 2);
882
- }
932
+ var OFF_REFERENCE_RANK = 1;
933
+ function anchorNode(node, visits, reference) {
934
+ let anchored = node;
935
+ if (visits && visits.length > 0) {
936
+ const anchor = visits.find((v) => v.path === reference) ?? visits[0];
937
+ anchored = {
938
+ ...node,
939
+ stable: {
940
+ refName: anchor.path,
941
+ start: anchor.start,
942
+ rank: anchor.path === reference ? REFERENCE_RANK : OFF_REFERENCE_RANK,
943
+ strand: anchor.strand
944
+ },
945
+ samples: [...new Set(visits.map((v) => v.sample))].sort()
946
+ };
883
947
  }
884
- return midpoints;
948
+ return anchored;
885
949
  }
886
-
887
- // src/layout/referenceSeeds.ts
888
- var LANE_GAP = 30;
889
- function referenceSeeds(graph, scaling) {
890
- const { opts } = scaling;
891
- const drawn = new Map(
892
- scaling.nodes.map((node) => [node.id, drawnNodeLength(opts, node.length)])
893
- );
894
- const drawnOf = (id) => drawn.get(id) ?? opts.minimumNodeLength;
895
- const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
896
- const seeds = /* @__PURE__ */ new Map();
897
- let x = 0;
898
- for (const node of backbone) {
899
- seeds.set(node.id, { x, y: 0 });
900
- x += drawnOf(node.id) + opts.edgeLength;
901
- }
902
- const adjacent = buildNeighbors(graph);
903
- const depth = new Map(backbone.map((node) => [node.id, 0]));
904
- const queue = backbone.map((node) => node.id);
905
- for (const id of queue) {
906
- const at = seeds.get(id);
907
- const next = depth.get(id) + 1;
908
- for (const neighbour of adjacent.get(id) ?? []) {
909
- if (depth.has(neighbour)) {
910
- continue;
911
- }
912
- depth.set(neighbour, next);
913
- seeds.set(neighbour, { x: at.x + drawnOf(id) / 2, y: LANE_GAP * next });
914
- queue.push(neighbour);
915
- }
950
+ function anchorFromPaths(graph, preferred) {
951
+ const { anchorPaths, pathVisits } = graph;
952
+ const reference = anchorPaths && pathVisits ? chooseReferencePath(anchorPaths, preferred) : void 0;
953
+ return reference && pathVisits ? {
954
+ ...graph,
955
+ nodes: graph.nodes.map(
956
+ (node) => anchorNode(node, pathVisits.get(node.name), reference.name)
957
+ ),
958
+ anchoredBy: "paths",
959
+ referencePath: reference.name
960
+ } : graph;
961
+ }
962
+ function anchorGraph(graph, preferred) {
963
+ return graph.anchoredBy === "tags" ? graph : anchorFromPaths(graph, preferred);
964
+ }
965
+ function trimOrigins(graph, kept) {
966
+ if (!graph.anchorPaths) {
967
+ return void 0;
916
968
  }
917
- let unreached = 0;
918
- for (const node of graph.nodes) {
919
- if (!seeds.has(node.id)) {
920
- seeds.set(node.id, {
921
- x: unreached++ * x / graph.nodes.length,
922
- y: LANE_GAP * 3
923
- });
969
+ const lengthOf = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
970
+ const bp = (ids, end = ids.length) => {
971
+ let sum = 0;
972
+ for (let i = 0; i < end; i++) {
973
+ sum += lengthOf.get(ids[i]) ?? 0;
924
974
  }
975
+ return sum;
976
+ };
977
+ const byOrigin = /* @__PURE__ */ new Map();
978
+ for (const path of graph.paths ?? []) {
979
+ const origin = pathOrigin(path.name).name;
980
+ byOrigin.set(origin, [...byOrigin.get(origin) ?? [], path]);
925
981
  }
926
- return seeds;
927
- }
928
- function seededNodes(graph, scaling) {
929
- const seeds = referenceSeeds(graph, scaling);
930
- return scaling.nodes.map((node) => ({ ...node, ...seeds.get(node.id) }));
982
+ return graph.anchorPaths.map((o) => {
983
+ const pieces = byOrigin.get(o.name);
984
+ const path = pieces?.length === 1 ? pieces[0] : void 0;
985
+ const stretch = path && kept.get(path.name);
986
+ return path && stretch ? {
987
+ ...o,
988
+ start: o.start + bp(path.nodeIds, stretch.first),
989
+ length: bp(stretch.nodeIds)
990
+ } : o;
991
+ });
931
992
  }
932
993
 
933
- // src/pipeline.ts
934
- function loadGraph(text, name, opts = {}) {
935
- const graph = anchorGraph(
936
- convertGFAToGraph(parseGFA(text), name),
937
- opts.referencePath
938
- );
939
- if (graph.nodes.length === 0) {
940
- throw new Error(`No graph segments in ${name}`);
994
+ // src/gfa/gfaConverter.ts
995
+ function surveySegments(gfaGraph) {
996
+ const canonical = /* @__PURE__ */ new Map();
997
+ const traversals = /* @__PURE__ */ new Map();
998
+ function claim(id, strand) {
999
+ if (!canonical.has(id)) {
1000
+ canonical.set(id, strand);
1001
+ }
941
1002
  }
942
- if (opts.maxNodes !== void 0 && graph.nodes.length > opts.maxNodes) {
943
- throw new Error(
944
- `Graph too large to draw: ${graph.nodes.length.toLocaleString()} nodes (limit ${opts.maxNodes.toLocaleString()}). Zoom in to a smaller region, or raise maxGraphNodes on this view.`
945
- );
1003
+ function traverse(id, strand) {
1004
+ claim(id, strand);
1005
+ traversals.set(id, (traversals.get(id) ?? 0) + 1);
946
1006
  }
947
- return graph;
948
- }
949
- function engineRequest(graph, scaling, settings) {
950
- const anchored = graph.nodes.some(isBackbone);
951
- return {
952
- graph: {
953
- nodes: anchored ? seededNodes(graph, scaling) : scaling.nodes,
954
- edges: graph.edges
955
- },
956
- options: {
957
- quality: settings.quality,
958
- linearLayout: settings.linearLayout,
959
- ...scaling.opts,
960
- ...anchored ? { rotateComponents: false } : {}
1007
+ for (const link of gfaGraph.links) {
1008
+ claim(link.source, link.strand1 === "-" ? "-" : "+");
1009
+ claim(link.target, link.strand2 === "-" ? "-" : "+");
1010
+ }
1011
+ for (const gfaPath of gfaGraph.paths) {
1012
+ for (const segment of gfaPath.path.split(",")) {
1013
+ traverse(segment.slice(0, -1), segment.endsWith("-") ? "-" : "+");
961
1014
  }
962
- };
963
- }
964
- function engineKey(graph, settings) {
965
- const anchored = graph.nodes.some(isBackbone);
966
- return `${settings.quality}|${settings.linearLayout}|${settings.bubbleSpread}|${anchored}|${graph.referencePath ?? ""}`;
967
- }
968
- async function forceLayout(graph, settings, engine) {
969
- const spread = spreadFor(settings.bubbleSpread);
970
- const merged = mergeRuns(graph);
971
- const { result, duration } = await engine(
972
- engineRequest(merged.graph, layoutScaling(merged.graph, spread), settings)
973
- );
974
- const scaling = layoutScaling(graph, spread);
975
- const drawn = new Map(
976
- scaling.nodes.map((n2) => [n2.id, drawnNodeLength(scaling.opts, n2.length)])
977
- );
978
- const positions = splitRuns(
979
- result.nodePositions,
980
- merged.runs,
981
- (id) => drawn.get(id) ?? 0
982
- );
983
- return {
984
- result: {
985
- ...result,
986
- nodePositions: graph.nodes.some(isBackbone) ? orientToReference(graph, positions) : positions
987
- },
988
- duration
989
- };
990
- }
991
- function layoutExtent(nodePositions) {
992
- let minX = Infinity;
993
- let minY = Infinity;
994
- let maxX = -Infinity;
995
- let maxY = -Infinity;
996
- for (const segments of Object.values(nodePositions)) {
997
- for (const seg of segments) {
998
- minX = Math.min(minX, seg.x);
999
- minY = Math.min(minY, seg.y);
1000
- maxX = Math.max(maxX, seg.x);
1001
- maxY = Math.max(maxY, seg.y);
1015
+ }
1016
+ for (const walk of gfaGraph.walks) {
1017
+ for (const seg of walk.segments) {
1018
+ traverse(seg.id, seg.strand === "-" ? "-" : "+");
1002
1019
  }
1003
1020
  }
1004
- return { minX, minY, maxX, maxY };
1005
- }
1006
- var FIT_PADDING = 40;
1007
- var MIN_ZOOM = 1e-6;
1008
- var MAX_ZOOM = 100;
1009
- function clampZoom(zoom) {
1010
- return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
1021
+ return { canonical, traversals };
1011
1022
  }
1012
- function fittedTranslateY(bounds, usableHeight, yScale, padTop = FIT_PADDING) {
1013
- const leftoverY = usableHeight - bounds.h * yScale;
1014
- return padTop - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
1023
+ var walkStart = (w) => w.start === -1 ? 0 : w.start;
1024
+ function anchorablePaths(gfaGraph) {
1025
+ return [
1026
+ ...gfaGraph.paths.map((p) => ({
1027
+ ...pathOrigin(p.name),
1028
+ steps: p.path.split(",").map((segment) => ({
1029
+ id: segment.slice(0, -1),
1030
+ strand: segment.endsWith("-") ? "-" : "+"
1031
+ }))
1032
+ })),
1033
+ ...gfaGraph.walks.map((w) => ({
1034
+ name: `${w.sample}#${w.haplotype}#${w.contig}`,
1035
+ start: walkStart(w),
1036
+ steps: w.segments.map((s) => ({
1037
+ id: s.id,
1038
+ strand: s.strand === "-" ? "-" : "+"
1039
+ }))
1040
+ }))
1041
+ ];
1015
1042
  }
1016
- function fitTransform(bounds, paneWidth, paneHeight, pixelRows, {
1017
- minScale = 0,
1018
- padLeft = FIT_PADDING,
1019
- padTop = FIT_PADDING,
1020
- padRight = FIT_PADDING,
1021
- padBottom = FIT_PADDING
1022
- } = {}) {
1023
- const usableWidth = paneWidth - padLeft - padRight;
1024
- const usableHeight = paneHeight - padTop - padBottom;
1025
- if (usableWidth <= 0 || usableHeight <= 0 || !(bounds.w > 0 || !pixelRows && bounds.h > 0)) {
1043
+ function carriedSamples(gfaNode) {
1044
+ const sm = gfaNode.tags.SM;
1045
+ if (typeof sm !== "string") {
1026
1046
  return void 0;
1027
1047
  }
1028
- const fitX = bounds.w > 0 ? usableWidth / bounds.w : Infinity;
1029
- const fitY = bounds.h > 0 ? usableHeight / bounds.h : Infinity;
1030
- const scale = clampZoom(
1031
- Math.max(minScale, pixelRows ? fitX : Math.min(fitX, fitY))
1032
- );
1033
- const leftoverX = usableWidth - bounds.w * scale;
1048
+ const samples = sm.split(",").map((s) => s.trim()).filter(Boolean);
1049
+ return samples.length > 0 ? samples : void 0;
1050
+ }
1051
+ var positive = (v) => typeof v === "number" && v > 0 ? v : void 0;
1052
+ function statedDepth({ tags, length }) {
1053
+ const count = positive(tags.RC) ?? positive(tags.FC) ?? positive(tags.KC);
1054
+ return positive(tags.dp) ?? positive(tags.DP) ?? (count !== void 0 && length > 0 ? count / length : void 0);
1055
+ }
1056
+ function makeNode(gfaNode, strand, depth) {
1034
1057
  return {
1035
- scale,
1036
- translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
1037
- translateY: fittedTranslateY(
1038
- bounds,
1039
- usableHeight,
1040
- pixelRows ? 1 : scale,
1041
- padTop
1042
- )
1058
+ id: `${gfaNode.id}${strand}`,
1059
+ name: gfaNode.id,
1060
+ length: gfaNode.length,
1061
+ depth,
1062
+ stable: stableCoordinate(gfaNode),
1063
+ samples: carriedSamples(gfaNode)
1043
1064
  };
1044
1065
  }
1045
- function drawingBounds(layout, opts = {}) {
1046
- let { minX, minY, maxX, maxY } = layoutExtent(layout.nodePositions);
1047
- const { region } = opts;
1048
- if (layout.referenceAxis && region && region.end > region.start) {
1049
- minX = region.start;
1050
- maxX = region.end;
1066
+ function convertGFAToGraph(gfaGraph, name = "Imported GFA") {
1067
+ const nodes = [];
1068
+ const edges = [];
1069
+ const { canonical, traversals } = surveySegments(gfaGraph);
1070
+ const nodeId = (segmentId) => `${segmentId}${canonical.get(segmentId) ?? "+"}`;
1071
+ for (const gfaNode of gfaGraph.nodes) {
1072
+ const depth = statedDepth(gfaNode) ?? Math.max(traversals.get(gfaNode.id) ?? 1, 1);
1073
+ nodes.push(makeNode(gfaNode, canonical.get(gfaNode.id) ?? "+", depth));
1051
1074
  }
1052
- const extent = opts.extent ?? layout.extent;
1053
- if (extent) {
1054
- minX = Math.min(minX, extent.minX ?? minX);
1055
- minY = Math.min(minY, extent.minY ?? minY);
1056
- maxX = Math.max(maxX, extent.maxX ?? maxX);
1057
- maxY = Math.max(maxY, extent.maxY ?? maxY);
1075
+ for (const link of gfaGraph.links) {
1076
+ edges.push({
1077
+ from: nodeId(link.source),
1078
+ to: nodeId(link.target),
1079
+ fromStrand: link.strand1 === "-" ? "-" : "+",
1080
+ toStrand: link.strand2 === "-" ? "-" : "+"
1081
+ });
1058
1082
  }
1059
- return { minX, minY, w: maxX - minX, h: maxY - minY };
1060
- }
1061
-
1062
- // src/layout/rowSpacing.ts
1063
- var ROW_HEIGHT_PX = 20;
1064
-
1065
- // src/util/geometry.ts
1066
- function svgPath(points) {
1067
- const round = (v) => Math.round(v * 100) / 100;
1068
- return points.map((p, i) => `${i ? "L" : "M"}${round(p.x)},${round(p.y)}`).join("");
1069
- }
1070
- function projectLine(x1, y1, x2, y2, distance) {
1071
- const d = Math.hypot(y2 - y1, x2 - x1);
1072
- if (d === 0) {
1073
- return [x2, y2];
1083
+ const paths = [];
1084
+ const edgeToPathsMap = /* @__PURE__ */ new Map();
1085
+ const edgeKey = (a, b) => [a, b].sort().join("--");
1086
+ function recordPathEdges(nodeIds, name2) {
1087
+ for (let i = 0; i < nodeIds.length - 1; i++) {
1088
+ const key = edgeKey(nodeIds[i], nodeIds[i + 1]);
1089
+ if (!edgeToPathsMap.has(key)) {
1090
+ edgeToPathsMap.set(key, /* @__PURE__ */ new Set());
1091
+ }
1092
+ edgeToPathsMap.get(key).add(name2);
1093
+ }
1074
1094
  }
1075
- const vx = (x2 - x1) / d;
1076
- const vy = (y2 - y1) / d;
1077
- return [x2 + distance * vx, y2 + distance * vy];
1078
- }
1079
- function curvePointAt(c, t) {
1080
- const u = 1 - t;
1081
- const a = u * u * u;
1082
- const b = 3 * u * u * t;
1083
- const d = 3 * u * t * t;
1084
- const e = t * t * t;
1085
- return {
1086
- x: a * c.x0 + b * c.cx0 + d * c.cx1 + e * c.x1,
1087
- y: a * c.y0 + b * c.cy0 + d * c.cy1 + e * c.y1
1088
- };
1089
- }
1090
- function curveMidpoint(curves) {
1091
- const mid = curves[Math.floor(curves.length / 2)];
1092
- return mid ? curvePointAt(mid, 0.5) : void 0;
1093
- }
1094
- function curveBounds(curves) {
1095
- let minX = Infinity;
1096
- let minY = Infinity;
1097
- let maxX = -Infinity;
1098
- let maxY = -Infinity;
1099
- for (const c of curves) {
1100
- minX = Math.min(minX, c.x0, c.cx0, c.cx1, c.x1);
1101
- maxX = Math.max(maxX, c.x0, c.cx0, c.cx1, c.x1);
1102
- minY = Math.min(minY, c.y0, c.cy0, c.cy1, c.y1);
1103
- maxY = Math.max(maxY, c.y0, c.cy0, c.cy1, c.y1);
1095
+ for (const gfaPath of gfaGraph.paths) {
1096
+ const nodeIds = gfaPath.path.split(",").map((segment) => nodeId(segment.slice(0, -1)));
1097
+ paths.push({
1098
+ name: gfaPath.name,
1099
+ nodeIds,
1100
+ start: pathOrigin(gfaPath.name).start
1101
+ });
1102
+ recordPathEdges(nodeIds, gfaPath.name);
1104
1103
  }
1105
- return { minX, minY, maxX, maxY };
1106
- }
1107
- function subCurve(c, t0, t1) {
1108
- const p0 = curvePointAt(c, t0);
1109
- const p1 = curvePointAt(c, t1);
1110
- const dt = (t1 - t0) / 3;
1111
- const d0 = curveTangentAt(c, t0);
1112
- const d1 = curveTangentAt(c, t1);
1104
+ for (const walk of gfaGraph.walks) {
1105
+ const nodeIds = walk.segments.map((seg) => nodeId(seg.id));
1106
+ const name2 = `${walk.sample}#${walk.haplotype}#${walk.contig}`;
1107
+ paths.push({
1108
+ name: name2,
1109
+ nodeIds,
1110
+ start: walkStart(walk),
1111
+ sample: walk.sample,
1112
+ haplotype: walk.haplotype,
1113
+ contig: walk.contig
1114
+ });
1115
+ recordPathEdges(nodeIds, name2);
1116
+ }
1117
+ for (const edge of edges) {
1118
+ const pathIds = edgeToPathsMap.get(edgeKey(edge.from, edge.to));
1119
+ if (pathIds && pathIds.size > 0) {
1120
+ edge.pathIds = [...pathIds];
1121
+ }
1122
+ }
1123
+ const lengths = new Map(gfaGraph.nodes.map((n2) => [n2.id, n2.length]));
1124
+ const anchoring = gfaGraph.paths.length > 0 || gfaGraph.walks.length > 0 ? surveyPaths(anchorablePaths(gfaGraph), (id) => lengths.get(id) ?? 0) : void 0;
1113
1125
  return {
1114
- x0: p0.x,
1115
- y0: p0.y,
1116
- cx0: p0.x + d0.x * dt,
1117
- cy0: p0.y + d0.y * dt,
1118
- cx1: p1.x - d1.x * dt,
1119
- cy1: p1.y - d1.y * dt,
1120
- x1: p1.x,
1121
- y1: p1.y
1126
+ name,
1127
+ nodes,
1128
+ edges,
1129
+ paths: paths.length > 0 ? paths : void 0,
1130
+ anchorPaths: anchoring?.anchorPaths,
1131
+ pathVisits: anchoring?.pathVisits,
1132
+ // rGFA's own tags win wherever they are present; pathAnchoring only ever
1133
+ // fills in for a graph that carries none.
1134
+ anchoredBy: nodes.some((n2) => n2.stable) ? "tags" : void 0
1122
1135
  };
1123
1136
  }
1124
- function curveTangentAt(c, t) {
1125
- const u = 1 - t;
1126
- const a = 3 * u * u;
1127
- const b = 6 * u * t;
1128
- const d = 3 * t * t;
1137
+
1138
+ // src/layout/deletionRoutes.ts
1139
+ var ROUTE_ID = "\0deletion:";
1140
+ var ROUTE_FRACTION = 0.5;
1141
+ function withDeletionRoutes(graph, deletions) {
1142
+ const routes = new Map(
1143
+ deletions.map((d) => [d.edgeIndex, `${ROUTE_ID}${d.edgeIndex}`])
1144
+ );
1145
+ const edges = [];
1146
+ graph.edges.forEach((edge, ei) => {
1147
+ const id = routes.get(ei);
1148
+ if (id === void 0) {
1149
+ edges.push(edge);
1150
+ } else {
1151
+ const { fromStrand, toStrand } = edge;
1152
+ edges.push(
1153
+ { from: edge.from, to: id, ...fromStrand ? { fromStrand } : {} },
1154
+ { from: id, to: edge.to, ...toStrand ? { toStrand } : {} }
1155
+ );
1156
+ }
1157
+ });
1129
1158
  return {
1130
- x: a * (c.cx0 - c.x0) + b * (c.cx1 - c.cx0) + d * (c.x1 - c.cx1),
1131
- y: a * (c.cy0 - c.y0) + b * (c.cy1 - c.cy0) + d * (c.y1 - c.cy1)
1159
+ graph: {
1160
+ ...graph,
1161
+ nodes: [
1162
+ ...graph.nodes,
1163
+ ...[...routes.values()].map((id) => ({
1164
+ id,
1165
+ name: id,
1166
+ length: 0,
1167
+ depth: 0
1168
+ }))
1169
+ ],
1170
+ edges
1171
+ },
1172
+ routes,
1173
+ ids: new Set(routes.values())
1132
1174
  };
1133
1175
  }
1134
- function dashCurves(curves, dashWorld) {
1135
- const out = [];
1136
- for (const c of curves) {
1137
- const polyLen = Math.hypot(c.cx0 - c.x0, c.cy0 - c.y0) + Math.hypot(c.cx1 - c.cx0, c.cy1 - c.cy0) + Math.hypot(c.x1 - c.cx1, c.y1 - c.cy1);
1138
- const spans = Math.max(3, Math.round(polyLen / dashWorld) | 1);
1139
- for (let i = 0; i < spans; i += 2) {
1140
- out.push([subCurve(c, i / spans, (i + 1) / spans)]);
1141
- }
1142
- }
1143
- return out;
1144
- }
1145
- function translateCurves(curves, dx, dy) {
1146
- const out = new Array(curves.length);
1147
- for (let i = 0; i < curves.length; i++) {
1148
- const c = curves[i];
1149
- out[i] = {
1150
- x0: c.x0 + dx,
1151
- y0: c.y0 + dy,
1152
- cx0: c.cx0 + dx,
1153
- cy0: c.cy0 + dy,
1154
- cx1: c.cx1 + dx,
1155
- cy1: c.cy1 + dy,
1156
- x1: c.x1 + dx,
1157
- y1: c.y1 + dy
1176
+ function routeNodes(graph, deletions, routes, drawnOf, opts) {
1177
+ const lengths = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
1178
+ return deletions.map((d) => {
1179
+ const skipped = d.bypassed.reduce(
1180
+ (sum, id2) => sum + (lengths.get(id2) ?? 0),
1181
+ 0
1182
+ );
1183
+ const drawn = Math.max(
1184
+ opts.minimumNodeLength,
1185
+ drawnOf(skipped) * ROUTE_FRACTION
1186
+ );
1187
+ const id = routes.get(d.edgeIndex);
1188
+ return {
1189
+ id,
1190
+ name: id,
1191
+ length: Math.round(drawn * 1e6 / opts.nodeLengthPerMegabase),
1192
+ depth: 0
1158
1193
  };
1159
- }
1160
- return out;
1161
- }
1162
- function attachment(segments, side) {
1163
- const last = segments.length - 1;
1164
- return side === "end" ? { at: segments[last], inward: segments[last - 1] } : { at: segments[0], inward: segments[1] };
1165
- }
1166
- var TANGENT_SPAN_FACTOR = 0.5;
1167
- var MAX_TANGENT = 80;
1168
- function spanOf(segments) {
1169
- let min = Infinity;
1170
- let max = -Infinity;
1171
- for (const s of segments) {
1172
- min = Math.min(min, s.x);
1173
- max = Math.max(max, s.x);
1174
- }
1175
- return { min, max };
1176
- }
1177
- function distanceToSpan(x, span) {
1178
- return Math.max(0, span.min - x, x - span.max);
1179
- }
1180
- function facingSide(segments, other, tieBreak) {
1181
- const startGap = distanceToSpan(segments[0].x, other);
1182
- const endGap = distanceToSpan(segments[segments.length - 1].x, other);
1183
- return startGap === endGap ? tieBreak : startGap < endGap ? "start" : "end";
1184
- }
1185
- function facingSides(fromSegments, toSegments) {
1186
- return {
1187
- from: facingSide(fromSegments, spanOf(toSegments), "end"),
1188
- to: facingSide(toSegments, spanOf(fromSegments), "start")
1189
- };
1194
+ });
1190
1195
  }
1191
- var APEX_FRACTION = 0.75;
1192
- var BOW_CLEARANCE = 1.15;
1193
- var ALONG_FRACTION = 0.35;
1194
- var MAX_ROW_BOW_PX = 3 * ROW_HEIGHT_PX;
1195
- function bowAround(p1x, p1y, p2x, p2y, toward, maxBow) {
1196
- const chord = Math.hypot(p2x - p1x, p2y - p1y);
1197
- const ux = chord === 0 ? 1 : (p2x - p1x) / chord;
1198
- const uy = chord === 0 ? 0 : (p2y - p1y) / chord;
1199
- let reach = 0;
1200
- let minAlong = Infinity;
1201
- let maxAlong = -Infinity;
1202
- for (const p of toward) {
1203
- const dx = p.x - p1x;
1204
- const dy = p.y - p1y;
1205
- const perp = dx * -uy + dy * ux;
1206
- if (Math.abs(perp) > Math.abs(reach)) {
1207
- reach = perp;
1196
+ function takeRoutes(positions, routes) {
1197
+ const nodePositions = { ...positions };
1198
+ const deletionRoutes = {};
1199
+ for (const [edgeIndex, id] of routes) {
1200
+ const route = nodePositions[id];
1201
+ delete nodePositions[id];
1202
+ if (route?.length) {
1203
+ deletionRoutes[edgeIndex] = route;
1208
1204
  }
1209
- const along = dx * ux + dy * uy;
1210
- minAlong = Math.min(minAlong, along);
1211
- maxAlong = Math.max(maxAlong, along);
1212
1205
  }
1213
- const span = toward.length ? maxAlong - minAlong : 0;
1214
- const uncapped = Math.max(
1215
- Math.abs(reach) * BOW_CLEARANCE / APEX_FRACTION,
1216
- span * ALONG_FRACTION
1217
- );
1218
- const size = maxBow === void 0 ? uncapped : Math.min(uncapped, maxBow);
1219
- return reach < 0 ? -size : size;
1220
- }
1221
- function yToXOf({ scaleX, scaleY }) {
1222
- return scaleY / scaleX;
1206
+ return { nodePositions, deletionRoutes };
1223
1207
  }
1224
- var PATH_RIBBON_SPACING = 3;
1225
- function drawnDirection(a, b, yToX) {
1226
- const dx = b.x - a.x;
1227
- const dy = (b.y - a.y) * yToX;
1228
- const len = Math.hypot(dx, dy);
1229
- return len === 0 ? void 0 : { x: dx / len, y: dy / len };
1208
+
1209
+ // src/layout/mergeRuns.ts
1210
+ function ownStrand(id) {
1211
+ return id.endsWith("-") ? "-" : "+";
1230
1212
  }
1231
- function pathRibbonOffsets(fromSegments, toSegments, count, axis, spacing = PATH_RIBBON_SPACING) {
1232
- const yToX = yToXOf(axis);
1233
- const dir = drawnDirection(
1234
- fromSegments[fromSegments.length - 1],
1235
- toSegments[0],
1236
- yToX
1237
- ) ?? drawnDirection(fromSegments[0], fromSegments.at(-1), yToX) ?? drawnDirection(toSegments[0], toSegments.at(-1), yToX) ?? { x: 1, y: 0 };
1238
- const offsets = [];
1239
- for (let i = 0; i < count; i++) {
1240
- const distance = (i - (count - 1) / 2) * spacing;
1241
- offsets.push({ x: -dir.y * distance, y: dir.x * distance / yToX });
1242
- }
1243
- return offsets;
1213
+ function flip(strand) {
1214
+ return strand === "+" ? "-" : "+";
1244
1215
  }
1245
- function scaleYOf(points, yToX) {
1246
- return points.map((p) => ({ x: p.x, y: p.y * yToX }));
1216
+ function sidesOf(edge) {
1217
+ const from = (edge.fromStrand ?? ownStrand(edge.from)) === ownStrand(edge.from) ? "end" : "start";
1218
+ const to = (edge.toStrand ?? ownStrand(edge.to)) === ownStrand(edge.to) ? "start" : "end";
1219
+ return { from, to };
1247
1220
  }
1248
- function unscaleYOf(curves, yToX) {
1249
- for (const c of curves) {
1250
- c.y0 /= yToX;
1251
- c.cy0 /= yToX;
1252
- c.cy1 /= yToX;
1253
- c.y1 /= yToX;
1221
+ function mergeRuns(graph, alone = /* @__PURE__ */ new Set()) {
1222
+ const sides = graph.edges.map(sidesOf);
1223
+ const at = /* @__PURE__ */ new Map();
1224
+ const attach = (id, side, ei) => {
1225
+ const key = `${id}|${side}`;
1226
+ const list = at.get(key);
1227
+ if (list) {
1228
+ list.push(ei);
1229
+ } else {
1230
+ at.set(key, [ei]);
1231
+ }
1232
+ };
1233
+ graph.edges.forEach((edge, ei) => {
1234
+ attach(edge.from, sides[ei].from, ei);
1235
+ attach(edge.to, sides[ei].to, ei);
1236
+ });
1237
+ const joinAfter = (a) => {
1238
+ const out = at.get(`${a}|end`);
1239
+ if (out?.length !== 1) {
1240
+ return void 0;
1241
+ }
1242
+ const ei = out[0];
1243
+ const edge = graph.edges[ei];
1244
+ return edge.from === a && edge.to !== a && !alone.has(a) && !alone.has(edge.to) && sides[ei].from === "end" && sides[ei].to === "start" && at.get(`${edge.to}|start`)?.length === 1 ? ei : void 0;
1245
+ };
1246
+ const joined = /* @__PURE__ */ new Set();
1247
+ const runOf = /* @__PURE__ */ new Map();
1248
+ const runs = /* @__PURE__ */ new Map();
1249
+ for (const node of graph.nodes) {
1250
+ const into = at.get(`${node.id}|start`);
1251
+ const pred = into?.length === 1 ? graph.edges[into[0]].from : void 0;
1252
+ if (runOf.has(node.id) || pred !== void 0 && joinAfter(pred) === into[0]) {
1253
+ continue;
1254
+ }
1255
+ const run = [node.id];
1256
+ for (; ; ) {
1257
+ const ei = joinAfter(run.at(-1));
1258
+ const next = ei === void 0 ? void 0 : graph.edges[ei].to;
1259
+ if (next === void 0 || runOf.has(next)) {
1260
+ break;
1261
+ }
1262
+ joined.add(ei);
1263
+ run.push(next);
1264
+ }
1265
+ for (const id of run) {
1266
+ runOf.set(id, node.id);
1267
+ }
1268
+ if (run.length > 1) {
1269
+ runs.set(node.id, run);
1270
+ }
1254
1271
  }
1255
- return curves;
1272
+ for (const node of graph.nodes) {
1273
+ if (!runOf.has(node.id)) {
1274
+ runOf.set(node.id, node.id);
1275
+ }
1276
+ }
1277
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
1278
+ const nodes = [];
1279
+ for (const node of graph.nodes) {
1280
+ if (runOf.get(node.id) !== node.id) {
1281
+ continue;
1282
+ }
1283
+ const run = runs.get(node.id);
1284
+ if (!run) {
1285
+ nodes.push(node);
1286
+ continue;
1287
+ }
1288
+ const members = run.map((id) => byId.get(id));
1289
+ const backbone = members.filter(isBackbone);
1290
+ const first = backbone.length ? backbone.reduce((a, b) => a.stable.start <= b.stable.start ? a : b) : members[0];
1291
+ nodes.push({
1292
+ id: node.id,
1293
+ name: node.name,
1294
+ length: members.reduce((sum, n2) => sum + n2.length, 0),
1295
+ depth: node.depth,
1296
+ stable: first.stable
1297
+ });
1298
+ }
1299
+ const seen = /* @__PURE__ */ new Set();
1300
+ const edges = [];
1301
+ graph.edges.forEach((edge, ei) => {
1302
+ const from = runOf.get(edge.from);
1303
+ const to = runOf.get(edge.to);
1304
+ const side = sides[ei];
1305
+ const key = `${from}|${side.from}>${to}|${side.to}`;
1306
+ if (joined.has(ei) || seen.has(key)) {
1307
+ return;
1308
+ }
1309
+ seen.add(key);
1310
+ edges.push({
1311
+ from,
1312
+ to,
1313
+ ...side.from === "start" ? { fromStrand: flip(ownStrand(from)) } : {},
1314
+ ...side.to === "end" ? { toStrand: flip(ownStrand(to)) } : {}
1315
+ });
1316
+ });
1317
+ return {
1318
+ graph: { ...graph, nodes, edges, paths: void 0, pathVisits: void 0 },
1319
+ runs
1320
+ };
1256
1321
  }
1257
- var SELF_LOOP_MIN_PX = 12;
1258
- var SELF_LOOP_MAX_PX = 40;
1259
- function selfLoopReach(segments, scale) {
1260
- let length = 0;
1261
- for (let i = 1; i < segments.length; i++) {
1262
- length += Math.hypot(
1263
- segments[i].x - segments[i - 1].x,
1264
- segments[i].y - segments[i - 1].y
1322
+ function splitRuns(positions, runs, weightOf) {
1323
+ const out = { ...positions };
1324
+ for (const [id, members] of runs) {
1325
+ const line = positions[id];
1326
+ if (!line?.length) {
1327
+ continue;
1328
+ }
1329
+ delete out[id];
1330
+ const arc = arcPrefix(line);
1331
+ const total = arc.at(-1);
1332
+ const weights = members.map((m) => Math.max(weightOf(m), 0));
1333
+ const weightSum = weights.reduce((a, b) => a + b, 0) || members.length;
1334
+ let from = 0;
1335
+ members.forEach((member, k) => {
1336
+ const share = weightSum ? weights[k] / weightSum * total : total / members.length;
1337
+ const to = k === members.length - 1 ? total : from + share;
1338
+ out[member] = slice(line, arc, from, to);
1339
+ from = to;
1340
+ });
1341
+ }
1342
+ return out;
1343
+ }
1344
+ function polylineSlice(line, from, to) {
1345
+ if (line.length === 0) {
1346
+ return [];
1347
+ }
1348
+ const arc = arcPrefix(line);
1349
+ const total = arc.at(-1);
1350
+ return slice(line, arc, from * total, to * total);
1351
+ }
1352
+ function polylineMidpoint(line) {
1353
+ const arc = arcPrefix(line);
1354
+ return pointAt(line, arc, arc.at(-1) / 2);
1355
+ }
1356
+ function arcPrefix(line) {
1357
+ const arc = [0];
1358
+ for (let i = 1; i < line.length; i++) {
1359
+ arc.push(
1360
+ arc[i - 1] + Math.hypot(line[i].x - line[i - 1].x, line[i].y - line[i - 1].y)
1265
1361
  );
1266
1362
  }
1267
- const nodePx = length * scale;
1268
- const reachPx = Math.min(
1269
- SELF_LOOP_MAX_PX,
1270
- Math.max(SELF_LOOP_MIN_PX, nodePx / 2)
1271
- );
1272
- return reachPx / Math.max(scale, Number.EPSILON);
1363
+ return arc;
1273
1364
  }
1274
- function selfLinkOf(edge) {
1275
- if (edge.from !== edge.to) {
1276
- return false;
1365
+ function pointAt(line, arc, s) {
1366
+ if (s <= 0) {
1367
+ return { ...line[0] };
1277
1368
  }
1278
- return edge.fromStrand && edge.toStrand && edge.fromStrand !== edge.toStrand ? edge.fromStrand === "+" ? "end" : "start" : true;
1369
+ let i = 1;
1370
+ while (i < arc.length && arc[i] < s) {
1371
+ i++;
1372
+ }
1373
+ if (i >= arc.length) {
1374
+ return { ...line.at(-1) };
1375
+ }
1376
+ const span = arc[i] - arc[i - 1];
1377
+ const t = span > 0 ? (s - arc[i - 1]) / span : 0;
1378
+ const a = line[i - 1];
1379
+ const b = line[i];
1380
+ return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
1279
1381
  }
1280
- function hairpinCurves(attach, reach, offsetX, offsetY) {
1281
- const { at, inward } = attach;
1282
- const dx = inward ? at.x - inward.x : 1;
1283
- const dy = inward ? at.y - inward.y : 0;
1284
- const len = Math.hypot(dx, dy) || 1;
1285
- const ux = dx / len;
1286
- const uy = dy / len;
1287
- const px = at.x + offsetX;
1288
- const py = at.y + offsetY;
1289
- const nearX = px + ux * reach / 3;
1290
- const nearY = py + uy * reach / 3;
1291
- const apexX = px + ux * reach;
1292
- const apexY = py + uy * reach;
1293
- const sideX = -uy * reach / 2;
1294
- const sideY = ux * reach / 2;
1295
- return [
1296
- {
1297
- x0: px,
1298
- y0: py,
1299
- cx0: nearX,
1300
- cy0: nearY,
1301
- cx1: apexX + sideX,
1302
- cy1: apexY + sideY,
1303
- x1: apexX,
1304
- y1: apexY
1305
- },
1306
- {
1307
- x0: apexX,
1308
- y0: apexY,
1309
- cx0: apexX - sideX,
1310
- cy0: apexY - sideY,
1311
- cx1: nearX,
1312
- cy1: nearY,
1313
- x1: px,
1314
- y1: py
1382
+ function slice(line, arc, from, to) {
1383
+ const points = [pointAt(line, arc, from)];
1384
+ for (let i = 0; i < line.length; i++) {
1385
+ if (arc[i] > from && arc[i] < to) {
1386
+ points.push({ ...line[i] });
1315
1387
  }
1316
- ];
1388
+ }
1389
+ points.push(pointAt(line, arc, to));
1390
+ return points;
1317
1391
  }
1318
- function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY, axis, bypassed = [], maxBowIn) {
1319
- const scale = axis.scaleX;
1320
- const yToX = yToXOf(axis);
1321
- const maxBow = maxBowIn ?? (axis.pixelRows ? MAX_ROW_BOW_PX : void 0);
1322
- if (yToX !== 1) {
1323
- return unscaleYOf(
1324
- computeEdgeCurves(
1325
- scaleYOf(fromSegments, yToX),
1326
- scaleYOf(toSegments, yToX),
1327
- selfLink,
1328
- offsetX,
1329
- offsetY * yToX,
1330
- { scaleX: scale, scaleY: scale },
1331
- scaleYOf(bypassed, yToX),
1332
- // the cap is a y measure and the body works in x units, so it converts
1333
- // with everything else that crosses this boundary
1334
- maxBow === void 0 ? void 0 : maxBow * yToX
1335
- ),
1336
- yToX
1337
- );
1392
+
1393
+ // src/layout/orientToReference.ts
1394
+ function orientToReference(graph, positions) {
1395
+ const backbone = graph.nodes.filter(
1396
+ (node) => isBackbone(node) && (positions[node.id]?.length ?? 0) > 0
1397
+ );
1398
+ if (backbone.length < 2) {
1399
+ return positions;
1338
1400
  }
1339
- if (typeof selfLink === "string") {
1340
- return hairpinCurves(
1341
- attachment(fromSegments, selfLink),
1342
- selfLoopReach(fromSegments, scale),
1343
- offsetX,
1344
- offsetY
1345
- );
1401
+ const points = backbone.map((node) => {
1402
+ const segments = positions[node.id];
1403
+ const mid = segments[Math.floor(segments.length / 2)];
1404
+ return {
1405
+ x: mid.x,
1406
+ y: mid.y,
1407
+ r: node.stable.start + node.length / 2,
1408
+ w: node.length
1409
+ };
1410
+ });
1411
+ const weight = points.reduce((sum, p) => sum + p.w, 0);
1412
+ const cx = points.reduce((sum, p) => sum + p.x * p.w, 0) / weight;
1413
+ const cy = points.reduce((sum, p) => sum + p.y * p.w, 0) / weight;
1414
+ const cr = points.reduce((sum, p) => sum + p.r * p.w, 0) / weight;
1415
+ let sxr = 0;
1416
+ let syr = 0;
1417
+ for (const p of points) {
1418
+ sxr += p.w * (p.x - cx) * (p.r - cr);
1419
+ syr += p.w * (p.y - cy) * (p.r - cr);
1346
1420
  }
1347
- const sides = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
1348
- const fromAttach = attachment(fromSegments, sides.from);
1349
- const toAttach = attachment(toSegments, sides.to);
1350
- const fromEnd = fromAttach.at;
1351
- const toStart = toAttach.at;
1352
- const p1x = fromEnd.x + offsetX;
1353
- const p1y = fromEnd.y + offsetY;
1354
- const p2x = toStart.x + offsetX;
1355
- const p2y = toStart.y + offsetY;
1356
- if (selfLink) {
1357
- let segDirX = 1;
1358
- let segDirY = 0;
1359
- if (fromAttach.inward) {
1360
- const dx = fromEnd.x - fromAttach.inward.x;
1361
- const dy = fromEnd.y - fromAttach.inward.y;
1362
- const len = Math.hypot(dx, dy);
1363
- if (len > 0) {
1364
- segDirX = dx / len;
1365
- segDirY = dy / len;
1421
+ const angle = -Math.atan2(syr, sxr);
1422
+ const c = Math.cos(angle);
1423
+ const s = Math.sin(angle);
1424
+ const out = {};
1425
+ for (const [id, segments] of Object.entries(positions)) {
1426
+ out[id] = segments.map((p) => {
1427
+ const x = p.x - cx;
1428
+ const y = p.y - cy;
1429
+ return { x: cx + c * x - s * y, y: cy + s * x + c * y };
1430
+ });
1431
+ }
1432
+ let below = 0;
1433
+ let above = 0;
1434
+ for (const node of graph.nodes) {
1435
+ if (isBackbone(node)) {
1436
+ continue;
1437
+ }
1438
+ for (const p of out[node.id] ?? []) {
1439
+ if (p.y > cy) {
1440
+ below++;
1441
+ } else {
1442
+ above++;
1366
1443
  }
1367
1444
  }
1368
- const ext = selfLoopReach(fromSegments, scale);
1369
- const perpX = -segDirY;
1370
- const perpY = segDirX;
1371
- const midX = (fromEnd.x + toStart.x) / 2 + offsetX + perpX * ext;
1372
- const midY = (fromEnd.y + toStart.y) / 2 + offsetY + perpY * ext;
1373
- const cp1x = p1x + segDirX * ext;
1374
- const cp1y = p1y + segDirY * ext;
1375
- const cp2x = p2x - segDirX * ext;
1376
- const cp2y = p2y - segDirY * ext;
1377
- return [
1378
- {
1379
- x0: p1x,
1380
- y0: p1y,
1381
- cx0: cp1x,
1382
- cy0: cp1y,
1383
- cx1: cp1x + perpX * ext,
1384
- cy1: cp1y + perpY * ext,
1385
- x1: midX,
1386
- y1: midY
1387
- },
1388
- {
1389
- x0: midX,
1390
- y0: midY,
1391
- cx0: cp2x + perpX * ext,
1392
- cy0: cp2y + perpY * ext,
1393
- cx1: cp2x,
1394
- cy1: cp2y,
1395
- x1: p2x,
1396
- y1: p2y
1445
+ }
1446
+ if (above > below) {
1447
+ for (const segments of Object.values(out)) {
1448
+ for (const p of segments) {
1449
+ p.y = 2 * cy - p.y;
1397
1450
  }
1398
- ];
1399
- } else {
1400
- const fromPrev = fromAttach.inward ?? {
1401
- x: fromEnd.x - (toStart.x - fromEnd.x),
1402
- y: fromEnd.y - (toStart.y - fromEnd.y)
1403
- };
1404
- const toNext = toAttach.inward ?? {
1405
- x: toStart.x - (fromEnd.x - toStart.x),
1406
- y: toStart.y - (fromEnd.y - toStart.y)
1407
- };
1408
- const dist = Math.hypot(p2x - p1x, p2y - p1y);
1409
- const tangentProj = (prev, at, towardX, towardY) => {
1410
- const dx = at.x - prev.x;
1411
- const dy = at.y - prev.y;
1412
- const len = Math.hypot(dx, dy);
1413
- const along = len === 0 ? 0 : (towardX * dx + towardY * dy) / len;
1414
- return Math.max(
1415
- 0,
1416
- Math.min(dist * TANGENT_SPAN_FACTOR, MAX_TANGENT, along * 0.5)
1417
- );
1418
- };
1419
- const towardToX = toStart.x - fromEnd.x;
1420
- const towardToY = toStart.y - fromEnd.y;
1421
- const [cx1, cy1] = projectLine(
1422
- fromPrev.x,
1423
- fromPrev.y,
1424
- fromEnd.x,
1425
- fromEnd.y,
1426
- tangentProj(fromPrev, fromEnd, towardToX, towardToY)
1427
- );
1428
- const [cx2, cy2] = projectLine(
1429
- toNext.x,
1430
- toNext.y,
1431
- toStart.x,
1432
- toStart.y,
1433
- tangentProj(toNext, toStart, -towardToX, -towardToY)
1434
- );
1435
- const bulge = bypassed.length ? bowAround(p1x, p1y, p2x, p2y, bypassed, maxBow) : 0;
1436
- const chordLen = dist === 0 ? 1 : dist;
1437
- const ux = (p2x - p1x) / chordLen;
1438
- const uy = (p2y - p1y) / chordLen;
1439
- const bulgeX = -uy * bulge;
1440
- const bulgeY = ux * bulge;
1441
- const spreadX = ux * Math.abs(bulge);
1442
- const spreadY = uy * Math.abs(bulge);
1443
- return [
1444
- {
1445
- x0: p1x,
1446
- y0: p1y,
1447
- cx0: cx1 + offsetX + bulgeX - spreadX,
1448
- cy0: cy1 + offsetY + bulgeY - spreadY,
1449
- cx1: cx2 + offsetX + bulgeX + spreadX,
1450
- cy1: cy2 + offsetY + bulgeY + spreadY,
1451
- x1: p2x,
1452
- y1: p2y
1451
+ }
1452
+ }
1453
+ return out;
1454
+ }
1455
+
1456
+ // src/referenceSpan.ts
1457
+ var MAX_BACKBONE_HOPS = 4;
1458
+ function buildNeighbors(graph) {
1459
+ const neighbors = /* @__PURE__ */ new Map();
1460
+ function add(from, to) {
1461
+ const list = neighbors.get(from);
1462
+ if (list) {
1463
+ list.push(to);
1464
+ } else {
1465
+ neighbors.set(from, [to]);
1466
+ }
1467
+ }
1468
+ for (const edge of graph.edges) {
1469
+ add(edge.from, edge.to);
1470
+ add(edge.to, edge.from);
1471
+ }
1472
+ return neighbors;
1473
+ }
1474
+ function flankingSpan({
1475
+ nodeId,
1476
+ nodeById,
1477
+ neighbors
1478
+ }) {
1479
+ const seen = /* @__PURE__ */ new Set([nodeId]);
1480
+ let frontier = [nodeId];
1481
+ let minEnd = Infinity;
1482
+ let maxStart = -Infinity;
1483
+ for (let hop = 0; hop < MAX_BACKBONE_HOPS && frontier.length > 0; hop++) {
1484
+ const next = [];
1485
+ for (const id of frontier) {
1486
+ const adjacent = neighbors.get(id);
1487
+ if (adjacent) {
1488
+ for (const neighborId of adjacent) {
1489
+ if (!seen.has(neighborId)) {
1490
+ seen.add(neighborId);
1491
+ const neighbor = nodeById.get(neighborId);
1492
+ if (neighbor && isBackbone(neighbor)) {
1493
+ minEnd = Math.min(minEnd, neighbor.stable.start + neighbor.length);
1494
+ maxStart = Math.max(maxStart, neighbor.stable.start);
1495
+ } else {
1496
+ next.push(neighborId);
1497
+ }
1498
+ }
1499
+ }
1500
+ }
1501
+ }
1502
+ frontier = next;
1503
+ }
1504
+ return minEnd === Infinity ? void 0 : {
1505
+ start: Math.min(minEnd, maxStart),
1506
+ end: Math.max(minEnd, maxStart)
1507
+ };
1508
+ }
1509
+ function nodeReferenceSpan({
1510
+ nodeId,
1511
+ nodeById,
1512
+ neighbors
1513
+ }) {
1514
+ const node = nodeById.get(nodeId);
1515
+ return node === void 0 ? void 0 : isBackbone(node) ? { start: node.stable.start, end: node.stable.start + node.length } : flankingSpan({ nodeId, nodeById, neighbors });
1516
+ }
1517
+ function referenceMidpoints(graph) {
1518
+ const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
1519
+ const neighbors = buildNeighbors(graph);
1520
+ const midpoints = /* @__PURE__ */ new Map();
1521
+ for (const node of graph.nodes) {
1522
+ const span = nodeReferenceSpan({ nodeId: node.id, nodeById, neighbors });
1523
+ if (span) {
1524
+ midpoints.set(node.id, (span.start + span.end) / 2);
1525
+ }
1526
+ }
1527
+ return midpoints;
1528
+ }
1529
+
1530
+ // src/layout/referenceSeeds.ts
1531
+ var LANE_GAP = 30;
1532
+ function referenceSeeds(graph, scaling) {
1533
+ const { opts } = scaling;
1534
+ const drawn = new Map(
1535
+ scaling.nodes.map((node) => [node.id, drawnNodeLength(opts, node.length)])
1536
+ );
1537
+ const drawnOf = (id) => drawn.get(id) ?? opts.minimumNodeLength;
1538
+ const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
1539
+ const seeds = /* @__PURE__ */ new Map();
1540
+ let x = 0;
1541
+ for (const node of backbone) {
1542
+ seeds.set(node.id, { x, y: 0 });
1543
+ x += drawnOf(node.id) + opts.edgeLength;
1544
+ }
1545
+ const adjacent = buildNeighbors(graph);
1546
+ const depth = new Map(backbone.map((node) => [node.id, 0]));
1547
+ const queue = backbone.map((node) => node.id);
1548
+ for (const id of queue) {
1549
+ const at = seeds.get(id);
1550
+ const next = depth.get(id) + 1;
1551
+ for (const neighbour of adjacent.get(id) ?? []) {
1552
+ if (depth.has(neighbour)) {
1553
+ continue;
1453
1554
  }
1454
- ];
1555
+ depth.set(neighbour, next);
1556
+ seeds.set(neighbour, { x: at.x + drawnOf(id) / 2, y: LANE_GAP * next });
1557
+ queue.push(neighbour);
1558
+ }
1455
1559
  }
1456
- }
1457
-
1458
- // src/deletionEdges.ts
1459
- var MIN_DELETION_BP = 1;
1460
- function bypassedPoints(nodePositions, bypassed) {
1461
- return bypassed.flatMap((id) => nodePositions[id] ?? []);
1462
- }
1463
- function deletionArcCurves(nodePositions, deletion, axis) {
1464
- const from = nodePositions[deletion.from];
1465
- const to = nodePositions[deletion.to];
1466
- return from?.length && to?.length ? computeEdgeCurves(
1467
- from,
1468
- to,
1469
- deletion.from === deletion.to,
1470
- 0,
1471
- 0,
1472
- axis,
1473
- bypassedPoints(nodePositions, deletion.bypassed)
1474
- ) : void 0;
1475
- }
1476
- function backboneBySequence(graph) {
1477
- const bySequence = /* @__PURE__ */ new Map();
1560
+ let unreached = 0;
1478
1561
  for (const node of graph.nodes) {
1479
- if (isBackbone(node)) {
1480
- const list = bySequence.get(node.stable.refName);
1481
- if (list) {
1482
- list.push(node);
1483
- } else {
1484
- bySequence.set(node.stable.refName, [node]);
1485
- }
1562
+ if (!seeds.has(node.id)) {
1563
+ seeds.set(node.id, {
1564
+ x: unreached++ * x / graph.nodes.length,
1565
+ y: LANE_GAP * 3
1566
+ });
1486
1567
  }
1487
1568
  }
1488
- for (const list of bySequence.values()) {
1489
- list.sort((a, b) => a.stable.start - b.stable.start);
1569
+ return seeds;
1570
+ }
1571
+ function seededNodes(graph, scaling) {
1572
+ const seeds = referenceSeeds(graph, scaling);
1573
+ return scaling.nodes.map((node) => ({ ...node, ...seeds.get(node.id) }));
1574
+ }
1575
+
1576
+ // src/pipeline.ts
1577
+ function loadGraph(text, name, opts = {}) {
1578
+ const graph = anchorGraph(
1579
+ convertGFAToGraph(parseGFA(text), name),
1580
+ opts.referencePath
1581
+ );
1582
+ if (graph.nodes.length === 0) {
1583
+ throw new Error(`No graph segments in ${name}`);
1490
1584
  }
1491
- return bySequence;
1585
+ if (opts.maxNodes !== void 0 && graph.nodes.length > opts.maxNodes) {
1586
+ throw new Error(
1587
+ `Graph too large to draw: ${graph.nodes.length.toLocaleString()} nodes (limit ${opts.maxNodes.toLocaleString()}). Zoom in to a smaller region, or raise maxGraphNodes on this view.`
1588
+ );
1589
+ }
1590
+ return graph;
1492
1591
  }
1493
- function bypassedNodes(nodes, start, end) {
1494
- const bypassed = [];
1495
- if (nodes) {
1496
- for (let i = firstNodeAtOrAfter(nodes, start); i < nodes.length; i++) {
1497
- const node = nodes[i];
1498
- if (node.stable.start >= end) {
1499
- break;
1500
- }
1501
- if (node.stable.start + node.length <= end) {
1502
- bypassed.push(node.id);
1503
- }
1592
+ function engineRequest(graph, scaling, settings) {
1593
+ const anchored = graph.nodes.some(isBackbone);
1594
+ return {
1595
+ graph: {
1596
+ nodes: anchored ? seededNodes(graph, scaling) : scaling.nodes,
1597
+ edges: graph.edges
1598
+ },
1599
+ options: {
1600
+ quality: settings.quality,
1601
+ linearLayout: settings.linearLayout,
1602
+ ...scaling.opts,
1603
+ ...anchored ? { rotateComponents: false } : {}
1604
+ }
1605
+ };
1606
+ }
1607
+ function engineKey(graph, settings) {
1608
+ const anchored = graph.nodes.some(isBackbone);
1609
+ return `${settings.quality}|${settings.linearLayout}|${settings.bubbleSpread}|${anchored}|${graph.referencePath ?? ""}`;
1610
+ }
1611
+ async function forceLayout(graph, settings, engine) {
1612
+ const spread = spreadFor(settings.bubbleSpread);
1613
+ const deletions = deletionEdges(graph);
1614
+ const routed = withDeletionRoutes(graph, deletions);
1615
+ const merged = mergeRuns(routed.graph, routed.ids);
1616
+ const runs = {
1617
+ ...merged.graph,
1618
+ nodes: merged.graph.nodes.filter((n2) => !routed.ids.has(n2.id))
1619
+ };
1620
+ const engineScaling = layoutScaling(runs, spread);
1621
+ const { result, duration } = await engine(
1622
+ engineRequest(
1623
+ merged.graph,
1624
+ {
1625
+ ...engineScaling,
1626
+ nodes: [
1627
+ ...engineScaling.nodes,
1628
+ ...routeNodes(
1629
+ graph,
1630
+ deletions,
1631
+ routed.routes,
1632
+ drawnLengthOf(runs, spread),
1633
+ engineScaling.opts
1634
+ )
1635
+ ]
1636
+ },
1637
+ settings
1638
+ )
1639
+ );
1640
+ const scaling = layoutScaling(graph, spread);
1641
+ const drawn = new Map(
1642
+ scaling.nodes.map((n2) => [n2.id, drawnNodeLength(scaling.opts, n2.length)])
1643
+ );
1644
+ const positions = splitRuns(
1645
+ result.nodePositions,
1646
+ merged.runs,
1647
+ (id) => drawn.get(id) ?? 0
1648
+ );
1649
+ const { nodePositions, deletionRoutes } = takeRoutes(
1650
+ graph.nodes.some(isBackbone) ? orientToReference(graph, positions) : positions,
1651
+ routed.routes
1652
+ );
1653
+ return {
1654
+ result: {
1655
+ ...result,
1656
+ nodePositions,
1657
+ ...deletions.length ? { deletionRoutes, extent: layoutExtent(deletionRoutes) } : {}
1658
+ },
1659
+ duration
1660
+ };
1661
+ }
1662
+ function layoutExtent(nodePositions) {
1663
+ let minX = Infinity;
1664
+ let minY = Infinity;
1665
+ let maxX = -Infinity;
1666
+ let maxY = -Infinity;
1667
+ for (const segments of Object.values(nodePositions)) {
1668
+ for (const seg of segments) {
1669
+ minX = Math.min(minX, seg.x);
1670
+ minY = Math.min(minY, seg.y);
1671
+ maxX = Math.max(maxX, seg.x);
1672
+ maxY = Math.max(maxY, seg.y);
1504
1673
  }
1505
1674
  }
1506
- return bypassed;
1675
+ return { minX, minY, maxX, maxY };
1507
1676
  }
1508
- function skipsBetween(edge, from, to, forwards) {
1509
- const { fromStrand, toStrand } = edge;
1510
- if (fromStrand === void 0 || toStrand === void 0) {
1511
- return true;
1677
+ var FIT_PADDING = 40;
1678
+ var MIN_ZOOM = 1e-6;
1679
+ var MAX_ZOOM = 100;
1680
+ function clampZoom(zoom) {
1681
+ return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
1682
+ }
1683
+ function fittedTranslateY(bounds, usableHeight, yScale, padTop = FIT_PADDING) {
1684
+ const leftoverY = usableHeight - bounds.h * yScale;
1685
+ return padTop - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
1686
+ }
1687
+ function fitTransform(bounds, paneWidth, paneHeight, pixelRows, {
1688
+ minScale = 0,
1689
+ padLeft = FIT_PADDING,
1690
+ padTop = FIT_PADDING,
1691
+ padRight = FIT_PADDING,
1692
+ padBottom = FIT_PADDING
1693
+ } = {}) {
1694
+ const usableWidth = paneWidth - padLeft - padRight;
1695
+ const usableHeight = paneHeight - padTop - padBottom;
1696
+ if (usableWidth <= 0 || usableHeight <= 0 || !(bounds.w > 0 || !pixelRows && bounds.h > 0)) {
1697
+ return void 0;
1512
1698
  }
1513
- const leavesRight = fromStrand === (from.stable.strand ?? "+");
1514
- const arrivesLeft = toStrand === (to.stable.strand ?? "+");
1515
- return forwards ? leavesRight && arrivesLeft : !leavesRight && !arrivesLeft;
1699
+ const fitX = bounds.w > 0 ? usableWidth / bounds.w : Infinity;
1700
+ const fitY = bounds.h > 0 ? usableHeight / bounds.h : Infinity;
1701
+ const scale = clampZoom(
1702
+ Math.max(minScale, pixelRows ? fitX : Math.min(fitX, fitY))
1703
+ );
1704
+ const leftoverX = usableWidth - bounds.w * scale;
1705
+ return {
1706
+ scale,
1707
+ translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
1708
+ translateY: fittedTranslateY(
1709
+ bounds,
1710
+ usableHeight,
1711
+ pixelRows ? 1 : scale,
1712
+ padTop
1713
+ )
1714
+ };
1516
1715
  }
1517
- function deletionEdges(graph) {
1518
- const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
1519
- const backbone = backboneBySequence(graph);
1520
- const found = [];
1521
- for (let edgeIndex = 0; edgeIndex < graph.edges.length; edgeIndex++) {
1522
- const edge = graph.edges[edgeIndex];
1523
- const from = byId.get(edge.from);
1524
- const to = byId.get(edge.to);
1525
- if (from !== void 0 && to !== void 0 && isBackbone(from) && isBackbone(to) && from.stable.refName === to.stable.refName) {
1526
- const forwards = from.stable.start <= to.stable.start;
1527
- const [left, right] = forwards ? [from, to] : [to, from];
1528
- const start = left.stable.start + left.length;
1529
- const end = right.stable.start;
1530
- const bp = end - start;
1531
- if (bp >= MIN_DELETION_BP && skipsBetween(edge, from, to, forwards)) {
1532
- const refName = left.stable.refName;
1533
- found.push({
1534
- edgeIndex,
1535
- from: edge.from,
1536
- to: edge.to,
1537
- refName,
1538
- start,
1539
- end,
1540
- bp,
1541
- bypassed: bypassedNodes(backbone.get(refName), start, end)
1542
- });
1543
- }
1544
- }
1716
+ function drawingBounds(layout, opts = {}) {
1717
+ let { minX, minY, maxX, maxY } = layoutExtent(layout.nodePositions);
1718
+ const { region } = opts;
1719
+ if (layout.referenceAxis && region && region.end > region.start) {
1720
+ minX = region.start;
1721
+ maxX = region.end;
1545
1722
  }
1546
- return found;
1723
+ const extent = opts.extent ?? layout.extent;
1724
+ if (extent) {
1725
+ minX = Math.min(minX, extent.minX ?? minX);
1726
+ minY = Math.min(minY, extent.minY ?? minY);
1727
+ maxX = Math.max(maxX, extent.maxX ?? maxX);
1728
+ maxY = Math.max(maxY, extent.maxY ?? maxY);
1729
+ }
1730
+ return { minX, minY, w: maxX - minX, h: maxY - minY };
1547
1731
  }
1548
1732
 
1549
1733
  // src/overlayLabels.ts
@@ -1704,15 +1888,20 @@ function labelOrder(nodePositions, nodeLengths) {
1704
1888
  return order;
1705
1889
  }
1706
1890
  var arcCache = /* @__PURE__ */ new WeakMap();
1707
- function arcPlacements(nodePositions, deletions, axis, version2) {
1891
+ function arcPlacements(nodePositions, deletions, axis, version2, routes) {
1708
1892
  const { scaleX, scaleY } = axis;
1709
1893
  const cached2 = arcCache.get(nodePositions);
1710
- if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version2) {
1894
+ if (cached2?.deletions === deletions && cached2.routes === routes && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version2) {
1711
1895
  return cached2.arcs;
1712
1896
  }
1713
1897
  const arcs = [];
1714
1898
  for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
1715
- const curves = deletionArcCurves(nodePositions, deletion, axis);
1899
+ const curves = deletionArcCurves(
1900
+ nodePositions,
1901
+ deletion,
1902
+ axis,
1903
+ routes?.[deletion.edgeIndex]
1904
+ );
1716
1905
  const apex = curves ? curveMidpoint(curves) : void 0;
1717
1906
  if (curves && apex) {
1718
1907
  const { minX, minY, maxX, maxY } = curveBounds(curves);
@@ -1725,13 +1914,21 @@ function arcPlacements(nodePositions, deletions, axis, version2) {
1725
1914
  });
1726
1915
  }
1727
1916
  }
1728
- arcCache.set(nodePositions, { deletions, scaleX, scaleY, version: version2, arcs });
1917
+ arcCache.set(nodePositions, {
1918
+ deletions,
1919
+ routes,
1920
+ scaleX,
1921
+ scaleY,
1922
+ version: version2,
1923
+ arcs
1924
+ });
1729
1925
  return arcs;
1730
1926
  }
1731
1927
  function sizeLabelCandidates({
1732
1928
  nodePositions,
1733
1929
  nodeLengths,
1734
1930
  deletions,
1931
+ deletionRoutes,
1735
1932
  alleleDeletions,
1736
1933
  axis,
1737
1934
  translateX,
@@ -1747,7 +1944,8 @@ function sizeLabelCandidates({
1747
1944
  nodePositions,
1748
1945
  deletions,
1749
1946
  axis,
1750
- version2
1947
+ version2,
1948
+ deletionRoutes
1751
1949
  )) {
1752
1950
  if (extent >= MIN_DELETION_LABEL_PX) {
1753
1951
  const text = deletionText(deletion.bp);
@@ -3262,10 +3460,10 @@ function brightenAbgr(c, factor) {
3262
3460
 
3263
3461
  // src/util/edgeCurves.ts
3264
3462
  var cache = /* @__PURE__ */ new WeakMap();
3265
- function baseEdgeCurves(nodePositions, graph, axis, deletions, version2) {
3463
+ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2, routes) {
3266
3464
  const { scaleX, scaleY } = axis;
3267
3465
  const cached2 = cache.get(nodePositions);
3268
- if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.version === version2) {
3466
+ if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.routes === routes && cached2.version === version2) {
3269
3467
  return cached2.curves;
3270
3468
  }
3271
3469
  const curves = /* @__PURE__ */ new Map();
@@ -3275,9 +3473,10 @@ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2) {
3275
3473
  const to = nodePositions[edge.to];
3276
3474
  if (from?.length && to?.length) {
3277
3475
  const bypassed = deletions?.get(ei);
3476
+ const route = bypassed && routes?.[ei];
3278
3477
  curves.set(
3279
3478
  ei,
3280
- computeEdgeCurves(
3479
+ route ? routedEdgeCurves(from, to, route) : computeEdgeCurves(
3281
3480
  from,
3282
3481
  to,
3283
3482
  selfLinkOf(edge),
@@ -3294,6 +3493,7 @@ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2) {
3294
3493
  scaleX,
3295
3494
  scaleY,
3296
3495
  deletions,
3496
+ routes,
3297
3497
  version: version2,
3298
3498
  curves
3299
3499
  });
@@ -3758,6 +3958,7 @@ function buildGeometry(options) {
3758
3958
  viewportBounds,
3759
3959
  referenceRamp,
3760
3960
  deletions,
3961
+ deletionRoutes,
3761
3962
  hiddenEdges,
3762
3963
  version: version2 = 0
3763
3964
  } = options;
@@ -3769,7 +3970,8 @@ function buildGeometry(options) {
3769
3970
  graph,
3770
3971
  axis,
3771
3972
  deletions,
3772
- version2
3973
+ version2,
3974
+ deletionRoutes
3773
3975
  );
3774
3976
  const nodeStrokes = [];
3775
3977
  const nodeStrokeRuns = /* @__PURE__ */ new Map();
@@ -4724,6 +4926,7 @@ function layoutLabels(m) {
4724
4926
  nodePositions: m.nodePositions,
4725
4927
  nodeLengths: m.nodeLengths,
4726
4928
  deletions: m.showDeletionEdges ? m.deletions : [],
4929
+ deletionRoutes: m.deletionRoutes,
4727
4930
  alleleDeletions: m.alleleDeletions,
4728
4931
  axis: m.axisScale,
4729
4932
  translateX,
@@ -5007,6 +5210,56 @@ function walkRowsKeyTree(entries, x, y) {
5007
5210
  )
5008
5211
  };
5009
5212
  }
5213
+ function coalesceRuns(runs, pxPerBp) {
5214
+ const minBp = 1 / pxPerBp;
5215
+ const out = [];
5216
+ let bucket = [];
5217
+ const flush = () => {
5218
+ if (bucket.length === 0) {
5219
+ return;
5220
+ }
5221
+ if (bucket.length === 1) {
5222
+ out.push(bucket[0]);
5223
+ } else {
5224
+ let on = 0;
5225
+ let off = 0;
5226
+ for (const r of bucket) {
5227
+ if (r.onReference) {
5228
+ on += r.bp;
5229
+ } else {
5230
+ off += r.bp;
5231
+ }
5232
+ }
5233
+ const onReference = on >= off;
5234
+ const lead = bucket.find((r) => r.onReference === onReference);
5235
+ out.push({
5236
+ start: bucket[0].start,
5237
+ bp: on + off,
5238
+ onReference,
5239
+ referenceStart: onReference ? lead.referenceStart : void 0,
5240
+ ...lead.reversed ? { reversed: true } : {}
5241
+ });
5242
+ }
5243
+ bucket = [];
5244
+ };
5245
+ let held = 0;
5246
+ for (const run of runs) {
5247
+ if (run.gap || run.bp >= minBp) {
5248
+ flush();
5249
+ held = 0;
5250
+ out.push(run);
5251
+ continue;
5252
+ }
5253
+ bucket.push(run);
5254
+ held += run.bp;
5255
+ if (held >= minBp) {
5256
+ flush();
5257
+ held = 0;
5258
+ }
5259
+ }
5260
+ flush();
5261
+ return out;
5262
+ }
5010
5263
  function geneTree(box, y) {
5011
5264
  return el(
5012
5265
  "g",
@@ -5051,18 +5304,20 @@ function geneTree(box, y) {
5051
5304
  }
5052
5305
  function walkRowsTree(bars, frame, o = {}) {
5053
5306
  const X = (bp) => bp * frame.scaleX + frame.translateX;
5054
- const Y = (row) => row * ROW_HEIGHT_PX * frame.scaleY + frame.translateY;
5307
+ const rowPx = frame.rowPx ?? ROW_HEIGHT_PX;
5308
+ const barPx = frame.barPx ?? BAR_PX;
5309
+ const Y = (row) => row * rowPx * frame.scaleY + frame.translateY;
5055
5310
  const { origin, unit, reference, rows } = bars;
5056
5311
  const along = (offset) => X(origin + offset);
5057
5312
  const prefix = o.idPrefix ?? "walkrow";
5058
5313
  const rowTree = (row, i) => {
5059
5314
  const y = Y(i);
5060
- if (y < -BAR_PX || y > frame.height + BAR_PX) {
5315
+ if (y < -barPx || y > frame.height + barPx) {
5061
5316
  return void 0;
5062
5317
  }
5063
- const runs = row.runs.flatMap((run) => {
5318
+ const runs = coalesceRuns(row.runs, frame.scaleX).flatMap((run) => {
5064
5319
  const paint2 = runPaint(run, o.ramp);
5065
- const h = run.gap ? GAP_PX : BAR_PX;
5320
+ const h = run.gap ? Math.min(GAP_PX, barPx) : barPx;
5066
5321
  const id = `${prefix}-${i}-${run.start}`;
5067
5322
  const rect = el("rect", {
5068
5323
  x: along(run.start),
@@ -5089,8 +5344,8 @@ function walkRowsTree(bars, frame, o = {}) {
5089
5344
  (k) => el("line", {
5090
5345
  x1: along(k),
5091
5346
  x2: along(k),
5092
- y1: y - BAR_PX / 2,
5093
- y2: y + BAR_PX / 2,
5347
+ y1: y - barPx / 2,
5348
+ y2: y + barPx / 2,
5094
5349
  stroke: "white",
5095
5350
  "stroke-width": 1
5096
5351
  })
@@ -5103,6 +5358,7 @@ function walkRowsTree(bars, frame, o = {}) {
5103
5358
  call
5104
5359
  );
5105
5360
  const at = readoutPlacement(text, along(row.bp), frame.width);
5361
+ const readouts = frame.readouts ?? true;
5106
5362
  return el(
5107
5363
  "g",
5108
5364
  {
@@ -5116,12 +5372,12 @@ function walkRowsTree(bars, frame, o = {}) {
5116
5372
  call && el("rect", {
5117
5373
  "data-testid": "graph-walk-call",
5118
5374
  x: along(call.bp) - 1,
5119
- y: y - BAR_PX / 2 - 3,
5375
+ y: y - barPx / 2 - 3,
5120
5376
  width: 2,
5121
- height: BAR_PX + 6,
5377
+ height: barPx + 6,
5122
5378
  fill: call.spanningReads === 0 ? UNBACKED_TICK : CALL_TICK
5123
5379
  }),
5124
- el(
5380
+ readouts && el(
5125
5381
  "text",
5126
5382
  {
5127
5383
  x: at.x,
@@ -5254,7 +5510,7 @@ function svgCanvas(width, height) {
5254
5510
  }
5255
5511
 
5256
5512
  // src/version.ts
5257
- var version = "4.2.0";
5513
+ var version = "4.3.0";
5258
5514
 
5259
5515
  // src/viewport.ts
5260
5516
  function axisScaleOf(scale, pixelRows) {
@@ -5482,6 +5738,7 @@ function figureSvg(graph, layout, o = {}) {
5482
5738
  axis,
5483
5739
  referenceRamp,
5484
5740
  deletions: new Map(deletions.map((d) => [d.edgeIndex, d.bypassed])),
5741
+ deletionRoutes: layout.deletionRoutes,
5485
5742
  hiddenEdges: new Set(
5486
5743
  o.showDeletionEdges ? [] : deletions.map((d) => d.edgeIndex)
5487
5744
  )
@@ -6189,9 +6446,17 @@ function loadBandage() {
6189
6446
  export {
6190
6447
  isBackbone,
6191
6448
  BUBBLE_SPREADS,
6449
+ ROW_HEIGHT_PX,
6450
+ svgPath,
6451
+ curveBounds,
6452
+ translateCurves,
6453
+ yToXOf,
6454
+ pathRibbonOffsets,
6455
+ deletionEdges,
6192
6456
  panSNSample,
6193
6457
  panSNHaplotype,
6194
6458
  panSNContig,
6459
+ pathOrigin,
6195
6460
  trimOrigins,
6196
6461
  loadGraph,
6197
6462
  engineRequest,
@@ -6202,14 +6467,7 @@ export {
6202
6467
  clampZoom,
6203
6468
  fitTransform,
6204
6469
  drawingBounds,
6205
- ROW_HEIGHT_PX,
6206
6470
  referenceOrder,
6207
- svgPath,
6208
- curveBounds,
6209
- translateCurves,
6210
- yToXOf,
6211
- pathRibbonOffsets,
6212
- deletionEdges,
6213
6471
  LABEL_PX,
6214
6472
  LABEL_CHAR_PX,
6215
6473
  LABEL_PAD,
@@ -6283,6 +6541,7 @@ export {
6283
6541
  walkRowsKey,
6284
6542
  DISAGREES,
6285
6543
  walkRowsKeyTree,
6544
+ coalesceRuns,
6286
6545
  walkRowsTree,
6287
6546
  svgCanvas,
6288
6547
  version,