@jbrowse/bandage-core 0.1.7 → 0.1.9

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.
package/dist/index.js CHANGED
@@ -296,6 +296,10 @@ var SEP = "#";
296
296
  function panSNSample(refName) {
297
297
  return refName.split(SEP)[0];
298
298
  }
299
+ var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
300
+ function isGenericSample(sample) {
301
+ return GENERIC_SAMPLES.has(sample);
302
+ }
299
303
  function panSNHaplotype(refName) {
300
304
  const parts = refName.split(SEP);
301
305
  return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : void 0;
@@ -692,8 +696,8 @@ function pointAt(line, arc, s) {
692
696
  if (i >= arc.length) {
693
697
  return { ...line.at(-1) };
694
698
  }
695
- const span = arc[i] - arc[i - 1];
696
- const t = span > 0 ? (s - arc[i - 1]) / span : 0;
699
+ const span2 = arc[i] - arc[i - 1];
700
+ const t = span2 > 0 ? (s - arc[i - 1]) / span2 : 0;
697
701
  const a = line[i - 1];
698
702
  const b = line[i];
699
703
  return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
@@ -838,9 +842,9 @@ function referenceMidpoints(graph) {
838
842
  const neighbors = buildNeighbors(graph);
839
843
  const midpoints = /* @__PURE__ */ new Map();
840
844
  for (const node of graph.nodes) {
841
- const span = nodeReferenceSpan({ nodeId: node.id, nodeById, neighbors });
842
- if (span) {
843
- midpoints.set(node.id, (span.start + span.end) / 2);
845
+ const span2 = nodeReferenceSpan({ nodeId: node.id, nodeById, neighbors });
846
+ if (span2) {
847
+ midpoints.set(node.id, (span2.start + span2.end) / 2);
844
848
  }
845
849
  }
846
850
  return midpoints;
@@ -1119,12 +1123,12 @@ function projectAlleles(graph) {
1119
1123
  for (const node of byId.values()) {
1120
1124
  if (node.stable.rank !== REFERENCE_RANK && !claimed.has(node.id)) {
1121
1125
  const { members, anchors } = collectRun(node.id, byId, adj, claimed);
1122
- const span = anchorSpan(anchors);
1123
- if (span) {
1124
- const { offsets, ordered } = runOffsets(members, span.entry, adj);
1126
+ const span2 = anchorSpan(anchors);
1127
+ if (span2) {
1128
+ const { offsets, ordered } = runOffsets(members, span2.entry, adj);
1125
1129
  alleles.push({
1126
- start: span.start,
1127
- refSpan: span.end - span.start,
1130
+ start: span2.start,
1131
+ refSpan: span2.end - span2.start,
1128
1132
  nodeIds: ordered.map((m) => m.id),
1129
1133
  nodeOffsets: ordered.map((m) => offsets.get(m.id)),
1130
1134
  pathLength: ordered.reduce(
@@ -1143,11 +1147,11 @@ var MIN_DELETION_BP = 1;
1143
1147
  var MIN_ALLELE_SPAN_FRACTION = 0.015;
1144
1148
  function placeOffReference({
1145
1149
  graph,
1146
- span,
1150
+ span: span2,
1147
1151
  rowY,
1148
1152
  positions
1149
1153
  }) {
1150
- const minSpan = span * MIN_ALLELE_SPAN_FRACTION;
1154
+ const minSpan = span2 * MIN_ALLELE_SPAN_FRACTION;
1151
1155
  const alleles = projectAlleles(graph);
1152
1156
  const byId = new Map(graph.nodes.map((n) => [n.id, n]));
1153
1157
  const alleleDeletions = [];
@@ -1212,12 +1216,12 @@ function anchoredLayout(graph, region) {
1212
1216
  if (backbone.length === 0) {
1213
1217
  return void 0;
1214
1218
  }
1215
- const span = referenceSpan(backbone, region);
1219
+ const span2 = referenceSpan(backbone, region);
1216
1220
  const rows = rankRows(graph);
1217
1221
  const nodePositions = backbonePositions(backbone);
1218
1222
  const alleleDeletions = placeOffReference({
1219
1223
  graph,
1220
- span,
1224
+ span: span2,
1221
1225
  rowY: (node) => (node.stable ? rows.get(node.stable.rank) ?? rows.size : rows.size) * ROW_HEIGHT_PX,
1222
1226
  positions: nodePositions
1223
1227
  });
@@ -1539,13 +1543,13 @@ function sampleRowLayout(graph, region, keep = []) {
1539
1543
  if (backbone.length === 0 || samples.length === 0) {
1540
1544
  return void 0;
1541
1545
  }
1542
- const span = referenceSpan(backbone, region);
1546
+ const span2 = referenceSpan(backbone, region);
1543
1547
  const rowOf = new Map(samples.map((s, i) => [s, i + 1]));
1544
1548
  const nodePositions = backbonePositions(backbone);
1545
1549
  const spareRow = samples.length + 1;
1546
1550
  const alleleDeletions = placeOffReference({
1547
1551
  graph,
1548
- span,
1552
+ span: span2,
1549
1553
  rowY: (node) => {
1550
1554
  const stable = node.stable;
1551
1555
  const row = stable ? rowOf.get(parsePanSN(stable.refName).sample) : void 0;
@@ -1594,9 +1598,18 @@ function pathColorsLegible(pathCount) {
1594
1598
  function pathCssColor(index, count) {
1595
1599
  return `hsl(${pathHueAt(index, count)}, ${PATH_SATURATION * 100}%, ${PATH_LIGHTNESS * 100}%)`;
1596
1600
  }
1601
+ function pathGreyCssColor(index, count) {
1602
+ const lightness = count > 1 ? 25 + 45 * index / (count - 1) : 45;
1603
+ return `hsl(0, 0%, ${lightness}%)`;
1604
+ }
1597
1605
  function labelTiers(name) {
1598
1606
  const parts = pathOrigin(name).name.split("#");
1599
- return [parts[0], parts.slice(0, 2).join("#"), parts.join("#")];
1607
+ const full = parts.join("#");
1608
+ if (parts.length >= 3 && isGenericSample(parts[0])) {
1609
+ const contig = parts.slice(2).join("#");
1610
+ return [contig, contig, full];
1611
+ }
1612
+ return [parts[0], parts.slice(0, 2).join("#"), full];
1600
1613
  }
1601
1614
  function groupDigits(n) {
1602
1615
  return String(n).replace(/\B(?=(\d{3})+$)/g, ",");
@@ -1605,7 +1618,7 @@ function offsetTiers(name, tiers) {
1605
1618
  const { start } = pathOrigin(name);
1606
1619
  return tiers.map((t) => `${t} @${groupDigits(start)}`);
1607
1620
  }
1608
- function pathLegend(paths) {
1621
+ function pathLegend(paths, colors) {
1609
1622
  const named = paths.map((p) => labelTiers(p.name));
1610
1623
  const candidates = [0, 1, 2].flatMap((t) => [
1611
1624
  paths.map((_, i) => named[i][t]),
@@ -1618,7 +1631,7 @@ function pathLegend(paths) {
1618
1631
  return paths.map((path, i) => ({
1619
1632
  name: path.name,
1620
1633
  label: labels[i],
1621
- color: pathCssColor(i, paths.length)
1634
+ color: colors?.[i] ?? pathCssColor(i, paths.length)
1622
1635
  }));
1623
1636
  }
1624
1637
 
@@ -1836,11 +1849,14 @@ function runTubeMap(graph) {
1836
1849
  const paths = graph.paths ?? [];
1837
1850
  const tracks = tubeMapTracks(graph);
1838
1851
  const reads = graph.reads ? tubeMapReads(graph, graph.reads, paths.length) : [];
1839
- return layoutTubeMap(tubeMapNodes(graph), tracks, reads, {
1852
+ const tubeColor = reads.length > 0 ? pathGreyCssColor : pathCssColor;
1853
+ const pathColors = paths.map((_, i) => tubeColor(i, paths.length));
1854
+ const layout = layoutTubeMap(tubeMapNodes(graph), tracks, reads, {
1840
1855
  nodeWidthOption: "compressed",
1841
1856
  trackWidth: tubeWidth(tracks.length),
1842
- trackColor: (track) => track.type === "read" ? readColor(track) : pathCssColor(track.id, paths.length)
1857
+ trackColor: (track) => track.type === "read" ? readColor(track) : pathColors[track.id]
1843
1858
  });
1859
+ return layout ? { layout, pathColors } : void 0;
1844
1860
  }
1845
1861
  function drawnNodes(layout) {
1846
1862
  const nodes = [];
@@ -1865,10 +1881,11 @@ function extentOf(layout, yOffset) {
1865
1881
  };
1866
1882
  }
1867
1883
  function tubeMapLayout(graph) {
1868
- const layout = hasTubeMapPaths(graph) ? runTubeMap(graph) : void 0;
1869
- if (!layout) {
1884
+ const run = hasTubeMapPaths(graph) ? runTubeMap(graph) : void 0;
1885
+ if (!run) {
1870
1886
  return void 0;
1871
1887
  }
1888
+ const { layout, pathColors } = run;
1872
1889
  const yOffset = layout.bounds.minY;
1873
1890
  const nodePositions = {};
1874
1891
  for (const node of drawnNodes(layout)) {
@@ -1882,7 +1899,7 @@ function tubeMapLayout(graph) {
1882
1899
  const { minY, maxY } = extentOf(layout, yOffset);
1883
1900
  return {
1884
1901
  nodePositions,
1885
- tubeMap: { layout, yOffset },
1902
+ tubeMap: { layout, yOffset, pathColors },
1886
1903
  extent: {
1887
1904
  minX: layout.bounds.minX,
1888
1905
  maxX: layout.bounds.maxX,
@@ -1943,10 +1960,11 @@ function pathsReachBackbone(graph) {
1943
1960
  return graph.nodes.some(isBackbone) && hasTubeMapPaths(graph);
1944
1961
  }
1945
1962
  function tubeMapReferenceLayout(graph) {
1946
- const layout = pathsReachBackbone(graph) ? runTubeMap(graph) : void 0;
1947
- if (!layout) {
1963
+ const run = pathsReachBackbone(graph) ? runTubeMap(graph) : void 0;
1964
+ if (!run) {
1948
1965
  return void 0;
1949
1966
  }
1967
+ const { layout, pathColors } = run;
1950
1968
  const columns = tubeMapColumns(graph, layout);
1951
1969
  const byOrder = new Map(columns.map((c) => [c.order, c]));
1952
1970
  const yOffset = layout.bounds.minY;
@@ -1957,7 +1975,7 @@ function tubeMapReferenceLayout(graph) {
1957
1975
  }
1958
1976
  return {
1959
1977
  nodePositions,
1960
- tubeMap: { layout, yOffset, columns },
1978
+ tubeMap: { layout, yOffset, columns, pathColors },
1961
1979
  referenceAxis: true,
1962
1980
  pixelRows: true,
1963
1981
  extent: extentOf(layout, yOffset)
@@ -1984,7 +2002,7 @@ function sampleOf(path) {
1984
2002
  function labelOf(path) {
1985
2003
  return path.haplotype !== void 0 && path.sample !== void 0 ? `${path.sample}#${path.haplotype}` : sampleOf(path);
1986
2004
  }
1987
- function sliceBetween(nodeIds, span, region, flanked = true) {
2005
+ function sliceBetween(nodeIds, span2, region, flanked = true) {
1988
2006
  if (!region || !flanked) {
1989
2007
  return { ids: nodeIds, complete: true };
1990
2008
  }
@@ -1993,7 +2011,7 @@ function sliceBetween(nodeIds, span, region, flanked = true) {
1993
2011
  let bestEnd = -Infinity;
1994
2012
  let bestStart = Infinity;
1995
2013
  nodeIds.forEach((id, i) => {
1996
- const s = span.get(id);
2014
+ const s = span2.get(id);
1997
2015
  if (s) {
1998
2016
  if (s.end <= region.start && s.end > bestEnd) {
1999
2017
  bestEnd = s.end;
@@ -2020,25 +2038,25 @@ function walkRows(graph, region, unit) {
2020
2038
  const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
2021
2039
  const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
2022
2040
  const cut = region && region.end > region.start ? region : void 0;
2023
- const span = /* @__PURE__ */ new Map();
2041
+ const span2 = /* @__PURE__ */ new Map();
2024
2042
  let pos = referenceStart;
2025
2043
  for (const id of reference.nodeIds) {
2026
2044
  const len = lengthOf.get(id) ?? 0;
2027
- if (!span.has(id)) {
2028
- span.set(id, { start: pos, end: pos + len });
2045
+ if (!span2.has(id)) {
2046
+ span2.set(id, { start: pos, end: pos + len });
2029
2047
  }
2030
2048
  pos += len;
2031
2049
  }
2032
- const flanked = cut === void 0 || [...span.values()].some((s) => s.end <= cut.start) && [...span.values()].some((s) => s.start >= cut.end);
2050
+ const flanked = cut === void 0 || [...span2.values()].some((s) => s.end <= cut.start) && [...span2.values()].some((s) => s.start >= cut.end);
2033
2051
  const rowOf = (path) => {
2034
- const { ids, complete } = sliceBetween(path.nodeIds, span, cut, flanked);
2052
+ const { ids, complete } = sliceBetween(path.nodeIds, span2, cut, flanked);
2035
2053
  const runs = [];
2036
2054
  let bp = 0;
2037
2055
  let offReferenceBp = 0;
2038
2056
  let step = 0;
2039
2057
  for (const id of ids) {
2040
2058
  const len = lengthOf.get(id) ?? 0;
2041
- const s = span.get(id);
2059
+ const s = span2.get(id);
2042
2060
  const last = runs.at(-1);
2043
2061
  const at = last?.referenceStart;
2044
2062
  const forward2 = s && at !== void 0 && step >= 0 && at + last.bp === s.start;
@@ -2429,8 +2447,8 @@ function spanOf(segments) {
2429
2447
  }
2430
2448
  return { min, max };
2431
2449
  }
2432
- function distanceToSpan(x, span) {
2433
- return Math.max(0, span.min - x, x - span.max);
2450
+ function distanceToSpan(x, span2) {
2451
+ return Math.max(0, span2.min - x, x - span2.max);
2434
2452
  }
2435
2453
  function facingSide(segments, other, tieBreak) {
2436
2454
  const startGap = distanceToSpan(segments[0].x, other);
@@ -2465,10 +2483,10 @@ function bowAround(p1x, p1y, p2x, p2y, toward, maxBow) {
2465
2483
  minAlong = Math.min(minAlong, along);
2466
2484
  maxAlong = Math.max(maxAlong, along);
2467
2485
  }
2468
- const span = toward.length ? maxAlong - minAlong : 0;
2486
+ const span2 = toward.length ? maxAlong - minAlong : 0;
2469
2487
  const uncapped = Math.max(
2470
2488
  Math.abs(reach) * BOW_CLEARANCE / APEX_FRACTION,
2471
- span * ALONG_FRACTION
2489
+ span2 * ALONG_FRACTION
2472
2490
  );
2473
2491
  const size = maxBow === void 0 ? uncapped : Math.min(uncapped, maxBow);
2474
2492
  return reach < 0 ? -size : size;
@@ -4022,6 +4040,123 @@ function wheelZoomFactor(e) {
4022
4040
  return ZOOM_PER_NOTCH ** -Math.max(-MAX_NOTCHES_PER_EVENT, Math.min(MAX_NOTCHES_PER_EVENT, notches));
4023
4041
  }
4024
4042
 
4043
+ // src/referenceStrip.ts
4044
+ import { abgrToCssRgba as abgrToCssRgba2 } from "@jbrowse/core/util/colorBits";
4045
+ var REFERENCE_STRIP_PX = 10;
4046
+ var REFERENCE_STRIP_ZONE_PX = REFERENCE_STRIP_PX + 8;
4047
+ var OVERHANG_PX = 5;
4048
+ function referenceStripBlocks(graph, {
4049
+ colorScheme,
4050
+ referenceRamp,
4051
+ walkNodes
4052
+ }) {
4053
+ const range = { ...computeColorSchemeRange(graph), referenceRamp };
4054
+ const out = [];
4055
+ graph.nodes.forEach((node, index) => {
4056
+ if (isBackbone(node)) {
4057
+ const own = getNodeColor(node, index, colorScheme, range);
4058
+ out.push({
4059
+ node: node.id,
4060
+ bp0: node.stable.start,
4061
+ bp1: node.stable.start + node.length,
4062
+ color: abgrToCssRgba2(
4063
+ walkNodes && !walkNodes.has(node.id) ? fadeAbgr(own, FADED_ALPHA) : own
4064
+ )
4065
+ });
4066
+ }
4067
+ });
4068
+ return out.sort((a, b) => a.bp0 - b.bp0);
4069
+ }
4070
+ function span(frame, bp0, bp1) {
4071
+ const a = bp0 * frame.scale + frame.translateX;
4072
+ const b = bp1 * frame.scale + frame.translateX;
4073
+ return a < b ? [a, b] : [b, a];
4074
+ }
4075
+ var HIT_SLOP_PX = 2;
4076
+ function stripOverhang(blocks, frame, width) {
4077
+ const a = -frame.translateX / frame.scale;
4078
+ const b = (width - frame.translateX) / frame.scale;
4079
+ const lo = Math.min(a, b);
4080
+ const hi = Math.max(a, b);
4081
+ let below = 0;
4082
+ let above = 0;
4083
+ for (const block of blocks) {
4084
+ below += Math.max(0, Math.min(block.bp1, lo) - block.bp0);
4085
+ above += Math.max(0, block.bp1 - Math.max(block.bp0, hi));
4086
+ }
4087
+ return frame.scale >= 0 ? { left: Math.round(below), right: Math.round(above) } : { left: Math.round(above), right: Math.round(below) };
4088
+ }
4089
+ function stripBlockAt(blocks, frame, sx, sy) {
4090
+ if (sy < 0 || sy > REFERENCE_STRIP_PX + HIT_SLOP_PX) {
4091
+ return void 0;
4092
+ }
4093
+ let hit;
4094
+ let best = HIT_SLOP_PX;
4095
+ for (const b of blocks) {
4096
+ const [x0, x1] = span(frame, b.bp0, b.bp1);
4097
+ const dist = Math.max(0, x0 - sx, sx - x1);
4098
+ if (dist <= best) {
4099
+ best = dist;
4100
+ hit = b.node;
4101
+ }
4102
+ }
4103
+ return hit;
4104
+ }
4105
+ function drawOverhang(ctx, tipX, inward) {
4106
+ const mid = REFERENCE_STRIP_PX / 2;
4107
+ const baseX = tipX + inward * (OVERHANG_PX + 1);
4108
+ ctx.moveTo(tipX + inward, mid);
4109
+ ctx.lineTo(baseX, mid - OVERHANG_PX / 1.4);
4110
+ ctx.lineTo(baseX, mid + OVERHANG_PX / 1.4);
4111
+ ctx.closePath();
4112
+ }
4113
+ function drawReferenceStrip(ctx, blocks, frame, {
4114
+ width,
4115
+ lit,
4116
+ darkMode
4117
+ }) {
4118
+ const ink = darkMode ? "#ffffff" : "#18181c";
4119
+ ctx.fillStyle = darkMode ? "#1f1f1f" : "#ffffff";
4120
+ ctx.fillRect(0, 0, width, REFERENCE_STRIP_ZONE_PX);
4121
+ for (const b of blocks) {
4122
+ const [x0, x1] = span(frame, b.bp0, b.bp1);
4123
+ if (x1 >= 0 && x0 <= width) {
4124
+ ctx.fillStyle = b.color;
4125
+ ctx.fillRect(x0, 0, Math.max(x1 - x0, 1), REFERENCE_STRIP_PX);
4126
+ }
4127
+ }
4128
+ const overhang = stripOverhang(blocks, frame, width);
4129
+ if (overhang.left > 0 || overhang.right > 0) {
4130
+ ctx.beginPath();
4131
+ if (overhang.left > 0) {
4132
+ drawOverhang(ctx, 0, 1);
4133
+ }
4134
+ if (overhang.right > 0) {
4135
+ drawOverhang(ctx, width, -1);
4136
+ }
4137
+ ctx.fillStyle = ink;
4138
+ ctx.fill();
4139
+ ctx.strokeStyle = darkMode ? "#1f1f1f" : "#ffffff";
4140
+ ctx.lineWidth = 1;
4141
+ ctx.stroke();
4142
+ }
4143
+ if (lit) {
4144
+ const [x0, x1] = span(frame, lit.start, lit.end);
4145
+ const w = Math.max(x1 - x0, 3);
4146
+ const left = (x0 + x1) / 2 - w / 2;
4147
+ ctx.strokeStyle = ink;
4148
+ ctx.lineWidth = 1.5;
4149
+ ctx.strokeRect(left, 0.75, w, REFERENCE_STRIP_PX - 1.5);
4150
+ if (lit.anchor) {
4151
+ ctx.beginPath();
4152
+ ctx.moveTo(left + w / 2, REFERENCE_STRIP_PX);
4153
+ ctx.lineTo(lit.anchor.x, lit.anchor.y);
4154
+ ctx.lineWidth = 1;
4155
+ ctx.stroke();
4156
+ }
4157
+ }
4158
+ }
4159
+
4025
4160
  // src/tubeMap/draw.ts
4026
4161
  import { curvePaths, nodeOutlinePath } from "@gmod/tubemap-core";
4027
4162
 
@@ -4259,16 +4394,23 @@ function drawTubeMap(ctx, picture, frame) {
4259
4394
  }
4260
4395
  var MISMATCH_FONT_PX = 12;
4261
4396
  var MIN_MISMATCH_FONT_PX = 6;
4262
- function drawMismatches(ctx, marks, { x, y, yScale, width, darkMode }) {
4263
- const fontPx = MISMATCH_FONT_PX * yScale;
4264
- if (fontPx < MIN_MISMATCH_FONT_PX || marks.length === 0) {
4397
+ function mismatchesLegible(yScale) {
4398
+ return MISMATCH_FONT_PX * yScale >= MIN_MISMATCH_FONT_PX;
4399
+ }
4400
+ function mismatchOnScreen(m, { x, width }) {
4401
+ const [x0, x1] = m.kind === "insertion" ? [m.x, m.x] : [m.x0, m.x1];
4402
+ return x(x1) >= 0 && x(x0) <= width;
4403
+ }
4404
+ function drawMismatches(ctx, marks, frame) {
4405
+ const { x, y, yScale, darkMode } = frame;
4406
+ if (!mismatchesLegible(yScale) || marks.length === 0) {
4265
4407
  return;
4266
4408
  }
4267
- const onScreen = (x0, x1) => x(x1) >= 0 && x(x0) <= width;
4409
+ const fontPx = MISMATCH_FONT_PX * yScale;
4268
4410
  ctx.fillStyle = "grey";
4269
4411
  ctx.beginPath();
4270
4412
  for (const m of marks) {
4271
- if (m.kind === "deletion" && onScreen(m.x0, m.x1)) {
4413
+ if (m.kind === "deletion" && mismatchOnScreen(m, frame)) {
4272
4414
  const left = x(m.x0);
4273
4415
  const top = y(m.y);
4274
4416
  ctx.rect(left, top, x(m.x1) - left, y(m.y + m.height) - top);
@@ -4279,9 +4421,12 @@ function drawMismatches(ctx, marks, { x, y, yScale, width, darkMode }) {
4279
4421
  ctx.textBaseline = "alphabetic";
4280
4422
  ctx.fillStyle = darkMode ? "#ffffff" : "#000000";
4281
4423
  for (const m of marks) {
4282
- if (m.kind === "substitution" && onScreen(m.x0, m.x1)) {
4424
+ if (!mismatchOnScreen(m, frame)) {
4425
+ continue;
4426
+ }
4427
+ if (m.kind === "substitution") {
4283
4428
  ctx.fillText(m.seq, x(m.x0) + 1, y(m.y + m.height));
4284
- } else if (m.kind === "insertion" && onScreen(m.x, m.x)) {
4429
+ } else if (m.kind === "insertion") {
4285
4430
  ctx.fillText("*", x(m.x) - 3, y(m.y + m.height));
4286
4431
  }
4287
4432
  }
@@ -4352,8 +4497,8 @@ function warpX(knots, tx) {
4352
4497
  }
4353
4498
  const a = knots[lo];
4354
4499
  const b = knots[hi];
4355
- const span = b.tx - a.tx;
4356
- return span > 0 ? a.sx + (tx - a.tx) / span * (b.sx - a.sx) : a.sx;
4500
+ const span2 = b.tx - a.tx;
4501
+ return span2 > 0 ? a.sx + (tx - a.tx) / span2 * (b.sx - a.sx) : a.sx;
4357
4502
  }
4358
4503
 
4359
4504
  // src/tubeMap/frame.ts
@@ -4746,8 +4891,8 @@ function tubeX(boxes, bp, isEnd = false) {
4746
4891
  if (bp <= box.bp0) {
4747
4892
  return box.x0;
4748
4893
  }
4749
- const span = box.bp1 - box.bp0;
4750
- return span > 0 ? box.x0 + (Math.min(bp, box.bp1) - box.bp0) / span * (box.x1 - box.x0) : box.x0;
4894
+ const span2 = box.bp1 - box.bp0;
4895
+ return span2 > 0 ? box.x0 + (Math.min(bp, box.bp1) - box.bp0) / span2 * (box.x1 - box.x0) : box.x0;
4751
4896
  }
4752
4897
  function tubeSpan(boxes, start, end) {
4753
4898
  const a = tubeX(boxes, start);
@@ -4766,8 +4911,7 @@ function rulerBoxes(byRefName) {
4766
4911
  var TICK_PX = 5;
4767
4912
  var LABEL_GAP_PX2 = 10;
4768
4913
  var TARGET_TICK_PX = 110;
4769
- var ZIGZAG_PX = 4;
4770
- var ZIGZAG_AMPLITUDE_PX = 2.5;
4914
+ var MIN_BRACKET_PX = 3;
4771
4915
  function rulerInk(darkMode) {
4772
4916
  return darkMode ? "#b0b0b8" : "#55555c";
4773
4917
  }
@@ -4776,7 +4920,18 @@ function niceStep(raw) {
4776
4920
  const m = raw / pow;
4777
4921
  return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;
4778
4922
  }
4779
- function rulerMarks(boxes, x, width, measure) {
4923
+ function placeLabels2(candidates) {
4924
+ const placed = [];
4925
+ for (const a of candidates) {
4926
+ if (placed.every(
4927
+ (b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX2
4928
+ )) {
4929
+ placed.push(a);
4930
+ }
4931
+ }
4932
+ return placed.map(({ text, at }) => ({ text, at }));
4933
+ }
4934
+ function scaleMarks(boxes, x, width, measure) {
4780
4935
  const first = boxes[0];
4781
4936
  const last = boxes.at(-1);
4782
4937
  if (!first || !last) {
@@ -4789,31 +4944,9 @@ function rulerMarks(boxes, x, width, measure) {
4789
4944
  return void 0;
4790
4945
  }
4791
4946
  const step = niceStep(bpSpan / (to - from) * TARGET_TICK_PX);
4792
- const squeezedBoxes = boxes.filter((box) => {
4793
- const bp = box.bp1 - box.bp0;
4794
- return bp >= step && (x(box.x1) - x(box.x0)) * step / bp < TARGET_TICK_PX / 2;
4795
- });
4796
- const squeezed = [];
4797
- const lengths = [];
4798
- for (const box of squeezedBoxes) {
4799
- const x0 = x(box.x0);
4800
- const x1 = x(box.x1);
4801
- if (x1 >= 0 && x0 <= width) {
4802
- squeezed.push({ x0, x1 });
4803
- const text = formatBp(box.bp1 - box.bp0);
4804
- lengths.push({ text, at: (x0 + x1) / 2, width: measure(text) });
4805
- }
4806
- }
4807
4947
  const ticks = [];
4808
4948
  const positions = [];
4809
- let k = 0;
4810
4949
  for (let bp = Math.ceil(first.bp0 / step) * step; bp <= last.bp1; bp += step) {
4811
- while (k < squeezedBoxes.length && squeezedBoxes[k].bp1 <= bp) {
4812
- k++;
4813
- }
4814
- if (k < squeezedBoxes.length && squeezedBoxes[k].bp0 < bp) {
4815
- continue;
4816
- }
4817
4950
  const sx = Math.round(x(tubeX(boxes, bp))) + 0.5;
4818
4951
  if (sx >= 0 && sx <= width) {
4819
4952
  ticks.push(sx);
@@ -4821,35 +4954,66 @@ function rulerMarks(boxes, x, width, measure) {
4821
4954
  positions.push({ text, at: sx, width: measure(text) });
4822
4955
  }
4823
4956
  }
4824
- const placed = [];
4825
- const clear = (a) => placed.every(
4826
- (b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX2
4827
- );
4828
- for (const label of [...positions, ...lengths]) {
4829
- if (clear(label)) {
4830
- placed.push(label);
4957
+ return {
4958
+ spans: [{ x0: from, x1: to }],
4959
+ ticks,
4960
+ labels: placeLabels2(positions)
4961
+ };
4962
+ }
4963
+ function boxMarks(boxes, x, width, measure) {
4964
+ const shown = boxes.map((box) => ({ box, s0: x(box.x0), s1: x(box.x1) })).filter(({ s0, s1 }) => s1 >= 0 && s0 <= width);
4965
+ const first = shown[0];
4966
+ const last = shown.at(-1);
4967
+ if (!first || !last) {
4968
+ return void 0;
4969
+ }
4970
+ const label = (text, at) => ({
4971
+ text,
4972
+ at,
4973
+ width: measure(text)
4974
+ });
4975
+ const position = (bp, at) => label(bp.toLocaleString("en-US"), at);
4976
+ const spans = [];
4977
+ const ticks = [];
4978
+ const lengths = [];
4979
+ const boundaries = [];
4980
+ for (const { box, s0, s1 } of shown) {
4981
+ spans.push({ x0: s0, x1: s1 });
4982
+ ticks.push(Math.round(s0) + 0.5);
4983
+ if (s1 - s0 >= MIN_BRACKET_PX) {
4984
+ ticks.push(Math.round(s1) + 0.5);
4985
+ }
4986
+ const length = label(formatBp(box.bp1 - box.bp0), (s0 + s1) / 2);
4987
+ if (length.width <= s1 - s0) {
4988
+ lengths.push(length);
4989
+ }
4990
+ if (box !== first.box) {
4991
+ boundaries.push(position(box.bp0, s0));
4831
4992
  }
4832
4993
  }
4994
+ const ends = [
4995
+ position(first.box.bp0, first.s0),
4996
+ position(last.box.bp1, last.s1)
4997
+ ];
4833
4998
  return {
4834
- from,
4835
- to,
4999
+ spans,
4836
5000
  ticks,
4837
- squeezed,
4838
- labels: placed.map(({ text, at }) => ({ text, at }))
5001
+ labels: placeLabels2([...ends, ...lengths, ...boundaries])
4839
5002
  };
4840
5003
  }
4841
- function zigzag(ctx, x0, x1, y) {
4842
- const teeth = Math.max(1, Math.round((x1 - x0) / ZIGZAG_PX));
4843
- const dx = (x1 - x0) / teeth;
4844
- for (let i = 0; i < teeth; i++) {
4845
- ctx.lineTo(x0 + (i + 0.5) * dx, y + (i % 2 ? 1 : -1) * ZIGZAG_AMPLITUDE_PX);
4846
- }
4847
- ctx.lineTo(x1, y);
5004
+ function rulerMarks(boxes, x, width, measure, referenceAxis = false) {
5005
+ return (referenceAxis ? scaleMarks : boxMarks)(boxes, x, width, measure);
4848
5006
  }
4849
- function drawTubeMapRuler(ctx, boxes, frame, top) {
5007
+ function drawTubeMapRuler(ctx, boxes, frame, top, referenceAxis = false) {
4850
5008
  const { x, width, darkMode } = frame;
4851
5009
  ctx.font = "10px sans-serif";
4852
- const marks = rulerMarks(boxes, x, width, (text) => ctx.measureText(text).width);
5010
+ const marks = rulerMarks(
5011
+ boxes,
5012
+ x,
5013
+ width,
5014
+ (text) => ctx.measureText(text).width,
5015
+ referenceAxis
5016
+ );
4853
5017
  if (!marks) {
4854
5018
  return;
4855
5019
  }
@@ -4861,12 +5025,10 @@ function drawTubeMapRuler(ctx, boxes, frame, top) {
4861
5025
  ctx.textAlign = "center";
4862
5026
  ctx.textBaseline = "top";
4863
5027
  ctx.beginPath();
4864
- ctx.moveTo(Math.max(0, marks.from), y);
4865
- for (const { x0, x1 } of marks.squeezed) {
4866
- ctx.lineTo(x0, y);
4867
- zigzag(ctx, x0, x1, y);
5028
+ for (const { x0, x1 } of marks.spans) {
5029
+ ctx.moveTo(Math.max(0, x0), y);
5030
+ ctx.lineTo(Math.min(width, x1), y);
4868
5031
  }
4869
- ctx.lineTo(Math.min(width, marks.to), y);
4870
5032
  for (const sx of marks.ticks) {
4871
5033
  ctx.moveTo(sx, top);
4872
5034
  ctx.lineTo(sx, top + TICK_PX);
@@ -4978,14 +5140,14 @@ function tubeMapConnectors(nodes, bpToScreen, tubeToScreen, width) {
4978
5140
  }
4979
5141
  return out;
4980
5142
  }
4981
- var HIT_SLOP_PX = 3;
5143
+ var HIT_SLOP_PX2 = 3;
4982
5144
  function connectorAt(connectors, zoneBottom, sx, sy) {
4983
5145
  if (zoneBottom <= 0 || sy < 0 || sy > zoneBottom) {
4984
5146
  return void 0;
4985
5147
  }
4986
5148
  const t = sy / zoneBottom;
4987
5149
  let hit;
4988
- let best = HIT_SLOP_PX;
5150
+ let best = HIT_SLOP_PX2;
4989
5151
  for (const c of connectors) {
4990
5152
  const a = c.top0 + (c.bottom0 - c.top0) * t;
4991
5153
  const b = c.top1 + (c.bottom1 - c.top1) * t;
@@ -5220,10 +5382,10 @@ function walkRoutes(graph, byId, walkIndex, startId, endId) {
5220
5382
  // src/bubbles/classifyBubble.ts
5221
5383
  var REPEAT_GROWTH = 10;
5222
5384
  function coveringRepeat(b, repeats) {
5223
- const span = b.end - b.start;
5385
+ const span2 = b.end - b.start;
5224
5386
  const growth = b.longestAlleleLength - b.shortestAlleleLength;
5225
5387
  return repeats.find(
5226
- (r) => growth <= REPEAT_GROWTH * (r.end - r.start) && (span === 0 ? r.start <= b.start && b.start <= r.end : Math.min(b.end, r.end) - Math.max(b.start, r.start) >= span / 2)
5388
+ (r) => growth <= REPEAT_GROWTH * (r.end - r.start) && (span2 === 0 ? r.start <= b.start && b.start <= r.end : Math.min(b.end, r.end) - Math.max(b.start, r.start) >= span2 / 2)
5227
5389
  );
5228
5390
  }
5229
5391
  var SUPERBUBBLE_SEGMENTS = 40;
@@ -5461,12 +5623,16 @@ function layoutLabels(m) {
5461
5623
  const reserved = m.drawnRowLabels.map(
5462
5624
  ({ label, y }) => rowLabelBox(label, y * scaleY + translateY)
5463
5625
  );
5626
+ const stripPx = m.referenceStripZonePx ?? 0;
5627
+ if (stripPx > 0) {
5628
+ reserved.push({ x0: 0, x1: width, y0: 0, y1: stripPx });
5629
+ }
5464
5630
  if (m.legendSize.width > 0) {
5465
5631
  reserved.push({
5466
5632
  x0: width - LEGEND_INSET_PX - m.legendSize.width,
5467
5633
  x1: width,
5468
5634
  y0: 0,
5469
- y1: LEGEND_INSET_PX + m.legendSize.height
5635
+ y1: stripPx + LEGEND_INSET_PX + m.legendSize.height
5470
5636
  });
5471
5637
  }
5472
5638
  if (m.poppedFrom) {
@@ -5670,10 +5836,9 @@ var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
5670
5836
  function wellKnownSample(assemblyName) {
5671
5837
  return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
5672
5838
  }
5673
- var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
5674
5839
  function sampleOf2(refName) {
5675
5840
  const sample = refName.includes("#") ? panSNSample(refName) : void 0;
5676
- return sample === void 0 || GENERIC_SAMPLES.has(sample) ? void 0 : sample;
5841
+ return sample === void 0 || isGenericSample(sample) ? void 0 : sample;
5677
5842
  }
5678
5843
  function haplotypeOf(refName) {
5679
5844
  return panSNHaplotype(refName) ?? panSNSample(refName);
@@ -5720,10 +5885,10 @@ function graphBackbone(graph) {
5720
5885
  if (isBackbone(node)) {
5721
5886
  const { refName, start } = node.stable;
5722
5887
  const end = start + node.length;
5723
- const span = spans.get(refName);
5724
- if (span) {
5725
- span.start = Math.min(span.start, start);
5726
- span.end = Math.max(span.end, end);
5888
+ const span2 = spans.get(refName);
5889
+ if (span2) {
5890
+ span2.start = Math.min(span2.start, start);
5891
+ span2.end = Math.max(span2.end, end);
5727
5892
  } else {
5728
5893
  spans.set(refName, {
5729
5894
  refName,
@@ -5931,6 +6096,7 @@ export {
5931
6096
  LEGEND_INSET_PX,
5932
6097
  NODE_WIDTHS,
5933
6098
  REFERENCE_RAMP_MAX_HUE,
6099
+ REFERENCE_STRIP_ZONE_PX,
5934
6100
  ROW_HEIGHT_PX,
5935
6101
  WELL_KNOWN_SAMPLES,
5936
6102
  assemblyWalk,
@@ -5948,6 +6114,7 @@ export {
5948
6114
  contains,
5949
6115
  cutWindowGFA,
5950
6116
  deletionEdges,
6117
+ drawReferenceStrip,
5951
6118
  drawTubeMap,
5952
6119
  drawTubeMapConnectors,
5953
6120
  drawTubeMapGenes,
@@ -5987,10 +6154,12 @@ export {
5987
6154
  referenceNodes,
5988
6155
  referencePathQuery,
5989
6156
  referenceSamplesOf,
6157
+ referenceStripBlocks,
5990
6158
  resolveColorScheme,
5991
6159
  resolveReferenceSample,
5992
6160
  rulerBoxes,
5993
6161
  screenToLayout,
6162
+ stripBlockAt,
5994
6163
  tubeMapConnectors,
5995
6164
  tubeMapFrame,
5996
6165
  tubeMapGenes,
@@ -19,6 +19,7 @@ export interface BubbleRoute {
19
19
  walks: string[];
20
20
  }
21
21
  export declare function parseBubbleLine(line: string): MinigraphBubble;
22
+ export declare function sameBubble(a: MinigraphBubble, b: MinigraphBubble): boolean;
22
23
  export declare function bubbleLabel(bubble: MinigraphBubble): string;
23
24
  export declare const SATURATED_PATH_COUNT = 2147483647;
24
25
  export declare function bubbleDescription(bubble: MinigraphBubble): string;
@@ -16,6 +16,8 @@ export { findHoveredEdge, findHoveredNode } from './util/hitDetection.js';
16
16
  export type { NodeInk } from './util/hitDetection.js';
17
17
  export type { AxisScale } from './util/geometry.js';
18
18
  export { wheelZoomFactor } from './util/wheelZoom.js';
19
+ export { REFERENCE_STRIP_ZONE_PX, drawReferenceStrip, referenceStripBlocks, stripBlockAt, } from './referenceStrip.js';
20
+ export type { StripBlock } from './referenceStrip.js';
19
21
  export { drawTubeMap, tubeMapPicture } from './tubeMap/draw.js';
20
22
  export type { TubeMapFrame, TubeMapPicture } from './tubeMap/draw.js';
21
23
  export { referenceKnots, warpX } from './tubeMap/warp.js';
@@ -25,6 +25,7 @@ export interface LabelLayoutSource {
25
25
  width: number;
26
26
  height: number;
27
27
  };
28
+ referenceStripZonePx?: number;
28
29
  drawnRowLabels: {
29
30
  label: string;
30
31
  y: number;
@@ -10,6 +10,7 @@ export interface TubeMapColumn {
10
10
  }
11
11
  export interface TubeMapDrawing {
12
12
  layout: TubeMapLayout;
13
+ pathColors: string[];
13
14
  yOffset: number;
14
15
  columns?: TubeMapColumn[];
15
16
  coarse?: Coarsened;
@@ -1,5 +1,6 @@
1
1
  import type { BaseFeatureDataAdapter } from '@jbrowse/core/data_adapters/BaseAdapter';
2
2
  export declare function panSNSample(refName: string): string;
3
+ export declare function isGenericSample(sample: string): boolean;
3
4
  export declare function panSNHaplotype(refName: string): string | undefined;
4
5
  export declare function panSNContig(refName: string): string;
5
6
  export declare function panSNMatchesPrefix(refName: string, prefix: string | undefined): boolean;
@@ -6,9 +6,10 @@ export declare const PATH_LIGHTNESS = 0.5;
6
6
  export declare const MAX_PATH_COLORS = 16;
7
7
  export declare function pathColorsLegible(pathCount: number): boolean;
8
8
  export declare function pathCssColor(index: number, count: number): string;
9
+ export declare function pathGreyCssColor(index: number, count: number): string;
9
10
  export interface PathLegendEntry {
10
11
  name: string;
11
12
  label: string;
12
13
  color: string;
13
14
  }
14
- export declare function pathLegend(paths: GraphPath[]): PathLegendEntry[];
15
+ export declare function pathLegend(paths: GraphPath[], colors?: readonly string[]): PathLegendEntry[];
@@ -0,0 +1,45 @@
1
+ import type { ResolvedColorScheme } from './colorSchemes.js';
2
+ import type { ReferenceRamp } from './renderer/GeometryBuilder.js';
3
+ import type { Graph, NodeSegment } from './types.js';
4
+ export declare const REFERENCE_STRIP_PX = 10;
5
+ export declare const REFERENCE_STRIP_ZONE_PX: number;
6
+ export interface StripBlock {
7
+ node: string;
8
+ bp0: number;
9
+ bp1: number;
10
+ color: string;
11
+ }
12
+ export declare function referenceStripBlocks(graph: Graph, { colorScheme, referenceRamp, walkNodes, }: {
13
+ colorScheme: ResolvedColorScheme;
14
+ referenceRamp?: ReferenceRamp;
15
+ walkNodes?: ReadonlySet<string>;
16
+ }): StripBlock[];
17
+ export interface BpFrame {
18
+ scale: number;
19
+ translateX: number;
20
+ }
21
+ export declare function stripOverhang(blocks: readonly StripBlock[], frame: BpFrame, width: number): {
22
+ left: number;
23
+ right: number;
24
+ };
25
+ export declare function stripBlockAt(blocks: readonly StripBlock[], frame: BpFrame, sx: number, sy: number): string | undefined;
26
+ export declare function nodeAnchor(segments: readonly NodeSegment[] | undefined, toScreen: (p: NodeSegment) => {
27
+ x: number;
28
+ y: number;
29
+ }): {
30
+ x: number;
31
+ y: number;
32
+ } | undefined;
33
+ export interface StripLit {
34
+ start: number;
35
+ end: number;
36
+ anchor?: {
37
+ x: number;
38
+ y: number;
39
+ };
40
+ }
41
+ export declare function drawReferenceStrip(ctx: CanvasRenderingContext2D, blocks: readonly StripBlock[], frame: BpFrame, { width, lit, darkMode, }: {
42
+ width: number;
43
+ lit?: StripLit;
44
+ darkMode?: boolean;
45
+ }): void;
@@ -4,6 +4,7 @@ import type { Graph, GraphNode, NodeSegment } from '../types.js';
4
4
  import type { WalkHighlight } from '../walkHighlight.js';
5
5
  import type { RenderBatch } from './types.js';
6
6
  import type { AxisScale, BezierCurve } from '../util/geometry.js';
7
+ export declare const FADED_ALPHA = 0.18;
7
8
  export interface BuildOptions {
8
9
  nodePositions: Record<string, NodeSegment[]>;
9
10
  graph: Graph;
@@ -15,21 +15,17 @@ export declare function tubeSpan(boxes: readonly Box[], start: number, end: numb
15
15
  x1: number;
16
16
  };
17
17
  export declare function rulerBoxes(byRefName: ReferenceBoxes): Box[] | undefined;
18
- export declare const ZIGZAG_PX = 4;
19
- export declare const ZIGZAG_AMPLITUDE_PX = 2.5;
20
18
  export declare function rulerInk(darkMode?: boolean): "#b0b0b8" | "#55555c";
21
19
  export interface RulerMarks {
22
- from: number;
23
- to: number;
24
- ticks: number[];
25
- squeezed: {
20
+ spans: {
26
21
  x0: number;
27
22
  x1: number;
28
23
  }[];
24
+ ticks: number[];
29
25
  labels: {
30
26
  text: string;
31
27
  at: number;
32
28
  }[];
33
29
  }
34
- export declare function rulerMarks(boxes: readonly Box[], x: (tx: number) => number, width: number, measure: (text: string) => number): RulerMarks | undefined;
35
- export declare function drawTubeMapRuler(ctx: CanvasRenderingContext2D, boxes: readonly Box[], frame: TubeMapFrame, top: number): void;
30
+ export declare function rulerMarks(boxes: readonly Box[], x: (tx: number) => number, width: number, measure: (text: string) => number, referenceAxis?: boolean): RulerMarks | undefined;
31
+ export declare function drawTubeMapRuler(ctx: CanvasRenderingContext2D, boxes: readonly Box[], frame: TubeMapFrame, top: number, referenceAxis?: boolean): void;
@@ -64,4 +64,6 @@ export interface TubeMapFrame extends TubeMapTransform {
64
64
  darkMode?: boolean;
65
65
  }
66
66
  export declare function drawTubeMap(ctx: CanvasRenderingContext2D, picture: TubeMapPicture, frame: TubeMapFrame): void;
67
+ export declare function mismatchesLegible(yScale: number): boolean;
68
+ export declare function mismatchOnScreen(m: TubeMapMismatch, { x, width }: Pick<TubeMapFrame, 'x' | 'width'>): boolean;
67
69
  export {};
@@ -4,6 +4,8 @@ import type { InputTrack } from '@gmod/tubemap-core';
4
4
  export declare function canonicalStrand(node: GraphNode): "+" | "-";
5
5
  export declare function stepName(node: GraphNode, strand: '+' | '-'): string;
6
6
  export declare function tubeMapReads(graph: Graph, records: readonly GafRecord[], firstId: number): InputTrack[];
7
+ export declare const FORWARD_READ_COLORS: string[];
8
+ export declare const REVERSE_READ_COLORS: string[];
7
9
  export declare function readColor(read: {
8
10
  id: number;
9
11
  is_reverse?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/bandage-core",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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": {