@jbrowse/bandage-core 4.0.24 → 4.0.25

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.
@@ -1772,9 +1772,9 @@ function sampleRowLayout(graph, region, keep = []) {
1772
1772
  },
1773
1773
  positions: nodePositions
1774
1774
  });
1775
- const referenceSample = parsePanSN(backbone[0].stable.refName).sample;
1775
+ const referenceSample2 = parsePanSN(backbone[0].stable.refName).sample;
1776
1776
  const rowLabels = [
1777
- { label: referenceSample, y: 0 },
1777
+ { label: referenceSample2, y: 0 },
1778
1778
  ...samples.map((sample) => ({
1779
1779
  label: sample,
1780
1780
  y: rowOf.get(sample) * ROW_HEIGHT_PX
@@ -2818,10 +2818,10 @@ function brightenAbgr(c, factor) {
2818
2818
 
2819
2819
  // src/util/edgeCurves.ts
2820
2820
  var cache = /* @__PURE__ */ new WeakMap();
2821
- function baseEdgeCurves(nodePositions, graph, axis, deletions, version) {
2821
+ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2) {
2822
2822
  const { scaleX, scaleY } = axis;
2823
2823
  const cached2 = cache.get(nodePositions);
2824
- if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.version === version) {
2824
+ if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.version === version2) {
2825
2825
  return cached2.curves;
2826
2826
  }
2827
2827
  const curves = /* @__PURE__ */ new Map();
@@ -2850,7 +2850,7 @@ function baseEdgeCurves(nodePositions, graph, axis, deletions, version) {
2850
2850
  scaleX,
2851
2851
  scaleY,
2852
2852
  deletions,
2853
- version,
2853
+ version: version2,
2854
2854
  curves
2855
2855
  });
2856
2856
  return curves;
@@ -3312,7 +3312,7 @@ function buildGeometry(options) {
3312
3312
  referenceRamp,
3313
3313
  deletions,
3314
3314
  hiddenEdges,
3315
- version = 0
3315
+ version: version2 = 0
3316
3316
  } = options;
3317
3317
  const depthNorm = nodeWidth2 === "depth" ? meanDepth(graph) : 0;
3318
3318
  const scale = axis.scaleX;
@@ -3322,7 +3322,7 @@ function buildGeometry(options) {
3322
3322
  graph,
3323
3323
  axis,
3324
3324
  deletions,
3325
- version
3325
+ version2
3326
3326
  );
3327
3327
  const nodeStrokes = [];
3328
3328
  const nodeStrokeRuns = /* @__PURE__ */ new Map();
@@ -3871,10 +3871,10 @@ function labelOrder(nodePositions, nodeLengths) {
3871
3871
  return order;
3872
3872
  }
3873
3873
  var arcCache = /* @__PURE__ */ new WeakMap();
3874
- function arcPlacements(nodePositions, deletions, axis, version) {
3874
+ function arcPlacements(nodePositions, deletions, axis, version2) {
3875
3875
  const { scaleX, scaleY } = axis;
3876
3876
  const cached2 = arcCache.get(nodePositions);
3877
- if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
3877
+ if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version2) {
3878
3878
  return cached2.arcs;
3879
3879
  }
3880
3880
  const arcs = [];
@@ -3892,7 +3892,7 @@ function arcPlacements(nodePositions, deletions, axis, version) {
3892
3892
  });
3893
3893
  }
3894
3894
  }
3895
- arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
3895
+ arcCache.set(nodePositions, { deletions, scaleX, scaleY, version: version2, arcs });
3896
3896
  return arcs;
3897
3897
  }
3898
3898
  function sizeLabelCandidates({
@@ -3905,7 +3905,7 @@ function sizeLabelCandidates({
3905
3905
  translateY,
3906
3906
  width,
3907
3907
  height,
3908
- version = 0
3908
+ version: version2 = 0
3909
3909
  }) {
3910
3910
  const { scaleX, scaleY } = axis;
3911
3911
  const deletionCandidates = [];
@@ -3914,7 +3914,7 @@ function sizeLabelCandidates({
3914
3914
  nodePositions,
3915
3915
  deletions,
3916
3916
  axis,
3917
- version
3917
+ version2
3918
3918
  )) {
3919
3919
  if (extent >= MIN_DELETION_LABEL_PX) {
3920
3920
  const text = deletionText(deletion.bp);
@@ -4292,6 +4292,15 @@ function walkKey(walk, reference) {
4292
4292
  hover: scale === own ? void 0 : own
4293
4293
  };
4294
4294
  }
4295
+ function walkPosition(walk, nodeId, length) {
4296
+ const progress = walk.progress.get(nodeId);
4297
+ const range = walk.range;
4298
+ if (progress === void 0 || !range) {
4299
+ return void 0;
4300
+ }
4301
+ const start = Math.round(range.start + progress * walk.bp - length / 2);
4302
+ return `${range.contig}:${start.toLocaleString()}-${(start + length).toLocaleString()}`;
4303
+ }
4295
4304
 
4296
4305
  // src/facetGrid.ts
4297
4306
  var FACET_GAP_PX = 8;
@@ -4364,6 +4373,223 @@ function facetCells(names, by) {
4364
4373
  return cells.includes(-1) || new Set(cells).size < cells.length ? wrap : { columns, count: rows.length * columns, cells };
4365
4374
  }
4366
4375
 
4376
+ // src/genes/genePins.ts
4377
+ function genePins(graph, genes, positions) {
4378
+ const byRefName = /* @__PURE__ */ new Map();
4379
+ for (const node of graph.nodes) {
4380
+ if (isBackbone(node)) {
4381
+ const { refName } = node.stable;
4382
+ if (positions[node.id]?.length) {
4383
+ const entry = byRefName.get(refName) ?? byRefName.set(refName, { nodes: [], reach: 0 }).get(refName);
4384
+ entry.nodes.push(node);
4385
+ entry.reach = Math.max(entry.reach, node.length);
4386
+ }
4387
+ }
4388
+ }
4389
+ for (const { nodes } of byRefName.values()) {
4390
+ nodes.sort((a, b) => a.stable.start - b.stable.start);
4391
+ }
4392
+ const pins = [];
4393
+ for (const gene of genes) {
4394
+ const backbone = byRefName.get(gene.refName);
4395
+ if (!backbone) {
4396
+ continue;
4397
+ }
4398
+ const parts = [];
4399
+ const exonsByNode = [];
4400
+ let at;
4401
+ let atDistance = Infinity;
4402
+ let covered = 0;
4403
+ const mid = (gene.start + gene.end) / 2;
4404
+ const { nodes, reach } = backbone;
4405
+ for (let i = firstNodeAtOrAfter(nodes, gene.start - reach); i < nodes.length && nodes[i].stable.start < gene.end; i++) {
4406
+ const node = nodes[i];
4407
+ const nodeStart = node.stable.start;
4408
+ const nodeEnd = nodeStart + node.length;
4409
+ if (nodeEnd <= gene.start) {
4410
+ continue;
4411
+ }
4412
+ const line = positions[node.id];
4413
+ covered += Math.min(nodeEnd, gene.end) - Math.max(nodeStart, gene.start);
4414
+ const nodeParts = [];
4415
+ for (const exon of gene.exons) {
4416
+ const a = Math.max(exon.start, nodeStart);
4417
+ const b = Math.min(exon.end, nodeEnd);
4418
+ if (b <= a) {
4419
+ continue;
4420
+ }
4421
+ const stretch = polylineSlice(
4422
+ line,
4423
+ (a - nodeStart) / node.length,
4424
+ (b - nodeStart) / node.length
4425
+ );
4426
+ if (stretch.length === 1) {
4427
+ stretch.push({ ...stretch[0] });
4428
+ }
4429
+ nodeParts.push(svgPath(stretch));
4430
+ }
4431
+ if (nodeParts.length > 0) {
4432
+ parts.push(...nodeParts);
4433
+ exonsByNode.push({ nodeId: node.id, d: nodeParts.join("") });
4434
+ }
4435
+ const pinBp = Math.min(Math.max(mid, nodeStart), nodeEnd);
4436
+ const distance = Math.abs(pinBp - mid);
4437
+ if (distance < atDistance) {
4438
+ atDistance = distance;
4439
+ const [p] = polylineSlice(
4440
+ line,
4441
+ (pinBp - nodeStart) / node.length,
4442
+ (pinBp - nodeStart) / node.length
4443
+ );
4444
+ at = p;
4445
+ }
4446
+ }
4447
+ if (at) {
4448
+ pins.push({
4449
+ gene,
4450
+ exons: parts.join(""),
4451
+ exonsByNode,
4452
+ at,
4453
+ covered: covered / (gene.end - gene.start)
4454
+ });
4455
+ }
4456
+ }
4457
+ return pins;
4458
+ }
4459
+
4460
+ // src/labelLayout.ts
4461
+ var HALO_FACTOR = 3.4;
4462
+ var LEGEND_INSET_PX = 6;
4463
+ var ROUTE_STACK = 8;
4464
+ var GENE_PIN_DROP_PX = 18;
4465
+ var GENE_STACK = 2;
4466
+ var TOPMOST_BASELINE = LABEL_PX + LABEL_PAD;
4467
+ function geneCoverageNote(pin) {
4468
+ const length = pin.gene.end - pin.gene.start;
4469
+ if (pin.covered >= 0.98) {
4470
+ return void 0;
4471
+ }
4472
+ const [per, unit] = length >= 1e6 ? [1e6, "Mb"] : length >= 1e3 ? [1e3, "kb"] : [1, "bp"];
4473
+ const amount = (bp) => +(bp / per).toFixed(per === 1 ? 0 : 1);
4474
+ return `${amount(pin.covered * length)} of ${amount(length)} ${unit}`;
4475
+ }
4476
+ function geneLabelCandidates(pins, screen, contigThickness) {
4477
+ return byExtent(pins, (pin) => pin.gene.end - pin.gene.start).map((pin) => {
4478
+ const { x, y } = screen(pin.at);
4479
+ const note = geneCoverageNote(pin);
4480
+ return {
4481
+ item: pin,
4482
+ x,
4483
+ y: y + contigThickness + GENE_PIN_DROP_PX,
4484
+ text: note ? `${pin.gene.name} \xB7 ${note}` : pin.gene.name,
4485
+ fallback: note ? `${pin.gene.name} \u2026` : void 0,
4486
+ stack: GENE_STACK
4487
+ };
4488
+ });
4489
+ }
4490
+ function byExtent(items, extent) {
4491
+ return [...items].sort((a, b) => extent(b) - extent(a));
4492
+ }
4493
+ function layoutLabels(m) {
4494
+ const { paneWidth: width, canvasHeight: height, translateX, translateY } = m;
4495
+ const { scaleX, scaleY } = m.axisScale;
4496
+ const frame = { width, height };
4497
+ const screen = (p) => ({
4498
+ x: p.x * scaleX + translateX,
4499
+ y: p.y * scaleY + translateY
4500
+ });
4501
+ const reserved = m.drawnRowLabels.map(
4502
+ ({ label, y }) => rowLabelBox(label, y * scaleY + translateY)
4503
+ );
4504
+ const stripPx = m.referenceStripZonePx ?? 0;
4505
+ if (stripPx > 0) {
4506
+ reserved.push({ x0: 0, x1: width, y0: 0, y1: stripPx });
4507
+ }
4508
+ if (m.legendSize.width > 0) {
4509
+ reserved.push({
4510
+ x0: width - LEGEND_INSET_PX - m.legendSize.width,
4511
+ x1: width,
4512
+ y0: 0,
4513
+ y1: stripPx + LEGEND_INSET_PX + m.legendSize.height
4514
+ });
4515
+ }
4516
+ if (m.poppedFrom) {
4517
+ reserved.push({
4518
+ x0: 0,
4519
+ x1: 60 + m.poppedFrom.label.length * LABEL_CHAR_PX * 1.2,
4520
+ y0: 0,
4521
+ y1: 44
4522
+ });
4523
+ }
4524
+ const take = occupancy(frame, reserved);
4525
+ const onRows = m.drawnRowLabels.length > 0;
4526
+ const rowsTop = onRows ? Math.min(...m.drawnRowLabels.map((r) => r.y)) : Infinity;
4527
+ const halo = m.contigThickness * HALO_FACTOR;
4528
+ const byBubble = byExtent(m.bubbleHalos, (h) => h.members);
4529
+ const bubbles = placeLabels(
4530
+ byBubble.map((h) => {
4531
+ const { x, y } = screen({
4532
+ x: h.labelAt.x,
4533
+ y: Math.min(h.labelAt.y, rowsTop)
4534
+ });
4535
+ const baseline = y - halo / 2 - 6;
4536
+ return {
4537
+ item: h,
4538
+ x,
4539
+ y: onRows ? Math.max(baseline, TOPMOST_BASELINE) : baseline,
4540
+ text: h.label
4541
+ };
4542
+ }),
4543
+ frame,
4544
+ take
4545
+ );
4546
+ const genes = placeLabels(
4547
+ geneLabelCandidates(m.genePins, screen, m.contigThickness),
4548
+ frame,
4549
+ take
4550
+ );
4551
+ const sizeCandidates = m.nodePositions && m.labelsNodeSizes ? sizeLabelCandidates({
4552
+ nodePositions: m.nodePositions,
4553
+ nodeLengths: m.nodeLengths,
4554
+ deletions: m.showDeletionEdges ? m.deletions : [],
4555
+ alleleDeletions: m.alleleDeletions,
4556
+ axis: m.axisScale,
4557
+ translateX,
4558
+ translateY,
4559
+ width,
4560
+ height,
4561
+ version: m.positionsVersion
4562
+ }) : { deletions: [], nodes: [] };
4563
+ const deletionLabels = placeSizeLabels(sizeCandidates.deletions, take);
4564
+ const routes = placeLabels(
4565
+ byBubble.flatMap(
4566
+ (h) => h.routes.map((route) => {
4567
+ const { x, y } = screen(route.at);
4568
+ return {
4569
+ item: { halo: h, route },
4570
+ x,
4571
+ y: y + 4,
4572
+ text: route.text,
4573
+ stack: ROUTE_STACK
4574
+ };
4575
+ })
4576
+ ),
4577
+ frame,
4578
+ take
4579
+ );
4580
+ const nodeLabels = placeSizeLabels(
4581
+ sizeCandidates.nodes,
4582
+ take,
4583
+ nodeLabelBudget(width, height)
4584
+ );
4585
+ return {
4586
+ bubbles,
4587
+ genes,
4588
+ routes,
4589
+ sizes: [...deletionLabels, ...nodeLabels]
4590
+ };
4591
+ }
4592
+
4367
4593
  // src/renderer/svgCanvas.ts
4368
4594
  var n = (v) => +v.toFixed(1);
4369
4595
  function paint(css) {
@@ -4380,9 +4606,43 @@ function paintAttrs(kind, css) {
4380
4606
  const { color, opacity } = paint(css);
4381
4607
  return `${kind}="${color}"${opacity < 1 ? ` ${kind}-opacity="${n(opacity)}"` : ""}`;
4382
4608
  }
4609
+ var DOT_PX = 0.5;
4610
+ var DOT_CELL_PX = 0.25;
4383
4611
  function svgCanvas(width, height) {
4384
4612
  const out = [];
4385
- let d = "";
4613
+ let subpaths = [];
4614
+ const start = (x, y) => {
4615
+ const s = { d: `M${n(x)} ${n(y)}`, segments: 0, x0: x, x1: x, y0: y, y1: y };
4616
+ subpaths.push(s);
4617
+ return s;
4618
+ };
4619
+ const reach = (points) => {
4620
+ const s = subpaths.at(-1) ?? start(points[0], points[1]);
4621
+ for (let i = 0; i < points.length; i += 2) {
4622
+ s.x0 = Math.min(s.x0, points[i]);
4623
+ s.x1 = Math.max(s.x1, points[i]);
4624
+ s.y0 = Math.min(s.y0, points[i + 1]);
4625
+ s.y1 = Math.max(s.y1, points[i + 1]);
4626
+ }
4627
+ s.segments++;
4628
+ return s;
4629
+ };
4630
+ const pathData = (dots) => {
4631
+ const seen = /* @__PURE__ */ new Set();
4632
+ return subpaths.map((s) => {
4633
+ if (!dots || s.x1 - s.x0 >= DOT_PX || s.y1 - s.y0 >= DOT_PX) {
4634
+ return s.d;
4635
+ }
4636
+ const x = (s.x0 + s.x1) / 2;
4637
+ const y = (s.y0 + s.y1) / 2;
4638
+ const cell = `${Math.round(x / DOT_CELL_PX)} ${Math.round(y / DOT_CELL_PX)}`;
4639
+ if (s.segments === 0 || seen.has(cell)) {
4640
+ return "";
4641
+ }
4642
+ seen.add(cell);
4643
+ return `M${n(x)} ${n(y)}h0`;
4644
+ }).join("");
4645
+ };
4386
4646
  const ctx = {
4387
4647
  canvas: void 0,
4388
4648
  fillStyle: "#000",
@@ -4391,21 +4651,25 @@ function svgCanvas(width, height) {
4391
4651
  lineCap: "butt",
4392
4652
  lineJoin: "miter",
4393
4653
  beginPath() {
4394
- d = "";
4654
+ subpaths = [];
4395
4655
  },
4396
4656
  moveTo(x, y) {
4397
- d += `M${n(x)} ${n(y)}`;
4657
+ start(x, y);
4398
4658
  },
4399
4659
  lineTo(x, y) {
4400
- d += `L${n(x)} ${n(y)}`;
4660
+ reach([x, y]).d += `L${n(x)} ${n(y)}`;
4401
4661
  },
4402
4662
  bezierCurveTo(cx0, cy0, cx1, cy1, x, y) {
4403
- d += `C${n(cx0)} ${n(cy0)} ${n(cx1)} ${n(cy1)} ${n(x)} ${n(y)}`;
4663
+ reach([cx0, cy0, cx1, cy1, x, y]).d += `C${n(cx0)} ${n(cy0)} ${n(cx1)} ${n(cy1)} ${n(x)} ${n(y)}`;
4404
4664
  },
4405
4665
  closePath() {
4406
- d += "Z";
4666
+ const s = subpaths.at(-1);
4667
+ if (s) {
4668
+ s.d += "Z";
4669
+ }
4407
4670
  },
4408
4671
  stroke() {
4672
+ const d = pathData(ctx.lineCap !== "butt");
4409
4673
  if (d) {
4410
4674
  out.push(
4411
4675
  `<path d="${d}" fill="none" ${paintAttrs("stroke", ctx.strokeStyle)} stroke-width="${n(ctx.lineWidth)}" stroke-linecap="${ctx.lineCap}" stroke-linejoin="${ctx.lineJoin}"/>`
@@ -4413,6 +4677,7 @@ function svgCanvas(width, height) {
4413
4677
  }
4414
4678
  },
4415
4679
  fill() {
4680
+ const d = pathData(false);
4416
4681
  if (d) {
4417
4682
  out.push(`<path d="${d}" ${paintAttrs("fill", ctx.fillStyle)}/>`);
4418
4683
  }
@@ -4437,6 +4702,9 @@ function svgCanvas(width, height) {
4437
4702
  };
4438
4703
  }
4439
4704
 
4705
+ // src/version.ts
4706
+ var version = "4.0.25";
4707
+
4440
4708
  // src/viewport.ts
4441
4709
  function axisScaleOf(scale, pixelRows) {
4442
4710
  return { scaleX: scale, scaleY: pixelRows ? 1 : scale, pixelRows };
@@ -4484,6 +4752,11 @@ var BAR_PX = 48;
4484
4752
  var SWATCH_PX = 18;
4485
4753
  var KEY_GAP_PX = 16;
4486
4754
  var FADED = "rgb(160,160,160)";
4755
+ var EXON_COLOR = "#daa520";
4756
+ var GENE_INK = "#1c1c22";
4757
+ var MIN_LANE_PX = 4;
4758
+ var EXON_GAP_PX = 1;
4759
+ var EXON_LINE_PX = 2;
4487
4760
  function esc(s) {
4488
4761
  return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4489
4762
  }
@@ -4512,6 +4785,15 @@ function walkKeySvg(id, x, y, walk, label, reference) {
4512
4785
  key.scale
4513
4786
  );
4514
4787
  }
4788
+ function screenPath(d, t) {
4789
+ return d.replaceAll(
4790
+ /([ML])(-?[\d.e-]+),(-?[\d.e-]+)/g,
4791
+ (_, cmd, x, y) => `${cmd}${+(Number(x) * t.scaleX + t.translateX).toFixed(1)},${+(Number(y) * t.scaleY + t.translateY).toFixed(1)}`
4792
+ );
4793
+ }
4794
+ function chip(x, y, w, name, note) {
4795
+ 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>`;
4796
+ }
4515
4797
  function figureSvg(graph, layout, o = {}) {
4516
4798
  const width = o.width ?? 1200;
4517
4799
  const room = o.height ?? 800;
@@ -4538,6 +4820,58 @@ function figureSvg(graph, layout, o = {}) {
4538
4820
  ...lift.referenceDomain,
4539
4821
  name: region?.refName
4540
4822
  };
4823
+ const contigThickness = o.contigThickness ?? 6;
4824
+ const nodeWidth2 = o.nodeWidth ?? "depth";
4825
+ const pins = o.genes?.length && !layout.tubeMap ? genePins(graph, o.genes, layout.nodePositions) : [];
4826
+ const ink = nodeInk(graph, nodeById, contigThickness, nodeWidth2);
4827
+ let masks = 0;
4828
+ function overlays(highlight, t, w, h) {
4829
+ const out = [];
4830
+ const inkPx = (nodeId) => {
4831
+ const own = ink.halfWidthPx(nodeId) * 2;
4832
+ return highlight?.nodeIds.has(nodeId) ? Math.max(own, highlight.walks.length * MIN_LANE_PX) : own;
4833
+ };
4834
+ const stretches = pins.flatMap(
4835
+ (pin) => pin.exonsByNode.map(({ nodeId, d }) => ({
4836
+ d: screenPath(d, t),
4837
+ inner: inkPx(nodeId) + 2 * EXON_GAP_PX
4838
+ }))
4839
+ );
4840
+ if (stretches.length > 0) {
4841
+ const id = `exons${masks++}`;
4842
+ const stroke = (color, width2, d) => `<path d="${d}" fill="none" stroke="${color}" stroke-width="${width2}" stroke-linecap="round" stroke-linejoin="round"/>`;
4843
+ out.push(
4844
+ `<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(
4845
+ ""
4846
+ )}</mask><rect width="${w}" height="${h}" fill="${EXON_COLOR}" mask="url(#${id})"/>`
4847
+ );
4848
+ }
4849
+ const screen = (p) => ({
4850
+ x: p.x * t.scaleX + t.translateX,
4851
+ y: p.y * t.scaleY + t.translateY
4852
+ });
4853
+ const frame = { width: w, height: h };
4854
+ for (const { item: pin, x, y, w: cw, text } of placeLabels(
4855
+ geneLabelCandidates(pins, screen, contigThickness),
4856
+ frame,
4857
+ occupancy(frame)
4858
+ )) {
4859
+ const pinY = screen(pin.at).y + contigThickness / 2;
4860
+ out.push(
4861
+ `<line x1="${x}" x2="${x}" y1="${y - LABEL_PX - 2}" y2="${pinY}" stroke="${GENE_INK}" stroke-width="0.8" stroke-opacity="0.6"/>`,
4862
+ chip(x, y, cw, pin.gene.name, text.slice(pin.gene.name.length))
4863
+ );
4864
+ }
4865
+ for (const { label, y } of layout.rowLabels ?? []) {
4866
+ const sy = y * t.scaleY + t.translateY;
4867
+ if (sy >= 0 && sy <= h) {
4868
+ out.push(
4869
+ `<text x="6" y="${sy + 4}" ${FONT} stroke="#fff" stroke-width="3" paint-order="stroke">${esc(label)}</text>`
4870
+ );
4871
+ }
4872
+ }
4873
+ return out.join("");
4874
+ }
4541
4875
  function drawing(highlight, w, h) {
4542
4876
  const fit = fitTransform(
4543
4877
  bounds,
@@ -4563,10 +4897,10 @@ function figureSvg(graph, layout, o = {}) {
4563
4897
  graph,
4564
4898
  nodeById,
4565
4899
  colorScheme,
4566
- contigThickness: o.contigThickness ?? 6,
4900
+ contigThickness,
4567
4901
  connectorThickness: o.connectorThickness ?? 2,
4568
4902
  drawPaths: false,
4569
- nodeWidth: o.nodeWidth ?? "depth",
4903
+ nodeWidth: nodeWidth2,
4570
4904
  highlight,
4571
4905
  axis,
4572
4906
  referenceRamp,
@@ -4576,15 +4910,15 @@ function figureSvg(graph, layout, o = {}) {
4576
4910
  )
4577
4911
  })
4578
4912
  );
4579
- renderer.updateTransform({
4913
+ const t = {
4580
4914
  scaleX: axis.scaleX,
4581
4915
  scaleY: axis.scaleY,
4582
4916
  translateX: fit.translateX,
4583
- translateY: fit.translateY,
4584
- dpr: 1
4585
- });
4917
+ translateY: fit.translateY
4918
+ };
4919
+ renderer.updateTransform({ ...t, dpr: 1 });
4586
4920
  renderer.render([1, 1, 1, 1]);
4587
- return markup();
4921
+ return markup() + overlays(highlight, t, w, h);
4588
4922
  }
4589
4923
  const parts = [];
4590
4924
  let height;
@@ -4663,10 +4997,169 @@ function figureSvg(graph, layout, o = {}) {
4663
4997
  );
4664
4998
  height = header + h;
4665
4999
  }
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>
5000
+ const metadata = JSON.stringify({
5001
+ generator: `@jbrowse/bandage-core@${version}`,
5002
+ ...o.spec === void 0 ? {} : { spec: o.spec }
5003
+ });
5004
+ 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
5005
  `;
4668
5006
  }
4669
5007
 
5008
+ // src/genes/geneFiles.ts
5009
+ var GENE_TYPE = /(gene|gene_segment|RNA|transcript)$/i;
5010
+ var isGene = (type) => GENE_TYPE.test(type) || type === "CDS" || type === "exon";
5011
+ function mergedIntervals(intervals) {
5012
+ const out = [];
5013
+ for (const iv of [...intervals].sort((a, b) => a.start - b.start)) {
5014
+ const last = out.at(-1);
5015
+ if (last && iv.start <= last.end) {
5016
+ last.end = Math.max(last.end, iv.end);
5017
+ } else {
5018
+ out.push({ ...iv });
5019
+ }
5020
+ }
5021
+ return out;
5022
+ }
5023
+ function strandOf(s) {
5024
+ return s === "+" ? 1 : s === "-" ? -1 : 0;
5025
+ }
5026
+ function unescape(value) {
5027
+ try {
5028
+ return decodeURIComponent(value);
5029
+ } catch {
5030
+ return value;
5031
+ }
5032
+ }
5033
+ function attributes(column) {
5034
+ const out = /* @__PURE__ */ new Map();
5035
+ for (const pair of column.split(";")) {
5036
+ const eq = pair.indexOf("=");
5037
+ if (eq > 0) {
5038
+ out.set(pair.slice(0, eq).trim(), unescape(pair.slice(eq + 1).trim()));
5039
+ }
5040
+ }
5041
+ return out;
5042
+ }
5043
+ function gff3Row(line) {
5044
+ const cols = line.split(" ");
5045
+ const start = Number(cols[3]);
5046
+ const end = Number(cols[4]);
5047
+ if (cols.length < 9 || !Number.isInteger(start) || !Number.isInteger(end)) {
5048
+ return void 0;
5049
+ }
5050
+ return {
5051
+ refName: cols[0],
5052
+ type: cols[2],
5053
+ start: start - 1,
5054
+ end,
5055
+ strand: strandOf(cols[6]),
5056
+ attrs: attributes(cols[8])
5057
+ };
5058
+ }
5059
+ function genesFromGff3Lines(lines) {
5060
+ const rows = [];
5061
+ for (const line of lines) {
5062
+ if (line.startsWith("##FASTA")) {
5063
+ break;
5064
+ }
5065
+ const row = line.startsWith("#") ? void 0 : gff3Row(line);
5066
+ if (row) {
5067
+ rows.push(row);
5068
+ }
5069
+ }
5070
+ const parentOf = /* @__PURE__ */ new Map();
5071
+ for (const { attrs } of rows) {
5072
+ const id = attrs.get("ID");
5073
+ const parent = attrs.get("Parent")?.split(",")[0];
5074
+ if (id && parent) {
5075
+ parentOf.set(id, parent);
5076
+ }
5077
+ }
5078
+ const rootOf = (row) => {
5079
+ let id = row.attrs.get("Parent")?.split(",")[0] ?? row.attrs.get("ID");
5080
+ if (!id) {
5081
+ return row.attrs.get("gene_id");
5082
+ }
5083
+ const seen = /* @__PURE__ */ new Set();
5084
+ while (id && parentOf.has(id) && !seen.has(id)) {
5085
+ seen.add(id);
5086
+ id = parentOf.get(id);
5087
+ }
5088
+ return id;
5089
+ };
5090
+ const groups = /* @__PURE__ */ new Map();
5091
+ rows.forEach((row, i) => {
5092
+ const root = rootOf(row) ?? `#${i}`;
5093
+ const key = `${row.refName} ${root}`;
5094
+ const group = groups.get(key) ?? groups.set(key, { rows: [] }).get(key);
5095
+ group.rows.push(row);
5096
+ if (!row.attrs.has("Parent") && !group.top) {
5097
+ group.top = row;
5098
+ }
5099
+ });
5100
+ const genes = [];
5101
+ for (const [key, { top, rows: rows2 }] of groups) {
5102
+ if (top && !isGene(top.type)) {
5103
+ continue;
5104
+ }
5105
+ const first = top ?? rows2[0];
5106
+ const start = top?.start ?? Math.min(...rows2.map((r) => r.start));
5107
+ const end = top?.end ?? Math.max(...rows2.map((r) => r.end));
5108
+ const a = first.attrs;
5109
+ const exons = rows2.filter((r) => r.type === "exon").map((r) => ({ start: r.start, end: r.end }));
5110
+ genes.push({
5111
+ name: a.get("Name") ?? a.get("gene_name") ?? a.get("gene") ?? a.get("gene_id") ?? key.split(" ")[1],
5112
+ refName: first.refName,
5113
+ start,
5114
+ end,
5115
+ strand: first.strand,
5116
+ exons: exons.length ? mergedIntervals(exons) : [{ start, end }]
5117
+ });
5118
+ }
5119
+ return genes;
5120
+ }
5121
+ function genesFromBed(text) {
5122
+ const genes = /* @__PURE__ */ new Map();
5123
+ for (const line of text.split("\n")) {
5124
+ const cols = line.replace(/\r$/, "").split(" ");
5125
+ const start = Number(cols[1]);
5126
+ const end = Number(cols[2]);
5127
+ if (/^(#|track|browser)/.test(line) || cols.length < 3 || !Number.isInteger(start) || !(end > start)) {
5128
+ continue;
5129
+ }
5130
+ const refName = cols[0];
5131
+ const name = cols[3] || `${refName}:${start + 1}-${end}`;
5132
+ const sizes = cols[10]?.split(",").filter((s) => s !== "").map(Number);
5133
+ const starts = cols[11]?.split(",").filter((s) => s !== "").map(Number);
5134
+ const blocks = sizes?.length && sizes.length === starts?.length ? sizes.map((size, i) => ({
5135
+ start: start + starts[i],
5136
+ end: start + starts[i] + size
5137
+ })) : [{ start, end }];
5138
+ const key = `${refName} ${name}`;
5139
+ const gene = genes.get(key);
5140
+ if (gene) {
5141
+ gene.start = Math.min(gene.start, start);
5142
+ gene.end = Math.max(gene.end, end);
5143
+ gene.exons = mergedIntervals([...gene.exons, ...blocks]);
5144
+ } else {
5145
+ genes.set(key, {
5146
+ name,
5147
+ refName,
5148
+ start,
5149
+ end,
5150
+ strand: strandOf(cols[5]),
5151
+ exons: mergedIntervals(blocks)
5152
+ });
5153
+ }
5154
+ }
5155
+ return [...genes.values()];
5156
+ }
5157
+ function genesFromText(text) {
5158
+ const first = text.split("\n", 1e3).find((l) => l.trim() !== "" && !/^(#|track|browser)/.test(l));
5159
+ const cols = first?.split(" ") ?? [];
5160
+ return /^\d+$/.test(cols[1] ?? "") && /^\d+$/.test(cols[2] ?? "") ? genesFromBed(text) : genesFromGff3Lines(text.split(/\r?\n/));
5161
+ }
5162
+
4670
5163
  // src/reference.ts
4671
5164
  var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
4672
5165
  ["hg38", "GRCh38"],
@@ -4841,9 +5334,9 @@ function resolveReferenceSample({
4841
5334
  }
4842
5335
  return sample;
4843
5336
  }
4844
- async function referencePathQuery(db, referenceSample, refName) {
5337
+ async function referencePathQuery(db, referenceSample2, refName) {
4845
5338
  const path = (await db.paths()).find(
4846
- (p) => p.isIndexed && p.name.sample === referenceSample && p.name.contig === refName
5339
+ (p) => p.isIndexed && p.name.sample === referenceSample2 && p.name.contig === refName
4847
5340
  );
4848
5341
  return path ? {
4849
5342
  sample: path.name.sample,
@@ -4872,6 +5365,60 @@ async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
4872
5365
  return subgraph ? subgraph.toGFA({ names: "resolved" }) : "";
4873
5366
  }
4874
5367
 
5368
+ // src/gbzCut.ts
5369
+ import { GBZBase } from "@gmod/gbz-base";
5370
+ var HPRC_GBZ = {
5371
+ 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",
5372
+ index: "https://jbrowse.org/demos/hprc/hprc-v2.1-mc-grch38.haplotype-index.anchored.db"
5373
+ };
5374
+ var WALK_LIMIT = 1e5;
5375
+ function parseRegion(text) {
5376
+ const m = /^\s*([^:\s]+):([\d,]+)-([\d,]+)\s*$/.exec(text);
5377
+ if (!m) {
5378
+ throw new Error(
5379
+ `"${text}" is not a region like chr6:160,614,798-160,647,758`
5380
+ );
5381
+ }
5382
+ const n2 = (s) => Number(s.replaceAll(",", ""));
5383
+ return { refName: m[1], start: n2(m[2]), end: n2(m[3]) };
5384
+ }
5385
+ async function openGbz(db, index) {
5386
+ const base = await GBZBase.open(db, index ? { haplotypeIndex: index } : {});
5387
+ return { base, referenceSamples: await referenceSamplesOf(base) };
5388
+ }
5389
+ function referenceSample(ref, samples) {
5390
+ try {
5391
+ return resolveReferenceSample({
5392
+ configured: "",
5393
+ anchorPrefix: ref ?? samples[0] ?? "",
5394
+ referenceSamples: samples
5395
+ });
5396
+ } catch (e) {
5397
+ if (ref) {
5398
+ return ref;
5399
+ }
5400
+ throw e;
5401
+ }
5402
+ }
5403
+ async function cutGbzRegion({ base, referenceSamples }, src, signal) {
5404
+ const region = parseRegion(src.region);
5405
+ const sample = referenceSample(src.referenceSample, referenceSamples);
5406
+ const query = await referencePathQuery(base, sample, region.refName);
5407
+ if (!query) {
5408
+ throw new Error(`${sample} has no indexed path named ${region.refName}`);
5409
+ }
5410
+ signal?.throwIfAborted();
5411
+ const wanted = src.haplotypes?.length ? src.haplotypes : void 0;
5412
+ const text = await cutWindowGFA(base, query, region.start, region.end, {
5413
+ context: src.context ?? 1e3,
5414
+ snarls: src.snarls ?? "contained",
5415
+ limit: WALK_LIMIT,
5416
+ signal,
5417
+ ...wanted ? { keep: (name) => haplotypeWanted(name, wanted) } : {}
5418
+ });
5419
+ return { text, region, sample };
5420
+ }
5421
+
4875
5422
  // src/loadBandage.ts
4876
5423
  var cached;
4877
5424
  function loadBandage() {
@@ -4884,12 +5431,10 @@ function loadBandage() {
4884
5431
 
4885
5432
  export {
4886
5433
  isBackbone,
4887
- firstNodeAtOrAfter,
4888
5434
  BUBBLE_SPREADS,
4889
5435
  panSNSample,
4890
5436
  panSNHaplotype,
4891
5437
  panSNContig,
4892
- polylineSlice,
4893
5438
  loadGraph,
4894
5439
  engineRequest,
4895
5440
  engineKey,
@@ -4935,13 +5480,7 @@ export {
4935
5480
  LABEL_PX,
4936
5481
  LABEL_CHAR_PX,
4937
5482
  LABEL_PAD,
4938
- occupancy,
4939
- placeLabels,
4940
- rowLabelBox,
4941
5483
  formatBp,
4942
- sizeLabelCandidates,
4943
- nodeLabelBudget,
4944
- placeSizeLabels,
4945
5484
  WALK_FIELDS,
4946
5485
  WALK_SCHEMES,
4947
5486
  resolveEncoding,
@@ -4953,12 +5492,19 @@ export {
4953
5492
  facetLifts,
4954
5493
  rangeText,
4955
5494
  walkKey,
5495
+ walkPosition,
4956
5496
  FACET_GAP_PX,
4957
5497
  FACET_TITLE_PX,
4958
5498
  FACET_PAD_PX,
4959
5499
  facetGrid,
4960
5500
  facetCells,
5501
+ genePins,
5502
+ HALO_FACTOR,
5503
+ LEGEND_INSET_PX,
5504
+ geneCoverageNote,
5505
+ layoutLabels,
4961
5506
  svgCanvas,
5507
+ version,
4962
5508
  axisScaleOf,
4963
5509
  viewportOf,
4964
5510
  padded,
@@ -4966,6 +5512,9 @@ export {
4966
5512
  zoomAbout,
4967
5513
  screenToLayout,
4968
5514
  figureSvg,
5515
+ genesFromGff3Lines,
5516
+ genesFromBed,
5517
+ genesFromText,
4969
5518
  WELL_KNOWN_SAMPLES,
4970
5519
  wellKnownSample,
4971
5520
  graphBackbone,
@@ -4978,5 +5527,9 @@ export {
4978
5527
  resolveReferenceSample,
4979
5528
  referencePathQuery,
4980
5529
  cutWindowGFA,
5530
+ HPRC_GBZ,
5531
+ parseRegion,
5532
+ openGbz,
5533
+ cutGbzRegion,
4981
5534
  loadBandage
4982
5535
  };