@jbrowse/bandage-core 5.0.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -297,6 +297,16 @@ function attachment(segments, side) {
297
297
  }
298
298
  var TANGENT_SPAN_FACTOR = 0.5;
299
299
  var MAX_TANGENT = 80;
300
+ function ownStrand(id) {
301
+ return id.endsWith("-") ? "-" : "+";
302
+ }
303
+ function strandSides(edge) {
304
+ const own = (id, strand) => (strand ?? ownStrand(id)) === ownStrand(id);
305
+ return {
306
+ from: own(edge.from, edge.fromStrand) ? "end" : "start",
307
+ to: own(edge.to, edge.toStrand) ? "start" : "end"
308
+ };
309
+ }
300
310
  function spanOf(segments) {
301
311
  let min = Infinity;
302
312
  let max = -Infinity;
@@ -409,6 +419,14 @@ function selfLinkOf(edge) {
409
419
  }
410
420
  return edge.fromStrand && edge.toStrand && edge.fromStrand !== edge.toStrand ? edge.fromStrand === "+" ? "end" : "start" : true;
411
421
  }
422
+ function edgeJoinOf(edge, stranded = false) {
423
+ const self = selfLinkOf(edge);
424
+ if (!stranded) {
425
+ return self;
426
+ }
427
+ const sides = strandSides(edge);
428
+ return self === false ? sides : sides.from === sides.to ? sides.from : true;
429
+ }
412
430
  function hairpinCurves(attach, reach, offsetX, offsetY) {
413
431
  const { at, inward } = attach;
414
432
  const dx = inward ? at.x - inward.x : 1;
@@ -447,7 +465,7 @@ function hairpinCurves(attach, reach, offsetX, offsetY) {
447
465
  }
448
466
  ];
449
467
  }
450
- function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY, axis, bypassed = [], maxBowIn) {
468
+ function computeEdgeCurves(fromSegments, toSegments, join, offsetX, offsetY, axis, bypassed = [], maxBowIn) {
451
469
  const scale = axis.scaleX;
452
470
  const yToX = yToXOf(axis);
453
471
  const maxBow = maxBowIn ?? (axis.pixelRows ? MAX_ROW_BOW_PX : void 0);
@@ -456,7 +474,7 @@ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY,
456
474
  computeEdgeCurves(
457
475
  scaleYOf(fromSegments, yToX),
458
476
  scaleYOf(toSegments, yToX),
459
- selfLink,
477
+ join,
460
478
  offsetX,
461
479
  offsetY * yToX,
462
480
  { scaleX: scale, scaleY: scale },
@@ -468,15 +486,16 @@ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY,
468
486
  yToX
469
487
  );
470
488
  }
471
- if (typeof selfLink === "string") {
489
+ if (typeof join === "string") {
472
490
  return hairpinCurves(
473
- attachment(fromSegments, selfLink),
491
+ attachment(fromSegments, join),
474
492
  selfLoopReach(fromSegments, scale),
475
493
  offsetX,
476
494
  offsetY
477
495
  );
478
496
  }
479
- const sides = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
497
+ const loop = join === true;
498
+ const sides = loop ? { from: "end", to: "start" } : join || facingSides(fromSegments, toSegments);
480
499
  const fromAttach = attachment(fromSegments, sides.from);
481
500
  const toAttach = attachment(toSegments, sides.to);
482
501
  const fromEnd = fromAttach.at;
@@ -485,7 +504,7 @@ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY,
485
504
  const p1y = fromEnd.y + offsetY;
486
505
  const p2x = toStart.x + offsetX;
487
506
  const p2y = toStart.y + offsetY;
488
- if (selfLink) {
507
+ if (loop) {
489
508
  let segDirX = 1;
490
509
  let segDirY = 0;
491
510
  if (fromAttach.inward) {
@@ -591,12 +610,15 @@ function nearerEnd(segments, to) {
591
610
  const end = segments.at(-1);
592
611
  return Math.hypot(start.x - to.x, start.y - to.y) < Math.hypot(end.x - to.x, end.y - to.y) ? "start" : "end";
593
612
  }
594
- function routedEdgeCurves(fromSegments, toSegments, route) {
613
+ function routedEdgeCurves(fromSegments, toSegments, route, sides) {
595
614
  const fromAttach = attachment(
596
615
  fromSegments,
597
- nearerEnd(fromSegments, route[0])
616
+ sides?.from ?? nearerEnd(fromSegments, route[0])
617
+ );
618
+ const toAttach = attachment(
619
+ toSegments,
620
+ sides?.to ?? nearerEnd(toSegments, route.at(-1))
598
621
  );
599
- const toAttach = attachment(toSegments, nearerEnd(toSegments, route.at(-1)));
600
622
  const points = [fromAttach.at, ...route, toAttach.at];
601
623
  const tangent = (i) => {
602
624
  const p = points[i];
@@ -646,16 +668,21 @@ var MIN_DELETION_BP = 1;
646
668
  function bypassedPoints(nodePositions, bypassed) {
647
669
  return bypassed.flatMap((id) => nodePositions[id] ?? []);
648
670
  }
649
- function deletionArcCurves(nodePositions, deletion, axis, route) {
671
+ function deletionArcCurves(nodePositions, deletion, axis, route, stranded = false) {
650
672
  const from = nodePositions[deletion.from];
651
673
  const to = nodePositions[deletion.to];
652
674
  if (!from?.length || !to?.length) {
653
675
  return void 0;
654
676
  }
655
- return route ? routedEdgeCurves(from, to, route) : computeEdgeCurves(
677
+ return route ? routedEdgeCurves(
678
+ from,
679
+ to,
680
+ route,
681
+ stranded ? strandSides(deletion) : void 0
682
+ ) : computeEdgeCurves(
656
683
  from,
657
684
  to,
658
- deletion.from === deletion.to,
685
+ edgeJoinOf(deletion, stranded),
659
686
  0,
660
687
  0,
661
688
  axis,
@@ -723,6 +750,8 @@ function deletionEdges(graph) {
723
750
  edgeIndex,
724
751
  from: edge.from,
725
752
  to: edge.to,
753
+ ...edge.fromStrand ? { fromStrand: edge.fromStrand } : {},
754
+ ...edge.toStrand ? { toStrand: edge.toStrand } : {},
726
755
  refName,
727
756
  start,
728
757
  end,
@@ -1207,19 +1236,11 @@ function takeRoutes(positions, routes) {
1207
1236
  }
1208
1237
 
1209
1238
  // src/layout/mergeRuns.ts
1210
- function ownStrand(id) {
1211
- return id.endsWith("-") ? "-" : "+";
1212
- }
1213
1239
  function flip(strand) {
1214
1240
  return strand === "+" ? "-" : "+";
1215
1241
  }
1216
- function sidesOf(edge) {
1217
- const from = (edge.fromStrand ?? ownStrand(edge.from)) === ownStrand(edge.from) ? "end" : "start";
1218
- const to = (edge.toStrand ?? ownStrand(edge.to)) === ownStrand(edge.to) ? "start" : "end";
1219
- return { from, to };
1220
- }
1221
1242
  function mergeRuns(graph, alone = /* @__PURE__ */ new Set()) {
1222
- const sides = graph.edges.map(sidesOf);
1243
+ const sides = graph.edges.map(strandSides);
1223
1244
  const at = /* @__PURE__ */ new Map();
1224
1245
  const attach = (id, side, ei) => {
1225
1246
  const key = `${id}|${side}`;
@@ -1589,24 +1610,33 @@ function loadGraph(text, name, opts = {}) {
1589
1610
  }
1590
1611
  return graph;
1591
1612
  }
1613
+ var COMPONENT_SEPARATION = 15;
1592
1614
  function engineRequest(graph, scaling, settings) {
1593
1615
  const anchored = graph.nodes.some(isBackbone);
1616
+ const spaced = {
1617
+ ...scaling,
1618
+ opts: {
1619
+ ...scaling.opts,
1620
+ edgeLength: scaling.opts.edgeLength * (settings.spacing ?? 1)
1621
+ }
1622
+ };
1594
1623
  return {
1595
1624
  graph: {
1596
- nodes: anchored ? seededNodes(graph, scaling) : scaling.nodes,
1625
+ nodes: anchored ? seededNodes(graph, spaced) : spaced.nodes,
1597
1626
  edges: graph.edges
1598
1627
  },
1599
1628
  options: {
1600
1629
  quality: settings.quality,
1601
1630
  linearLayout: settings.linearLayout,
1602
- ...scaling.opts,
1631
+ ...spaced.opts,
1632
+ componentSeparation: COMPONENT_SEPARATION * (settings.componentSeparation ?? 1),
1603
1633
  ...anchored ? { rotateComponents: false } : {}
1604
1634
  }
1605
1635
  };
1606
1636
  }
1607
1637
  function engineKey(graph, settings) {
1608
1638
  const anchored = graph.nodes.some(isBackbone);
1609
- return `${settings.quality}|${settings.linearLayout}|${settings.bubbleSpread}|${anchored}|${graph.referencePath ?? ""}`;
1639
+ return `${settings.quality}|${settings.linearLayout}|${settings.bubbleSpread}|${settings.spacing ?? 1}|${settings.componentSeparation ?? 1}|${anchored}|${graph.referencePath ?? ""}`;
1610
1640
  }
1611
1641
  async function forceLayout(graph, settings, engine) {
1612
1642
  const spread = spreadFor(settings.bubbleSpread);
@@ -1654,6 +1684,7 @@ async function forceLayout(graph, settings, engine) {
1654
1684
  result: {
1655
1685
  ...result,
1656
1686
  nodePositions,
1687
+ stranded: true,
1657
1688
  ...deletions.length ? { deletionRoutes, extent: layoutExtent(deletionRoutes) } : {}
1658
1689
  },
1659
1690
  duration
@@ -1773,7 +1804,7 @@ function placeLabels(candidates, frame, take) {
1773
1804
  if (offFrame(box, frame)) {
1774
1805
  return void 0;
1775
1806
  }
1776
- if (insideFrame(box, frame) && take(box)) {
1807
+ if (insideFrame(box, frame) && (c.fits?.(box) ?? true) && take(box)) {
1777
1808
  return { ...c, text, y, w };
1778
1809
  }
1779
1810
  }
@@ -1888,10 +1919,10 @@ function labelOrder(nodePositions, nodeLengths) {
1888
1919
  return order;
1889
1920
  }
1890
1921
  var arcCache = /* @__PURE__ */ new WeakMap();
1891
- function arcPlacements(nodePositions, deletions, axis, version2, routes) {
1922
+ function arcPlacements(nodePositions, deletions, axis, version2, routes, stranded) {
1892
1923
  const { scaleX, scaleY } = axis;
1893
1924
  const cached2 = arcCache.get(nodePositions);
1894
- if (cached2?.deletions === deletions && cached2.routes === routes && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version2) {
1925
+ if (cached2?.deletions === deletions && cached2.routes === routes && cached2.stranded === stranded && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version2) {
1895
1926
  return cached2.arcs;
1896
1927
  }
1897
1928
  const arcs = [];
@@ -1900,7 +1931,8 @@ function arcPlacements(nodePositions, deletions, axis, version2, routes) {
1900
1931
  nodePositions,
1901
1932
  deletion,
1902
1933
  axis,
1903
- routes?.[deletion.edgeIndex]
1934
+ routes?.[deletion.edgeIndex],
1935
+ stranded
1904
1936
  );
1905
1937
  const apex = curves ? curveMidpoint(curves) : void 0;
1906
1938
  if (curves && apex) {
@@ -1917,6 +1949,7 @@ function arcPlacements(nodePositions, deletions, axis, version2, routes) {
1917
1949
  arcCache.set(nodePositions, {
1918
1950
  deletions,
1919
1951
  routes,
1952
+ stranded,
1920
1953
  scaleX,
1921
1954
  scaleY,
1922
1955
  version: version2,
@@ -1929,6 +1962,7 @@ function sizeLabelCandidates({
1929
1962
  nodeLengths,
1930
1963
  deletions,
1931
1964
  deletionRoutes,
1965
+ stranded = false,
1932
1966
  alleleDeletions,
1933
1967
  axis,
1934
1968
  translateX,
@@ -1945,7 +1979,8 @@ function sizeLabelCandidates({
1945
1979
  deletions,
1946
1980
  axis,
1947
1981
  version2,
1948
- deletionRoutes
1982
+ deletionRoutes,
1983
+ stranded
1949
1984
  )) {
1950
1985
  if (extent >= MIN_DELETION_LABEL_PX) {
1951
1986
  const text = deletionText(deletion.bp);
@@ -3374,7 +3409,7 @@ var COLOR_SCHEMES = [
3374
3409
  { value: "random", label: "Random" },
3375
3410
  { value: "rainbow", label: "Rainbow" },
3376
3411
  { value: "depth", label: "Depth" },
3377
- { value: "node-length", label: "Node Length" },
3412
+ { value: "node-length", label: "Node length" },
3378
3413
  { value: "stable-rank", label: "Stable rank" },
3379
3414
  { value: "reference-position", label: "Reference position" },
3380
3415
  { value: "grey", label: "Grey" }
@@ -3465,10 +3500,10 @@ function brightenAbgr(c, factor) {
3465
3500
 
3466
3501
  // src/util/edgeCurves.ts
3467
3502
  var cache = /* @__PURE__ */ new WeakMap();
3468
- function baseEdgeCurves(nodePositions, graph, axis, deletions, version2, routes) {
3503
+ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2, routes, stranded = false) {
3469
3504
  const { scaleX, scaleY } = axis;
3470
3505
  const cached2 = cache.get(nodePositions);
3471
- if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.routes === routes && cached2.version === version2) {
3506
+ if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.routes === routes && cached2.stranded === stranded && cached2.version === version2) {
3472
3507
  return cached2.curves;
3473
3508
  }
3474
3509
  const curves = /* @__PURE__ */ new Map();
@@ -3481,10 +3516,15 @@ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2, routes)
3481
3516
  const route = bypassed && routes?.[ei];
3482
3517
  curves.set(
3483
3518
  ei,
3484
- route ? routedEdgeCurves(from, to, route) : computeEdgeCurves(
3519
+ route ? routedEdgeCurves(
3520
+ from,
3521
+ to,
3522
+ route,
3523
+ stranded ? strandSides(edge) : void 0
3524
+ ) : computeEdgeCurves(
3485
3525
  from,
3486
3526
  to,
3487
- selfLinkOf(edge),
3527
+ edgeJoinOf(edge, stranded),
3488
3528
  0,
3489
3529
  0,
3490
3530
  axis,
@@ -3499,6 +3539,7 @@ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2, routes)
3499
3539
  scaleY,
3500
3540
  deletions,
3501
3541
  routes,
3542
+ stranded,
3502
3543
  version: version2,
3503
3544
  curves
3504
3545
  });
@@ -3523,6 +3564,7 @@ var ARROW_LENGTH_BASE_PX = 7;
3523
3564
  var ARROW_LENGTH_PER_EDGE_PX = 3;
3524
3565
  var ARROW_HALF_WIDTH_BASE_PX = 3;
3525
3566
  var ARROW_HALF_WIDTH_PER_EDGE_PX = 1.5;
3567
+ var MIN_LINK_HEADS = 3;
3526
3568
  var MIN_ARROW_FIT = 0.5;
3527
3569
  var MIN_ARROW_SCALE = 0.45;
3528
3570
  var ARROW_CURVE_STEPS = 12;
@@ -3587,12 +3629,17 @@ function arrowheadFor({
3587
3629
  toSegments
3588
3630
  }) {
3589
3631
  const length = ARROW_LENGTH_BASE_PX + ARROW_LENGTH_PER_EDGE_PX * edgeHalfWidth;
3590
- const n2 = backFromEnd(curves, scale, yToX, nodeHalfWidth + length);
3632
+ const n2 = backFromEnd(
3633
+ curves,
3634
+ scale,
3635
+ yToX,
3636
+ nodeHalfWidth + MIN_LINK_HEADS * length
3637
+ );
3591
3638
  const lengthPx = walkPx[n2 - 1];
3639
+ const abutting = lengthPx < ABUTTING_PX;
3592
3640
  const inset = Math.min(nodeHalfWidth, lengthPx);
3593
- const room = lengthPx < ABUTTING_PX ? polylinePx(fromSegments, scale, yToX) : lengthPx - inset;
3594
- const fit = Math.min(1, room / length);
3595
- if (fit < MIN_ARROW_FIT) {
3641
+ const fit = abutting ? Math.min(1, polylinePx(fromSegments, scale, yToX) / length) : 1;
3642
+ if (abutting ? fit < MIN_ARROW_FIT : lengthPx < MIN_LINK_HEADS * length) {
3596
3643
  return void 0;
3597
3644
  }
3598
3645
  const tip = walkPointAt(n2, inset);
@@ -3619,6 +3666,13 @@ function polylinePx(segments, scale, yToX) {
3619
3666
  }
3620
3667
  return length * scale;
3621
3668
  }
3669
+ function leavesStart(edge, segments, first, stranded, yToX) {
3670
+ if (stranded) {
3671
+ return strandSides(edge).from === "start";
3672
+ }
3673
+ const gap = (p) => Math.hypot(p.x - first.x0, (p.y - first.y0) * yToX);
3674
+ return gap(segments[0]) < gap(segments.at(-1));
3675
+ }
3622
3676
  function intoDirection(last, toSegments) {
3623
3677
  if (toSegments.length < 2) {
3624
3678
  return void 0;
@@ -3964,6 +4018,7 @@ function buildGeometry(options) {
3964
4018
  referenceRamp,
3965
4019
  deletions,
3966
4020
  deletionRoutes,
4021
+ stranded,
3967
4022
  hiddenEdges,
3968
4023
  version: version2 = 0
3969
4024
  } = options;
@@ -3976,7 +4031,8 @@ function buildGeometry(options) {
3976
4031
  axis,
3977
4032
  deletions,
3978
4033
  version2,
3979
- deletionRoutes
4034
+ deletionRoutes,
4035
+ stranded
3980
4036
  );
3981
4037
  const nodeStrokes = [];
3982
4038
  const nodeStrokeRuns = /* @__PURE__ */ new Map();
@@ -4028,7 +4084,7 @@ function buildGeometry(options) {
4028
4084
  }
4029
4085
  const head = showArrows && arrowColor !== void 0 ? arrowheadFor({
4030
4086
  curves,
4031
- edgeHalfWidth: edgeThickness,
4087
+ edgeHalfWidth: connectorThickness / 2,
4032
4088
  nodeHalfWidth: intoHalfWidth,
4033
4089
  scale,
4034
4090
  yToX,
@@ -4048,7 +4104,13 @@ function buildGeometry(options) {
4048
4104
  const width = fromNode ? nodeWidthPx(fromNode, contigThickness, nodeWidth2, depthNorm) : contigThickness;
4049
4105
  const laneWidth = Math.max(width / laneWalks.length, MIN_WALK_LANE_PX);
4050
4106
  const normals = pointNormalsOf(fromSegments, yToX);
4051
- const { nx, ny } = (edge.fromStrand === "-" ? normals[0] : normals.at(-1)) ?? {
4107
+ const { nx, ny } = (leavesStart(
4108
+ edge,
4109
+ fromSegments,
4110
+ baseCurves[0],
4111
+ stranded,
4112
+ yToX
4113
+ ) ? normals[0] : normals.at(-1)) ?? {
4052
4114
  nx: 0,
4053
4115
  ny: 0
4054
4116
  };
@@ -4370,6 +4432,415 @@ function groupByPaint(items, paintOf) {
4370
4432
  return [...groups.values(), ...deferred.values()];
4371
4433
  }
4372
4434
 
4435
+ // src/util/SpatialIndex.ts
4436
+ var FALLBACK_CELL_SIZE = 50;
4437
+ function meanExtentCellSize(extentTotal, count) {
4438
+ return count > 0 ? Math.max(1, extentTotal / count) : FALLBACK_CELL_SIZE;
4439
+ }
4440
+ function rowPitchOf(boxes) {
4441
+ const distinct = /* @__PURE__ */ new Set();
4442
+ let min = Infinity;
4443
+ let max = -Infinity;
4444
+ for (const box of boxes) {
4445
+ distinct.add(box.minY);
4446
+ min = Math.min(min, box.minY);
4447
+ max = Math.max(max, box.minY);
4448
+ }
4449
+ return distinct.size > 1 ? (max - min) / (distinct.size - 1) : void 0;
4450
+ }
4451
+ function cellSizeFrom(boxes, extentX, extentY) {
4452
+ const x = meanExtentCellSize(extentX, boxes.length);
4453
+ return {
4454
+ x,
4455
+ y: extentY > 0 ? meanExtentCellSize(extentY, boxes.length) : rowPitchOf(boxes) ?? x
4456
+ };
4457
+ }
4458
+ function resolveCellSize(given, boxes, extentX, extentY) {
4459
+ return given === void 0 ? cellSizeFrom(boxes, extentX, extentY) : typeof given === "number" ? { x: given, y: given } : given;
4460
+ }
4461
+ function getOrCreateCell(cells, cx, cy) {
4462
+ let row = cells.get(cx);
4463
+ if (!row) {
4464
+ row = /* @__PURE__ */ new Map();
4465
+ cells.set(cx, row);
4466
+ }
4467
+ let cell = row.get(cy);
4468
+ if (!cell) {
4469
+ cell = [];
4470
+ row.set(cy, cell);
4471
+ }
4472
+ return cell;
4473
+ }
4474
+ function addToGrid(cells, cellSize, minX, minY, maxX, maxY, entry) {
4475
+ const cx0 = Math.floor(minX / cellSize.x);
4476
+ const cx1 = Math.floor(maxX / cellSize.x);
4477
+ const cy0 = Math.floor(minY / cellSize.y);
4478
+ const cy1 = Math.floor(maxY / cellSize.y);
4479
+ for (let cx = cx0; cx <= cx1; cx++) {
4480
+ for (let cy = cy0; cy <= cy1; cy++) {
4481
+ getOrCreateCell(cells, cx, cy).push(entry);
4482
+ }
4483
+ }
4484
+ }
4485
+ function queryGrid(cells, cellSize, x, y, radiusX, radiusY) {
4486
+ const cx0 = Math.floor((x - radiusX) / cellSize.x);
4487
+ const cx1 = Math.floor((x + radiusX) / cellSize.x);
4488
+ const cy0 = Math.floor((y - radiusY) / cellSize.y);
4489
+ const cy1 = Math.floor((y + radiusY) / cellSize.y);
4490
+ const results = [];
4491
+ const seen = /* @__PURE__ */ new Set();
4492
+ for (let cx = cx0; cx <= cx1; cx++) {
4493
+ const row = cells.get(cx);
4494
+ if (row) {
4495
+ for (let cy = cy0; cy <= cy1; cy++) {
4496
+ const cell = row.get(cy);
4497
+ if (cell) {
4498
+ for (const entry of cell) {
4499
+ if (!seen.has(entry)) {
4500
+ seen.add(entry);
4501
+ results.push(entry);
4502
+ }
4503
+ }
4504
+ }
4505
+ }
4506
+ }
4507
+ }
4508
+ return results;
4509
+ }
4510
+ var SpatialIndex = class {
4511
+ constructor(nodePositions, cellSize) {
4512
+ this.cells = /* @__PURE__ */ new Map();
4513
+ const boxes = [];
4514
+ let extentX = 0;
4515
+ let extentY = 0;
4516
+ for (const [nodeId, segments] of Object.entries(nodePositions)) {
4517
+ for (let i = 0; i < segments.length - 1; i++) {
4518
+ const seg = segments[i];
4519
+ const next = segments[i + 1];
4520
+ const minX = Math.min(seg.x, next.x);
4521
+ const minY = Math.min(seg.y, next.y);
4522
+ const maxX = Math.max(seg.x, next.x);
4523
+ const maxY = Math.max(seg.y, next.y);
4524
+ extentX += maxX - minX;
4525
+ extentY += maxY - minY;
4526
+ boxes.push({ entry: { nodeId, segmentIdx: i }, minX, minY, maxX, maxY });
4527
+ }
4528
+ }
4529
+ this.cellSize = resolveCellSize(cellSize, boxes, extentX, extentY);
4530
+ for (const b of boxes) {
4531
+ addToGrid(
4532
+ this.cells,
4533
+ this.cellSize,
4534
+ b.minX,
4535
+ b.minY,
4536
+ b.maxX,
4537
+ b.maxY,
4538
+ b.entry
4539
+ );
4540
+ }
4541
+ }
4542
+ query(x, y, radiusX, radiusY = radiusX) {
4543
+ return queryGrid(this.cells, this.cellSize, x, y, radiusX, radiusY);
4544
+ }
4545
+ };
4546
+ var EdgeSpatialIndex = class {
4547
+ constructor(nodePositions, graph, drawPaths, axis, cellSize, deletions, version2 = 0, routes, stranded = false) {
4548
+ this.cells = /* @__PURE__ */ new Map();
4549
+ this.edgeCurves = baseEdgeCurves(
4550
+ nodePositions,
4551
+ graph,
4552
+ axis,
4553
+ deletions,
4554
+ version2,
4555
+ routes,
4556
+ stranded
4557
+ );
4558
+ const boxes = [];
4559
+ let extentX = 0;
4560
+ let extentY = 0;
4561
+ for (let ei = 0; ei < graph.edges.length; ei++) {
4562
+ const edge = graph.edges[ei];
4563
+ const fromSegments = nodePositions[edge.from];
4564
+ const toSegments = nodePositions[edge.to];
4565
+ const curves = this.edgeCurves.get(ei);
4566
+ if (fromSegments?.length && toSegments?.length && curves) {
4567
+ let { minX, minY, maxX, maxY } = curveBounds(curves);
4568
+ const numPaths = edge.pathIds?.length ?? 0;
4569
+ if (drawPaths && numPaths > 0) {
4570
+ let padX = 0;
4571
+ let padY = 0;
4572
+ for (const o of pathRibbonOffsets(
4573
+ fromSegments,
4574
+ toSegments,
4575
+ numPaths,
4576
+ axis
4577
+ )) {
4578
+ padX = Math.max(padX, Math.abs(o.x));
4579
+ padY = Math.max(padY, Math.abs(o.y));
4580
+ }
4581
+ minX -= padX;
4582
+ minY -= padY;
4583
+ maxX += padX;
4584
+ maxY += padY;
4585
+ }
4586
+ extentX += maxX - minX;
4587
+ extentY += maxY - minY;
4588
+ boxes.push({ ei, minX, minY, maxX, maxY });
4589
+ }
4590
+ }
4591
+ this.cellSize = resolveCellSize(cellSize, boxes, extentX, extentY);
4592
+ for (const b of boxes) {
4593
+ addToGrid(this.cells, this.cellSize, b.minX, b.minY, b.maxX, b.maxY, b.ei);
4594
+ }
4595
+ }
4596
+ getCurves(ei) {
4597
+ return this.edgeCurves.get(ei);
4598
+ }
4599
+ query(x, y, radiusX, radiusY = radiusX) {
4600
+ return queryGrid(this.cells, this.cellSize, x, y, radiusX, radiusY);
4601
+ }
4602
+ };
4603
+
4604
+ // src/util/hitDetection.ts
4605
+ function distanceToSegment(px, py, x1, y1, x2, y2) {
4606
+ const dx = x2 - x1;
4607
+ const dy = y2 - y1;
4608
+ const lenSq = dx * dx + dy * dy;
4609
+ if (lenSq === 0) {
4610
+ return Math.hypot(px - x1, py - y1);
4611
+ }
4612
+ const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
4613
+ return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
4614
+ }
4615
+ function distanceToCubicBezier(px, py, x1, y1, cx1, cy1, cx2, cy2, x2, y2) {
4616
+ let minDist = Infinity;
4617
+ const samples = 20;
4618
+ let lastX = x1;
4619
+ let lastY = y1;
4620
+ for (let i = 1; i <= samples; i++) {
4621
+ const t = i / samples;
4622
+ const u = 1 - t;
4623
+ const bx = u * u * u * x1 + 3 * u * u * t * cx1 + 3 * u * t * t * cx2 + t * t * t * x2;
4624
+ const by = u * u * u * y1 + 3 * u * u * t * cy1 + 3 * u * t * t * cy2 + t * t * t * y2;
4625
+ minDist = Math.min(minDist, distanceToSegment(px, py, lastX, lastY, bx, by));
4626
+ lastX = bx;
4627
+ lastY = by;
4628
+ }
4629
+ return minDist;
4630
+ }
4631
+ function distanceToBezierCurve(px, py, c) {
4632
+ return distanceToCubicBezier(
4633
+ px,
4634
+ py,
4635
+ c.x0,
4636
+ c.y0,
4637
+ c.cx0,
4638
+ c.cy0,
4639
+ c.cx1,
4640
+ c.cy1,
4641
+ c.x1,
4642
+ c.y1
4643
+ );
4644
+ }
4645
+ function distanceToEdgeCurves(px, py, curves, yToX = 1) {
4646
+ let minDist = Infinity;
4647
+ for (const c of curves) {
4648
+ const d = distanceToBezierCurve(
4649
+ px,
4650
+ py,
4651
+ yToX === 1 ? c : {
4652
+ x0: c.x0,
4653
+ y0: c.y0 * yToX,
4654
+ cx0: c.cx0,
4655
+ cy0: c.cy0 * yToX,
4656
+ cx1: c.cx1,
4657
+ cy1: c.cy1 * yToX,
4658
+ x1: c.x1,
4659
+ y1: c.y1 * yToX
4660
+ }
4661
+ );
4662
+ if (d < minDist) {
4663
+ minDist = d;
4664
+ }
4665
+ }
4666
+ return minDist;
4667
+ }
4668
+ var nodeCache = /* @__PURE__ */ new WeakMap();
4669
+ function getSpatialIndex(nodePositions, version2) {
4670
+ const cached2 = nodeCache.get(nodePositions);
4671
+ if (cached2?.version === version2) {
4672
+ return cached2.index;
4673
+ }
4674
+ const index = new SpatialIndex(nodePositions);
4675
+ nodeCache.set(nodePositions, { version: version2, index });
4676
+ return index;
4677
+ }
4678
+ var edgeCache = /* @__PURE__ */ new WeakMap();
4679
+ function getEdgeSpatialIndex(nodePositions, graph, drawPaths, axis, version2, deletions, routes, stranded = false) {
4680
+ const cached2 = edgeCache.get(nodePositions);
4681
+ if (cached2?.graph === graph && cached2.drawPaths === drawPaths && cached2.scaleX === axis.scaleX && cached2.scaleY === axis.scaleY && cached2.version === version2 && cached2.deletions === deletions && cached2.routes === routes && cached2.stranded === stranded) {
4682
+ return cached2.index;
4683
+ }
4684
+ const index = new EdgeSpatialIndex(
4685
+ nodePositions,
4686
+ graph,
4687
+ drawPaths,
4688
+ axis,
4689
+ void 0,
4690
+ deletions,
4691
+ version2,
4692
+ routes,
4693
+ stranded
4694
+ );
4695
+ edgeCache.set(nodePositions, {
4696
+ graph,
4697
+ drawPaths,
4698
+ scaleX: axis.scaleX,
4699
+ scaleY: axis.scaleY,
4700
+ version: version2,
4701
+ index,
4702
+ deletions,
4703
+ routes,
4704
+ stranded
4705
+ });
4706
+ return index;
4707
+ }
4708
+ var MIN_NODE_REACH_PX = 5;
4709
+ function findHoveredNode(nodePositions, graphX, graphY, axis, version2 = 0, ink) {
4710
+ const yToX = yToXOf(axis);
4711
+ const index = getSpatialIndex(nodePositions, version2);
4712
+ const reach = (px) => Math.max(MIN_NODE_REACH_PX, px) / axis.scaleX;
4713
+ const queryReach = reach(ink?.maxHalfWidthPx ?? 0);
4714
+ const candidates = index.query(graphX, graphY, queryReach, queryReach / yToX);
4715
+ let hovered = null;
4716
+ let best = Infinity;
4717
+ for (const { nodeId, segmentIdx } of candidates) {
4718
+ const segments = nodePositions[nodeId];
4719
+ const dist = distanceToSegment(
4720
+ graphX,
4721
+ graphY * yToX,
4722
+ segments[segmentIdx].x,
4723
+ segments[segmentIdx].y * yToX,
4724
+ segments[segmentIdx + 1].x,
4725
+ segments[segmentIdx + 1].y * yToX
4726
+ );
4727
+ if (dist < best && dist < reach(ink?.halfWidthPx(nodeId) ?? 0)) {
4728
+ best = dist;
4729
+ hovered = nodeId;
4730
+ }
4731
+ }
4732
+ return hovered;
4733
+ }
4734
+ function segmentMeetsBox(a, b, box) {
4735
+ const dx = b.x - a.x;
4736
+ const dy = b.y - a.y;
4737
+ let t0 = 0;
4738
+ let t1 = 1;
4739
+ for (const [p, q] of [
4740
+ [-dx, a.x - box.x0],
4741
+ [dx, box.x1 - a.x],
4742
+ [-dy, a.y - box.y0],
4743
+ [dy, box.y1 - a.y]
4744
+ ]) {
4745
+ if (p === 0) {
4746
+ if (q < 0) {
4747
+ return false;
4748
+ }
4749
+ } else if (p < 0) {
4750
+ t0 = Math.max(t0, q / p);
4751
+ } else {
4752
+ t1 = Math.min(t1, q / p);
4753
+ }
4754
+ }
4755
+ return t0 <= t1;
4756
+ }
4757
+ function nodesUnderBox(nodePositions, t, version2 = 0) {
4758
+ const index = getSpatialIndex(nodePositions, version2);
4759
+ const screen = (p) => ({
4760
+ x: p.x * t.scaleX + t.translateX,
4761
+ y: p.y * t.scaleY + t.translateY
4762
+ });
4763
+ return (box) => {
4764
+ const hits = /* @__PURE__ */ new Set();
4765
+ const candidates = index.query(
4766
+ ((box.x0 + box.x1) / 2 - t.translateX) / t.scaleX,
4767
+ ((box.y0 + box.y1) / 2 - t.translateY) / t.scaleY,
4768
+ Math.abs((box.x1 - box.x0) / 2 / t.scaleX),
4769
+ Math.abs((box.y1 - box.y0) / 2 / t.scaleY)
4770
+ );
4771
+ for (const { nodeId, segmentIdx } of candidates) {
4772
+ const line = nodePositions[nodeId];
4773
+ if (!hits.has(nodeId) && segmentMeetsBox(
4774
+ screen(line[segmentIdx]),
4775
+ screen(line[segmentIdx + 1]),
4776
+ box
4777
+ )) {
4778
+ hits.add(nodeId);
4779
+ }
4780
+ }
4781
+ return hits;
4782
+ };
4783
+ }
4784
+ function findHoveredEdge(nodePositions, graph, graphX, graphY, axis, drawPaths, version2 = 0, deletions, hiddenEdges, deletionRoutes, stranded = false) {
4785
+ const yToX = yToXOf(axis);
4786
+ const edgeThreshold = 10 / axis.scaleX;
4787
+ const edgeIndex = getEdgeSpatialIndex(
4788
+ nodePositions,
4789
+ graph,
4790
+ drawPaths,
4791
+ axis,
4792
+ version2,
4793
+ deletions,
4794
+ deletionRoutes,
4795
+ stranded
4796
+ );
4797
+ const candidates = edgeIndex.query(
4798
+ graphX,
4799
+ graphY,
4800
+ edgeThreshold,
4801
+ edgeThreshold / yToX
4802
+ );
4803
+ let hovered = null;
4804
+ let best = edgeThreshold;
4805
+ for (const edgeIdx of candidates) {
4806
+ const edge = graph.edges[edgeIdx];
4807
+ const fromSegments = nodePositions[edge.from];
4808
+ const toSegments = nodePositions[edge.to];
4809
+ if (!fromSegments?.length || !toSegments?.length || hiddenEdges?.has(edgeIdx)) {
4810
+ continue;
4811
+ }
4812
+ const numPaths = edge.pathIds?.length ?? 0;
4813
+ const baseCurves = edgeIndex.getCurves(edgeIdx);
4814
+ if (!baseCurves) {
4815
+ continue;
4816
+ }
4817
+ let dist;
4818
+ if (!drawPaths || numPaths === 0) {
4819
+ dist = distanceToEdgeCurves(graphX, graphY * yToX, baseCurves, yToX);
4820
+ } else {
4821
+ let minDist = Infinity;
4822
+ for (const offset of pathRibbonOffsets(
4823
+ fromSegments,
4824
+ toSegments,
4825
+ numPaths,
4826
+ axis
4827
+ )) {
4828
+ const curves = offset.x === 0 && offset.y === 0 ? baseCurves : translateCurves(baseCurves, offset.x, offset.y);
4829
+ const d = distanceToEdgeCurves(graphX, graphY * yToX, curves, yToX);
4830
+ if (d < minDist) {
4831
+ minDist = d;
4832
+ }
4833
+ }
4834
+ dist = minDist;
4835
+ }
4836
+ if (dist < best) {
4837
+ best = dist;
4838
+ hovered = edgeIdx;
4839
+ }
4840
+ }
4841
+ return hovered;
4842
+ }
4843
+
4373
4844
  // src/walkEncoding.ts
4374
4845
  import { packAbgr as packAbgr3 } from "@jbrowse/core/util/colorBits";
4375
4846
  var WALK_FIELDS = [
@@ -4752,11 +5223,71 @@ function facetCells(names, by, domain = []) {
4752
5223
  return cells.includes(-1) || new Set(cells).size < cells.length ? wrap : { columns, count: rows.length * columns, cells };
4753
5224
  }
4754
5225
 
5226
+ // src/genes/exonOutline.ts
5227
+ var EXON_COLOR = "#daa520";
5228
+ var EXON_GAP_PX = 1;
5229
+ var EXON_LINE_PX = 2;
5230
+ var MIN_LANE_PX = 4;
5231
+ function exonStretches(pins, halfWidthPx, lift, pathOf = (d) => d) {
5232
+ const inkPx = (nodeId) => {
5233
+ const own = halfWidthPx(nodeId) * 2;
5234
+ return lift?.nodeIds.has(nodeId) ? Math.max(own, lift.walks.length * MIN_LANE_PX) : own;
5235
+ };
5236
+ return pins.flatMap(
5237
+ (pin) => pin.exonsByNode.map(({ nodeId, d }) => ({
5238
+ key: `${pin.gene.name}-${pin.gene.start}-${nodeId}`,
5239
+ d: pathOf(d),
5240
+ inner: inkPx(nodeId) + 2 * EXON_GAP_PX
5241
+ }))
5242
+ );
5243
+ }
5244
+ function exonOutlineTree(stretches, o) {
5245
+ if (stretches.length === 0) {
5246
+ return void 0;
5247
+ }
5248
+ const stroke = (s, color, width) => el("path", {
5249
+ d: s.d,
5250
+ fill: "none",
5251
+ stroke: color,
5252
+ "stroke-width": width,
5253
+ "stroke-linecap": "round",
5254
+ "stroke-linejoin": "round",
5255
+ "vector-effect": o.transform ? "non-scaling-stroke" : void 0
5256
+ });
5257
+ return el(
5258
+ "g",
5259
+ {},
5260
+ el(
5261
+ "mask",
5262
+ {
5263
+ id: o.id,
5264
+ maskUnits: "userSpaceOnUse",
5265
+ x: 0,
5266
+ y: 0,
5267
+ width: o.width,
5268
+ height: o.height
5269
+ },
5270
+ el(
5271
+ "g",
5272
+ { transform: o.transform },
5273
+ ...stretches.map((s) => stroke(s, "#fff", s.inner + 2 * EXON_LINE_PX)),
5274
+ ...stretches.map((s) => stroke(s, "#000", s.inner))
5275
+ )
5276
+ ),
5277
+ el("rect", {
5278
+ width: o.width,
5279
+ height: o.height,
5280
+ fill: EXON_COLOR,
5281
+ mask: `url(#${o.id})`
5282
+ })
5283
+ );
5284
+ }
5285
+
4755
5286
  // src/genes/genePins.ts
4756
5287
  function genePins(graph, genes, positions) {
4757
5288
  const byRefName = /* @__PURE__ */ new Map();
4758
5289
  for (const node of graph.nodes) {
4759
- if (isBackbone(node)) {
5290
+ if (isAnchored(node)) {
4760
5291
  const { refName } = node.stable;
4761
5292
  if (positions[node.id]?.length) {
4762
5293
  const entry = byRefName.get(refName) ?? byRefName.set(refName, { nodes: [], reach: 0 }).get(refName);
@@ -4770,17 +5301,18 @@ function genePins(graph, genes, positions) {
4770
5301
  }
4771
5302
  const pins = [];
4772
5303
  for (const gene of genes) {
4773
- const backbone = byRefName.get(gene.refName);
4774
- if (!backbone) {
5304
+ const onRefName = byRefName.get(gene.refName);
5305
+ if (!onRefName) {
4775
5306
  continue;
4776
5307
  }
4777
5308
  const parts = [];
4778
5309
  const exonsByNode = [];
5310
+ const nodeIds = /* @__PURE__ */ new Set();
4779
5311
  let at;
4780
5312
  let atDistance = Infinity;
4781
5313
  let covered = 0;
4782
5314
  const mid = (gene.start + gene.end) / 2;
4783
- const { nodes, reach } = backbone;
5315
+ const { nodes, reach } = onRefName;
4784
5316
  for (let i = firstNodeAtOrAfter(nodes, gene.start - reach); i < nodes.length && nodes[i].stable.start < gene.end; i++) {
4785
5317
  const node = nodes[i];
4786
5318
  const nodeStart = node.stable.start;
@@ -4789,6 +5321,7 @@ function genePins(graph, genes, positions) {
4789
5321
  continue;
4790
5322
  }
4791
5323
  const line = positions[node.id];
5324
+ nodeIds.add(node.id);
4792
5325
  covered += Math.min(nodeEnd, gene.end) - Math.max(nodeStart, gene.start);
4793
5326
  const nodeParts = [];
4794
5327
  for (const exon of gene.exons) {
@@ -4828,6 +5361,7 @@ function genePins(graph, genes, positions) {
4828
5361
  gene,
4829
5362
  exons: parts.join(""),
4830
5363
  exonsByNode,
5364
+ nodeIds,
4831
5365
  at,
4832
5366
  covered: covered / (gene.end - gene.start)
4833
5367
  });
@@ -4852,7 +5386,8 @@ function geneCoverageNote(pin) {
4852
5386
  const amount = (bp) => +(bp / per).toFixed(per === 1 ? 0 : 1);
4853
5387
  return `${amount(pin.covered * length)} of ${amount(length)} ${unit}`;
4854
5388
  }
4855
- function geneLabelCandidates(pins, screen, contigThickness) {
5389
+ function geneLabelCandidates(pins, screen, contigThickness, nodesUnder) {
5390
+ const ink = contigThickness / 2;
4856
5391
  return byExtent(pins, (pin) => pin.gene.end - pin.gene.start).map((pin) => {
4857
5392
  const { x, y } = screen(pin.at);
4858
5393
  const note = geneCoverageNote(pin);
@@ -4862,7 +5397,15 @@ function geneLabelCandidates(pins, screen, contigThickness) {
4862
5397
  y: y + contigThickness + GENE_PIN_DROP_PX,
4863
5398
  text: note ? `${pin.gene.name} \xB7 ${note}` : pin.gene.name,
4864
5399
  fallback: note ? `${pin.gene.name} \u2026` : void 0,
4865
- stack: GENE_STACK
5400
+ stack: GENE_STACK,
5401
+ fits: (box) => [
5402
+ ...nodesUnder({
5403
+ x0: box.x0 - ink,
5404
+ x1: box.x1 + ink,
5405
+ y0: box.y0 - ink,
5406
+ y1: box.y1 + ink
5407
+ })
5408
+ ].every((id) => pin.nodeIds.has(id))
4866
5409
  };
4867
5410
  });
4868
5411
  }
@@ -4923,7 +5466,16 @@ function layoutLabels(m) {
4923
5466
  take
4924
5467
  );
4925
5468
  const genes = placeLabels(
4926
- geneLabelCandidates(m.genePins, screen, m.contigThickness),
5469
+ geneLabelCandidates(
5470
+ m.genePins,
5471
+ screen,
5472
+ m.contigThickness,
5473
+ nodesUnderBox(
5474
+ m.nodePositions ?? {},
5475
+ { scaleX, scaleY, translateX, translateY },
5476
+ m.positionsVersion
5477
+ )
5478
+ ),
4927
5479
  frame,
4928
5480
  take
4929
5481
  );
@@ -4932,6 +5484,7 @@ function layoutLabels(m) {
4932
5484
  nodeLengths: m.nodeLengths,
4933
5485
  deletions: m.showDeletionEdges ? m.deletions : [],
4934
5486
  deletionRoutes: m.deletionRoutes,
5487
+ stranded: m.stranded,
4935
5488
  alleleDeletions: m.alleleDeletions,
4936
5489
  axis: m.axisScale,
4937
5490
  translateX,
@@ -5609,6 +6162,154 @@ function stripGeneGaps(frame, rowGenes, gaps) {
5609
6162
  return frame.boxesGenes ? gaps : { untracked: 0, unread: 0, ...gaps, crowded: true };
5610
6163
  }
5611
6164
 
6165
+ // src/reference.ts
6166
+ var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
6167
+ ["hg38", "GRCh38"],
6168
+ ["hg19", "GRCh37"],
6169
+ ["hs1", "CHM13"],
6170
+ ["t2t-chm13", "CHM13"],
6171
+ ["chm13v2", "CHM13"]
6172
+ ]);
6173
+ function wellKnownSample(assemblyName) {
6174
+ return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
6175
+ }
6176
+ function sampleOf2(refName) {
6177
+ const sample = refName.includes("#") ? panSNSample(refName) : void 0;
6178
+ return sample === void 0 || isGenericSample(sample) ? void 0 : sample;
6179
+ }
6180
+ function haplotypeOf(refName) {
6181
+ return panSNHaplotype(refName) ?? panSNSample(refName);
6182
+ }
6183
+ function graphHaplotypes(graph) {
6184
+ const refNames = new Set((graph.anchorPaths ?? []).map((p) => p.name));
6185
+ for (const node of graph.nodes) {
6186
+ if (node.stable) {
6187
+ refNames.add(node.stable.refName);
6188
+ }
6189
+ }
6190
+ const bySample = /* @__PURE__ */ new Map();
6191
+ for (const refName of refNames) {
6192
+ const sample = sampleOf2(refName);
6193
+ if (sample !== void 0) {
6194
+ const haplotypes = bySample.get(sample) ?? bySample.set(sample, /* @__PURE__ */ new Set()).get(sample);
6195
+ haplotypes.add(haplotypeOf(refName));
6196
+ }
6197
+ }
6198
+ return bySample;
6199
+ }
6200
+ function backboneFrom(contigs, haplotypes) {
6201
+ const samples = new Set(contigs.map((c) => sampleOf2(c.refName)));
6202
+ const [sample] = samples;
6203
+ if (samples.size !== 1 || sample === void 0) {
6204
+ return { contigs, prefixes: [], named: false };
6205
+ }
6206
+ const own = new Set(contigs.map((c) => haplotypeOf(c.refName)));
6207
+ const unique = [...haplotypes.get(sample) ?? []].every((h) => own.has(h));
6208
+ const shared = new Set(contigs.map((c) => panSNHaplotype(c.refName)));
6209
+ const [haplotype] = shared;
6210
+ return {
6211
+ contigs,
6212
+ prefixes: [
6213
+ ...unique ? [sample] : [],
6214
+ ...shared.size === 1 && haplotype ? [haplotype] : []
6215
+ ],
6216
+ named: true
6217
+ };
6218
+ }
6219
+ function graphBackbone(graph) {
6220
+ const spans = /* @__PURE__ */ new Map();
6221
+ for (const node of graph.nodes) {
6222
+ if (isBackbone(node)) {
6223
+ const { refName, start } = node.stable;
6224
+ const end = start + node.length;
6225
+ const span = spans.get(refName);
6226
+ if (span) {
6227
+ span.start = Math.min(span.start, start);
6228
+ span.end = Math.max(span.end, end);
6229
+ } else {
6230
+ spans.set(refName, {
6231
+ refName,
6232
+ contig: panSNContig(refName),
6233
+ start,
6234
+ end
6235
+ });
6236
+ }
6237
+ }
6238
+ }
6239
+ const contigs = [...spans.values()];
6240
+ return contigs.length ? backboneFrom(contigs, graphHaplotypes(graph)) : void 0;
6241
+ }
6242
+ function referenceLabel(graph, region) {
6243
+ const backbone = graphBackbone(graph);
6244
+ const contig = region ? panSNContig(region.refName) : backbone?.contigs.length === 1 ? backbone.contigs[0].contig : void 0;
6245
+ const sample = backbone?.named ? panSNSample(backbone.contigs[0].refName) : void 0;
6246
+ return contig && (sample ? `${sample} ${contig}` : contig);
6247
+ }
6248
+ function backboneAssembly(backbone, assemblies) {
6249
+ const names = (a) => [a.name, ...a.aliases ?? []];
6250
+ const naming = (prefix) => (a) => names(a).some((n2) => n2.toLowerCase() === prefix.toLowerCase());
6251
+ const prefixes = [...backbone?.prefixes ?? []].reverse();
6252
+ const sample = backbone?.prefixes.find((p) => !p.includes("#"))?.toLowerCase();
6253
+ for (const prefix of prefixes) {
6254
+ const assembly = assemblies.find(naming(prefix));
6255
+ if (assembly) {
6256
+ return assembly;
6257
+ }
6258
+ }
6259
+ return sample === void 0 ? void 0 : assemblies.find(
6260
+ (a) => names(a).some((n2) => wellKnownSample(n2)?.toLowerCase() === sample)
6261
+ );
6262
+ }
6263
+ function assemblyWalk(graph, assembly) {
6264
+ const haplotypes = graphHaplotypes(graph);
6265
+ return graph.anchorPaths?.find(
6266
+ (walk) => backboneAssembly(
6267
+ backboneFrom(
6268
+ [
6269
+ {
6270
+ refName: walk.name,
6271
+ contig: panSNContig(walk.name),
6272
+ start: walk.start,
6273
+ end: walk.start + walk.length
6274
+ }
6275
+ ],
6276
+ haplotypes
6277
+ ),
6278
+ [assembly]
6279
+ )
6280
+ );
6281
+ }
6282
+ function contigNames(contig) {
6283
+ const bare = contig.replace(/^chr/i, "");
6284
+ const forms = !bare ? [] : /^(M|MT)$/i.test(bare) ? ["M", "MT"] : [bare];
6285
+ return [.../* @__PURE__ */ new Set([contig, ...forms.flatMap((f) => [f, `chr${f}`])])];
6286
+ }
6287
+ function refNameBinding(refNames, aliases = {}) {
6288
+ const exact = new Set(refNames);
6289
+ const byName = /* @__PURE__ */ new Map();
6290
+ for (const refName of exact) {
6291
+ const contig = panSNContig(refName);
6292
+ const alias = aliases[contig];
6293
+ for (const name of /* @__PURE__ */ new Set([
6294
+ ...contigNames(contig),
6295
+ ...alias ? [alias] : []
6296
+ ])) {
6297
+ byName.set(name, byName.has(name) ? void 0 : refName);
6298
+ }
6299
+ }
6300
+ return (name) => exact.has(name) ? name : byName.get(name);
6301
+ }
6302
+ function featuresOnBackbone(features, backbone, aliases) {
6303
+ const bind = refNameBinding(
6304
+ backbone.contigs.map((c) => c.refName),
6305
+ aliases
6306
+ );
6307
+ return features.flatMap((f) => {
6308
+ const refName = bind(f.refName);
6309
+ return refName === void 0 ? [] : [refName === f.refName ? f : { ...f, refName }];
6310
+ });
6311
+ }
6312
+
5612
6313
  // src/renderer/svgCanvas.ts
5613
6314
  var n = (v) => +v.toFixed(1);
5614
6315
  function paint(css) {
@@ -5722,7 +6423,7 @@ function svgCanvas(width, height) {
5722
6423
  }
5723
6424
 
5724
6425
  // src/version.ts
5725
- var version = "5.0.1";
6426
+ var version = "5.2.0";
5726
6427
 
5727
6428
  // src/viewport.ts
5728
6429
  function axisScaleOf(scale, pixelRows) {
@@ -5772,11 +6473,7 @@ var SWATCH_PX = 18;
5772
6473
  var KEY_GAP_PX = 16;
5773
6474
  var STRIP_KEY_PX = 22;
5774
6475
  var FADED = "rgb(160,160,160)";
5775
- var EXON_COLOR = "#daa520";
5776
6476
  var GENE_INK2 = "#1c1c22";
5777
- var MIN_LANE_PX = 4;
5778
- var EXON_GAP_PX = 1;
5779
- var EXON_LINE_PX = 2;
5780
6477
  function esc(s) {
5781
6478
  return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
5782
6479
  }
@@ -5793,6 +6490,12 @@ function keySvg(id, x, y, stops, label, after, under, opacity = 1) {
5793
6490
  markup: `${defs}<g transform="translate(${x} ${y})" ${FONT}><rect y="5" width="${bar}" height="8" rx="2" ${fill}/><text x="${bar + 5}" y="13"><tspan font-weight="bold">${esc(label)}</tspan>${esc(after)}</text>${under ? `<text y="28">${esc(under)}</text>` : ""}</g>`
5794
6491
  };
5795
6492
  }
6493
+ function exonKeySvg(x, y) {
6494
+ return {
6495
+ width: SWATCH_PX + 5 + 4 * LABEL_CHAR_PX,
6496
+ markup: `<g transform="translate(${x} ${y})" ${FONT}><rect x="1" y="4" width="${SWATCH_PX - 2}" height="10" rx="4" fill="none" stroke="${EXON_COLOR}" stroke-width="2"/><text x="${SWATCH_PX + 5}" y="13">exon</text></g>`
6497
+ };
6498
+ }
5796
6499
  function walkKeySvg(id, x, y, walk, label, reference) {
5797
6500
  const key = walkKey(walk, reference);
5798
6501
  return keySvg(
@@ -5845,9 +6548,10 @@ function figureSvg(graph, layout, o = {}) {
5845
6548
  ])
5846
6549
  );
5847
6550
  const labelOf2 = (name) => labels.get(name) ?? name;
6551
+ const referenceName = o.referenceName ?? referenceLabel(graph, region);
5848
6552
  const reference = lift?.referenceDomain && {
5849
6553
  ...lift.referenceDomain,
5850
- name: region?.refName
6554
+ name: referenceName
5851
6555
  };
5852
6556
  const contigThickness = o.contigThickness ?? 6;
5853
6557
  const nodeWidth2 = o.nodeWidth ?? "depth";
@@ -5856,24 +6560,12 @@ function figureSvg(graph, layout, o = {}) {
5856
6560
  let masks = 0;
5857
6561
  function overlays(highlight, t, w, h) {
5858
6562
  const out = [];
5859
- const inkPx = (nodeId) => {
5860
- const own = ink.halfWidthPx(nodeId) * 2;
5861
- return highlight?.nodeIds.has(nodeId) ? Math.max(own, highlight.walks.length * MIN_LANE_PX) : own;
5862
- };
5863
- const stretches = pins.flatMap(
5864
- (pin) => pin.exonsByNode.map(({ nodeId, d }) => ({
5865
- d: screenPath(d, t),
5866
- inner: inkPx(nodeId) + 2 * EXON_GAP_PX
5867
- }))
6563
+ const outlines = exonOutlineTree(
6564
+ exonStretches(pins, ink.halfWidthPx, highlight, (d) => screenPath(d, t)),
6565
+ { id: `exons${masks++}`, width: w, height: h }
5868
6566
  );
5869
- if (stretches.length > 0) {
5870
- const id = `exons${masks++}`;
5871
- const stroke = (color, width2, d) => `<path d="${d}" fill="none" stroke="${color}" stroke-width="${width2}" stroke-linecap="round" stroke-linejoin="round"/>`;
5872
- out.push(
5873
- `<mask id="${id}" maskUnits="userSpaceOnUse" x="0" y="0" width="${w}" height="${h}">${stretches.map((s) => stroke("#fff", s.inner + 2 * EXON_LINE_PX, s.d)).join("")}${stretches.map((s) => stroke("#000", s.inner, s.d)).join(
5874
- ""
5875
- )}</mask><rect width="${w}" height="${h}" fill="${EXON_COLOR}" mask="url(#${id})"/>`
5876
- );
6567
+ if (outlines) {
6568
+ out.push(serializeEl(outlines));
5877
6569
  }
5878
6570
  const screen = (p) => ({
5879
6571
  x: p.x * t.scaleX + t.translateX,
@@ -5881,7 +6573,12 @@ function figureSvg(graph, layout, o = {}) {
5881
6573
  });
5882
6574
  const frame = { width: w, height: h };
5883
6575
  for (const { item: pin, x, y, w: cw, text } of placeLabels(
5884
- geneLabelCandidates(pins, screen, contigThickness),
6576
+ geneLabelCandidates(
6577
+ pins,
6578
+ screen,
6579
+ contigThickness,
6580
+ nodesUnderBox(layout.nodePositions, t)
6581
+ ),
5885
6582
  frame,
5886
6583
  occupancy(frame)
5887
6584
  )) {
@@ -5953,6 +6650,7 @@ function figureSvg(graph, layout, o = {}) {
5953
6650
  referenceRamp,
5954
6651
  deletions: new Map(deletions.map((d) => [d.edgeIndex, d.bypassed])),
5955
6652
  deletionRoutes: layout.deletionRoutes,
6653
+ stranded: layout.stranded,
5956
6654
  hiddenEdges: new Set(
5957
6655
  o.showDeletionEdges ? [] : deletions.map((d) => d.edgeIndex)
5958
6656
  )
@@ -6036,16 +6734,19 @@ function figureSvg(graph, layout, o = {}) {
6036
6734
  x,
6037
6735
  2,
6038
6736
  encodingStops({ field: "reference", scheme: "rainbow" }),
6039
- "Reference position",
6737
+ referenceName ? `${referenceName} position` : "Reference position",
6040
6738
  "",
6041
6739
  rangeText(
6042
- region?.refName,
6740
+ referenceName,
6043
6741
  referenceRamp.start,
6044
6742
  referenceRamp.start + referenceRamp.span
6045
6743
  )
6046
6744
  )
6047
6745
  );
6048
6746
  }
6747
+ if (pins.some((pin) => pin.exonsByNode.length > 0)) {
6748
+ push(exonKeySvg(x, 2));
6749
+ }
6049
6750
  const header = parts.length > 0 ? FACET_TITLE_PX : 0;
6050
6751
  const usable = width - 2 * FIT_PADDING;
6051
6752
  const drawn = pixelRows || !(bounds.w > 0) ? bounds.h : bounds.h * usable / bounds.w;
@@ -6198,14 +6899,22 @@ function genesFromGff3Lines(lines) {
6198
6899
  const start = top?.start ?? Math.min(...rows2.map((r) => r.start));
6199
6900
  const end = top?.end ?? Math.max(...rows2.map((r) => r.end));
6200
6901
  const a = first.attrs;
6201
- const exons = rows2.filter((r) => r.type === "exon").map((r) => ({ start: r.start, end: r.end }));
6902
+ const spans = (type) => rows2.filter((r) => r.type === type).map((r) => ({ start: r.start, end: r.end }));
6903
+ const exons = spans("exon");
6904
+ const coding = spans("CDS");
6905
+ const transcribed = rows2.some(
6906
+ (r) => r !== top && r.type !== "exon" && r.type !== "CDS"
6907
+ );
6202
6908
  genes.push({
6203
6909
  name: a.get("Name") ?? a.get("gene_name") ?? a.get("gene") ?? a.get("gene_id") ?? key.split(" ")[1],
6204
6910
  refName: first.refName,
6205
6911
  start,
6206
6912
  end,
6207
6913
  strand: first.strand,
6208
- exons: exons.length ? mergedIntervals(exons) : [{ start, end }]
6914
+ // A range read holds only the rows over its window, so a gene with
6915
+ // transcripts but no exon or CDS there is intronic in it. Only a gene
6916
+ // with no transcripts, as a bacterium's gene and CDS, is one exon.
6917
+ exons: exons.length ? mergedIntervals(exons) : transcribed ? mergedIntervals(coding) : [{ start, end }]
6209
6918
  });
6210
6919
  }
6211
6920
  return genes;
@@ -6261,148 +6970,6 @@ function genesFromText(text) {
6261
6970
  return /^\d+$/.test(cols[1] ?? "") && /^\d+$/.test(cols[2] ?? "") ? genesFromBed(text) : genesFromGff3Lines(text.split(/\r?\n/));
6262
6971
  }
6263
6972
 
6264
- // src/reference.ts
6265
- var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
6266
- ["hg38", "GRCh38"],
6267
- ["hg19", "GRCh37"],
6268
- ["hs1", "CHM13"],
6269
- ["t2t-chm13", "CHM13"],
6270
- ["chm13v2", "CHM13"]
6271
- ]);
6272
- function wellKnownSample(assemblyName) {
6273
- return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
6274
- }
6275
- function sampleOf2(refName) {
6276
- const sample = refName.includes("#") ? panSNSample(refName) : void 0;
6277
- return sample === void 0 || isGenericSample(sample) ? void 0 : sample;
6278
- }
6279
- function haplotypeOf(refName) {
6280
- return panSNHaplotype(refName) ?? panSNSample(refName);
6281
- }
6282
- function graphHaplotypes(graph) {
6283
- const refNames = new Set((graph.anchorPaths ?? []).map((p) => p.name));
6284
- for (const node of graph.nodes) {
6285
- if (node.stable) {
6286
- refNames.add(node.stable.refName);
6287
- }
6288
- }
6289
- const bySample = /* @__PURE__ */ new Map();
6290
- for (const refName of refNames) {
6291
- const sample = sampleOf2(refName);
6292
- if (sample !== void 0) {
6293
- const haplotypes = bySample.get(sample) ?? bySample.set(sample, /* @__PURE__ */ new Set()).get(sample);
6294
- haplotypes.add(haplotypeOf(refName));
6295
- }
6296
- }
6297
- return bySample;
6298
- }
6299
- function backboneFrom(contigs, haplotypes) {
6300
- const samples = new Set(contigs.map((c) => sampleOf2(c.refName)));
6301
- const [sample] = samples;
6302
- if (samples.size !== 1 || sample === void 0) {
6303
- return { contigs, prefixes: [], named: false };
6304
- }
6305
- const own = new Set(contigs.map((c) => haplotypeOf(c.refName)));
6306
- const unique = [...haplotypes.get(sample) ?? []].every((h) => own.has(h));
6307
- const shared = new Set(contigs.map((c) => panSNHaplotype(c.refName)));
6308
- const [haplotype] = shared;
6309
- return {
6310
- contigs,
6311
- prefixes: [
6312
- ...unique ? [sample] : [],
6313
- ...shared.size === 1 && haplotype ? [haplotype] : []
6314
- ],
6315
- named: true
6316
- };
6317
- }
6318
- function graphBackbone(graph) {
6319
- const spans = /* @__PURE__ */ new Map();
6320
- for (const node of graph.nodes) {
6321
- if (isBackbone(node)) {
6322
- const { refName, start } = node.stable;
6323
- const end = start + node.length;
6324
- const span = spans.get(refName);
6325
- if (span) {
6326
- span.start = Math.min(span.start, start);
6327
- span.end = Math.max(span.end, end);
6328
- } else {
6329
- spans.set(refName, {
6330
- refName,
6331
- contig: panSNContig(refName),
6332
- start,
6333
- end
6334
- });
6335
- }
6336
- }
6337
- }
6338
- const contigs = [...spans.values()];
6339
- return contigs.length ? backboneFrom(contigs, graphHaplotypes(graph)) : void 0;
6340
- }
6341
- function backboneAssembly(backbone, assemblies) {
6342
- const names = (a) => [a.name, ...a.aliases ?? []];
6343
- const naming = (prefix) => (a) => names(a).some((n2) => n2.toLowerCase() === prefix.toLowerCase());
6344
- const prefixes = [...backbone?.prefixes ?? []].reverse();
6345
- const sample = backbone?.prefixes.find((p) => !p.includes("#"))?.toLowerCase();
6346
- for (const prefix of prefixes) {
6347
- const assembly = assemblies.find(naming(prefix));
6348
- if (assembly) {
6349
- return assembly;
6350
- }
6351
- }
6352
- return sample === void 0 ? void 0 : assemblies.find(
6353
- (a) => names(a).some((n2) => wellKnownSample(n2)?.toLowerCase() === sample)
6354
- );
6355
- }
6356
- function assemblyWalk(graph, assembly) {
6357
- const haplotypes = graphHaplotypes(graph);
6358
- return graph.anchorPaths?.find(
6359
- (walk) => backboneAssembly(
6360
- backboneFrom(
6361
- [
6362
- {
6363
- refName: walk.name,
6364
- contig: panSNContig(walk.name),
6365
- start: walk.start,
6366
- end: walk.start + walk.length
6367
- }
6368
- ],
6369
- haplotypes
6370
- ),
6371
- [assembly]
6372
- )
6373
- );
6374
- }
6375
- function contigNames(contig) {
6376
- const bare = contig.replace(/^chr/i, "");
6377
- const forms = !bare ? [] : /^(M|MT)$/i.test(bare) ? ["M", "MT"] : [bare];
6378
- return [.../* @__PURE__ */ new Set([contig, ...forms.flatMap((f) => [f, `chr${f}`])])];
6379
- }
6380
- function refNameBinding(refNames, aliases = {}) {
6381
- const exact = new Set(refNames);
6382
- const byName = /* @__PURE__ */ new Map();
6383
- for (const refName of exact) {
6384
- const contig = panSNContig(refName);
6385
- const alias = aliases[contig];
6386
- for (const name of /* @__PURE__ */ new Set([
6387
- ...contigNames(contig),
6388
- ...alias ? [alias] : []
6389
- ])) {
6390
- byName.set(name, byName.has(name) ? void 0 : refName);
6391
- }
6392
- }
6393
- return (name) => exact.has(name) ? name : byName.get(name);
6394
- }
6395
- function featuresOnBackbone(features, backbone, aliases) {
6396
- const bind = refNameBinding(
6397
- backbone.contigs.map((c) => c.refName),
6398
- aliases
6399
- );
6400
- return features.flatMap((f) => {
6401
- const refName = bind(f.refName);
6402
- return refName === void 0 ? [] : [refName === f.refName ? f : { ...f, refName }];
6403
- });
6404
- }
6405
-
6406
6973
  // src/gbzJoin.ts
6407
6974
  var UNKNOWN_SAMPLE = "unknown";
6408
6975
  function parseWalk(line) {
@@ -6546,7 +7113,9 @@ var NodeLimitError = class extends Error {
6546
7113
  super(
6547
7114
  `this ${windowBp.toLocaleString()} bp window reads more than nodeLimit (${limit.toLocaleString()}) graph nodes; zoom in to about ${fitsBp.toLocaleString()} bp or raise nodeLimit`
6548
7115
  );
7116
+ this.fitsBp = fitsBp;
6549
7117
  this.name = "NodeLimitError";
7118
+ this.regionTooLarge = true;
6550
7119
  }
6551
7120
  };
6552
7121
  function haplotypePrefix(name) {
@@ -6691,10 +7260,6 @@ export {
6691
7260
  BUBBLE_SPREADS,
6692
7261
  ROW_HEIGHT_PX,
6693
7262
  svgPath,
6694
- curveBounds,
6695
- translateCurves,
6696
- yToXOf,
6697
- pathRibbonOffsets,
6698
7263
  deletionEdges,
6699
7264
  panSNSample,
6700
7265
  panSNHaplotype,
@@ -6730,7 +7295,6 @@ export {
6730
7295
  nodeWidthPx,
6731
7296
  maxNodeWidthPx,
6732
7297
  nodeInk,
6733
- baseEdgeCurves,
6734
7298
  LIFT_BACKDROP_CSS,
6735
7299
  REFERENCE_RAMP_MAX_HUE,
6736
7300
  computeReferenceRamp,
@@ -6738,6 +7302,8 @@ export {
6738
7302
  getNodeColor,
6739
7303
  buildGeometry,
6740
7304
  Canvas2DRenderer,
7305
+ findHoveredNode,
7306
+ findHoveredEdge,
6741
7307
  WALK_FIELDS,
6742
7308
  WALK_SCHEMES,
6743
7309
  resolveEncoding,
@@ -6758,6 +7324,9 @@ export {
6758
7324
  facetGrid,
6759
7325
  facetSettingOf,
6760
7326
  facetCells,
7327
+ EXON_COLOR,
7328
+ exonStretches,
7329
+ exonOutlineTree,
6761
7330
  genePins,
6762
7331
  HALO_FACTOR,
6763
7332
  LEGEND_INSET_PX,
@@ -6796,6 +7365,14 @@ export {
6796
7365
  walkMarksTree,
6797
7366
  walkStripLabelsTree,
6798
7367
  stripGeneGaps,
7368
+ WELL_KNOWN_SAMPLES,
7369
+ wellKnownSample,
7370
+ graphBackbone,
7371
+ referenceLabel,
7372
+ backboneAssembly,
7373
+ assemblyWalk,
7374
+ refNameBinding,
7375
+ featuresOnBackbone,
6799
7376
  svgCanvas,
6800
7377
  version,
6801
7378
  axisScaleOf,
@@ -6809,13 +7386,6 @@ export {
6809
7386
  mergeOverlappingByName,
6810
7387
  genesFromBed,
6811
7388
  genesFromText,
6812
- WELL_KNOWN_SAMPLES,
6813
- wellKnownSample,
6814
- graphBackbone,
6815
- backboneAssembly,
6816
- assemblyWalk,
6817
- refNameBinding,
6818
- featuresOnBackbone,
6819
7389
  haplotypeWanted,
6820
7390
  referenceSamplesOf,
6821
7391
  resolveReferenceSample,