@jbrowse/bandage-core 4.0.29 → 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.
@@ -1946,16 +1946,52 @@ function sampleOf(path) {
1946
1946
  function labelOf(path) {
1947
1947
  return path.haplotype !== void 0 && path.sample !== void 0 ? `${path.sample}#${path.haplotype}` : sampleOf(path);
1948
1948
  }
1949
- function sliceBetween(nodeIds, span, region, flanked = true) {
1949
+ function stepsOf(pieces, lengthOf) {
1950
+ const steps = [];
1951
+ let end;
1952
+ for (const piece of pieces) {
1953
+ if (end !== void 0 && piece.start !== void 0 && piece.start > end) {
1954
+ steps.push(piece.start - end);
1955
+ }
1956
+ steps.push(...piece.nodeIds);
1957
+ const bp = piece.nodeIds.reduce(
1958
+ (sum, id) => sum + (lengthOf.get(id) ?? 0),
1959
+ 0
1960
+ );
1961
+ end = piece.start === void 0 ? void 0 : piece.start + bp;
1962
+ }
1963
+ return steps;
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
+ }
1985
+ function sliceBetween(steps, span, region, flanked = true) {
1950
1986
  if (!region || !flanked) {
1951
- return { ids: nodeIds, complete: true };
1987
+ return { ids: steps, complete: true, from: -1, to: -1 };
1952
1988
  }
1953
1989
  let i0 = -1;
1954
1990
  let i1 = -1;
1955
1991
  let bestEnd = -Infinity;
1956
1992
  let bestStart = Infinity;
1957
- nodeIds.forEach((id, i) => {
1958
- const s = span.get(id);
1993
+ steps.forEach((id, i) => {
1994
+ const s = typeof id === "string" ? span.get(id) : void 0;
1959
1995
  if (s) {
1960
1996
  if (s.end <= region.start && s.end > bestEnd) {
1961
1997
  bestEnd = s.end;
@@ -1968,25 +2004,34 @@ function sliceBetween(nodeIds, span, region, flanked = true) {
1968
2004
  }
1969
2005
  });
1970
2006
  if (i0 < 0 || i1 < 0) {
1971
- return { ids: nodeIds, complete: false };
2007
+ return { ids: steps, complete: false, from: -1, to: -1 };
1972
2008
  }
1973
- const ids = nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
1974
- return { ids: i0 < i1 ? ids : ids.reverse(), complete: true };
2009
+ const ids = steps.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
2010
+ return {
2011
+ ids: i0 < i1 ? ids : ids.reverse(),
2012
+ complete: true,
2013
+ from: i0,
2014
+ to: i1
2015
+ };
1975
2016
  }
1976
2017
  function walkRows(graph, region, unit) {
1977
2018
  const paths = graph.paths ?? [];
1978
2019
  const isReference2 = (p) => pathOrigin(p.name).name === graph.referencePath;
1979
- const referencePieces2 = paths.some(isReference2) ? paths.filter(isReference2).sort((a, b) => (a.start ?? 0) - (b.start ?? 0)) : paths.slice(0, 1);
1980
- const first = referencePieces2[0];
1981
- 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));
1982
2023
  if (!first || others.length === 0) {
1983
2024
  return void 0;
1984
2025
  }
2026
+ const walks = /* @__PURE__ */ new Map();
2027
+ for (const path of others) {
2028
+ walks.set(path.name, [...walks.get(path.name) ?? [], path]);
2029
+ }
1985
2030
  const lengthOf = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
1986
2031
  const referenceStart = first.start ?? 0;
1987
2032
  const cut = region && region.end > region.start ? region : void 0;
1988
2033
  const span = /* @__PURE__ */ new Map();
1989
- for (const piece of referencePieces2) {
2034
+ for (const piece of referencePieces) {
1990
2035
  let pos = piece.start ?? 0;
1991
2036
  for (const id of piece.nodeIds) {
1992
2037
  const len = lengthOf.get(id) ?? 0;
@@ -1997,20 +2042,38 @@ function walkRows(graph, region, unit) {
1997
2042
  }
1998
2043
  }
1999
2044
  const flanked = cut === void 0 || [...span.values()].some((s) => s.end <= cut.start) && [...span.values()].some((s) => s.start >= cut.end);
2000
- const rowOf = (path) => {
2001
- const { ids, complete } = sliceBetween(path.nodeIds, span, cut, flanked);
2045
+ const rowOf = (pieces) => {
2046
+ const path = pieces[0];
2047
+ const ordered = [...pieces].sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
2048
+ const { ids, complete, from, to } = sliceBetween(
2049
+ stepsOf(ordered, lengthOf),
2050
+ span,
2051
+ cut,
2052
+ flanked
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 };
2002
2057
  const runs = [];
2003
2058
  let bp = 0;
2004
2059
  let offReferenceBp = 0;
2060
+ let gapBp = 0;
2005
2061
  let step = 0;
2006
2062
  for (const id of ids) {
2063
+ if (typeof id === "number") {
2064
+ runs.push({ start: bp, bp: id, onReference: false, gap: true });
2065
+ bp += id;
2066
+ gapBp += id;
2067
+ step = 0;
2068
+ continue;
2069
+ }
2007
2070
  const len = lengthOf.get(id) ?? 0;
2008
2071
  const s = span.get(id);
2009
2072
  const last = runs.at(-1);
2010
2073
  const at = last?.referenceStart;
2011
2074
  const forward = s && at !== void 0 && step >= 0 && at + last.bp === s.start;
2012
2075
  const backward = s && at !== void 0 && step <= 0 && s.end === at;
2013
- if (!s && last && !last.onReference) {
2076
+ if (!s && last && !last.onReference && !last.gap) {
2014
2077
  last.bp += len;
2015
2078
  } else if (forward || backward) {
2016
2079
  last.bp += len;
@@ -2040,19 +2103,18 @@ function walkRows(graph, region, unit) {
2040
2103
  haplotype: path.haplotype,
2041
2104
  bp,
2042
2105
  offReferenceBp,
2106
+ gapBp,
2043
2107
  complete,
2044
- runs
2108
+ runs,
2109
+ axis
2045
2110
  };
2046
2111
  };
2047
2112
  const origin = cut ? cut.start : referenceStart;
2048
2113
  return {
2049
2114
  origin,
2050
2115
  unit,
2051
- reference: rowOf({
2052
- ...first,
2053
- nodeIds: referencePieces2.flatMap((piece) => piece.nodeIds)
2054
- }),
2055
- rows: others.map(rowOf).sort(
2116
+ reference: rowOf(referencePieces),
2117
+ rows: [...walks.values()].map(rowOf).sort(
2056
2118
  (a, b) => Number(b.complete) - Number(a.complete) || b.bp - a.bp || a.label.localeCompare(b.label)
2057
2119
  )
2058
2120
  };
@@ -2407,7 +2469,7 @@ function placeByWalks(graph, backbone) {
2407
2469
  return placed;
2408
2470
  }
2409
2471
  function placeBySearch(adjacent, key) {
2410
- 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);
2411
2473
  const placed = queue.length;
2412
2474
  for (const id of queue) {
2413
2475
  const from = key.get(id);
@@ -3031,7 +3093,7 @@ var LAYOUT_MODES = [
3031
3093
  {
3032
3094
  value: "walkrows",
3033
3095
  label: "Walk rows",
3034
- 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.",
3035
3097
  run: walkRowLayout,
3036
3098
  cutMargins: false,
3037
3099
  wholeWalks: true,
@@ -4235,7 +4297,7 @@ function walkHighlight(graph, name) {
4235
4297
  });
4236
4298
  }
4237
4299
  const reference = isReference(graph, name);
4238
- const referencePieces2 = reference ? [] : (graph.paths ?? []).filter((p) => isReference(graph, p.name));
4300
+ const referencePieces = reference ? [] : (graph.paths ?? []).filter((p) => isReference(graph, p.name));
4239
4301
  return {
4240
4302
  name,
4241
4303
  reference,
@@ -4245,7 +4307,7 @@ function walkHighlight(graph, name) {
4245
4307
  progress,
4246
4308
  steps: pieces.reduce((sum, piece) => sum + piece.nodeIds.length, 0),
4247
4309
  bp,
4248
- 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
4249
4311
  };
4250
4312
  }
4251
4313
  function laneColors(graph, walk, encoding, ramp) {
@@ -4349,8 +4411,11 @@ function walkKey(walk, reference) {
4349
4411
  const { field } = walk.encoding;
4350
4412
  const own = walk.range ? rangeText(walk.range.contig, walk.range.start, walk.range.end) : void 0;
4351
4413
  const scale = field === "progress" ? own : field === "reference" && reference ? rangeText(reference.name, reference.start, reference.end) : void 0;
4414
+ const spanned = walk.range ? walk.range.end - walk.range.start : walk.bp;
4415
+ const outside = spanned - walk.bp;
4352
4416
  return {
4353
- delta: walk.referenceBp === void 0 || walk.bp === walk.referenceBp ? "" : ` ${walk.bp > walk.referenceBp ? "+" : "\u2212"}${formatBp(Math.abs(walk.bp - walk.referenceBp))}`,
4417
+ delta: walk.referenceBp === void 0 || spanned === walk.referenceBp ? "" : ` ${spanned > walk.referenceBp ? "+" : "\u2212"}${formatBp(Math.abs(spanned - walk.referenceBp))}`,
4418
+ outside: outside > 0 ? `, ${formatBp(outside)} outside the cut` : "",
4354
4419
  reversed: walk.reversedBp > 0 ? `, ${formatBp(walk.reversedBp)} reversed` : "",
4355
4420
  shades: field !== "walk",
4356
4421
  scale,
@@ -4369,6 +4434,28 @@ function walkPosition(walk, nodeId, length) {
4369
4434
  return `${range.contig}:${groupDigits(start)}-${groupDigits(start + length)}`;
4370
4435
  }
4371
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
+
4372
4459
  // src/facetGrid.ts
4373
4460
  var FACET_GAP_PX = 8;
4374
4461
  var FACET_TITLE_PX = 34;
@@ -4675,6 +4762,385 @@ function layoutLabels(m) {
4675
4762
  };
4676
4763
  }
4677
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
+
4678
5144
  // src/renderer/svgCanvas.ts
4679
5145
  var n = (v) => +v.toFixed(1);
4680
5146
  function paint(css) {
@@ -4788,7 +5254,7 @@ function svgCanvas(width, height) {
4788
5254
  }
4789
5255
 
4790
5256
  // src/version.ts
4791
- var version = "4.0.29";
5257
+ var version = "4.1.0";
4792
5258
 
4793
5259
  // src/viewport.ts
4794
5260
  function axisScaleOf(scale, pixelRows) {
@@ -4833,12 +5299,12 @@ function screenToLayout(x, y, t, axis) {
4833
5299
 
4834
5300
  // src/figure.ts
4835
5301
  var FONT = 'font-family="Helvetica, Arial, sans-serif" font-size="11"';
4836
- var BAR_PX = 48;
5302
+ var BAR_PX2 = 48;
4837
5303
  var SWATCH_PX = 18;
4838
5304
  var KEY_GAP_PX = 16;
4839
5305
  var FADED = "rgb(160,160,160)";
4840
5306
  var EXON_COLOR = "#daa520";
4841
- var GENE_INK = "#1c1c22";
5307
+ var GENE_INK2 = "#1c1c22";
4842
5308
  var MIN_LANE_PX = 4;
4843
5309
  var EXON_GAP_PX = 1;
4844
5310
  var EXON_LINE_PX = 2;
@@ -4846,7 +5312,7 @@ function esc(s) {
4846
5312
  return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4847
5313
  }
4848
5314
  function keySvg(id, x, y, stops, label, after, under, opacity = 1) {
4849
- const bar = stops.length > 1 ? BAR_PX : SWATCH_PX;
5315
+ const bar = stops.length > 1 ? BAR_PX2 : SWATCH_PX;
4850
5316
  const fill = stops.length > 1 ? `fill="url(#${id})"` : `fill="${stops[0].color}"${opacity < 1 ? ` fill-opacity="${opacity}"` : ""}`;
4851
5317
  const defs = stops.length > 1 ? `<linearGradient id="${id}">${stops.map((s) => `<stop offset="${s.offset}" stop-color="${s.color}"/>`).join("")}</linearGradient>` : "";
4852
5318
  const width = Math.max(
@@ -4866,7 +5332,7 @@ function walkKeySvg(id, x, y, walk, label, reference) {
4866
5332
  y,
4867
5333
  encodingStops(walk.encoding),
4868
5334
  label,
4869
- key.delta + key.reversed,
5335
+ key.delta + key.outside + key.reversed,
4870
5336
  key.scale
4871
5337
  );
4872
5338
  }
@@ -4877,15 +5343,17 @@ function screenPath(d, t) {
4877
5343
  );
4878
5344
  }
4879
5345
  function chip(x, y, w, name, note) {
4880
- 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>`;
4881
5347
  }
4882
5348
  function figureSvg(graph, layout, o = {}) {
4883
5349
  const width = o.width ?? 1200;
4884
5350
  const room = o.height ?? 800;
4885
5351
  const region = o.region;
4886
5352
  const rampDomain = o.colorDomain ?? region;
5353
+ const rows = o.walkRows;
4887
5354
  const bounds = drawingBounds(layout, {
4888
- region: o.fitToDrawing ? void 0 : region
5355
+ region: o.fitToDrawing ? void 0 : region,
5356
+ extent: rows && layout.extent ? walkRowsExtent(rows) : void 0
4889
5357
  });
4890
5358
  const pixelRows = layout.pixelRows ?? false;
4891
5359
  const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
@@ -4910,7 +5378,7 @@ function figureSvg(graph, layout, o = {}) {
4910
5378
  };
4911
5379
  const contigThickness = o.contigThickness ?? 6;
4912
5380
  const nodeWidth2 = o.nodeWidth ?? "depth";
4913
- 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) : [];
4914
5382
  const ink = nodeInk(graph, nodeById, contigThickness, nodeWidth2);
4915
5383
  let masks = 0;
4916
5384
  function overlays(highlight, t, w, h) {
@@ -4946,11 +5414,32 @@ function figureSvg(graph, layout, o = {}) {
4946
5414
  )) {
4947
5415
  const pinY = screen(pin.at).y + contigThickness / 2;
4948
5416
  out.push(
4949
- `<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"/>`,
4950
5418
  chip(x, y, cw, pin.gene.name, text.slice(pin.gene.name.length))
4951
5419
  );
4952
5420
  }
4953
- 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) {
4954
5443
  const sy = y * t.scaleY + t.translateY;
4955
5444
  if (sy >= 0 && sy <= h) {
4956
5445
  out.push(
@@ -4981,7 +5470,7 @@ function figureSvg(graph, layout, o = {}) {
4981
5470
  const renderer = new Canvas2DRenderer(canvas);
4982
5471
  renderer.uploadGeometry(
4983
5472
  buildGeometry({
4984
- nodePositions: layout.nodePositions,
5473
+ nodePositions: rows ? {} : layout.nodePositions,
4985
5474
  graph,
4986
5475
  nodeById,
4987
5476
  colorScheme,
@@ -5057,7 +5546,21 @@ function figureSvg(graph, layout, o = {}) {
5057
5546
  0.3
5058
5547
  )
5059
5548
  );
5060
- } 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) {
5061
5564
  push(
5062
5565
  keySvg(
5063
5566
  "ramp",
@@ -5207,8 +5710,25 @@ function genesFromGff3Lines(lines) {
5207
5710
  }
5208
5711
  return genes;
5209
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
+ }
5210
5730
  function genesFromBed(text) {
5211
- const genes = /* @__PURE__ */ new Map();
5731
+ const rows = [];
5212
5732
  for (const line of text.split("\n")) {
5213
5733
  const cols = line.replace(/\r$/, "").split(" ");
5214
5734
  const start = Number(cols[1]);
@@ -5224,24 +5744,16 @@ function genesFromBed(text) {
5224
5744
  start: start + starts[i],
5225
5745
  end: start + starts[i] + size
5226
5746
  })) : [{ start, end }];
5227
- const key = `${refName} ${name}`;
5228
- const gene = genes.get(key);
5229
- if (gene) {
5230
- gene.start = Math.min(gene.start, start);
5231
- gene.end = Math.max(gene.end, end);
5232
- gene.exons = mergedIntervals([...gene.exons, ...blocks]);
5233
- } else {
5234
- genes.set(key, {
5235
- name,
5236
- refName,
5237
- start,
5238
- end,
5239
- strand: strandOf(cols[5]),
5240
- exons: mergedIntervals(blocks)
5241
- });
5242
- }
5747
+ rows.push({
5748
+ name,
5749
+ refName,
5750
+ start,
5751
+ end,
5752
+ strand: strandOf(cols[5]),
5753
+ exons: blocks
5754
+ });
5243
5755
  }
5244
- return [...genes.values()];
5756
+ return mergeOverlappingByName(rows);
5245
5757
  }
5246
5758
  function genesFromText(text) {
5247
5759
  const first = text.split("\n", 1e3).find((l) => l.trim() !== "" && !/^(#|track|browser)/.test(l));
@@ -5402,7 +5914,7 @@ function parseWalk(line) {
5402
5914
  end: +fields[5]
5403
5915
  };
5404
5916
  }
5405
- var stepsOf = (walk) => walk.fields[6].match(/[<>][^<>]+/g) ?? [];
5917
+ var stepsOf2 = (walk) => walk.fields[6].match(/[<>][^<>]+/g) ?? [];
5406
5918
  function segmentLengths(lines) {
5407
5919
  const lengths = /* @__PURE__ */ new Map();
5408
5920
  for (const line of lines) {
@@ -5421,7 +5933,7 @@ function extend(a, b, lengths) {
5421
5933
  if (b.end <= a.end) {
5422
5934
  return a;
5423
5935
  }
5424
- const steps = stepsOf(b);
5936
+ const steps = stepsOf2(b);
5425
5937
  let pos = b.start;
5426
5938
  for (const [i, step] of steps.entries()) {
5427
5939
  if (pos === a.end) {
@@ -5455,7 +5967,7 @@ function joinPieces(pieces, lengths, startsPath) {
5455
5967
  }
5456
5968
  var flip2 = (sign) => sign === "+" ? "-" : "+";
5457
5969
  function* stepLinks(walk) {
5458
- const steps = stepsOf(walk).map((step) => ({
5970
+ const steps = stepsOf2(walk).map((step) => ({
5459
5971
  id: step.slice(1),
5460
5972
  sign: step.startsWith(">") ? "+" : "-"
5461
5973
  }));
@@ -5576,13 +6088,13 @@ async function referencePathQuery(db, referenceSample2, refName) {
5576
6088
  haplotype: path.name.haplotype
5577
6089
  } : void 0;
5578
6090
  }
5579
- function nodeLimitError(error, limit, windowBp, walkedFrom = 0) {
6091
+ function nodeLimitError(error, limit, windowBp) {
5580
6092
  const isLimit = error instanceof Error && (error.name === "SubgraphLimitError" || /^Subgraph size limit of \d+ nodes exceeded/.test(error.message));
5581
6093
  if (!isLimit) {
5582
6094
  return void 0;
5583
6095
  } else {
5584
6096
  const walked = error.walkedBp;
5585
- 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);
5586
6098
  return new NodeLimitError(limit, windowBp, Math.max(fits, 1));
5587
6099
  }
5588
6100
  }
@@ -5591,44 +6103,16 @@ var GBZ_CUT_DEFAULTS = {
5591
6103
  snarls: "contained",
5592
6104
  limit: 1e5
5593
6105
  };
5594
- async function referencePieces(db, query, start, end) {
5595
- const inside = (await db.paths()).filter(
5596
- ({ isIndexed, name }) => isIndexed && name.sample === query.sample && name.haplotype === query.haplotype && name.contig === query.contig && name.fragment > start && name.fragment < end
5597
- ).map((path) => path.name.fragment);
5598
- const bounds = [start, ...[...new Set(inside)].sort((a, b) => a - b), end];
5599
- return bounds.slice(1).map((pieceEnd, i) => ({
5600
- start: bounds[i],
5601
- end: pieceEnd
5602
- }));
5603
- }
5604
- async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
6106
+ async function cutWindowGFA(db, query, start, end, opts) {
5605
6107
  if (!query) {
5606
6108
  return "";
5607
6109
  }
5608
- const pieces = await referencePieces(db, query, start, end);
5609
- const subgraphs = await Promise.all(
5610
- pieces.map(
5611
- (piece) => db.getSubgraphForRange(query, piece.start, piece.end, {
5612
- ...opts,
5613
- haplotypes: "all",
5614
- ...keep === void 0 ? {} : { keep }
5615
- }).catch((error) => {
5616
- throw nodeLimitError(
5617
- error,
5618
- opts.limit,
5619
- end - start,
5620
- piece.start - start
5621
- ) ?? error;
5622
- })
5623
- )
5624
- );
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
+ });
5625
6113
  const paths = await db.paths();
5626
6114
  return joinCuts(
5627
- await Promise.all(
5628
- subgraphs.flatMap(
5629
- (subgraph) => subgraph ? [subgraph.toGFA({ names: "resolved" })] : []
5630
- )
5631
- ),
6115
+ await Promise.all(subgraphs.map((subgraph) => subgraph.toGFA())),
5632
6116
  (sample, haplotype, contig, at) => paths.some(
5633
6117
  ({ name }) => name.sample === sample && name.haplotype === haplotype && name.contig === contig && name.fragment === at
5634
6118
  )
@@ -5656,7 +6140,7 @@ function parseRegion(text) {
5656
6140
  return { refName: m[1], start: n2(m[2]), end: n2(m[3]) };
5657
6141
  }
5658
6142
  async function openGbz(db, index) {
5659
- const base = await GBZBase.open(db, index ? { haplotypeIndex: index } : {});
6143
+ const base = await GBZBase.open({ source: db, haplotypeIndex: index });
5660
6144
  return { base, referenceSamples: await referenceSamplesOf(base) };
5661
6145
  }
5662
6146
  function referenceSample(ref, samples) {
@@ -5687,7 +6171,7 @@ async function cutGbzRegion({ base, referenceSamples }, src, signal) {
5687
6171
  snarls: src.snarls ?? GBZ_CUT_DEFAULTS.snarls,
5688
6172
  limit: GBZ_CUT_DEFAULTS.limit,
5689
6173
  signal,
5690
- ...wanted ? { keep: (name) => haplotypeWanted(name, wanted) } : {}
6174
+ keep: wanted ? (name) => haplotypeWanted(name, wanted) : void 0
5691
6175
  });
5692
6176
  return { text, region, sample };
5693
6177
  }
@@ -5765,6 +6249,8 @@ export {
5765
6249
  rangeText,
5766
6250
  walkKey,
5767
6251
  walkPosition,
6252
+ el,
6253
+ serializeEl,
5768
6254
  FACET_GAP_PX,
5769
6255
  FACET_TITLE_PX,
5770
6256
  FACET_PAD_PX,
@@ -5776,6 +6262,28 @@ export {
5776
6262
  LEGEND_INSET_PX,
5777
6263
  geneCoverageNote,
5778
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,
5779
6287
  svgCanvas,
5780
6288
  version,
5781
6289
  axisScaleOf,
@@ -5786,6 +6294,7 @@ export {
5786
6294
  screenToLayout,
5787
6295
  figureSvg,
5788
6296
  genesFromGff3Lines,
6297
+ mergeOverlappingByName,
5789
6298
  genesFromBed,
5790
6299
  genesFromText,
5791
6300
  WELL_KNOWN_SAMPLES,