@jbrowse/bandage-core 4.0.30 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1962,9 +1962,29 @@ function stepsOf(pieces, lengthOf) {
1962
1962
  }
1963
1963
  return steps;
1964
1964
  }
1965
+ function stepSpans(pieces, lengthOf) {
1966
+ const spans = [];
1967
+ let end;
1968
+ for (const piece of pieces) {
1969
+ if (piece.start === void 0 || end !== void 0 && piece.start < end) {
1970
+ return void 0;
1971
+ }
1972
+ if (end !== void 0 && piece.start > end) {
1973
+ spans.push({ start: end, end: piece.start });
1974
+ }
1975
+ let pos = piece.start;
1976
+ for (const id of piece.nodeIds) {
1977
+ const len = lengthOf.get(id) ?? 0;
1978
+ spans.push({ start: pos, end: pos + len });
1979
+ pos += len;
1980
+ }
1981
+ end = pos;
1982
+ }
1983
+ return spans;
1984
+ }
1965
1985
  function sliceBetween(steps, span, region, flanked = true) {
1966
1986
  if (!region || !flanked) {
1967
- return { ids: steps, complete: true };
1987
+ return { ids: steps, complete: true, from: -1, to: -1 };
1968
1988
  }
1969
1989
  let i0 = -1;
1970
1990
  let i1 = -1;
@@ -1984,17 +2004,22 @@ function sliceBetween(steps, span, region, flanked = true) {
1984
2004
  }
1985
2005
  });
1986
2006
  if (i0 < 0 || i1 < 0) {
1987
- return { ids: steps, complete: false };
2007
+ return { ids: steps, complete: false, from: -1, to: -1 };
1988
2008
  }
1989
2009
  const ids = steps.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
1990
- return { ids: i0 < i1 ? ids : ids.reverse(), complete: true };
2010
+ return {
2011
+ ids: i0 < i1 ? ids : ids.reverse(),
2012
+ complete: true,
2013
+ from: i0,
2014
+ to: i1
2015
+ };
1991
2016
  }
1992
2017
  function walkRows(graph, region, unit) {
1993
2018
  const paths = graph.paths ?? [];
1994
2019
  const isReference2 = (p) => pathOrigin(p.name).name === graph.referencePath;
1995
- const referencePieces2 = paths.some(isReference2) ? paths.filter(isReference2).sort((a, b) => (a.start ?? 0) - (b.start ?? 0)) : paths.slice(0, 1);
1996
- const first = referencePieces2[0];
1997
- const others = paths.filter((p) => !referencePieces2.includes(p));
2020
+ const referencePieces = paths.some(isReference2) ? paths.filter(isReference2).sort((a, b) => (a.start ?? 0) - (b.start ?? 0)) : paths.slice(0, 1);
2021
+ const first = referencePieces[0];
2022
+ const others = paths.filter((p) => !referencePieces.includes(p));
1998
2023
  if (!first || others.length === 0) {
1999
2024
  return void 0;
2000
2025
  }
@@ -2006,7 +2031,7 @@ function walkRows(graph, region, unit) {
2006
2031
  const referenceStart = first.start ?? 0;
2007
2032
  const cut = region && region.end > region.start ? region : void 0;
2008
2033
  const span = /* @__PURE__ */ new Map();
2009
- for (const piece of referencePieces2) {
2034
+ for (const piece of referencePieces) {
2010
2035
  let pos = piece.start ?? 0;
2011
2036
  for (const id of piece.nodeIds) {
2012
2037
  const len = lengthOf.get(id) ?? 0;
@@ -2020,12 +2045,15 @@ function walkRows(graph, region, unit) {
2020
2045
  const rowOf = (pieces) => {
2021
2046
  const path = pieces[0];
2022
2047
  const ordered = [...pieces].sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
2023
- const { ids, complete } = sliceBetween(
2048
+ const { ids, complete, from, to } = sliceBetween(
2024
2049
  stepsOf(ordered, lengthOf),
2025
2050
  span,
2026
2051
  cut,
2027
2052
  flanked
2028
2053
  );
2054
+ const spans = stepSpans(ordered, lengthOf);
2055
+ const contig = path.contig ?? panSNContig(pathOrigin(path.name).name);
2056
+ const axis = !spans?.length ? void 0 : from < 0 ? { contig, start: spans[0].start, reversed: false } : from < to ? { contig, start: spans[from].end, reversed: false } : { contig, start: spans[from].start, reversed: true };
2029
2057
  const runs = [];
2030
2058
  let bp = 0;
2031
2059
  let offReferenceBp = 0;
@@ -2077,14 +2105,15 @@ function walkRows(graph, region, unit) {
2077
2105
  offReferenceBp,
2078
2106
  gapBp,
2079
2107
  complete,
2080
- runs
2108
+ runs,
2109
+ axis
2081
2110
  };
2082
2111
  };
2083
2112
  const origin = cut ? cut.start : referenceStart;
2084
2113
  return {
2085
2114
  origin,
2086
2115
  unit,
2087
- reference: rowOf(referencePieces2),
2116
+ reference: rowOf(referencePieces),
2088
2117
  rows: [...walks.values()].map(rowOf).sort(
2089
2118
  (a, b) => Number(b.complete) - Number(a.complete) || b.bp - a.bp || a.label.localeCompare(b.label)
2090
2119
  )
@@ -2440,7 +2469,7 @@ function placeByWalks(graph, backbone) {
2440
2469
  return placed;
2441
2470
  }
2442
2471
  function placeBySearch(adjacent, key) {
2443
- const queue = [...key.entries()].sort(([a, ka], [b, kb]) => compareKeys(ka, kb) || compare(a, b)).map(([id]) => id);
2472
+ const queue = [...key.entries()].sort(([a, ka], [b, kb2]) => compareKeys(ka, kb2) || compare(a, b)).map(([id]) => id);
2444
2473
  const placed = queue.length;
2445
2474
  for (const id of queue) {
2446
2475
  const from = key.get(id);
@@ -3064,7 +3093,7 @@ var LAYOUT_MODES = [
3064
3093
  {
3065
3094
  value: "walkrows",
3066
3095
  label: "Walk rows",
3067
- description: "x is each walk's own bp: one bar per haplotype, sequence the reference also carries in blue and sequence it does not in purple, so a repeat expansion reads as bar length. Needs W or P lines.",
3096
+ description: "x is each walk's own bp: one bar per haplotype, blue where the graph aligns it to the reference and purple where it does not, so a repeat expansion reads as bar length, and each bar boxes its haplotype's own genes where the session has them. Needs W or P lines.",
3068
3097
  run: walkRowLayout,
3069
3098
  cutMargins: false,
3070
3099
  wholeWalks: true,
@@ -4268,7 +4297,7 @@ function walkHighlight(graph, name) {
4268
4297
  });
4269
4298
  }
4270
4299
  const reference = isReference(graph, name);
4271
- const referencePieces2 = reference ? [] : (graph.paths ?? []).filter((p) => isReference(graph, p.name));
4300
+ const referencePieces = reference ? [] : (graph.paths ?? []).filter((p) => isReference(graph, p.name));
4272
4301
  return {
4273
4302
  name,
4274
4303
  reference,
@@ -4278,7 +4307,7 @@ function walkHighlight(graph, name) {
4278
4307
  progress,
4279
4308
  steps: pieces.reduce((sum, piece) => sum + piece.nodeIds.length, 0),
4280
4309
  bp,
4281
- referenceBp: referencePieces2.length > 0 ? referencePieces2.reduce((sum, piece) => sum + bpOf(piece), 0) : void 0
4310
+ referenceBp: referencePieces.length > 0 ? referencePieces.reduce((sum, piece) => sum + bpOf(piece), 0) : void 0
4282
4311
  };
4283
4312
  }
4284
4313
  function laneColors(graph, walk, encoding, ramp) {
@@ -4405,6 +4434,28 @@ function walkPosition(walk, nodeId, length) {
4405
4434
  return `${range.contig}:${groupDigits(start)}-${groupDigits(start + length)}`;
4406
4435
  }
4407
4436
 
4437
+ // src/el.ts
4438
+ function el(tag, attrs = {}, ...children) {
4439
+ return {
4440
+ tag,
4441
+ attrs,
4442
+ children: children.filter((c) => !!c || c === "")
4443
+ };
4444
+ }
4445
+ function escapeText(s) {
4446
+ return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
4447
+ }
4448
+ function escapeAttr(s) {
4449
+ return escapeText(s).replaceAll('"', "&quot;");
4450
+ }
4451
+ function serializeEl(node) {
4452
+ if (typeof node === "string") {
4453
+ return escapeText(node);
4454
+ }
4455
+ const attrs = Object.entries(node.attrs).filter(([, v]) => v !== void 0).map(([k, v]) => ` ${k}="${escapeAttr(String(v))}"`).join("");
4456
+ return `<${node.tag}${attrs}>${node.children.map(serializeEl).join("")}</${node.tag}>`;
4457
+ }
4458
+
4408
4459
  // src/facetGrid.ts
4409
4460
  var FACET_GAP_PX = 8;
4410
4461
  var FACET_TITLE_PX = 34;
@@ -4711,6 +4762,385 @@ function layoutLabels(m) {
4711
4762
  };
4712
4763
  }
4713
4764
 
4765
+ // src/referenceRampCss.ts
4766
+ function rampHueCss(hue) {
4767
+ return `hsl(${hue}, 70%, 50%)`;
4768
+ }
4769
+ function rampHue(bp, domain) {
4770
+ const frac = (bp - domain.start) / (domain.end - domain.start);
4771
+ return Math.max(0, Math.min(1, frac)) * REFERENCE_RAMP_MAX_HUE;
4772
+ }
4773
+ var HUE_STOP_DEG = 30;
4774
+ function rampStops({ start, bp, reversed }, domain) {
4775
+ const low = rampHue(start, domain);
4776
+ const high = rampHue(start + bp, domain);
4777
+ if (high - low < 1) {
4778
+ return [(low + high) / 2];
4779
+ }
4780
+ const n2 = Math.ceil((high - low) / HUE_STOP_DEG);
4781
+ return Array.from(
4782
+ { length: n2 + 1 },
4783
+ (_, i) => rampHue(start + bp * (reversed ? n2 - i : i) / n2, domain)
4784
+ );
4785
+ }
4786
+ var RAMP_GRADIENT_CSS = `linear-gradient(to right, ${Array.from(
4787
+ { length: 7 },
4788
+ (_, i) => `${rampHueCss(i / 6 * REFERENCE_RAMP_MAX_HUE)} ${i / 6 * 100}%`
4789
+ ).join(", ")})`;
4790
+
4791
+ // src/layout/walkRowDraw.ts
4792
+ var ON_REFERENCE = "#2f8fd6";
4793
+ var OFF_REFERENCE = "#8e3fbf";
4794
+ var OUTSIDE_CUT = "#bdbdbd";
4795
+ var GENE_INK = "#1c1c22";
4796
+ var BAR_PX = 12;
4797
+ var GAP_PX = 4;
4798
+ var MIN_TILE_PX = 3;
4799
+ function kb(bp) {
4800
+ return `${(bp / 1e3).toFixed(bp < 1e4 ? 1 : 0)} kb`;
4801
+ }
4802
+ function units(bp, unit) {
4803
+ return unit ? ` \u2248 ${Math.round(bp / unit)} units` : "";
4804
+ }
4805
+ function walkRowReadout(row, reference, unit, call) {
4806
+ const { bp, gapBp, complete } = row;
4807
+ if (!reference) {
4808
+ return `${kb(bp)}${units(bp, unit)}`;
4809
+ }
4810
+ const delta = bp - reference.bp;
4811
+ const against = delta === 0 ? "" : ` (${delta > 0 ? "+" : "\u2212"}${kb(Math.abs(delta))})`;
4812
+ const outside = gapBp > 0 ? ` \xB7 ${kb(gapBp)} outside the cut` : "";
4813
+ const called = call ? ` \xB7 called ${kb(call.bp)}${call.spanningReads === 0 ? " \xB7 no spanning read" : ""}` : "";
4814
+ return `${kb(bp)}${units(bp, unit)}${against}${outside}${complete ? "" : " \xB7 partial walk"}${called}`;
4815
+ }
4816
+ function readoutPlacement(text, endX, width) {
4817
+ const x = endX + 6;
4818
+ const fits = x + text.length * LABEL_CHAR_PX < width;
4819
+ return fits ? { x, anchor: "start", halo: false } : { x: endX - 6, anchor: "end", halo: true };
4820
+ }
4821
+ function unitTicks(bp, unit, pxPerBp) {
4822
+ if (unit * pxPerBp < MIN_TILE_PX) {
4823
+ return [];
4824
+ }
4825
+ const ticks = [];
4826
+ for (let k = unit; k < bp; k += unit) {
4827
+ ticks.push(k);
4828
+ }
4829
+ return ticks;
4830
+ }
4831
+ function runPaint(run, ramp) {
4832
+ if (run.gap) {
4833
+ return { fill: OUTSIDE_CUT };
4834
+ }
4835
+ if (!ramp) {
4836
+ return { fill: run.onReference ? ON_REFERENCE : OFF_REFERENCE };
4837
+ }
4838
+ if (run.referenceStart === void 0) {
4839
+ return { fill: REFERENCE_RAMP_ALT_CSS };
4840
+ }
4841
+ const stops = rampStops({ ...run, start: run.referenceStart }, ramp);
4842
+ return stops.length === 1 ? { fill: rampHueCss(stops[0]) } : { stops: stops.map(rampHueCss) };
4843
+ }
4844
+ function alongRow(axis, start, end) {
4845
+ return axis.reversed ? { start: axis.start - end, end: axis.start - start } : { start: start - axis.start, end: end - axis.start };
4846
+ }
4847
+ function rowSpan(axis, bp) {
4848
+ return axis.reversed ? { start: axis.start - bp, end: axis.start } : { start: axis.start, end: axis.start + bp };
4849
+ }
4850
+ function placeRowGenes(rows, byRow) {
4851
+ const placed = /* @__PURE__ */ new Map();
4852
+ for (const row of rows) {
4853
+ const genes = byRow.get(row.name);
4854
+ if (!row.axis || !genes) {
4855
+ continue;
4856
+ }
4857
+ const axis = row.axis;
4858
+ const clip = ({ start, end }) => ({
4859
+ start: Math.max(0, start),
4860
+ end: Math.min(row.bp, end)
4861
+ });
4862
+ placed.set(
4863
+ row.name,
4864
+ genes.map((g) => ({
4865
+ name: g.name,
4866
+ ...clip(alongRow(axis, g.start, g.end)),
4867
+ exons: g.exons.map((e) => clip(alongRow(axis, e.start, e.end))).filter((e) => e.end > e.start)
4868
+ })).filter((g) => g.end > g.start).sort((a, b) => a.start - b.start)
4869
+ );
4870
+ }
4871
+ return placed;
4872
+ }
4873
+ var GENE_FONT_PX = 9;
4874
+ var GENE_CHAR_PX = 5.2;
4875
+ function rowGeneBoxes(genes, X, y) {
4876
+ const boxes = genes.map((g) => {
4877
+ const x = X(g.start);
4878
+ return {
4879
+ g,
4880
+ x,
4881
+ w: Math.max(2, X(g.end) - x)
4882
+ };
4883
+ });
4884
+ const taken = [];
4885
+ const named = new Set(
4886
+ [...boxes].sort((a, b) => a.w - b.w).filter(({ g, x, w }) => {
4887
+ const half = g.name.length * GENE_CHAR_PX / 2;
4888
+ const at = [x + w / 2 - half, x + w / 2 + half];
4889
+ if (2 * half > w + 6 || taken.some(([a, b]) => at[0] < b + 2 && at[1] > a - 2)) {
4890
+ return false;
4891
+ }
4892
+ taken.push(at);
4893
+ return true;
4894
+ })
4895
+ );
4896
+ return boxes.map((box) => ({
4897
+ name: box.g.name,
4898
+ x: box.x,
4899
+ y: y - BAR_PX / 2 - 2,
4900
+ w: box.w,
4901
+ h: BAR_PX + 4,
4902
+ exons: box.g.exons.map((e) => ({
4903
+ x: X(e.start),
4904
+ w: Math.max(1, X(e.end) - X(e.start))
4905
+ })),
4906
+ label: named.has(box) ? { x: box.x + box.w / 2, y: y + 3, size: GENE_FONT_PX } : void 0
4907
+ }));
4908
+ }
4909
+ var rowsText = (n2) => `${n2} row${n2 === 1 ? "" : "s"}`;
4910
+ function walkRowsKey(bars, o = {}) {
4911
+ const entries = [
4912
+ {
4913
+ swatch: {
4914
+ kind: "bar",
4915
+ fill: o.ramp ? o.rampCss ?? ON_REFERENCE : ON_REFERENCE
4916
+ },
4917
+ label: `aligned to ${bars.reference.label} in the graph`
4918
+ },
4919
+ {
4920
+ swatch: {
4921
+ kind: "bar",
4922
+ fill: o.ramp ? REFERENCE_RAMP_ALT_CSS : OFF_REFERENCE
4923
+ },
4924
+ label: "not aligned to it in the graph"
4925
+ }
4926
+ ];
4927
+ if ([bars.reference, ...bars.rows].some((row) => row.gapBp > 0)) {
4928
+ entries.push({
4929
+ swatch: { kind: "gap", fill: OUTSIDE_CUT },
4930
+ label: "walked outside the cut"
4931
+ });
4932
+ }
4933
+ if (o.genes) {
4934
+ entries.push({
4935
+ swatch: { kind: "gene" },
4936
+ label: "genes, each row's own annotation"
4937
+ });
4938
+ if (o.genes.untracked) {
4939
+ entries.push({
4940
+ swatch: { kind: "gene" },
4941
+ label: `no gene track for ${rowsText(o.genes.untracked)}`,
4942
+ note: true
4943
+ });
4944
+ }
4945
+ if (o.genes.unplaced) {
4946
+ entries.push({
4947
+ swatch: { kind: "gene" },
4948
+ label: `no contig coordinates for ${rowsText(o.genes.unplaced)}`,
4949
+ note: true
4950
+ });
4951
+ }
4952
+ if (o.genes.unread) {
4953
+ entries.push({
4954
+ swatch: { kind: "gene" },
4955
+ label: `genes not read for the last ${rowsText(o.genes.unread)}`,
4956
+ note: true
4957
+ });
4958
+ }
4959
+ }
4960
+ return entries;
4961
+ }
4962
+ var CALL_TICK = "#111";
4963
+ var UNBACKED_TICK = "#9e9e9e";
4964
+ var DISAGREES = "#c62828";
4965
+ function walkRowsKeyTree(entries, x, y) {
4966
+ let at = 0;
4967
+ const parts = entries.flatMap((e) => {
4968
+ const swatch = e.note ? void 0 : e.swatch.kind === "gene" ? el("rect", {
4969
+ x: at,
4970
+ y: 5,
4971
+ width: 18,
4972
+ height: 8,
4973
+ fill: "none",
4974
+ stroke: GENE_INK,
4975
+ "stroke-width": 1.5
4976
+ }) : el("rect", {
4977
+ x: at,
4978
+ y: e.swatch.kind === "gap" ? 7 : 5,
4979
+ width: 18,
4980
+ height: e.swatch.kind === "gap" ? GAP_PX : 8,
4981
+ rx: 2,
4982
+ fill: e.swatch.fill
4983
+ });
4984
+ const text = el(
4985
+ "text",
4986
+ {
4987
+ x: e.note ? at : at + 23,
4988
+ y: 13,
4989
+ "font-style": e.note ? "italic" : void 0,
4990
+ fill: e.note ? "#666" : void 0
4991
+ },
4992
+ e.label
4993
+ );
4994
+ at += (e.note ? 0 : 23) + e.label.length * LABEL_CHAR_PX + 16;
4995
+ return swatch ? [swatch, text] : [text];
4996
+ });
4997
+ return {
4998
+ width: at,
4999
+ tree: el(
5000
+ "g",
5001
+ {
5002
+ transform: `translate(${x} ${y})`,
5003
+ "font-family": "Helvetica, Arial, sans-serif",
5004
+ "font-size": 11
5005
+ },
5006
+ ...parts
5007
+ )
5008
+ };
5009
+ }
5010
+ function geneTree(box, y) {
5011
+ return el(
5012
+ "g",
5013
+ { class: "row-gene", "data-testid": "graph-walk-gene" },
5014
+ el("title", {}, box.name),
5015
+ ...box.exons.map(
5016
+ (e) => el("rect", {
5017
+ x: e.x,
5018
+ y: y - BAR_PX / 2,
5019
+ width: e.w,
5020
+ height: BAR_PX,
5021
+ fill: GENE_INK,
5022
+ opacity: 0.35
5023
+ })
5024
+ ),
5025
+ el("rect", {
5026
+ x: box.x,
5027
+ y: box.y,
5028
+ width: box.w,
5029
+ height: box.h,
5030
+ fill: "none",
5031
+ stroke: GENE_INK,
5032
+ "stroke-width": 1.5
5033
+ }),
5034
+ box.label && el(
5035
+ "text",
5036
+ {
5037
+ x: box.label.x,
5038
+ y: box.label.y,
5039
+ "font-family": "sans-serif",
5040
+ "font-size": box.label.size,
5041
+ "font-weight": 600,
5042
+ "text-anchor": "middle",
5043
+ fill: GENE_INK,
5044
+ stroke: "white",
5045
+ "stroke-width": 2.5,
5046
+ "paint-order": "stroke"
5047
+ },
5048
+ box.name
5049
+ )
5050
+ );
5051
+ }
5052
+ function walkRowsTree(bars, frame, o = {}) {
5053
+ const X = (bp) => bp * frame.scaleX + frame.translateX;
5054
+ const Y = (row) => row * ROW_HEIGHT_PX * frame.scaleY + frame.translateY;
5055
+ const { origin, unit, reference, rows } = bars;
5056
+ const along = (offset) => X(origin + offset);
5057
+ const prefix = o.idPrefix ?? "walkrow";
5058
+ const rowTree = (row, i) => {
5059
+ const y = Y(i);
5060
+ if (y < -BAR_PX || y > frame.height + BAR_PX) {
5061
+ return void 0;
5062
+ }
5063
+ const runs = row.runs.flatMap((run) => {
5064
+ const paint2 = runPaint(run, o.ramp);
5065
+ const h = run.gap ? GAP_PX : BAR_PX;
5066
+ const id = `${prefix}-${i}-${run.start}`;
5067
+ const rect = el("rect", {
5068
+ x: along(run.start),
5069
+ y: y - h / 2,
5070
+ width: Math.max(1, run.bp * frame.scaleX),
5071
+ height: h,
5072
+ fill: "stops" in paint2 ? `url(#${id})` : paint2.fill
5073
+ });
5074
+ return "stops" in paint2 ? [
5075
+ el(
5076
+ "linearGradient",
5077
+ { id },
5078
+ ...paint2.stops.map(
5079
+ (color, k) => el("stop", {
5080
+ offset: k / (paint2.stops.length - 1),
5081
+ "stop-color": color
5082
+ })
5083
+ )
5084
+ ),
5085
+ rect
5086
+ ] : [rect];
5087
+ });
5088
+ const ticks = unit ? unitTicks(row.bp, unit, frame.scaleX).map(
5089
+ (k) => el("line", {
5090
+ x1: along(k),
5091
+ x2: along(k),
5092
+ y1: y - BAR_PX / 2,
5093
+ y2: y + BAR_PX / 2,
5094
+ stroke: "white",
5095
+ "stroke-width": 1
5096
+ })
5097
+ ) : [];
5098
+ const { call } = row;
5099
+ const text = walkRowReadout(
5100
+ row,
5101
+ i === 0 ? void 0 : reference,
5102
+ unit,
5103
+ call
5104
+ );
5105
+ const at = readoutPlacement(text, along(row.bp), frame.width);
5106
+ return el(
5107
+ "g",
5108
+ {
5109
+ "data-testid": i === 0 ? "graph-walk-reference" : "graph-walk-row"
5110
+ },
5111
+ ...runs,
5112
+ ...ticks,
5113
+ ...rowGeneBoxes(o.rowGenes?.get(row.name) ?? [], along, y).map(
5114
+ (box) => geneTree(box, y)
5115
+ ),
5116
+ call && el("rect", {
5117
+ "data-testid": "graph-walk-call",
5118
+ x: along(call.bp) - 1,
5119
+ y: y - BAR_PX / 2 - 3,
5120
+ width: 2,
5121
+ height: BAR_PX + 6,
5122
+ fill: call.spanningReads === 0 ? UNBACKED_TICK : CALL_TICK
5123
+ }),
5124
+ el(
5125
+ "text",
5126
+ {
5127
+ x: at.x,
5128
+ y: y + 4,
5129
+ "font-family": "sans-serif",
5130
+ "font-size": 11,
5131
+ fill: call?.agrees === false ? DISAGREES : "#333",
5132
+ stroke: at.halo ? "white" : void 0,
5133
+ "stroke-width": at.halo ? 3 : void 0,
5134
+ "paint-order": "stroke",
5135
+ "text-anchor": at.anchor
5136
+ },
5137
+ text
5138
+ )
5139
+ );
5140
+ };
5141
+ return el("g", {}, ...[reference, ...rows].map(rowTree));
5142
+ }
5143
+
4714
5144
  // src/renderer/svgCanvas.ts
4715
5145
  var n = (v) => +v.toFixed(1);
4716
5146
  function paint(css) {
@@ -4824,7 +5254,7 @@ function svgCanvas(width, height) {
4824
5254
  }
4825
5255
 
4826
5256
  // src/version.ts
4827
- var version = "4.0.30";
5257
+ var version = "4.1.0";
4828
5258
 
4829
5259
  // src/viewport.ts
4830
5260
  function axisScaleOf(scale, pixelRows) {
@@ -4869,12 +5299,12 @@ function screenToLayout(x, y, t, axis) {
4869
5299
 
4870
5300
  // src/figure.ts
4871
5301
  var FONT = 'font-family="Helvetica, Arial, sans-serif" font-size="11"';
4872
- var BAR_PX = 48;
5302
+ var BAR_PX2 = 48;
4873
5303
  var SWATCH_PX = 18;
4874
5304
  var KEY_GAP_PX = 16;
4875
5305
  var FADED = "rgb(160,160,160)";
4876
5306
  var EXON_COLOR = "#daa520";
4877
- var GENE_INK = "#1c1c22";
5307
+ var GENE_INK2 = "#1c1c22";
4878
5308
  var MIN_LANE_PX = 4;
4879
5309
  var EXON_GAP_PX = 1;
4880
5310
  var EXON_LINE_PX = 2;
@@ -4882,7 +5312,7 @@ function esc(s) {
4882
5312
  return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4883
5313
  }
4884
5314
  function keySvg(id, x, y, stops, label, after, under, opacity = 1) {
4885
- const bar = stops.length > 1 ? BAR_PX : SWATCH_PX;
5315
+ const bar = stops.length > 1 ? BAR_PX2 : SWATCH_PX;
4886
5316
  const fill = stops.length > 1 ? `fill="url(#${id})"` : `fill="${stops[0].color}"${opacity < 1 ? ` fill-opacity="${opacity}"` : ""}`;
4887
5317
  const defs = stops.length > 1 ? `<linearGradient id="${id}">${stops.map((s) => `<stop offset="${s.offset}" stop-color="${s.color}"/>`).join("")}</linearGradient>` : "";
4888
5318
  const width = Math.max(
@@ -4913,15 +5343,17 @@ function screenPath(d, t) {
4913
5343
  );
4914
5344
  }
4915
5345
  function chip(x, y, w, name, note) {
4916
- 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>`;
5346
+ 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_INK2}" text-anchor="middle"><tspan font-style="italic" font-weight="600">${esc(name)}</tspan>${note ? esc(note) : ""}</text>`;
4917
5347
  }
4918
5348
  function figureSvg(graph, layout, o = {}) {
4919
5349
  const width = o.width ?? 1200;
4920
5350
  const room = o.height ?? 800;
4921
5351
  const region = o.region;
4922
5352
  const rampDomain = o.colorDomain ?? region;
5353
+ const rows = o.walkRows;
4923
5354
  const bounds = drawingBounds(layout, {
4924
- region: o.fitToDrawing ? void 0 : region
5355
+ region: o.fitToDrawing ? void 0 : region,
5356
+ extent: rows && layout.extent ? walkRowsExtent(rows) : void 0
4925
5357
  });
4926
5358
  const pixelRows = layout.pixelRows ?? false;
4927
5359
  const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
@@ -4946,7 +5378,7 @@ function figureSvg(graph, layout, o = {}) {
4946
5378
  };
4947
5379
  const contigThickness = o.contigThickness ?? 6;
4948
5380
  const nodeWidth2 = o.nodeWidth ?? "depth";
4949
- const pins = o.genes?.length && !layout.tubeMap ? genePins(graph, o.genes, layout.nodePositions) : [];
5381
+ const pins = o.genes?.length && !layout.tubeMap && !rows ? genePins(graph, o.genes, layout.nodePositions) : [];
4950
5382
  const ink = nodeInk(graph, nodeById, contigThickness, nodeWidth2);
4951
5383
  let masks = 0;
4952
5384
  function overlays(highlight, t, w, h) {
@@ -4982,11 +5414,32 @@ function figureSvg(graph, layout, o = {}) {
4982
5414
  )) {
4983
5415
  const pinY = screen(pin.at).y + contigThickness / 2;
4984
5416
  out.push(
4985
- `<line x1="${x}" x2="${x}" y1="${y - LABEL_PX - 2}" y2="${pinY}" stroke="${GENE_INK}" stroke-width="0.8" stroke-opacity="0.6"/>`,
5417
+ `<line x1="${x}" x2="${x}" y1="${y - LABEL_PX - 2}" y2="${pinY}" stroke="${GENE_INK2}" stroke-width="0.8" stroke-opacity="0.6"/>`,
4986
5418
  chip(x, y, cw, pin.gene.name, text.slice(pin.gene.name.length))
4987
5419
  );
4988
5420
  }
4989
- for (const { label, y } of layout.rowLabels ?? []) {
5421
+ if (rows) {
5422
+ out.push(
5423
+ serializeEl(
5424
+ walkRowsTree(
5425
+ rows,
5426
+ { ...t, width: w, height: h },
5427
+ {
5428
+ ramp: referenceRamp && {
5429
+ start: referenceRamp.start,
5430
+ end: referenceRamp.start + referenceRamp.span
5431
+ },
5432
+ rowGenes: o.rowGenes
5433
+ }
5434
+ )
5435
+ )
5436
+ );
5437
+ }
5438
+ const rowLabels = rows ? [rows.reference, ...rows.rows].map((r, i) => ({
5439
+ label: r.label,
5440
+ y: i * ROW_HEIGHT_PX
5441
+ })) : layout.rowLabels ?? [];
5442
+ for (const { label, y } of rowLabels) {
4990
5443
  const sy = y * t.scaleY + t.translateY;
4991
5444
  if (sy >= 0 && sy <= h) {
4992
5445
  out.push(
@@ -5017,7 +5470,7 @@ function figureSvg(graph, layout, o = {}) {
5017
5470
  const renderer = new Canvas2DRenderer(canvas);
5018
5471
  renderer.uploadGeometry(
5019
5472
  buildGeometry({
5020
- nodePositions: layout.nodePositions,
5473
+ nodePositions: rows ? {} : layout.nodePositions,
5021
5474
  graph,
5022
5475
  nodeById,
5023
5476
  colorScheme,
@@ -5093,7 +5546,21 @@ function figureSvg(graph, layout, o = {}) {
5093
5546
  0.3
5094
5547
  )
5095
5548
  );
5096
- } else if (referenceRamp) {
5549
+ }
5550
+ if (rows) {
5551
+ const key = walkRowsKeyTree(
5552
+ walkRowsKey(rows, {
5553
+ ramp: referenceRamp && {
5554
+ start: referenceRamp.start,
5555
+ end: referenceRamp.start + referenceRamp.span
5556
+ },
5557
+ genes: o.rowGenes?.size ? o.rowGeneGaps : void 0
5558
+ }),
5559
+ x,
5560
+ 2
5561
+ );
5562
+ push({ width: key.width, markup: serializeEl(key.tree) });
5563
+ } else if (!lift && referenceRamp) {
5097
5564
  push(
5098
5565
  keySvg(
5099
5566
  "ramp",
@@ -5243,8 +5710,25 @@ function genesFromGff3Lines(lines) {
5243
5710
  }
5244
5711
  return genes;
5245
5712
  }
5713
+ function mergeOverlappingByName(genes) {
5714
+ const out = [];
5715
+ const open = /* @__PURE__ */ new Map();
5716
+ for (const g of [...genes].sort((a, b) => a.start - b.start)) {
5717
+ const key = `${g.refName} ${g.strand} ${g.name}`;
5718
+ const last = open.get(key);
5719
+ if (last && g.start < last.end) {
5720
+ last.end = Math.max(last.end, g.end);
5721
+ last.exons = mergedIntervals([...last.exons, ...g.exons]);
5722
+ } else {
5723
+ const gene = { ...g, exons: mergedIntervals(g.exons) };
5724
+ out.push(gene);
5725
+ open.set(key, gene);
5726
+ }
5727
+ }
5728
+ return out;
5729
+ }
5246
5730
  function genesFromBed(text) {
5247
- const genes = /* @__PURE__ */ new Map();
5731
+ const rows = [];
5248
5732
  for (const line of text.split("\n")) {
5249
5733
  const cols = line.replace(/\r$/, "").split(" ");
5250
5734
  const start = Number(cols[1]);
@@ -5260,24 +5744,16 @@ function genesFromBed(text) {
5260
5744
  start: start + starts[i],
5261
5745
  end: start + starts[i] + size
5262
5746
  })) : [{ start, end }];
5263
- const key = `${refName} ${name}`;
5264
- const gene = genes.get(key);
5265
- if (gene) {
5266
- gene.start = Math.min(gene.start, start);
5267
- gene.end = Math.max(gene.end, end);
5268
- gene.exons = mergedIntervals([...gene.exons, ...blocks]);
5269
- } else {
5270
- genes.set(key, {
5271
- name,
5272
- refName,
5273
- start,
5274
- end,
5275
- strand: strandOf(cols[5]),
5276
- exons: mergedIntervals(blocks)
5277
- });
5278
- }
5747
+ rows.push({
5748
+ name,
5749
+ refName,
5750
+ start,
5751
+ end,
5752
+ strand: strandOf(cols[5]),
5753
+ exons: blocks
5754
+ });
5279
5755
  }
5280
- return [...genes.values()];
5756
+ return mergeOverlappingByName(rows);
5281
5757
  }
5282
5758
  function genesFromText(text) {
5283
5759
  const first = text.split("\n", 1e3).find((l) => l.trim() !== "" && !/^(#|track|browser)/.test(l));
@@ -5612,13 +6088,13 @@ async function referencePathQuery(db, referenceSample2, refName) {
5612
6088
  haplotype: path.name.haplotype
5613
6089
  } : void 0;
5614
6090
  }
5615
- function nodeLimitError(error, limit, windowBp, walkedFrom = 0) {
6091
+ function nodeLimitError(error, limit, windowBp) {
5616
6092
  const isLimit = error instanceof Error && (error.name === "SubgraphLimitError" || /^Subgraph size limit of \d+ nodes exceeded/.test(error.message));
5617
6093
  if (!isLimit) {
5618
6094
  return void 0;
5619
6095
  } else {
5620
6096
  const walked = error.walkedBp;
5621
- const fits = typeof walked === "number" && walked > 0 ? Math.floor((walkedFrom + walked) * 0.8) : Math.floor(windowBp / 2);
6097
+ const fits = typeof walked === "number" && walked > 0 ? Math.floor(walked * 0.8) : Math.floor(windowBp / 2);
5622
6098
  return new NodeLimitError(limit, windowBp, Math.max(fits, 1));
5623
6099
  }
5624
6100
  }
@@ -5627,44 +6103,16 @@ var GBZ_CUT_DEFAULTS = {
5627
6103
  snarls: "contained",
5628
6104
  limit: 1e5
5629
6105
  };
5630
- async function referencePieces(db, query, start, end) {
5631
- const inside = (await db.paths()).filter(
5632
- ({ isIndexed, name }) => isIndexed && name.sample === query.sample && name.haplotype === query.haplotype && name.contig === query.contig && name.fragment > start && name.fragment < end
5633
- ).map((path) => path.name.fragment);
5634
- const bounds = [start, ...[...new Set(inside)].sort((a, b) => a - b), end];
5635
- return bounds.slice(1).map((pieceEnd, i) => ({
5636
- start: bounds[i],
5637
- end: pieceEnd
5638
- }));
5639
- }
5640
- async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
6106
+ async function cutWindowGFA(db, query, start, end, opts) {
5641
6107
  if (!query) {
5642
6108
  return "";
5643
6109
  }
5644
- const pieces = await referencePieces(db, query, start, end);
5645
- const subgraphs = await Promise.all(
5646
- pieces.map(
5647
- (piece) => db.getSubgraphForRange(query, piece.start, piece.end, {
5648
- ...opts,
5649
- haplotypes: "all",
5650
- ...keep === void 0 ? {} : { keep }
5651
- }).catch((error) => {
5652
- throw nodeLimitError(
5653
- error,
5654
- opts.limit,
5655
- end - start,
5656
- piece.start - start
5657
- ) ?? error;
5658
- })
5659
- )
5660
- );
6110
+ const subgraphs = await db.getSubgraphs({ ...opts, path: query, start, end, haplotypes: "all" }).catch((error) => {
6111
+ throw nodeLimitError(error, opts.limit, end - start) ?? error;
6112
+ });
5661
6113
  const paths = await db.paths();
5662
6114
  return joinCuts(
5663
- await Promise.all(
5664
- subgraphs.flatMap(
5665
- (subgraph) => subgraph ? [subgraph.toGFA({ names: "resolved" })] : []
5666
- )
5667
- ),
6115
+ await Promise.all(subgraphs.map((subgraph) => subgraph.toGFA())),
5668
6116
  (sample, haplotype, contig, at) => paths.some(
5669
6117
  ({ name }) => name.sample === sample && name.haplotype === haplotype && name.contig === contig && name.fragment === at
5670
6118
  )
@@ -5692,7 +6140,7 @@ function parseRegion(text) {
5692
6140
  return { refName: m[1], start: n2(m[2]), end: n2(m[3]) };
5693
6141
  }
5694
6142
  async function openGbz(db, index) {
5695
- const base = await GBZBase.open(db, index ? { haplotypeIndex: index } : {});
6143
+ const base = await GBZBase.open({ source: db, haplotypeIndex: index });
5696
6144
  return { base, referenceSamples: await referenceSamplesOf(base) };
5697
6145
  }
5698
6146
  function referenceSample(ref, samples) {
@@ -5723,7 +6171,7 @@ async function cutGbzRegion({ base, referenceSamples }, src, signal) {
5723
6171
  snarls: src.snarls ?? GBZ_CUT_DEFAULTS.snarls,
5724
6172
  limit: GBZ_CUT_DEFAULTS.limit,
5725
6173
  signal,
5726
- ...wanted ? { keep: (name) => haplotypeWanted(name, wanted) } : {}
6174
+ keep: wanted ? (name) => haplotypeWanted(name, wanted) : void 0
5727
6175
  });
5728
6176
  return { text, region, sample };
5729
6177
  }
@@ -5801,6 +6249,8 @@ export {
5801
6249
  rangeText,
5802
6250
  walkKey,
5803
6251
  walkPosition,
6252
+ el,
6253
+ serializeEl,
5804
6254
  FACET_GAP_PX,
5805
6255
  FACET_TITLE_PX,
5806
6256
  FACET_PAD_PX,
@@ -5812,6 +6262,28 @@ export {
5812
6262
  LEGEND_INSET_PX,
5813
6263
  geneCoverageNote,
5814
6264
  layoutLabels,
6265
+ rampHueCss,
6266
+ rampHue,
6267
+ rampStops,
6268
+ RAMP_GRADIENT_CSS,
6269
+ ON_REFERENCE,
6270
+ OFF_REFERENCE,
6271
+ OUTSIDE_CUT,
6272
+ GENE_INK,
6273
+ BAR_PX,
6274
+ GAP_PX,
6275
+ walkRowReadout,
6276
+ readoutPlacement,
6277
+ unitTicks,
6278
+ runPaint,
6279
+ alongRow,
6280
+ rowSpan,
6281
+ placeRowGenes,
6282
+ rowGeneBoxes,
6283
+ walkRowsKey,
6284
+ DISAGREES,
6285
+ walkRowsKeyTree,
6286
+ walkRowsTree,
5815
6287
  svgCanvas,
5816
6288
  version,
5817
6289
  axisScaleOf,
@@ -5822,6 +6294,7 @@ export {
5822
6294
  screenToLayout,
5823
6295
  figureSvg,
5824
6296
  genesFromGff3Lines,
6297
+ mergeOverlappingByName,
5825
6298
  genesFromBed,
5826
6299
  genesFromText,
5827
6300
  WELL_KNOWN_SAMPLES,