@jbrowse/bandage-core 4.2.0 → 4.4.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,341 +173,924 @@ 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
- }
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
+ );
470
+ }
471
+ if (typeof selfLink === "string") {
472
+ return hairpinCurves(
473
+ attachment(fromSegments, selfLink),
474
+ selfLoopReach(fromSegments, scale),
475
+ offsetX,
476
+ offsetY
477
+ );
478
+ }
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
+ }
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
+ ];
587
+ }
588
+ }
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";
593
+ }
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]);
614
+ }
615
+ if (i === points.length - 1) {
616
+ const out = along(toAttach.inward, points[i - 1]);
617
+ return { x: -out.x, y: -out.y };
618
+ }
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
+ };
623
+ };
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
+ }
675
+ }
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) {
688
+ break;
689
+ }
690
+ if (node.stable.start + node.length <= end) {
691
+ bypassed.push(node.id);
692
+ }
693
+ }
694
+ }
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;
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) {
707
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
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
+ }
733
+ }
734
+ }
735
+ return found;
736
+ }
737
+
738
+ // src/pansn.ts
739
+ var SEP = "#";
740
+ function panSNSample(refName) {
741
+ return refName.split(SEP)[0];
742
+ }
743
+ var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
744
+ function isGenericSample(sample) {
745
+ return GENERIC_SAMPLES.has(sample);
746
+ }
747
+ function panSNHaplotype(refName) {
748
+ const parts = refName.split(SEP);
749
+ return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : void 0;
750
+ }
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;
754
+ }
755
+ function panSNMatchesPrefix(refName, prefix) {
756
+ return prefix !== void 0 && (refName === prefix || refName.startsWith(prefix + SEP));
757
+ }
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;
771
+ }
772
+ }
773
+ }
774
+ function parseTags(fields) {
775
+ const tags = {};
776
+ for (const field of fields) {
777
+ parseTag(field, tags);
778
+ }
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
+ }
795
+ }
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;
802
+ }
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);
832
+ } else {
833
+ gfa1 = true;
834
+ seq = first;
835
+ len = seq === "*" ? 0 : seq.length;
836
+ tagfields = rest.slice(1);
837
+ }
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 });
884
+ }
885
+ }
886
+ }
887
+ return graph;
888
+ }
889
+
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 };
895
+ }
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]);
914
+ }
915
+ pos += lengthOf(step.id);
916
+ }
917
+ anchorPaths.push({
918
+ name: path.name,
919
+ sample,
920
+ start: path.start,
921
+ length: pos - path.start
922
+ });
923
+ }
924
+ return { anchorPaths, pathVisits };
925
+ }
926
+ function chooseReferencePath(anchorPaths, preferred) {
927
+ const named = anchorPaths.find(
928
+ (p) => p.sample === preferred || p.name === preferred
929
+ );
930
+ return named ?? anchorPaths[0];
931
+ }
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
+ };
947
+ }
948
+ return anchored;
949
+ }
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;
968
+ }
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;
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]);
981
+ }
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
+ });
992
+ }
993
+
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
+ }
1002
+ }
1003
+ function traverse(id, strand) {
1004
+ claim(id, strand);
1005
+ traversals.set(id, (traversals.get(id) ?? 0) + 1);
1006
+ }
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("-") ? "-" : "+");
1014
+ }
1015
+ }
1016
+ for (const walk of gfaGraph.walks) {
1017
+ for (const seg of walk.segments) {
1018
+ traverse(seg.id, seg.strand === "-" ? "-" : "+");
1019
+ }
1020
+ }
1021
+ return { canonical, traversals };
1022
+ }
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
+ ];
1042
+ }
1043
+ function carriedSamples(gfaNode) {
1044
+ const sm = gfaNode.tags.SM;
1045
+ if (typeof sm !== "string") {
1046
+ return void 0;
1047
+ }
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) {
1057
+ return {
1058
+ id: `${gfaNode.id}${strand}`,
1059
+ name: gfaNode.id,
1060
+ length: gfaNode.length,
1061
+ depth,
1062
+ stable: stableCoordinate(gfaNode),
1063
+ samples: carriedSamples(gfaNode)
1064
+ };
1065
+ }
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));
1074
+ }
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
+ });
1082
+ }
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
+ }
522
1094
  }
523
1095
  for (const gfaPath of gfaGraph.paths) {
524
1096
  const nodeIds = gfaPath.path.split(",").map((segment) => nodeId(segment.slice(0, -1)));
@@ -559,991 +1131,603 @@ function convertGFAToGraph(gfaGraph, name = "Imported GFA") {
559
1131
  pathVisits: anchoring?.pathVisits,
560
1132
  // rGFA's own tags win wherever they are present; pathAnchoring only ever
561
1133
  // 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
- }
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 };
577
- }
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]);
588
- }
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;
598
- }
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;
602
- };
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;
611
- }
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)) {
617
- break;
618
- }
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);
627
- }
628
- }
629
- for (const node of graph.nodes) {
630
- if (!runOf.has(node.id)) {
631
- runOf.set(node.id, node.id);
632
- }
633
- }
634
- 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;
644
- }
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
- }
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
- };
678
- }
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;
700
- }
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);
708
- }
709
- function polylineMidpoint(line) {
710
- const arc = arcPrefix(line);
711
- return pointAt(line, arc, arc.at(-1) / 2);
712
- }
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;
721
- }
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 };
738
- }
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] });
744
- }
745
- }
746
- points.push(pointAt(line, arc, to));
747
- return points;
1134
+ anchoredBy: nodes.some((n2) => n2.stable) ? "tags" : void 0
1135
+ };
748
1136
  }
749
1137
 
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
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}`])
754
1144
  );
755
- if (backbone.length < 2) {
756
- return positions;
757
- }
758
- const points = backbone.map((node) => {
759
- const segments = positions[node.id];
760
- const mid = segments[Math.floor(segments.length / 2)];
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
+ });
1158
+ return {
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())
1174
+ };
1175
+ }
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);
761
1188
  return {
762
- x: mid.x,
763
- y: mid.y,
764
- r: node.stable.start + node.length / 2,
765
- w: node.length
1189
+ id,
1190
+ name: id,
1191
+ length: Math.round(drawn * 1e6 / opts.nodeLengthPerMegabase),
1192
+ depth: 0
766
1193
  };
767
1194
  });
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
- });
788
- }
789
- let below = 0;
790
- let above = 0;
791
- for (const node of graph.nodes) {
792
- if (isBackbone(node)) {
793
- continue;
794
- }
795
- for (const p of out[node.id] ?? []) {
796
- if (p.y > cy) {
797
- below++;
798
- } else {
799
- above++;
800
- }
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;
807
- }
1195
+ }
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;
808
1204
  }
809
1205
  }
810
- return out;
1206
+ return { nodePositions, deletionRoutes };
811
1207
  }
812
1208
 
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);
1209
+ // src/layout/mergeRuns.ts
1210
+ function ownStrand(id) {
1211
+ return id.endsWith("-") ? "-" : "+";
1212
+ }
1213
+ function flip(strand) {
1214
+ return strand === "+" ? "-" : "+";
1215
+ }
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 };
1220
+ }
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);
819
1227
  if (list) {
820
- list.push(to);
1228
+ list.push(ei);
821
1229
  } else {
822
- neighbors.set(from, [to]);
1230
+ at.set(key, [ei]);
823
1231
  }
824
- }
825
- for (const edge of graph.edges) {
826
- add(edge.from, edge.to);
827
- add(edge.to, edge.from);
828
- }
829
- return neighbors;
830
- }
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
- }
857
- }
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;
858
1241
  }
859
- frontier = next;
860
- }
861
- return minEnd === Infinity ? void 0 : {
862
- start: Math.min(minEnd, maxStart),
863
- end: Math.max(minEnd, maxStart)
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;
864
1245
  };
865
- }
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 });
873
- }
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();
1246
+ const joined = /* @__PURE__ */ new Set();
1247
+ const runOf = /* @__PURE__ */ new Map();
1248
+ const runs = /* @__PURE__ */ new Map();
878
1249
  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);
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;
882
1254
  }
883
- }
884
- return midpoints;
885
- }
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;
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;
911
1261
  }
912
- depth.set(neighbour, next);
913
- seeds.set(neighbour, { x: at.x + drawnOf(id) / 2, y: LANE_GAP * next });
914
- queue.push(neighbour);
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);
915
1270
  }
916
1271
  }
917
- let unreached = 0;
918
1272
  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
- });
1273
+ if (!runOf.has(node.id)) {
1274
+ runOf.set(node.id, node.id);
924
1275
  }
925
1276
  }
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) }));
931
- }
932
-
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}`);
941
- }
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
- );
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
+ });
946
1298
  }
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 } : {}
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;
961
1308
  }
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
- );
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
+ });
983
1317
  return {
984
- result: {
985
- ...result,
986
- nodePositions: graph.nodes.some(isBackbone) ? orientToReference(graph, positions) : positions
987
- },
988
- duration
1318
+ graph: { ...graph, nodes, edges, paths: void 0, pathVisits: void 0 },
1319
+ runs
989
1320
  };
990
1321
  }
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);
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;
1002
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
+ });
1003
1341
  }
1004
- return { minX, minY, maxX, maxY };
1342
+ return out;
1005
1343
  }
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));
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);
1011
1351
  }
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;
1352
+ function polylineMidpoint(line) {
1353
+ const arc = arcPrefix(line);
1354
+ return pointAt(line, arc, arc.at(-1) / 2);
1015
1355
  }
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)) {
1026
- return void 0;
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)
1361
+ );
1027
1362
  }
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;
1034
- 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
- )
1043
- };
1363
+ return arc;
1044
1364
  }
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;
1051
- }
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);
1365
+ function pointAt(line, arc, s) {
1366
+ if (s <= 0) {
1367
+ return { ...line[0] };
1058
1368
  }
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];
1369
+ let i = 1;
1370
+ while (i < arc.length && arc[i] < s) {
1371
+ i++;
1074
1372
  }
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);
1373
+ if (i >= arc.length) {
1374
+ return { ...line.at(-1) };
1104
1375
  }
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);
1113
- 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
1122
- };
1123
- }
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;
1129
- 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)
1132
- };
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 };
1133
1381
  }
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)]);
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] });
1141
1387
  }
1142
1388
  }
1143
- return out;
1389
+ points.push(pointAt(line, arc, to));
1390
+ return points;
1144
1391
  }
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
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;
1400
+ }
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
1158
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);
1420
+ }
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++;
1443
+ }
1444
+ }
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;
1450
+ }
1451
+ }
1159
1452
  }
1160
1453
  return out;
1161
1454
  }
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);
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
+ }
1174
1467
  }
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
- };
1468
+ for (const edge of graph.edges) {
1469
+ add(edge.from, edge.to);
1470
+ add(edge.to, edge.from);
1471
+ }
1472
+ return neighbors;
1190
1473
  }
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;
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
+ }
1208
1501
  }
1209
- const along = dx * ux + dy * uy;
1210
- minAlong = Math.min(minAlong, along);
1211
- maxAlong = Math.max(maxAlong, along);
1502
+ frontier = next;
1212
1503
  }
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;
1504
+ return minEnd === Infinity ? void 0 : {
1505
+ start: Math.min(minEnd, maxStart),
1506
+ end: Math.max(minEnd, maxStart)
1507
+ };
1220
1508
  }
1221
- function yToXOf({ scaleX, scaleY }) {
1222
- return scaleY / scaleX;
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 });
1223
1516
  }
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 };
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;
1230
1528
  }
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 });
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;
1242
1544
  }
1243
- return offsets;
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;
1554
+ }
1555
+ depth.set(neighbour, next);
1556
+ seeds.set(neighbour, { x: at.x + drawnOf(id) / 2, y: LANE_GAP * next });
1557
+ queue.push(neighbour);
1558
+ }
1559
+ }
1560
+ let unreached = 0;
1561
+ for (const node of graph.nodes) {
1562
+ if (!seeds.has(node.id)) {
1563
+ seeds.set(node.id, {
1564
+ x: unreached++ * x / graph.nodes.length,
1565
+ y: LANE_GAP * 3
1566
+ });
1567
+ }
1568
+ }
1569
+ return seeds;
1244
1570
  }
1245
- function scaleYOf(points, yToX) {
1246
- return points.map((p) => ({ x: p.x, y: p.y * yToX }));
1571
+ function seededNodes(graph, scaling) {
1572
+ const seeds = referenceSeeds(graph, scaling);
1573
+ return scaling.nodes.map((node) => ({ ...node, ...seeds.get(node.id) }));
1247
1574
  }
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;
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}`);
1254
1584
  }
1255
- return curves;
1256
- }
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
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.`
1265
1588
  );
1266
1589
  }
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);
1273
- }
1274
- function selfLinkOf(edge) {
1275
- if (edge.from !== edge.to) {
1276
- return false;
1277
- }
1278
- return edge.fromStrand && edge.toStrand && edge.fromStrand !== edge.toStrand ? edge.fromStrand === "+" ? "end" : "start" : true;
1590
+ return graph;
1279
1591
  }
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
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
1305
1598
  },
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
1599
+ options: {
1600
+ quality: settings.quality,
1601
+ linearLayout: settings.linearLayout,
1602
+ ...scaling.opts,
1603
+ ...anchored ? { rotateComponents: false } : {}
1315
1604
  }
1316
- ];
1605
+ };
1317
1606
  }
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
- );
1338
- }
1339
- if (typeof selfLink === "string") {
1340
- return hairpinCurves(
1341
- attachment(fromSegments, selfLink),
1342
- selfLoopReach(fromSegments, scale),
1343
- offsetX,
1344
- offsetY
1345
- );
1346
- }
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;
1366
- }
1367
- }
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 [
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,
1378
1624
  {
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
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
+ ]
1387
1636
  },
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
1397
- }
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
1453
- }
1454
- ];
1455
- }
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;
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
+ };
1475
1661
  }
1476
- function backboneBySequence(graph) {
1477
- const bySequence = /* @__PURE__ */ new Map();
1478
- 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
- }
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);
1486
1673
  }
1487
1674
  }
1488
- for (const list of bySequence.values()) {
1489
- list.sort((a, b) => a.stable.start - b.stable.start);
1490
- }
1491
- return bySequence;
1675
+ return { minX, minY, maxX, maxY };
1492
1676
  }
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
- }
1504
- }
1505
- }
1506
- return bypassed;
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));
1507
1682
  }
1508
- function skipsBetween(edge, from, to, forwards) {
1509
- const { fromStrand, toStrand } = edge;
1510
- if (fromStrand === void 0 || toStrand === void 0) {
1511
- return true;
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,
@@ -4872,7 +5075,16 @@ function placeRowGenes(rows, byRow) {
4872
5075
  }
4873
5076
  var GENE_FONT_PX = 9;
4874
5077
  var GENE_CHAR_PX = 5.2;
4875
- function rowGeneBoxes(genes, X, y) {
5078
+ var MIN_LETTERED_BAR_PX = 8;
5079
+ var genePad = (rowPx, barPx) => Math.min(2, (rowPx - barPx) / 2 - 1);
5080
+ function boxesGenes(rowPx, barPx) {
5081
+ return genePad(rowPx, barPx) >= 1;
5082
+ }
5083
+ function rowGeneBoxes(genes, X, y, { rowPx = ROW_HEIGHT_PX, barPx = BAR_PX } = {}) {
5084
+ if (!boxesGenes(rowPx, barPx)) {
5085
+ return [];
5086
+ }
5087
+ const pad = genePad(rowPx, barPx);
4876
5088
  const boxes = genes.map((g) => {
4877
5089
  const x = X(g.start);
4878
5090
  return {
@@ -4883,7 +5095,7 @@ function rowGeneBoxes(genes, X, y) {
4883
5095
  });
4884
5096
  const taken = [];
4885
5097
  const named = new Set(
4886
- [...boxes].sort((a, b) => a.w - b.w).filter(({ g, x, w }) => {
5098
+ (barPx >= MIN_LETTERED_BAR_PX ? [...boxes] : []).sort((a, b) => a.w - b.w).filter(({ g, x, w }) => {
4887
5099
  const half = g.name.length * GENE_CHAR_PX / 2;
4888
5100
  const at = [x + w / 2 - half, x + w / 2 + half];
4889
5101
  if (2 * half > w + 6 || taken.some(([a, b]) => at[0] < b + 2 && at[1] > a - 2)) {
@@ -4896,9 +5108,9 @@ function rowGeneBoxes(genes, X, y) {
4896
5108
  return boxes.map((box) => ({
4897
5109
  name: box.g.name,
4898
5110
  x: box.x,
4899
- y: y - BAR_PX / 2 - 2,
5111
+ y: y - barPx / 2 - pad,
4900
5112
  w: box.w,
4901
- h: BAR_PX + 4,
5113
+ h: barPx + 2 * pad,
4902
5114
  exons: box.g.exons.map((e) => ({
4903
5115
  x: X(e.start),
4904
5116
  w: Math.max(1, X(e.end) - X(e.start))
@@ -4930,7 +5142,13 @@ function walkRowsKey(bars, o = {}) {
4930
5142
  label: "walked outside the cut"
4931
5143
  });
4932
5144
  }
4933
- if (o.genes) {
5145
+ if (o.genes?.crowded) {
5146
+ entries.push({
5147
+ swatch: { kind: "gene" },
5148
+ label: "genes left out: too many rows to box them in",
5149
+ note: true
5150
+ });
5151
+ } else if (o.genes) {
4934
5152
  entries.push({
4935
5153
  swatch: { kind: "gene" },
4936
5154
  label: "genes, each row's own annotation"
@@ -5007,7 +5225,57 @@ function walkRowsKeyTree(entries, x, y) {
5007
5225
  )
5008
5226
  };
5009
5227
  }
5010
- function geneTree(box, y) {
5228
+ function coalesceRuns(runs, pxPerBp) {
5229
+ const minBp = 1 / pxPerBp;
5230
+ const out = [];
5231
+ let bucket = [];
5232
+ const flush = () => {
5233
+ if (bucket.length === 0) {
5234
+ return;
5235
+ }
5236
+ if (bucket.length === 1) {
5237
+ out.push(bucket[0]);
5238
+ } else {
5239
+ let on = 0;
5240
+ let off = 0;
5241
+ for (const r of bucket) {
5242
+ if (r.onReference) {
5243
+ on += r.bp;
5244
+ } else {
5245
+ off += r.bp;
5246
+ }
5247
+ }
5248
+ const onReference = on >= off;
5249
+ const lead = bucket.find((r) => r.onReference === onReference);
5250
+ out.push({
5251
+ start: bucket[0].start,
5252
+ bp: on + off,
5253
+ onReference,
5254
+ referenceStart: onReference ? lead.referenceStart : void 0,
5255
+ ...lead.reversed ? { reversed: true } : {}
5256
+ });
5257
+ }
5258
+ bucket = [];
5259
+ };
5260
+ let held = 0;
5261
+ for (const run of runs) {
5262
+ if (run.gap || run.bp >= minBp) {
5263
+ flush();
5264
+ held = 0;
5265
+ out.push(run);
5266
+ continue;
5267
+ }
5268
+ bucket.push(run);
5269
+ held += run.bp;
5270
+ if (held >= minBp) {
5271
+ flush();
5272
+ held = 0;
5273
+ }
5274
+ }
5275
+ flush();
5276
+ return out;
5277
+ }
5278
+ function geneTree(box, y, barPx) {
5011
5279
  return el(
5012
5280
  "g",
5013
5281
  { class: "row-gene", "data-testid": "graph-walk-gene" },
@@ -5015,9 +5283,9 @@ function geneTree(box, y) {
5015
5283
  ...box.exons.map(
5016
5284
  (e) => el("rect", {
5017
5285
  x: e.x,
5018
- y: y - BAR_PX / 2,
5286
+ y: y - barPx / 2,
5019
5287
  width: e.w,
5020
- height: BAR_PX,
5288
+ height: barPx,
5021
5289
  fill: GENE_INK,
5022
5290
  opacity: 0.35
5023
5291
  })
@@ -5051,18 +5319,20 @@ function geneTree(box, y) {
5051
5319
  }
5052
5320
  function walkRowsTree(bars, frame, o = {}) {
5053
5321
  const X = (bp) => bp * frame.scaleX + frame.translateX;
5054
- const Y = (row) => row * ROW_HEIGHT_PX * frame.scaleY + frame.translateY;
5322
+ const rowPx = frame.rowPx ?? ROW_HEIGHT_PX;
5323
+ const barPx = frame.barPx ?? BAR_PX;
5324
+ const Y = (row) => row * rowPx * frame.scaleY + frame.translateY;
5055
5325
  const { origin, unit, reference, rows } = bars;
5056
5326
  const along = (offset) => X(origin + offset);
5057
5327
  const prefix = o.idPrefix ?? "walkrow";
5058
5328
  const rowTree = (row, i) => {
5059
5329
  const y = Y(i);
5060
- if (y < -BAR_PX || y > frame.height + BAR_PX) {
5330
+ if (y < -barPx || y > frame.height + barPx) {
5061
5331
  return void 0;
5062
5332
  }
5063
- const runs = row.runs.flatMap((run) => {
5333
+ const runs = coalesceRuns(row.runs, frame.scaleX).flatMap((run) => {
5064
5334
  const paint2 = runPaint(run, o.ramp);
5065
- const h = run.gap ? GAP_PX : BAR_PX;
5335
+ const h = run.gap ? Math.min(GAP_PX, barPx) : barPx;
5066
5336
  const id = `${prefix}-${i}-${run.start}`;
5067
5337
  const rect = el("rect", {
5068
5338
  x: along(run.start),
@@ -5089,8 +5359,8 @@ function walkRowsTree(bars, frame, o = {}) {
5089
5359
  (k) => el("line", {
5090
5360
  x1: along(k),
5091
5361
  x2: along(k),
5092
- y1: y - BAR_PX / 2,
5093
- y2: y + BAR_PX / 2,
5362
+ y1: y - barPx / 2,
5363
+ y2: y + barPx / 2,
5094
5364
  stroke: "white",
5095
5365
  "stroke-width": 1
5096
5366
  })
@@ -5103,6 +5373,7 @@ function walkRowsTree(bars, frame, o = {}) {
5103
5373
  call
5104
5374
  );
5105
5375
  const at = readoutPlacement(text, along(row.bp), frame.width);
5376
+ const readouts = frame.readouts ?? true;
5106
5377
  return el(
5107
5378
  "g",
5108
5379
  {
@@ -5110,18 +5381,19 @@ function walkRowsTree(bars, frame, o = {}) {
5110
5381
  },
5111
5382
  ...runs,
5112
5383
  ...ticks,
5113
- ...rowGeneBoxes(o.rowGenes?.get(row.name) ?? [], along, y).map(
5114
- (box) => geneTree(box, y)
5115
- ),
5384
+ ...rowGeneBoxes(o.rowGenes?.get(row.name) ?? [], along, y, {
5385
+ rowPx,
5386
+ barPx
5387
+ }).map((box) => geneTree(box, y, barPx)),
5116
5388
  call && el("rect", {
5117
5389
  "data-testid": "graph-walk-call",
5118
5390
  x: along(call.bp) - 1,
5119
- y: y - BAR_PX / 2 - 3,
5391
+ y: y - barPx / 2 - 3,
5120
5392
  width: 2,
5121
- height: BAR_PX + 6,
5393
+ height: barPx + 6,
5122
5394
  fill: call.spanningReads === 0 ? UNBACKED_TICK : CALL_TICK
5123
5395
  }),
5124
- el(
5396
+ readouts && el(
5125
5397
  "text",
5126
5398
  {
5127
5399
  x: at.x,
@@ -5141,6 +5413,180 @@ function walkRowsTree(bars, frame, o = {}) {
5141
5413
  return el("g", {}, ...[reference, ...rows].map(rowTree));
5142
5414
  }
5143
5415
 
5416
+ // src/layout/walkStrip.ts
5417
+ var PAD_PX = 6;
5418
+ var WALK_STRIP_CEILING_PX = 260;
5419
+ var LABEL_FONT_PX = 11;
5420
+ var MIN_ROW_PX = 3;
5421
+ var LABELLED_ROW_PX = 12;
5422
+ var READOUT_ROOM2 = 1.3;
5423
+ var MARK_INK = "#111";
5424
+ var TICK_PX = 3;
5425
+ function stripMarks(graph, rows, node) {
5426
+ const visits = graph.pathVisits?.get(node.name) ?? [];
5427
+ const marks = [];
5428
+ for (const row of rows) {
5429
+ const axis = row.axis;
5430
+ if (!axis) {
5431
+ continue;
5432
+ }
5433
+ const path = pathOrigin(row.name).name;
5434
+ for (const v of visits) {
5435
+ if (v.path !== path) {
5436
+ continue;
5437
+ }
5438
+ const { start, end } = alongRow(axis, v.start, v.start + node.length);
5439
+ if (end > 0 && start < row.bp) {
5440
+ marks.push({ row: row.name, start, bp: end - start });
5441
+ }
5442
+ }
5443
+ }
5444
+ return marks;
5445
+ }
5446
+ var spanCache = /* @__PURE__ */ new WeakMap();
5447
+ function spansOf(graph, name) {
5448
+ let byName = spanCache.get(graph);
5449
+ if (!byName) {
5450
+ byName = /* @__PURE__ */ new Map();
5451
+ spanCache.set(graph, byName);
5452
+ }
5453
+ if (byName.has(name)) {
5454
+ return byName.get(name);
5455
+ }
5456
+ const lengthOf = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
5457
+ const pieces = (graph.paths ?? []).filter((p) => p.name === name);
5458
+ let spans;
5459
+ if (pieces.every((p) => p.start !== void 0)) {
5460
+ spans = { ids: [], starts: [], ends: [] };
5461
+ for (const piece of [...pieces].sort((a, b) => a.start - b.start)) {
5462
+ let pos = piece.start;
5463
+ for (const id of piece.nodeIds) {
5464
+ const len = lengthOf.get(id) ?? 0;
5465
+ spans.ids.push(id);
5466
+ spans.starts.push(pos);
5467
+ spans.ends.push(pos + len);
5468
+ pos += len;
5469
+ }
5470
+ }
5471
+ }
5472
+ byName.set(name, spans);
5473
+ return spans;
5474
+ }
5475
+ function segmentAt(graph, row, offset, pxPerBp) {
5476
+ const axis = row.axis;
5477
+ const spans = axis && spansOf(graph, row.name);
5478
+ if (!axis || !spans || offset < 0 || offset > row.bp) {
5479
+ return void 0;
5480
+ }
5481
+ const at = axis.reversed ? axis.start - offset : axis.start + offset;
5482
+ const tol = 0.5 / pxPerBp;
5483
+ const bar = axis.reversed ? { start: axis.start - row.bp, end: axis.start } : { start: axis.start, end: axis.start + row.bp };
5484
+ const from = Math.max(at - tol, bar.start);
5485
+ const to = Math.min(at + tol, bar.end);
5486
+ let lo = 0;
5487
+ let hi = spans.starts.length;
5488
+ while (lo < hi) {
5489
+ const mid = lo + hi >> 1;
5490
+ if (spans.ends[mid] <= from) {
5491
+ lo = mid + 1;
5492
+ } else {
5493
+ hi = mid;
5494
+ }
5495
+ }
5496
+ let best;
5497
+ let bestLen = -1;
5498
+ let bestHolds = false;
5499
+ for (let i = lo; i < spans.starts.length && spans.starts[i] < to; i++) {
5500
+ const len = spans.ends[i] - spans.starts[i];
5501
+ const holds = spans.starts[i] <= at && at < spans.ends[i];
5502
+ if (len > bestLen || len === bestLen && holds && !bestHolds) {
5503
+ bestLen = len;
5504
+ best = spans.ids[i];
5505
+ bestHolds = holds;
5506
+ }
5507
+ }
5508
+ return best;
5509
+ }
5510
+ function walkStripFrame(bars, o) {
5511
+ const maxHeight = o.maxHeight ?? WALK_STRIP_CEILING_PX;
5512
+ const labelPx = o.labelPx ?? Math.max(...[bars.reference, ...bars.rows].map((r) => r.label.length)) * LABEL_CHAR_PX + 16;
5513
+ const count = bars.rows.length + 1;
5514
+ const rowPx = Math.max(
5515
+ MIN_ROW_PX,
5516
+ Math.min(ROW_HEIGHT_PX, Math.floor((maxHeight - 2 * PAD_PX) / count))
5517
+ );
5518
+ const labelled = rowPx >= LABELLED_ROW_PX;
5519
+ const readouts = rowPx >= 14;
5520
+ const barPx = Math.max(2, Math.round(rowPx * 0.6));
5521
+ const longest = Math.max(bars.reference.bp, ...bars.rows.map((r) => r.bp), 1);
5522
+ const left = labelled ? labelPx : PAD_PX;
5523
+ const usable = Math.max(1, o.width - left - PAD_PX);
5524
+ const scaleX = usable / (longest * (readouts ? READOUT_ROOM2 : 1));
5525
+ return {
5526
+ scaleX,
5527
+ scaleY: 1,
5528
+ translateX: left - bars.origin * scaleX,
5529
+ translateY: PAD_PX + rowPx / 2,
5530
+ width: o.width,
5531
+ height: count * rowPx + 2 * PAD_PX,
5532
+ rowPx,
5533
+ barPx,
5534
+ readouts,
5535
+ labelled,
5536
+ boxesGenes: boxesGenes(rowPx, barPx)
5537
+ };
5538
+ }
5539
+ function stripRowAt(bars, frame, x, y) {
5540
+ const i = Math.round((y - frame.translateY) / frame.rowPx);
5541
+ const row = [bars.reference, ...bars.rows][i];
5542
+ const offset = (x - frame.translateX) / frame.scaleX - bars.origin;
5543
+ return row && i >= 0 && offset >= 0 && offset <= row.bp ? { index: i, row, offset } : void 0;
5544
+ }
5545
+ function walkMarksTree(bars, frame, marks) {
5546
+ const index = new Map(
5547
+ [bars.reference, ...bars.rows].map((row, i) => [row.name, i])
5548
+ );
5549
+ const X = (offset) => (bars.origin + offset) * frame.scaleX + frame.translateX;
5550
+ return el(
5551
+ "g",
5552
+ { "data-testid": "graph-walk-marks" },
5553
+ ...marks.flatMap((m) => {
5554
+ const i = index.get(m.row);
5555
+ if (i === void 0) {
5556
+ return [];
5557
+ }
5558
+ const y = i * frame.rowPx + frame.translateY;
5559
+ const x = X(m.start);
5560
+ const w = Math.max(2, m.bp * frame.scaleX);
5561
+ const tick = (top) => el("rect", { x, y: top, width: w, height: TICK_PX, fill: MARK_INK });
5562
+ return [
5563
+ tick(y - frame.barPx / 2 - TICK_PX - 1),
5564
+ tick(y + frame.barPx / 2 + 1)
5565
+ ];
5566
+ })
5567
+ );
5568
+ }
5569
+ function walkStripLabelsTree(bars, frame, bold = /* @__PURE__ */ new Set()) {
5570
+ return el(
5571
+ "g",
5572
+ { "font-family": "sans-serif", "font-size": LABEL_FONT_PX, fill: "#333" },
5573
+ ...(frame.labelled ? [bars.reference, ...bars.rows] : []).map(
5574
+ (row, i) => el(
5575
+ "text",
5576
+ {
5577
+ x: 6,
5578
+ y: i * frame.rowPx + frame.translateY + 4,
5579
+ "font-weight": bold.has(row.name) ? 600 : void 0
5580
+ },
5581
+ row.label
5582
+ )
5583
+ )
5584
+ );
5585
+ }
5586
+ function stripGeneGaps(frame, gaps) {
5587
+ return gaps && !frame.boxesGenes ? { ...gaps, crowded: true } : gaps;
5588
+ }
5589
+
5144
5590
  // src/renderer/svgCanvas.ts
5145
5591
  var n = (v) => +v.toFixed(1);
5146
5592
  function paint(css) {
@@ -5254,7 +5700,7 @@ function svgCanvas(width, height) {
5254
5700
  }
5255
5701
 
5256
5702
  // src/version.ts
5257
- var version = "4.2.0";
5703
+ var version = "4.4.0";
5258
5704
 
5259
5705
  // src/viewport.ts
5260
5706
  function axisScaleOf(scale, pixelRows) {
@@ -5302,6 +5748,7 @@ var FONT = 'font-family="Helvetica, Arial, sans-serif" font-size="11"';
5302
5748
  var BAR_PX2 = 48;
5303
5749
  var SWATCH_PX = 18;
5304
5750
  var KEY_GAP_PX = 16;
5751
+ var STRIP_KEY_PX = 22;
5305
5752
  var FADED = "rgb(160,160,160)";
5306
5753
  var EXON_COLOR = "#daa520";
5307
5754
  var GENE_INK2 = "#1c1c22";
@@ -5482,6 +5929,7 @@ function figureSvg(graph, layout, o = {}) {
5482
5929
  axis,
5483
5930
  referenceRamp,
5484
5931
  deletions: new Map(deletions.map((d) => [d.edgeIndex, d.bypassed])),
5932
+ deletionRoutes: layout.deletionRoutes,
5485
5933
  hiddenEdges: new Set(
5486
5934
  o.showDeletionEdges ? [] : deletions.map((d) => d.edgeIndex)
5487
5935
  )
@@ -5589,6 +6037,42 @@ function figureSvg(graph, layout, o = {}) {
5589
6037
  );
5590
6038
  height = header + h;
5591
6039
  }
6040
+ const strip = o.walkStrip;
6041
+ if (strip && !rows && !layout.tubeMap) {
6042
+ const frame = walkStripFrame(strip.rows, { width });
6043
+ const key = walkRowsKeyTree(
6044
+ walkRowsKey(strip.rows, {
6045
+ ramp: referenceRamp && {
6046
+ start: referenceRamp.start,
6047
+ end: referenceRamp.start + referenceRamp.span
6048
+ },
6049
+ genes: strip.rowGenes?.size ? stripGeneGaps(frame, strip.rowGeneGaps) : void 0
6050
+ }),
6051
+ 6,
6052
+ height + frame.height
6053
+ );
6054
+ parts.push(
6055
+ `<line x1="0" x2="${width}" y1="${height + 0.5}" y2="${height + 0.5}" stroke="#ddd"/>`,
6056
+ `<svg y="${height}" width="${width}" height="${frame.height}">${serializeEl(
6057
+ walkRowsTree(strip.rows, frame, {
6058
+ ramp: referenceRamp && {
6059
+ start: referenceRamp.start,
6060
+ end: referenceRamp.start + referenceRamp.span
6061
+ },
6062
+ rowGenes: strip.rowGenes,
6063
+ idPrefix: "strip"
6064
+ })
6065
+ )}${serializeEl(
6066
+ walkStripLabelsTree(
6067
+ strip.rows,
6068
+ frame,
6069
+ new Set(layers.map((l) => l.walk))
6070
+ )
6071
+ )}</svg>`,
6072
+ serializeEl(key.tree)
6073
+ );
6074
+ height += frame.height + STRIP_KEY_PX;
6075
+ }
5592
6076
  const metadata = JSON.stringify({
5593
6077
  generator: `@jbrowse/bandage-core@${version}`,
5594
6078
  ...o.spec === void 0 ? {} : { spec: o.spec }
@@ -6189,6 +6673,13 @@ function loadBandage() {
6189
6673
  export {
6190
6674
  isBackbone,
6191
6675
  BUBBLE_SPREADS,
6676
+ ROW_HEIGHT_PX,
6677
+ svgPath,
6678
+ curveBounds,
6679
+ translateCurves,
6680
+ yToXOf,
6681
+ pathRibbonOffsets,
6682
+ deletionEdges,
6192
6683
  panSNSample,
6193
6684
  panSNHaplotype,
6194
6685
  panSNContig,
@@ -6202,14 +6693,7 @@ export {
6202
6693
  clampZoom,
6203
6694
  fitTransform,
6204
6695
  drawingBounds,
6205
- ROW_HEIGHT_PX,
6206
6696
  referenceOrder,
6207
- svgPath,
6208
- curveBounds,
6209
- translateCurves,
6210
- yToXOf,
6211
- pathRibbonOffsets,
6212
- deletionEdges,
6213
6697
  LABEL_PX,
6214
6698
  LABEL_CHAR_PX,
6215
6699
  LABEL_PAD,
@@ -6283,7 +6767,17 @@ export {
6283
6767
  walkRowsKey,
6284
6768
  DISAGREES,
6285
6769
  walkRowsKeyTree,
6770
+ coalesceRuns,
6286
6771
  walkRowsTree,
6772
+ WALK_STRIP_CEILING_PX,
6773
+ LABELLED_ROW_PX,
6774
+ stripMarks,
6775
+ segmentAt,
6776
+ walkStripFrame,
6777
+ stripRowAt,
6778
+ walkMarksTree,
6779
+ walkStripLabelsTree,
6780
+ stripGeneGaps,
6287
6781
  svgCanvas,
6288
6782
  version,
6289
6783
  axisScaleOf,