@jbrowse/bandage-core 4.0.28 → 4.0.30

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.
@@ -145,6 +145,9 @@ function isOffReference(node) {
145
145
  function backboneNodes(graph) {
146
146
  return graph.nodes.filter(isBackbone);
147
147
  }
148
+ function overlapsWindow(node, window) {
149
+ return node.stable.start < window.end && node.stable.start + node.length > window.start;
150
+ }
148
151
  function backbonePositions(backbone) {
149
152
  const positions = {};
150
153
  for (const node of backbone) {
@@ -445,6 +448,7 @@ function surveySegments(gfaGraph) {
445
448
  }
446
449
  return { canonical, traversals };
447
450
  }
451
+ var walkStart = (w) => w.start === -1 ? 0 : w.start;
448
452
  function anchorablePaths(gfaGraph) {
449
453
  return [
450
454
  ...gfaGraph.paths.map((p) => ({
@@ -456,10 +460,7 @@ function anchorablePaths(gfaGraph) {
456
460
  })),
457
461
  ...gfaGraph.walks.map((w) => ({
458
462
  name: `${w.sample}#${w.haplotype}#${w.contig}`,
459
- // `*` parses to -1, meaning the record declines to say; 0 is then the
460
- // only offset that can be assumed, and it makes the walk self-relative
461
- // rather than wrong
462
- start: w.start === -1 ? 0 : w.start,
463
+ start: walkStart(w),
463
464
  steps: w.segments.map((s) => ({
464
465
  id: s.id,
465
466
  strand: s.strand === "-" ? "-" : "+"
@@ -521,7 +522,11 @@ function convertGFAToGraph(gfaGraph, name = "Imported GFA") {
521
522
  }
522
523
  for (const gfaPath of gfaGraph.paths) {
523
524
  const nodeIds = gfaPath.path.split(",").map((segment) => nodeId(segment.slice(0, -1)));
524
- paths.push({ name: gfaPath.name, nodeIds });
525
+ paths.push({
526
+ name: gfaPath.name,
527
+ nodeIds,
528
+ start: pathOrigin(gfaPath.name).start
529
+ });
525
530
  recordPathEdges(nodeIds, gfaPath.name);
526
531
  }
527
532
  for (const walk of gfaGraph.walks) {
@@ -530,6 +535,7 @@ function convertGFAToGraph(gfaGraph, name = "Imported GFA") {
530
535
  paths.push({
531
536
  name: name2,
532
537
  nodeIds,
538
+ start: walkStart(walk),
533
539
  sample: walk.sample,
534
540
  haplotype: walk.haplotype,
535
541
  contig: walk.contig
@@ -1910,18 +1916,25 @@ function offsetTiers(name, tiers) {
1910
1916
  return tiers.map((t) => `${t} @${groupDigits(start)}`);
1911
1917
  }
1912
1918
  function pathLegend(paths, colors) {
1913
- const named = paths.map((p) => labelTiers(p.name));
1919
+ const firstOf = /* @__PURE__ */ new Map();
1920
+ paths.forEach((path, i) => {
1921
+ if (!firstOf.has(path.name)) {
1922
+ firstOf.set(path.name, i);
1923
+ }
1924
+ });
1925
+ const entries = [...firstOf].map(([name, i]) => ({ name, i }));
1926
+ const named = entries.map((e) => labelTiers(e.name));
1914
1927
  const candidates = [0, 1, 2].flatMap((t) => [
1915
- paths.map((_, i) => named[i][t]),
1916
- paths.map((p, i) => offsetTiers(p.name, named[i])[t])
1928
+ entries.map((_, k) => named[k][t]),
1929
+ entries.map((e, k) => offsetTiers(e.name, named[k])[t])
1917
1930
  ]);
1918
1931
  const distinct = candidates.find(
1919
- (labels2) => new Set(labels2).size === paths.length
1932
+ (labels2) => new Set(labels2).size === entries.length
1920
1933
  );
1921
1934
  const labels = distinct ?? candidates.at(-1);
1922
- return paths.map((path, i) => ({
1923
- name: path.name,
1924
- label: labels[i],
1935
+ return entries.map(({ name, i }, k) => ({
1936
+ name,
1937
+ label: labels[k],
1925
1938
  color: colors?.[i] ?? pathCssColor(i, paths.length)
1926
1939
  }));
1927
1940
  }
@@ -1933,16 +1946,32 @@ function sampleOf(path) {
1933
1946
  function labelOf(path) {
1934
1947
  return path.haplotype !== void 0 && path.sample !== void 0 ? `${path.sample}#${path.haplotype}` : sampleOf(path);
1935
1948
  }
1936
- 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 sliceBetween(steps, span, region, flanked = true) {
1937
1966
  if (!region || !flanked) {
1938
- return { ids: nodeIds, complete: true };
1967
+ return { ids: steps, complete: true };
1939
1968
  }
1940
1969
  let i0 = -1;
1941
1970
  let i1 = -1;
1942
1971
  let bestEnd = -Infinity;
1943
1972
  let bestStart = Infinity;
1944
- nodeIds.forEach((id, i) => {
1945
- const s = span.get(id);
1973
+ steps.forEach((id, i) => {
1974
+ const s = typeof id === "string" ? span.get(id) : void 0;
1946
1975
  if (s) {
1947
1976
  if (s.end <= region.start && s.end > bestEnd) {
1948
1977
  bestEnd = s.end;
@@ -1955,44 +1984,68 @@ function sliceBetween(nodeIds, span, region, flanked = true) {
1955
1984
  }
1956
1985
  });
1957
1986
  if (i0 < 0 || i1 < 0) {
1958
- return { ids: nodeIds, complete: false };
1987
+ return { ids: steps, complete: false };
1959
1988
  }
1960
- const ids = nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
1989
+ const ids = steps.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
1961
1990
  return { ids: i0 < i1 ? ids : ids.reverse(), complete: true };
1962
1991
  }
1963
1992
  function walkRows(graph, region, unit) {
1964
1993
  const paths = graph.paths ?? [];
1965
- const reference = paths.find((p) => pathOrigin(p.name).name === graph.referencePath) ?? paths[0];
1966
- if (!reference || paths.length < 2) {
1994
+ 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));
1998
+ if (!first || others.length === 0) {
1967
1999
  return void 0;
1968
2000
  }
2001
+ const walks = /* @__PURE__ */ new Map();
2002
+ for (const path of others) {
2003
+ walks.set(path.name, [...walks.get(path.name) ?? [], path]);
2004
+ }
1969
2005
  const lengthOf = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
1970
- const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
2006
+ const referenceStart = first.start ?? 0;
1971
2007
  const cut = region && region.end > region.start ? region : void 0;
1972
2008
  const span = /* @__PURE__ */ new Map();
1973
- let pos = referenceStart;
1974
- for (const id of reference.nodeIds) {
1975
- const len = lengthOf.get(id) ?? 0;
1976
- if (!span.has(id)) {
1977
- span.set(id, { start: pos, end: pos + len });
2009
+ for (const piece of referencePieces2) {
2010
+ let pos = piece.start ?? 0;
2011
+ for (const id of piece.nodeIds) {
2012
+ const len = lengthOf.get(id) ?? 0;
2013
+ if (!span.has(id)) {
2014
+ span.set(id, { start: pos, end: pos + len });
2015
+ }
2016
+ pos += len;
1978
2017
  }
1979
- pos += len;
1980
2018
  }
1981
2019
  const flanked = cut === void 0 || [...span.values()].some((s) => s.end <= cut.start) && [...span.values()].some((s) => s.start >= cut.end);
1982
- const rowOf = (path) => {
1983
- const { ids, complete } = sliceBetween(path.nodeIds, span, cut, flanked);
2020
+ const rowOf = (pieces) => {
2021
+ const path = pieces[0];
2022
+ const ordered = [...pieces].sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
2023
+ const { ids, complete } = sliceBetween(
2024
+ stepsOf(ordered, lengthOf),
2025
+ span,
2026
+ cut,
2027
+ flanked
2028
+ );
1984
2029
  const runs = [];
1985
2030
  let bp = 0;
1986
2031
  let offReferenceBp = 0;
2032
+ let gapBp = 0;
1987
2033
  let step = 0;
1988
2034
  for (const id of ids) {
2035
+ if (typeof id === "number") {
2036
+ runs.push({ start: bp, bp: id, onReference: false, gap: true });
2037
+ bp += id;
2038
+ gapBp += id;
2039
+ step = 0;
2040
+ continue;
2041
+ }
1989
2042
  const len = lengthOf.get(id) ?? 0;
1990
2043
  const s = span.get(id);
1991
2044
  const last = runs.at(-1);
1992
2045
  const at = last?.referenceStart;
1993
2046
  const forward = s && at !== void 0 && step >= 0 && at + last.bp === s.start;
1994
2047
  const backward = s && at !== void 0 && step <= 0 && s.end === at;
1995
- if (!s && last && !last.onReference) {
2048
+ if (!s && last && !last.onReference && !last.gap) {
1996
2049
  last.bp += len;
1997
2050
  } else if (forward || backward) {
1998
2051
  last.bp += len;
@@ -2022,22 +2075,17 @@ function walkRows(graph, region, unit) {
2022
2075
  haplotype: path.haplotype,
2023
2076
  bp,
2024
2077
  offReferenceBp,
2078
+ gapBp,
2025
2079
  complete,
2026
2080
  runs
2027
2081
  };
2028
2082
  };
2029
- const meetsReference = (p) => p.nodeIds.some((id) => span.has(id));
2030
- const placedWalks = new Set(
2031
- paths.filter((p) => p !== reference && meetsReference(p)).map((p) => p.name)
2032
- );
2033
2083
  const origin = cut ? cut.start : referenceStart;
2034
2084
  return {
2035
2085
  origin,
2036
2086
  unit,
2037
- reference: rowOf(reference),
2038
- rows: paths.filter(
2039
- (p) => p !== reference && (meetsReference(p) || !placedWalks.has(p.name))
2040
- ).map(rowOf).sort(
2087
+ reference: rowOf(referencePieces2),
2088
+ rows: [...walks.values()].map(rowOf).sort(
2041
2089
  (a, b) => Number(b.complete) - Number(a.complete) || b.bp - a.bp || a.label.localeCompare(b.label)
2042
2090
  )
2043
2091
  };
@@ -2061,7 +2109,10 @@ function walkRowLayout(graph, region) {
2061
2109
  if (backbone.length === 0 || !walks) {
2062
2110
  return void 0;
2063
2111
  }
2064
- const nodePositions = backbonePositions(backbone);
2112
+ const overWindow = region ? backbone.filter((n2) => overlapsWindow(n2, region)) : backbone;
2113
+ const nodePositions = backbonePositions(
2114
+ overWindow.length > 0 ? overWindow : backbone
2115
+ );
2065
2116
  const rowLabels = [
2066
2117
  { label: walks.reference.label, y: 0 },
2067
2118
  ...walks.rows.map((row, i) => ({
@@ -4168,12 +4219,14 @@ function encodingSwatchCss(encoding) {
4168
4219
  }
4169
4220
 
4170
4221
  // src/walkHighlight.ts
4171
- function referenceOf(graph) {
4172
- return graph.referencePath ? graph.paths?.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
4222
+ var isReference = (graph, name) => !!graph.referencePath && pathOrigin(name).name === graph.referencePath;
4223
+ function piecesOf(graph, name) {
4224
+ return (graph.paths ?? []).filter((p) => p.name === name).sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
4173
4225
  }
4174
4226
  function walkHighlight(graph, name) {
4175
- const path = graph.paths?.find((p) => p.name === name);
4176
- if (!path) {
4227
+ const pieces = piecesOf(graph, name);
4228
+ const first = pieces[0];
4229
+ if (!first) {
4177
4230
  return void 0;
4178
4231
  }
4179
4232
  const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
@@ -4181,43 +4234,51 @@ function walkHighlight(graph, name) {
4181
4234
  graph.edges.forEach((e, i) => {
4182
4235
  edgeAt.set(`${e.from}>${e.to}`, i);
4183
4236
  });
4184
- const edgeIndexes = /* @__PURE__ */ new Set();
4185
- for (let i = 1; i < path.nodeIds.length; i++) {
4186
- const a = path.nodeIds[i - 1];
4187
- const b = path.nodeIds[i];
4188
- const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
4189
- if (index !== void 0) {
4190
- edgeIndexes.add(index);
4191
- }
4192
- }
4193
4237
  const lengthOf = (id) => byId.get(id)?.length ?? 0;
4194
- const bp = path.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0);
4238
+ const bpOf = (path) => path.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0);
4239
+ const bp = pieces.reduce((sum, piece) => sum + bpOf(piece), 0);
4240
+ const range = first.start === void 0 ? void 0 : {
4241
+ contig: first.contig ?? panSNContig(pathOrigin(name).name),
4242
+ start: first.start,
4243
+ end: Math.max(...pieces.map((p) => (p.start ?? 0) + bpOf(p)))
4244
+ };
4245
+ const span = range ? range.end - range.start : bp;
4246
+ const nodeIds = /* @__PURE__ */ new Set();
4247
+ const edgeIndexes = /* @__PURE__ */ new Set();
4195
4248
  const progress = /* @__PURE__ */ new Map();
4196
4249
  let before = 0;
4197
- for (const id of path.nodeIds) {
4198
- const length = lengthOf(id);
4199
- if (!progress.has(id)) {
4200
- progress.set(id, bp > 0 ? (before + length / 2) / bp : 0);
4201
- }
4202
- before += length;
4250
+ for (const piece of pieces) {
4251
+ if (range) {
4252
+ before = Math.max(before, (piece.start ?? 0) - range.start);
4253
+ }
4254
+ piece.nodeIds.forEach((id, i) => {
4255
+ nodeIds.add(id);
4256
+ if (i > 0) {
4257
+ const a = piece.nodeIds[i - 1];
4258
+ const index = edgeAt.get(`${a}>${id}`) ?? edgeAt.get(`${id}>${a}`);
4259
+ if (index !== void 0) {
4260
+ edgeIndexes.add(index);
4261
+ }
4262
+ }
4263
+ const length = lengthOf(id);
4264
+ if (!progress.has(id)) {
4265
+ progress.set(id, span > 0 ? (before + length / 2) / span : 0);
4266
+ }
4267
+ before += length;
4268
+ });
4203
4269
  }
4204
- const reference = referenceOf(graph);
4205
- const originName = pathOrigin(name).name;
4206
- const origin = graph.anchorPaths?.find((a) => a.name === originName && a.length === bp) ?? graph.anchorPaths?.find((a) => a.name === originName);
4270
+ const reference = isReference(graph, name);
4271
+ const referencePieces2 = reference ? [] : (graph.paths ?? []).filter((p) => isReference(graph, p.name));
4207
4272
  return {
4208
4273
  name,
4209
- reference: reference === path,
4210
- range: origin ? {
4211
- contig: path.contig ?? panSNContig(origin.name),
4212
- start: origin.start,
4213
- end: origin.start + origin.length
4214
- } : void 0,
4215
- nodeIds: new Set(path.nodeIds),
4274
+ reference,
4275
+ range,
4276
+ nodeIds,
4216
4277
  edgeIndexes,
4217
4278
  progress,
4218
- steps: path.nodeIds.length,
4279
+ steps: pieces.reduce((sum, piece) => sum + piece.nodeIds.length, 0),
4219
4280
  bp,
4220
- referenceBp: reference && reference !== path ? reference.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0) : void 0
4281
+ referenceBp: referencePieces2.length > 0 ? referencePieces2.reduce((sum, piece) => sum + bpOf(piece), 0) : void 0
4221
4282
  };
4222
4283
  }
4223
4284
  function laneColors(graph, walk, encoding, ramp) {
@@ -4232,14 +4293,13 @@ function laneColors(graph, walk, encoding, ramp) {
4232
4293
  return colors;
4233
4294
  }
4234
4295
  function reversedNodes(graph, walk) {
4235
- const reference = referenceOf(graph);
4236
4296
  const reversed = /* @__PURE__ */ new Set();
4237
4297
  let bp = 0;
4238
- if (!reference || walk.reference) {
4298
+ const referencePath = graph.referencePath;
4299
+ if (!referencePath || walk.reference) {
4239
4300
  return { reversed, bp };
4240
4301
  }
4241
4302
  const walkPath = pathOrigin(walk.name).name;
4242
- const referencePath = pathOrigin(reference.name).name;
4243
4303
  const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
4244
4304
  for (const id of walk.progress.keys()) {
4245
4305
  const node = byId.get(id);
@@ -4322,8 +4382,11 @@ function walkKey(walk, reference) {
4322
4382
  const { field } = walk.encoding;
4323
4383
  const own = walk.range ? rangeText(walk.range.contig, walk.range.start, walk.range.end) : void 0;
4324
4384
  const scale = field === "progress" ? own : field === "reference" && reference ? rangeText(reference.name, reference.start, reference.end) : void 0;
4385
+ const spanned = walk.range ? walk.range.end - walk.range.start : walk.bp;
4386
+ const outside = spanned - walk.bp;
4325
4387
  return {
4326
- delta: walk.referenceBp === void 0 || walk.bp === walk.referenceBp ? "" : ` ${walk.bp > walk.referenceBp ? "+" : "\u2212"}${formatBp(Math.abs(walk.bp - walk.referenceBp))}`,
4388
+ delta: walk.referenceBp === void 0 || spanned === walk.referenceBp ? "" : ` ${spanned > walk.referenceBp ? "+" : "\u2212"}${formatBp(Math.abs(spanned - walk.referenceBp))}`,
4389
+ outside: outside > 0 ? `, ${formatBp(outside)} outside the cut` : "",
4327
4390
  reversed: walk.reversedBp > 0 ? `, ${formatBp(walk.reversedBp)} reversed` : "",
4328
4391
  shades: field !== "walk",
4329
4392
  scale,
@@ -4336,7 +4399,9 @@ function walkPosition(walk, nodeId, length) {
4336
4399
  if (progress === void 0 || !range) {
4337
4400
  return void 0;
4338
4401
  }
4339
- const start = Math.round(range.start + progress * walk.bp - length / 2);
4402
+ const start = Math.round(
4403
+ range.start + progress * (range.end - range.start) - length / 2
4404
+ );
4340
4405
  return `${range.contig}:${groupDigits(start)}-${groupDigits(start + length)}`;
4341
4406
  }
4342
4407
 
@@ -4389,11 +4454,26 @@ function facetGrid({
4389
4454
  }
4390
4455
  return best;
4391
4456
  }
4392
- function facetCells(names, by) {
4457
+ function facetSettingOf(facet) {
4458
+ const raw = typeof facet === "string" ? { field: facet } : facet ?? {};
4459
+ const field = raw.field === "walk" || raw.field === "sample" ? raw.field : "";
4460
+ return {
4461
+ field,
4462
+ domain: [...raw.domain ?? []],
4463
+ ...raw.columns === void 0 ? {} : { columns: raw.columns }
4464
+ };
4465
+ }
4466
+ function domainFirst(names, domain) {
4467
+ const rank = new Map(domain.map((name, i) => [name, i]));
4468
+ const listed = (name) => rank.get(name) ?? domain.length;
4469
+ return names.map((name, i) => ({ name, i })).sort((a, b) => listed(a.name) - listed(b.name) || a.i - b.i).map(({ name }) => name);
4470
+ }
4471
+ function facetCells(names, by, domain = []) {
4472
+ const wrapOrder = domainFirst(names, domain);
4393
4473
  const wrap = {
4394
4474
  columns: void 0,
4395
4475
  count: names.length,
4396
- cells: names.map((_, i) => i)
4476
+ cells: names.map((name) => wrapOrder.indexOf(name))
4397
4477
  };
4398
4478
  if (by === "walk") {
4399
4479
  return wrap;
@@ -4403,7 +4483,10 @@ function facetCells(names, by) {
4403
4483
  const n2 = Number(haplotype);
4404
4484
  return contig !== void 0 && Number.isInteger(n2) && n2 >= 0 ? { sample, column: Math.max(0, n2 - 1) } : void 0;
4405
4485
  });
4406
- const rows = [...new Set(places.map((p) => p?.sample))];
4486
+ const rows = domainFirst(
4487
+ [...new Set(places.flatMap((p) => p ? [p.sample] : []))],
4488
+ domain
4489
+ );
4407
4490
  const columns = Math.max(1, ...places.map((p) => p ? p.column + 1 : 1));
4408
4491
  const cells = places.map(
4409
4492
  (p) => p ? rows.indexOf(p.sample) * columns + p.column : -1
@@ -4741,7 +4824,7 @@ function svgCanvas(width, height) {
4741
4824
  }
4742
4825
 
4743
4826
  // src/version.ts
4744
- var version = "4.0.28";
4827
+ var version = "4.0.30";
4745
4828
 
4746
4829
  // src/viewport.ts
4747
4830
  function axisScaleOf(scale, pixelRows) {
@@ -4819,7 +4902,7 @@ function walkKeySvg(id, x, y, walk, label, reference) {
4819
4902
  y,
4820
4903
  encodingStops(walk.encoding),
4821
4904
  label,
4822
- key.delta + key.reversed,
4905
+ key.delta + key.outside + key.reversed,
4823
4906
  key.scale
4824
4907
  );
4825
4908
  }
@@ -4848,8 +4931,8 @@ function figureSvg(graph, layout, o = {}) {
4848
4931
  const layers = o.walks ?? [];
4849
4932
  const walkRamp = layers.some((l) => l.color?.field === "reference") ? referenceRamp ?? computeReferenceRamp(graph, rampDomain) : void 0;
4850
4933
  const lift = layers.length > 0 && !layout.tubeMap ? walkLift(graph, layers, walkRamp) : void 0;
4851
- const by = o.facet ?? "none";
4852
- const panels = by !== "none" && lift && lift.walks.length > 1 ? facetLifts(graph, lift, layers, walkRamp) : void 0;
4934
+ const facet = facetSettingOf(o.facet);
4935
+ const panels = facet.field && lift && lift.walks.length > 1 ? facetLifts(graph, lift, layers, walkRamp) : void 0;
4853
4936
  const labels = new Map(
4854
4937
  (graph.paths?.length ? pathLegend(graph.paths) : []).map((p) => [
4855
4938
  p.name,
@@ -4966,7 +5049,8 @@ function figureSvg(graph, layout, o = {}) {
4966
5049
  if (panels) {
4967
5050
  const place = facetCells(
4968
5051
  panels.map((p) => p.walks[0].name),
4969
- by
5052
+ facet.field || "walk",
5053
+ facet.domain
4970
5054
  );
4971
5055
  const grid = facetGrid({
4972
5056
  count: place.count,
@@ -4974,7 +5058,7 @@ function figureSvg(graph, layout, o = {}) {
4974
5058
  pixelRows,
4975
5059
  width,
4976
5060
  room,
4977
- columns: place.columns ?? o.columns
5061
+ columns: place.columns ?? facet.columns
4978
5062
  });
4979
5063
  panels.forEach((panel, i) => {
4980
5064
  const cell = place.cells[i];
@@ -5343,6 +5427,135 @@ function featuresOnBackbone(features, backbone, aliases) {
5343
5427
  });
5344
5428
  }
5345
5429
 
5430
+ // src/gbzJoin.ts
5431
+ var UNKNOWN_SAMPLE = "unknown";
5432
+ function parseWalk(line) {
5433
+ const fields = line.split(" ");
5434
+ return {
5435
+ fields,
5436
+ name: fields.slice(1, 4).join(" "),
5437
+ start: +fields[4],
5438
+ end: +fields[5]
5439
+ };
5440
+ }
5441
+ var stepsOf2 = (walk) => walk.fields[6].match(/[<>][^<>]+/g) ?? [];
5442
+ function segmentLengths(lines) {
5443
+ const lengths = /* @__PURE__ */ new Map();
5444
+ for (const line of lines) {
5445
+ if (line.startsWith("S ")) {
5446
+ const [, id, sequence, ...tags] = line.split(" ");
5447
+ const ln = tags.find((tag) => tag.startsWith("LN:i:"));
5448
+ lengths.set(
5449
+ id,
5450
+ sequence === "*" && ln ? +ln.slice("LN:i:".length) : sequence.length
5451
+ );
5452
+ }
5453
+ }
5454
+ return lengths;
5455
+ }
5456
+ function extend(a, b, lengths) {
5457
+ if (b.end <= a.end) {
5458
+ return a;
5459
+ }
5460
+ const steps = stepsOf2(b);
5461
+ let pos = b.start;
5462
+ for (const [i, step] of steps.entries()) {
5463
+ if (pos === a.end) {
5464
+ const fields = [...a.fields];
5465
+ fields[5] = String(b.end);
5466
+ fields[6] = a.fields[6] + steps.slice(i).join("");
5467
+ return { ...a, fields, end: b.end };
5468
+ }
5469
+ const length = lengths.get(step.slice(1));
5470
+ if (pos > a.end || length === void 0) {
5471
+ return void 0;
5472
+ }
5473
+ pos += length;
5474
+ }
5475
+ return void 0;
5476
+ }
5477
+ function joinPieces(pieces, lengths, startsPath) {
5478
+ const joined = [];
5479
+ for (const piece of pieces.sort((a, b) => a.start - b.start)) {
5480
+ const last = joined.at(-1);
5481
+ const [, sample, haplotype, contig] = piece.fields;
5482
+ const joins = last !== void 0 && (piece.start < last.end || piece.start === last.end && !startsPath(sample, +haplotype, contig, piece.start));
5483
+ const extended = joins ? extend(last, piece, lengths) : void 0;
5484
+ if (extended) {
5485
+ joined[joined.length - 1] = extended;
5486
+ } else {
5487
+ joined.push(piece);
5488
+ }
5489
+ }
5490
+ return joined;
5491
+ }
5492
+ var flip2 = (sign) => sign === "+" ? "-" : "+";
5493
+ function* stepLinks(walk) {
5494
+ const steps = stepsOf2(walk).map((step) => ({
5495
+ id: step.slice(1),
5496
+ sign: step.startsWith(">") ? "+" : "-"
5497
+ }));
5498
+ for (let i = 1; i < steps.length; i++) {
5499
+ const a = steps[i - 1];
5500
+ const b = steps[i];
5501
+ yield {
5502
+ forward: `L ${a.id} ${a.sign} ${b.id} ${b.sign} 0M`,
5503
+ reverse: `L ${b.id} ${flip2(b.sign)} ${a.id} ${flip2(a.sign)} 0M`
5504
+ };
5505
+ }
5506
+ }
5507
+ function joinCuts(gfas, startsPath) {
5508
+ if (gfas.length <= 1) {
5509
+ return gfas[0] ?? "";
5510
+ }
5511
+ const cuts = gfas.map((gfa) => gfa.split("\n").filter((line) => line !== ""));
5512
+ const ofType = (cut, type) => cut.filter((line) => line.startsWith(`${type} `));
5513
+ const lengths = segmentLengths(cuts.flat());
5514
+ let unknownOffset = 0;
5515
+ const walksByCut = cuts.map((cut) => {
5516
+ const walks2 = ofType(cut, "W").map(parseWalk);
5517
+ const offset = unknownOffset;
5518
+ for (const walk of walks2.filter((w) => w.fields[1] === UNKNOWN_SAMPLE)) {
5519
+ const haplotype = +walk.fields[2] + offset;
5520
+ walk.fields[2] = String(haplotype);
5521
+ walk.name = walk.fields.slice(1, 4).join(" ");
5522
+ unknownOffset = Math.max(unknownOffset, haplotype);
5523
+ }
5524
+ return walks2;
5525
+ });
5526
+ const byName = /* @__PURE__ */ new Map();
5527
+ for (const walk of [
5528
+ ...walksByCut.flatMap((walks2) => walks2.slice(0, 1)),
5529
+ ...walksByCut.flatMap((walks2) => walks2.slice(1))
5530
+ ]) {
5531
+ const pieces = byName.get(walk.name);
5532
+ if (pieces) {
5533
+ pieces.push(walk);
5534
+ } else {
5535
+ byName.set(walk.name, [walk]);
5536
+ }
5537
+ }
5538
+ const walks = [...byName.values()].flatMap(
5539
+ (pieces) => joinPieces(pieces, lengths, startsPath)
5540
+ );
5541
+ const links = new Set(cuts.flatMap((cut) => ofType(cut, "L")));
5542
+ for (const walk of walks) {
5543
+ for (const link of stepLinks(walk)) {
5544
+ if (!links.has(link.forward) && !links.has(link.reverse)) {
5545
+ links.add(link.forward);
5546
+ }
5547
+ }
5548
+ }
5549
+ const lines = /* @__PURE__ */ new Set([
5550
+ ...ofType(cuts[0], "H"),
5551
+ ...cuts.flatMap((cut) => ofType(cut, "S")),
5552
+ ...links,
5553
+ ...walks.map((walk) => walk.fields.join(" "))
5554
+ ]);
5555
+ return `${[...lines].join("\n")}
5556
+ `;
5557
+ }
5558
+
5346
5559
  // src/gbzWindow.ts
5347
5560
  var NoReferenceSampleError = class extends Error {
5348
5561
  constructor(anchor, referenceSamples) {
@@ -5399,13 +5612,13 @@ async function referencePathQuery(db, referenceSample2, refName) {
5399
5612
  haplotype: path.name.haplotype
5400
5613
  } : void 0;
5401
5614
  }
5402
- function nodeLimitError(error, limit, windowBp) {
5615
+ function nodeLimitError(error, limit, windowBp, walkedFrom = 0) {
5403
5616
  const isLimit = error instanceof Error && (error.name === "SubgraphLimitError" || /^Subgraph size limit of \d+ nodes exceeded/.test(error.message));
5404
5617
  if (!isLimit) {
5405
5618
  return void 0;
5406
5619
  } else {
5407
5620
  const walked = error.walkedBp;
5408
- const fits = typeof walked === "number" && walked > 0 ? Math.floor(walked * 0.8) : Math.floor(windowBp / 2);
5621
+ const fits = typeof walked === "number" && walked > 0 ? Math.floor((walkedFrom + walked) * 0.8) : Math.floor(windowBp / 2);
5409
5622
  return new NodeLimitError(limit, windowBp, Math.max(fits, 1));
5410
5623
  }
5411
5624
  }
@@ -5424,19 +5637,6 @@ async function referencePieces(db, query, start, end) {
5424
5637
  end: pieceEnd
5425
5638
  }));
5426
5639
  }
5427
- function joinCuts(gfas) {
5428
- const cuts = gfas.map((gfa) => gfa.split("\n").filter((line) => line !== ""));
5429
- const ofType = (cut, type) => cut.filter((line) => line.startsWith(type));
5430
- const lines = /* @__PURE__ */ new Set([
5431
- ...ofType(cuts[0] ?? [], "H"),
5432
- ...cuts.flatMap((cut) => ofType(cut, "S")),
5433
- ...cuts.flatMap((cut) => ofType(cut, "L")),
5434
- ...cuts.flatMap((cut) => ofType(cut, "W").slice(0, 1)),
5435
- ...cuts.flatMap((cut) => ofType(cut, "W").slice(1))
5436
- ]);
5437
- return lines.size === 0 ? "" : `${[...lines].join("\n")}
5438
- `;
5439
- }
5440
5640
  async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
5441
5641
  if (!query) {
5442
5642
  return "";
@@ -5449,15 +5649,24 @@ async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
5449
5649
  haplotypes: "all",
5450
5650
  ...keep === void 0 ? {} : { keep }
5451
5651
  }).catch((error) => {
5452
- throw nodeLimitError(error, opts.limit, end - start) ?? error;
5652
+ throw nodeLimitError(
5653
+ error,
5654
+ opts.limit,
5655
+ end - start,
5656
+ piece.start - start
5657
+ ) ?? error;
5453
5658
  })
5454
5659
  )
5455
5660
  );
5661
+ const paths = await db.paths();
5456
5662
  return joinCuts(
5457
5663
  await Promise.all(
5458
5664
  subgraphs.flatMap(
5459
5665
  (subgraph) => subgraph ? [subgraph.toGFA({ names: "resolved" })] : []
5460
5666
  )
5667
+ ),
5668
+ (sample, haplotype, contig, at) => paths.some(
5669
+ ({ name }) => name.sample === sample && name.haplotype === haplotype && name.contig === contig && name.fragment === at
5461
5670
  )
5462
5671
  );
5463
5672
  }
@@ -5596,6 +5805,7 @@ export {
5596
5805
  FACET_TITLE_PX,
5597
5806
  FACET_PAD_PX,
5598
5807
  facetGrid,
5808
+ facetSettingOf,
5599
5809
  facetCells,
5600
5810
  genePins,
5601
5811
  HALO_FACTOR,
@@ -17,7 +17,7 @@ import {
17
17
  openGbz,
18
18
  parseRegion,
19
19
  refNameBinding
20
- } from "../chunks/chunk-G3HO75W7.js";
20
+ } from "../chunks/chunk-P7UDDVHK.js";
21
21
 
22
22
  // src/cli/figure.ts
23
23
  import { access, open, readFile, writeFile } from "node:fs/promises";
package/dist/index.js CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  facetCells,
43
43
  facetGrid,
44
44
  facetLifts,
45
+ facetSettingOf,
45
46
  featuresOnBackbone,
46
47
  figureSvg,
47
48
  fitTransform,
@@ -100,7 +101,7 @@ import {
100
101
  wellKnownSample,
101
102
  yToXOf,
102
103
  zoomAbout
103
- } from "./chunks/chunk-G3HO75W7.js";
104
+ } from "./chunks/chunk-P7UDDVHK.js";
104
105
 
105
106
  // src/index.ts
106
107
  import { getDpr } from "@jbrowse/render-core/canvas2dUtils";
@@ -1715,6 +1716,7 @@ function bubbleSubgraph(graph, segmentIds) {
1715
1716
  const keep = new Set(segmentIds);
1716
1717
  const nodes = graph.nodes.filter((n) => keep.has(n.name));
1717
1718
  const ids = new Set(nodes.map((n) => n.id));
1719
+ const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
1718
1720
  const kept = /* @__PURE__ */ new Map();
1719
1721
  const paths = graph.paths?.flatMap((p) => {
1720
1722
  const first = p.nodeIds.findIndex((id) => ids.has(id));
@@ -1723,7 +1725,14 @@ function bubbleSubgraph(graph, segmentIds) {
1723
1725
  }
1724
1726
  const nodeIds = p.nodeIds.filter((id) => ids.has(id));
1725
1727
  kept.set(p.name, { first, nodeIds });
1726
- return [{ ...p, nodeIds }];
1728
+ const before = p.nodeIds.slice(0, first).reduce((sum, id) => sum + (lengthOf.get(id) ?? 0), 0);
1729
+ return [
1730
+ {
1731
+ ...p,
1732
+ nodeIds,
1733
+ ...p.start === void 0 ? {} : { start: p.start + before }
1734
+ }
1735
+ ];
1727
1736
  });
1728
1737
  return {
1729
1738
  name: graph.name,
@@ -1790,6 +1799,7 @@ export {
1790
1799
  facetCells,
1791
1800
  facetGrid,
1792
1801
  facetLifts,
1802
+ facetSettingOf,
1793
1803
  featuresOnBackbone,
1794
1804
  figureSvg,
1795
1805
  findHoveredEdge,
@@ -8,6 +8,10 @@ export declare function isAnchored(node: GraphNode): node is AnchoredNode;
8
8
  export declare function isBackbone(node: GraphNode): node is AnchoredNode;
9
9
  export declare function isOffReference(node: GraphNode): node is AnchoredNode;
10
10
  export declare function backboneNodes(graph: Graph): AnchoredNode[];
11
+ export declare function overlapsWindow(node: AnchoredNode, window: {
12
+ start: number;
13
+ end: number;
14
+ }): boolean;
11
15
  export declare function backbonePositions(backbone: AnchoredNode[]): Record<string, NodeSegment[]>;
12
16
  export declare function firstNodeAtOrAfter(nodes: AnchoredNode[], bp: number): number;
13
17
  export declare function backboneSpan(backbone: AnchoredNode[]): number;
@@ -20,7 +20,28 @@ export declare function facetGrid({ count, bounds, pixelRows, width, room, colum
20
20
  columns?: number;
21
21
  }): FacetGrid;
22
22
  export type FacetBy = 'walk' | 'sample';
23
- export declare function facetCells(names: string[], by: FacetBy): {
23
+ /**
24
+ * The facet as a session or a figure spec writes it, in the shape every
25
+ * JBrowse display's `facet` takes: the field the panels split on, `walk` for
26
+ * a panel per lifted walk or `sample` for a row per sample and a column per
27
+ * haplotype, the order the panels take, and how many go across under `walk`.
28
+ */
29
+ export interface FacetSetting {
30
+ field: '' | FacetBy;
31
+ /** The walks, or under `sample` the samples, whose panels come first, in order. */
32
+ domain: string[];
33
+ /** How many panels go across; unset takes whichever count draws each largest. */
34
+ columns?: number;
35
+ }
36
+ /** What a session or a spec may write for the facet: the field bare, or the setting's members. */
37
+ export type FacetInput = string | {
38
+ field?: string;
39
+ domain?: readonly string[];
40
+ columns?: number;
41
+ };
42
+ /** A facet written bare is its field, and a field the pane cannot split on is none. */
43
+ export declare function facetSettingOf(facet: FacetInput | undefined): FacetSetting;
44
+ export declare function facetCells(names: string[], by: FacetBy, domain?: readonly string[]): {
24
45
  columns: undefined;
25
46
  count: number;
26
47
  cells: number[];
@@ -1,5 +1,5 @@
1
1
  import type { ColorScheme } from './colorSchemes.js';
2
- import type { FacetBy } from './facetGrid.js';
2
+ import type { FacetInput } from './facetGrid.js';
3
3
  import type { GeneModel } from './genes/genePins.js';
4
4
  import type { NodeWidth } from './nodeWidths.js';
5
5
  import type { Graph, LayoutResult } from './types.js';
@@ -8,8 +8,7 @@ export interface FigureOptions {
8
8
  width?: number;
9
9
  height?: number;
10
10
  walks?: WalkLayer[];
11
- facet?: 'none' | FacetBy;
12
- columns?: number;
11
+ facet?: FacetInput;
13
12
  colorScheme?: ColorScheme;
14
13
  nodeWidth?: NodeWidth;
15
14
  showDeletionEdges?: boolean;
@@ -0,0 +1,2 @@
1
+ export type StartsPath = (sample: string, haplotype: number, contig: string, at: number) => boolean;
2
+ export declare function joinCuts(gfas: string[], startsPath: StartsPath): string;
@@ -16,7 +16,7 @@ export declare function resolveReferenceSample({ configured, anchorPrefix, refer
16
16
  referenceSamples: string[];
17
17
  }): string;
18
18
  export declare function referencePathQuery(db: GBZBase, referenceSample: string, refName: string): Promise<PathQuery | undefined>;
19
- export declare function nodeLimitError(error: unknown, limit: number, windowBp: number): NodeLimitError | undefined;
19
+ export declare function nodeLimitError(error: unknown, limit: number, windowBp: number, walkedFrom?: number): NodeLimitError | undefined;
20
20
  export interface GbzWindowOptions {
21
21
  context: number;
22
22
  snarls: RangeOptions['snarls'];
@@ -42,8 +42,8 @@ export type { GbzSource } from './gbzCut.js';
42
42
  export { version } from './version.js';
43
43
  export type { FigureOptions } from './figure.js';
44
44
  export { svgCanvas } from './renderer/svgCanvas.js';
45
- export { FACET_GAP_PX, FACET_PAD_PX, FACET_TITLE_PX, facetCells, facetGrid, } from './facetGrid.js';
46
- export type { FacetBy, FacetGrid } from './facetGrid.js';
45
+ export { FACET_GAP_PX, FACET_PAD_PX, FACET_TITLE_PX, facetCells, facetGrid, facetSettingOf, } from './facetGrid.js';
46
+ export type { FacetBy, FacetGrid, FacetInput, FacetSetting } from './facetGrid.js';
47
47
  export type { LiftedWalk, WalkLift } from './walkHighlight.js';
48
48
  export { WALK_FIELDS, WALK_SCHEMES, encodingStops, encodingSwatchCss, resolveEncoding, schemeCss, } from './walkEncoding.js';
49
49
  export type { WalkEncoding, WalkField, WalkLayer, WalkScheme, } from './walkEncoding.js';
@@ -5,6 +5,7 @@ export interface WalkRun {
5
5
  onReference: boolean;
6
6
  referenceStart?: number;
7
7
  reversed?: true;
8
+ gap?: true;
8
9
  }
9
10
  export interface WalkRow {
10
11
  name: string;
@@ -13,6 +14,7 @@ export interface WalkRow {
13
14
  haplotype?: number;
14
15
  bp: number;
15
16
  offReferenceBp: number;
17
+ gapBp: number;
16
18
  complete: boolean;
17
19
  runs: WalkRun[];
18
20
  }
@@ -19,6 +19,7 @@ export interface GraphEdge {
19
19
  export interface GraphPath {
20
20
  name: string;
21
21
  nodeIds: string[];
22
+ start?: number;
22
23
  sample?: string;
23
24
  haplotype?: number;
24
25
  contig?: string;
@@ -1 +1 @@
1
- export declare const version = "4.0.28";
1
+ export declare const version = "4.0.30";
@@ -6,6 +6,7 @@ export declare function walkKey(walk: LiftedWalk, reference?: {
6
6
  end: number;
7
7
  }): {
8
8
  delta: string;
9
+ outside: string;
9
10
  reversed: string;
10
11
  shades: boolean;
11
12
  scale: string | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/bandage-core",
3
- "version": "4.0.28",
3
+ "version": "4.0.30",
4
4
  "description": "Pangenome graph layout and drawing with no host: GFA in, the Bandage force-directed engine and reference-anchored layouts, a Canvas2D renderer, hit testing and labels out",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "dist"
32
32
  ],
33
33
  "dependencies": {
34
- "@gmod/gbz-base": "^4.1.0",
34
+ "@gmod/gbz-base": "^5.0.0",
35
35
  "@gmod/tabix": "^3.9.0",
36
36
  "@gmod/tubemap-core": "^0.1.0",
37
37
  "@jbrowse/core": "5.0.0-beta.9",