@jbrowse/bandage-core 4.0.24 → 4.0.26

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.
@@ -387,6 +387,18 @@ function anchorFromPaths(graph, preferred) {
387
387
  function anchorGraph(graph, preferred) {
388
388
  return graph.anchoredBy === "tags" ? graph : anchorFromPaths(graph, preferred);
389
389
  }
390
+ function trimOrigins(origins, paths, stretches) {
391
+ const pieces = /* @__PURE__ */ new Map();
392
+ for (const path of paths) {
393
+ const origin = pathOrigin(path.name).name;
394
+ pieces.set(origin, (pieces.get(origin) ?? 0) + 1);
395
+ }
396
+ return origins.map((o) => {
397
+ const path = paths.find((p) => pathOrigin(p.name).name === o.name);
398
+ const stretch = path ? stretches.get(path.name) : void 0;
399
+ return stretch && pieces.get(o.name) === 1 ? { ...o, start: o.start + stretch.dropped, length: stretch.bp } : o;
400
+ });
401
+ }
390
402
 
391
403
  // src/gfa/gfaConverter.ts
392
404
  function surveySegments(gfaGraph) {
@@ -1772,9 +1784,9 @@ function sampleRowLayout(graph, region, keep = []) {
1772
1784
  },
1773
1785
  positions: nodePositions
1774
1786
  });
1775
- const referenceSample = parsePanSN(backbone[0].stable.refName).sample;
1787
+ const referenceSample2 = parsePanSN(backbone[0].stable.refName).sample;
1776
1788
  const rowLabels = [
1777
- { label: referenceSample, y: 0 },
1789
+ { label: referenceSample2, y: 0 },
1778
1790
  ...samples.map((sample) => ({
1779
1791
  label: sample,
1780
1792
  y: rowOf.get(sample) * ROW_HEIGHT_PX
@@ -2818,10 +2830,10 @@ function brightenAbgr(c, factor) {
2818
2830
 
2819
2831
  // src/util/edgeCurves.ts
2820
2832
  var cache = /* @__PURE__ */ new WeakMap();
2821
- function baseEdgeCurves(nodePositions, graph, axis, deletions, version) {
2833
+ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2) {
2822
2834
  const { scaleX, scaleY } = axis;
2823
2835
  const cached2 = cache.get(nodePositions);
2824
- if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.version === version) {
2836
+ if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.version === version2) {
2825
2837
  return cached2.curves;
2826
2838
  }
2827
2839
  const curves = /* @__PURE__ */ new Map();
@@ -2850,7 +2862,7 @@ function baseEdgeCurves(nodePositions, graph, axis, deletions, version) {
2850
2862
  scaleX,
2851
2863
  scaleY,
2852
2864
  deletions,
2853
- version,
2865
+ version: version2,
2854
2866
  curves
2855
2867
  });
2856
2868
  return curves;
@@ -3312,7 +3324,7 @@ function buildGeometry(options) {
3312
3324
  referenceRamp,
3313
3325
  deletions,
3314
3326
  hiddenEdges,
3315
- version = 0
3327
+ version: version2 = 0
3316
3328
  } = options;
3317
3329
  const depthNorm = nodeWidth2 === "depth" ? meanDepth(graph) : 0;
3318
3330
  const scale = axis.scaleX;
@@ -3322,7 +3334,7 @@ function buildGeometry(options) {
3322
3334
  graph,
3323
3335
  axis,
3324
3336
  deletions,
3325
- version
3337
+ version2
3326
3338
  );
3327
3339
  const nodeStrokes = [];
3328
3340
  const nodeStrokeRuns = /* @__PURE__ */ new Map();
@@ -3871,10 +3883,10 @@ function labelOrder(nodePositions, nodeLengths) {
3871
3883
  return order;
3872
3884
  }
3873
3885
  var arcCache = /* @__PURE__ */ new WeakMap();
3874
- function arcPlacements(nodePositions, deletions, axis, version) {
3886
+ function arcPlacements(nodePositions, deletions, axis, version2) {
3875
3887
  const { scaleX, scaleY } = axis;
3876
3888
  const cached2 = arcCache.get(nodePositions);
3877
- if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
3889
+ if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version2) {
3878
3890
  return cached2.arcs;
3879
3891
  }
3880
3892
  const arcs = [];
@@ -3892,7 +3904,7 @@ function arcPlacements(nodePositions, deletions, axis, version) {
3892
3904
  });
3893
3905
  }
3894
3906
  }
3895
- arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
3907
+ arcCache.set(nodePositions, { deletions, scaleX, scaleY, version: version2, arcs });
3896
3908
  return arcs;
3897
3909
  }
3898
3910
  function sizeLabelCandidates({
@@ -3905,7 +3917,7 @@ function sizeLabelCandidates({
3905
3917
  translateY,
3906
3918
  width,
3907
3919
  height,
3908
- version = 0
3920
+ version: version2 = 0
3909
3921
  }) {
3910
3922
  const { scaleX, scaleY } = axis;
3911
3923
  const deletionCandidates = [];
@@ -3914,7 +3926,7 @@ function sizeLabelCandidates({
3914
3926
  nodePositions,
3915
3927
  deletions,
3916
3928
  axis,
3917
- version
3929
+ version2
3918
3930
  )) {
3919
3931
  if (extent >= MIN_DELETION_LABEL_PX) {
3920
3932
  const text = deletionText(deletion.bp);
@@ -4278,7 +4290,7 @@ function liftOf(walks, ramp) {
4278
4290
  function rangeText(contig, start, end) {
4279
4291
  const s = Math.round(start);
4280
4292
  const e = Math.round(end);
4281
- return `${contig ? `${contig}:` : ""}${s.toLocaleString()}-${e.toLocaleString()} (${formatBp(e - s)})`;
4293
+ return `${contig ? `${contig}:` : ""}${s.toLocaleString("en-US")}-${e.toLocaleString("en-US")} (${formatBp(e - s)})`;
4282
4294
  }
4283
4295
  function walkKey(walk, reference) {
4284
4296
  const { field } = walk.encoding;
@@ -4292,6 +4304,15 @@ function walkKey(walk, reference) {
4292
4304
  hover: scale === own ? void 0 : own
4293
4305
  };
4294
4306
  }
4307
+ function walkPosition(walk, nodeId, length) {
4308
+ const progress = walk.progress.get(nodeId);
4309
+ const range = walk.range;
4310
+ if (progress === void 0 || !range) {
4311
+ return void 0;
4312
+ }
4313
+ const start = Math.round(range.start + progress * walk.bp - length / 2);
4314
+ return `${range.contig}:${start.toLocaleString("en-US")}-${(start + length).toLocaleString("en-US")}`;
4315
+ }
4295
4316
 
4296
4317
  // src/facetGrid.ts
4297
4318
  var FACET_GAP_PX = 8;
@@ -4364,6 +4385,223 @@ function facetCells(names, by) {
4364
4385
  return cells.includes(-1) || new Set(cells).size < cells.length ? wrap : { columns, count: rows.length * columns, cells };
4365
4386
  }
4366
4387
 
4388
+ // src/genes/genePins.ts
4389
+ function genePins(graph, genes, positions) {
4390
+ const byRefName = /* @__PURE__ */ new Map();
4391
+ for (const node of graph.nodes) {
4392
+ if (isBackbone(node)) {
4393
+ const { refName } = node.stable;
4394
+ if (positions[node.id]?.length) {
4395
+ const entry = byRefName.get(refName) ?? byRefName.set(refName, { nodes: [], reach: 0 }).get(refName);
4396
+ entry.nodes.push(node);
4397
+ entry.reach = Math.max(entry.reach, node.length);
4398
+ }
4399
+ }
4400
+ }
4401
+ for (const { nodes } of byRefName.values()) {
4402
+ nodes.sort((a, b) => a.stable.start - b.stable.start);
4403
+ }
4404
+ const pins = [];
4405
+ for (const gene of genes) {
4406
+ const backbone = byRefName.get(gene.refName);
4407
+ if (!backbone) {
4408
+ continue;
4409
+ }
4410
+ const parts = [];
4411
+ const exonsByNode = [];
4412
+ let at;
4413
+ let atDistance = Infinity;
4414
+ let covered = 0;
4415
+ const mid = (gene.start + gene.end) / 2;
4416
+ const { nodes, reach } = backbone;
4417
+ for (let i = firstNodeAtOrAfter(nodes, gene.start - reach); i < nodes.length && nodes[i].stable.start < gene.end; i++) {
4418
+ const node = nodes[i];
4419
+ const nodeStart = node.stable.start;
4420
+ const nodeEnd = nodeStart + node.length;
4421
+ if (nodeEnd <= gene.start) {
4422
+ continue;
4423
+ }
4424
+ const line = positions[node.id];
4425
+ covered += Math.min(nodeEnd, gene.end) - Math.max(nodeStart, gene.start);
4426
+ const nodeParts = [];
4427
+ for (const exon of gene.exons) {
4428
+ const a = Math.max(exon.start, nodeStart);
4429
+ const b = Math.min(exon.end, nodeEnd);
4430
+ if (b <= a) {
4431
+ continue;
4432
+ }
4433
+ const stretch = polylineSlice(
4434
+ line,
4435
+ (a - nodeStart) / node.length,
4436
+ (b - nodeStart) / node.length
4437
+ );
4438
+ if (stretch.length === 1) {
4439
+ stretch.push({ ...stretch[0] });
4440
+ }
4441
+ nodeParts.push(svgPath(stretch));
4442
+ }
4443
+ if (nodeParts.length > 0) {
4444
+ parts.push(...nodeParts);
4445
+ exonsByNode.push({ nodeId: node.id, d: nodeParts.join("") });
4446
+ }
4447
+ const pinBp = Math.min(Math.max(mid, nodeStart), nodeEnd);
4448
+ const distance = Math.abs(pinBp - mid);
4449
+ if (distance < atDistance) {
4450
+ atDistance = distance;
4451
+ const [p] = polylineSlice(
4452
+ line,
4453
+ (pinBp - nodeStart) / node.length,
4454
+ (pinBp - nodeStart) / node.length
4455
+ );
4456
+ at = p;
4457
+ }
4458
+ }
4459
+ if (at) {
4460
+ pins.push({
4461
+ gene,
4462
+ exons: parts.join(""),
4463
+ exonsByNode,
4464
+ at,
4465
+ covered: covered / (gene.end - gene.start)
4466
+ });
4467
+ }
4468
+ }
4469
+ return pins;
4470
+ }
4471
+
4472
+ // src/labelLayout.ts
4473
+ var HALO_FACTOR = 3.4;
4474
+ var LEGEND_INSET_PX = 6;
4475
+ var ROUTE_STACK = 8;
4476
+ var GENE_PIN_DROP_PX = 18;
4477
+ var GENE_STACK = 2;
4478
+ var TOPMOST_BASELINE = LABEL_PX + LABEL_PAD;
4479
+ function geneCoverageNote(pin) {
4480
+ const length = pin.gene.end - pin.gene.start;
4481
+ if (pin.covered >= 0.98) {
4482
+ return void 0;
4483
+ }
4484
+ const [per, unit] = length >= 1e6 ? [1e6, "Mb"] : length >= 1e3 ? [1e3, "kb"] : [1, "bp"];
4485
+ const amount = (bp) => +(bp / per).toFixed(per === 1 ? 0 : 1);
4486
+ return `${amount(pin.covered * length)} of ${amount(length)} ${unit}`;
4487
+ }
4488
+ function geneLabelCandidates(pins, screen, contigThickness) {
4489
+ return byExtent(pins, (pin) => pin.gene.end - pin.gene.start).map((pin) => {
4490
+ const { x, y } = screen(pin.at);
4491
+ const note = geneCoverageNote(pin);
4492
+ return {
4493
+ item: pin,
4494
+ x,
4495
+ y: y + contigThickness + GENE_PIN_DROP_PX,
4496
+ text: note ? `${pin.gene.name} \xB7 ${note}` : pin.gene.name,
4497
+ fallback: note ? `${pin.gene.name} \u2026` : void 0,
4498
+ stack: GENE_STACK
4499
+ };
4500
+ });
4501
+ }
4502
+ function byExtent(items, extent) {
4503
+ return [...items].sort((a, b) => extent(b) - extent(a));
4504
+ }
4505
+ function layoutLabels(m) {
4506
+ const { paneWidth: width, canvasHeight: height, translateX, translateY } = m;
4507
+ const { scaleX, scaleY } = m.axisScale;
4508
+ const frame = { width, height };
4509
+ const screen = (p) => ({
4510
+ x: p.x * scaleX + translateX,
4511
+ y: p.y * scaleY + translateY
4512
+ });
4513
+ const reserved = m.drawnRowLabels.map(
4514
+ ({ label, y }) => rowLabelBox(label, y * scaleY + translateY)
4515
+ );
4516
+ const stripPx = m.referenceStripZonePx ?? 0;
4517
+ if (stripPx > 0) {
4518
+ reserved.push({ x0: 0, x1: width, y0: 0, y1: stripPx });
4519
+ }
4520
+ if (m.legendSize.width > 0) {
4521
+ reserved.push({
4522
+ x0: width - LEGEND_INSET_PX - m.legendSize.width,
4523
+ x1: width,
4524
+ y0: 0,
4525
+ y1: stripPx + LEGEND_INSET_PX + m.legendSize.height
4526
+ });
4527
+ }
4528
+ if (m.poppedFrom) {
4529
+ reserved.push({
4530
+ x0: 0,
4531
+ x1: 60 + m.poppedFrom.label.length * LABEL_CHAR_PX * 1.2,
4532
+ y0: 0,
4533
+ y1: 44
4534
+ });
4535
+ }
4536
+ const take = occupancy(frame, reserved);
4537
+ const onRows = m.drawnRowLabels.length > 0;
4538
+ const rowsTop = onRows ? Math.min(...m.drawnRowLabels.map((r) => r.y)) : Infinity;
4539
+ const halo = m.contigThickness * HALO_FACTOR;
4540
+ const byBubble = byExtent(m.bubbleHalos, (h) => h.members);
4541
+ const bubbles = placeLabels(
4542
+ byBubble.map((h) => {
4543
+ const { x, y } = screen({
4544
+ x: h.labelAt.x,
4545
+ y: Math.min(h.labelAt.y, rowsTop)
4546
+ });
4547
+ const baseline = y - halo / 2 - 6;
4548
+ return {
4549
+ item: h,
4550
+ x,
4551
+ y: onRows ? Math.max(baseline, TOPMOST_BASELINE) : baseline,
4552
+ text: h.label
4553
+ };
4554
+ }),
4555
+ frame,
4556
+ take
4557
+ );
4558
+ const genes = placeLabels(
4559
+ geneLabelCandidates(m.genePins, screen, m.contigThickness),
4560
+ frame,
4561
+ take
4562
+ );
4563
+ const sizeCandidates = m.nodePositions && m.labelsNodeSizes ? sizeLabelCandidates({
4564
+ nodePositions: m.nodePositions,
4565
+ nodeLengths: m.nodeLengths,
4566
+ deletions: m.showDeletionEdges ? m.deletions : [],
4567
+ alleleDeletions: m.alleleDeletions,
4568
+ axis: m.axisScale,
4569
+ translateX,
4570
+ translateY,
4571
+ width,
4572
+ height,
4573
+ version: m.positionsVersion
4574
+ }) : { deletions: [], nodes: [] };
4575
+ const deletionLabels = placeSizeLabels(sizeCandidates.deletions, take);
4576
+ const routes = placeLabels(
4577
+ byBubble.flatMap(
4578
+ (h) => h.routes.map((route) => {
4579
+ const { x, y } = screen(route.at);
4580
+ return {
4581
+ item: { halo: h, route },
4582
+ x,
4583
+ y: y + 4,
4584
+ text: route.text,
4585
+ stack: ROUTE_STACK
4586
+ };
4587
+ })
4588
+ ),
4589
+ frame,
4590
+ take
4591
+ );
4592
+ const nodeLabels = placeSizeLabels(
4593
+ sizeCandidates.nodes,
4594
+ take,
4595
+ nodeLabelBudget(width, height)
4596
+ );
4597
+ return {
4598
+ bubbles,
4599
+ genes,
4600
+ routes,
4601
+ sizes: [...deletionLabels, ...nodeLabels]
4602
+ };
4603
+ }
4604
+
4367
4605
  // src/renderer/svgCanvas.ts
4368
4606
  var n = (v) => +v.toFixed(1);
4369
4607
  function paint(css) {
@@ -4380,9 +4618,43 @@ function paintAttrs(kind, css) {
4380
4618
  const { color, opacity } = paint(css);
4381
4619
  return `${kind}="${color}"${opacity < 1 ? ` ${kind}-opacity="${n(opacity)}"` : ""}`;
4382
4620
  }
4621
+ var DOT_PX = 0.5;
4622
+ var DOT_CELL_PX = 0.25;
4383
4623
  function svgCanvas(width, height) {
4384
4624
  const out = [];
4385
- let d = "";
4625
+ let subpaths = [];
4626
+ const start = (x, y) => {
4627
+ const s = { d: `M${n(x)} ${n(y)}`, segments: 0, x0: x, x1: x, y0: y, y1: y };
4628
+ subpaths.push(s);
4629
+ return s;
4630
+ };
4631
+ const reach = (points) => {
4632
+ const s = subpaths.at(-1) ?? start(points[0], points[1]);
4633
+ for (let i = 0; i < points.length; i += 2) {
4634
+ s.x0 = Math.min(s.x0, points[i]);
4635
+ s.x1 = Math.max(s.x1, points[i]);
4636
+ s.y0 = Math.min(s.y0, points[i + 1]);
4637
+ s.y1 = Math.max(s.y1, points[i + 1]);
4638
+ }
4639
+ s.segments++;
4640
+ return s;
4641
+ };
4642
+ const pathData = (dots) => {
4643
+ const seen = /* @__PURE__ */ new Set();
4644
+ return subpaths.map((s) => {
4645
+ if (!dots || s.x1 - s.x0 >= DOT_PX || s.y1 - s.y0 >= DOT_PX) {
4646
+ return s.d;
4647
+ }
4648
+ const x = (s.x0 + s.x1) / 2;
4649
+ const y = (s.y0 + s.y1) / 2;
4650
+ const cell = `${Math.round(x / DOT_CELL_PX)} ${Math.round(y / DOT_CELL_PX)}`;
4651
+ if (s.segments === 0 || seen.has(cell)) {
4652
+ return "";
4653
+ }
4654
+ seen.add(cell);
4655
+ return `M${n(x)} ${n(y)}h0`;
4656
+ }).join("");
4657
+ };
4386
4658
  const ctx = {
4387
4659
  canvas: void 0,
4388
4660
  fillStyle: "#000",
@@ -4391,21 +4663,25 @@ function svgCanvas(width, height) {
4391
4663
  lineCap: "butt",
4392
4664
  lineJoin: "miter",
4393
4665
  beginPath() {
4394
- d = "";
4666
+ subpaths = [];
4395
4667
  },
4396
4668
  moveTo(x, y) {
4397
- d += `M${n(x)} ${n(y)}`;
4669
+ start(x, y);
4398
4670
  },
4399
4671
  lineTo(x, y) {
4400
- d += `L${n(x)} ${n(y)}`;
4672
+ reach([x, y]).d += `L${n(x)} ${n(y)}`;
4401
4673
  },
4402
4674
  bezierCurveTo(cx0, cy0, cx1, cy1, x, y) {
4403
- d += `C${n(cx0)} ${n(cy0)} ${n(cx1)} ${n(cy1)} ${n(x)} ${n(y)}`;
4675
+ reach([cx0, cy0, cx1, cy1, x, y]).d += `C${n(cx0)} ${n(cy0)} ${n(cx1)} ${n(cy1)} ${n(x)} ${n(y)}`;
4404
4676
  },
4405
4677
  closePath() {
4406
- d += "Z";
4678
+ const s = subpaths.at(-1);
4679
+ if (s) {
4680
+ s.d += "Z";
4681
+ }
4407
4682
  },
4408
4683
  stroke() {
4684
+ const d = pathData(ctx.lineCap !== "butt");
4409
4685
  if (d) {
4410
4686
  out.push(
4411
4687
  `<path d="${d}" fill="none" ${paintAttrs("stroke", ctx.strokeStyle)} stroke-width="${n(ctx.lineWidth)}" stroke-linecap="${ctx.lineCap}" stroke-linejoin="${ctx.lineJoin}"/>`
@@ -4413,6 +4689,7 @@ function svgCanvas(width, height) {
4413
4689
  }
4414
4690
  },
4415
4691
  fill() {
4692
+ const d = pathData(false);
4416
4693
  if (d) {
4417
4694
  out.push(`<path d="${d}" ${paintAttrs("fill", ctx.fillStyle)}/>`);
4418
4695
  }
@@ -4437,6 +4714,9 @@ function svgCanvas(width, height) {
4437
4714
  };
4438
4715
  }
4439
4716
 
4717
+ // src/version.ts
4718
+ var version = "4.0.26";
4719
+
4440
4720
  // src/viewport.ts
4441
4721
  function axisScaleOf(scale, pixelRows) {
4442
4722
  return { scaleX: scale, scaleY: pixelRows ? 1 : scale, pixelRows };
@@ -4484,6 +4764,11 @@ var BAR_PX = 48;
4484
4764
  var SWATCH_PX = 18;
4485
4765
  var KEY_GAP_PX = 16;
4486
4766
  var FADED = "rgb(160,160,160)";
4767
+ var EXON_COLOR = "#daa520";
4768
+ var GENE_INK = "#1c1c22";
4769
+ var MIN_LANE_PX = 4;
4770
+ var EXON_GAP_PX = 1;
4771
+ var EXON_LINE_PX = 2;
4487
4772
  function esc(s) {
4488
4773
  return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4489
4774
  }
@@ -4512,18 +4797,30 @@ function walkKeySvg(id, x, y, walk, label, reference) {
4512
4797
  key.scale
4513
4798
  );
4514
4799
  }
4800
+ function screenPath(d, t) {
4801
+ return d.replaceAll(
4802
+ /([ML])(-?[\d.e-]+),(-?[\d.e-]+)/g,
4803
+ (_, cmd, x, y) => `${cmd}${+(Number(x) * t.scaleX + t.translateX).toFixed(1)},${+(Number(y) * t.scaleY + t.translateY).toFixed(1)}`
4804
+ );
4805
+ }
4806
+ function chip(x, y, w, name, note) {
4807
+ return `<rect x="${x - w / 2}" y="${y - LABEL_PX - LABEL_PAD + 2}" width="${w}" height="${LABEL_PX + LABEL_PAD * 2 - 2}" rx="3" fill="#fff" fill-opacity="0.85" stroke="${EXON_COLOR}"/><text x="${x}" y="${y}" font-family="Helvetica, Arial, sans-serif" font-size="${LABEL_PX}" fill="${GENE_INK}" text-anchor="middle"><tspan font-style="italic" font-weight="600">${esc(name)}</tspan>${note ? esc(note) : ""}</text>`;
4808
+ }
4515
4809
  function figureSvg(graph, layout, o = {}) {
4516
4810
  const width = o.width ?? 1200;
4517
4811
  const room = o.height ?? 800;
4518
4812
  const region = o.region;
4519
- const bounds = drawingBounds(layout, { region });
4813
+ const rampDomain = o.colorDomain ?? region;
4814
+ const bounds = drawingBounds(layout, {
4815
+ region: o.fitToDrawing ? void 0 : region
4816
+ });
4520
4817
  const pixelRows = layout.pixelRows ?? false;
4521
4818
  const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
4522
4819
  const colorScheme = resolveColorScheme(o.colorScheme ?? "auto", graph);
4523
- const referenceRamp = colorScheme === "reference-position" && !layout.tubeMap ? computeReferenceRamp(graph, region) : void 0;
4820
+ const referenceRamp = colorScheme === "reference-position" && !layout.tubeMap ? computeReferenceRamp(graph, rampDomain) : void 0;
4524
4821
  const deletions = layout.tubeMap ? [] : deletionEdges(graph);
4525
4822
  const layers = o.walks ?? [];
4526
- const walkRamp = layers.some((l) => l.color?.field === "reference") ? referenceRamp ?? computeReferenceRamp(graph, region) : void 0;
4823
+ const walkRamp = layers.some((l) => l.color?.field === "reference") ? referenceRamp ?? computeReferenceRamp(graph, rampDomain) : void 0;
4527
4824
  const lift = layers.length > 0 && !layout.tubeMap ? walkLift(graph, layers, walkRamp) : void 0;
4528
4825
  const by = o.facet ?? "none";
4529
4826
  const panels = by !== "none" && lift && lift.walks.length > 1 ? facetLifts(graph, lift, layers, walkRamp) : void 0;
@@ -4538,6 +4835,58 @@ function figureSvg(graph, layout, o = {}) {
4538
4835
  ...lift.referenceDomain,
4539
4836
  name: region?.refName
4540
4837
  };
4838
+ const contigThickness = o.contigThickness ?? 6;
4839
+ const nodeWidth2 = o.nodeWidth ?? "depth";
4840
+ const pins = o.genes?.length && !layout.tubeMap ? genePins(graph, o.genes, layout.nodePositions) : [];
4841
+ const ink = nodeInk(graph, nodeById, contigThickness, nodeWidth2);
4842
+ let masks = 0;
4843
+ function overlays(highlight, t, w, h) {
4844
+ const out = [];
4845
+ const inkPx = (nodeId) => {
4846
+ const own = ink.halfWidthPx(nodeId) * 2;
4847
+ return highlight?.nodeIds.has(nodeId) ? Math.max(own, highlight.walks.length * MIN_LANE_PX) : own;
4848
+ };
4849
+ const stretches = pins.flatMap(
4850
+ (pin) => pin.exonsByNode.map(({ nodeId, d }) => ({
4851
+ d: screenPath(d, t),
4852
+ inner: inkPx(nodeId) + 2 * EXON_GAP_PX
4853
+ }))
4854
+ );
4855
+ if (stretches.length > 0) {
4856
+ const id = `exons${masks++}`;
4857
+ const stroke = (color, width2, d) => `<path d="${d}" fill="none" stroke="${color}" stroke-width="${width2}" stroke-linecap="round" stroke-linejoin="round"/>`;
4858
+ out.push(
4859
+ `<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(
4860
+ ""
4861
+ )}</mask><rect width="${w}" height="${h}" fill="${EXON_COLOR}" mask="url(#${id})"/>`
4862
+ );
4863
+ }
4864
+ const screen = (p) => ({
4865
+ x: p.x * t.scaleX + t.translateX,
4866
+ y: p.y * t.scaleY + t.translateY
4867
+ });
4868
+ const frame = { width: w, height: h };
4869
+ for (const { item: pin, x, y, w: cw, text } of placeLabels(
4870
+ geneLabelCandidates(pins, screen, contigThickness),
4871
+ frame,
4872
+ occupancy(frame)
4873
+ )) {
4874
+ const pinY = screen(pin.at).y + contigThickness / 2;
4875
+ out.push(
4876
+ `<line x1="${x}" x2="${x}" y1="${y - LABEL_PX - 2}" y2="${pinY}" stroke="${GENE_INK}" stroke-width="0.8" stroke-opacity="0.6"/>`,
4877
+ chip(x, y, cw, pin.gene.name, text.slice(pin.gene.name.length))
4878
+ );
4879
+ }
4880
+ for (const { label, y } of layout.rowLabels ?? []) {
4881
+ const sy = y * t.scaleY + t.translateY;
4882
+ if (sy >= 0 && sy <= h) {
4883
+ out.push(
4884
+ `<text x="6" y="${sy + 4}" ${FONT} stroke="#fff" stroke-width="3" paint-order="stroke">${esc(label)}</text>`
4885
+ );
4886
+ }
4887
+ }
4888
+ return out.join("");
4889
+ }
4541
4890
  function drawing(highlight, w, h) {
4542
4891
  const fit = fitTransform(
4543
4892
  bounds,
@@ -4563,10 +4912,10 @@ function figureSvg(graph, layout, o = {}) {
4563
4912
  graph,
4564
4913
  nodeById,
4565
4914
  colorScheme,
4566
- contigThickness: o.contigThickness ?? 6,
4915
+ contigThickness,
4567
4916
  connectorThickness: o.connectorThickness ?? 2,
4568
4917
  drawPaths: false,
4569
- nodeWidth: o.nodeWidth ?? "depth",
4918
+ nodeWidth: nodeWidth2,
4570
4919
  highlight,
4571
4920
  axis,
4572
4921
  referenceRamp,
@@ -4576,15 +4925,15 @@ function figureSvg(graph, layout, o = {}) {
4576
4925
  )
4577
4926
  })
4578
4927
  );
4579
- renderer.updateTransform({
4928
+ const t = {
4580
4929
  scaleX: axis.scaleX,
4581
4930
  scaleY: axis.scaleY,
4582
4931
  translateX: fit.translateX,
4583
- translateY: fit.translateY,
4584
- dpr: 1
4585
- });
4932
+ translateY: fit.translateY
4933
+ };
4934
+ renderer.updateTransform({ ...t, dpr: 1 });
4586
4935
  renderer.render([1, 1, 1, 1]);
4587
- return markup();
4936
+ return markup() + overlays(highlight, t, w, h);
4588
4937
  }
4589
4938
  const parts = [];
4590
4939
  let height;
@@ -4663,10 +5012,169 @@ function figureSvg(graph, layout, o = {}) {
4663
5012
  );
4664
5013
  height = header + h;
4665
5014
  }
4666
- return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${o.metadata ? `<metadata>${esc(o.metadata)}</metadata>` : ""}<rect width="${width}" height="${height}" fill="#fff"/>${parts.join("")}</svg>
5015
+ const metadata = JSON.stringify({
5016
+ generator: `@jbrowse/bandage-core@${version}`,
5017
+ ...o.spec === void 0 ? {} : { spec: o.spec }
5018
+ });
5019
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><metadata>${esc(metadata)}</metadata><rect width="${width}" height="${height}" fill="#fff"/>${parts.join("")}</svg>
4667
5020
  `;
4668
5021
  }
4669
5022
 
5023
+ // src/genes/geneFiles.ts
5024
+ var GENE_TYPE = /(gene|gene_segment|RNA|transcript)$/i;
5025
+ var isGene = (type) => GENE_TYPE.test(type) || type === "CDS" || type === "exon";
5026
+ function mergedIntervals(intervals) {
5027
+ const out = [];
5028
+ for (const iv of [...intervals].sort((a, b) => a.start - b.start)) {
5029
+ const last = out.at(-1);
5030
+ if (last && iv.start <= last.end) {
5031
+ last.end = Math.max(last.end, iv.end);
5032
+ } else {
5033
+ out.push({ ...iv });
5034
+ }
5035
+ }
5036
+ return out;
5037
+ }
5038
+ function strandOf(s) {
5039
+ return s === "+" ? 1 : s === "-" ? -1 : 0;
5040
+ }
5041
+ function unescape(value) {
5042
+ try {
5043
+ return decodeURIComponent(value);
5044
+ } catch {
5045
+ return value;
5046
+ }
5047
+ }
5048
+ function attributes(column) {
5049
+ const out = /* @__PURE__ */ new Map();
5050
+ for (const pair of column.split(";")) {
5051
+ const eq = pair.indexOf("=");
5052
+ if (eq > 0) {
5053
+ out.set(pair.slice(0, eq).trim(), unescape(pair.slice(eq + 1).trim()));
5054
+ }
5055
+ }
5056
+ return out;
5057
+ }
5058
+ function gff3Row(line) {
5059
+ const cols = line.split(" ");
5060
+ const start = Number(cols[3]);
5061
+ const end = Number(cols[4]);
5062
+ if (cols.length < 9 || !Number.isInteger(start) || !Number.isInteger(end)) {
5063
+ return void 0;
5064
+ }
5065
+ return {
5066
+ refName: cols[0],
5067
+ type: cols[2],
5068
+ start: start - 1,
5069
+ end,
5070
+ strand: strandOf(cols[6]),
5071
+ attrs: attributes(cols[8])
5072
+ };
5073
+ }
5074
+ function genesFromGff3Lines(lines) {
5075
+ const rows = [];
5076
+ for (const line of lines) {
5077
+ if (line.startsWith("##FASTA")) {
5078
+ break;
5079
+ }
5080
+ const row = line.startsWith("#") ? void 0 : gff3Row(line);
5081
+ if (row) {
5082
+ rows.push(row);
5083
+ }
5084
+ }
5085
+ const parentOf = /* @__PURE__ */ new Map();
5086
+ for (const { attrs } of rows) {
5087
+ const id = attrs.get("ID");
5088
+ const parent = attrs.get("Parent")?.split(",")[0];
5089
+ if (id && parent) {
5090
+ parentOf.set(id, parent);
5091
+ }
5092
+ }
5093
+ const rootOf = (row) => {
5094
+ let id = row.attrs.get("Parent")?.split(",")[0] ?? row.attrs.get("ID");
5095
+ if (!id) {
5096
+ return row.attrs.get("gene_id");
5097
+ }
5098
+ const seen = /* @__PURE__ */ new Set();
5099
+ while (id && parentOf.has(id) && !seen.has(id)) {
5100
+ seen.add(id);
5101
+ id = parentOf.get(id);
5102
+ }
5103
+ return id;
5104
+ };
5105
+ const groups = /* @__PURE__ */ new Map();
5106
+ rows.forEach((row, i) => {
5107
+ const root = rootOf(row) ?? `#${i}`;
5108
+ const key = `${row.refName} ${root}`;
5109
+ const group = groups.get(key) ?? groups.set(key, { rows: [] }).get(key);
5110
+ group.rows.push(row);
5111
+ if (!row.attrs.has("Parent") && !group.top) {
5112
+ group.top = row;
5113
+ }
5114
+ });
5115
+ const genes = [];
5116
+ for (const [key, { top, rows: rows2 }] of groups) {
5117
+ if (top && !isGene(top.type)) {
5118
+ continue;
5119
+ }
5120
+ const first = top ?? rows2[0];
5121
+ const start = top?.start ?? Math.min(...rows2.map((r) => r.start));
5122
+ const end = top?.end ?? Math.max(...rows2.map((r) => r.end));
5123
+ const a = first.attrs;
5124
+ const exons = rows2.filter((r) => r.type === "exon").map((r) => ({ start: r.start, end: r.end }));
5125
+ genes.push({
5126
+ name: a.get("Name") ?? a.get("gene_name") ?? a.get("gene") ?? a.get("gene_id") ?? key.split(" ")[1],
5127
+ refName: first.refName,
5128
+ start,
5129
+ end,
5130
+ strand: first.strand,
5131
+ exons: exons.length ? mergedIntervals(exons) : [{ start, end }]
5132
+ });
5133
+ }
5134
+ return genes;
5135
+ }
5136
+ function genesFromBed(text) {
5137
+ const genes = /* @__PURE__ */ new Map();
5138
+ for (const line of text.split("\n")) {
5139
+ const cols = line.replace(/\r$/, "").split(" ");
5140
+ const start = Number(cols[1]);
5141
+ const end = Number(cols[2]);
5142
+ if (/^(#|track|browser)/.test(line) || cols.length < 3 || !Number.isInteger(start) || !(end > start)) {
5143
+ continue;
5144
+ }
5145
+ const refName = cols[0];
5146
+ const name = cols[3] && cols[3] !== "." ? cols[3] : `${refName}:${start + 1}-${end}`;
5147
+ const sizes = cols[10]?.split(",").filter((s) => s !== "").map(Number);
5148
+ const starts = cols[11]?.split(",").filter((s) => s !== "").map(Number);
5149
+ const blocks = sizes?.length && sizes.length === starts?.length ? sizes.map((size, i) => ({
5150
+ start: start + starts[i],
5151
+ end: start + starts[i] + size
5152
+ })) : [{ start, end }];
5153
+ const key = `${refName} ${name}`;
5154
+ const gene = genes.get(key);
5155
+ if (gene) {
5156
+ gene.start = Math.min(gene.start, start);
5157
+ gene.end = Math.max(gene.end, end);
5158
+ gene.exons = mergedIntervals([...gene.exons, ...blocks]);
5159
+ } else {
5160
+ genes.set(key, {
5161
+ name,
5162
+ refName,
5163
+ start,
5164
+ end,
5165
+ strand: strandOf(cols[5]),
5166
+ exons: mergedIntervals(blocks)
5167
+ });
5168
+ }
5169
+ }
5170
+ return [...genes.values()];
5171
+ }
5172
+ function genesFromText(text) {
5173
+ const first = text.split("\n", 1e3).find((l) => l.trim() !== "" && !/^(#|track|browser)/.test(l));
5174
+ const cols = first?.split(" ") ?? [];
5175
+ return /^\d+$/.test(cols[1] ?? "") && /^\d+$/.test(cols[2] ?? "") ? genesFromBed(text) : genesFromGff3Lines(text.split(/\r?\n/));
5176
+ }
5177
+
4670
5178
  // src/reference.ts
4671
5179
  var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
4672
5180
  ["hg38", "GRCh38"],
@@ -4841,9 +5349,9 @@ function resolveReferenceSample({
4841
5349
  }
4842
5350
  return sample;
4843
5351
  }
4844
- async function referencePathQuery(db, referenceSample, refName) {
5352
+ async function referencePathQuery(db, referenceSample2, refName) {
4845
5353
  const path = (await db.paths()).find(
4846
- (p) => p.isIndexed && p.name.sample === referenceSample && p.name.contig === refName
5354
+ (p) => p.isIndexed && p.name.sample === referenceSample2 && p.name.contig === refName
4847
5355
  );
4848
5356
  return path ? {
4849
5357
  sample: path.name.sample,
@@ -4872,6 +5380,60 @@ async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
4872
5380
  return subgraph ? subgraph.toGFA({ names: "resolved" }) : "";
4873
5381
  }
4874
5382
 
5383
+ // src/gbzCut.ts
5384
+ import { GBZBase } from "@gmod/gbz-base";
5385
+ var HPRC_GBZ = {
5386
+ db: "https://s3-us-west-2.amazonaws.com/human-pangenomics/pangenomes/freeze/release2/minigraph-cactus/v2.1/hprc-v2.1-mc-grch38/hprc-v2.1-mc-grch38.gbz.db",
5387
+ index: "https://jbrowse.org/demos/hprc/hprc-v2.1-mc-grch38.haplotype-index.anchored.db"
5388
+ };
5389
+ var WALK_LIMIT = 1e5;
5390
+ function parseRegion(text) {
5391
+ const m = /^\s*([^:\s]+):([\d,]+)-([\d,]+)\s*$/.exec(text);
5392
+ if (!m) {
5393
+ throw new Error(
5394
+ `"${text}" is not a region like chr6:160,614,798-160,647,758`
5395
+ );
5396
+ }
5397
+ const n2 = (s) => Number(s.replaceAll(",", ""));
5398
+ return { refName: m[1], start: n2(m[2]), end: n2(m[3]) };
5399
+ }
5400
+ async function openGbz(db, index) {
5401
+ const base = await GBZBase.open(db, index ? { haplotypeIndex: index } : {});
5402
+ return { base, referenceSamples: await referenceSamplesOf(base) };
5403
+ }
5404
+ function referenceSample(ref, samples) {
5405
+ try {
5406
+ return resolveReferenceSample({
5407
+ configured: "",
5408
+ anchorPrefix: ref ?? samples[0] ?? "",
5409
+ referenceSamples: samples
5410
+ });
5411
+ } catch (e) {
5412
+ if (ref) {
5413
+ return ref;
5414
+ }
5415
+ throw e;
5416
+ }
5417
+ }
5418
+ async function cutGbzRegion({ base, referenceSamples }, src, signal) {
5419
+ const region = parseRegion(src.region);
5420
+ const sample = referenceSample(src.referenceSample, referenceSamples);
5421
+ const query = await referencePathQuery(base, sample, region.refName);
5422
+ if (!query) {
5423
+ throw new Error(`${sample} has no indexed path named ${region.refName}`);
5424
+ }
5425
+ signal?.throwIfAborted();
5426
+ const wanted = src.haplotypes?.length ? src.haplotypes : void 0;
5427
+ const text = await cutWindowGFA(base, query, region.start, region.end, {
5428
+ context: src.context ?? 1e3,
5429
+ snarls: src.snarls ?? "contained",
5430
+ limit: WALK_LIMIT,
5431
+ signal,
5432
+ ...wanted ? { keep: (name) => haplotypeWanted(name, wanted) } : {}
5433
+ });
5434
+ return { text, region, sample };
5435
+ }
5436
+
4875
5437
  // src/loadBandage.ts
4876
5438
  var cached;
4877
5439
  function loadBandage() {
@@ -4884,12 +5446,11 @@ function loadBandage() {
4884
5446
 
4885
5447
  export {
4886
5448
  isBackbone,
4887
- firstNodeAtOrAfter,
4888
5449
  BUBBLE_SPREADS,
4889
5450
  panSNSample,
4890
5451
  panSNHaplotype,
4891
5452
  panSNContig,
4892
- polylineSlice,
5453
+ trimOrigins,
4893
5454
  loadGraph,
4894
5455
  engineRequest,
4895
5456
  engineKey,
@@ -4907,6 +5468,7 @@ export {
4907
5468
  walkRowsExtent,
4908
5469
  FORCE_LAYOUT_LABEL,
4909
5470
  LAYOUT_MODES,
5471
+ LAYOUT_MODE_VALUES,
4910
5472
  modeUsesLayoutEngine,
4911
5473
  layoutModeByValue,
4912
5474
  COLOR_SCHEMES,
@@ -4935,13 +5497,7 @@ export {
4935
5497
  LABEL_PX,
4936
5498
  LABEL_CHAR_PX,
4937
5499
  LABEL_PAD,
4938
- occupancy,
4939
- placeLabels,
4940
- rowLabelBox,
4941
5500
  formatBp,
4942
- sizeLabelCandidates,
4943
- nodeLabelBudget,
4944
- placeSizeLabels,
4945
5501
  WALK_FIELDS,
4946
5502
  WALK_SCHEMES,
4947
5503
  resolveEncoding,
@@ -4953,12 +5509,19 @@ export {
4953
5509
  facetLifts,
4954
5510
  rangeText,
4955
5511
  walkKey,
5512
+ walkPosition,
4956
5513
  FACET_GAP_PX,
4957
5514
  FACET_TITLE_PX,
4958
5515
  FACET_PAD_PX,
4959
5516
  facetGrid,
4960
5517
  facetCells,
5518
+ genePins,
5519
+ HALO_FACTOR,
5520
+ LEGEND_INSET_PX,
5521
+ geneCoverageNote,
5522
+ layoutLabels,
4961
5523
  svgCanvas,
5524
+ version,
4962
5525
  axisScaleOf,
4963
5526
  viewportOf,
4964
5527
  padded,
@@ -4966,6 +5529,9 @@ export {
4966
5529
  zoomAbout,
4967
5530
  screenToLayout,
4968
5531
  figureSvg,
5532
+ genesFromGff3Lines,
5533
+ genesFromBed,
5534
+ genesFromText,
4969
5535
  WELL_KNOWN_SAMPLES,
4970
5536
  wellKnownSample,
4971
5537
  graphBackbone,
@@ -4978,5 +5544,9 @@ export {
4978
5544
  resolveReferenceSample,
4979
5545
  referencePathQuery,
4980
5546
  cutWindowGFA,
5547
+ HPRC_GBZ,
5548
+ parseRegion,
5549
+ openGbz,
5550
+ cutGbzRegion,
4981
5551
  loadBandage
4982
5552
  };