@jbrowse/bandage-core 0.1.20 → 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();
@@ -3558,6 +3558,9 @@ var Canvas2DRenderer = class extends Canvas2DRenderingBackendBase {
3558
3558
  }
3559
3559
  const ctx = this.ctx;
3560
3560
  const { width, height } = ctx.canvas;
3561
+ if (clearColor[3] < 1) {
3562
+ ctx.clearRect(0, 0, width, height);
3563
+ }
3561
3564
  ctx.fillStyle = normalizedRgbToCssRgba(
3562
3565
  [clearColor[0], clearColor[1], clearColor[2]],
3563
3566
  clearColor[3]
@@ -3742,8 +3745,8 @@ function occupancy(frame, reserved = []) {
3742
3745
  function placeLabels(candidates, frame, take) {
3743
3746
  const out = [];
3744
3747
  const row = LABEL_PX + LABEL_PAD * 2;
3745
- for (const c of candidates) {
3746
- const w = labelWidth(c.text);
3748
+ const place = (c, text) => {
3749
+ const w = labelWidth(text);
3747
3750
  const direction = c.y > frame.height / 2 ? -1 : 1;
3748
3751
  for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
3749
3752
  const y = c.y + direction * tries * row;
@@ -3754,13 +3757,19 @@ function placeLabels(candidates, frame, take) {
3754
3757
  y1: y + LABEL_PAD
3755
3758
  };
3756
3759
  if (offFrame(box, frame)) {
3757
- break;
3760
+ return void 0;
3758
3761
  }
3759
3762
  if (insideFrame(box, frame) && take(box)) {
3760
- out.push({ ...c, y, w });
3761
- break;
3763
+ return { ...c, text, y, w };
3762
3764
  }
3763
3765
  }
3766
+ return void 0;
3767
+ };
3768
+ for (const c of candidates) {
3769
+ const placed = place(c, c.text) ?? (c.fallback === void 0 ? void 0 : place(c, c.fallback));
3770
+ if (placed) {
3771
+ out.push(placed);
3772
+ }
3764
3773
  }
3765
3774
  return out;
3766
3775
  }
@@ -3862,10 +3871,10 @@ function labelOrder(nodePositions, nodeLengths) {
3862
3871
  return order;
3863
3872
  }
3864
3873
  var arcCache = /* @__PURE__ */ new WeakMap();
3865
- function arcPlacements(nodePositions, deletions, axis, version) {
3874
+ function arcPlacements(nodePositions, deletions, axis, version2) {
3866
3875
  const { scaleX, scaleY } = axis;
3867
3876
  const cached2 = arcCache.get(nodePositions);
3868
- 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) {
3869
3878
  return cached2.arcs;
3870
3879
  }
3871
3880
  const arcs = [];
@@ -3883,7 +3892,7 @@ function arcPlacements(nodePositions, deletions, axis, version) {
3883
3892
  });
3884
3893
  }
3885
3894
  }
3886
- arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
3895
+ arcCache.set(nodePositions, { deletions, scaleX, scaleY, version: version2, arcs });
3887
3896
  return arcs;
3888
3897
  }
3889
3898
  function sizeLabelCandidates({
@@ -3896,7 +3905,7 @@ function sizeLabelCandidates({
3896
3905
  translateY,
3897
3906
  width,
3898
3907
  height,
3899
- version = 0
3908
+ version: version2 = 0
3900
3909
  }) {
3901
3910
  const { scaleX, scaleY } = axis;
3902
3911
  const deletionCandidates = [];
@@ -3905,7 +3914,7 @@ function sizeLabelCandidates({
3905
3914
  nodePositions,
3906
3915
  deletions,
3907
3916
  axis,
3908
- version
3917
+ version2
3909
3918
  )) {
3910
3919
  if (extent >= MIN_DELETION_LABEL_PX) {
3911
3920
  const text = deletionText(deletion.bp);
@@ -4283,6 +4292,15 @@ function walkKey(walk, reference) {
4283
4292
  hover: scale === own ? void 0 : own
4284
4293
  };
4285
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
+ }
4286
4304
 
4287
4305
  // src/facetGrid.ts
4288
4306
  var FACET_GAP_PX = 8;
@@ -4355,6 +4373,223 @@ function facetCells(names, by) {
4355
4373
  return cells.includes(-1) || new Set(cells).size < cells.length ? wrap : { columns, count: rows.length * columns, cells };
4356
4374
  }
4357
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
+
4358
4593
  // src/renderer/svgCanvas.ts
4359
4594
  var n = (v) => +v.toFixed(1);
4360
4595
  function paint(css) {
@@ -4371,9 +4606,43 @@ function paintAttrs(kind, css) {
4371
4606
  const { color, opacity } = paint(css);
4372
4607
  return `${kind}="${color}"${opacity < 1 ? ` ${kind}-opacity="${n(opacity)}"` : ""}`;
4373
4608
  }
4609
+ var DOT_PX = 0.5;
4610
+ var DOT_CELL_PX = 0.25;
4374
4611
  function svgCanvas(width, height) {
4375
4612
  const out = [];
4376
- 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
+ };
4377
4646
  const ctx = {
4378
4647
  canvas: void 0,
4379
4648
  fillStyle: "#000",
@@ -4382,21 +4651,25 @@ function svgCanvas(width, height) {
4382
4651
  lineCap: "butt",
4383
4652
  lineJoin: "miter",
4384
4653
  beginPath() {
4385
- d = "";
4654
+ subpaths = [];
4386
4655
  },
4387
4656
  moveTo(x, y) {
4388
- d += `M${n(x)} ${n(y)}`;
4657
+ start(x, y);
4389
4658
  },
4390
4659
  lineTo(x, y) {
4391
- d += `L${n(x)} ${n(y)}`;
4660
+ reach([x, y]).d += `L${n(x)} ${n(y)}`;
4392
4661
  },
4393
4662
  bezierCurveTo(cx0, cy0, cx1, cy1, x, y) {
4394
- 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)}`;
4395
4664
  },
4396
4665
  closePath() {
4397
- d += "Z";
4666
+ const s = subpaths.at(-1);
4667
+ if (s) {
4668
+ s.d += "Z";
4669
+ }
4398
4670
  },
4399
4671
  stroke() {
4672
+ const d = pathData(ctx.lineCap !== "butt");
4400
4673
  if (d) {
4401
4674
  out.push(
4402
4675
  `<path d="${d}" fill="none" ${paintAttrs("stroke", ctx.strokeStyle)} stroke-width="${n(ctx.lineWidth)}" stroke-linecap="${ctx.lineCap}" stroke-linejoin="${ctx.lineJoin}"/>`
@@ -4404,6 +4677,7 @@ function svgCanvas(width, height) {
4404
4677
  }
4405
4678
  },
4406
4679
  fill() {
4680
+ const d = pathData(false);
4407
4681
  if (d) {
4408
4682
  out.push(`<path d="${d}" ${paintAttrs("fill", ctx.fillStyle)}/>`);
4409
4683
  }
@@ -4428,6 +4702,9 @@ function svgCanvas(width, height) {
4428
4702
  };
4429
4703
  }
4430
4704
 
4705
+ // src/version.ts
4706
+ var version = "4.0.25";
4707
+
4431
4708
  // src/viewport.ts
4432
4709
  function axisScaleOf(scale, pixelRows) {
4433
4710
  return { scaleX: scale, scaleY: pixelRows ? 1 : scale, pixelRows };
@@ -4475,6 +4752,11 @@ var BAR_PX = 48;
4475
4752
  var SWATCH_PX = 18;
4476
4753
  var KEY_GAP_PX = 16;
4477
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;
4478
4760
  function esc(s) {
4479
4761
  return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4480
4762
  }
@@ -4503,6 +4785,15 @@ function walkKeySvg(id, x, y, walk, label, reference) {
4503
4785
  key.scale
4504
4786
  );
4505
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
+ }
4506
4797
  function figureSvg(graph, layout, o = {}) {
4507
4798
  const width = o.width ?? 1200;
4508
4799
  const room = o.height ?? 800;
@@ -4529,6 +4820,58 @@ function figureSvg(graph, layout, o = {}) {
4529
4820
  ...lift.referenceDomain,
4530
4821
  name: region?.refName
4531
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
+ }
4532
4875
  function drawing(highlight, w, h) {
4533
4876
  const fit = fitTransform(
4534
4877
  bounds,
@@ -4554,10 +4897,10 @@ function figureSvg(graph, layout, o = {}) {
4554
4897
  graph,
4555
4898
  nodeById,
4556
4899
  colorScheme,
4557
- contigThickness: o.contigThickness ?? 6,
4900
+ contigThickness,
4558
4901
  connectorThickness: o.connectorThickness ?? 2,
4559
4902
  drawPaths: false,
4560
- nodeWidth: o.nodeWidth ?? "depth",
4903
+ nodeWidth: nodeWidth2,
4561
4904
  highlight,
4562
4905
  axis,
4563
4906
  referenceRamp,
@@ -4567,15 +4910,15 @@ function figureSvg(graph, layout, o = {}) {
4567
4910
  )
4568
4911
  })
4569
4912
  );
4570
- renderer.updateTransform({
4913
+ const t = {
4571
4914
  scaleX: axis.scaleX,
4572
4915
  scaleY: axis.scaleY,
4573
4916
  translateX: fit.translateX,
4574
- translateY: fit.translateY,
4575
- dpr: 1
4576
- });
4917
+ translateY: fit.translateY
4918
+ };
4919
+ renderer.updateTransform({ ...t, dpr: 1 });
4577
4920
  renderer.render([1, 1, 1, 1]);
4578
- return markup();
4921
+ return markup() + overlays(highlight, t, w, h);
4579
4922
  }
4580
4923
  const parts = [];
4581
4924
  let height;
@@ -4654,10 +4997,169 @@ function figureSvg(graph, layout, o = {}) {
4654
4997
  );
4655
4998
  height = header + h;
4656
4999
  }
4657
- 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>
4658
5005
  `;
4659
5006
  }
4660
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
+
4661
5163
  // src/reference.ts
4662
5164
  var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
4663
5165
  ["hg38", "GRCh38"],
@@ -4832,9 +5334,9 @@ function resolveReferenceSample({
4832
5334
  }
4833
5335
  return sample;
4834
5336
  }
4835
- async function referencePathQuery(db, referenceSample, refName) {
5337
+ async function referencePathQuery(db, referenceSample2, refName) {
4836
5338
  const path = (await db.paths()).find(
4837
- (p) => p.isIndexed && p.name.sample === referenceSample && p.name.contig === refName
5339
+ (p) => p.isIndexed && p.name.sample === referenceSample2 && p.name.contig === refName
4838
5340
  );
4839
5341
  return path ? {
4840
5342
  sample: path.name.sample,
@@ -4863,6 +5365,60 @@ async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
4863
5365
  return subgraph ? subgraph.toGFA({ names: "resolved" }) : "";
4864
5366
  }
4865
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
+
4866
5422
  // src/loadBandage.ts
4867
5423
  var cached;
4868
5424
  function loadBandage() {
@@ -4875,12 +5431,10 @@ function loadBandage() {
4875
5431
 
4876
5432
  export {
4877
5433
  isBackbone,
4878
- firstNodeAtOrAfter,
4879
5434
  BUBBLE_SPREADS,
4880
5435
  panSNSample,
4881
5436
  panSNHaplotype,
4882
5437
  panSNContig,
4883
- polylineSlice,
4884
5438
  loadGraph,
4885
5439
  engineRequest,
4886
5440
  engineKey,
@@ -4926,13 +5480,7 @@ export {
4926
5480
  LABEL_PX,
4927
5481
  LABEL_CHAR_PX,
4928
5482
  LABEL_PAD,
4929
- occupancy,
4930
- placeLabels,
4931
- rowLabelBox,
4932
5483
  formatBp,
4933
- sizeLabelCandidates,
4934
- nodeLabelBudget,
4935
- placeSizeLabels,
4936
5484
  WALK_FIELDS,
4937
5485
  WALK_SCHEMES,
4938
5486
  resolveEncoding,
@@ -4944,12 +5492,19 @@ export {
4944
5492
  facetLifts,
4945
5493
  rangeText,
4946
5494
  walkKey,
5495
+ walkPosition,
4947
5496
  FACET_GAP_PX,
4948
5497
  FACET_TITLE_PX,
4949
5498
  FACET_PAD_PX,
4950
5499
  facetGrid,
4951
5500
  facetCells,
5501
+ genePins,
5502
+ HALO_FACTOR,
5503
+ LEGEND_INSET_PX,
5504
+ geneCoverageNote,
5505
+ layoutLabels,
4952
5506
  svgCanvas,
5507
+ version,
4953
5508
  axisScaleOf,
4954
5509
  viewportOf,
4955
5510
  padded,
@@ -4957,6 +5512,9 @@ export {
4957
5512
  zoomAbout,
4958
5513
  screenToLayout,
4959
5514
  figureSvg,
5515
+ genesFromGff3Lines,
5516
+ genesFromBed,
5517
+ genesFromText,
4960
5518
  WELL_KNOWN_SAMPLES,
4961
5519
  wellKnownSample,
4962
5520
  graphBackbone,
@@ -4969,5 +5527,9 @@ export {
4969
5527
  resolveReferenceSample,
4970
5528
  referencePathQuery,
4971
5529
  cutWindowGFA,
5530
+ HPRC_GBZ,
5531
+ parseRegion,
5532
+ openGbz,
5533
+ cutGbzRegion,
4972
5534
  loadBandage
4973
5535
  };