@jbrowse/bandage-core 0.1.5 → 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
@@ -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;
@@ -975,18 +975,21 @@ function fittedTranslateY(bounds, usableHeight, yScale) {
975
975
  const leftoverY = usableHeight - bounds.h * yScale;
976
976
  return FIT_PADDING - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
977
977
  }
978
- function fitTransform(bounds, paneWidth, paneHeight, pixelRows) {
979
- 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;
980
980
  const usableHeight = paneHeight - FIT_PADDING * 2;
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,
992
+ translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
990
993
  translateY: fittedTranslateY(bounds, usableHeight, pixelRows ? 1 : scale)
991
994
  };
992
995
  }
@@ -2010,7 +2013,6 @@ function walkRows(graph, region, unit) {
2010
2013
  return void 0;
2011
2014
  }
2012
2015
  const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
2013
- const onReference = new Set(reference.nodeIds);
2014
2016
  const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
2015
2017
  const cut = region && region.end > region.start ? region : void 0;
2016
2018
  const span = /* @__PURE__ */ new Map();
@@ -2028,17 +2030,34 @@ function walkRows(graph, region, unit) {
2028
2030
  const runs = [];
2029
2031
  let bp = 0;
2030
2032
  let offReferenceBp = 0;
2033
+ let step = 0;
2031
2034
  for (const id of ids) {
2032
2035
  const len = lengthOf.get(id) ?? 0;
2033
- const shared = onReference.has(id);
2036
+ const s = span.get(id);
2034
2037
  const last = runs.at(-1);
2035
- 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) {
2036
2044
  last.bp += len;
2045
+ if (!forward2) {
2046
+ last.referenceStart = s.start;
2047
+ last.reversed = true;
2048
+ }
2049
+ step = forward2 ? 1 : -1;
2037
2050
  } else {
2038
- 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;
2039
2058
  }
2040
2059
  bp += len;
2041
- if (!shared) {
2060
+ if (!s) {
2042
2061
  offReferenceBp += len;
2043
2062
  }
2044
2063
  }
@@ -2574,9 +2593,9 @@ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY,
2574
2593
  offsetY
2575
2594
  );
2576
2595
  }
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);
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);
2580
2599
  const fromEnd = fromAttach.at;
2581
2600
  const toStart = toAttach.at;
2582
2601
  const p1x = fromEnd.x + offsetX;
@@ -3066,7 +3085,9 @@ function hashColor(str, alpha) {
3066
3085
  var REFERENCE_RAMP_MAX_HUE = 300;
3067
3086
  var REFERENCE_RAMP_SATURATION = 0.7;
3068
3087
  var REFERENCE_RAMP_LIGHTNESS = 0.5;
3069
- 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(", ")})`;
3070
3091
  function computeReferenceRamp(graph, domain) {
3071
3092
  const midpoints = referenceMidpoints(graph);
3072
3093
  if (domain && domain.end > domain.start) {
@@ -4123,6 +4144,7 @@ function tubeMapPicture(layout) {
4123
4144
  }
4124
4145
  });
4125
4146
  return {
4147
+ bounds: layout.bounds,
4126
4148
  layers: [layerOf(layout, "haplotype"), layerOf(layout, "read")],
4127
4149
  nodes,
4128
4150
  mismatches: tubeMapMismatches(layout).filter(
@@ -4193,6 +4215,11 @@ function fillByColor(ctx, items, visible, addTo) {
4193
4215
  }
4194
4216
  ctx.globalAlpha = 1;
4195
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
+ }
4196
4223
  function drawTubeMap(ctx, picture, frame) {
4197
4224
  const { x, y, width } = frame;
4198
4225
  const visible = (item) => x(item.x1) >= 0 && x(item.x0) <= width;
@@ -4217,7 +4244,9 @@ function drawTubeMap(ctx, picture, frame) {
4217
4244
  ctx.fillStyle = lit ? "rgba(255,192,203,0.5)" : fill;
4218
4245
  ctx.strokeStyle = lit ? "#ff0000" : stroke;
4219
4246
  ctx.fill();
4247
+ ctx.globalAlpha = lit ? 1 : outlineAlpha(x(node.x1) - x(node.x0));
4220
4248
  ctx.stroke();
4249
+ ctx.globalAlpha = 1;
4221
4250
  }
4222
4251
  }
4223
4252
  drawMismatches(ctx, picture.mismatches, frame);
@@ -4350,720 +4379,1122 @@ function tubeMapNodeAt(drawing, { x, y }, sx, sy) {
4350
4379
  return hit;
4351
4380
  }
4352
4381
 
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);
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;
4372
4403
  }
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
4404
+ placed.push(box);
4405
+ return true;
4383
4406
  };
4384
4407
  }
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;
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;
4441
4424
  }
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
- });
4425
+ if (insideFrame(box, frame) && take(box)) {
4426
+ out.push({ ...c, y, w });
4427
+ break;
4454
4428
  }
4455
4429
  }
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
4430
  }
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;
4431
+ return out;
4523
4432
  }
4524
4433
 
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`;
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;
4543
4446
  }
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}`;
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
+ };
4549
4456
  }
4550
- function routes(count) {
4551
- if (count >= SATURATED_PATH_COUNT) {
4552
- return "\u22652.1B routes";
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
+ };
4464
+ }
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`;
4553
4471
  }
4554
- if (count >= 1e5) {
4555
- return `${(count / 1e3).toFixed(0)}k routes`;
4472
+ return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
4473
+ }
4474
+ function deletionText(bp) {
4475
+ return `${formatBp(bp)} del`;
4476
+ }
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);
4556
4502
  }
4557
- return `${count} routes`;
4503
+ return total;
4558
4504
  }
4559
- function classifyBubble(b, repeats = []) {
4560
- const c = classifyShape(b, repeats);
4561
- return b.partial ? { ...c, label: `${c.label}, partial` } : c;
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
+ }
4517
+ }
4518
+ return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
4562
4519
  }
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
- };
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;
4578
4525
  }
4579
- if (inversion) {
4580
- return { kind: "inversion", label: `${formatBp(longest)} inv` };
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;
4581
4536
  }
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
- };
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
+ }
4589
4551
  }
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` };
4552
+ arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
4553
+ return arcs;
4554
+ }
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
+ };
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
+ });
4614
+ }
4593
4615
  }
4594
- if (refSpan === 0 || shortest === refSpan) {
4595
- return {
4596
- kind: "insertion",
4597
- label: `\u2264${formatBp(longest - refSpan)} ins${alleles}`
4598
- };
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) {
4621
+ continue;
4622
+ }
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;
4629
+ }
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);
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
+ });
4653
+ }
4599
4654
  }
4600
- if (longest === refSpan) {
4601
- return {
4602
- kind: "deletion",
4603
- label: `${formatBp(refSpan - shortest)} del${alleles}`
4604
- };
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 };
4685
+ }
4686
+ function nodeLabelBudget(width, height) {
4687
+ return Math.max(
4688
+ MIN_NODE_LABELS,
4689
+ Math.floor(width * height / PX_PER_NODE_LABEL)
4690
+ );
4691
+ }
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;
4703
+ }
4704
+
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
+ });
4720
+ }
4721
+ });
4722
+ for (const boxes of byRefName.values()) {
4723
+ boxes.sort((a, b) => a.bp0 - b.bp0);
4724
+ }
4725
+ return byRefName;
4726
+ }
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;
4737
+ }
4738
+ }
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;
4745
+ }
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) };
4750
+ }
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;
4759
+ }
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
+ }
4605
4823
  }
4606
4824
  return {
4607
- kind: "complex",
4608
- label: `${formatBp(refSpan)} ref \u2192 ${range}, ${pathCount} alleles`
4825
+ from,
4826
+ to,
4827
+ ticks,
4828
+ squeezed,
4829
+ labels: placed.map(({ text, at }) => ({ text, at }))
4609
4830
  };
4610
4831
  }
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);
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);
4633
4839
  }
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);
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);
4868
+ }
4869
+ }
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
+ });
4890
+ }
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
+ });
4665
4908
  }
4666
4909
  }
4667
- if (nodeIds.length === 0) {
4910
+ }
4911
+ const contigs = [...spans.values()];
4912
+ return contigs.length ? { contigs, prefixes: sharedPrefixes(contigs.map((c) => c.refName)) } : void 0;
4913
+ }
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)));
4919
+ }
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);
4928
+ }
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) {
4668
4947
  continue;
4669
4948
  }
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
- }
4680
- }
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) });
4706
- }
4949
+ const start = Math.max(gene.start, first.bp0);
4950
+ const end = Math.min(gene.end, last.bp1);
4951
+ if (end <= start) {
4952
+ continue;
4707
4953
  }
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
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
+ )
4717
4960
  });
4718
4961
  }
4719
- return halos;
4720
- }
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)}`;
4962
+ return out;
4726
4963
  }
4727
-
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
- };
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);
5000
+ }
5001
+ ctx.fillText(label, mid, glyphY - EXON_PX / 2 - 2);
5002
+ }
4744
5003
  }
4745
5004
 
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;
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;
4752
5022
  }
4753
- function overlaps(a, b) {
4754
- return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
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
+ }
5037
+ }
5038
+ return out;
4755
5039
  }
4756
- function offFrame(box, frame) {
4757
- return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
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;
5044
+ }
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;
5055
+ }
5056
+ }
5057
+ return hit;
4758
5058
  }
4759
- function insideFrame(box, frame) {
4760
- return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
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
5075
+ }) {
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
+ }
4761
5105
  }
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;
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);
4767
5126
  }
4768
- placed.push(box);
4769
- return true;
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
4770
5137
  };
4771
5138
  }
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;
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);
4788
5178
  }
4789
- if (insideFrame(box, frame) && take(box)) {
4790
- out.push({ ...c, y, w });
4791
- break;
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;
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
+ });
4792
5208
  }
4793
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
+ });
4794
5244
  }
4795
- return out;
4796
- }
4797
-
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
- };
5245
+ return bubbles;
4820
5246
  }
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
- };
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;
4828
5277
  }
4829
- function clamp(v, lo, hi) {
4830
- return hi < lo ? v : Math.min(Math.max(v, lo), hi);
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
+ );
4831
5287
  }
5288
+ var SUPERBUBBLE_SEGMENTS = 40;
4832
5289
  function formatBp2(bp) {
4833
- if (bp >= 1e6) {
4834
- return `${+(bp / 1e6).toFixed(1)} Mb`;
5290
+ if (bp >= 1e4) {
5291
+ return `${(bp / 1e3).toFixed(0)} kb`;
4835
5292
  }
4836
- return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
4837
- }
4838
- function deletionText(bp) {
4839
- return `${formatBp2(bp)} del`;
5293
+ if (bp >= 1e3) {
5294
+ return `${(bp / 1e3).toFixed(1)} kb`;
5295
+ }
5296
+ return `${bp} bp`;
4840
5297
  }
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 };
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}`;
4848
5303
  }
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;
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`;
4851
5312
  }
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
- );
5313
+ function classifyBubble(b, repeats = []) {
5314
+ const c = classifyShape(b, repeats);
5315
+ return b.partial ? { ...c, label: `${c.label}, partial` } : c;
4859
5316
  }
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);
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
+ };
4866
5332
  }
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
- }
5333
+ if (inversion) {
5334
+ return { kind: "inversion", label: `${formatBp2(longest)} inv` };
4881
5335
  }
4882
- return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
4883
- }
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;
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
+ };
4889
5343
  }
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;
4893
- }
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;
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` };
4900
5347
  }
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
- }
5348
+ if (refSpan === 0 || shortest === refSpan) {
5349
+ return {
5350
+ kind: "insertion",
5351
+ label: `\u2264${formatBp2(longest - refSpan)} ins${alleles}`
5352
+ };
4915
5353
  }
4916
- arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
4917
- return arcs;
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);
4918
5387
  }
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
- };
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;
4966
5408
  }
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
- }
4979
- }
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);
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);
5000
5419
  }
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
- }
5018
- }
5019
- for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
5020
- if (labelledByDeletion.has(id)) {
5021
- continue;
5022
5420
  }
5023
- const { x, y } = polylineMidpoint(segments);
5024
- const screenY = y * scaleY + translateY;
5025
- if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
5421
+ if (nodeIds.length === 0) {
5026
5422
  continue;
5027
5423
  }
5028
- const drawn = drawnLength(segments, scaleX, scaleY);
5029
- if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
5030
- 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
+ }
5031
5434
  }
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
- });
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
+ }
5046
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
+ });
5047
5472
  }
5048
- return { deletions: deletionCandidates, nodes: nodeCandidates };
5473
+ return halos;
5049
5474
  }
5050
- function nodeLabelBudget(width, height) {
5051
- return Math.max(
5052
- MIN_NODE_LABELS,
5053
- Math.floor(width * height / PX_PER_NODE_LABEL)
5054
- );
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)}`;
5055
5480
  }
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;
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
+ };
5067
5498
  }
5068
5499
 
5069
5500
  // src/labelLayout.ts
@@ -5211,27 +5642,28 @@ function layoutLabels(m) {
5211
5642
  }
5212
5643
 
5213
5644
  // src/genes/genePins.ts
5214
- function contig(name) {
5215
- return name.split("#").at(-1);
5216
- }
5217
5645
  function genePins(graph, genes, positions) {
5218
- const byContig = /* @__PURE__ */ new Map();
5219
- const contigOf = /* @__PURE__ */ new Map();
5646
+ const byRefName = /* @__PURE__ */ new Map();
5647
+ const refNames = /* @__PURE__ */ new Set();
5220
5648
  for (const node of graph.nodes) {
5221
- if (isBackbone(node) && positions[node.id]?.length) {
5649
+ if (isBackbone(node)) {
5222
5650
  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);
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
+ }
5227
5657
  }
5228
5658
  }
5229
- for (const { nodes } of byContig.values()) {
5659
+ for (const { nodes } of byRefName.values()) {
5230
5660
  nodes.sort((a, b) => a.stable.start - b.stable.start);
5231
5661
  }
5662
+ const bind = refNameBinding(refNames);
5232
5663
  const pins = [];
5233
5664
  for (const gene of genes) {
5234
- const backbone = byContig.get(contig(gene.refName));
5665
+ const refName = bind(gene.refName);
5666
+ const backbone = refName === void 0 ? void 0 : byRefName.get(refName);
5235
5667
  if (!backbone) {
5236
5668
  continue;
5237
5669
  }
@@ -5317,16 +5749,9 @@ function haplotypeWanted(name, wanted) {
5317
5749
  async function referenceSamplesOf(db) {
5318
5750
  return (await db.tag("gbwt_reference_samples") ?? "").split(/\s+/).filter((sample) => sample !== "");
5319
5751
  }
5320
- var SAMPLE_ALIASES = {
5321
- hg38: "grch38",
5322
- hg19: "grch37",
5323
- hs1: "chm13",
5324
- "t2t-chm13": "chm13",
5325
- chm13v2: "chm13"
5326
- };
5327
5752
  function aliasedSample(anchorSample, referenceSamples) {
5328
5753
  const lower = anchorSample.toLowerCase();
5329
- const alias = SAMPLE_ALIASES[lower];
5754
+ const alias = wellKnownSample(anchorSample)?.toLowerCase();
5330
5755
  return referenceSamples.find((s) => s === anchorSample) ?? referenceSamples.find((s) => s.toLowerCase() === lower) ?? referenceSamples.find((s) => s.toLowerCase() === alias);
5331
5756
  }
5332
5757
  function resolveReferenceSample({
@@ -5441,7 +5866,9 @@ export {
5441
5866
  NODE_WIDTHS,
5442
5867
  REFERENCE_RAMP_MAX_HUE,
5443
5868
  ROW_HEIGHT_PX,
5869
+ WELL_KNOWN_SAMPLES,
5444
5870
  axisScaleOf,
5871
+ backboneAssembly,
5445
5872
  bubbleHalos,
5446
5873
  bubbleSegmentIds,
5447
5874
  bubbleSubgraph,
@@ -5450,20 +5877,26 @@ export {
5450
5877
  clampZoom,
5451
5878
  classifyBubble,
5452
5879
  computeReferenceRamp,
5880
+ connectorAt,
5453
5881
  contains,
5454
5882
  cutWindowGFA,
5455
5883
  deletionEdges,
5456
5884
  drawTubeMap,
5885
+ drawTubeMapConnectors,
5886
+ drawTubeMapGenes,
5887
+ drawTubeMapRuler,
5457
5888
  drawingBounds,
5458
5889
  engineKey,
5459
5890
  engineRequest,
5891
+ featuresOnBackbone,
5460
5892
  findHoveredEdge,
5461
5893
  findHoveredNode,
5462
5894
  fitTransform,
5463
5895
  forceLayout,
5464
- formatBp2 as formatBp,
5896
+ formatBp,
5465
5897
  genePins,
5466
5898
  getDpr,
5899
+ graphBackbone,
5467
5900
  haplotypeWanted,
5468
5901
  layoutExtent,
5469
5902
  layoutLabels,
@@ -5481,20 +5914,28 @@ export {
5481
5914
  panSNSample,
5482
5915
  pathColorsLegible,
5483
5916
  pathLegend,
5917
+ refNameBinding,
5918
+ referenceBoxes,
5484
5919
  referenceKnots,
5920
+ referenceNodes,
5485
5921
  referencePathQuery,
5486
5922
  referenceSamplesOf,
5487
5923
  resolveColorScheme,
5488
5924
  resolveReferenceSample,
5925
+ rulerBoxes,
5489
5926
  screenToLayout,
5927
+ tubeMapConnectors,
5490
5928
  tubeMapFrame,
5929
+ tubeMapGenes,
5491
5930
  tubeMapNodeAt,
5492
5931
  tubeMapPicture,
5932
+ tubeX,
5493
5933
  viewportOf,
5494
5934
  walkHighlight,
5495
5935
  walkRows,
5496
5936
  walkRowsExtent,
5497
5937
  warpX,
5938
+ wellKnownSample,
5498
5939
  wheelZoomFactor,
5499
5940
  zoomAbout
5500
5941
  };