@jbrowse/bandage-core 0.1.4 → 0.1.6

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
@@ -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;
@@ -660,6 +660,14 @@ function splitRuns(positions, runs, weightOf) {
660
660
  }
661
661
  return out;
662
662
  }
663
+ function polylineSlice(line, from, to) {
664
+ if (line.length === 0) {
665
+ return [];
666
+ }
667
+ const arc = arcPrefix(line);
668
+ const total = arc.at(-1);
669
+ return slice(line, arc, from * total, to * total);
670
+ }
663
671
  function polylineMidpoint(line) {
664
672
  const arc = arcPrefix(line);
665
673
  return pointAt(line, arc, arc.at(-1) / 2);
@@ -967,18 +975,21 @@ function fittedTranslateY(bounds, usableHeight, yScale) {
967
975
  const leftoverY = usableHeight - bounds.h * yScale;
968
976
  return FIT_PADDING - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
969
977
  }
970
- function fitTransform(bounds, paneWidth, paneHeight, pixelRows) {
971
- const usableWidth = paneWidth - FIT_PADDING * 2;
978
+ function fitTransform(bounds, paneWidth, paneHeight, pixelRows, { minScale = 0, padLeft = FIT_PADDING } = {}) {
979
+ const usableWidth = paneWidth - padLeft - FIT_PADDING;
972
980
  const usableHeight = paneHeight - FIT_PADDING * 2;
973
981
  if (usableWidth <= 0 || usableHeight <= 0 || !(bounds.w > 0 || !pixelRows && bounds.h > 0)) {
974
982
  return void 0;
975
983
  }
976
984
  const fitX = bounds.w > 0 ? usableWidth / bounds.w : Infinity;
977
985
  const fitY = bounds.h > 0 ? usableHeight / bounds.h : Infinity;
978
- 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;
979
990
  return {
980
991
  scale,
981
- translateX: FIT_PADDING - bounds.minX * scale + (usableWidth - bounds.w * scale) / 2,
992
+ translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
982
993
  translateY: fittedTranslateY(bounds, usableHeight, pixelRows ? 1 : scale)
983
994
  };
984
995
  }
@@ -2002,7 +2013,6 @@ function walkRows(graph, region, unit) {
2002
2013
  return void 0;
2003
2014
  }
2004
2015
  const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
2005
- const onReference = new Set(reference.nodeIds);
2006
2016
  const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
2007
2017
  const cut = region && region.end > region.start ? region : void 0;
2008
2018
  const span = /* @__PURE__ */ new Map();
@@ -2020,17 +2030,34 @@ function walkRows(graph, region, unit) {
2020
2030
  const runs = [];
2021
2031
  let bp = 0;
2022
2032
  let offReferenceBp = 0;
2033
+ let step = 0;
2023
2034
  for (const id of ids) {
2024
2035
  const len = lengthOf.get(id) ?? 0;
2025
- const shared = onReference.has(id);
2036
+ const s = span.get(id);
2026
2037
  const last = runs.at(-1);
2027
- if (last?.onReference === shared) {
2038
+ const at = last?.referenceStart;
2039
+ const forward2 = s && at !== void 0 && step >= 0 && at + last.bp === s.start;
2040
+ const backward = s && at !== void 0 && step <= 0 && s.end === at;
2041
+ if (!s && last && !last.onReference) {
2042
+ last.bp += len;
2043
+ } else if (forward2 || backward) {
2028
2044
  last.bp += len;
2045
+ if (!forward2) {
2046
+ last.referenceStart = s.start;
2047
+ last.reversed = true;
2048
+ }
2049
+ step = forward2 ? 1 : -1;
2029
2050
  } else {
2030
- runs.push({ start: bp, bp: len, onReference: shared });
2051
+ runs.push({
2052
+ start: bp,
2053
+ bp: len,
2054
+ onReference: s !== void 0,
2055
+ referenceStart: s?.start
2056
+ });
2057
+ step = 0;
2031
2058
  }
2032
2059
  bp += len;
2033
- if (!shared) {
2060
+ if (!s) {
2034
2061
  offReferenceBp += len;
2035
2062
  }
2036
2063
  }
@@ -2566,9 +2593,9 @@ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY,
2566
2593
  offsetY
2567
2594
  );
2568
2595
  }
2569
- const sides = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
2570
- const fromAttach = attachment(fromSegments, sides.from);
2571
- const toAttach = attachment(toSegments, sides.to);
2596
+ const sides2 = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
2597
+ const fromAttach = attachment(fromSegments, sides2.from);
2598
+ const toAttach = attachment(toSegments, sides2.to);
2572
2599
  const fromEnd = fromAttach.at;
2573
2600
  const toStart = toAttach.at;
2574
2601
  const p1x = fromEnd.x + offsetX;
@@ -3058,7 +3085,9 @@ function hashColor(str, alpha) {
3058
3085
  var REFERENCE_RAMP_MAX_HUE = 300;
3059
3086
  var REFERENCE_RAMP_SATURATION = 0.7;
3060
3087
  var REFERENCE_RAMP_LIGHTNESS = 0.5;
3061
- var REFERENCE_RAMP_ALT_COLOR = packAbgr2(60, 65, 72, 255);
3088
+ var REFERENCE_RAMP_ALT_RGB = [60, 65, 72];
3089
+ var REFERENCE_RAMP_ALT_COLOR = packAbgr2(...REFERENCE_RAMP_ALT_RGB, 255);
3090
+ var REFERENCE_RAMP_ALT_CSS = `rgb(${REFERENCE_RAMP_ALT_RGB.join(", ")})`;
3062
3091
  function computeReferenceRamp(graph, domain) {
3063
3092
  const midpoints = referenceMidpoints(graph);
3064
3093
  if (domain && domain.end > domain.start) {
@@ -4115,6 +4144,7 @@ function tubeMapPicture(layout) {
4115
4144
  }
4116
4145
  });
4117
4146
  return {
4147
+ bounds: layout.bounds,
4118
4148
  layers: [layerOf(layout, "haplotype"), layerOf(layout, "read")],
4119
4149
  nodes,
4120
4150
  mismatches: tubeMapMismatches(layout).filter(
@@ -4185,6 +4215,11 @@ function fillByColor(ctx, items, visible, addTo) {
4185
4215
  }
4186
4216
  ctx.globalAlpha = 1;
4187
4217
  }
4218
+ var FULL_OUTLINE_PX = 12;
4219
+ var MIN_OUTLINE_ALPHA = 0.15;
4220
+ function outlineAlpha(screenWidth) {
4221
+ return Math.max(MIN_OUTLINE_ALPHA, Math.min(1, screenWidth / FULL_OUTLINE_PX));
4222
+ }
4188
4223
  function drawTubeMap(ctx, picture, frame) {
4189
4224
  const { x, y, width } = frame;
4190
4225
  const visible = (item) => x(item.x1) >= 0 && x(item.x0) <= width;
@@ -4209,7 +4244,9 @@ function drawTubeMap(ctx, picture, frame) {
4209
4244
  ctx.fillStyle = lit ? "rgba(255,192,203,0.5)" : fill;
4210
4245
  ctx.strokeStyle = lit ? "#ff0000" : stroke;
4211
4246
  ctx.fill();
4247
+ ctx.globalAlpha = lit ? 1 : outlineAlpha(x(node.x1) - x(node.x0));
4212
4248
  ctx.stroke();
4249
+ ctx.globalAlpha = 1;
4213
4250
  }
4214
4251
  }
4215
4252
  drawMismatches(ctx, picture.mismatches, frame);
@@ -4342,720 +4379,1122 @@ function tubeMapNodeAt(drawing, { x, y }, sx, sy) {
4342
4379
  return hit;
4343
4380
  }
4344
4381
 
4345
- // src/walkHighlight.ts
4346
- function walkHighlight(graph, name) {
4347
- const path = graph.paths?.find((p) => p.name === name);
4348
- if (!path) {
4349
- return void 0;
4350
- }
4351
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
4352
- const edgeAt = /* @__PURE__ */ new Map();
4353
- graph.edges.forEach((e, i) => {
4354
- edgeAt.set(`${e.from}>${e.to}`, i);
4355
- });
4356
- const nodeIds = new Set(path.nodeIds);
4357
- const edgeIndexes = /* @__PURE__ */ new Set();
4358
- for (let i = 1; i < path.nodeIds.length; i++) {
4359
- const a = path.nodeIds[i - 1];
4360
- const b = path.nodeIds[i];
4361
- const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
4362
- if (index !== void 0) {
4363
- edgeIndexes.add(index);
4382
+ // src/overlayLabels.ts
4383
+ var LABEL_PX = 11;
4384
+ var LABEL_CHAR_PX = 6.2;
4385
+ var LABEL_PAD = 4;
4386
+ function labelWidth(text) {
4387
+ return text.length * LABEL_CHAR_PX + LABEL_PAD * 2;
4388
+ }
4389
+ function overlaps(a, b) {
4390
+ return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
4391
+ }
4392
+ function offFrame(box, frame) {
4393
+ return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
4394
+ }
4395
+ function insideFrame(box, frame) {
4396
+ return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
4397
+ }
4398
+ function occupancy(frame, reserved = []) {
4399
+ const placed = [...reserved];
4400
+ return (box) => {
4401
+ if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
4402
+ return false;
4364
4403
  }
4365
- }
4366
- const bpOf = (ids) => ids.reduce((sum, id) => sum + (byId.get(id)?.length ?? 0), 0);
4367
- const reference = graph.referencePath ? graph.paths.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
4368
- return {
4369
- name,
4370
- nodeIds,
4371
- edgeIndexes,
4372
- steps: path.nodeIds.length,
4373
- bp: bpOf(path.nodeIds),
4374
- referenceBp: reference && reference !== path ? bpOf(reference.nodeIds) : void 0
4404
+ placed.push(box);
4405
+ return true;
4375
4406
  };
4376
4407
  }
4377
-
4378
- // src/bubbles/bubbleLine.ts
4379
- var SATURATED_PATH_COUNT = 2147483647;
4380
-
4381
- // src/bubbles/bubblesFromGraph.ts
4382
- function bubblesFromGraph(graph) {
4383
- if (!graph.nodes.some(isBackbone)) {
4384
- return [];
4385
- }
4386
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
4387
- const order = referenceOrder(graph);
4388
- const indexOf = new Map(order.map((id, i) => [id, i]));
4389
- const reach = new Int32Array(order.length + 1);
4390
- const succ = /* @__PURE__ */ new Map();
4391
- const linked = /* @__PURE__ */ new Set();
4392
- for (const e of graph.edges) {
4393
- const a = indexOf.get(e.from);
4394
- const b = indexOf.get(e.to);
4395
- if (a === void 0 || b === void 0 || a === b) {
4396
- continue;
4397
- }
4398
- linked.add(`${e.from}>${e.to}`).add(`${e.to}>${e.from}`);
4399
- const [lo, hi] = a < b ? [a, b] : [b, a];
4400
- reach[lo + 1]++;
4401
- reach[hi]--;
4402
- const [from, to] = a < b ? [e.from, e.to] : [e.to, e.from];
4403
- (succ.get(from) ?? succ.set(from, /* @__PURE__ */ new Set()).get(from)).add(to);
4404
- }
4405
- for (let i = 1; i < reach.length; i++) {
4406
- reach[i] += reach[i - 1];
4407
- }
4408
- const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
4409
- const ends = /* @__PURE__ */ new Set([backbone[0].id, backbone.at(-1).id]);
4410
- 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);
4411
- const walkIndex = graph.paths?.map((p) => {
4412
- const at = /* @__PURE__ */ new Map();
4413
- p.nodeIds.forEach((id, i) => {
4414
- if (!at.has(id)) {
4415
- at.set(id, i);
4416
- }
4417
- });
4418
- return at;
4419
- });
4420
- const bubbles = [];
4421
- for (let i = 0; i + 1 < boundaries.length; i++) {
4422
- const [start, i0] = boundaries[i];
4423
- const [end, i1] = boundaries[i + 1];
4424
- const interior = order.slice(i0 + 1, i1);
4425
- if (interior.length === 0) {
4426
- continue;
4427
- }
4428
- const best = /* @__PURE__ */ new Map([[start.id, { min: 0, max: 0, n: 1 }]]);
4429
- for (const id of [start.id, ...interior]) {
4430
- const cur = best.get(id);
4431
- if (!cur) {
4432
- continue;
4408
+ function placeLabels(candidates, frame, take) {
4409
+ const out = [];
4410
+ const row = LABEL_PX + LABEL_PAD * 2;
4411
+ for (const c of candidates) {
4412
+ const w = labelWidth(c.text);
4413
+ const direction = c.y > frame.height / 2 ? -1 : 1;
4414
+ for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
4415
+ const y = c.y + direction * tries * row;
4416
+ const box = {
4417
+ x0: c.x - w / 2,
4418
+ x1: c.x + w / 2,
4419
+ y0: y - LABEL_PX - LABEL_PAD,
4420
+ y1: y + LABEL_PAD
4421
+ };
4422
+ if (offFrame(box, frame)) {
4423
+ break;
4433
4424
  }
4434
- for (const t of succ.get(id) ?? []) {
4435
- const ti = indexOf.get(t);
4436
- if (ti > i1) {
4437
- continue;
4438
- }
4439
- const add = t === end.id ? 0 : byId.get(t).length;
4440
- const prev = best.get(t);
4441
- best.set(t, {
4442
- min: Math.min(prev?.min ?? Infinity, cur.min + add),
4443
- max: Math.max(prev?.max ?? -Infinity, cur.max + add),
4444
- n: Math.min(SATURATED_PATH_COUNT, cur.n + (prev?.n ?? 0))
4445
- });
4425
+ if (insideFrame(box, frame) && take(box)) {
4426
+ out.push({ ...c, y, w });
4427
+ break;
4446
4428
  }
4447
4429
  }
4448
- const walked = walkIndex && walkRoutes(graph, byId, walkIndex, start.id, end.id);
4449
- const crossed = walked?.n ? walked : void 0;
4450
- const routes2 = crossed ?? best.get(end.id) ?? { min: 0, max: 0, n: 0 };
4451
- const walksLeave = walked !== void 0 && walked.left > 0;
4452
- const refStart = start.stable.start + start.length;
4453
- const refEnd = end.stable.start;
4454
- const chain = [
4455
- start.id,
4456
- ...interior.filter((id) => isBackbone(byId.get(id))),
4457
- end.id
4458
- ];
4459
- const chainBroken = chain.some(
4460
- (id, k) => k > 0 && !linked.has(`${chain[k - 1]}>${id}`)
4461
- );
4462
- const partial = chainBroken || walksLeave;
4463
- const fallback = chainBroken && !crossed;
4464
- bubbles.push({
4465
- refName: start.stable.refName,
4466
- start: refStart,
4467
- end: refEnd,
4468
- segmentCount: interior.length + 2,
4469
- pathCount: routes2.n,
4470
- // a real inversion test needs each walk's direction relative to the
4471
- // reference; the first visit's strand is just whichever path anchored the
4472
- // node
4473
- inversion: false,
4474
- shortestAlleleLength: fallback ? refEnd - refStart : routes2.min,
4475
- longestAlleleLength: fallback ? refEnd - refStart : routes2.max,
4476
- segments: [start, ...interior.map((id) => byId.get(id)), end].map((n) => n.name).join(","),
4477
- shortestAllele: void 0,
4478
- longestAllele: void 0,
4479
- partial,
4480
- routes: crossed?.routes
4481
- });
4482
4430
  }
4483
- return bubbles;
4484
- }
4485
- function walkRoutes(graph, byId, walkIndex, startId, endId) {
4486
- const seen = /* @__PURE__ */ new Map();
4487
- let min = Infinity;
4488
- let max = -Infinity;
4489
- let left = 0;
4490
- graph.paths.forEach((p, k) => {
4491
- const i0 = walkIndex[k].get(startId);
4492
- const i1 = walkIndex[k].get(endId);
4493
- if (i0 === void 0 || i1 === void 0) {
4494
- if (i0 !== void 0 || i1 !== void 0) {
4495
- left++;
4496
- }
4497
- return;
4498
- }
4499
- const steps = p.nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
4500
- if (i1 < i0) {
4501
- steps.reverse();
4502
- }
4503
- let bp = 0;
4504
- for (const id of steps) {
4505
- bp += byId.get(id)?.length ?? 0;
4506
- }
4507
- const key = steps.join(",");
4508
- const route = seen.get(key) ?? { steps, bp, walks: [] };
4509
- route.walks.push(p.name);
4510
- seen.set(key, route);
4511
- min = Math.min(min, bp);
4512
- max = Math.max(max, bp);
4513
- });
4514
- return seen.size || left ? { min, max, n: seen.size, left, routes: [...seen.values()] } : void 0;
4431
+ return out;
4515
4432
  }
4516
4433
 
4517
- // src/bubbles/classifyBubble.ts
4518
- var REPEAT_GROWTH = 10;
4519
- function coveringRepeat(b, repeats) {
4520
- const span = b.end - b.start;
4521
- const growth = b.longestAlleleLength - b.shortestAlleleLength;
4522
- return repeats.find(
4523
- (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)
4524
- );
4434
+ // src/graphLabels.ts
4435
+ var MIN_DELETION_LABEL_PX = 26;
4436
+ var PX_PER_NODE_LABEL = 4e4;
4437
+ var MIN_NODE_LABELS = 8;
4438
+ var LEADER_GAP_PX = 8;
4439
+ var LABEL_CHAR_PX2 = 5.7;
4440
+ var LABEL_PAD_PX = 6;
4441
+ var LABEL_HEIGHT_PX = 13;
4442
+ var LABEL_GAP_PX = 3;
4443
+ var MAX_LABEL_OVERHANG = 2;
4444
+ function labelHalfWidth(text) {
4445
+ return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
4525
4446
  }
4526
- var SUPERBUBBLE_SEGMENTS = 40;
4527
- function formatBp(bp) {
4528
- if (bp >= 1e4) {
4529
- return `${(bp / 1e3).toFixed(0)} kb`;
4530
- }
4531
- if (bp >= 1e3) {
4532
- return `${(bp / 1e3).toFixed(1)} kb`;
4533
- }
4534
- return `${bp} bp`;
4447
+ var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
4448
+ var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
4449
+ function boxAt(halfW, x, y) {
4450
+ return {
4451
+ x0: x - halfW,
4452
+ x1: x + halfW,
4453
+ y0: y - LABEL_HALF_HEIGHT,
4454
+ y1: y + LABEL_HALF_HEIGHT
4455
+ };
4535
4456
  }
4536
- function formatBpRange(shortest, longest) {
4537
- const low = formatBp(shortest);
4538
- const high = formatBp(longest);
4539
- const unit = high.slice(high.indexOf(" "));
4540
- return low.endsWith(unit) ? `${low.slice(0, -unit.length)}\u2013${high}` : `${low}\u2013${high}`;
4457
+ function rowLabelBox(text, screenY) {
4458
+ return {
4459
+ x0: 6,
4460
+ x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
4461
+ y0: screenY - 8 - LABEL_GAP_PX / 2,
4462
+ y1: screenY + 8 + LABEL_GAP_PX / 2
4463
+ };
4541
4464
  }
4542
- function routes(count) {
4543
- if (count >= SATURATED_PATH_COUNT) {
4544
- return "\u22652.1B routes";
4545
- }
4546
- if (count >= 1e5) {
4547
- return `${(count / 1e3).toFixed(0)}k routes`;
4465
+ function clamp(v, lo, hi) {
4466
+ return hi < lo ? v : Math.min(Math.max(v, lo), hi);
4467
+ }
4468
+ function formatBp(bp) {
4469
+ if (bp >= 1e6) {
4470
+ return `${+(bp / 1e6).toFixed(1)} Mb`;
4548
4471
  }
4549
- return `${count} routes`;
4472
+ return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
4550
4473
  }
4551
- function classifyBubble(b, repeats = []) {
4552
- const c = classifyShape(b, repeats);
4553
- return b.partial ? { ...c, label: `${c.label}, partial` } : c;
4474
+ function deletionText(bp) {
4475
+ return `${formatBp(bp)} del`;
4554
4476
  }
4555
- function classifyShape(b, repeats) {
4556
- const refSpan = b.end - b.start;
4557
- const {
4558
- shortestAlleleLength: shortest,
4559
- longestAlleleLength: longest,
4560
- pathCount,
4561
- segmentCount,
4562
- inversion
4563
- } = b;
4564
- const range = formatBpRange(shortest, longest);
4565
- if (segmentCount >= SUPERBUBBLE_SEGMENTS) {
4566
- return {
4567
- kind: "superbubble",
4568
- label: `${range} superbubble, ${segmentCount} segments, ${routes(pathCount)}${inversion ? ", inv" : ""}`
4569
- };
4570
- }
4571
- if (inversion) {
4572
- return { kind: "inversion", label: `${formatBp(longest)} inv` };
4477
+ function arcOutward(curves, apex, scaleX, scaleY) {
4478
+ const first = curves[0];
4479
+ const last = curves[curves.length - 1];
4480
+ const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
4481
+ const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
4482
+ const len = Math.hypot(dx, dy);
4483
+ return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
4484
+ }
4485
+ function boxSupport(box, dir) {
4486
+ return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
4487
+ }
4488
+ function boxRayCrossing(box, dir) {
4489
+ const halfW = (box.x1 - box.x0) / 2;
4490
+ const halfH = (box.y1 - box.y0) / 2;
4491
+ return Math.min(
4492
+ dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
4493
+ dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
4494
+ );
4495
+ }
4496
+ function drawnLength(segments, scaleX, scaleY) {
4497
+ let total = 0;
4498
+ for (let i = 1; i < segments.length; i++) {
4499
+ const a = segments[i - 1];
4500
+ const b = segments[i];
4501
+ total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
4573
4502
  }
4574
- const repeat = longest !== shortest ? coveringRepeat(b, repeats) : void 0;
4575
- if (repeat) {
4576
- const name = repeat.name ? ` (${repeat.name})` : "";
4577
- return {
4578
- kind: "repeat",
4579
- label: `${range} repeat array${name}, ${routes(pathCount)}`
4580
- };
4503
+ return total;
4504
+ }
4505
+ function runBounds(nodeIds, nodePositions) {
4506
+ let minX = Infinity;
4507
+ let minY = Infinity;
4508
+ let maxX = -Infinity;
4509
+ let maxY = -Infinity;
4510
+ for (const id of nodeIds) {
4511
+ for (const p of nodePositions[id] ?? []) {
4512
+ minX = Math.min(minX, p.x);
4513
+ maxX = Math.max(maxX, p.x);
4514
+ minY = Math.min(minY, p.y);
4515
+ maxY = Math.max(maxY, p.y);
4516
+ }
4581
4517
  }
4582
- const alleles = pathCount > 2 ? `, ${pathCount} alleles` : "";
4583
- if (shortest === longest && shortest === refSpan) {
4584
- return refSpan <= 1 ? { kind: "snp", label: "SNP" } : { kind: "substitution", label: `${formatBp(refSpan)} sub` };
4518
+ return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
4519
+ }
4520
+ var nodeOrderCache = /* @__PURE__ */ new WeakMap();
4521
+ function labelOrder(nodePositions, nodeLengths) {
4522
+ const cached2 = nodeOrderCache.get(nodePositions);
4523
+ if (cached2?.nodeLengths === nodeLengths) {
4524
+ return cached2.order;
4585
4525
  }
4586
- if (refSpan === 0 || shortest === refSpan) {
4587
- return {
4588
- kind: "insertion",
4589
- label: `\u2264${formatBp(longest - refSpan)} ins${alleles}`
4590
- };
4526
+ const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
4527
+ nodeOrderCache.set(nodePositions, { nodeLengths, order });
4528
+ return order;
4529
+ }
4530
+ var arcCache = /* @__PURE__ */ new WeakMap();
4531
+ function arcPlacements(nodePositions, deletions, axis, version) {
4532
+ const { scaleX, scaleY } = axis;
4533
+ const cached2 = arcCache.get(nodePositions);
4534
+ if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
4535
+ return cached2.arcs;
4591
4536
  }
4592
- if (longest === refSpan) {
4593
- return {
4594
- kind: "deletion",
4595
- label: `${formatBp(refSpan - shortest)} del${alleles}`
4596
- };
4537
+ const arcs = [];
4538
+ for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
4539
+ const curves = deletionArcCurves(nodePositions, deletion, axis);
4540
+ const apex = curves ? curveMidpoint(curves) : void 0;
4541
+ if (curves && apex) {
4542
+ const { minX, minY, maxX, maxY } = curveBounds(curves);
4543
+ arcs.push({
4544
+ deletion,
4545
+ curves,
4546
+ apex,
4547
+ // in screen px, so the two axes' units cannot be added by accident
4548
+ extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
4549
+ });
4550
+ }
4597
4551
  }
4598
- return {
4599
- kind: "complex",
4600
- label: `${formatBp(refSpan)} ref \u2192 ${range}, ${pathCount} alleles`
4601
- };
4602
- }
4603
- var BUBBLE_KIND_COLORS = {
4604
- snp: "#5b6b7a",
4605
- substitution: "#5b6b7a",
4606
- insertion: "#2f8fd6",
4607
- deletion: "#c94040",
4608
- inversion: "#e07b00",
4609
- repeat: "#8e3fbf",
4610
- superbubble: "#8e3fbf",
4611
- complex: "#8e3fbf"
4612
- };
4613
- var BUBBLE_KIND_NAMES = {
4614
- snp: "SNP",
4615
- substitution: "substitution",
4616
- insertion: "insertion",
4617
- deletion: "deletion",
4618
- inversion: "inversion",
4619
- repeat: "repeat array",
4620
- superbubble: "superbubble",
4621
- complex: "site"
4622
- };
4623
- function bubbleSegmentIds(b) {
4624
- return b.segments.split(",").filter(Boolean);
4552
+ arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
4553
+ return arcs;
4625
4554
  }
4626
-
4627
- // src/bubbles/bubbleHalos.ts
4628
- var WHOLE_FRACTION = 0.9;
4629
- var NAMED_WALKS = 2;
4630
- var MIN_CHIPPED_BP = 1e3;
4631
- function bubbleHalos(graph, bubbles, positions, walkLabel = (name) => name, repeats = []) {
4632
- const byName = new Map(graph.nodes.map((n) => [n.name, n]));
4633
- const halos = [];
4634
- for (const bubble of bubbles) {
4635
- const parts = [];
4636
- let minX = Infinity;
4637
- let maxX = -Infinity;
4638
- let minY = Infinity;
4639
- const nodeIds = [];
4640
- const ends = [];
4641
- for (const name of bubbleSegmentIds(bubble)) {
4642
- const node = byName.get(name);
4643
- const line = node && positions[node.id];
4644
- if (!node || !line?.length) {
4645
- continue;
4646
- }
4647
- if (isBackbone(node) && (node.stable.start < bubble.start || node.stable.start >= bubble.end)) {
4648
- ends.push(line[Math.floor(line.length / 2)]);
4649
- continue;
4650
- }
4651
- nodeIds.push(node.id);
4652
- parts.push(svgPath(line.length === 1 ? [line[0], line[0]] : line));
4653
- for (const p of line) {
4654
- minX = Math.min(minX, p.x);
4655
- maxX = Math.max(maxX, p.x);
4656
- minY = Math.min(minY, p.y);
4555
+ function sizeLabelCandidates({
4556
+ nodePositions,
4557
+ nodeLengths,
4558
+ deletions,
4559
+ alleleDeletions,
4560
+ axis,
4561
+ translateX,
4562
+ translateY,
4563
+ width,
4564
+ height,
4565
+ version = 0
4566
+ }) {
4567
+ const { scaleX, scaleY } = axis;
4568
+ const deletionCandidates = [];
4569
+ const nodeCandidates = [];
4570
+ for (const { deletion, curves, apex, extent } of arcPlacements(
4571
+ nodePositions,
4572
+ deletions,
4573
+ axis,
4574
+ version
4575
+ )) {
4576
+ if (extent >= MIN_DELETION_LABEL_PX) {
4577
+ const text = deletionText(deletion.bp);
4578
+ const halfW = labelHalfWidth(text);
4579
+ const box = boxAt(halfW, 0, 0);
4580
+ const arcX = apex.x * scaleX + translateX;
4581
+ const arcY = apex.y * scaleY + translateY;
4582
+ const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
4583
+ const bow = arcOutward(curves, apex, scaleX, scaleY);
4584
+ const offset = boxSupport(box, bow) + LEADER_GAP_PX;
4585
+ const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
4586
+ const y = fits ? arcY : clamp(
4587
+ arcY + bow.y * offset,
4588
+ LABEL_HALF_HEIGHT,
4589
+ height - LABEL_HALF_HEIGHT
4590
+ );
4591
+ const away = Math.hypot(x - arcX, y - arcY);
4592
+ let leader;
4593
+ if (away > 0) {
4594
+ const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
4595
+ const crossing = boxRayCrossing(box, dir);
4596
+ leader = {
4597
+ arcX,
4598
+ arcY,
4599
+ labelX: x - dir.x * crossing,
4600
+ labelY: y - dir.y * crossing
4601
+ };
4657
4602
  }
4603
+ deletionCandidates.push({
4604
+ label: {
4605
+ key: `del:${deletion.edgeIndex}`,
4606
+ text,
4607
+ x,
4608
+ y,
4609
+ kind: "deletion",
4610
+ leader
4611
+ },
4612
+ box: boxAt(halfW, x, y)
4613
+ });
4658
4614
  }
4659
- if (nodeIds.length === 0) {
4615
+ }
4616
+ const labelledByDeletion = /* @__PURE__ */ new Set();
4617
+ const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
4618
+ for (const run of alleleRuns) {
4619
+ const drawn = runBounds(run.nodeIds, nodePositions);
4620
+ if (!drawn) {
4660
4621
  continue;
4661
4622
  }
4662
- const labelAt = { x: (minX + maxX) / 2, y: minY };
4663
- const anchor = ends.length ? {
4664
- x: ends.reduce((s, p) => s + p.x, 0) / ends.length,
4665
- y: ends.reduce((s, p) => s + p.y, 0) / ends.length
4666
- } : labelAt;
4667
- const sharing = /* @__PURE__ */ new Map();
4668
- for (const route of bubble.routes ?? []) {
4669
- for (const id of new Set(route.steps)) {
4670
- sharing.set(id, (sharing.get(id) ?? 0) + 1);
4671
- }
4623
+ const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
4624
+ const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
4625
+ const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
4626
+ const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
4627
+ if (shownRight < shownLeft || shownBottom < shownTop) {
4628
+ continue;
4672
4629
  }
4673
- const routes2 = [];
4674
- for (const route of bubble.routes ?? []) {
4675
- if (bubble.longestAlleleLength < MIN_CHIPPED_BP) {
4676
- break;
4677
- }
4678
- if (route.steps.length === 0) {
4679
- continue;
4680
- }
4681
- const rarest = Math.min(...route.steps.map((id) => sharing.get(id)));
4682
- let at;
4683
- let far = -1;
4684
- for (const id of route.steps) {
4685
- if (sharing.get(id) !== rarest) {
4686
- continue;
4687
- }
4688
- for (const p of positions[id] ?? []) {
4689
- const d = Math.hypot(p.x - anchor.x, p.y - anchor.y);
4690
- if (d > far) {
4691
- far = d;
4692
- at = p;
4693
- }
4694
- }
4695
- }
4696
- if (at) {
4697
- routes2.push({ route, at, text: routeText(route, walkLabel) });
4630
+ const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
4631
+ const text = deletionText(run.bp);
4632
+ const halfW = labelHalfWidth(text);
4633
+ if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
4634
+ for (const id of run.nodeIds) {
4635
+ labelledByDeletion.add(id);
4698
4636
  }
4637
+ const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
4638
+ const y = clamp(
4639
+ (shownTop + shownBottom) / 2,
4640
+ LABEL_HALF_HEIGHT,
4641
+ height - LABEL_HALF_HEIGHT
4642
+ );
4643
+ deletionCandidates.push({
4644
+ label: {
4645
+ key: `alleledel:${run.nodeIds.join(",")}`,
4646
+ text,
4647
+ x,
4648
+ y,
4649
+ kind: "deletion"
4650
+ },
4651
+ box: boxAt(halfW, x, y)
4652
+ });
4699
4653
  }
4700
- halos.push({
4701
- bubble,
4702
- ...classifyBubble(bubble, repeats),
4703
- path: parts.join(""),
4704
- labelAt,
4705
- members: nodeIds.length,
4706
- nodeIds,
4707
- whole: nodeIds.length >= WHOLE_FRACTION * graph.nodes.length,
4708
- routes: routes2
4709
- });
4710
4654
  }
4711
- return halos;
4655
+ for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
4656
+ if (labelledByDeletion.has(id)) {
4657
+ continue;
4658
+ }
4659
+ const { x, y } = polylineMidpoint(segments);
4660
+ const screenY = y * scaleY + translateY;
4661
+ if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
4662
+ continue;
4663
+ }
4664
+ const drawn = drawnLength(segments, scaleX, scaleY);
4665
+ if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
4666
+ continue;
4667
+ }
4668
+ const text = formatBp(nodeLengths.get(id));
4669
+ const halfW = labelHalfWidth(text);
4670
+ if (drawn >= halfW * 2 / MAX_LABEL_OVERHANG) {
4671
+ const screenX = x * scaleX + translateX;
4672
+ nodeCandidates.push({
4673
+ label: {
4674
+ key: `node:${id}`,
4675
+ text,
4676
+ x: screenX,
4677
+ y: screenY,
4678
+ kind: "node"
4679
+ },
4680
+ box: boxAt(halfW, screenX, screenY)
4681
+ });
4682
+ }
4683
+ }
4684
+ return { deletions: deletionCandidates, nodes: nodeCandidates };
4712
4685
  }
4713
- function routeText(route, walkLabel) {
4714
- const names = [...new Set(route.walks.map(walkLabel))].sort();
4715
- const shown = names.slice(0, NAMED_WALKS).join(", ");
4716
- const more = names.length > NAMED_WALKS ? ` +${names.length - NAMED_WALKS}` : "";
4717
- return `${shown}${more} \xB7 ${formatBp(route.bp)}`;
4686
+ function nodeLabelBudget(width, height) {
4687
+ return Math.max(
4688
+ MIN_NODE_LABELS,
4689
+ Math.floor(width * height / PX_PER_NODE_LABEL)
4690
+ );
4718
4691
  }
4719
-
4720
- // src/bubbles/popBubble.ts
4721
- function bubbleSubgraph(graph, segmentIds) {
4722
- const keep = new Set(segmentIds);
4723
- const nodes = graph.nodes.filter((n) => keep.has(n.name));
4724
- const ids = new Set(nodes.map((n) => n.id));
4725
- const paths = graph.paths?.map((p) => ({ ...p, nodeIds: p.nodeIds.filter((id) => ids.has(id)) })).filter((p) => p.nodeIds.length > 0);
4726
- return {
4727
- name: graph.name,
4728
- nodes,
4729
- edges: graph.edges.filter((e) => ids.has(e.from) && ids.has(e.to)),
4730
- ...paths?.length ? { paths } : {},
4731
- anchorPaths: graph.anchorPaths,
4732
- pathVisits: graph.pathVisits,
4733
- anchoredBy: graph.anchoredBy,
4734
- referencePath: graph.referencePath
4735
- };
4692
+ function placeSizeLabels(candidates, take, budget = Infinity) {
4693
+ const labels = [];
4694
+ for (const { label, box } of candidates) {
4695
+ if (labels.length >= budget) {
4696
+ break;
4697
+ }
4698
+ if (take(box)) {
4699
+ labels.push(label);
4700
+ }
4701
+ }
4702
+ return labels;
4736
4703
  }
4737
4704
 
4738
- // src/overlayLabels.ts
4739
- var LABEL_PX = 11;
4740
- var LABEL_CHAR_PX = 6.2;
4741
- var LABEL_PAD = 4;
4742
- function labelWidth(text) {
4743
- return text.length * LABEL_CHAR_PX + LABEL_PAD * 2;
4744
- }
4745
- function overlaps(a, b) {
4746
- return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
4747
- }
4748
- function offFrame(box, frame) {
4749
- return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
4750
- }
4751
- function insideFrame(box, frame) {
4752
- return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
4753
- }
4754
- function occupancy(frame, reserved = []) {
4755
- const placed = [...reserved];
4756
- return (box) => {
4757
- if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
4758
- return false;
4705
+ // src/tubeMap/axis.ts
4706
+ function referenceBoxes(graph, layout) {
4707
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
4708
+ const byRefName = /* @__PURE__ */ new Map();
4709
+ layout.nodes.forEach((node) => {
4710
+ const graphNode = nodeById.get(node.name);
4711
+ if (node.order >= 0 && graphNode && isBackbone(graphNode)) {
4712
+ const { refName, start } = graphNode.stable;
4713
+ const boxes = byRefName.get(refName) ?? byRefName.set(refName, []).get(refName);
4714
+ boxes.push({
4715
+ bp0: start,
4716
+ bp1: start + node.sequenceLength,
4717
+ x0: node.x,
4718
+ x1: node.x + node.pixelWidth
4719
+ });
4759
4720
  }
4760
- placed.push(box);
4761
- return true;
4762
- };
4721
+ });
4722
+ for (const boxes of byRefName.values()) {
4723
+ boxes.sort((a, b) => a.bp0 - b.bp0);
4724
+ }
4725
+ return byRefName;
4763
4726
  }
4764
- function placeLabels(candidates, frame, take) {
4765
- const out = [];
4766
- const row = LABEL_PX + LABEL_PAD * 2;
4767
- for (const c of candidates) {
4768
- const w = labelWidth(c.text);
4769
- const direction = c.y > frame.height / 2 ? -1 : 1;
4770
- for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
4771
- const y = c.y + direction * tries * row;
4772
- const box = {
4773
- x0: c.x - w / 2,
4774
- x1: c.x + w / 2,
4775
- y0: y - LABEL_PX - LABEL_PAD,
4776
- y1: y + LABEL_PAD
4777
- };
4778
- if (offFrame(box, frame)) {
4779
- break;
4780
- }
4781
- if (insideFrame(box, frame) && take(box)) {
4782
- out.push({ ...c, y, w });
4783
- break;
4784
- }
4727
+ function tubeX(boxes, bp, isEnd = false) {
4728
+ let lo = 0;
4729
+ let hi = boxes.length - 1;
4730
+ while (lo < hi) {
4731
+ const mid = lo + hi >> 1;
4732
+ const bp1 = boxes[mid].bp1;
4733
+ if (isEnd ? bp1 < bp : bp1 <= bp) {
4734
+ lo = mid + 1;
4735
+ } else {
4736
+ hi = mid;
4785
4737
  }
4786
4738
  }
4787
- return out;
4739
+ const box = boxes[lo];
4740
+ if (bp <= box.bp0) {
4741
+ return box.x0;
4742
+ }
4743
+ const span = box.bp1 - box.bp0;
4744
+ return span > 0 ? box.x0 + (Math.min(bp, box.bp1) - box.bp0) / span * (box.x1 - box.x0) : box.x0;
4788
4745
  }
4789
-
4790
- // src/graphLabels.ts
4791
- var MIN_DELETION_LABEL_PX = 26;
4792
- var PX_PER_NODE_LABEL = 4e4;
4793
- var MIN_NODE_LABELS = 8;
4794
- var LEADER_GAP_PX = 8;
4795
- var LABEL_CHAR_PX2 = 5.7;
4796
- var LABEL_PAD_PX = 6;
4797
- var LABEL_HEIGHT_PX = 13;
4798
- var LABEL_GAP_PX = 3;
4799
- var MAX_LABEL_OVERHANG = 2;
4800
- function labelHalfWidth(text) {
4801
- return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
4746
+ function tubeSpan(boxes, start, end) {
4747
+ const a = tubeX(boxes, start);
4748
+ const b = tubeX(boxes, end, true);
4749
+ return { x0: Math.min(a, b), x1: Math.max(a, b) };
4802
4750
  }
4803
- var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
4804
- var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
4805
- function boxAt(halfW, x, y) {
4806
- return {
4807
- x0: x - halfW,
4808
- x1: x + halfW,
4809
- y0: y - LABEL_HALF_HEIGHT,
4810
- y1: y + LABEL_HALF_HEIGHT
4811
- };
4751
+ function rulerBoxes(byContig) {
4752
+ let best;
4753
+ for (const boxes of byContig.values()) {
4754
+ if (!best || boxes.length > best.length) {
4755
+ best = boxes;
4756
+ }
4757
+ }
4758
+ return best;
4812
4759
  }
4813
- function rowLabelBox(text, screenY) {
4760
+ var TICK_PX = 5;
4761
+ var LABEL_GAP_PX2 = 10;
4762
+ var TARGET_TICK_PX = 110;
4763
+ var ZIGZAG_PX = 4;
4764
+ var ZIGZAG_AMPLITUDE_PX = 2.5;
4765
+ function niceStep(raw) {
4766
+ const pow = 10 ** Math.floor(Math.log10(raw));
4767
+ const m = raw / pow;
4768
+ return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;
4769
+ }
4770
+ function rulerMarks(boxes, x, width, measure) {
4771
+ const first = boxes[0];
4772
+ const last = boxes.at(-1);
4773
+ if (!first || !last) {
4774
+ return void 0;
4775
+ }
4776
+ const from = x(first.x0);
4777
+ const to = x(last.x1);
4778
+ const bpSpan = last.bp1 - first.bp0;
4779
+ if (to <= from || bpSpan <= 0) {
4780
+ return void 0;
4781
+ }
4782
+ const step = niceStep(bpSpan / (to - from) * TARGET_TICK_PX);
4783
+ const squeezedBoxes = boxes.filter((box) => {
4784
+ const bp = box.bp1 - box.bp0;
4785
+ return bp >= step && (x(box.x1) - x(box.x0)) * step / bp < TARGET_TICK_PX / 2;
4786
+ });
4787
+ const squeezed = [];
4788
+ const lengths = [];
4789
+ for (const box of squeezedBoxes) {
4790
+ const x0 = x(box.x0);
4791
+ const x1 = x(box.x1);
4792
+ if (x1 >= 0 && x0 <= width) {
4793
+ squeezed.push({ x0, x1 });
4794
+ const text = formatBp(box.bp1 - box.bp0);
4795
+ lengths.push({ text, at: (x0 + x1) / 2, width: measure(text) });
4796
+ }
4797
+ }
4798
+ const ticks = [];
4799
+ const positions = [];
4800
+ let k = 0;
4801
+ for (let bp = Math.ceil(first.bp0 / step) * step; bp <= last.bp1; bp += step) {
4802
+ while (k < squeezedBoxes.length && squeezedBoxes[k].bp1 <= bp) {
4803
+ k++;
4804
+ }
4805
+ if (k < squeezedBoxes.length && squeezedBoxes[k].bp0 < bp) {
4806
+ continue;
4807
+ }
4808
+ const sx = Math.round(x(tubeX(boxes, bp))) + 0.5;
4809
+ if (sx >= 0 && sx <= width) {
4810
+ ticks.push(sx);
4811
+ const text = bp.toLocaleString("en-US");
4812
+ positions.push({ text, at: sx, width: measure(text) });
4813
+ }
4814
+ }
4815
+ const placed = [];
4816
+ const clear = (a) => placed.every(
4817
+ (b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX2
4818
+ );
4819
+ for (const label of [...positions, ...lengths]) {
4820
+ if (clear(label)) {
4821
+ placed.push(label);
4822
+ }
4823
+ }
4814
4824
  return {
4815
- x0: 6,
4816
- x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
4817
- y0: screenY - 8 - LABEL_GAP_PX / 2,
4818
- y1: screenY + 8 + LABEL_GAP_PX / 2
4825
+ from,
4826
+ to,
4827
+ ticks,
4828
+ squeezed,
4829
+ labels: placed.map(({ text, at }) => ({ text, at }))
4819
4830
  };
4820
4831
  }
4821
- function clamp(v, lo, hi) {
4822
- return hi < lo ? v : Math.min(Math.max(v, lo), hi);
4832
+ function zigzag(ctx, x0, x1, y) {
4833
+ const teeth = Math.max(1, Math.round((x1 - x0) / ZIGZAG_PX));
4834
+ const dx = (x1 - x0) / teeth;
4835
+ for (let i = 0; i < teeth; i++) {
4836
+ ctx.lineTo(x0 + (i + 0.5) * dx, y + (i % 2 ? 1 : -1) * ZIGZAG_AMPLITUDE_PX);
4837
+ }
4838
+ ctx.lineTo(x1, y);
4823
4839
  }
4824
- function formatBp2(bp) {
4825
- if (bp >= 1e6) {
4826
- return `${+(bp / 1e6).toFixed(1)} Mb`;
4840
+ function drawTubeMapRuler(ctx, boxes, frame, top) {
4841
+ const { x, width, darkMode } = frame;
4842
+ ctx.font = "10px sans-serif";
4843
+ const marks = rulerMarks(boxes, x, width, (text) => ctx.measureText(text).width);
4844
+ if (!marks) {
4845
+ return;
4846
+ }
4847
+ const ink = darkMode ? "#b0b0b8" : "#55555c";
4848
+ const y = top + 0.5;
4849
+ ctx.strokeStyle = ink;
4850
+ ctx.fillStyle = ink;
4851
+ ctx.lineWidth = 1;
4852
+ ctx.textAlign = "center";
4853
+ ctx.textBaseline = "top";
4854
+ ctx.beginPath();
4855
+ ctx.moveTo(Math.max(0, marks.from), y);
4856
+ for (const { x0, x1 } of marks.squeezed) {
4857
+ ctx.lineTo(x0, y);
4858
+ zigzag(ctx, x0, x1, y);
4859
+ }
4860
+ ctx.lineTo(Math.min(width, marks.to), y);
4861
+ for (const sx of marks.ticks) {
4862
+ ctx.moveTo(sx, top);
4863
+ ctx.lineTo(sx, top + TICK_PX);
4864
+ }
4865
+ ctx.stroke();
4866
+ for (const { text, at } of marks.labels) {
4867
+ ctx.fillText(text, at, top + TICK_PX + 2);
4827
4868
  }
4828
- return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
4829
4869
  }
4830
- function deletionText(bp) {
4831
- return `${formatBp2(bp)} del`;
4870
+
4871
+ // src/reference.ts
4872
+ var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
4873
+ ["hg38", "GRCh38"],
4874
+ ["hg19", "GRCh37"],
4875
+ ["hs1", "CHM13"],
4876
+ ["t2t-chm13", "CHM13"],
4877
+ ["chm13v2", "CHM13"]
4878
+ ]);
4879
+ function wellKnownSample(assemblyName) {
4880
+ return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
4881
+ }
4882
+ function sharedPrefixes(refNames) {
4883
+ return [panSNSample, panSNHaplotype].flatMap((prefixOf) => {
4884
+ const prefixes = new Set(
4885
+ refNames.map((n) => n.includes("#") ? prefixOf(n) : void 0)
4886
+ );
4887
+ const [only] = prefixes;
4888
+ return prefixes.size === 1 && only ? [only] : [];
4889
+ });
4832
4890
  }
4833
- function arcOutward(curves, apex, scaleX, scaleY) {
4834
- const first = curves[0];
4835
- const last = curves[curves.length - 1];
4836
- const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
4837
- const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
4838
- const len = Math.hypot(dx, dy);
4839
- return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
4891
+ function graphBackbone(graph) {
4892
+ const spans = /* @__PURE__ */ new Map();
4893
+ for (const node of graph.nodes) {
4894
+ if (isBackbone(node)) {
4895
+ const { refName, start } = node.stable;
4896
+ const end = start + node.length;
4897
+ const span = spans.get(refName);
4898
+ if (span) {
4899
+ span.start = Math.min(span.start, start);
4900
+ span.end = Math.max(span.end, end);
4901
+ } else {
4902
+ spans.set(refName, {
4903
+ refName,
4904
+ contig: panSNContig(refName),
4905
+ start,
4906
+ end
4907
+ });
4908
+ }
4909
+ }
4910
+ }
4911
+ const contigs = [...spans.values()];
4912
+ return contigs.length ? { contigs, prefixes: sharedPrefixes(contigs.map((c) => c.refName)) } : void 0;
4840
4913
  }
4841
- function boxSupport(box, dir) {
4842
- return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
4914
+ function backboneAssembly(backbone, assemblies) {
4915
+ const prefixes = new Set(backbone?.prefixes.map((p) => p.toLowerCase()));
4916
+ const names = (a) => [a.name, ...a.aliases ?? []];
4917
+ const named = (candidates) => candidates.some((n) => n !== void 0 && prefixes.has(n.toLowerCase()));
4918
+ return assemblies.find((a) => named(names(a))) ?? assemblies.find((a) => named(names(a).map(wellKnownSample)));
4843
4919
  }
4844
- function boxRayCrossing(box, dir) {
4845
- const halfW = (box.x1 - box.x0) / 2;
4846
- const halfH = (box.y1 - box.y0) / 2;
4847
- return Math.min(
4848
- dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
4849
- dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
4850
- );
4920
+ function refNameBinding(refNames) {
4921
+ const exact = new Set(refNames);
4922
+ const byContig = /* @__PURE__ */ new Map();
4923
+ for (const refName of exact) {
4924
+ const contig = panSNContig(refName);
4925
+ byContig.set(contig, byContig.has(contig) ? void 0 : refName);
4926
+ }
4927
+ return (name) => exact.has(name) ? name : byContig.get(name);
4851
4928
  }
4852
- function drawnLength(segments, scaleX, scaleY) {
4853
- let total = 0;
4854
- for (let i = 1; i < segments.length; i++) {
4855
- const a = segments[i - 1];
4856
- const b = segments[i];
4857
- total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
4929
+ function featuresOnBackbone(features, backbone) {
4930
+ const bind = refNameBinding(backbone.contigs.map((c) => c.refName));
4931
+ return features.flatMap((f) => {
4932
+ const refName = bind(f.refName);
4933
+ return refName === void 0 ? [] : [refName === f.refName ? f : { ...f, refName }];
4934
+ });
4935
+ }
4936
+
4937
+ // src/tubeMap/genes.ts
4938
+ function tubeMapGenes(byRefName, genes) {
4939
+ const out = [];
4940
+ const bind = refNameBinding(byRefName.keys());
4941
+ for (const gene of genes) {
4942
+ const refName = bind(gene.refName);
4943
+ const boxes = refName === void 0 ? void 0 : byRefName.get(refName);
4944
+ const first = boxes?.[0];
4945
+ const last = boxes?.at(-1);
4946
+ if (!boxes || !first || !last) {
4947
+ continue;
4948
+ }
4949
+ const start = Math.max(gene.start, first.bp0);
4950
+ const end = Math.min(gene.end, last.bp1);
4951
+ if (end <= start) {
4952
+ continue;
4953
+ }
4954
+ out.push({
4955
+ gene,
4956
+ ...tubeSpan(boxes, start, end),
4957
+ exons: gene.exons.filter((e) => e.end > start && e.start < end).map(
4958
+ (e) => tubeSpan(boxes, Math.max(e.start, start), Math.min(e.end, end))
4959
+ )
4960
+ });
4858
4961
  }
4859
- return total;
4962
+ return out;
4860
4963
  }
4861
- function runBounds(nodeIds, nodePositions) {
4862
- let minX = Infinity;
4863
- let minY = Infinity;
4864
- let maxX = -Infinity;
4865
- let maxY = -Infinity;
4866
- for (const id of nodeIds) {
4867
- for (const p of nodePositions[id] ?? []) {
4868
- minX = Math.min(minX, p.x);
4869
- maxX = Math.max(maxX, p.x);
4870
- minY = Math.min(minY, p.y);
4871
- maxY = Math.max(maxY, p.y);
4964
+ var ROW_PX = 22;
4965
+ var EXON_PX = 8;
4966
+ var NAME_FONT = "11px sans-serif";
4967
+ var NAME_GAP_PX = 8;
4968
+ function drawTubeMapGenes(ctx, genes, frame, bottom, top = 0) {
4969
+ const { x, width, darkMode } = frame;
4970
+ const rows = Math.max(1, Math.floor((bottom - top) / ROW_PX));
4971
+ const rowEnds = [];
4972
+ const ink = darkMode ? "#e0e0e6" : "#1c1c22";
4973
+ ctx.font = NAME_FONT;
4974
+ ctx.textAlign = "center";
4975
+ ctx.textBaseline = "alphabetic";
4976
+ ctx.fillStyle = ink;
4977
+ ctx.strokeStyle = ink;
4978
+ ctx.lineWidth = 1;
4979
+ 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);
4980
+ for (const { g, s0, s1 } of placed) {
4981
+ const arrow = g.gene.strand > 0 ? " \u2192" : g.gene.strand < 0 ? "\u2190 " : "";
4982
+ const label = g.gene.strand < 0 ? `${arrow}${g.gene.name}` : `${g.gene.name}${arrow}`;
4983
+ const mid = (Math.max(s0, 0) + Math.min(s1, width)) / 2;
4984
+ const half = ctx.measureText(label).width / 2;
4985
+ const left = Math.min(s0, mid - half);
4986
+ const right = Math.max(s1, mid + half);
4987
+ let row = rowEnds.findIndex((end) => end + NAME_GAP_PX <= left);
4988
+ if (row === -1) {
4989
+ row = Math.min(rowEnds.length, rows - 1);
4990
+ }
4991
+ rowEnds[row] = Math.max(rowEnds[row] ?? -Infinity, right);
4992
+ const glyphY = bottom - row * ROW_PX - EXON_PX / 2;
4993
+ ctx.beginPath();
4994
+ ctx.moveTo(s0, glyphY + 0.5);
4995
+ ctx.lineTo(s1, glyphY + 0.5);
4996
+ ctx.stroke();
4997
+ for (const e of g.exons) {
4998
+ const e0 = x(e.x0);
4999
+ ctx.fillRect(e0, glyphY - EXON_PX / 2, Math.max(1, x(e.x1) - e0), EXON_PX);
4872
5000
  }
5001
+ ctx.fillText(label, mid, glyphY - EXON_PX / 2 - 2);
4873
5002
  }
4874
- return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
4875
5003
  }
4876
- var nodeOrderCache = /* @__PURE__ */ new WeakMap();
4877
- function labelOrder(nodePositions, nodeLengths) {
4878
- const cached2 = nodeOrderCache.get(nodePositions);
4879
- if (cached2?.nodeLengths === nodeLengths) {
4880
- return cached2.order;
5004
+
5005
+ // src/tubeMap/connectors.ts
5006
+ function referenceNodes(graph, layout) {
5007
+ const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
5008
+ const out = [];
5009
+ layout.nodes.forEach((node) => {
5010
+ const graphNode = nodeById.get(node.name);
5011
+ if (node.order >= 0 && graphNode && isBackbone(graphNode)) {
5012
+ out.push({
5013
+ node: node.name,
5014
+ bp0: graphNode.stable.start,
5015
+ bp1: graphNode.stable.start + node.sequenceLength,
5016
+ x0: node.x,
5017
+ x1: node.x + node.pixelWidth
5018
+ });
5019
+ }
5020
+ });
5021
+ return out;
5022
+ }
5023
+ function tubeMapConnectors(nodes, bpToScreen, tubeToScreen, width) {
5024
+ const out = [];
5025
+ for (const n of nodes) {
5026
+ const bottom0 = tubeToScreen(n.x0);
5027
+ const bottom1 = tubeToScreen(n.x1);
5028
+ if (bottom1 >= 0 && bottom0 <= width) {
5029
+ out.push({
5030
+ node: n.node,
5031
+ top0: bpToScreen(n.bp0),
5032
+ top1: bpToScreen(n.bp1),
5033
+ bottom0,
5034
+ bottom1
5035
+ });
5036
+ }
4881
5037
  }
4882
- const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
4883
- nodeOrderCache.set(nodePositions, { nodeLengths, order });
4884
- return order;
5038
+ return out;
4885
5039
  }
4886
- var arcCache = /* @__PURE__ */ new WeakMap();
4887
- function arcPlacements(nodePositions, deletions, axis, version) {
4888
- const { scaleX, scaleY } = axis;
4889
- const cached2 = arcCache.get(nodePositions);
4890
- if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
4891
- return cached2.arcs;
5040
+ var HIT_SLOP_PX = 3;
5041
+ function connectorAt(connectors, zoneBottom, sx, sy) {
5042
+ if (zoneBottom <= 0 || sy < 0 || sy > zoneBottom) {
5043
+ return void 0;
4892
5044
  }
4893
- const arcs = [];
4894
- for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
4895
- const curves = deletionArcCurves(nodePositions, deletion, axis);
4896
- const apex = curves ? curveMidpoint(curves) : void 0;
4897
- if (curves && apex) {
4898
- const { minX, minY, maxX, maxY } = curveBounds(curves);
4899
- arcs.push({
4900
- deletion,
4901
- curves,
4902
- apex,
4903
- // in screen px, so the two axes' units cannot be added by accident
4904
- extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
4905
- });
5045
+ const t = sy / zoneBottom;
5046
+ let hit;
5047
+ let best = HIT_SLOP_PX;
5048
+ for (const c of connectors) {
5049
+ const a = c.top0 + (c.bottom0 - c.top0) * t;
5050
+ const b = c.top1 + (c.bottom1 - c.top1) * t;
5051
+ const dist = Math.max(0, Math.min(a, b) - sx, sx - Math.max(a, b));
5052
+ if (dist < best) {
5053
+ best = dist;
5054
+ hit = c.node;
4906
5055
  }
4907
5056
  }
4908
- arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
4909
- return arcs;
5057
+ return hit;
4910
5058
  }
4911
- function sizeLabelCandidates({
4912
- nodePositions,
4913
- nodeLengths,
4914
- deletions,
4915
- alleleDeletions,
4916
- axis,
4917
- translateX,
4918
- translateY,
4919
- width,
4920
- height,
4921
- version = 0
5059
+ function band(ctx, c, bottom) {
5060
+ ctx.moveTo(c.top0, 0);
5061
+ ctx.lineTo(c.top1, 0);
5062
+ ctx.lineTo(c.bottom1, bottom);
5063
+ ctx.lineTo(c.bottom0, bottom);
5064
+ ctx.closePath();
5065
+ }
5066
+ function sides(ctx, c, bottom) {
5067
+ ctx.moveTo(c.top0, 0);
5068
+ ctx.lineTo(c.bottom0, bottom);
5069
+ ctx.moveTo(c.top1, 0);
5070
+ ctx.lineTo(c.bottom1, bottom);
5071
+ }
5072
+ function drawTubeMapConnectors(ctx, connectors, bottom, {
5073
+ highlightNode,
5074
+ darkMode
4922
5075
  }) {
4923
- const { scaleX, scaleY } = axis;
4924
- const deletionCandidates = [];
4925
- const nodeCandidates = [];
4926
- for (const { deletion, curves, apex, extent } of arcPlacements(
4927
- nodePositions,
4928
- deletions,
4929
- axis,
4930
- version
4931
- )) {
4932
- if (extent >= MIN_DELETION_LABEL_PX) {
4933
- const text = deletionText(deletion.bp);
4934
- const halfW = labelHalfWidth(text);
4935
- const box = boxAt(halfW, 0, 0);
4936
- const arcX = apex.x * scaleX + translateX;
4937
- const arcY = apex.y * scaleY + translateY;
4938
- const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
4939
- const bow = arcOutward(curves, apex, scaleX, scaleY);
4940
- const offset = boxSupport(box, bow) + LEADER_GAP_PX;
4941
- const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
4942
- const y = fits ? arcY : clamp(
4943
- arcY + bow.y * offset,
4944
- LABEL_HALF_HEIGHT,
4945
- height - LABEL_HALF_HEIGHT
4946
- );
4947
- const away = Math.hypot(x - arcX, y - arcY);
4948
- let leader;
4949
- if (away > 0) {
4950
- const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
4951
- const crossing = boxRayCrossing(box, dir);
4952
- leader = {
4953
- arcX,
4954
- arcY,
4955
- labelX: x - dir.x * crossing,
4956
- labelY: y - dir.y * crossing
4957
- };
5076
+ if (bottom <= 0 || connectors.length === 0) {
5077
+ return;
5078
+ }
5079
+ const ink = darkMode ? "255,255,255" : "0,0,0";
5080
+ ctx.beginPath();
5081
+ for (const c of connectors) {
5082
+ band(ctx, c, bottom);
5083
+ }
5084
+ ctx.fillStyle = `rgba(${ink},0.07)`;
5085
+ ctx.fill();
5086
+ ctx.beginPath();
5087
+ for (const c of connectors) {
5088
+ sides(ctx, c, bottom);
5089
+ }
5090
+ ctx.strokeStyle = `rgba(${ink},0.35)`;
5091
+ ctx.lineWidth = 0.5;
5092
+ ctx.stroke();
5093
+ const lit = connectors.find((c) => c.node === highlightNode);
5094
+ if (lit) {
5095
+ ctx.beginPath();
5096
+ band(ctx, lit, bottom);
5097
+ ctx.fillStyle = "rgba(255,192,203,0.5)";
5098
+ ctx.fill();
5099
+ ctx.beginPath();
5100
+ sides(ctx, lit, bottom);
5101
+ ctx.strokeStyle = "#ff0000";
5102
+ ctx.lineWidth = 1;
5103
+ ctx.stroke();
5104
+ }
5105
+ }
5106
+
5107
+ // src/walkHighlight.ts
5108
+ function walkHighlight(graph, name) {
5109
+ const path = graph.paths?.find((p) => p.name === name);
5110
+ if (!path) {
5111
+ return void 0;
5112
+ }
5113
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
5114
+ const edgeAt = /* @__PURE__ */ new Map();
5115
+ graph.edges.forEach((e, i) => {
5116
+ edgeAt.set(`${e.from}>${e.to}`, i);
5117
+ });
5118
+ const nodeIds = new Set(path.nodeIds);
5119
+ const edgeIndexes = /* @__PURE__ */ new Set();
5120
+ for (let i = 1; i < path.nodeIds.length; i++) {
5121
+ const a = path.nodeIds[i - 1];
5122
+ const b = path.nodeIds[i];
5123
+ const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
5124
+ if (index !== void 0) {
5125
+ edgeIndexes.add(index);
5126
+ }
5127
+ }
5128
+ const bpOf = (ids) => ids.reduce((sum, id) => sum + (byId.get(id)?.length ?? 0), 0);
5129
+ const reference = graph.referencePath ? graph.paths.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
5130
+ return {
5131
+ name,
5132
+ nodeIds,
5133
+ edgeIndexes,
5134
+ steps: path.nodeIds.length,
5135
+ bp: bpOf(path.nodeIds),
5136
+ referenceBp: reference && reference !== path ? bpOf(reference.nodeIds) : void 0
5137
+ };
5138
+ }
5139
+
5140
+ // src/bubbles/bubbleLine.ts
5141
+ var SATURATED_PATH_COUNT = 2147483647;
5142
+
5143
+ // src/bubbles/bubblesFromGraph.ts
5144
+ function bubblesFromGraph(graph) {
5145
+ if (!graph.nodes.some(isBackbone)) {
5146
+ return [];
5147
+ }
5148
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
5149
+ const order = referenceOrder(graph);
5150
+ const indexOf = new Map(order.map((id, i) => [id, i]));
5151
+ const reach = new Int32Array(order.length + 1);
5152
+ const succ = /* @__PURE__ */ new Map();
5153
+ const linked = /* @__PURE__ */ new Set();
5154
+ for (const e of graph.edges) {
5155
+ const a = indexOf.get(e.from);
5156
+ const b = indexOf.get(e.to);
5157
+ if (a === void 0 || b === void 0 || a === b) {
5158
+ continue;
5159
+ }
5160
+ linked.add(`${e.from}>${e.to}`).add(`${e.to}>${e.from}`);
5161
+ const [lo, hi] = a < b ? [a, b] : [b, a];
5162
+ reach[lo + 1]++;
5163
+ reach[hi]--;
5164
+ const [from, to] = a < b ? [e.from, e.to] : [e.to, e.from];
5165
+ (succ.get(from) ?? succ.set(from, /* @__PURE__ */ new Set()).get(from)).add(to);
5166
+ }
5167
+ for (let i = 1; i < reach.length; i++) {
5168
+ reach[i] += reach[i - 1];
5169
+ }
5170
+ const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
5171
+ const ends = /* @__PURE__ */ new Set([backbone[0].id, backbone.at(-1).id]);
5172
+ 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);
5173
+ const walkIndex = graph.paths?.map((p) => {
5174
+ const at = /* @__PURE__ */ new Map();
5175
+ p.nodeIds.forEach((id, i) => {
5176
+ if (!at.has(id)) {
5177
+ at.set(id, i);
4958
5178
  }
4959
- deletionCandidates.push({
4960
- label: {
4961
- key: `del:${deletion.edgeIndex}`,
4962
- text,
4963
- x,
4964
- y,
4965
- kind: "deletion",
4966
- leader
4967
- },
4968
- box: boxAt(halfW, x, y)
4969
- });
5179
+ });
5180
+ return at;
5181
+ });
5182
+ const bubbles = [];
5183
+ for (let i = 0; i + 1 < boundaries.length; i++) {
5184
+ const [start, i0] = boundaries[i];
5185
+ const [end, i1] = boundaries[i + 1];
5186
+ const interior = order.slice(i0 + 1, i1);
5187
+ if (interior.length === 0) {
5188
+ continue;
4970
5189
  }
5190
+ const best = /* @__PURE__ */ new Map([[start.id, { min: 0, max: 0, n: 1 }]]);
5191
+ for (const id of [start.id, ...interior]) {
5192
+ const cur = best.get(id);
5193
+ if (!cur) {
5194
+ continue;
5195
+ }
5196
+ for (const t of succ.get(id) ?? []) {
5197
+ const ti = indexOf.get(t);
5198
+ if (ti > i1) {
5199
+ continue;
5200
+ }
5201
+ const add = t === end.id ? 0 : byId.get(t).length;
5202
+ const prev = best.get(t);
5203
+ best.set(t, {
5204
+ min: Math.min(prev?.min ?? Infinity, cur.min + add),
5205
+ max: Math.max(prev?.max ?? -Infinity, cur.max + add),
5206
+ n: Math.min(SATURATED_PATH_COUNT, cur.n + (prev?.n ?? 0))
5207
+ });
5208
+ }
5209
+ }
5210
+ const walked = walkIndex && walkRoutes(graph, byId, walkIndex, start.id, end.id);
5211
+ const crossed = walked?.n ? walked : void 0;
5212
+ const routes2 = crossed ?? best.get(end.id) ?? { min: 0, max: 0, n: 0 };
5213
+ const walksLeave = walked !== void 0 && walked.left > 0;
5214
+ const refStart = start.stable.start + start.length;
5215
+ const refEnd = end.stable.start;
5216
+ const chain = [
5217
+ start.id,
5218
+ ...interior.filter((id) => isBackbone(byId.get(id))),
5219
+ end.id
5220
+ ];
5221
+ const chainBroken = chain.some(
5222
+ (id, k) => k > 0 && !linked.has(`${chain[k - 1]}>${id}`)
5223
+ );
5224
+ const partial = chainBroken || walksLeave;
5225
+ const fallback = chainBroken && !crossed;
5226
+ bubbles.push({
5227
+ refName: start.stable.refName,
5228
+ start: refStart,
5229
+ end: refEnd,
5230
+ segmentCount: interior.length + 2,
5231
+ pathCount: routes2.n,
5232
+ // a real inversion test needs each walk's direction relative to the
5233
+ // reference; the first visit's strand is just whichever path anchored the
5234
+ // node
5235
+ inversion: false,
5236
+ shortestAlleleLength: fallback ? refEnd - refStart : routes2.min,
5237
+ longestAlleleLength: fallback ? refEnd - refStart : routes2.max,
5238
+ segments: [start, ...interior.map((id) => byId.get(id)), end].map((n) => n.name).join(","),
5239
+ shortestAllele: void 0,
5240
+ longestAllele: void 0,
5241
+ partial,
5242
+ routes: crossed?.routes
5243
+ });
5244
+ }
5245
+ return bubbles;
5246
+ }
5247
+ function walkRoutes(graph, byId, walkIndex, startId, endId) {
5248
+ const seen = /* @__PURE__ */ new Map();
5249
+ let min = Infinity;
5250
+ let max = -Infinity;
5251
+ let left = 0;
5252
+ graph.paths.forEach((p, k) => {
5253
+ const i0 = walkIndex[k].get(startId);
5254
+ const i1 = walkIndex[k].get(endId);
5255
+ if (i0 === void 0 || i1 === void 0) {
5256
+ if (i0 !== void 0 || i1 !== void 0) {
5257
+ left++;
5258
+ }
5259
+ return;
5260
+ }
5261
+ const steps = p.nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
5262
+ if (i1 < i0) {
5263
+ steps.reverse();
5264
+ }
5265
+ let bp = 0;
5266
+ for (const id of steps) {
5267
+ bp += byId.get(id)?.length ?? 0;
5268
+ }
5269
+ const key = steps.join(",");
5270
+ const route = seen.get(key) ?? { steps, bp, walks: [] };
5271
+ route.walks.push(p.name);
5272
+ seen.set(key, route);
5273
+ min = Math.min(min, bp);
5274
+ max = Math.max(max, bp);
5275
+ });
5276
+ return seen.size || left ? { min, max, n: seen.size, left, routes: [...seen.values()] } : void 0;
5277
+ }
5278
+
5279
+ // src/bubbles/classifyBubble.ts
5280
+ var REPEAT_GROWTH = 10;
5281
+ function coveringRepeat(b, repeats) {
5282
+ const span = b.end - b.start;
5283
+ const growth = b.longestAlleleLength - b.shortestAlleleLength;
5284
+ return repeats.find(
5285
+ (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)
5286
+ );
5287
+ }
5288
+ var SUPERBUBBLE_SEGMENTS = 40;
5289
+ function formatBp2(bp) {
5290
+ if (bp >= 1e4) {
5291
+ return `${(bp / 1e3).toFixed(0)} kb`;
5292
+ }
5293
+ if (bp >= 1e3) {
5294
+ return `${(bp / 1e3).toFixed(1)} kb`;
5295
+ }
5296
+ return `${bp} bp`;
5297
+ }
5298
+ function formatBpRange(shortest, longest) {
5299
+ const low = formatBp2(shortest);
5300
+ const high = formatBp2(longest);
5301
+ const unit = high.slice(high.indexOf(" "));
5302
+ return low.endsWith(unit) ? `${low.slice(0, -unit.length)}\u2013${high}` : `${low}\u2013${high}`;
5303
+ }
5304
+ function routes(count) {
5305
+ if (count >= SATURATED_PATH_COUNT) {
5306
+ return "\u22652.1B routes";
5307
+ }
5308
+ if (count >= 1e5) {
5309
+ return `${(count / 1e3).toFixed(0)}k routes`;
5310
+ }
5311
+ return `${count} routes`;
5312
+ }
5313
+ function classifyBubble(b, repeats = []) {
5314
+ const c = classifyShape(b, repeats);
5315
+ return b.partial ? { ...c, label: `${c.label}, partial` } : c;
5316
+ }
5317
+ function classifyShape(b, repeats) {
5318
+ const refSpan = b.end - b.start;
5319
+ const {
5320
+ shortestAlleleLength: shortest,
5321
+ longestAlleleLength: longest,
5322
+ pathCount,
5323
+ segmentCount,
5324
+ inversion
5325
+ } = b;
5326
+ const range = formatBpRange(shortest, longest);
5327
+ if (segmentCount >= SUPERBUBBLE_SEGMENTS) {
5328
+ return {
5329
+ kind: "superbubble",
5330
+ label: `${range} superbubble, ${segmentCount} segments, ${routes(pathCount)}${inversion ? ", inv" : ""}`
5331
+ };
4971
5332
  }
4972
- const labelledByDeletion = /* @__PURE__ */ new Set();
4973
- const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
4974
- for (const run of alleleRuns) {
4975
- const drawn = runBounds(run.nodeIds, nodePositions);
4976
- if (!drawn) {
4977
- continue;
4978
- }
4979
- const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
4980
- const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
4981
- const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
4982
- const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
4983
- if (shownRight < shownLeft || shownBottom < shownTop) {
4984
- continue;
4985
- }
4986
- const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
4987
- const text = deletionText(run.bp);
4988
- const halfW = labelHalfWidth(text);
4989
- if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
4990
- for (const id of run.nodeIds) {
4991
- labelledByDeletion.add(id);
4992
- }
4993
- const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
4994
- const y = clamp(
4995
- (shownTop + shownBottom) / 2,
4996
- LABEL_HALF_HEIGHT,
4997
- height - LABEL_HALF_HEIGHT
4998
- );
4999
- deletionCandidates.push({
5000
- label: {
5001
- key: `alleledel:${run.nodeIds.join(",")}`,
5002
- text,
5003
- x,
5004
- y,
5005
- kind: "deletion"
5006
- },
5007
- box: boxAt(halfW, x, y)
5008
- });
5009
- }
5333
+ if (inversion) {
5334
+ return { kind: "inversion", label: `${formatBp2(longest)} inv` };
5010
5335
  }
5011
- for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
5012
- if (labelledByDeletion.has(id)) {
5013
- continue;
5336
+ const repeat = longest !== shortest ? coveringRepeat(b, repeats) : void 0;
5337
+ if (repeat) {
5338
+ const name = repeat.name ? ` (${repeat.name})` : "";
5339
+ return {
5340
+ kind: "repeat",
5341
+ label: `${range} repeat array${name}, ${routes(pathCount)}`
5342
+ };
5343
+ }
5344
+ const alleles = pathCount > 2 ? `, ${pathCount} alleles` : "";
5345
+ if (shortest === longest && shortest === refSpan) {
5346
+ return refSpan <= 1 ? { kind: "snp", label: "SNP" } : { kind: "substitution", label: `${formatBp2(refSpan)} sub` };
5347
+ }
5348
+ if (refSpan === 0 || shortest === refSpan) {
5349
+ return {
5350
+ kind: "insertion",
5351
+ label: `\u2264${formatBp2(longest - refSpan)} ins${alleles}`
5352
+ };
5353
+ }
5354
+ if (longest === refSpan) {
5355
+ return {
5356
+ kind: "deletion",
5357
+ label: `${formatBp2(refSpan - shortest)} del${alleles}`
5358
+ };
5359
+ }
5360
+ return {
5361
+ kind: "complex",
5362
+ label: `${formatBp2(refSpan)} ref \u2192 ${range}, ${pathCount} alleles`
5363
+ };
5364
+ }
5365
+ var BUBBLE_KIND_COLORS = {
5366
+ snp: "#5b6b7a",
5367
+ substitution: "#5b6b7a",
5368
+ insertion: "#2f8fd6",
5369
+ deletion: "#c94040",
5370
+ inversion: "#e07b00",
5371
+ repeat: "#8e3fbf",
5372
+ superbubble: "#8e3fbf",
5373
+ complex: "#8e3fbf"
5374
+ };
5375
+ var BUBBLE_KIND_NAMES = {
5376
+ snp: "SNP",
5377
+ substitution: "substitution",
5378
+ insertion: "insertion",
5379
+ deletion: "deletion",
5380
+ inversion: "inversion",
5381
+ repeat: "repeat array",
5382
+ superbubble: "superbubble",
5383
+ complex: "site"
5384
+ };
5385
+ function bubbleSegmentIds(b) {
5386
+ return b.segments.split(",").filter(Boolean);
5387
+ }
5388
+
5389
+ // src/bubbles/bubbleHalos.ts
5390
+ var WHOLE_FRACTION = 0.9;
5391
+ var NAMED_WALKS = 2;
5392
+ var MIN_CHIPPED_BP = 1e3;
5393
+ function bubbleHalos(graph, bubbles, positions, walkLabel = (name) => name, repeats = []) {
5394
+ const byName = new Map(graph.nodes.map((n) => [n.name, n]));
5395
+ const halos = [];
5396
+ for (const bubble of bubbles) {
5397
+ const parts = [];
5398
+ let minX = Infinity;
5399
+ let maxX = -Infinity;
5400
+ let minY = Infinity;
5401
+ const nodeIds = [];
5402
+ const ends = [];
5403
+ for (const name of bubbleSegmentIds(bubble)) {
5404
+ const node = byName.get(name);
5405
+ const line = node && positions[node.id];
5406
+ if (!node || !line?.length) {
5407
+ continue;
5408
+ }
5409
+ if (isBackbone(node) && (node.stable.start < bubble.start || node.stable.start >= bubble.end)) {
5410
+ ends.push(line[Math.floor(line.length / 2)]);
5411
+ continue;
5412
+ }
5413
+ nodeIds.push(node.id);
5414
+ parts.push(svgPath(line.length === 1 ? [line[0], line[0]] : line));
5415
+ for (const p of line) {
5416
+ minX = Math.min(minX, p.x);
5417
+ maxX = Math.max(maxX, p.x);
5418
+ minY = Math.min(minY, p.y);
5419
+ }
5014
5420
  }
5015
- const { x, y } = polylineMidpoint(segments);
5016
- const screenY = y * scaleY + translateY;
5017
- if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
5421
+ if (nodeIds.length === 0) {
5018
5422
  continue;
5019
5423
  }
5020
- const drawn = drawnLength(segments, scaleX, scaleY);
5021
- if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
5022
- continue;
5424
+ const labelAt = { x: (minX + maxX) / 2, y: minY };
5425
+ const anchor = ends.length ? {
5426
+ x: ends.reduce((s, p) => s + p.x, 0) / ends.length,
5427
+ y: ends.reduce((s, p) => s + p.y, 0) / ends.length
5428
+ } : labelAt;
5429
+ const sharing = /* @__PURE__ */ new Map();
5430
+ for (const route of bubble.routes ?? []) {
5431
+ for (const id of new Set(route.steps)) {
5432
+ sharing.set(id, (sharing.get(id) ?? 0) + 1);
5433
+ }
5023
5434
  }
5024
- const text = formatBp2(nodeLengths.get(id));
5025
- const halfW = labelHalfWidth(text);
5026
- if (drawn >= halfW * 2 / MAX_LABEL_OVERHANG) {
5027
- const screenX = x * scaleX + translateX;
5028
- nodeCandidates.push({
5029
- label: {
5030
- key: `node:${id}`,
5031
- text,
5032
- x: screenX,
5033
- y: screenY,
5034
- kind: "node"
5035
- },
5036
- box: boxAt(halfW, screenX, screenY)
5037
- });
5435
+ const routes2 = [];
5436
+ for (const route of bubble.routes ?? []) {
5437
+ if (bubble.longestAlleleLength < MIN_CHIPPED_BP) {
5438
+ break;
5439
+ }
5440
+ if (route.steps.length === 0) {
5441
+ continue;
5442
+ }
5443
+ const rarest = Math.min(...route.steps.map((id) => sharing.get(id)));
5444
+ let at;
5445
+ let far = -1;
5446
+ for (const id of route.steps) {
5447
+ if (sharing.get(id) !== rarest) {
5448
+ continue;
5449
+ }
5450
+ for (const p of positions[id] ?? []) {
5451
+ const d = Math.hypot(p.x - anchor.x, p.y - anchor.y);
5452
+ if (d > far) {
5453
+ far = d;
5454
+ at = p;
5455
+ }
5456
+ }
5457
+ }
5458
+ if (at) {
5459
+ routes2.push({ route, at, text: routeText(route, walkLabel) });
5460
+ }
5038
5461
  }
5462
+ halos.push({
5463
+ bubble,
5464
+ ...classifyBubble(bubble, repeats),
5465
+ path: parts.join(""),
5466
+ labelAt,
5467
+ members: nodeIds.length,
5468
+ nodeIds,
5469
+ whole: nodeIds.length >= WHOLE_FRACTION * graph.nodes.length,
5470
+ routes: routes2
5471
+ });
5039
5472
  }
5040
- return { deletions: deletionCandidates, nodes: nodeCandidates };
5473
+ return halos;
5041
5474
  }
5042
- function nodeLabelBudget(width, height) {
5043
- return Math.max(
5044
- MIN_NODE_LABELS,
5045
- Math.floor(width * height / PX_PER_NODE_LABEL)
5046
- );
5475
+ function routeText(route, walkLabel) {
5476
+ const names = [...new Set(route.walks.map(walkLabel))].sort();
5477
+ const shown = names.slice(0, NAMED_WALKS).join(", ");
5478
+ const more = names.length > NAMED_WALKS ? ` +${names.length - NAMED_WALKS}` : "";
5479
+ return `${shown}${more} \xB7 ${formatBp2(route.bp)}`;
5047
5480
  }
5048
- function placeSizeLabels(candidates, take, budget = Infinity) {
5049
- const labels = [];
5050
- for (const { label, box } of candidates) {
5051
- if (labels.length >= budget) {
5052
- break;
5053
- }
5054
- if (take(box)) {
5055
- labels.push(label);
5056
- }
5057
- }
5058
- return labels;
5481
+
5482
+ // src/bubbles/popBubble.ts
5483
+ function bubbleSubgraph(graph, segmentIds) {
5484
+ const keep = new Set(segmentIds);
5485
+ const nodes = graph.nodes.filter((n) => keep.has(n.name));
5486
+ const ids = new Set(nodes.map((n) => n.id));
5487
+ const paths = graph.paths?.map((p) => ({ ...p, nodeIds: p.nodeIds.filter((id) => ids.has(id)) })).filter((p) => p.nodeIds.length > 0);
5488
+ return {
5489
+ name: graph.name,
5490
+ nodes,
5491
+ edges: graph.edges.filter((e) => ids.has(e.from) && ids.has(e.to)),
5492
+ ...paths?.length ? { paths } : {},
5493
+ anchorPaths: graph.anchorPaths,
5494
+ pathVisits: graph.pathVisits,
5495
+ anchoredBy: graph.anchoredBy,
5496
+ referencePath: graph.referencePath
5497
+ };
5059
5498
  }
5060
5499
 
5061
5500
  // src/labelLayout.ts
@@ -5202,6 +5641,87 @@ function layoutLabels(m) {
5202
5641
  };
5203
5642
  }
5204
5643
 
5644
+ // src/genes/genePins.ts
5645
+ function genePins(graph, genes, positions) {
5646
+ const byRefName = /* @__PURE__ */ new Map();
5647
+ const refNames = /* @__PURE__ */ new Set();
5648
+ for (const node of graph.nodes) {
5649
+ if (isBackbone(node)) {
5650
+ const { refName } = node.stable;
5651
+ refNames.add(refName);
5652
+ if (positions[node.id]?.length) {
5653
+ const entry = byRefName.get(refName) ?? byRefName.set(refName, { nodes: [], reach: 0 }).get(refName);
5654
+ entry.nodes.push(node);
5655
+ entry.reach = Math.max(entry.reach, node.length);
5656
+ }
5657
+ }
5658
+ }
5659
+ for (const { nodes } of byRefName.values()) {
5660
+ nodes.sort((a, b) => a.stable.start - b.stable.start);
5661
+ }
5662
+ const bind = refNameBinding(refNames);
5663
+ const pins = [];
5664
+ for (const gene of genes) {
5665
+ const refName = bind(gene.refName);
5666
+ const backbone = refName === void 0 ? void 0 : byRefName.get(refName);
5667
+ if (!backbone) {
5668
+ continue;
5669
+ }
5670
+ const parts = [];
5671
+ let at;
5672
+ let atDistance = Infinity;
5673
+ let covered = 0;
5674
+ const mid = (gene.start + gene.end) / 2;
5675
+ const { nodes, reach } = backbone;
5676
+ for (let i = firstNodeAtOrAfter(nodes, gene.start - reach); i < nodes.length && nodes[i].stable.start < gene.end; i++) {
5677
+ const node = nodes[i];
5678
+ const nodeStart = node.stable.start;
5679
+ const nodeEnd = nodeStart + node.length;
5680
+ if (nodeEnd <= gene.start) {
5681
+ continue;
5682
+ }
5683
+ const line = positions[node.id];
5684
+ covered += Math.min(nodeEnd, gene.end) - Math.max(nodeStart, gene.start);
5685
+ for (const exon of gene.exons) {
5686
+ const a = Math.max(exon.start, nodeStart);
5687
+ const b = Math.min(exon.end, nodeEnd);
5688
+ if (b <= a) {
5689
+ continue;
5690
+ }
5691
+ const stretch = polylineSlice(
5692
+ line,
5693
+ (a - nodeStart) / node.length,
5694
+ (b - nodeStart) / node.length
5695
+ );
5696
+ if (stretch.length === 1) {
5697
+ stretch.push({ ...stretch[0] });
5698
+ }
5699
+ parts.push(svgPath(stretch));
5700
+ }
5701
+ const pinBp = Math.min(Math.max(mid, nodeStart), nodeEnd);
5702
+ const distance = Math.abs(pinBp - mid);
5703
+ if (distance < atDistance) {
5704
+ atDistance = distance;
5705
+ const [p] = polylineSlice(
5706
+ line,
5707
+ (pinBp - nodeStart) / node.length,
5708
+ (pinBp - nodeStart) / node.length
5709
+ );
5710
+ at = p;
5711
+ }
5712
+ }
5713
+ if (at) {
5714
+ pins.push({
5715
+ gene,
5716
+ exons: parts.join(""),
5717
+ at,
5718
+ covered: covered / (gene.end - gene.start)
5719
+ });
5720
+ }
5721
+ }
5722
+ return pins;
5723
+ }
5724
+
5205
5725
  // src/gbzWindow.ts
5206
5726
  var NoReferenceSampleError = class extends Error {
5207
5727
  constructor(anchor, referenceSamples) {
@@ -5229,16 +5749,9 @@ function haplotypeWanted(name, wanted) {
5229
5749
  async function referenceSamplesOf(db) {
5230
5750
  return (await db.tag("gbwt_reference_samples") ?? "").split(/\s+/).filter((sample) => sample !== "");
5231
5751
  }
5232
- var SAMPLE_ALIASES = {
5233
- hg38: "grch38",
5234
- hg19: "grch37",
5235
- hs1: "chm13",
5236
- "t2t-chm13": "chm13",
5237
- chm13v2: "chm13"
5238
- };
5239
5752
  function aliasedSample(anchorSample, referenceSamples) {
5240
5753
  const lower = anchorSample.toLowerCase();
5241
- const alias = SAMPLE_ALIASES[lower];
5754
+ const alias = wellKnownSample(anchorSample)?.toLowerCase();
5242
5755
  return referenceSamples.find((s) => s === anchorSample) ?? referenceSamples.find((s) => s.toLowerCase() === lower) ?? referenceSamples.find((s) => s.toLowerCase() === alias);
5243
5756
  }
5244
5757
  function resolveReferenceSample({
@@ -5353,7 +5866,9 @@ export {
5353
5866
  NODE_WIDTHS,
5354
5867
  REFERENCE_RAMP_MAX_HUE,
5355
5868
  ROW_HEIGHT_PX,
5869
+ WELL_KNOWN_SAMPLES,
5356
5870
  axisScaleOf,
5871
+ backboneAssembly,
5357
5872
  bubbleHalos,
5358
5873
  bubbleSegmentIds,
5359
5874
  bubbleSubgraph,
@@ -5362,19 +5877,26 @@ export {
5362
5877
  clampZoom,
5363
5878
  classifyBubble,
5364
5879
  computeReferenceRamp,
5880
+ connectorAt,
5365
5881
  contains,
5366
5882
  cutWindowGFA,
5367
5883
  deletionEdges,
5368
5884
  drawTubeMap,
5885
+ drawTubeMapConnectors,
5886
+ drawTubeMapGenes,
5887
+ drawTubeMapRuler,
5369
5888
  drawingBounds,
5370
5889
  engineKey,
5371
5890
  engineRequest,
5891
+ featuresOnBackbone,
5372
5892
  findHoveredEdge,
5373
5893
  findHoveredNode,
5374
5894
  fitTransform,
5375
5895
  forceLayout,
5376
- formatBp2 as formatBp,
5896
+ formatBp,
5897
+ genePins,
5377
5898
  getDpr,
5899
+ graphBackbone,
5378
5900
  haplotypeWanted,
5379
5901
  layoutExtent,
5380
5902
  layoutLabels,
@@ -5392,20 +5914,28 @@ export {
5392
5914
  panSNSample,
5393
5915
  pathColorsLegible,
5394
5916
  pathLegend,
5917
+ refNameBinding,
5918
+ referenceBoxes,
5395
5919
  referenceKnots,
5920
+ referenceNodes,
5396
5921
  referencePathQuery,
5397
5922
  referenceSamplesOf,
5398
5923
  resolveColorScheme,
5399
5924
  resolveReferenceSample,
5925
+ rulerBoxes,
5400
5926
  screenToLayout,
5927
+ tubeMapConnectors,
5401
5928
  tubeMapFrame,
5929
+ tubeMapGenes,
5402
5930
  tubeMapNodeAt,
5403
5931
  tubeMapPicture,
5932
+ tubeX,
5404
5933
  viewportOf,
5405
5934
  walkHighlight,
5406
5935
  walkRows,
5407
5936
  walkRowsExtent,
5408
5937
  warpX,
5938
+ wellKnownSample,
5409
5939
  wheelZoomFactor,
5410
5940
  zoomAbout
5411
5941
  };