@jbrowse/bandage-core 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -269,12 +269,12 @@ function parseGFA(file) {
269
269
  });
270
270
  }
271
271
  } else if (line.startsWith("W")) {
272
- const [, sample, hap, contig2, start, end, body, ...rest] = line.split(" ");
273
- if (sample && hap && contig2 && start && end && body) {
272
+ const [, sample, hap, contig, start, end, body, ...rest] = line.split(" ");
273
+ if (sample && hap && contig && start && end && body) {
274
274
  graph.walks.push({
275
275
  sample,
276
276
  haplotype: +hap,
277
- contig: contig2,
277
+ contig,
278
278
  start: start === "*" ? -1 : +start,
279
279
  end: end === "*" ? -1 : +end,
280
280
  segments: parseWalkBody(body),
@@ -538,7 +538,7 @@ function sidesOf(edge) {
538
538
  return { from, to };
539
539
  }
540
540
  function mergeRuns(graph) {
541
- const sides = graph.edges.map(sidesOf);
541
+ const sides2 = graph.edges.map(sidesOf);
542
542
  const at = /* @__PURE__ */ new Map();
543
543
  const attach = (id, side, ei) => {
544
544
  const key = `${id}|${side}`;
@@ -550,8 +550,8 @@ function mergeRuns(graph) {
550
550
  }
551
551
  };
552
552
  graph.edges.forEach((edge, ei) => {
553
- attach(edge.from, sides[ei].from, ei);
554
- attach(edge.to, sides[ei].to, ei);
553
+ attach(edge.from, sides2[ei].from, ei);
554
+ attach(edge.to, sides2[ei].to, ei);
555
555
  });
556
556
  const joinAfter = (a) => {
557
557
  const out = at.get(`${a}|end`);
@@ -560,7 +560,7 @@ function mergeRuns(graph) {
560
560
  }
561
561
  const ei = out[0];
562
562
  const edge = graph.edges[ei];
563
- 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;
563
+ return edge.from === a && edge.to !== a && sides2[ei].from === "end" && sides2[ei].to === "start" && at.get(`${edge.to}|start`)?.length === 1 ? ei : void 0;
564
564
  };
565
565
  const joined = /* @__PURE__ */ new Set();
566
566
  const runOf = /* @__PURE__ */ new Map();
@@ -620,7 +620,7 @@ function mergeRuns(graph) {
620
620
  graph.edges.forEach((edge, ei) => {
621
621
  const from = runOf.get(edge.from);
622
622
  const to = runOf.get(edge.to);
623
- const side = sides[ei];
623
+ const side = sides2[ei];
624
624
  const key = `${from}|${side.from}>${to}|${side.to}`;
625
625
  if (joined.has(ei) || seen.has(key)) {
626
626
  return;
@@ -971,23 +971,31 @@ var MAX_ZOOM = 100;
971
971
  function clampZoom(zoom) {
972
972
  return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
973
973
  }
974
- function fittedTranslateY(bounds, usableHeight, yScale) {
974
+ function fittedTranslateY(bounds, usableHeight, yScale, padTop = FIT_PADDING) {
975
975
  const leftoverY = usableHeight - bounds.h * yScale;
976
- return FIT_PADDING - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
976
+ return padTop - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
977
977
  }
978
- function fitTransform(bounds, paneWidth, paneHeight, pixelRows) {
979
- const usableWidth = paneWidth - FIT_PADDING * 2;
980
- const usableHeight = paneHeight - FIT_PADDING * 2;
978
+ function fitTransform(bounds, paneWidth, paneHeight, pixelRows, { minScale = 0, padLeft = FIT_PADDING, padTop = FIT_PADDING } = {}) {
979
+ const usableWidth = paneWidth - padLeft - FIT_PADDING;
980
+ const usableHeight = paneHeight - padTop - FIT_PADDING;
981
981
  if (usableWidth <= 0 || usableHeight <= 0 || !(bounds.w > 0 || !pixelRows && bounds.h > 0)) {
982
982
  return void 0;
983
983
  }
984
984
  const fitX = bounds.w > 0 ? usableWidth / bounds.w : Infinity;
985
985
  const fitY = bounds.h > 0 ? usableHeight / bounds.h : Infinity;
986
- const scale = clampZoom(pixelRows ? fitX : Math.min(fitX, fitY));
986
+ const scale = clampZoom(
987
+ Math.max(minScale, pixelRows ? fitX : Math.min(fitX, fitY))
988
+ );
989
+ const leftoverX = usableWidth - bounds.w * scale;
987
990
  return {
988
991
  scale,
989
- translateX: FIT_PADDING - bounds.minX * scale + (usableWidth - bounds.w * scale) / 2,
990
- translateY: fittedTranslateY(bounds, usableHeight, pixelRows ? 1 : scale)
992
+ translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
993
+ translateY: fittedTranslateY(
994
+ bounds,
995
+ usableHeight,
996
+ pixelRows ? 1 : scale,
997
+ padTop
998
+ )
991
999
  };
992
1000
  }
993
1001
  function drawingBounds(layout, opts = {}) {
@@ -2010,7 +2018,6 @@ function walkRows(graph, region, unit) {
2010
2018
  return void 0;
2011
2019
  }
2012
2020
  const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
2013
- const onReference = new Set(reference.nodeIds);
2014
2021
  const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
2015
2022
  const cut = region && region.end > region.start ? region : void 0;
2016
2023
  const span = /* @__PURE__ */ new Map();
@@ -2028,17 +2035,34 @@ function walkRows(graph, region, unit) {
2028
2035
  const runs = [];
2029
2036
  let bp = 0;
2030
2037
  let offReferenceBp = 0;
2038
+ let step = 0;
2031
2039
  for (const id of ids) {
2032
2040
  const len = lengthOf.get(id) ?? 0;
2033
- const shared = onReference.has(id);
2041
+ const s = span.get(id);
2034
2042
  const last = runs.at(-1);
2035
- if (last?.onReference === shared) {
2043
+ const at = last?.referenceStart;
2044
+ const forward2 = s && at !== void 0 && step >= 0 && at + last.bp === s.start;
2045
+ const backward = s && at !== void 0 && step <= 0 && s.end === at;
2046
+ if (!s && last && !last.onReference) {
2036
2047
  last.bp += len;
2048
+ } else if (forward2 || backward) {
2049
+ last.bp += len;
2050
+ if (!forward2) {
2051
+ last.referenceStart = s.start;
2052
+ last.reversed = true;
2053
+ }
2054
+ step = forward2 ? 1 : -1;
2037
2055
  } else {
2038
- runs.push({ start: bp, bp: len, onReference: shared });
2056
+ runs.push({
2057
+ start: bp,
2058
+ bp: len,
2059
+ onReference: s !== void 0,
2060
+ referenceStart: s?.start
2061
+ });
2062
+ step = 0;
2039
2063
  }
2040
2064
  bp += len;
2041
- if (!shared) {
2065
+ if (!s) {
2042
2066
  offReferenceBp += len;
2043
2067
  }
2044
2068
  }
@@ -2046,6 +2070,7 @@ function walkRows(graph, region, unit) {
2046
2070
  name: path.name,
2047
2071
  label: labelOf(path),
2048
2072
  sample: sampleOf(path),
2073
+ haplotype: path.haplotype,
2049
2074
  bp,
2050
2075
  offReferenceBp,
2051
2076
  complete,
@@ -2574,9 +2599,9 @@ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY,
2574
2599
  offsetY
2575
2600
  );
2576
2601
  }
2577
- const sides = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
2578
- const fromAttach = attachment(fromSegments, sides.from);
2579
- const toAttach = attachment(toSegments, sides.to);
2602
+ const sides2 = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
2603
+ const fromAttach = attachment(fromSegments, sides2.from);
2604
+ const toAttach = attachment(toSegments, sides2.to);
2580
2605
  const fromEnd = fromAttach.at;
2581
2606
  const toStart = toAttach.at;
2582
2607
  const p1x = fromEnd.x + offsetX;
@@ -3066,7 +3091,9 @@ function hashColor(str, alpha) {
3066
3091
  var REFERENCE_RAMP_MAX_HUE = 300;
3067
3092
  var REFERENCE_RAMP_SATURATION = 0.7;
3068
3093
  var REFERENCE_RAMP_LIGHTNESS = 0.5;
3069
- var REFERENCE_RAMP_ALT_COLOR = packAbgr2(60, 65, 72, 255);
3094
+ var REFERENCE_RAMP_ALT_RGB = [60, 65, 72];
3095
+ var REFERENCE_RAMP_ALT_COLOR = packAbgr2(...REFERENCE_RAMP_ALT_RGB, 255);
3096
+ var REFERENCE_RAMP_ALT_CSS = `rgb(${REFERENCE_RAMP_ALT_RGB.join(", ")})`;
3070
3097
  function computeReferenceRamp(graph, domain) {
3071
3098
  const midpoints = referenceMidpoints(graph);
3072
3099
  if (domain && domain.end > domain.start) {
@@ -4123,6 +4150,7 @@ function tubeMapPicture(layout) {
4123
4150
  }
4124
4151
  });
4125
4152
  return {
4153
+ bounds: layout.bounds,
4126
4154
  layers: [layerOf(layout, "haplotype"), layerOf(layout, "read")],
4127
4155
  nodes,
4128
4156
  mismatches: tubeMapMismatches(layout).filter(
@@ -4193,6 +4221,11 @@ function fillByColor(ctx, items, visible, addTo) {
4193
4221
  }
4194
4222
  ctx.globalAlpha = 1;
4195
4223
  }
4224
+ var FULL_OUTLINE_PX = 12;
4225
+ var MIN_OUTLINE_ALPHA = 0.15;
4226
+ function outlineAlpha(screenWidth) {
4227
+ return Math.max(MIN_OUTLINE_ALPHA, Math.min(1, screenWidth / FULL_OUTLINE_PX));
4228
+ }
4196
4229
  function drawTubeMap(ctx, picture, frame) {
4197
4230
  const { x, y, width } = frame;
4198
4231
  const visible = (item) => x(item.x1) >= 0 && x(item.x0) <= width;
@@ -4217,7 +4250,9 @@ function drawTubeMap(ctx, picture, frame) {
4217
4250
  ctx.fillStyle = lit ? "rgba(255,192,203,0.5)" : fill;
4218
4251
  ctx.strokeStyle = lit ? "#ff0000" : stroke;
4219
4252
  ctx.fill();
4253
+ ctx.globalAlpha = lit ? 1 : outlineAlpha(x(node.x1) - x(node.x0));
4220
4254
  ctx.stroke();
4255
+ ctx.globalAlpha = 1;
4221
4256
  }
4222
4257
  }
4223
4258
  drawMismatches(ctx, picture.mismatches, frame);
@@ -4350,720 +4385,1057 @@ function tubeMapNodeAt(drawing, { x, y }, sx, sy) {
4350
4385
  return hit;
4351
4386
  }
4352
4387
 
4353
- // src/walkHighlight.ts
4354
- function walkHighlight(graph, name) {
4355
- const path = graph.paths?.find((p) => p.name === name);
4356
- if (!path) {
4357
- return void 0;
4358
- }
4359
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
4360
- const edgeAt = /* @__PURE__ */ new Map();
4361
- graph.edges.forEach((e, i) => {
4362
- edgeAt.set(`${e.from}>${e.to}`, i);
4363
- });
4364
- const nodeIds = new Set(path.nodeIds);
4365
- const edgeIndexes = /* @__PURE__ */ new Set();
4366
- for (let i = 1; i < path.nodeIds.length; i++) {
4367
- const a = path.nodeIds[i - 1];
4368
- const b = path.nodeIds[i];
4369
- const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
4370
- if (index !== void 0) {
4371
- edgeIndexes.add(index);
4388
+ // src/overlayLabels.ts
4389
+ var LABEL_PX = 11;
4390
+ var LABEL_CHAR_PX = 6.2;
4391
+ var LABEL_PAD = 4;
4392
+ function labelWidth(text) {
4393
+ return text.length * LABEL_CHAR_PX + LABEL_PAD * 2;
4394
+ }
4395
+ function overlaps(a, b) {
4396
+ return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
4397
+ }
4398
+ function offFrame(box, frame) {
4399
+ return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
4400
+ }
4401
+ function insideFrame(box, frame) {
4402
+ return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
4403
+ }
4404
+ function occupancy(frame, reserved = []) {
4405
+ const placed = [...reserved];
4406
+ return (box) => {
4407
+ if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
4408
+ return false;
4372
4409
  }
4373
- }
4374
- const bpOf = (ids) => ids.reduce((sum, id) => sum + (byId.get(id)?.length ?? 0), 0);
4375
- const reference = graph.referencePath ? graph.paths.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
4376
- return {
4377
- name,
4378
- nodeIds,
4379
- edgeIndexes,
4380
- steps: path.nodeIds.length,
4381
- bp: bpOf(path.nodeIds),
4382
- referenceBp: reference && reference !== path ? bpOf(reference.nodeIds) : void 0
4410
+ placed.push(box);
4411
+ return true;
4383
4412
  };
4384
4413
  }
4385
-
4386
- // src/bubbles/bubbleLine.ts
4387
- var SATURATED_PATH_COUNT = 2147483647;
4388
-
4389
- // src/bubbles/bubblesFromGraph.ts
4390
- function bubblesFromGraph(graph) {
4391
- if (!graph.nodes.some(isBackbone)) {
4392
- return [];
4393
- }
4394
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
4395
- const order = referenceOrder(graph);
4396
- const indexOf = new Map(order.map((id, i) => [id, i]));
4397
- const reach = new Int32Array(order.length + 1);
4398
- const succ = /* @__PURE__ */ new Map();
4399
- const linked = /* @__PURE__ */ new Set();
4400
- for (const e of graph.edges) {
4401
- const a = indexOf.get(e.from);
4402
- const b = indexOf.get(e.to);
4403
- if (a === void 0 || b === void 0 || a === b) {
4404
- continue;
4405
- }
4406
- linked.add(`${e.from}>${e.to}`).add(`${e.to}>${e.from}`);
4407
- const [lo, hi] = a < b ? [a, b] : [b, a];
4408
- reach[lo + 1]++;
4409
- reach[hi]--;
4410
- const [from, to] = a < b ? [e.from, e.to] : [e.to, e.from];
4411
- (succ.get(from) ?? succ.set(from, /* @__PURE__ */ new Set()).get(from)).add(to);
4412
- }
4413
- for (let i = 1; i < reach.length; i++) {
4414
- reach[i] += reach[i - 1];
4415
- }
4416
- const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
4417
- const ends = /* @__PURE__ */ new Set([backbone[0].id, backbone.at(-1).id]);
4418
- const boundaries = backbone.map((node) => [node, indexOf.get(node.id)]).filter(([node, i]) => ends.has(node.id) || reach[i] === 0).sort(([a, ia], [b, ib]) => ia - ib || a.stable.start - b.stable.start);
4419
- const walkIndex = graph.paths?.map((p) => {
4420
- const at = /* @__PURE__ */ new Map();
4421
- p.nodeIds.forEach((id, i) => {
4422
- if (!at.has(id)) {
4423
- at.set(id, i);
4424
- }
4425
- });
4426
- return at;
4427
- });
4428
- const bubbles = [];
4429
- for (let i = 0; i + 1 < boundaries.length; i++) {
4430
- const [start, i0] = boundaries[i];
4431
- const [end, i1] = boundaries[i + 1];
4432
- const interior = order.slice(i0 + 1, i1);
4433
- if (interior.length === 0) {
4434
- continue;
4435
- }
4436
- const best = /* @__PURE__ */ new Map([[start.id, { min: 0, max: 0, n: 1 }]]);
4437
- for (const id of [start.id, ...interior]) {
4438
- const cur = best.get(id);
4439
- if (!cur) {
4440
- continue;
4414
+ function placeLabels(candidates, frame, take) {
4415
+ const out = [];
4416
+ const row = LABEL_PX + LABEL_PAD * 2;
4417
+ for (const c of candidates) {
4418
+ const w = labelWidth(c.text);
4419
+ const direction = c.y > frame.height / 2 ? -1 : 1;
4420
+ for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
4421
+ const y = c.y + direction * tries * row;
4422
+ const box = {
4423
+ x0: c.x - w / 2,
4424
+ x1: c.x + w / 2,
4425
+ y0: y - LABEL_PX - LABEL_PAD,
4426
+ y1: y + LABEL_PAD
4427
+ };
4428
+ if (offFrame(box, frame)) {
4429
+ break;
4441
4430
  }
4442
- for (const t of succ.get(id) ?? []) {
4443
- const ti = indexOf.get(t);
4444
- if (ti > i1) {
4445
- continue;
4446
- }
4447
- const add = t === end.id ? 0 : byId.get(t).length;
4448
- const prev = best.get(t);
4449
- best.set(t, {
4450
- min: Math.min(prev?.min ?? Infinity, cur.min + add),
4451
- max: Math.max(prev?.max ?? -Infinity, cur.max + add),
4452
- n: Math.min(SATURATED_PATH_COUNT, cur.n + (prev?.n ?? 0))
4453
- });
4431
+ if (insideFrame(box, frame) && take(box)) {
4432
+ out.push({ ...c, y, w });
4433
+ break;
4454
4434
  }
4455
4435
  }
4456
- const walked = walkIndex && walkRoutes(graph, byId, walkIndex, start.id, end.id);
4457
- const crossed = walked?.n ? walked : void 0;
4458
- const routes2 = crossed ?? best.get(end.id) ?? { min: 0, max: 0, n: 0 };
4459
- const walksLeave = walked !== void 0 && walked.left > 0;
4460
- const refStart = start.stable.start + start.length;
4461
- const refEnd = end.stable.start;
4462
- const chain = [
4463
- start.id,
4464
- ...interior.filter((id) => isBackbone(byId.get(id))),
4465
- end.id
4466
- ];
4467
- const chainBroken = chain.some(
4468
- (id, k) => k > 0 && !linked.has(`${chain[k - 1]}>${id}`)
4469
- );
4470
- const partial = chainBroken || walksLeave;
4471
- const fallback = chainBroken && !crossed;
4472
- bubbles.push({
4473
- refName: start.stable.refName,
4474
- start: refStart,
4475
- end: refEnd,
4476
- segmentCount: interior.length + 2,
4477
- pathCount: routes2.n,
4478
- // a real inversion test needs each walk's direction relative to the
4479
- // reference; the first visit's strand is just whichever path anchored the
4480
- // node
4481
- inversion: false,
4482
- shortestAlleleLength: fallback ? refEnd - refStart : routes2.min,
4483
- longestAlleleLength: fallback ? refEnd - refStart : routes2.max,
4484
- segments: [start, ...interior.map((id) => byId.get(id)), end].map((n) => n.name).join(","),
4485
- shortestAllele: void 0,
4486
- longestAllele: void 0,
4487
- partial,
4488
- routes: crossed?.routes
4489
- });
4490
4436
  }
4491
- return bubbles;
4492
- }
4493
- function walkRoutes(graph, byId, walkIndex, startId, endId) {
4494
- const seen = /* @__PURE__ */ new Map();
4495
- let min = Infinity;
4496
- let max = -Infinity;
4497
- let left = 0;
4498
- graph.paths.forEach((p, k) => {
4499
- const i0 = walkIndex[k].get(startId);
4500
- const i1 = walkIndex[k].get(endId);
4501
- if (i0 === void 0 || i1 === void 0) {
4502
- if (i0 !== void 0 || i1 !== void 0) {
4503
- left++;
4504
- }
4505
- return;
4506
- }
4507
- const steps = p.nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
4508
- if (i1 < i0) {
4509
- steps.reverse();
4510
- }
4511
- let bp = 0;
4512
- for (const id of steps) {
4513
- bp += byId.get(id)?.length ?? 0;
4514
- }
4515
- const key = steps.join(",");
4516
- const route = seen.get(key) ?? { steps, bp, walks: [] };
4517
- route.walks.push(p.name);
4518
- seen.set(key, route);
4519
- min = Math.min(min, bp);
4520
- max = Math.max(max, bp);
4521
- });
4522
- return seen.size || left ? { min, max, n: seen.size, left, routes: [...seen.values()] } : void 0;
4437
+ return out;
4523
4438
  }
4524
4439
 
4525
- // src/bubbles/classifyBubble.ts
4526
- var REPEAT_GROWTH = 10;
4527
- function coveringRepeat(b, repeats) {
4528
- const span = b.end - b.start;
4529
- const growth = b.longestAlleleLength - b.shortestAlleleLength;
4530
- return repeats.find(
4531
- (r) => growth <= REPEAT_GROWTH * (r.end - r.start) && (span === 0 ? r.start <= b.start && b.start <= r.end : Math.min(b.end, r.end) - Math.max(b.start, r.start) >= span / 2)
4532
- );
4533
- }
4534
- var SUPERBUBBLE_SEGMENTS = 40;
4535
- function formatBp(bp) {
4536
- if (bp >= 1e4) {
4537
- return `${(bp / 1e3).toFixed(0)} kb`;
4538
- }
4539
- if (bp >= 1e3) {
4540
- return `${(bp / 1e3).toFixed(1)} kb`;
4541
- }
4542
- return `${bp} bp`;
4440
+ // src/graphLabels.ts
4441
+ var MIN_DELETION_LABEL_PX = 26;
4442
+ var PX_PER_NODE_LABEL = 4e4;
4443
+ var MIN_NODE_LABELS = 8;
4444
+ var LEADER_GAP_PX = 8;
4445
+ var LABEL_CHAR_PX2 = 5.7;
4446
+ var LABEL_PAD_PX = 6;
4447
+ var LABEL_HEIGHT_PX = 13;
4448
+ var LABEL_GAP_PX = 3;
4449
+ var MAX_LABEL_OVERHANG = 2;
4450
+ function labelHalfWidth(text) {
4451
+ return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
4543
4452
  }
4544
- function formatBpRange(shortest, longest) {
4545
- const low = formatBp(shortest);
4546
- const high = formatBp(longest);
4547
- const unit = high.slice(high.indexOf(" "));
4548
- return low.endsWith(unit) ? `${low.slice(0, -unit.length)}\u2013${high}` : `${low}\u2013${high}`;
4453
+ var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
4454
+ var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
4455
+ function boxAt(halfW, x, y) {
4456
+ return {
4457
+ x0: x - halfW,
4458
+ x1: x + halfW,
4459
+ y0: y - LABEL_HALF_HEIGHT,
4460
+ y1: y + LABEL_HALF_HEIGHT
4461
+ };
4549
4462
  }
4550
- function routes(count) {
4551
- if (count >= SATURATED_PATH_COUNT) {
4552
- return "\u22652.1B routes";
4553
- }
4554
- if (count >= 1e5) {
4555
- return `${(count / 1e3).toFixed(0)}k routes`;
4556
- }
4557
- return `${count} routes`;
4463
+ function rowLabelBox(text, screenY) {
4464
+ return {
4465
+ x0: 6,
4466
+ x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
4467
+ y0: screenY - 8 - LABEL_GAP_PX / 2,
4468
+ y1: screenY + 8 + LABEL_GAP_PX / 2
4469
+ };
4558
4470
  }
4559
- function classifyBubble(b, repeats = []) {
4560
- const c = classifyShape(b, repeats);
4561
- return b.partial ? { ...c, label: `${c.label}, partial` } : c;
4471
+ function clamp(v, lo, hi) {
4472
+ return hi < lo ? v : Math.min(Math.max(v, lo), hi);
4562
4473
  }
4563
- function classifyShape(b, repeats) {
4564
- const refSpan = b.end - b.start;
4565
- const {
4566
- shortestAlleleLength: shortest,
4567
- longestAlleleLength: longest,
4568
- pathCount,
4569
- segmentCount,
4570
- inversion
4571
- } = b;
4572
- const range = formatBpRange(shortest, longest);
4573
- if (segmentCount >= SUPERBUBBLE_SEGMENTS) {
4574
- return {
4575
- kind: "superbubble",
4576
- label: `${range} superbubble, ${segmentCount} segments, ${routes(pathCount)}${inversion ? ", inv" : ""}`
4577
- };
4474
+ function formatBp(bp) {
4475
+ if (bp >= 1e6) {
4476
+ return `${+(bp / 1e6).toFixed(1)} Mb`;
4578
4477
  }
4579
- if (inversion) {
4580
- return { kind: "inversion", label: `${formatBp(longest)} inv` };
4478
+ return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
4479
+ }
4480
+ function deletionText(bp) {
4481
+ return `${formatBp(bp)} del`;
4482
+ }
4483
+ function arcOutward(curves, apex, scaleX, scaleY) {
4484
+ const first = curves[0];
4485
+ const last = curves[curves.length - 1];
4486
+ const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
4487
+ const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
4488
+ const len = Math.hypot(dx, dy);
4489
+ return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
4490
+ }
4491
+ function boxSupport(box, dir) {
4492
+ return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
4493
+ }
4494
+ function boxRayCrossing(box, dir) {
4495
+ const halfW = (box.x1 - box.x0) / 2;
4496
+ const halfH = (box.y1 - box.y0) / 2;
4497
+ return Math.min(
4498
+ dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
4499
+ dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
4500
+ );
4501
+ }
4502
+ function drawnLength(segments, scaleX, scaleY) {
4503
+ let total = 0;
4504
+ for (let i = 1; i < segments.length; i++) {
4505
+ const a = segments[i - 1];
4506
+ const b = segments[i];
4507
+ total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
4581
4508
  }
4582
- const repeat = longest !== shortest ? coveringRepeat(b, repeats) : void 0;
4583
- if (repeat) {
4584
- const name = repeat.name ? ` (${repeat.name})` : "";
4585
- return {
4586
- kind: "repeat",
4587
- label: `${range} repeat array${name}, ${routes(pathCount)}`
4588
- };
4509
+ return total;
4510
+ }
4511
+ function runBounds(nodeIds, nodePositions) {
4512
+ let minX = Infinity;
4513
+ let minY = Infinity;
4514
+ let maxX = -Infinity;
4515
+ let maxY = -Infinity;
4516
+ for (const id of nodeIds) {
4517
+ for (const p of nodePositions[id] ?? []) {
4518
+ minX = Math.min(minX, p.x);
4519
+ maxX = Math.max(maxX, p.x);
4520
+ minY = Math.min(minY, p.y);
4521
+ maxY = Math.max(maxY, p.y);
4522
+ }
4589
4523
  }
4590
- const alleles = pathCount > 2 ? `, ${pathCount} alleles` : "";
4591
- if (shortest === longest && shortest === refSpan) {
4592
- return refSpan <= 1 ? { kind: "snp", label: "SNP" } : { kind: "substitution", label: `${formatBp(refSpan)} sub` };
4524
+ return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
4525
+ }
4526
+ var nodeOrderCache = /* @__PURE__ */ new WeakMap();
4527
+ function labelOrder(nodePositions, nodeLengths) {
4528
+ const cached2 = nodeOrderCache.get(nodePositions);
4529
+ if (cached2?.nodeLengths === nodeLengths) {
4530
+ return cached2.order;
4593
4531
  }
4594
- if (refSpan === 0 || shortest === refSpan) {
4595
- return {
4596
- kind: "insertion",
4597
- label: `\u2264${formatBp(longest - refSpan)} ins${alleles}`
4598
- };
4532
+ const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
4533
+ nodeOrderCache.set(nodePositions, { nodeLengths, order });
4534
+ return order;
4535
+ }
4536
+ var arcCache = /* @__PURE__ */ new WeakMap();
4537
+ function arcPlacements(nodePositions, deletions, axis, version) {
4538
+ const { scaleX, scaleY } = axis;
4539
+ const cached2 = arcCache.get(nodePositions);
4540
+ if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
4541
+ return cached2.arcs;
4599
4542
  }
4600
- if (longest === refSpan) {
4601
- return {
4602
- kind: "deletion",
4603
- label: `${formatBp(refSpan - shortest)} del${alleles}`
4604
- };
4543
+ const arcs = [];
4544
+ for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
4545
+ const curves = deletionArcCurves(nodePositions, deletion, axis);
4546
+ const apex = curves ? curveMidpoint(curves) : void 0;
4547
+ if (curves && apex) {
4548
+ const { minX, minY, maxX, maxY } = curveBounds(curves);
4549
+ arcs.push({
4550
+ deletion,
4551
+ curves,
4552
+ apex,
4553
+ // in screen px, so the two axes' units cannot be added by accident
4554
+ extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
4555
+ });
4556
+ }
4605
4557
  }
4606
- return {
4607
- kind: "complex",
4608
- label: `${formatBp(refSpan)} ref \u2192 ${range}, ${pathCount} alleles`
4609
- };
4610
- }
4611
- var BUBBLE_KIND_COLORS = {
4612
- snp: "#5b6b7a",
4613
- substitution: "#5b6b7a",
4614
- insertion: "#2f8fd6",
4615
- deletion: "#c94040",
4616
- inversion: "#e07b00",
4617
- repeat: "#8e3fbf",
4618
- superbubble: "#8e3fbf",
4619
- complex: "#8e3fbf"
4620
- };
4621
- var BUBBLE_KIND_NAMES = {
4622
- snp: "SNP",
4623
- substitution: "substitution",
4624
- insertion: "insertion",
4625
- deletion: "deletion",
4626
- inversion: "inversion",
4627
- repeat: "repeat array",
4628
- superbubble: "superbubble",
4629
- complex: "site"
4630
- };
4631
- function bubbleSegmentIds(b) {
4632
- return b.segments.split(",").filter(Boolean);
4558
+ arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
4559
+ return arcs;
4633
4560
  }
4634
-
4635
- // src/bubbles/bubbleHalos.ts
4636
- var WHOLE_FRACTION = 0.9;
4637
- var NAMED_WALKS = 2;
4638
- var MIN_CHIPPED_BP = 1e3;
4639
- function bubbleHalos(graph, bubbles, positions, walkLabel = (name) => name, repeats = []) {
4640
- const byName = new Map(graph.nodes.map((n) => [n.name, n]));
4641
- const halos = [];
4642
- for (const bubble of bubbles) {
4643
- const parts = [];
4644
- let minX = Infinity;
4645
- let maxX = -Infinity;
4646
- let minY = Infinity;
4647
- const nodeIds = [];
4648
- const ends = [];
4649
- for (const name of bubbleSegmentIds(bubble)) {
4650
- const node = byName.get(name);
4651
- const line = node && positions[node.id];
4652
- if (!node || !line?.length) {
4653
- continue;
4654
- }
4655
- if (isBackbone(node) && (node.stable.start < bubble.start || node.stable.start >= bubble.end)) {
4656
- ends.push(line[Math.floor(line.length / 2)]);
4657
- continue;
4658
- }
4659
- nodeIds.push(node.id);
4660
- parts.push(svgPath(line.length === 1 ? [line[0], line[0]] : line));
4661
- for (const p of line) {
4662
- minX = Math.min(minX, p.x);
4663
- maxX = Math.max(maxX, p.x);
4664
- minY = Math.min(minY, p.y);
4561
+ function sizeLabelCandidates({
4562
+ nodePositions,
4563
+ nodeLengths,
4564
+ deletions,
4565
+ alleleDeletions,
4566
+ axis,
4567
+ translateX,
4568
+ translateY,
4569
+ width,
4570
+ height,
4571
+ version = 0
4572
+ }) {
4573
+ const { scaleX, scaleY } = axis;
4574
+ const deletionCandidates = [];
4575
+ const nodeCandidates = [];
4576
+ for (const { deletion, curves, apex, extent } of arcPlacements(
4577
+ nodePositions,
4578
+ deletions,
4579
+ axis,
4580
+ version
4581
+ )) {
4582
+ if (extent >= MIN_DELETION_LABEL_PX) {
4583
+ const text = deletionText(deletion.bp);
4584
+ const halfW = labelHalfWidth(text);
4585
+ const box = boxAt(halfW, 0, 0);
4586
+ const arcX = apex.x * scaleX + translateX;
4587
+ const arcY = apex.y * scaleY + translateY;
4588
+ const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
4589
+ const bow = arcOutward(curves, apex, scaleX, scaleY);
4590
+ const offset = boxSupport(box, bow) + LEADER_GAP_PX;
4591
+ const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
4592
+ const y = fits ? arcY : clamp(
4593
+ arcY + bow.y * offset,
4594
+ LABEL_HALF_HEIGHT,
4595
+ height - LABEL_HALF_HEIGHT
4596
+ );
4597
+ const away = Math.hypot(x - arcX, y - arcY);
4598
+ let leader;
4599
+ if (away > 0) {
4600
+ const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
4601
+ const crossing = boxRayCrossing(box, dir);
4602
+ leader = {
4603
+ arcX,
4604
+ arcY,
4605
+ labelX: x - dir.x * crossing,
4606
+ labelY: y - dir.y * crossing
4607
+ };
4665
4608
  }
4609
+ deletionCandidates.push({
4610
+ label: {
4611
+ key: `del:${deletion.edgeIndex}`,
4612
+ text,
4613
+ x,
4614
+ y,
4615
+ kind: "deletion",
4616
+ leader
4617
+ },
4618
+ box: boxAt(halfW, x, y)
4619
+ });
4666
4620
  }
4667
- if (nodeIds.length === 0) {
4621
+ }
4622
+ const labelledByDeletion = /* @__PURE__ */ new Set();
4623
+ const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
4624
+ for (const run of alleleRuns) {
4625
+ const drawn = runBounds(run.nodeIds, nodePositions);
4626
+ if (!drawn) {
4668
4627
  continue;
4669
4628
  }
4670
- const labelAt = { x: (minX + maxX) / 2, y: minY };
4671
- const anchor = ends.length ? {
4672
- x: ends.reduce((s, p) => s + p.x, 0) / ends.length,
4673
- y: ends.reduce((s, p) => s + p.y, 0) / ends.length
4674
- } : labelAt;
4675
- const sharing = /* @__PURE__ */ new Map();
4676
- for (const route of bubble.routes ?? []) {
4677
- for (const id of new Set(route.steps)) {
4678
- sharing.set(id, (sharing.get(id) ?? 0) + 1);
4679
- }
4629
+ const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
4630
+ const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
4631
+ const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
4632
+ const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
4633
+ if (shownRight < shownLeft || shownBottom < shownTop) {
4634
+ continue;
4680
4635
  }
4681
- const routes2 = [];
4682
- for (const route of bubble.routes ?? []) {
4683
- if (bubble.longestAlleleLength < MIN_CHIPPED_BP) {
4684
- break;
4685
- }
4686
- if (route.steps.length === 0) {
4687
- continue;
4688
- }
4689
- const rarest = Math.min(...route.steps.map((id) => sharing.get(id)));
4690
- let at;
4691
- let far = -1;
4692
- for (const id of route.steps) {
4693
- if (sharing.get(id) !== rarest) {
4694
- continue;
4695
- }
4696
- for (const p of positions[id] ?? []) {
4697
- const d = Math.hypot(p.x - anchor.x, p.y - anchor.y);
4698
- if (d > far) {
4699
- far = d;
4700
- at = p;
4701
- }
4702
- }
4703
- }
4704
- if (at) {
4705
- routes2.push({ route, at, text: routeText(route, walkLabel) });
4636
+ const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
4637
+ const text = deletionText(run.bp);
4638
+ const halfW = labelHalfWidth(text);
4639
+ if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
4640
+ for (const id of run.nodeIds) {
4641
+ labelledByDeletion.add(id);
4706
4642
  }
4643
+ const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
4644
+ const y = clamp(
4645
+ (shownTop + shownBottom) / 2,
4646
+ LABEL_HALF_HEIGHT,
4647
+ height - LABEL_HALF_HEIGHT
4648
+ );
4649
+ deletionCandidates.push({
4650
+ label: {
4651
+ key: `alleledel:${run.nodeIds.join(",")}`,
4652
+ text,
4653
+ x,
4654
+ y,
4655
+ kind: "deletion"
4656
+ },
4657
+ box: boxAt(halfW, x, y)
4658
+ });
4659
+ }
4660
+ }
4661
+ for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
4662
+ if (labelledByDeletion.has(id)) {
4663
+ continue;
4664
+ }
4665
+ const { x, y } = polylineMidpoint(segments);
4666
+ const screenY = y * scaleY + translateY;
4667
+ if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
4668
+ continue;
4669
+ }
4670
+ const drawn = drawnLength(segments, scaleX, scaleY);
4671
+ if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
4672
+ continue;
4673
+ }
4674
+ const text = formatBp(nodeLengths.get(id));
4675
+ const halfW = labelHalfWidth(text);
4676
+ if (drawn >= halfW * 2 / MAX_LABEL_OVERHANG) {
4677
+ const screenX = x * scaleX + translateX;
4678
+ nodeCandidates.push({
4679
+ label: {
4680
+ key: `node:${id}`,
4681
+ text,
4682
+ x: screenX,
4683
+ y: screenY,
4684
+ kind: "node"
4685
+ },
4686
+ box: boxAt(halfW, screenX, screenY)
4687
+ });
4688
+ }
4689
+ }
4690
+ return { deletions: deletionCandidates, nodes: nodeCandidates };
4691
+ }
4692
+ function nodeLabelBudget(width, height) {
4693
+ return Math.max(
4694
+ MIN_NODE_LABELS,
4695
+ Math.floor(width * height / PX_PER_NODE_LABEL)
4696
+ );
4697
+ }
4698
+ function placeSizeLabels(candidates, take, budget = Infinity) {
4699
+ const labels = [];
4700
+ for (const { label, box } of candidates) {
4701
+ if (labels.length >= budget) {
4702
+ break;
4703
+ }
4704
+ if (take(box)) {
4705
+ labels.push(label);
4706
+ }
4707
+ }
4708
+ return labels;
4709
+ }
4710
+
4711
+ // src/tubeMap/axis.ts
4712
+ function referenceBoxes(graph, layout) {
4713
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
4714
+ const byRefName = /* @__PURE__ */ new Map();
4715
+ layout.nodes.forEach((node) => {
4716
+ const graphNode = nodeById.get(node.name);
4717
+ if (node.order >= 0 && graphNode && isBackbone(graphNode)) {
4718
+ const { refName, start } = graphNode.stable;
4719
+ const boxes = byRefName.get(refName) ?? byRefName.set(refName, []).get(refName);
4720
+ boxes.push({
4721
+ bp0: start,
4722
+ bp1: start + node.sequenceLength,
4723
+ x0: node.x,
4724
+ x1: node.x + node.pixelWidth
4725
+ });
4726
+ }
4727
+ });
4728
+ for (const boxes of byRefName.values()) {
4729
+ boxes.sort((a, b) => a.bp0 - b.bp0);
4730
+ }
4731
+ return byRefName;
4732
+ }
4733
+ function tubeX(boxes, bp, isEnd = false) {
4734
+ let lo = 0;
4735
+ let hi = boxes.length - 1;
4736
+ while (lo < hi) {
4737
+ const mid = lo + hi >> 1;
4738
+ const bp1 = boxes[mid].bp1;
4739
+ if (isEnd ? bp1 < bp : bp1 <= bp) {
4740
+ lo = mid + 1;
4741
+ } else {
4742
+ hi = mid;
4743
+ }
4744
+ }
4745
+ const box = boxes[lo];
4746
+ if (bp <= box.bp0) {
4747
+ return box.x0;
4748
+ }
4749
+ const span = box.bp1 - box.bp0;
4750
+ return span > 0 ? box.x0 + (Math.min(bp, box.bp1) - box.bp0) / span * (box.x1 - box.x0) : box.x0;
4751
+ }
4752
+ function tubeSpan(boxes, start, end) {
4753
+ const a = tubeX(boxes, start);
4754
+ const b = tubeX(boxes, end, true);
4755
+ return { x0: Math.min(a, b), x1: Math.max(a, b) };
4756
+ }
4757
+ function rulerBoxes(byRefName) {
4758
+ let best;
4759
+ for (const boxes of byRefName.values()) {
4760
+ if (!best || boxes.length > best.length) {
4761
+ best = boxes;
4762
+ }
4763
+ }
4764
+ return best;
4765
+ }
4766
+ var TICK_PX = 5;
4767
+ var LABEL_GAP_PX2 = 10;
4768
+ var TARGET_TICK_PX = 110;
4769
+ var ZIGZAG_PX = 4;
4770
+ var ZIGZAG_AMPLITUDE_PX = 2.5;
4771
+ function rulerInk(darkMode) {
4772
+ return darkMode ? "#b0b0b8" : "#55555c";
4773
+ }
4774
+ function niceStep(raw) {
4775
+ const pow = 10 ** Math.floor(Math.log10(raw));
4776
+ const m = raw / pow;
4777
+ return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;
4778
+ }
4779
+ function rulerMarks(boxes, x, width, measure) {
4780
+ const first = boxes[0];
4781
+ const last = boxes.at(-1);
4782
+ if (!first || !last) {
4783
+ return void 0;
4784
+ }
4785
+ const from = x(first.x0);
4786
+ const to = x(last.x1);
4787
+ const bpSpan = last.bp1 - first.bp0;
4788
+ if (to <= from || bpSpan <= 0) {
4789
+ return void 0;
4790
+ }
4791
+ const step = niceStep(bpSpan / (to - from) * TARGET_TICK_PX);
4792
+ const squeezedBoxes = boxes.filter((box) => {
4793
+ const bp = box.bp1 - box.bp0;
4794
+ return bp >= step && (x(box.x1) - x(box.x0)) * step / bp < TARGET_TICK_PX / 2;
4795
+ });
4796
+ const squeezed = [];
4797
+ const lengths = [];
4798
+ for (const box of squeezedBoxes) {
4799
+ const x0 = x(box.x0);
4800
+ const x1 = x(box.x1);
4801
+ if (x1 >= 0 && x0 <= width) {
4802
+ squeezed.push({ x0, x1 });
4803
+ const text = formatBp(box.bp1 - box.bp0);
4804
+ lengths.push({ text, at: (x0 + x1) / 2, width: measure(text) });
4805
+ }
4806
+ }
4807
+ const ticks = [];
4808
+ const positions = [];
4809
+ let k = 0;
4810
+ for (let bp = Math.ceil(first.bp0 / step) * step; bp <= last.bp1; bp += step) {
4811
+ while (k < squeezedBoxes.length && squeezedBoxes[k].bp1 <= bp) {
4812
+ k++;
4813
+ }
4814
+ if (k < squeezedBoxes.length && squeezedBoxes[k].bp0 < bp) {
4815
+ continue;
4816
+ }
4817
+ const sx = Math.round(x(tubeX(boxes, bp))) + 0.5;
4818
+ if (sx >= 0 && sx <= width) {
4819
+ ticks.push(sx);
4820
+ const text = bp.toLocaleString("en-US");
4821
+ positions.push({ text, at: sx, width: measure(text) });
4707
4822
  }
4708
- halos.push({
4709
- bubble,
4710
- ...classifyBubble(bubble, repeats),
4711
- path: parts.join(""),
4712
- labelAt,
4713
- members: nodeIds.length,
4714
- nodeIds,
4715
- whole: nodeIds.length >= WHOLE_FRACTION * graph.nodes.length,
4716
- routes: routes2
4717
- });
4718
4823
  }
4719
- return halos;
4824
+ const placed = [];
4825
+ const clear = (a) => placed.every(
4826
+ (b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX2
4827
+ );
4828
+ for (const label of [...positions, ...lengths]) {
4829
+ if (clear(label)) {
4830
+ placed.push(label);
4831
+ }
4832
+ }
4833
+ return {
4834
+ from,
4835
+ to,
4836
+ ticks,
4837
+ squeezed,
4838
+ labels: placed.map(({ text, at }) => ({ text, at }))
4839
+ };
4720
4840
  }
4721
- function routeText(route, walkLabel) {
4722
- const names = [...new Set(route.walks.map(walkLabel))].sort();
4723
- const shown = names.slice(0, NAMED_WALKS).join(", ");
4724
- const more = names.length > NAMED_WALKS ? ` +${names.length - NAMED_WALKS}` : "";
4725
- return `${shown}${more} \xB7 ${formatBp(route.bp)}`;
4841
+ function zigzag(ctx, x0, x1, y) {
4842
+ const teeth = Math.max(1, Math.round((x1 - x0) / ZIGZAG_PX));
4843
+ const dx = (x1 - x0) / teeth;
4844
+ for (let i = 0; i < teeth; i++) {
4845
+ ctx.lineTo(x0 + (i + 0.5) * dx, y + (i % 2 ? 1 : -1) * ZIGZAG_AMPLITUDE_PX);
4846
+ }
4847
+ ctx.lineTo(x1, y);
4848
+ }
4849
+ function drawTubeMapRuler(ctx, boxes, frame, top) {
4850
+ const { x, width, darkMode } = frame;
4851
+ ctx.font = "10px sans-serif";
4852
+ const marks = rulerMarks(boxes, x, width, (text) => ctx.measureText(text).width);
4853
+ if (!marks) {
4854
+ return;
4855
+ }
4856
+ const ink = rulerInk(darkMode);
4857
+ const y = top + 0.5;
4858
+ ctx.strokeStyle = ink;
4859
+ ctx.fillStyle = ink;
4860
+ ctx.lineWidth = 1;
4861
+ ctx.textAlign = "center";
4862
+ ctx.textBaseline = "top";
4863
+ ctx.beginPath();
4864
+ ctx.moveTo(Math.max(0, marks.from), y);
4865
+ for (const { x0, x1 } of marks.squeezed) {
4866
+ ctx.lineTo(x0, y);
4867
+ zigzag(ctx, x0, x1, y);
4868
+ }
4869
+ ctx.lineTo(Math.min(width, marks.to), y);
4870
+ for (const sx of marks.ticks) {
4871
+ ctx.moveTo(sx, top);
4872
+ ctx.lineTo(sx, top + TICK_PX);
4873
+ }
4874
+ ctx.stroke();
4875
+ for (const { text, at } of marks.labels) {
4876
+ ctx.fillText(text, at, top + TICK_PX + 2);
4877
+ }
4726
4878
  }
4727
4879
 
4728
- // src/bubbles/popBubble.ts
4729
- function bubbleSubgraph(graph, segmentIds) {
4730
- const keep = new Set(segmentIds);
4731
- const nodes = graph.nodes.filter((n) => keep.has(n.name));
4732
- const ids = new Set(nodes.map((n) => n.id));
4733
- const paths = graph.paths?.map((p) => ({ ...p, nodeIds: p.nodeIds.filter((id) => ids.has(id)) })).filter((p) => p.nodeIds.length > 0);
4734
- return {
4735
- name: graph.name,
4736
- nodes,
4737
- edges: graph.edges.filter((e) => ids.has(e.from) && ids.has(e.to)),
4738
- ...paths?.length ? { paths } : {},
4739
- anchorPaths: graph.anchorPaths,
4740
- pathVisits: graph.pathVisits,
4741
- anchoredBy: graph.anchoredBy,
4742
- referencePath: graph.referencePath
4743
- };
4880
+ // src/tubeMap/genes.ts
4881
+ function tubeMapGenes(byRefName, genes) {
4882
+ const out = [];
4883
+ for (const gene of genes) {
4884
+ const boxes = byRefName.get(gene.refName);
4885
+ const first = boxes?.[0];
4886
+ const last = boxes?.at(-1);
4887
+ if (!boxes || !first || !last) {
4888
+ continue;
4889
+ }
4890
+ const start = Math.max(gene.start, first.bp0);
4891
+ const end = Math.min(gene.end, last.bp1);
4892
+ if (end <= start) {
4893
+ continue;
4894
+ }
4895
+ out.push({
4896
+ gene,
4897
+ ...tubeSpan(boxes, start, end),
4898
+ exons: gene.exons.filter((e) => e.end > start && e.start < end).map(
4899
+ (e) => tubeSpan(boxes, Math.max(e.start, start), Math.min(e.end, end))
4900
+ )
4901
+ });
4902
+ }
4903
+ return out;
4904
+ }
4905
+ var GENE_ROW_PX = 22;
4906
+ var EXON_PX = 8;
4907
+ var NAME_FONT = "11px sans-serif";
4908
+ var NAME_GAP_PX = 8;
4909
+ function drawTubeMapGenes(ctx, genes, frame, bottom, top = 0) {
4910
+ const { x, width, darkMode } = frame;
4911
+ const rows = Math.max(1, Math.floor((bottom - top) / GENE_ROW_PX));
4912
+ const rowEnds = [];
4913
+ const ink = darkMode ? "#e0e0e6" : "#1c1c22";
4914
+ ctx.font = NAME_FONT;
4915
+ ctx.textAlign = "center";
4916
+ ctx.textBaseline = "alphabetic";
4917
+ ctx.fillStyle = ink;
4918
+ ctx.strokeStyle = ink;
4919
+ ctx.lineWidth = 1;
4920
+ const placed = genes.map((g) => ({ g, s0: x(g.x0), s1: x(g.x1) })).filter(({ s0, s1 }) => s1 >= 0 && s0 <= width).sort((a, b) => a.s0 - b.s0);
4921
+ for (const { g, s0, s1 } of placed) {
4922
+ const arrow = g.gene.strand > 0 ? " \u2192" : g.gene.strand < 0 ? "\u2190 " : "";
4923
+ const label = g.gene.strand < 0 ? `${arrow}${g.gene.name}` : `${g.gene.name}${arrow}`;
4924
+ const mid = (Math.max(s0, 0) + Math.min(s1, width)) / 2;
4925
+ const half = ctx.measureText(label).width / 2;
4926
+ const left = Math.min(s0, mid - half);
4927
+ const right = Math.max(s1, mid + half);
4928
+ let row = rowEnds.findIndex((end) => end + NAME_GAP_PX <= left);
4929
+ if (row === -1) {
4930
+ row = Math.min(rowEnds.length, rows - 1);
4931
+ }
4932
+ rowEnds[row] = Math.max(rowEnds[row] ?? -Infinity, right);
4933
+ const glyphY = bottom - row * GENE_ROW_PX - EXON_PX / 2;
4934
+ ctx.beginPath();
4935
+ ctx.moveTo(s0, glyphY + 0.5);
4936
+ ctx.lineTo(s1, glyphY + 0.5);
4937
+ ctx.stroke();
4938
+ for (const e of g.exons) {
4939
+ const e0 = x(e.x0);
4940
+ ctx.fillRect(e0, glyphY - EXON_PX / 2, Math.max(1, x(e.x1) - e0), EXON_PX);
4941
+ }
4942
+ ctx.fillText(label, mid, glyphY - EXON_PX / 2 - 2);
4943
+ }
4744
4944
  }
4745
4945
 
4746
- // src/overlayLabels.ts
4747
- var LABEL_PX = 11;
4748
- var LABEL_CHAR_PX = 6.2;
4749
- var LABEL_PAD = 4;
4750
- function labelWidth(text) {
4751
- return text.length * LABEL_CHAR_PX + LABEL_PAD * 2;
4946
+ // src/tubeMap/connectors.ts
4947
+ function referenceNodes(graph, layout) {
4948
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
4949
+ const out = [];
4950
+ layout.nodes.forEach((node) => {
4951
+ const graphNode = nodeById.get(node.name);
4952
+ if (node.order >= 0 && graphNode && isBackbone(graphNode)) {
4953
+ out.push({
4954
+ node: node.name,
4955
+ bp0: graphNode.stable.start,
4956
+ bp1: graphNode.stable.start + node.sequenceLength,
4957
+ x0: node.x,
4958
+ x1: node.x + node.pixelWidth
4959
+ });
4960
+ }
4961
+ });
4962
+ return out;
4752
4963
  }
4753
- function overlaps(a, b) {
4754
- return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
4964
+ function tubeMapConnectors(nodes, bpToScreen, tubeToScreen, width) {
4965
+ const out = [];
4966
+ for (const n of nodes) {
4967
+ const bottom0 = tubeToScreen(n.x0);
4968
+ const bottom1 = tubeToScreen(n.x1);
4969
+ if (bottom1 >= 0 && bottom0 <= width) {
4970
+ out.push({
4971
+ node: n.node,
4972
+ top0: bpToScreen(n.bp0),
4973
+ top1: bpToScreen(n.bp1),
4974
+ bottom0,
4975
+ bottom1
4976
+ });
4977
+ }
4978
+ }
4979
+ return out;
4755
4980
  }
4756
- function offFrame(box, frame) {
4757
- return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
4981
+ var HIT_SLOP_PX = 3;
4982
+ function connectorAt(connectors, zoneBottom, sx, sy) {
4983
+ if (zoneBottom <= 0 || sy < 0 || sy > zoneBottom) {
4984
+ return void 0;
4985
+ }
4986
+ const t = sy / zoneBottom;
4987
+ let hit;
4988
+ let best = HIT_SLOP_PX;
4989
+ for (const c of connectors) {
4990
+ const a = c.top0 + (c.bottom0 - c.top0) * t;
4991
+ const b = c.top1 + (c.bottom1 - c.top1) * t;
4992
+ const dist = Math.max(0, Math.min(a, b) - sx, sx - Math.max(a, b));
4993
+ if (dist < best) {
4994
+ best = dist;
4995
+ hit = c.node;
4996
+ }
4997
+ }
4998
+ return hit;
4758
4999
  }
4759
- function insideFrame(box, frame) {
4760
- return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
5000
+ function band(ctx, c, bottom) {
5001
+ ctx.moveTo(c.top0, 0);
5002
+ ctx.lineTo(c.top1, 0);
5003
+ ctx.lineTo(c.bottom1, bottom);
5004
+ ctx.lineTo(c.bottom0, bottom);
5005
+ ctx.closePath();
5006
+ }
5007
+ function sides(ctx, c, bottom) {
5008
+ ctx.moveTo(c.top0, 0);
5009
+ ctx.lineTo(c.bottom0, bottom);
5010
+ ctx.moveTo(c.top1, 0);
5011
+ ctx.lineTo(c.bottom1, bottom);
5012
+ }
5013
+ function drawTubeMapConnectors(ctx, connectors, bottom, {
5014
+ highlightNode,
5015
+ darkMode
5016
+ }) {
5017
+ if (bottom <= 0 || connectors.length === 0) {
5018
+ return;
5019
+ }
5020
+ const ink = darkMode ? "255,255,255" : "0,0,0";
5021
+ ctx.beginPath();
5022
+ for (const c of connectors) {
5023
+ band(ctx, c, bottom);
5024
+ }
5025
+ ctx.fillStyle = `rgba(${ink},0.07)`;
5026
+ ctx.fill();
5027
+ ctx.beginPath();
5028
+ for (const c of connectors) {
5029
+ sides(ctx, c, bottom);
5030
+ }
5031
+ ctx.strokeStyle = `rgba(${ink},0.35)`;
5032
+ ctx.lineWidth = 0.5;
5033
+ ctx.stroke();
5034
+ const lit = connectors.find((c) => c.node === highlightNode);
5035
+ if (lit) {
5036
+ ctx.beginPath();
5037
+ band(ctx, lit, bottom);
5038
+ ctx.fillStyle = "rgba(255,192,203,0.5)";
5039
+ ctx.fill();
5040
+ ctx.beginPath();
5041
+ sides(ctx, lit, bottom);
5042
+ ctx.strokeStyle = "#ff0000";
5043
+ ctx.lineWidth = 1;
5044
+ ctx.stroke();
5045
+ }
4761
5046
  }
4762
- function occupancy(frame, reserved = []) {
4763
- const placed = [...reserved];
4764
- return (box) => {
4765
- if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
4766
- return false;
5047
+
5048
+ // src/walkHighlight.ts
5049
+ function walkHighlight(graph, name) {
5050
+ const path = graph.paths?.find((p) => p.name === name);
5051
+ if (!path) {
5052
+ return void 0;
5053
+ }
5054
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
5055
+ const edgeAt = /* @__PURE__ */ new Map();
5056
+ graph.edges.forEach((e, i) => {
5057
+ edgeAt.set(`${e.from}>${e.to}`, i);
5058
+ });
5059
+ const nodeIds = new Set(path.nodeIds);
5060
+ const edgeIndexes = /* @__PURE__ */ new Set();
5061
+ for (let i = 1; i < path.nodeIds.length; i++) {
5062
+ const a = path.nodeIds[i - 1];
5063
+ const b = path.nodeIds[i];
5064
+ const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
5065
+ if (index !== void 0) {
5066
+ edgeIndexes.add(index);
4767
5067
  }
4768
- placed.push(box);
4769
- return true;
5068
+ }
5069
+ const bpOf = (ids) => ids.reduce((sum, id) => sum + (byId.get(id)?.length ?? 0), 0);
5070
+ const reference = graph.referencePath ? graph.paths.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
5071
+ return {
5072
+ name,
5073
+ nodeIds,
5074
+ edgeIndexes,
5075
+ steps: path.nodeIds.length,
5076
+ bp: bpOf(path.nodeIds),
5077
+ referenceBp: reference && reference !== path ? bpOf(reference.nodeIds) : void 0
4770
5078
  };
4771
5079
  }
4772
- function placeLabels(candidates, frame, take) {
4773
- const out = [];
4774
- const row = LABEL_PX + LABEL_PAD * 2;
4775
- for (const c of candidates) {
4776
- const w = labelWidth(c.text);
4777
- const direction = c.y > frame.height / 2 ? -1 : 1;
4778
- for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
4779
- const y = c.y + direction * tries * row;
4780
- const box = {
4781
- x0: c.x - w / 2,
4782
- x1: c.x + w / 2,
4783
- y0: y - LABEL_PX - LABEL_PAD,
4784
- y1: y + LABEL_PAD
4785
- };
4786
- if (offFrame(box, frame)) {
4787
- break;
5080
+
5081
+ // src/bubbles/bubbleLine.ts
5082
+ var SATURATED_PATH_COUNT = 2147483647;
5083
+
5084
+ // src/bubbles/bubblesFromGraph.ts
5085
+ function bubblesFromGraph(graph) {
5086
+ if (!graph.nodes.some(isBackbone)) {
5087
+ return [];
5088
+ }
5089
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
5090
+ const order = referenceOrder(graph);
5091
+ const indexOf = new Map(order.map((id, i) => [id, i]));
5092
+ const reach = new Int32Array(order.length + 1);
5093
+ const succ = /* @__PURE__ */ new Map();
5094
+ const linked = /* @__PURE__ */ new Set();
5095
+ for (const e of graph.edges) {
5096
+ const a = indexOf.get(e.from);
5097
+ const b = indexOf.get(e.to);
5098
+ if (a === void 0 || b === void 0 || a === b) {
5099
+ continue;
5100
+ }
5101
+ linked.add(`${e.from}>${e.to}`).add(`${e.to}>${e.from}`);
5102
+ const [lo, hi] = a < b ? [a, b] : [b, a];
5103
+ reach[lo + 1]++;
5104
+ reach[hi]--;
5105
+ const [from, to] = a < b ? [e.from, e.to] : [e.to, e.from];
5106
+ (succ.get(from) ?? succ.set(from, /* @__PURE__ */ new Set()).get(from)).add(to);
5107
+ }
5108
+ for (let i = 1; i < reach.length; i++) {
5109
+ reach[i] += reach[i - 1];
5110
+ }
5111
+ const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
5112
+ const ends = /* @__PURE__ */ new Set([backbone[0].id, backbone.at(-1).id]);
5113
+ const boundaries = backbone.map((node) => [node, indexOf.get(node.id)]).filter(([node, i]) => ends.has(node.id) || reach[i] === 0).sort(([a, ia], [b, ib]) => ia - ib || a.stable.start - b.stable.start);
5114
+ const walkIndex = graph.paths?.map((p) => {
5115
+ const at = /* @__PURE__ */ new Map();
5116
+ p.nodeIds.forEach((id, i) => {
5117
+ if (!at.has(id)) {
5118
+ at.set(id, i);
4788
5119
  }
4789
- if (insideFrame(box, frame) && take(box)) {
4790
- out.push({ ...c, y, w });
4791
- break;
5120
+ });
5121
+ return at;
5122
+ });
5123
+ const bubbles = [];
5124
+ for (let i = 0; i + 1 < boundaries.length; i++) {
5125
+ const [start, i0] = boundaries[i];
5126
+ const [end, i1] = boundaries[i + 1];
5127
+ const interior = order.slice(i0 + 1, i1);
5128
+ if (interior.length === 0) {
5129
+ continue;
5130
+ }
5131
+ const best = /* @__PURE__ */ new Map([[start.id, { min: 0, max: 0, n: 1 }]]);
5132
+ for (const id of [start.id, ...interior]) {
5133
+ const cur = best.get(id);
5134
+ if (!cur) {
5135
+ continue;
5136
+ }
5137
+ for (const t of succ.get(id) ?? []) {
5138
+ const ti = indexOf.get(t);
5139
+ if (ti > i1) {
5140
+ continue;
5141
+ }
5142
+ const add = t === end.id ? 0 : byId.get(t).length;
5143
+ const prev = best.get(t);
5144
+ best.set(t, {
5145
+ min: Math.min(prev?.min ?? Infinity, cur.min + add),
5146
+ max: Math.max(prev?.max ?? -Infinity, cur.max + add),
5147
+ n: Math.min(SATURATED_PATH_COUNT, cur.n + (prev?.n ?? 0))
5148
+ });
5149
+ }
5150
+ }
5151
+ const walked = walkIndex && walkRoutes(graph, byId, walkIndex, start.id, end.id);
5152
+ const crossed = walked?.n ? walked : void 0;
5153
+ const routes2 = crossed ?? best.get(end.id) ?? { min: 0, max: 0, n: 0 };
5154
+ const walksLeave = walked !== void 0 && walked.left > 0;
5155
+ const refStart = start.stable.start + start.length;
5156
+ const refEnd = end.stable.start;
5157
+ const chain = [
5158
+ start.id,
5159
+ ...interior.filter((id) => isBackbone(byId.get(id))),
5160
+ end.id
5161
+ ];
5162
+ const chainBroken = chain.some(
5163
+ (id, k) => k > 0 && !linked.has(`${chain[k - 1]}>${id}`)
5164
+ );
5165
+ const partial = chainBroken || walksLeave;
5166
+ const fallback = chainBroken && !crossed;
5167
+ bubbles.push({
5168
+ refName: start.stable.refName,
5169
+ start: refStart,
5170
+ end: refEnd,
5171
+ segmentCount: interior.length + 2,
5172
+ pathCount: routes2.n,
5173
+ // a real inversion test needs each walk's direction relative to the
5174
+ // reference; the first visit's strand is just whichever path anchored the
5175
+ // node
5176
+ inversion: false,
5177
+ shortestAlleleLength: fallback ? refEnd - refStart : routes2.min,
5178
+ longestAlleleLength: fallback ? refEnd - refStart : routes2.max,
5179
+ segments: [start, ...interior.map((id) => byId.get(id)), end].map((n) => n.name).join(","),
5180
+ shortestAllele: void 0,
5181
+ longestAllele: void 0,
5182
+ partial,
5183
+ routes: crossed?.routes
5184
+ });
5185
+ }
5186
+ return bubbles;
5187
+ }
5188
+ function walkRoutes(graph, byId, walkIndex, startId, endId) {
5189
+ const seen = /* @__PURE__ */ new Map();
5190
+ let min = Infinity;
5191
+ let max = -Infinity;
5192
+ let left = 0;
5193
+ graph.paths.forEach((p, k) => {
5194
+ const i0 = walkIndex[k].get(startId);
5195
+ const i1 = walkIndex[k].get(endId);
5196
+ if (i0 === void 0 || i1 === void 0) {
5197
+ if (i0 !== void 0 || i1 !== void 0) {
5198
+ left++;
4792
5199
  }
5200
+ return;
4793
5201
  }
4794
- }
4795
- return out;
5202
+ const steps = p.nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
5203
+ if (i1 < i0) {
5204
+ steps.reverse();
5205
+ }
5206
+ let bp = 0;
5207
+ for (const id of steps) {
5208
+ bp += byId.get(id)?.length ?? 0;
5209
+ }
5210
+ const key = steps.join(",");
5211
+ const route = seen.get(key) ?? { steps, bp, walks: [] };
5212
+ route.walks.push(p.name);
5213
+ seen.set(key, route);
5214
+ min = Math.min(min, bp);
5215
+ max = Math.max(max, bp);
5216
+ });
5217
+ return seen.size || left ? { min, max, n: seen.size, left, routes: [...seen.values()] } : void 0;
4796
5218
  }
4797
5219
 
4798
- // src/graphLabels.ts
4799
- var MIN_DELETION_LABEL_PX = 26;
4800
- var PX_PER_NODE_LABEL = 4e4;
4801
- var MIN_NODE_LABELS = 8;
4802
- var LEADER_GAP_PX = 8;
4803
- var LABEL_CHAR_PX2 = 5.7;
4804
- var LABEL_PAD_PX = 6;
4805
- var LABEL_HEIGHT_PX = 13;
4806
- var LABEL_GAP_PX = 3;
4807
- var MAX_LABEL_OVERHANG = 2;
4808
- function labelHalfWidth(text) {
4809
- return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
4810
- }
4811
- var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
4812
- var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
4813
- function boxAt(halfW, x, y) {
4814
- return {
4815
- x0: x - halfW,
4816
- x1: x + halfW,
4817
- y0: y - LABEL_HALF_HEIGHT,
4818
- y1: y + LABEL_HALF_HEIGHT
4819
- };
4820
- }
4821
- function rowLabelBox(text, screenY) {
4822
- return {
4823
- x0: 6,
4824
- x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
4825
- y0: screenY - 8 - LABEL_GAP_PX / 2,
4826
- y1: screenY + 8 + LABEL_GAP_PX / 2
4827
- };
4828
- }
4829
- function clamp(v, lo, hi) {
4830
- return hi < lo ? v : Math.min(Math.max(v, lo), hi);
5220
+ // src/bubbles/classifyBubble.ts
5221
+ var REPEAT_GROWTH = 10;
5222
+ function coveringRepeat(b, repeats) {
5223
+ const span = b.end - b.start;
5224
+ const growth = b.longestAlleleLength - b.shortestAlleleLength;
5225
+ return repeats.find(
5226
+ (r) => growth <= REPEAT_GROWTH * (r.end - r.start) && (span === 0 ? r.start <= b.start && b.start <= r.end : Math.min(b.end, r.end) - Math.max(b.start, r.start) >= span / 2)
5227
+ );
4831
5228
  }
5229
+ var SUPERBUBBLE_SEGMENTS = 40;
4832
5230
  function formatBp2(bp) {
4833
- if (bp >= 1e6) {
4834
- return `${+(bp / 1e6).toFixed(1)} Mb`;
5231
+ if (bp >= 1e4) {
5232
+ return `${(bp / 1e3).toFixed(0)} kb`;
4835
5233
  }
4836
- return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
4837
- }
4838
- function deletionText(bp) {
4839
- return `${formatBp2(bp)} del`;
4840
- }
4841
- function arcOutward(curves, apex, scaleX, scaleY) {
4842
- const first = curves[0];
4843
- const last = curves[curves.length - 1];
4844
- const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
4845
- const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
4846
- const len = Math.hypot(dx, dy);
4847
- return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
4848
- }
4849
- function boxSupport(box, dir) {
4850
- return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
5234
+ if (bp >= 1e3) {
5235
+ return `${(bp / 1e3).toFixed(1)} kb`;
5236
+ }
5237
+ return `${bp} bp`;
4851
5238
  }
4852
- function boxRayCrossing(box, dir) {
4853
- const halfW = (box.x1 - box.x0) / 2;
4854
- const halfH = (box.y1 - box.y0) / 2;
4855
- return Math.min(
4856
- dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
4857
- dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
4858
- );
5239
+ function formatBpRange(shortest, longest) {
5240
+ const low = formatBp2(shortest);
5241
+ const high = formatBp2(longest);
5242
+ const unit = high.slice(high.indexOf(" "));
5243
+ return low.endsWith(unit) ? `${low.slice(0, -unit.length)}\u2013${high}` : `${low}\u2013${high}`;
4859
5244
  }
4860
- function drawnLength(segments, scaleX, scaleY) {
4861
- let total = 0;
4862
- for (let i = 1; i < segments.length; i++) {
4863
- const a = segments[i - 1];
4864
- const b = segments[i];
4865
- total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
5245
+ function routes(count) {
5246
+ if (count >= SATURATED_PATH_COUNT) {
5247
+ return "\u22652.1B routes";
4866
5248
  }
4867
- return total;
4868
- }
4869
- function runBounds(nodeIds, nodePositions) {
4870
- let minX = Infinity;
4871
- let minY = Infinity;
4872
- let maxX = -Infinity;
4873
- let maxY = -Infinity;
4874
- for (const id of nodeIds) {
4875
- for (const p of nodePositions[id] ?? []) {
4876
- minX = Math.min(minX, p.x);
4877
- maxX = Math.max(maxX, p.x);
4878
- minY = Math.min(minY, p.y);
4879
- maxY = Math.max(maxY, p.y);
4880
- }
5249
+ if (count >= 1e5) {
5250
+ return `${(count / 1e3).toFixed(0)}k routes`;
4881
5251
  }
4882
- return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
5252
+ return `${count} routes`;
4883
5253
  }
4884
- var nodeOrderCache = /* @__PURE__ */ new WeakMap();
4885
- function labelOrder(nodePositions, nodeLengths) {
4886
- const cached2 = nodeOrderCache.get(nodePositions);
4887
- if (cached2?.nodeLengths === nodeLengths) {
4888
- return cached2.order;
4889
- }
4890
- const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
4891
- nodeOrderCache.set(nodePositions, { nodeLengths, order });
4892
- return order;
5254
+ function classifyBubble(b, repeats = []) {
5255
+ const c = classifyShape(b, repeats);
5256
+ return b.partial ? { ...c, label: `${c.label}, partial` } : c;
4893
5257
  }
4894
- var arcCache = /* @__PURE__ */ new WeakMap();
4895
- function arcPlacements(nodePositions, deletions, axis, version) {
4896
- const { scaleX, scaleY } = axis;
4897
- const cached2 = arcCache.get(nodePositions);
4898
- if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
4899
- return cached2.arcs;
5258
+ function classifyShape(b, repeats) {
5259
+ const refSpan = b.end - b.start;
5260
+ const {
5261
+ shortestAlleleLength: shortest,
5262
+ longestAlleleLength: longest,
5263
+ pathCount,
5264
+ segmentCount,
5265
+ inversion
5266
+ } = b;
5267
+ const range = formatBpRange(shortest, longest);
5268
+ if (segmentCount >= SUPERBUBBLE_SEGMENTS) {
5269
+ return {
5270
+ kind: "superbubble",
5271
+ label: `${range} superbubble, ${segmentCount} segments, ${routes(pathCount)}${inversion ? ", inv" : ""}`
5272
+ };
4900
5273
  }
4901
- const arcs = [];
4902
- for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
4903
- const curves = deletionArcCurves(nodePositions, deletion, axis);
4904
- const apex = curves ? curveMidpoint(curves) : void 0;
4905
- if (curves && apex) {
4906
- const { minX, minY, maxX, maxY } = curveBounds(curves);
4907
- arcs.push({
4908
- deletion,
4909
- curves,
4910
- apex,
4911
- // in screen px, so the two axes' units cannot be added by accident
4912
- extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
4913
- });
4914
- }
5274
+ if (inversion) {
5275
+ return { kind: "inversion", label: `${formatBp2(longest)} inv` };
4915
5276
  }
4916
- arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
4917
- return arcs;
4918
- }
4919
- function sizeLabelCandidates({
4920
- nodePositions,
4921
- nodeLengths,
4922
- deletions,
4923
- alleleDeletions,
4924
- axis,
4925
- translateX,
4926
- translateY,
4927
- width,
4928
- height,
4929
- version = 0
4930
- }) {
4931
- const { scaleX, scaleY } = axis;
4932
- const deletionCandidates = [];
4933
- const nodeCandidates = [];
4934
- for (const { deletion, curves, apex, extent } of arcPlacements(
4935
- nodePositions,
4936
- deletions,
4937
- axis,
4938
- version
4939
- )) {
4940
- if (extent >= MIN_DELETION_LABEL_PX) {
4941
- const text = deletionText(deletion.bp);
4942
- const halfW = labelHalfWidth(text);
4943
- const box = boxAt(halfW, 0, 0);
4944
- const arcX = apex.x * scaleX + translateX;
4945
- const arcY = apex.y * scaleY + translateY;
4946
- const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
4947
- const bow = arcOutward(curves, apex, scaleX, scaleY);
4948
- const offset = boxSupport(box, bow) + LEADER_GAP_PX;
4949
- const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
4950
- const y = fits ? arcY : clamp(
4951
- arcY + bow.y * offset,
4952
- LABEL_HALF_HEIGHT,
4953
- height - LABEL_HALF_HEIGHT
4954
- );
4955
- const away = Math.hypot(x - arcX, y - arcY);
4956
- let leader;
4957
- if (away > 0) {
4958
- const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
4959
- const crossing = boxRayCrossing(box, dir);
4960
- leader = {
4961
- arcX,
4962
- arcY,
4963
- labelX: x - dir.x * crossing,
4964
- labelY: y - dir.y * crossing
4965
- };
4966
- }
4967
- deletionCandidates.push({
4968
- label: {
4969
- key: `del:${deletion.edgeIndex}`,
4970
- text,
4971
- x,
4972
- y,
4973
- kind: "deletion",
4974
- leader
4975
- },
4976
- box: boxAt(halfW, x, y)
4977
- });
4978
- }
5277
+ const repeat = longest !== shortest ? coveringRepeat(b, repeats) : void 0;
5278
+ if (repeat) {
5279
+ const name = repeat.name ? ` (${repeat.name})` : "";
5280
+ return {
5281
+ kind: "repeat",
5282
+ label: `${range} repeat array${name}, ${routes(pathCount)}`
5283
+ };
5284
+ }
5285
+ const alleles = pathCount > 2 ? `, ${pathCount} alleles` : "";
5286
+ if (shortest === longest && shortest === refSpan) {
5287
+ return refSpan <= 1 ? { kind: "snp", label: "SNP" } : { kind: "substitution", label: `${formatBp2(refSpan)} sub` };
5288
+ }
5289
+ if (refSpan === 0 || shortest === refSpan) {
5290
+ return {
5291
+ kind: "insertion",
5292
+ label: `\u2264${formatBp2(longest - refSpan)} ins${alleles}`
5293
+ };
4979
5294
  }
4980
- const labelledByDeletion = /* @__PURE__ */ new Set();
4981
- const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
4982
- for (const run of alleleRuns) {
4983
- const drawn = runBounds(run.nodeIds, nodePositions);
4984
- if (!drawn) {
4985
- continue;
4986
- }
4987
- const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
4988
- const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
4989
- const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
4990
- const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
4991
- if (shownRight < shownLeft || shownBottom < shownTop) {
4992
- continue;
4993
- }
4994
- const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
4995
- const text = deletionText(run.bp);
4996
- const halfW = labelHalfWidth(text);
4997
- if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
4998
- for (const id of run.nodeIds) {
4999
- labelledByDeletion.add(id);
5000
- }
5001
- const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
5002
- const y = clamp(
5003
- (shownTop + shownBottom) / 2,
5004
- LABEL_HALF_HEIGHT,
5005
- height - LABEL_HALF_HEIGHT
5006
- );
5007
- deletionCandidates.push({
5008
- label: {
5009
- key: `alleledel:${run.nodeIds.join(",")}`,
5010
- text,
5011
- x,
5012
- y,
5013
- kind: "deletion"
5014
- },
5015
- box: boxAt(halfW, x, y)
5016
- });
5017
- }
5295
+ if (longest === refSpan) {
5296
+ return {
5297
+ kind: "deletion",
5298
+ label: `${formatBp2(refSpan - shortest)} del${alleles}`
5299
+ };
5018
5300
  }
5019
- for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
5020
- if (labelledByDeletion.has(id)) {
5021
- continue;
5301
+ return {
5302
+ kind: "complex",
5303
+ label: `${formatBp2(refSpan)} ref \u2192 ${range}, ${pathCount} alleles`
5304
+ };
5305
+ }
5306
+ var BUBBLE_KIND_COLORS = {
5307
+ snp: "#5b6b7a",
5308
+ substitution: "#5b6b7a",
5309
+ insertion: "#2f8fd6",
5310
+ deletion: "#c94040",
5311
+ inversion: "#e07b00",
5312
+ repeat: "#8e3fbf",
5313
+ superbubble: "#8e3fbf",
5314
+ complex: "#8e3fbf"
5315
+ };
5316
+ var BUBBLE_KIND_NAMES = {
5317
+ snp: "SNP",
5318
+ substitution: "substitution",
5319
+ insertion: "insertion",
5320
+ deletion: "deletion",
5321
+ inversion: "inversion",
5322
+ repeat: "repeat array",
5323
+ superbubble: "superbubble",
5324
+ complex: "site"
5325
+ };
5326
+ function bubbleSegmentIds(b) {
5327
+ return b.segments.split(",").filter(Boolean);
5328
+ }
5329
+
5330
+ // src/bubbles/bubbleHalos.ts
5331
+ var WHOLE_FRACTION = 0.9;
5332
+ var NAMED_WALKS = 2;
5333
+ var MIN_CHIPPED_BP = 1e3;
5334
+ function bubbleHalos(graph, bubbles, positions, walkLabel = (name) => name, repeats = []) {
5335
+ const byName = new Map(graph.nodes.map((n) => [n.name, n]));
5336
+ const halos = [];
5337
+ for (const bubble of bubbles) {
5338
+ const parts = [];
5339
+ let minX = Infinity;
5340
+ let maxX = -Infinity;
5341
+ let minY = Infinity;
5342
+ const nodeIds = [];
5343
+ const ends = [];
5344
+ for (const name of bubbleSegmentIds(bubble)) {
5345
+ const node = byName.get(name);
5346
+ const line = node && positions[node.id];
5347
+ if (!node || !line?.length) {
5348
+ continue;
5349
+ }
5350
+ if (isBackbone(node) && (node.stable.start < bubble.start || node.stable.start >= bubble.end)) {
5351
+ ends.push(line[Math.floor(line.length / 2)]);
5352
+ continue;
5353
+ }
5354
+ nodeIds.push(node.id);
5355
+ parts.push(svgPath(line.length === 1 ? [line[0], line[0]] : line));
5356
+ for (const p of line) {
5357
+ minX = Math.min(minX, p.x);
5358
+ maxX = Math.max(maxX, p.x);
5359
+ minY = Math.min(minY, p.y);
5360
+ }
5022
5361
  }
5023
- const { x, y } = polylineMidpoint(segments);
5024
- const screenY = y * scaleY + translateY;
5025
- if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
5362
+ if (nodeIds.length === 0) {
5026
5363
  continue;
5027
5364
  }
5028
- const drawn = drawnLength(segments, scaleX, scaleY);
5029
- if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
5030
- continue;
5365
+ const labelAt = { x: (minX + maxX) / 2, y: minY };
5366
+ const anchor = ends.length ? {
5367
+ x: ends.reduce((s, p) => s + p.x, 0) / ends.length,
5368
+ y: ends.reduce((s, p) => s + p.y, 0) / ends.length
5369
+ } : labelAt;
5370
+ const sharing = /* @__PURE__ */ new Map();
5371
+ for (const route of bubble.routes ?? []) {
5372
+ for (const id of new Set(route.steps)) {
5373
+ sharing.set(id, (sharing.get(id) ?? 0) + 1);
5374
+ }
5031
5375
  }
5032
- const text = formatBp2(nodeLengths.get(id));
5033
- const halfW = labelHalfWidth(text);
5034
- if (drawn >= halfW * 2 / MAX_LABEL_OVERHANG) {
5035
- const screenX = x * scaleX + translateX;
5036
- nodeCandidates.push({
5037
- label: {
5038
- key: `node:${id}`,
5039
- text,
5040
- x: screenX,
5041
- y: screenY,
5042
- kind: "node"
5043
- },
5044
- box: boxAt(halfW, screenX, screenY)
5045
- });
5376
+ const routes2 = [];
5377
+ for (const route of bubble.routes ?? []) {
5378
+ if (bubble.longestAlleleLength < MIN_CHIPPED_BP) {
5379
+ break;
5380
+ }
5381
+ if (route.steps.length === 0) {
5382
+ continue;
5383
+ }
5384
+ const rarest = Math.min(...route.steps.map((id) => sharing.get(id)));
5385
+ let at;
5386
+ let far = -1;
5387
+ for (const id of route.steps) {
5388
+ if (sharing.get(id) !== rarest) {
5389
+ continue;
5390
+ }
5391
+ for (const p of positions[id] ?? []) {
5392
+ const d = Math.hypot(p.x - anchor.x, p.y - anchor.y);
5393
+ if (d > far) {
5394
+ far = d;
5395
+ at = p;
5396
+ }
5397
+ }
5398
+ }
5399
+ if (at) {
5400
+ routes2.push({ route, at, text: routeText(route, walkLabel) });
5401
+ }
5046
5402
  }
5403
+ halos.push({
5404
+ bubble,
5405
+ ...classifyBubble(bubble, repeats),
5406
+ path: parts.join(""),
5407
+ labelAt,
5408
+ members: nodeIds.length,
5409
+ nodeIds,
5410
+ whole: nodeIds.length >= WHOLE_FRACTION * graph.nodes.length,
5411
+ routes: routes2
5412
+ });
5047
5413
  }
5048
- return { deletions: deletionCandidates, nodes: nodeCandidates };
5414
+ return halos;
5049
5415
  }
5050
- function nodeLabelBudget(width, height) {
5051
- return Math.max(
5052
- MIN_NODE_LABELS,
5053
- Math.floor(width * height / PX_PER_NODE_LABEL)
5054
- );
5416
+ function routeText(route, walkLabel) {
5417
+ const names = [...new Set(route.walks.map(walkLabel))].sort();
5418
+ const shown = names.slice(0, NAMED_WALKS).join(", ");
5419
+ const more = names.length > NAMED_WALKS ? ` +${names.length - NAMED_WALKS}` : "";
5420
+ return `${shown}${more} \xB7 ${formatBp2(route.bp)}`;
5055
5421
  }
5056
- function placeSizeLabels(candidates, take, budget = Infinity) {
5057
- const labels = [];
5058
- for (const { label, box } of candidates) {
5059
- if (labels.length >= budget) {
5060
- break;
5061
- }
5062
- if (take(box)) {
5063
- labels.push(label);
5064
- }
5065
- }
5066
- return labels;
5422
+
5423
+ // src/bubbles/popBubble.ts
5424
+ function bubbleSubgraph(graph, segmentIds) {
5425
+ const keep = new Set(segmentIds);
5426
+ const nodes = graph.nodes.filter((n) => keep.has(n.name));
5427
+ const ids = new Set(nodes.map((n) => n.id));
5428
+ const paths = graph.paths?.map((p) => ({ ...p, nodeIds: p.nodeIds.filter((id) => ids.has(id)) })).filter((p) => p.nodeIds.length > 0);
5429
+ return {
5430
+ name: graph.name,
5431
+ nodes,
5432
+ edges: graph.edges.filter((e) => ids.has(e.from) && ids.has(e.to)),
5433
+ ...paths?.length ? { paths } : {},
5434
+ anchorPaths: graph.anchorPaths,
5435
+ pathVisits: graph.pathVisits,
5436
+ anchoredBy: graph.anchoredBy,
5437
+ referencePath: graph.referencePath
5438
+ };
5067
5439
  }
5068
5440
 
5069
5441
  // src/labelLayout.ts
@@ -5211,27 +5583,24 @@ function layoutLabels(m) {
5211
5583
  }
5212
5584
 
5213
5585
  // src/genes/genePins.ts
5214
- function contig(name) {
5215
- return name.split("#").at(-1);
5216
- }
5217
5586
  function genePins(graph, genes, positions) {
5218
- const byContig = /* @__PURE__ */ new Map();
5219
- const contigOf = /* @__PURE__ */ new Map();
5587
+ const byRefName = /* @__PURE__ */ new Map();
5220
5588
  for (const node of graph.nodes) {
5221
- if (isBackbone(node) && positions[node.id]?.length) {
5589
+ if (isBackbone(node)) {
5222
5590
  const { refName } = node.stable;
5223
- const name = contigOf.get(refName) ?? contigOf.set(refName, contig(refName)).get(refName);
5224
- const entry = byContig.get(name) ?? byContig.set(name, { nodes: [], reach: 0 }).get(name);
5225
- entry.nodes.push(node);
5226
- entry.reach = Math.max(entry.reach, node.length);
5591
+ if (positions[node.id]?.length) {
5592
+ const entry = byRefName.get(refName) ?? byRefName.set(refName, { nodes: [], reach: 0 }).get(refName);
5593
+ entry.nodes.push(node);
5594
+ entry.reach = Math.max(entry.reach, node.length);
5595
+ }
5227
5596
  }
5228
5597
  }
5229
- for (const { nodes } of byContig.values()) {
5598
+ for (const { nodes } of byRefName.values()) {
5230
5599
  nodes.sort((a, b) => a.stable.start - b.stable.start);
5231
5600
  }
5232
5601
  const pins = [];
5233
5602
  for (const gene of genes) {
5234
- const backbone = byContig.get(contig(gene.refName));
5603
+ const backbone = byRefName.get(gene.refName);
5235
5604
  if (!backbone) {
5236
5605
  continue;
5237
5606
  }
@@ -5290,6 +5659,135 @@ function genePins(graph, genes, positions) {
5290
5659
  return pins;
5291
5660
  }
5292
5661
 
5662
+ // src/reference.ts
5663
+ var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
5664
+ ["hg38", "GRCh38"],
5665
+ ["hg19", "GRCh37"],
5666
+ ["hs1", "CHM13"],
5667
+ ["t2t-chm13", "CHM13"],
5668
+ ["chm13v2", "CHM13"]
5669
+ ]);
5670
+ function wellKnownSample(assemblyName) {
5671
+ return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
5672
+ }
5673
+ var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
5674
+ function sampleOf2(refName) {
5675
+ const sample = refName.includes("#") ? panSNSample(refName) : void 0;
5676
+ return sample === void 0 || GENERIC_SAMPLES.has(sample) ? void 0 : sample;
5677
+ }
5678
+ function haplotypeOf(refName) {
5679
+ return panSNHaplotype(refName) ?? panSNSample(refName);
5680
+ }
5681
+ function graphHaplotypes(graph) {
5682
+ const refNames = new Set((graph.anchorPaths ?? []).map((p) => p.name));
5683
+ for (const node of graph.nodes) {
5684
+ if (node.stable) {
5685
+ refNames.add(node.stable.refName);
5686
+ }
5687
+ }
5688
+ const bySample = /* @__PURE__ */ new Map();
5689
+ for (const refName of refNames) {
5690
+ const sample = sampleOf2(refName);
5691
+ if (sample !== void 0) {
5692
+ const haplotypes = bySample.get(sample) ?? bySample.set(sample, /* @__PURE__ */ new Set()).get(sample);
5693
+ haplotypes.add(haplotypeOf(refName));
5694
+ }
5695
+ }
5696
+ return bySample;
5697
+ }
5698
+ function backboneFrom(contigs, haplotypes) {
5699
+ const samples = new Set(contigs.map((c) => sampleOf2(c.refName)));
5700
+ const [sample] = samples;
5701
+ if (samples.size !== 1 || sample === void 0) {
5702
+ return { contigs, prefixes: [], named: false };
5703
+ }
5704
+ const own = new Set(contigs.map((c) => haplotypeOf(c.refName)));
5705
+ const unique = [...haplotypes.get(sample) ?? []].every((h) => own.has(h));
5706
+ const shared = new Set(contigs.map((c) => panSNHaplotype(c.refName)));
5707
+ const [haplotype] = shared;
5708
+ return {
5709
+ contigs,
5710
+ prefixes: [
5711
+ ...unique ? [sample] : [],
5712
+ ...shared.size === 1 && haplotype ? [haplotype] : []
5713
+ ],
5714
+ named: true
5715
+ };
5716
+ }
5717
+ function graphBackbone(graph) {
5718
+ const spans = /* @__PURE__ */ new Map();
5719
+ for (const node of graph.nodes) {
5720
+ if (isBackbone(node)) {
5721
+ const { refName, start } = node.stable;
5722
+ const end = start + node.length;
5723
+ const span = spans.get(refName);
5724
+ if (span) {
5725
+ span.start = Math.min(span.start, start);
5726
+ span.end = Math.max(span.end, end);
5727
+ } else {
5728
+ spans.set(refName, {
5729
+ refName,
5730
+ contig: panSNContig(refName),
5731
+ start,
5732
+ end
5733
+ });
5734
+ }
5735
+ }
5736
+ }
5737
+ const contigs = [...spans.values()];
5738
+ return contigs.length ? backboneFrom(contigs, graphHaplotypes(graph)) : void 0;
5739
+ }
5740
+ function backboneAssembly(backbone, assemblies) {
5741
+ const names = (a) => [a.name, ...a.aliases ?? []];
5742
+ const naming = (prefix) => (a) => names(a).some((n) => n.toLowerCase() === prefix.toLowerCase());
5743
+ const prefixes = [...backbone?.prefixes ?? []].reverse();
5744
+ const sample = backbone?.prefixes.find((p) => !p.includes("#"))?.toLowerCase();
5745
+ for (const prefix of prefixes) {
5746
+ const assembly = assemblies.find(naming(prefix));
5747
+ if (assembly) {
5748
+ return assembly;
5749
+ }
5750
+ }
5751
+ return sample === void 0 ? void 0 : assemblies.find(
5752
+ (a) => names(a).some((n) => wellKnownSample(n)?.toLowerCase() === sample)
5753
+ );
5754
+ }
5755
+ function assemblyWalk(graph, assembly) {
5756
+ const haplotypes = graphHaplotypes(graph);
5757
+ return graph.anchorPaths?.find(
5758
+ (walk) => backboneAssembly(
5759
+ backboneFrom(
5760
+ [
5761
+ {
5762
+ refName: walk.name,
5763
+ contig: panSNContig(walk.name),
5764
+ start: walk.start,
5765
+ end: walk.start + walk.length
5766
+ }
5767
+ ],
5768
+ haplotypes
5769
+ ),
5770
+ [assembly]
5771
+ )
5772
+ );
5773
+ }
5774
+ function refNameBinding(refNames) {
5775
+ const exact = new Set(refNames);
5776
+ const byContig = /* @__PURE__ */ new Map();
5777
+ for (const refName of exact) {
5778
+ const contig = panSNContig(refName);
5779
+ byContig.set(contig, byContig.has(contig) ? void 0 : refName);
5780
+ }
5781
+ return (name) => exact.has(name) ? name : byContig.get(name);
5782
+ }
5783
+ function featuresOnBackbone(features, backbone) {
5784
+ const bind = refNameBinding(backbone.contigs.map((c) => c.refName));
5785
+ return features.flatMap((f) => {
5786
+ const refName = bind(f.refName);
5787
+ return refName === void 0 ? [] : [refName === f.refName ? f : { ...f, refName }];
5788
+ });
5789
+ }
5790
+
5293
5791
  // src/gbzWindow.ts
5294
5792
  var NoReferenceSampleError = class extends Error {
5295
5793
  constructor(anchor, referenceSamples) {
@@ -5317,16 +5815,9 @@ function haplotypeWanted(name, wanted) {
5317
5815
  async function referenceSamplesOf(db) {
5318
5816
  return (await db.tag("gbwt_reference_samples") ?? "").split(/\s+/).filter((sample) => sample !== "");
5319
5817
  }
5320
- var SAMPLE_ALIASES = {
5321
- hg38: "grch38",
5322
- hg19: "grch37",
5323
- hs1: "chm13",
5324
- "t2t-chm13": "chm13",
5325
- chm13v2: "chm13"
5326
- };
5327
5818
  function aliasedSample(anchorSample, referenceSamples) {
5328
5819
  const lower = anchorSample.toLowerCase();
5329
- const alias = SAMPLE_ALIASES[lower];
5820
+ const alias = wellKnownSample(anchorSample)?.toLowerCase();
5330
5821
  return referenceSamples.find((s) => s === anchorSample) ?? referenceSamples.find((s) => s.toLowerCase() === lower) ?? referenceSamples.find((s) => s.toLowerCase() === alias);
5331
5822
  }
5332
5823
  function resolveReferenceSample({
@@ -5441,7 +5932,10 @@ export {
5441
5932
  NODE_WIDTHS,
5442
5933
  REFERENCE_RAMP_MAX_HUE,
5443
5934
  ROW_HEIGHT_PX,
5935
+ WELL_KNOWN_SAMPLES,
5936
+ assemblyWalk,
5444
5937
  axisScaleOf,
5938
+ backboneAssembly,
5445
5939
  bubbleHalos,
5446
5940
  bubbleSegmentIds,
5447
5941
  bubbleSubgraph,
@@ -5450,20 +5944,26 @@ export {
5450
5944
  clampZoom,
5451
5945
  classifyBubble,
5452
5946
  computeReferenceRamp,
5947
+ connectorAt,
5453
5948
  contains,
5454
5949
  cutWindowGFA,
5455
5950
  deletionEdges,
5456
5951
  drawTubeMap,
5952
+ drawTubeMapConnectors,
5953
+ drawTubeMapGenes,
5954
+ drawTubeMapRuler,
5457
5955
  drawingBounds,
5458
5956
  engineKey,
5459
5957
  engineRequest,
5958
+ featuresOnBackbone,
5460
5959
  findHoveredEdge,
5461
5960
  findHoveredNode,
5462
5961
  fitTransform,
5463
5962
  forceLayout,
5464
- formatBp2 as formatBp,
5963
+ formatBp,
5465
5964
  genePins,
5466
5965
  getDpr,
5966
+ graphBackbone,
5467
5967
  haplotypeWanted,
5468
5968
  layoutExtent,
5469
5969
  layoutLabels,
@@ -5481,20 +5981,28 @@ export {
5481
5981
  panSNSample,
5482
5982
  pathColorsLegible,
5483
5983
  pathLegend,
5984
+ refNameBinding,
5985
+ referenceBoxes,
5484
5986
  referenceKnots,
5987
+ referenceNodes,
5485
5988
  referencePathQuery,
5486
5989
  referenceSamplesOf,
5487
5990
  resolveColorScheme,
5488
5991
  resolveReferenceSample,
5992
+ rulerBoxes,
5489
5993
  screenToLayout,
5994
+ tubeMapConnectors,
5490
5995
  tubeMapFrame,
5996
+ tubeMapGenes,
5491
5997
  tubeMapNodeAt,
5492
5998
  tubeMapPicture,
5999
+ tubeX,
5493
6000
  viewportOf,
5494
6001
  walkHighlight,
5495
6002
  walkRows,
5496
6003
  walkRowsExtent,
5497
6004
  warpX,
6005
+ wellKnownSample,
5498
6006
  wheelZoomFactor,
5499
6007
  zoomAbout
5500
6008
  };