@jbrowse/bandage-core 4.0.27 → 4.0.29

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
  }
@@ -1962,21 +1975,26 @@ function sliceBetween(nodeIds, span, region, flanked = true) {
1962
1975
  }
1963
1976
  function walkRows(graph, region, unit) {
1964
1977
  const paths = graph.paths ?? [];
1965
- const reference = paths.find((p) => pathOrigin(p.name).name === graph.referencePath) ?? paths[0];
1966
- if (!reference || paths.length < 2) {
1978
+ 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));
1982
+ if (!first || others.length === 0) {
1967
1983
  return void 0;
1968
1984
  }
1969
1985
  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;
1986
+ const referenceStart = first.start ?? 0;
1971
1987
  const cut = region && region.end > region.start ? region : void 0;
1972
1988
  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 });
1989
+ for (const piece of referencePieces2) {
1990
+ let pos = piece.start ?? 0;
1991
+ for (const id of piece.nodeIds) {
1992
+ const len = lengthOf.get(id) ?? 0;
1993
+ if (!span.has(id)) {
1994
+ span.set(id, { start: pos, end: pos + len });
1995
+ }
1996
+ pos += len;
1978
1997
  }
1979
- pos += len;
1980
1998
  }
1981
1999
  const flanked = cut === void 0 || [...span.values()].some((s) => s.end <= cut.start) && [...span.values()].some((s) => s.start >= cut.end);
1982
2000
  const rowOf = (path) => {
@@ -2026,18 +2044,15 @@ function walkRows(graph, region, unit) {
2026
2044
  runs
2027
2045
  };
2028
2046
  };
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
2047
  const origin = cut ? cut.start : referenceStart;
2034
2048
  return {
2035
2049
  origin,
2036
2050
  unit,
2037
- reference: rowOf(reference),
2038
- rows: paths.filter(
2039
- (p) => p !== reference && (meetsReference(p) || !placedWalks.has(p.name))
2040
- ).map(rowOf).sort(
2051
+ reference: rowOf({
2052
+ ...first,
2053
+ nodeIds: referencePieces2.flatMap((piece) => piece.nodeIds)
2054
+ }),
2055
+ rows: others.map(rowOf).sort(
2041
2056
  (a, b) => Number(b.complete) - Number(a.complete) || b.bp - a.bp || a.label.localeCompare(b.label)
2042
2057
  )
2043
2058
  };
@@ -2061,7 +2076,10 @@ function walkRowLayout(graph, region) {
2061
2076
  if (backbone.length === 0 || !walks) {
2062
2077
  return void 0;
2063
2078
  }
2064
- const nodePositions = backbonePositions(backbone);
2079
+ const overWindow = region ? backbone.filter((n2) => overlapsWindow(n2, region)) : backbone;
2080
+ const nodePositions = backbonePositions(
2081
+ overWindow.length > 0 ? overWindow : backbone
2082
+ );
2065
2083
  const rowLabels = [
2066
2084
  { label: walks.reference.label, y: 0 },
2067
2085
  ...walks.rows.map((row, i) => ({
@@ -3153,7 +3171,7 @@ function nodeInk(graph, nodeById, contigThickness, nodeWidth2) {
3153
3171
  }
3154
3172
 
3155
3173
  // src/renderer/GeometryBuilder.ts
3156
- import { packAbgr as packAbgr2 } from "@jbrowse/core/util/colorBits";
3174
+ import { abgrToCssRgba, packAbgr as packAbgr2 } from "@jbrowse/core/util/colorBits";
3157
3175
 
3158
3176
  // src/renderer/colorBits.ts
3159
3177
  import {
@@ -3227,6 +3245,9 @@ var EDGE_DELETION_COLOR = packAbgr2(24, 24, 28, 240);
3227
3245
  var DELETION_THICKNESS_FACTOR = 2.2;
3228
3246
  var LIFT_BACKDROP_COLOR = packAbgr2(160, 160, 160, 255);
3229
3247
  var FADED_ALPHA = 0.18;
3248
+ var LIFT_BACKDROP_CSS = abgrToCssRgba(
3249
+ fadeAbgr(LIFT_BACKDROP_COLOR, FADED_ALPHA)
3250
+ );
3230
3251
  var DELETION_DASH_PX = 11;
3231
3252
  var MIN_PATH_STRIPE_PX = 1.2;
3232
3253
  var MIN_WALK_LANE_PX = 4;
@@ -3880,7 +3901,7 @@ function buildGeometry(options) {
3880
3901
 
3881
3902
  // src/renderer/Canvas2DRenderer.ts
3882
3903
  import {
3883
- abgrToCssRgba,
3904
+ abgrToCssRgba as abgrToCssRgba2,
3884
3905
  normalizedRgbToCssRgba
3885
3906
  } from "@jbrowse/core/util/colorBits";
3886
3907
  import { syncCanvasSize } from "@jbrowse/render-core/canvas2dUtils";
@@ -3949,7 +3970,7 @@ var Canvas2DRenderer = class extends Canvas2DRenderingBackendBase {
3949
3970
  weight: e.thickness,
3950
3971
  last: inRun(hl, i)
3951
3972
  }))) {
3952
- ctx.strokeStyle = abgrToCssRgba(color);
3973
+ ctx.strokeStyle = abgrToCssRgba2(color);
3953
3974
  ctx.lineWidth = items[0].thickness * 2 * t.dpr;
3954
3975
  ctx.beginPath();
3955
3976
  for (const e of items) {
@@ -3992,7 +4013,7 @@ var Canvas2DRenderer = class extends Canvas2DRenderingBackendBase {
3992
4013
  last: factor !== void 0
3993
4014
  };
3994
4015
  })) {
3995
- ctx.strokeStyle = abgrToCssRgba(color);
4016
+ ctx.strokeStyle = abgrToCssRgba2(color);
3996
4017
  ctx.lineWidth = items[0].thickness * 2 * t.dpr;
3997
4018
  ctx.beginPath();
3998
4019
  for (const s of items) {
@@ -4021,7 +4042,7 @@ var Canvas2DRenderer = class extends Canvas2DRenderingBackendBase {
4021
4042
  weight: 0,
4022
4043
  last: inRun(hl, i)
4023
4044
  }))) {
4024
- ctx.fillStyle = abgrToCssRgba(color);
4045
+ ctx.fillStyle = abgrToCssRgba2(color);
4025
4046
  ctx.beginPath();
4026
4047
  for (const a of items) {
4027
4048
  const [tip, left, notch, right] = arrowheadOutline(a, t);
@@ -4165,12 +4186,14 @@ function encodingSwatchCss(encoding) {
4165
4186
  }
4166
4187
 
4167
4188
  // src/walkHighlight.ts
4168
- function referenceOf(graph) {
4169
- return graph.referencePath ? graph.paths?.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
4189
+ var isReference = (graph, name) => !!graph.referencePath && pathOrigin(name).name === graph.referencePath;
4190
+ function piecesOf(graph, name) {
4191
+ return (graph.paths ?? []).filter((p) => p.name === name).sort((a, b) => (a.start ?? 0) - (b.start ?? 0));
4170
4192
  }
4171
4193
  function walkHighlight(graph, name) {
4172
- const path = graph.paths?.find((p) => p.name === name);
4173
- if (!path) {
4194
+ const pieces = piecesOf(graph, name);
4195
+ const first = pieces[0];
4196
+ if (!first) {
4174
4197
  return void 0;
4175
4198
  }
4176
4199
  const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
@@ -4178,43 +4201,51 @@ function walkHighlight(graph, name) {
4178
4201
  graph.edges.forEach((e, i) => {
4179
4202
  edgeAt.set(`${e.from}>${e.to}`, i);
4180
4203
  });
4181
- const edgeIndexes = /* @__PURE__ */ new Set();
4182
- for (let i = 1; i < path.nodeIds.length; i++) {
4183
- const a = path.nodeIds[i - 1];
4184
- const b = path.nodeIds[i];
4185
- const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
4186
- if (index !== void 0) {
4187
- edgeIndexes.add(index);
4188
- }
4189
- }
4190
4204
  const lengthOf = (id) => byId.get(id)?.length ?? 0;
4191
- const bp = path.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0);
4205
+ const bpOf = (path) => path.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0);
4206
+ const bp = pieces.reduce((sum, piece) => sum + bpOf(piece), 0);
4207
+ const range = first.start === void 0 ? void 0 : {
4208
+ contig: first.contig ?? panSNContig(pathOrigin(name).name),
4209
+ start: first.start,
4210
+ end: Math.max(...pieces.map((p) => (p.start ?? 0) + bpOf(p)))
4211
+ };
4212
+ const span = range ? range.end - range.start : bp;
4213
+ const nodeIds = /* @__PURE__ */ new Set();
4214
+ const edgeIndexes = /* @__PURE__ */ new Set();
4192
4215
  const progress = /* @__PURE__ */ new Map();
4193
4216
  let before = 0;
4194
- for (const id of path.nodeIds) {
4195
- const length = lengthOf(id);
4196
- if (!progress.has(id)) {
4197
- progress.set(id, bp > 0 ? (before + length / 2) / bp : 0);
4198
- }
4199
- before += length;
4217
+ for (const piece of pieces) {
4218
+ if (range) {
4219
+ before = Math.max(before, (piece.start ?? 0) - range.start);
4220
+ }
4221
+ piece.nodeIds.forEach((id, i) => {
4222
+ nodeIds.add(id);
4223
+ if (i > 0) {
4224
+ const a = piece.nodeIds[i - 1];
4225
+ const index = edgeAt.get(`${a}>${id}`) ?? edgeAt.get(`${id}>${a}`);
4226
+ if (index !== void 0) {
4227
+ edgeIndexes.add(index);
4228
+ }
4229
+ }
4230
+ const length = lengthOf(id);
4231
+ if (!progress.has(id)) {
4232
+ progress.set(id, span > 0 ? (before + length / 2) / span : 0);
4233
+ }
4234
+ before += length;
4235
+ });
4200
4236
  }
4201
- const reference = referenceOf(graph);
4202
- const originName = pathOrigin(name).name;
4203
- const origin = graph.anchorPaths?.find((a) => a.name === originName && a.length === bp) ?? graph.anchorPaths?.find((a) => a.name === originName);
4237
+ const reference = isReference(graph, name);
4238
+ const referencePieces2 = reference ? [] : (graph.paths ?? []).filter((p) => isReference(graph, p.name));
4204
4239
  return {
4205
4240
  name,
4206
- reference: reference === path,
4207
- range: origin ? {
4208
- contig: path.contig ?? panSNContig(origin.name),
4209
- start: origin.start,
4210
- end: origin.start + origin.length
4211
- } : void 0,
4212
- nodeIds: new Set(path.nodeIds),
4241
+ reference,
4242
+ range,
4243
+ nodeIds,
4213
4244
  edgeIndexes,
4214
4245
  progress,
4215
- steps: path.nodeIds.length,
4246
+ steps: pieces.reduce((sum, piece) => sum + piece.nodeIds.length, 0),
4216
4247
  bp,
4217
- referenceBp: reference && reference !== path ? reference.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0) : void 0
4248
+ referenceBp: referencePieces2.length > 0 ? referencePieces2.reduce((sum, piece) => sum + bpOf(piece), 0) : void 0
4218
4249
  };
4219
4250
  }
4220
4251
  function laneColors(graph, walk, encoding, ramp) {
@@ -4229,14 +4260,13 @@ function laneColors(graph, walk, encoding, ramp) {
4229
4260
  return colors;
4230
4261
  }
4231
4262
  function reversedNodes(graph, walk) {
4232
- const reference = referenceOf(graph);
4233
4263
  const reversed = /* @__PURE__ */ new Set();
4234
4264
  let bp = 0;
4235
- if (!reference || walk.reference) {
4265
+ const referencePath = graph.referencePath;
4266
+ if (!referencePath || walk.reference) {
4236
4267
  return { reversed, bp };
4237
4268
  }
4238
4269
  const walkPath = pathOrigin(walk.name).name;
4239
- const referencePath = pathOrigin(reference.name).name;
4240
4270
  const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
4241
4271
  for (const id of walk.progress.keys()) {
4242
4272
  const node = byId.get(id);
@@ -4333,7 +4363,9 @@ function walkPosition(walk, nodeId, length) {
4333
4363
  if (progress === void 0 || !range) {
4334
4364
  return void 0;
4335
4365
  }
4336
- const start = Math.round(range.start + progress * walk.bp - length / 2);
4366
+ const start = Math.round(
4367
+ range.start + progress * (range.end - range.start) - length / 2
4368
+ );
4337
4369
  return `${range.contig}:${groupDigits(start)}-${groupDigits(start + length)}`;
4338
4370
  }
4339
4371
 
@@ -4386,11 +4418,26 @@ function facetGrid({
4386
4418
  }
4387
4419
  return best;
4388
4420
  }
4389
- function facetCells(names, by) {
4421
+ function facetSettingOf(facet) {
4422
+ const raw = typeof facet === "string" ? { field: facet } : facet ?? {};
4423
+ const field = raw.field === "walk" || raw.field === "sample" ? raw.field : "";
4424
+ return {
4425
+ field,
4426
+ domain: [...raw.domain ?? []],
4427
+ ...raw.columns === void 0 ? {} : { columns: raw.columns }
4428
+ };
4429
+ }
4430
+ function domainFirst(names, domain) {
4431
+ const rank = new Map(domain.map((name, i) => [name, i]));
4432
+ const listed = (name) => rank.get(name) ?? domain.length;
4433
+ return names.map((name, i) => ({ name, i })).sort((a, b) => listed(a.name) - listed(b.name) || a.i - b.i).map(({ name }) => name);
4434
+ }
4435
+ function facetCells(names, by, domain = []) {
4436
+ const wrapOrder = domainFirst(names, domain);
4390
4437
  const wrap = {
4391
4438
  columns: void 0,
4392
4439
  count: names.length,
4393
- cells: names.map((_, i) => i)
4440
+ cells: names.map((name) => wrapOrder.indexOf(name))
4394
4441
  };
4395
4442
  if (by === "walk") {
4396
4443
  return wrap;
@@ -4400,7 +4447,10 @@ function facetCells(names, by) {
4400
4447
  const n2 = Number(haplotype);
4401
4448
  return contig !== void 0 && Number.isInteger(n2) && n2 >= 0 ? { sample, column: Math.max(0, n2 - 1) } : void 0;
4402
4449
  });
4403
- const rows = [...new Set(places.map((p) => p?.sample))];
4450
+ const rows = domainFirst(
4451
+ [...new Set(places.flatMap((p) => p ? [p.sample] : []))],
4452
+ domain
4453
+ );
4404
4454
  const columns = Math.max(1, ...places.map((p) => p ? p.column + 1 : 1));
4405
4455
  const cells = places.map(
4406
4456
  (p) => p ? rows.indexOf(p.sample) * columns + p.column : -1
@@ -4738,7 +4788,7 @@ function svgCanvas(width, height) {
4738
4788
  }
4739
4789
 
4740
4790
  // src/version.ts
4741
- var version = "4.0.27";
4791
+ var version = "4.0.29";
4742
4792
 
4743
4793
  // src/viewport.ts
4744
4794
  function axisScaleOf(scale, pixelRows) {
@@ -4845,8 +4895,8 @@ function figureSvg(graph, layout, o = {}) {
4845
4895
  const layers = o.walks ?? [];
4846
4896
  const walkRamp = layers.some((l) => l.color?.field === "reference") ? referenceRamp ?? computeReferenceRamp(graph, rampDomain) : void 0;
4847
4897
  const lift = layers.length > 0 && !layout.tubeMap ? walkLift(graph, layers, walkRamp) : void 0;
4848
- const by = o.facet ?? "none";
4849
- const panels = by !== "none" && lift && lift.walks.length > 1 ? facetLifts(graph, lift, layers, walkRamp) : void 0;
4898
+ const facet = facetSettingOf(o.facet);
4899
+ const panels = facet.field && lift && lift.walks.length > 1 ? facetLifts(graph, lift, layers, walkRamp) : void 0;
4850
4900
  const labels = new Map(
4851
4901
  (graph.paths?.length ? pathLegend(graph.paths) : []).map((p) => [
4852
4902
  p.name,
@@ -4963,7 +5013,8 @@ function figureSvg(graph, layout, o = {}) {
4963
5013
  if (panels) {
4964
5014
  const place = facetCells(
4965
5015
  panels.map((p) => p.walks[0].name),
4966
- by
5016
+ facet.field || "walk",
5017
+ facet.domain
4967
5018
  );
4968
5019
  const grid = facetGrid({
4969
5020
  count: place.count,
@@ -4971,7 +5022,7 @@ function figureSvg(graph, layout, o = {}) {
4971
5022
  pixelRows,
4972
5023
  width,
4973
5024
  room,
4974
- columns: place.columns ?? o.columns
5025
+ columns: place.columns ?? facet.columns
4975
5026
  });
4976
5027
  panels.forEach((panel, i) => {
4977
5028
  const cell = place.cells[i];
@@ -5340,6 +5391,135 @@ function featuresOnBackbone(features, backbone, aliases) {
5340
5391
  });
5341
5392
  }
5342
5393
 
5394
+ // src/gbzJoin.ts
5395
+ var UNKNOWN_SAMPLE = "unknown";
5396
+ function parseWalk(line) {
5397
+ const fields = line.split(" ");
5398
+ return {
5399
+ fields,
5400
+ name: fields.slice(1, 4).join(" "),
5401
+ start: +fields[4],
5402
+ end: +fields[5]
5403
+ };
5404
+ }
5405
+ var stepsOf = (walk) => walk.fields[6].match(/[<>][^<>]+/g) ?? [];
5406
+ function segmentLengths(lines) {
5407
+ const lengths = /* @__PURE__ */ new Map();
5408
+ for (const line of lines) {
5409
+ if (line.startsWith("S ")) {
5410
+ const [, id, sequence, ...tags] = line.split(" ");
5411
+ const ln = tags.find((tag) => tag.startsWith("LN:i:"));
5412
+ lengths.set(
5413
+ id,
5414
+ sequence === "*" && ln ? +ln.slice("LN:i:".length) : sequence.length
5415
+ );
5416
+ }
5417
+ }
5418
+ return lengths;
5419
+ }
5420
+ function extend(a, b, lengths) {
5421
+ if (b.end <= a.end) {
5422
+ return a;
5423
+ }
5424
+ const steps = stepsOf(b);
5425
+ let pos = b.start;
5426
+ for (const [i, step] of steps.entries()) {
5427
+ if (pos === a.end) {
5428
+ const fields = [...a.fields];
5429
+ fields[5] = String(b.end);
5430
+ fields[6] = a.fields[6] + steps.slice(i).join("");
5431
+ return { ...a, fields, end: b.end };
5432
+ }
5433
+ const length = lengths.get(step.slice(1));
5434
+ if (pos > a.end || length === void 0) {
5435
+ return void 0;
5436
+ }
5437
+ pos += length;
5438
+ }
5439
+ return void 0;
5440
+ }
5441
+ function joinPieces(pieces, lengths, startsPath) {
5442
+ const joined = [];
5443
+ for (const piece of pieces.sort((a, b) => a.start - b.start)) {
5444
+ const last = joined.at(-1);
5445
+ const [, sample, haplotype, contig] = piece.fields;
5446
+ const joins = last !== void 0 && (piece.start < last.end || piece.start === last.end && !startsPath(sample, +haplotype, contig, piece.start));
5447
+ const extended = joins ? extend(last, piece, lengths) : void 0;
5448
+ if (extended) {
5449
+ joined[joined.length - 1] = extended;
5450
+ } else {
5451
+ joined.push(piece);
5452
+ }
5453
+ }
5454
+ return joined;
5455
+ }
5456
+ var flip2 = (sign) => sign === "+" ? "-" : "+";
5457
+ function* stepLinks(walk) {
5458
+ const steps = stepsOf(walk).map((step) => ({
5459
+ id: step.slice(1),
5460
+ sign: step.startsWith(">") ? "+" : "-"
5461
+ }));
5462
+ for (let i = 1; i < steps.length; i++) {
5463
+ const a = steps[i - 1];
5464
+ const b = steps[i];
5465
+ yield {
5466
+ forward: `L ${a.id} ${a.sign} ${b.id} ${b.sign} 0M`,
5467
+ reverse: `L ${b.id} ${flip2(b.sign)} ${a.id} ${flip2(a.sign)} 0M`
5468
+ };
5469
+ }
5470
+ }
5471
+ function joinCuts(gfas, startsPath) {
5472
+ if (gfas.length <= 1) {
5473
+ return gfas[0] ?? "";
5474
+ }
5475
+ const cuts = gfas.map((gfa) => gfa.split("\n").filter((line) => line !== ""));
5476
+ const ofType = (cut, type) => cut.filter((line) => line.startsWith(`${type} `));
5477
+ const lengths = segmentLengths(cuts.flat());
5478
+ let unknownOffset = 0;
5479
+ const walksByCut = cuts.map((cut) => {
5480
+ const walks2 = ofType(cut, "W").map(parseWalk);
5481
+ const offset = unknownOffset;
5482
+ for (const walk of walks2.filter((w) => w.fields[1] === UNKNOWN_SAMPLE)) {
5483
+ const haplotype = +walk.fields[2] + offset;
5484
+ walk.fields[2] = String(haplotype);
5485
+ walk.name = walk.fields.slice(1, 4).join(" ");
5486
+ unknownOffset = Math.max(unknownOffset, haplotype);
5487
+ }
5488
+ return walks2;
5489
+ });
5490
+ const byName = /* @__PURE__ */ new Map();
5491
+ for (const walk of [
5492
+ ...walksByCut.flatMap((walks2) => walks2.slice(0, 1)),
5493
+ ...walksByCut.flatMap((walks2) => walks2.slice(1))
5494
+ ]) {
5495
+ const pieces = byName.get(walk.name);
5496
+ if (pieces) {
5497
+ pieces.push(walk);
5498
+ } else {
5499
+ byName.set(walk.name, [walk]);
5500
+ }
5501
+ }
5502
+ const walks = [...byName.values()].flatMap(
5503
+ (pieces) => joinPieces(pieces, lengths, startsPath)
5504
+ );
5505
+ const links = new Set(cuts.flatMap((cut) => ofType(cut, "L")));
5506
+ for (const walk of walks) {
5507
+ for (const link of stepLinks(walk)) {
5508
+ if (!links.has(link.forward) && !links.has(link.reverse)) {
5509
+ links.add(link.forward);
5510
+ }
5511
+ }
5512
+ }
5513
+ const lines = /* @__PURE__ */ new Set([
5514
+ ...ofType(cuts[0], "H"),
5515
+ ...cuts.flatMap((cut) => ofType(cut, "S")),
5516
+ ...links,
5517
+ ...walks.map((walk) => walk.fields.join(" "))
5518
+ ]);
5519
+ return `${[...lines].join("\n")}
5520
+ `;
5521
+ }
5522
+
5343
5523
  // src/gbzWindow.ts
5344
5524
  var NoReferenceSampleError = class extends Error {
5345
5525
  constructor(anchor, referenceSamples) {
@@ -5396,25 +5576,63 @@ async function referencePathQuery(db, referenceSample2, refName) {
5396
5576
  haplotype: path.name.haplotype
5397
5577
  } : void 0;
5398
5578
  }
5399
- function nodeLimitError(error, limit, windowBp) {
5579
+ function nodeLimitError(error, limit, windowBp, walkedFrom = 0) {
5400
5580
  const isLimit = error instanceof Error && (error.name === "SubgraphLimitError" || /^Subgraph size limit of \d+ nodes exceeded/.test(error.message));
5401
5581
  if (!isLimit) {
5402
5582
  return void 0;
5403
5583
  } else {
5404
5584
  const walked = error.walkedBp;
5405
- const fits = typeof walked === "number" && walked > 0 ? Math.floor(walked * 0.8) : Math.floor(windowBp / 2);
5585
+ const fits = typeof walked === "number" && walked > 0 ? Math.floor((walkedFrom + walked) * 0.8) : Math.floor(windowBp / 2);
5406
5586
  return new NodeLimitError(limit, windowBp, Math.max(fits, 1));
5407
5587
  }
5408
5588
  }
5589
+ var GBZ_CUT_DEFAULTS = {
5590
+ context: 1e3,
5591
+ snarls: "contained",
5592
+ limit: 1e5
5593
+ };
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
+ }
5409
5604
  async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
5410
- const subgraph = query ? await db.getSubgraphForRange(query, start, end, {
5411
- ...opts,
5412
- haplotypes: "all",
5413
- ...keep === void 0 ? {} : { keep }
5414
- }).catch((error) => {
5415
- throw nodeLimitError(error, opts.limit, end - start) ?? error;
5416
- }) : void 0;
5417
- return subgraph ? subgraph.toGFA({ names: "resolved" }) : "";
5605
+ if (!query) {
5606
+ return "";
5607
+ }
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
+ );
5625
+ const paths = await db.paths();
5626
+ return joinCuts(
5627
+ await Promise.all(
5628
+ subgraphs.flatMap(
5629
+ (subgraph) => subgraph ? [subgraph.toGFA({ names: "resolved" })] : []
5630
+ )
5631
+ ),
5632
+ (sample, haplotype, contig, at) => paths.some(
5633
+ ({ name }) => name.sample === sample && name.haplotype === haplotype && name.contig === contig && name.fragment === at
5634
+ )
5635
+ );
5418
5636
  }
5419
5637
 
5420
5638
  // src/gbzCut.ts
@@ -5427,7 +5645,6 @@ function haplotypeIndexBeside(db) {
5427
5645
  const GBZ_DB = /\.gbz\.db$/i;
5428
5646
  return GBZ_DB.test(db) ? db.replace(GBZ_DB, ".haplotype-index.db") : void 0;
5429
5647
  }
5430
- var WALK_LIMIT = 1e5;
5431
5648
  function parseRegion(text) {
5432
5649
  const m = /^\s*([^:\s]+):([\d,]+)-([\d,]+)\s*$/.exec(text);
5433
5650
  if (!m) {
@@ -5466,9 +5683,9 @@ async function cutGbzRegion({ base, referenceSamples }, src, signal) {
5466
5683
  signal?.throwIfAborted();
5467
5684
  const wanted = src.haplotypes?.length ? src.haplotypes : void 0;
5468
5685
  const text = await cutWindowGFA(base, query, region.start, region.end, {
5469
- context: src.context ?? 1e3,
5470
- snarls: src.snarls ?? "contained",
5471
- limit: WALK_LIMIT,
5686
+ context: src.context ?? GBZ_CUT_DEFAULTS.context,
5687
+ snarls: src.snarls ?? GBZ_CUT_DEFAULTS.snarls,
5688
+ limit: GBZ_CUT_DEFAULTS.limit,
5472
5689
  signal,
5473
5690
  ...wanted ? { keep: (name) => haplotypeWanted(name, wanted) } : {}
5474
5691
  });
@@ -5528,10 +5745,8 @@ export {
5528
5745
  nodeWidthPx,
5529
5746
  maxNodeWidthPx,
5530
5747
  nodeInk,
5531
- fadeAbgr,
5532
5748
  baseEdgeCurves,
5533
- LIFT_BACKDROP_COLOR,
5534
- FADED_ALPHA,
5749
+ LIFT_BACKDROP_CSS,
5535
5750
  REFERENCE_RAMP_MAX_HUE,
5536
5751
  computeReferenceRamp,
5537
5752
  computeColorSchemeRange,
@@ -5554,6 +5769,7 @@ export {
5554
5769
  FACET_TITLE_PX,
5555
5770
  FACET_PAD_PX,
5556
5771
  facetGrid,
5772
+ facetSettingOf,
5557
5773
  facetCells,
5558
5774
  genePins,
5559
5775
  HALO_FACTOR,
@@ -17,7 +17,7 @@ import {
17
17
  openGbz,
18
18
  parseRegion,
19
19
  refNameBinding
20
- } from "../chunks/chunk-VNCGWA2N.js";
20
+ } from "../chunks/chunk-QN464POI.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
@@ -5,7 +5,6 @@ import {
5
5
  FACET_GAP_PX,
6
6
  FACET_PAD_PX,
7
7
  FACET_TITLE_PX,
8
- FADED_ALPHA,
9
8
  FIT_PADDING,
10
9
  FORCE_LAYOUT_LABEL,
11
10
  HALO_FACTOR,
@@ -15,7 +14,7 @@ import {
15
14
  LABEL_PX,
16
15
  LAYOUT_MODES,
17
16
  LEGEND_INSET_PX,
18
- LIFT_BACKDROP_COLOR,
17
+ LIFT_BACKDROP_CSS,
19
18
  NODE_WIDTHS,
20
19
  REFERENCE_RAMP_MAX_HUE,
21
20
  ROW_HEIGHT_PX,
@@ -43,7 +42,7 @@ import {
43
42
  facetCells,
44
43
  facetGrid,
45
44
  facetLifts,
46
- fadeAbgr,
45
+ facetSettingOf,
47
46
  featuresOnBackbone,
48
47
  figureSvg,
49
48
  fitTransform,
@@ -102,7 +101,7 @@ import {
102
101
  wellKnownSample,
103
102
  yToXOf,
104
103
  zoomAbout
105
- } from "./chunks/chunk-VNCGWA2N.js";
104
+ } from "./chunks/chunk-QN464POI.js";
106
105
 
107
106
  // src/index.ts
108
107
  import { getDpr } from "@jbrowse/render-core/canvas2dUtils";
@@ -478,7 +477,6 @@ var REFERENCE_STRIP_ZONE_PX = REFERENCE_STRIP_PX + 8;
478
477
  var OVERHANG_PX = 8;
479
478
  var OVERHANG_CAP_PX = OVERHANG_PX + 3;
480
479
  var MIN_STAGGER_PX = 12;
481
- var PALE = abgrToCssRgba(fadeAbgr(LIFT_BACKDROP_COLOR, FADED_ALPHA));
482
480
  function referenceStripBlocks(graph, {
483
481
  colorScheme,
484
482
  referenceRamp,
@@ -490,14 +488,14 @@ function referenceStripBlocks(graph, {
490
488
  if (isBackbone(node)) {
491
489
  const colors = walks ? walks.map((w) => {
492
490
  const lane = w.colors.get(node.id);
493
- return lane === void 0 ? PALE : abgrToCssRgba(lane);
491
+ return lane === void 0 ? LIFT_BACKDROP_CSS : abgrToCssRgba(lane);
494
492
  }) : [abgrToCssRgba(getNodeColor(node, index, colorScheme, range))];
495
493
  out.push({
496
494
  node: node.id,
497
495
  bp0: node.stable.start,
498
496
  bp1: node.stable.start + node.length,
499
497
  colors,
500
- faded: colors.includes(PALE)
498
+ faded: colors.includes(LIFT_BACKDROP_CSS)
501
499
  });
502
500
  }
503
501
  });
@@ -1718,6 +1716,7 @@ function bubbleSubgraph(graph, segmentIds) {
1718
1716
  const keep = new Set(segmentIds);
1719
1717
  const nodes = graph.nodes.filter((n) => keep.has(n.name));
1720
1718
  const ids = new Set(nodes.map((n) => n.id));
1719
+ const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
1721
1720
  const kept = /* @__PURE__ */ new Map();
1722
1721
  const paths = graph.paths?.flatMap((p) => {
1723
1722
  const first = p.nodeIds.findIndex((id) => ids.has(id));
@@ -1726,7 +1725,14 @@ function bubbleSubgraph(graph, segmentIds) {
1726
1725
  }
1727
1726
  const nodeIds = p.nodeIds.filter((id) => ids.has(id));
1728
1727
  kept.set(p.name, { first, nodeIds });
1729
- 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
+ ];
1730
1736
  });
1731
1737
  return {
1732
1738
  name: graph.name,
@@ -1793,6 +1799,7 @@ export {
1793
1799
  facetCells,
1794
1800
  facetGrid,
1795
1801
  facetLifts,
1802
+ facetSettingOf,
1796
1803
  featuresOnBackbone,
1797
1804
  figureSvg,
1798
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'];
@@ -24,4 +24,13 @@ export interface GbzWindowOptions {
24
24
  keep?: (name: PathName) => boolean;
25
25
  signal?: AbortSignal;
26
26
  }
27
+ export declare const GBZ_CUT_DEFAULTS: {
28
+ readonly context: 1000;
29
+ readonly snarls: "contained";
30
+ readonly limit: 100000;
31
+ };
32
+ export declare function referencePieces(db: GBZBase, query: PathQuery, start: number, end: number): Promise<{
33
+ start: number;
34
+ end: number;
35
+ }[]>;
27
36
  export declare function cutWindowGFA(db: GBZBase, query: PathQuery | undefined, start: number, end: number, { keep, ...opts }: GbzWindowOptions): Promise<string>;
@@ -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';
@@ -6,6 +6,7 @@ import type { RenderBatch } from './types.js';
6
6
  import type { AxisScale, BezierCurve } from '../util/geometry.js';
7
7
  export declare const LIFT_BACKDROP_COLOR: number;
8
8
  export declare const FADED_ALPHA = 0.18;
9
+ export declare const LIFT_BACKDROP_CSS: string;
9
10
  export interface BuildOptions {
10
11
  nodePositions: Record<string, NodeSegment[]>;
11
12
  graph: Graph;
@@ -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.27";
1
+ export declare const version = "4.0.29";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/bandage-core",
3
- "version": "4.0.27",
3
+ "version": "4.0.29",
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": {