@jbrowse/bandage-core 0.1.4 → 0.1.6
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/README.md +2 -0
- package/dist/index.js +1197 -667
- package/dist/types/index.d.ts +10 -0
- package/dist/types/layout/walkRows.d.ts +2 -0
- package/dist/types/pipeline.d.ts +4 -1
- package/dist/types/reference.d.ts +23 -0
- package/dist/types/renderer/GeometryBuilder.d.ts +1 -0
- package/dist/types/tubeMap/axis.d.ts +32 -0
- package/dist/types/tubeMap/connectors.d.ts +23 -0
- package/dist/types/tubeMap/draw.d.ts +1 -0
- package/dist/types/tubeMap/frame.d.ts +1 -1
- package/dist/types/tubeMap/genes.d.ts +14 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -538,7 +538,7 @@ function sidesOf(edge) {
|
|
|
538
538
|
return { from, to };
|
|
539
539
|
}
|
|
540
540
|
function mergeRuns(graph) {
|
|
541
|
-
const
|
|
541
|
+
const sides2 = graph.edges.map(sidesOf);
|
|
542
542
|
const at = /* @__PURE__ */ new Map();
|
|
543
543
|
const attach = (id, side, ei) => {
|
|
544
544
|
const key = `${id}|${side}`;
|
|
@@ -550,8 +550,8 @@ function mergeRuns(graph) {
|
|
|
550
550
|
}
|
|
551
551
|
};
|
|
552
552
|
graph.edges.forEach((edge, ei) => {
|
|
553
|
-
attach(edge.from,
|
|
554
|
-
attach(edge.to,
|
|
553
|
+
attach(edge.from, sides2[ei].from, ei);
|
|
554
|
+
attach(edge.to, sides2[ei].to, ei);
|
|
555
555
|
});
|
|
556
556
|
const joinAfter = (a) => {
|
|
557
557
|
const out = at.get(`${a}|end`);
|
|
@@ -560,7 +560,7 @@ function mergeRuns(graph) {
|
|
|
560
560
|
}
|
|
561
561
|
const ei = out[0];
|
|
562
562
|
const edge = graph.edges[ei];
|
|
563
|
-
return edge.from === a && edge.to !== a &&
|
|
563
|
+
return edge.from === a && edge.to !== a && sides2[ei].from === "end" && sides2[ei].to === "start" && at.get(`${edge.to}|start`)?.length === 1 ? ei : void 0;
|
|
564
564
|
};
|
|
565
565
|
const joined = /* @__PURE__ */ new Set();
|
|
566
566
|
const runOf = /* @__PURE__ */ new Map();
|
|
@@ -620,7 +620,7 @@ function mergeRuns(graph) {
|
|
|
620
620
|
graph.edges.forEach((edge, ei) => {
|
|
621
621
|
const from = runOf.get(edge.from);
|
|
622
622
|
const to = runOf.get(edge.to);
|
|
623
|
-
const side =
|
|
623
|
+
const side = sides2[ei];
|
|
624
624
|
const key = `${from}|${side.from}>${to}|${side.to}`;
|
|
625
625
|
if (joined.has(ei) || seen.has(key)) {
|
|
626
626
|
return;
|
|
@@ -660,6 +660,14 @@ function splitRuns(positions, runs, weightOf) {
|
|
|
660
660
|
}
|
|
661
661
|
return out;
|
|
662
662
|
}
|
|
663
|
+
function polylineSlice(line, from, to) {
|
|
664
|
+
if (line.length === 0) {
|
|
665
|
+
return [];
|
|
666
|
+
}
|
|
667
|
+
const arc = arcPrefix(line);
|
|
668
|
+
const total = arc.at(-1);
|
|
669
|
+
return slice(line, arc, from * total, to * total);
|
|
670
|
+
}
|
|
663
671
|
function polylineMidpoint(line) {
|
|
664
672
|
const arc = arcPrefix(line);
|
|
665
673
|
return pointAt(line, arc, arc.at(-1) / 2);
|
|
@@ -967,18 +975,21 @@ function fittedTranslateY(bounds, usableHeight, yScale) {
|
|
|
967
975
|
const leftoverY = usableHeight - bounds.h * yScale;
|
|
968
976
|
return FIT_PADDING - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
|
|
969
977
|
}
|
|
970
|
-
function fitTransform(bounds, paneWidth, paneHeight, pixelRows) {
|
|
971
|
-
const usableWidth = paneWidth -
|
|
978
|
+
function fitTransform(bounds, paneWidth, paneHeight, pixelRows, { minScale = 0, padLeft = FIT_PADDING } = {}) {
|
|
979
|
+
const usableWidth = paneWidth - padLeft - FIT_PADDING;
|
|
972
980
|
const usableHeight = paneHeight - FIT_PADDING * 2;
|
|
973
981
|
if (usableWidth <= 0 || usableHeight <= 0 || !(bounds.w > 0 || !pixelRows && bounds.h > 0)) {
|
|
974
982
|
return void 0;
|
|
975
983
|
}
|
|
976
984
|
const fitX = bounds.w > 0 ? usableWidth / bounds.w : Infinity;
|
|
977
985
|
const fitY = bounds.h > 0 ? usableHeight / bounds.h : Infinity;
|
|
978
|
-
const scale = clampZoom(
|
|
986
|
+
const scale = clampZoom(
|
|
987
|
+
Math.max(minScale, pixelRows ? fitX : Math.min(fitX, fitY))
|
|
988
|
+
);
|
|
989
|
+
const leftoverX = usableWidth - bounds.w * scale;
|
|
979
990
|
return {
|
|
980
991
|
scale,
|
|
981
|
-
translateX:
|
|
992
|
+
translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
|
|
982
993
|
translateY: fittedTranslateY(bounds, usableHeight, pixelRows ? 1 : scale)
|
|
983
994
|
};
|
|
984
995
|
}
|
|
@@ -2002,7 +2013,6 @@ function walkRows(graph, region, unit) {
|
|
|
2002
2013
|
return void 0;
|
|
2003
2014
|
}
|
|
2004
2015
|
const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
|
|
2005
|
-
const onReference = new Set(reference.nodeIds);
|
|
2006
2016
|
const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
|
|
2007
2017
|
const cut = region && region.end > region.start ? region : void 0;
|
|
2008
2018
|
const span = /* @__PURE__ */ new Map();
|
|
@@ -2020,17 +2030,34 @@ function walkRows(graph, region, unit) {
|
|
|
2020
2030
|
const runs = [];
|
|
2021
2031
|
let bp = 0;
|
|
2022
2032
|
let offReferenceBp = 0;
|
|
2033
|
+
let step = 0;
|
|
2023
2034
|
for (const id of ids) {
|
|
2024
2035
|
const len = lengthOf.get(id) ?? 0;
|
|
2025
|
-
const
|
|
2036
|
+
const s = span.get(id);
|
|
2026
2037
|
const last = runs.at(-1);
|
|
2027
|
-
|
|
2038
|
+
const at = last?.referenceStart;
|
|
2039
|
+
const forward2 = s && at !== void 0 && step >= 0 && at + last.bp === s.start;
|
|
2040
|
+
const backward = s && at !== void 0 && step <= 0 && s.end === at;
|
|
2041
|
+
if (!s && last && !last.onReference) {
|
|
2042
|
+
last.bp += len;
|
|
2043
|
+
} else if (forward2 || backward) {
|
|
2028
2044
|
last.bp += len;
|
|
2045
|
+
if (!forward2) {
|
|
2046
|
+
last.referenceStart = s.start;
|
|
2047
|
+
last.reversed = true;
|
|
2048
|
+
}
|
|
2049
|
+
step = forward2 ? 1 : -1;
|
|
2029
2050
|
} else {
|
|
2030
|
-
runs.push({
|
|
2051
|
+
runs.push({
|
|
2052
|
+
start: bp,
|
|
2053
|
+
bp: len,
|
|
2054
|
+
onReference: s !== void 0,
|
|
2055
|
+
referenceStart: s?.start
|
|
2056
|
+
});
|
|
2057
|
+
step = 0;
|
|
2031
2058
|
}
|
|
2032
2059
|
bp += len;
|
|
2033
|
-
if (!
|
|
2060
|
+
if (!s) {
|
|
2034
2061
|
offReferenceBp += len;
|
|
2035
2062
|
}
|
|
2036
2063
|
}
|
|
@@ -2566,9 +2593,9 @@ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY,
|
|
|
2566
2593
|
offsetY
|
|
2567
2594
|
);
|
|
2568
2595
|
}
|
|
2569
|
-
const
|
|
2570
|
-
const fromAttach = attachment(fromSegments,
|
|
2571
|
-
const toAttach = attachment(toSegments,
|
|
2596
|
+
const sides2 = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
|
|
2597
|
+
const fromAttach = attachment(fromSegments, sides2.from);
|
|
2598
|
+
const toAttach = attachment(toSegments, sides2.to);
|
|
2572
2599
|
const fromEnd = fromAttach.at;
|
|
2573
2600
|
const toStart = toAttach.at;
|
|
2574
2601
|
const p1x = fromEnd.x + offsetX;
|
|
@@ -3058,7 +3085,9 @@ function hashColor(str, alpha) {
|
|
|
3058
3085
|
var REFERENCE_RAMP_MAX_HUE = 300;
|
|
3059
3086
|
var REFERENCE_RAMP_SATURATION = 0.7;
|
|
3060
3087
|
var REFERENCE_RAMP_LIGHTNESS = 0.5;
|
|
3061
|
-
var
|
|
3088
|
+
var REFERENCE_RAMP_ALT_RGB = [60, 65, 72];
|
|
3089
|
+
var REFERENCE_RAMP_ALT_COLOR = packAbgr2(...REFERENCE_RAMP_ALT_RGB, 255);
|
|
3090
|
+
var REFERENCE_RAMP_ALT_CSS = `rgb(${REFERENCE_RAMP_ALT_RGB.join(", ")})`;
|
|
3062
3091
|
function computeReferenceRamp(graph, domain) {
|
|
3063
3092
|
const midpoints = referenceMidpoints(graph);
|
|
3064
3093
|
if (domain && domain.end > domain.start) {
|
|
@@ -4115,6 +4144,7 @@ function tubeMapPicture(layout) {
|
|
|
4115
4144
|
}
|
|
4116
4145
|
});
|
|
4117
4146
|
return {
|
|
4147
|
+
bounds: layout.bounds,
|
|
4118
4148
|
layers: [layerOf(layout, "haplotype"), layerOf(layout, "read")],
|
|
4119
4149
|
nodes,
|
|
4120
4150
|
mismatches: tubeMapMismatches(layout).filter(
|
|
@@ -4185,6 +4215,11 @@ function fillByColor(ctx, items, visible, addTo) {
|
|
|
4185
4215
|
}
|
|
4186
4216
|
ctx.globalAlpha = 1;
|
|
4187
4217
|
}
|
|
4218
|
+
var FULL_OUTLINE_PX = 12;
|
|
4219
|
+
var MIN_OUTLINE_ALPHA = 0.15;
|
|
4220
|
+
function outlineAlpha(screenWidth) {
|
|
4221
|
+
return Math.max(MIN_OUTLINE_ALPHA, Math.min(1, screenWidth / FULL_OUTLINE_PX));
|
|
4222
|
+
}
|
|
4188
4223
|
function drawTubeMap(ctx, picture, frame) {
|
|
4189
4224
|
const { x, y, width } = frame;
|
|
4190
4225
|
const visible = (item) => x(item.x1) >= 0 && x(item.x0) <= width;
|
|
@@ -4209,7 +4244,9 @@ function drawTubeMap(ctx, picture, frame) {
|
|
|
4209
4244
|
ctx.fillStyle = lit ? "rgba(255,192,203,0.5)" : fill;
|
|
4210
4245
|
ctx.strokeStyle = lit ? "#ff0000" : stroke;
|
|
4211
4246
|
ctx.fill();
|
|
4247
|
+
ctx.globalAlpha = lit ? 1 : outlineAlpha(x(node.x1) - x(node.x0));
|
|
4212
4248
|
ctx.stroke();
|
|
4249
|
+
ctx.globalAlpha = 1;
|
|
4213
4250
|
}
|
|
4214
4251
|
}
|
|
4215
4252
|
drawMismatches(ctx, picture.mismatches, frame);
|
|
@@ -4342,720 +4379,1122 @@ function tubeMapNodeAt(drawing, { x, y }, sx, sy) {
|
|
|
4342
4379
|
return hit;
|
|
4343
4380
|
}
|
|
4344
4381
|
|
|
4345
|
-
// src/
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4382
|
+
// src/overlayLabels.ts
|
|
4383
|
+
var LABEL_PX = 11;
|
|
4384
|
+
var LABEL_CHAR_PX = 6.2;
|
|
4385
|
+
var LABEL_PAD = 4;
|
|
4386
|
+
function labelWidth(text) {
|
|
4387
|
+
return text.length * LABEL_CHAR_PX + LABEL_PAD * 2;
|
|
4388
|
+
}
|
|
4389
|
+
function overlaps(a, b) {
|
|
4390
|
+
return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
|
|
4391
|
+
}
|
|
4392
|
+
function offFrame(box, frame) {
|
|
4393
|
+
return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
|
|
4394
|
+
}
|
|
4395
|
+
function insideFrame(box, frame) {
|
|
4396
|
+
return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
|
|
4397
|
+
}
|
|
4398
|
+
function occupancy(frame, reserved = []) {
|
|
4399
|
+
const placed = [...reserved];
|
|
4400
|
+
return (box) => {
|
|
4401
|
+
if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
|
|
4402
|
+
return false;
|
|
4364
4403
|
}
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
const reference = graph.referencePath ? graph.paths.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
|
|
4368
|
-
return {
|
|
4369
|
-
name,
|
|
4370
|
-
nodeIds,
|
|
4371
|
-
edgeIndexes,
|
|
4372
|
-
steps: path.nodeIds.length,
|
|
4373
|
-
bp: bpOf(path.nodeIds),
|
|
4374
|
-
referenceBp: reference && reference !== path ? bpOf(reference.nodeIds) : void 0
|
|
4404
|
+
placed.push(box);
|
|
4405
|
+
return true;
|
|
4375
4406
|
};
|
|
4376
4407
|
}
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
const a = indexOf.get(e.from);
|
|
4394
|
-
const b = indexOf.get(e.to);
|
|
4395
|
-
if (a === void 0 || b === void 0 || a === b) {
|
|
4396
|
-
continue;
|
|
4397
|
-
}
|
|
4398
|
-
linked.add(`${e.from}>${e.to}`).add(`${e.to}>${e.from}`);
|
|
4399
|
-
const [lo, hi] = a < b ? [a, b] : [b, a];
|
|
4400
|
-
reach[lo + 1]++;
|
|
4401
|
-
reach[hi]--;
|
|
4402
|
-
const [from, to] = a < b ? [e.from, e.to] : [e.to, e.from];
|
|
4403
|
-
(succ.get(from) ?? succ.set(from, /* @__PURE__ */ new Set()).get(from)).add(to);
|
|
4404
|
-
}
|
|
4405
|
-
for (let i = 1; i < reach.length; i++) {
|
|
4406
|
-
reach[i] += reach[i - 1];
|
|
4407
|
-
}
|
|
4408
|
-
const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
|
|
4409
|
-
const ends = /* @__PURE__ */ new Set([backbone[0].id, backbone.at(-1).id]);
|
|
4410
|
-
const boundaries = backbone.map((node) => [node, indexOf.get(node.id)]).filter(([node, i]) => ends.has(node.id) || reach[i] === 0).sort(([a, ia], [b, ib]) => ia - ib || a.stable.start - b.stable.start);
|
|
4411
|
-
const walkIndex = graph.paths?.map((p) => {
|
|
4412
|
-
const at = /* @__PURE__ */ new Map();
|
|
4413
|
-
p.nodeIds.forEach((id, i) => {
|
|
4414
|
-
if (!at.has(id)) {
|
|
4415
|
-
at.set(id, i);
|
|
4416
|
-
}
|
|
4417
|
-
});
|
|
4418
|
-
return at;
|
|
4419
|
-
});
|
|
4420
|
-
const bubbles = [];
|
|
4421
|
-
for (let i = 0; i + 1 < boundaries.length; i++) {
|
|
4422
|
-
const [start, i0] = boundaries[i];
|
|
4423
|
-
const [end, i1] = boundaries[i + 1];
|
|
4424
|
-
const interior = order.slice(i0 + 1, i1);
|
|
4425
|
-
if (interior.length === 0) {
|
|
4426
|
-
continue;
|
|
4427
|
-
}
|
|
4428
|
-
const best = /* @__PURE__ */ new Map([[start.id, { min: 0, max: 0, n: 1 }]]);
|
|
4429
|
-
for (const id of [start.id, ...interior]) {
|
|
4430
|
-
const cur = best.get(id);
|
|
4431
|
-
if (!cur) {
|
|
4432
|
-
continue;
|
|
4408
|
+
function placeLabels(candidates, frame, take) {
|
|
4409
|
+
const out = [];
|
|
4410
|
+
const row = LABEL_PX + LABEL_PAD * 2;
|
|
4411
|
+
for (const c of candidates) {
|
|
4412
|
+
const w = labelWidth(c.text);
|
|
4413
|
+
const direction = c.y > frame.height / 2 ? -1 : 1;
|
|
4414
|
+
for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
|
|
4415
|
+
const y = c.y + direction * tries * row;
|
|
4416
|
+
const box = {
|
|
4417
|
+
x0: c.x - w / 2,
|
|
4418
|
+
x1: c.x + w / 2,
|
|
4419
|
+
y0: y - LABEL_PX - LABEL_PAD,
|
|
4420
|
+
y1: y + LABEL_PAD
|
|
4421
|
+
};
|
|
4422
|
+
if (offFrame(box, frame)) {
|
|
4423
|
+
break;
|
|
4433
4424
|
}
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
continue;
|
|
4438
|
-
}
|
|
4439
|
-
const add = t === end.id ? 0 : byId.get(t).length;
|
|
4440
|
-
const prev = best.get(t);
|
|
4441
|
-
best.set(t, {
|
|
4442
|
-
min: Math.min(prev?.min ?? Infinity, cur.min + add),
|
|
4443
|
-
max: Math.max(prev?.max ?? -Infinity, cur.max + add),
|
|
4444
|
-
n: Math.min(SATURATED_PATH_COUNT, cur.n + (prev?.n ?? 0))
|
|
4445
|
-
});
|
|
4425
|
+
if (insideFrame(box, frame) && take(box)) {
|
|
4426
|
+
out.push({ ...c, y, w });
|
|
4427
|
+
break;
|
|
4446
4428
|
}
|
|
4447
4429
|
}
|
|
4448
|
-
const walked = walkIndex && walkRoutes(graph, byId, walkIndex, start.id, end.id);
|
|
4449
|
-
const crossed = walked?.n ? walked : void 0;
|
|
4450
|
-
const routes2 = crossed ?? best.get(end.id) ?? { min: 0, max: 0, n: 0 };
|
|
4451
|
-
const walksLeave = walked !== void 0 && walked.left > 0;
|
|
4452
|
-
const refStart = start.stable.start + start.length;
|
|
4453
|
-
const refEnd = end.stable.start;
|
|
4454
|
-
const chain = [
|
|
4455
|
-
start.id,
|
|
4456
|
-
...interior.filter((id) => isBackbone(byId.get(id))),
|
|
4457
|
-
end.id
|
|
4458
|
-
];
|
|
4459
|
-
const chainBroken = chain.some(
|
|
4460
|
-
(id, k) => k > 0 && !linked.has(`${chain[k - 1]}>${id}`)
|
|
4461
|
-
);
|
|
4462
|
-
const partial = chainBroken || walksLeave;
|
|
4463
|
-
const fallback = chainBroken && !crossed;
|
|
4464
|
-
bubbles.push({
|
|
4465
|
-
refName: start.stable.refName,
|
|
4466
|
-
start: refStart,
|
|
4467
|
-
end: refEnd,
|
|
4468
|
-
segmentCount: interior.length + 2,
|
|
4469
|
-
pathCount: routes2.n,
|
|
4470
|
-
// a real inversion test needs each walk's direction relative to the
|
|
4471
|
-
// reference; the first visit's strand is just whichever path anchored the
|
|
4472
|
-
// node
|
|
4473
|
-
inversion: false,
|
|
4474
|
-
shortestAlleleLength: fallback ? refEnd - refStart : routes2.min,
|
|
4475
|
-
longestAlleleLength: fallback ? refEnd - refStart : routes2.max,
|
|
4476
|
-
segments: [start, ...interior.map((id) => byId.get(id)), end].map((n) => n.name).join(","),
|
|
4477
|
-
shortestAllele: void 0,
|
|
4478
|
-
longestAllele: void 0,
|
|
4479
|
-
partial,
|
|
4480
|
-
routes: crossed?.routes
|
|
4481
|
-
});
|
|
4482
4430
|
}
|
|
4483
|
-
return
|
|
4484
|
-
}
|
|
4485
|
-
function walkRoutes(graph, byId, walkIndex, startId, endId) {
|
|
4486
|
-
const seen = /* @__PURE__ */ new Map();
|
|
4487
|
-
let min = Infinity;
|
|
4488
|
-
let max = -Infinity;
|
|
4489
|
-
let left = 0;
|
|
4490
|
-
graph.paths.forEach((p, k) => {
|
|
4491
|
-
const i0 = walkIndex[k].get(startId);
|
|
4492
|
-
const i1 = walkIndex[k].get(endId);
|
|
4493
|
-
if (i0 === void 0 || i1 === void 0) {
|
|
4494
|
-
if (i0 !== void 0 || i1 !== void 0) {
|
|
4495
|
-
left++;
|
|
4496
|
-
}
|
|
4497
|
-
return;
|
|
4498
|
-
}
|
|
4499
|
-
const steps = p.nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
|
|
4500
|
-
if (i1 < i0) {
|
|
4501
|
-
steps.reverse();
|
|
4502
|
-
}
|
|
4503
|
-
let bp = 0;
|
|
4504
|
-
for (const id of steps) {
|
|
4505
|
-
bp += byId.get(id)?.length ?? 0;
|
|
4506
|
-
}
|
|
4507
|
-
const key = steps.join(",");
|
|
4508
|
-
const route = seen.get(key) ?? { steps, bp, walks: [] };
|
|
4509
|
-
route.walks.push(p.name);
|
|
4510
|
-
seen.set(key, route);
|
|
4511
|
-
min = Math.min(min, bp);
|
|
4512
|
-
max = Math.max(max, bp);
|
|
4513
|
-
});
|
|
4514
|
-
return seen.size || left ? { min, max, n: seen.size, left, routes: [...seen.values()] } : void 0;
|
|
4431
|
+
return out;
|
|
4515
4432
|
}
|
|
4516
4433
|
|
|
4517
|
-
// src/
|
|
4518
|
-
var
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4434
|
+
// src/graphLabels.ts
|
|
4435
|
+
var MIN_DELETION_LABEL_PX = 26;
|
|
4436
|
+
var PX_PER_NODE_LABEL = 4e4;
|
|
4437
|
+
var MIN_NODE_LABELS = 8;
|
|
4438
|
+
var LEADER_GAP_PX = 8;
|
|
4439
|
+
var LABEL_CHAR_PX2 = 5.7;
|
|
4440
|
+
var LABEL_PAD_PX = 6;
|
|
4441
|
+
var LABEL_HEIGHT_PX = 13;
|
|
4442
|
+
var LABEL_GAP_PX = 3;
|
|
4443
|
+
var MAX_LABEL_OVERHANG = 2;
|
|
4444
|
+
function labelHalfWidth(text) {
|
|
4445
|
+
return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
|
|
4525
4446
|
}
|
|
4526
|
-
var
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4447
|
+
var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
|
|
4448
|
+
var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
|
|
4449
|
+
function boxAt(halfW, x, y) {
|
|
4450
|
+
return {
|
|
4451
|
+
x0: x - halfW,
|
|
4452
|
+
x1: x + halfW,
|
|
4453
|
+
y0: y - LABEL_HALF_HEIGHT,
|
|
4454
|
+
y1: y + LABEL_HALF_HEIGHT
|
|
4455
|
+
};
|
|
4535
4456
|
}
|
|
4536
|
-
function
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4457
|
+
function rowLabelBox(text, screenY) {
|
|
4458
|
+
return {
|
|
4459
|
+
x0: 6,
|
|
4460
|
+
x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
|
|
4461
|
+
y0: screenY - 8 - LABEL_GAP_PX / 2,
|
|
4462
|
+
y1: screenY + 8 + LABEL_GAP_PX / 2
|
|
4463
|
+
};
|
|
4541
4464
|
}
|
|
4542
|
-
function
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
if (
|
|
4547
|
-
return `${(
|
|
4465
|
+
function clamp(v, lo, hi) {
|
|
4466
|
+
return hi < lo ? v : Math.min(Math.max(v, lo), hi);
|
|
4467
|
+
}
|
|
4468
|
+
function formatBp(bp) {
|
|
4469
|
+
if (bp >= 1e6) {
|
|
4470
|
+
return `${+(bp / 1e6).toFixed(1)} Mb`;
|
|
4548
4471
|
}
|
|
4549
|
-
return `${
|
|
4472
|
+
return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
|
|
4550
4473
|
}
|
|
4551
|
-
function
|
|
4552
|
-
|
|
4553
|
-
return b.partial ? { ...c, label: `${c.label}, partial` } : c;
|
|
4474
|
+
function deletionText(bp) {
|
|
4475
|
+
return `${formatBp(bp)} del`;
|
|
4554
4476
|
}
|
|
4555
|
-
function
|
|
4556
|
-
const
|
|
4557
|
-
const
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4477
|
+
function arcOutward(curves, apex, scaleX, scaleY) {
|
|
4478
|
+
const first = curves[0];
|
|
4479
|
+
const last = curves[curves.length - 1];
|
|
4480
|
+
const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
|
|
4481
|
+
const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
|
|
4482
|
+
const len = Math.hypot(dx, dy);
|
|
4483
|
+
return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
|
|
4484
|
+
}
|
|
4485
|
+
function boxSupport(box, dir) {
|
|
4486
|
+
return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
|
|
4487
|
+
}
|
|
4488
|
+
function boxRayCrossing(box, dir) {
|
|
4489
|
+
const halfW = (box.x1 - box.x0) / 2;
|
|
4490
|
+
const halfH = (box.y1 - box.y0) / 2;
|
|
4491
|
+
return Math.min(
|
|
4492
|
+
dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
|
|
4493
|
+
dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
|
|
4494
|
+
);
|
|
4495
|
+
}
|
|
4496
|
+
function drawnLength(segments, scaleX, scaleY) {
|
|
4497
|
+
let total = 0;
|
|
4498
|
+
for (let i = 1; i < segments.length; i++) {
|
|
4499
|
+
const a = segments[i - 1];
|
|
4500
|
+
const b = segments[i];
|
|
4501
|
+
total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
|
|
4573
4502
|
}
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4503
|
+
return total;
|
|
4504
|
+
}
|
|
4505
|
+
function runBounds(nodeIds, nodePositions) {
|
|
4506
|
+
let minX = Infinity;
|
|
4507
|
+
let minY = Infinity;
|
|
4508
|
+
let maxX = -Infinity;
|
|
4509
|
+
let maxY = -Infinity;
|
|
4510
|
+
for (const id of nodeIds) {
|
|
4511
|
+
for (const p of nodePositions[id] ?? []) {
|
|
4512
|
+
minX = Math.min(minX, p.x);
|
|
4513
|
+
maxX = Math.max(maxX, p.x);
|
|
4514
|
+
minY = Math.min(minY, p.y);
|
|
4515
|
+
maxY = Math.max(maxY, p.y);
|
|
4516
|
+
}
|
|
4581
4517
|
}
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4518
|
+
return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
|
|
4519
|
+
}
|
|
4520
|
+
var nodeOrderCache = /* @__PURE__ */ new WeakMap();
|
|
4521
|
+
function labelOrder(nodePositions, nodeLengths) {
|
|
4522
|
+
const cached2 = nodeOrderCache.get(nodePositions);
|
|
4523
|
+
if (cached2?.nodeLengths === nodeLengths) {
|
|
4524
|
+
return cached2.order;
|
|
4585
4525
|
}
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4526
|
+
const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
|
|
4527
|
+
nodeOrderCache.set(nodePositions, { nodeLengths, order });
|
|
4528
|
+
return order;
|
|
4529
|
+
}
|
|
4530
|
+
var arcCache = /* @__PURE__ */ new WeakMap();
|
|
4531
|
+
function arcPlacements(nodePositions, deletions, axis, version) {
|
|
4532
|
+
const { scaleX, scaleY } = axis;
|
|
4533
|
+
const cached2 = arcCache.get(nodePositions);
|
|
4534
|
+
if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
|
|
4535
|
+
return cached2.arcs;
|
|
4591
4536
|
}
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4537
|
+
const arcs = [];
|
|
4538
|
+
for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
|
|
4539
|
+
const curves = deletionArcCurves(nodePositions, deletion, axis);
|
|
4540
|
+
const apex = curves ? curveMidpoint(curves) : void 0;
|
|
4541
|
+
if (curves && apex) {
|
|
4542
|
+
const { minX, minY, maxX, maxY } = curveBounds(curves);
|
|
4543
|
+
arcs.push({
|
|
4544
|
+
deletion,
|
|
4545
|
+
curves,
|
|
4546
|
+
apex,
|
|
4547
|
+
// in screen px, so the two axes' units cannot be added by accident
|
|
4548
|
+
extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4597
4551
|
}
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
label: `${formatBp(refSpan)} ref \u2192 ${range}, ${pathCount} alleles`
|
|
4601
|
-
};
|
|
4602
|
-
}
|
|
4603
|
-
var BUBBLE_KIND_COLORS = {
|
|
4604
|
-
snp: "#5b6b7a",
|
|
4605
|
-
substitution: "#5b6b7a",
|
|
4606
|
-
insertion: "#2f8fd6",
|
|
4607
|
-
deletion: "#c94040",
|
|
4608
|
-
inversion: "#e07b00",
|
|
4609
|
-
repeat: "#8e3fbf",
|
|
4610
|
-
superbubble: "#8e3fbf",
|
|
4611
|
-
complex: "#8e3fbf"
|
|
4612
|
-
};
|
|
4613
|
-
var BUBBLE_KIND_NAMES = {
|
|
4614
|
-
snp: "SNP",
|
|
4615
|
-
substitution: "substitution",
|
|
4616
|
-
insertion: "insertion",
|
|
4617
|
-
deletion: "deletion",
|
|
4618
|
-
inversion: "inversion",
|
|
4619
|
-
repeat: "repeat array",
|
|
4620
|
-
superbubble: "superbubble",
|
|
4621
|
-
complex: "site"
|
|
4622
|
-
};
|
|
4623
|
-
function bubbleSegmentIds(b) {
|
|
4624
|
-
return b.segments.split(",").filter(Boolean);
|
|
4552
|
+
arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
|
|
4553
|
+
return arcs;
|
|
4625
4554
|
}
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4555
|
+
function sizeLabelCandidates({
|
|
4556
|
+
nodePositions,
|
|
4557
|
+
nodeLengths,
|
|
4558
|
+
deletions,
|
|
4559
|
+
alleleDeletions,
|
|
4560
|
+
axis,
|
|
4561
|
+
translateX,
|
|
4562
|
+
translateY,
|
|
4563
|
+
width,
|
|
4564
|
+
height,
|
|
4565
|
+
version = 0
|
|
4566
|
+
}) {
|
|
4567
|
+
const { scaleX, scaleY } = axis;
|
|
4568
|
+
const deletionCandidates = [];
|
|
4569
|
+
const nodeCandidates = [];
|
|
4570
|
+
for (const { deletion, curves, apex, extent } of arcPlacements(
|
|
4571
|
+
nodePositions,
|
|
4572
|
+
deletions,
|
|
4573
|
+
axis,
|
|
4574
|
+
version
|
|
4575
|
+
)) {
|
|
4576
|
+
if (extent >= MIN_DELETION_LABEL_PX) {
|
|
4577
|
+
const text = deletionText(deletion.bp);
|
|
4578
|
+
const halfW = labelHalfWidth(text);
|
|
4579
|
+
const box = boxAt(halfW, 0, 0);
|
|
4580
|
+
const arcX = apex.x * scaleX + translateX;
|
|
4581
|
+
const arcY = apex.y * scaleY + translateY;
|
|
4582
|
+
const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
|
|
4583
|
+
const bow = arcOutward(curves, apex, scaleX, scaleY);
|
|
4584
|
+
const offset = boxSupport(box, bow) + LEADER_GAP_PX;
|
|
4585
|
+
const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
|
|
4586
|
+
const y = fits ? arcY : clamp(
|
|
4587
|
+
arcY + bow.y * offset,
|
|
4588
|
+
LABEL_HALF_HEIGHT,
|
|
4589
|
+
height - LABEL_HALF_HEIGHT
|
|
4590
|
+
);
|
|
4591
|
+
const away = Math.hypot(x - arcX, y - arcY);
|
|
4592
|
+
let leader;
|
|
4593
|
+
if (away > 0) {
|
|
4594
|
+
const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
|
|
4595
|
+
const crossing = boxRayCrossing(box, dir);
|
|
4596
|
+
leader = {
|
|
4597
|
+
arcX,
|
|
4598
|
+
arcY,
|
|
4599
|
+
labelX: x - dir.x * crossing,
|
|
4600
|
+
labelY: y - dir.y * crossing
|
|
4601
|
+
};
|
|
4657
4602
|
}
|
|
4603
|
+
deletionCandidates.push({
|
|
4604
|
+
label: {
|
|
4605
|
+
key: `del:${deletion.edgeIndex}`,
|
|
4606
|
+
text,
|
|
4607
|
+
x,
|
|
4608
|
+
y,
|
|
4609
|
+
kind: "deletion",
|
|
4610
|
+
leader
|
|
4611
|
+
},
|
|
4612
|
+
box: boxAt(halfW, x, y)
|
|
4613
|
+
});
|
|
4658
4614
|
}
|
|
4659
|
-
|
|
4615
|
+
}
|
|
4616
|
+
const labelledByDeletion = /* @__PURE__ */ new Set();
|
|
4617
|
+
const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
|
|
4618
|
+
for (const run of alleleRuns) {
|
|
4619
|
+
const drawn = runBounds(run.nodeIds, nodePositions);
|
|
4620
|
+
if (!drawn) {
|
|
4660
4621
|
continue;
|
|
4661
4622
|
}
|
|
4662
|
-
const
|
|
4663
|
-
const
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
for (const route of bubble.routes ?? []) {
|
|
4669
|
-
for (const id of new Set(route.steps)) {
|
|
4670
|
-
sharing.set(id, (sharing.get(id) ?? 0) + 1);
|
|
4671
|
-
}
|
|
4623
|
+
const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
|
|
4624
|
+
const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
|
|
4625
|
+
const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
|
|
4626
|
+
const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
|
|
4627
|
+
if (shownRight < shownLeft || shownBottom < shownTop) {
|
|
4628
|
+
continue;
|
|
4672
4629
|
}
|
|
4673
|
-
const
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
continue;
|
|
4680
|
-
}
|
|
4681
|
-
const rarest = Math.min(...route.steps.map((id) => sharing.get(id)));
|
|
4682
|
-
let at;
|
|
4683
|
-
let far = -1;
|
|
4684
|
-
for (const id of route.steps) {
|
|
4685
|
-
if (sharing.get(id) !== rarest) {
|
|
4686
|
-
continue;
|
|
4687
|
-
}
|
|
4688
|
-
for (const p of positions[id] ?? []) {
|
|
4689
|
-
const d = Math.hypot(p.x - anchor.x, p.y - anchor.y);
|
|
4690
|
-
if (d > far) {
|
|
4691
|
-
far = d;
|
|
4692
|
-
at = p;
|
|
4693
|
-
}
|
|
4694
|
-
}
|
|
4695
|
-
}
|
|
4696
|
-
if (at) {
|
|
4697
|
-
routes2.push({ route, at, text: routeText(route, walkLabel) });
|
|
4630
|
+
const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
|
|
4631
|
+
const text = deletionText(run.bp);
|
|
4632
|
+
const halfW = labelHalfWidth(text);
|
|
4633
|
+
if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
|
|
4634
|
+
for (const id of run.nodeIds) {
|
|
4635
|
+
labelledByDeletion.add(id);
|
|
4698
4636
|
}
|
|
4637
|
+
const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
|
|
4638
|
+
const y = clamp(
|
|
4639
|
+
(shownTop + shownBottom) / 2,
|
|
4640
|
+
LABEL_HALF_HEIGHT,
|
|
4641
|
+
height - LABEL_HALF_HEIGHT
|
|
4642
|
+
);
|
|
4643
|
+
deletionCandidates.push({
|
|
4644
|
+
label: {
|
|
4645
|
+
key: `alleledel:${run.nodeIds.join(",")}`,
|
|
4646
|
+
text,
|
|
4647
|
+
x,
|
|
4648
|
+
y,
|
|
4649
|
+
kind: "deletion"
|
|
4650
|
+
},
|
|
4651
|
+
box: boxAt(halfW, x, y)
|
|
4652
|
+
});
|
|
4699
4653
|
}
|
|
4700
|
-
halos.push({
|
|
4701
|
-
bubble,
|
|
4702
|
-
...classifyBubble(bubble, repeats),
|
|
4703
|
-
path: parts.join(""),
|
|
4704
|
-
labelAt,
|
|
4705
|
-
members: nodeIds.length,
|
|
4706
|
-
nodeIds,
|
|
4707
|
-
whole: nodeIds.length >= WHOLE_FRACTION * graph.nodes.length,
|
|
4708
|
-
routes: routes2
|
|
4709
|
-
});
|
|
4710
4654
|
}
|
|
4711
|
-
|
|
4655
|
+
for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
|
|
4656
|
+
if (labelledByDeletion.has(id)) {
|
|
4657
|
+
continue;
|
|
4658
|
+
}
|
|
4659
|
+
const { x, y } = polylineMidpoint(segments);
|
|
4660
|
+
const screenY = y * scaleY + translateY;
|
|
4661
|
+
if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
|
|
4662
|
+
continue;
|
|
4663
|
+
}
|
|
4664
|
+
const drawn = drawnLength(segments, scaleX, scaleY);
|
|
4665
|
+
if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
|
|
4666
|
+
continue;
|
|
4667
|
+
}
|
|
4668
|
+
const text = formatBp(nodeLengths.get(id));
|
|
4669
|
+
const halfW = labelHalfWidth(text);
|
|
4670
|
+
if (drawn >= halfW * 2 / MAX_LABEL_OVERHANG) {
|
|
4671
|
+
const screenX = x * scaleX + translateX;
|
|
4672
|
+
nodeCandidates.push({
|
|
4673
|
+
label: {
|
|
4674
|
+
key: `node:${id}`,
|
|
4675
|
+
text,
|
|
4676
|
+
x: screenX,
|
|
4677
|
+
y: screenY,
|
|
4678
|
+
kind: "node"
|
|
4679
|
+
},
|
|
4680
|
+
box: boxAt(halfW, screenX, screenY)
|
|
4681
|
+
});
|
|
4682
|
+
}
|
|
4683
|
+
}
|
|
4684
|
+
return { deletions: deletionCandidates, nodes: nodeCandidates };
|
|
4712
4685
|
}
|
|
4713
|
-
function
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4686
|
+
function nodeLabelBudget(width, height) {
|
|
4687
|
+
return Math.max(
|
|
4688
|
+
MIN_NODE_LABELS,
|
|
4689
|
+
Math.floor(width * height / PX_PER_NODE_LABEL)
|
|
4690
|
+
);
|
|
4718
4691
|
}
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
...paths?.length ? { paths } : {},
|
|
4731
|
-
anchorPaths: graph.anchorPaths,
|
|
4732
|
-
pathVisits: graph.pathVisits,
|
|
4733
|
-
anchoredBy: graph.anchoredBy,
|
|
4734
|
-
referencePath: graph.referencePath
|
|
4735
|
-
};
|
|
4692
|
+
function placeSizeLabels(candidates, take, budget = Infinity) {
|
|
4693
|
+
const labels = [];
|
|
4694
|
+
for (const { label, box } of candidates) {
|
|
4695
|
+
if (labels.length >= budget) {
|
|
4696
|
+
break;
|
|
4697
|
+
}
|
|
4698
|
+
if (take(box)) {
|
|
4699
|
+
labels.push(label);
|
|
4700
|
+
}
|
|
4701
|
+
}
|
|
4702
|
+
return labels;
|
|
4736
4703
|
}
|
|
4737
4704
|
|
|
4738
|
-
// src/
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
}
|
|
4754
|
-
function occupancy(frame, reserved = []) {
|
|
4755
|
-
const placed = [...reserved];
|
|
4756
|
-
return (box) => {
|
|
4757
|
-
if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
|
|
4758
|
-
return false;
|
|
4705
|
+
// src/tubeMap/axis.ts
|
|
4706
|
+
function referenceBoxes(graph, layout) {
|
|
4707
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
4708
|
+
const byRefName = /* @__PURE__ */ new Map();
|
|
4709
|
+
layout.nodes.forEach((node) => {
|
|
4710
|
+
const graphNode = nodeById.get(node.name);
|
|
4711
|
+
if (node.order >= 0 && graphNode && isBackbone(graphNode)) {
|
|
4712
|
+
const { refName, start } = graphNode.stable;
|
|
4713
|
+
const boxes = byRefName.get(refName) ?? byRefName.set(refName, []).get(refName);
|
|
4714
|
+
boxes.push({
|
|
4715
|
+
bp0: start,
|
|
4716
|
+
bp1: start + node.sequenceLength,
|
|
4717
|
+
x0: node.x,
|
|
4718
|
+
x1: node.x + node.pixelWidth
|
|
4719
|
+
});
|
|
4759
4720
|
}
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4721
|
+
});
|
|
4722
|
+
for (const boxes of byRefName.values()) {
|
|
4723
|
+
boxes.sort((a, b) => a.bp0 - b.bp0);
|
|
4724
|
+
}
|
|
4725
|
+
return byRefName;
|
|
4763
4726
|
}
|
|
4764
|
-
function
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
const
|
|
4769
|
-
const
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
x1: c.x + w / 2,
|
|
4775
|
-
y0: y - LABEL_PX - LABEL_PAD,
|
|
4776
|
-
y1: y + LABEL_PAD
|
|
4777
|
-
};
|
|
4778
|
-
if (offFrame(box, frame)) {
|
|
4779
|
-
break;
|
|
4780
|
-
}
|
|
4781
|
-
if (insideFrame(box, frame) && take(box)) {
|
|
4782
|
-
out.push({ ...c, y, w });
|
|
4783
|
-
break;
|
|
4784
|
-
}
|
|
4727
|
+
function tubeX(boxes, bp, isEnd = false) {
|
|
4728
|
+
let lo = 0;
|
|
4729
|
+
let hi = boxes.length - 1;
|
|
4730
|
+
while (lo < hi) {
|
|
4731
|
+
const mid = lo + hi >> 1;
|
|
4732
|
+
const bp1 = boxes[mid].bp1;
|
|
4733
|
+
if (isEnd ? bp1 < bp : bp1 <= bp) {
|
|
4734
|
+
lo = mid + 1;
|
|
4735
|
+
} else {
|
|
4736
|
+
hi = mid;
|
|
4785
4737
|
}
|
|
4786
4738
|
}
|
|
4787
|
-
|
|
4739
|
+
const box = boxes[lo];
|
|
4740
|
+
if (bp <= box.bp0) {
|
|
4741
|
+
return box.x0;
|
|
4742
|
+
}
|
|
4743
|
+
const span = box.bp1 - box.bp0;
|
|
4744
|
+
return span > 0 ? box.x0 + (Math.min(bp, box.bp1) - box.bp0) / span * (box.x1 - box.x0) : box.x0;
|
|
4788
4745
|
}
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
var MIN_NODE_LABELS = 8;
|
|
4794
|
-
var LEADER_GAP_PX = 8;
|
|
4795
|
-
var LABEL_CHAR_PX2 = 5.7;
|
|
4796
|
-
var LABEL_PAD_PX = 6;
|
|
4797
|
-
var LABEL_HEIGHT_PX = 13;
|
|
4798
|
-
var LABEL_GAP_PX = 3;
|
|
4799
|
-
var MAX_LABEL_OVERHANG = 2;
|
|
4800
|
-
function labelHalfWidth(text) {
|
|
4801
|
-
return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
|
|
4746
|
+
function tubeSpan(boxes, start, end) {
|
|
4747
|
+
const a = tubeX(boxes, start);
|
|
4748
|
+
const b = tubeX(boxes, end, true);
|
|
4749
|
+
return { x0: Math.min(a, b), x1: Math.max(a, b) };
|
|
4802
4750
|
}
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
};
|
|
4751
|
+
function rulerBoxes(byContig) {
|
|
4752
|
+
let best;
|
|
4753
|
+
for (const boxes of byContig.values()) {
|
|
4754
|
+
if (!best || boxes.length > best.length) {
|
|
4755
|
+
best = boxes;
|
|
4756
|
+
}
|
|
4757
|
+
}
|
|
4758
|
+
return best;
|
|
4812
4759
|
}
|
|
4813
|
-
|
|
4760
|
+
var TICK_PX = 5;
|
|
4761
|
+
var LABEL_GAP_PX2 = 10;
|
|
4762
|
+
var TARGET_TICK_PX = 110;
|
|
4763
|
+
var ZIGZAG_PX = 4;
|
|
4764
|
+
var ZIGZAG_AMPLITUDE_PX = 2.5;
|
|
4765
|
+
function niceStep(raw) {
|
|
4766
|
+
const pow = 10 ** Math.floor(Math.log10(raw));
|
|
4767
|
+
const m = raw / pow;
|
|
4768
|
+
return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;
|
|
4769
|
+
}
|
|
4770
|
+
function rulerMarks(boxes, x, width, measure) {
|
|
4771
|
+
const first = boxes[0];
|
|
4772
|
+
const last = boxes.at(-1);
|
|
4773
|
+
if (!first || !last) {
|
|
4774
|
+
return void 0;
|
|
4775
|
+
}
|
|
4776
|
+
const from = x(first.x0);
|
|
4777
|
+
const to = x(last.x1);
|
|
4778
|
+
const bpSpan = last.bp1 - first.bp0;
|
|
4779
|
+
if (to <= from || bpSpan <= 0) {
|
|
4780
|
+
return void 0;
|
|
4781
|
+
}
|
|
4782
|
+
const step = niceStep(bpSpan / (to - from) * TARGET_TICK_PX);
|
|
4783
|
+
const squeezedBoxes = boxes.filter((box) => {
|
|
4784
|
+
const bp = box.bp1 - box.bp0;
|
|
4785
|
+
return bp >= step && (x(box.x1) - x(box.x0)) * step / bp < TARGET_TICK_PX / 2;
|
|
4786
|
+
});
|
|
4787
|
+
const squeezed = [];
|
|
4788
|
+
const lengths = [];
|
|
4789
|
+
for (const box of squeezedBoxes) {
|
|
4790
|
+
const x0 = x(box.x0);
|
|
4791
|
+
const x1 = x(box.x1);
|
|
4792
|
+
if (x1 >= 0 && x0 <= width) {
|
|
4793
|
+
squeezed.push({ x0, x1 });
|
|
4794
|
+
const text = formatBp(box.bp1 - box.bp0);
|
|
4795
|
+
lengths.push({ text, at: (x0 + x1) / 2, width: measure(text) });
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
const ticks = [];
|
|
4799
|
+
const positions = [];
|
|
4800
|
+
let k = 0;
|
|
4801
|
+
for (let bp = Math.ceil(first.bp0 / step) * step; bp <= last.bp1; bp += step) {
|
|
4802
|
+
while (k < squeezedBoxes.length && squeezedBoxes[k].bp1 <= bp) {
|
|
4803
|
+
k++;
|
|
4804
|
+
}
|
|
4805
|
+
if (k < squeezedBoxes.length && squeezedBoxes[k].bp0 < bp) {
|
|
4806
|
+
continue;
|
|
4807
|
+
}
|
|
4808
|
+
const sx = Math.round(x(tubeX(boxes, bp))) + 0.5;
|
|
4809
|
+
if (sx >= 0 && sx <= width) {
|
|
4810
|
+
ticks.push(sx);
|
|
4811
|
+
const text = bp.toLocaleString("en-US");
|
|
4812
|
+
positions.push({ text, at: sx, width: measure(text) });
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
const placed = [];
|
|
4816
|
+
const clear = (a) => placed.every(
|
|
4817
|
+
(b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX2
|
|
4818
|
+
);
|
|
4819
|
+
for (const label of [...positions, ...lengths]) {
|
|
4820
|
+
if (clear(label)) {
|
|
4821
|
+
placed.push(label);
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4814
4824
|
return {
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4825
|
+
from,
|
|
4826
|
+
to,
|
|
4827
|
+
ticks,
|
|
4828
|
+
squeezed,
|
|
4829
|
+
labels: placed.map(({ text, at }) => ({ text, at }))
|
|
4819
4830
|
};
|
|
4820
4831
|
}
|
|
4821
|
-
function
|
|
4822
|
-
|
|
4832
|
+
function zigzag(ctx, x0, x1, y) {
|
|
4833
|
+
const teeth = Math.max(1, Math.round((x1 - x0) / ZIGZAG_PX));
|
|
4834
|
+
const dx = (x1 - x0) / teeth;
|
|
4835
|
+
for (let i = 0; i < teeth; i++) {
|
|
4836
|
+
ctx.lineTo(x0 + (i + 0.5) * dx, y + (i % 2 ? 1 : -1) * ZIGZAG_AMPLITUDE_PX);
|
|
4837
|
+
}
|
|
4838
|
+
ctx.lineTo(x1, y);
|
|
4823
4839
|
}
|
|
4824
|
-
function
|
|
4825
|
-
|
|
4826
|
-
|
|
4840
|
+
function drawTubeMapRuler(ctx, boxes, frame, top) {
|
|
4841
|
+
const { x, width, darkMode } = frame;
|
|
4842
|
+
ctx.font = "10px sans-serif";
|
|
4843
|
+
const marks = rulerMarks(boxes, x, width, (text) => ctx.measureText(text).width);
|
|
4844
|
+
if (!marks) {
|
|
4845
|
+
return;
|
|
4846
|
+
}
|
|
4847
|
+
const ink = darkMode ? "#b0b0b8" : "#55555c";
|
|
4848
|
+
const y = top + 0.5;
|
|
4849
|
+
ctx.strokeStyle = ink;
|
|
4850
|
+
ctx.fillStyle = ink;
|
|
4851
|
+
ctx.lineWidth = 1;
|
|
4852
|
+
ctx.textAlign = "center";
|
|
4853
|
+
ctx.textBaseline = "top";
|
|
4854
|
+
ctx.beginPath();
|
|
4855
|
+
ctx.moveTo(Math.max(0, marks.from), y);
|
|
4856
|
+
for (const { x0, x1 } of marks.squeezed) {
|
|
4857
|
+
ctx.lineTo(x0, y);
|
|
4858
|
+
zigzag(ctx, x0, x1, y);
|
|
4859
|
+
}
|
|
4860
|
+
ctx.lineTo(Math.min(width, marks.to), y);
|
|
4861
|
+
for (const sx of marks.ticks) {
|
|
4862
|
+
ctx.moveTo(sx, top);
|
|
4863
|
+
ctx.lineTo(sx, top + TICK_PX);
|
|
4864
|
+
}
|
|
4865
|
+
ctx.stroke();
|
|
4866
|
+
for (const { text, at } of marks.labels) {
|
|
4867
|
+
ctx.fillText(text, at, top + TICK_PX + 2);
|
|
4827
4868
|
}
|
|
4828
|
-
return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
|
|
4829
4869
|
}
|
|
4830
|
-
|
|
4831
|
-
|
|
4870
|
+
|
|
4871
|
+
// src/reference.ts
|
|
4872
|
+
var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
|
|
4873
|
+
["hg38", "GRCh38"],
|
|
4874
|
+
["hg19", "GRCh37"],
|
|
4875
|
+
["hs1", "CHM13"],
|
|
4876
|
+
["t2t-chm13", "CHM13"],
|
|
4877
|
+
["chm13v2", "CHM13"]
|
|
4878
|
+
]);
|
|
4879
|
+
function wellKnownSample(assemblyName) {
|
|
4880
|
+
return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
|
|
4881
|
+
}
|
|
4882
|
+
function sharedPrefixes(refNames) {
|
|
4883
|
+
return [panSNSample, panSNHaplotype].flatMap((prefixOf) => {
|
|
4884
|
+
const prefixes = new Set(
|
|
4885
|
+
refNames.map((n) => n.includes("#") ? prefixOf(n) : void 0)
|
|
4886
|
+
);
|
|
4887
|
+
const [only] = prefixes;
|
|
4888
|
+
return prefixes.size === 1 && only ? [only] : [];
|
|
4889
|
+
});
|
|
4832
4890
|
}
|
|
4833
|
-
function
|
|
4834
|
-
const
|
|
4835
|
-
const
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4891
|
+
function graphBackbone(graph) {
|
|
4892
|
+
const spans = /* @__PURE__ */ new Map();
|
|
4893
|
+
for (const node of graph.nodes) {
|
|
4894
|
+
if (isBackbone(node)) {
|
|
4895
|
+
const { refName, start } = node.stable;
|
|
4896
|
+
const end = start + node.length;
|
|
4897
|
+
const span = spans.get(refName);
|
|
4898
|
+
if (span) {
|
|
4899
|
+
span.start = Math.min(span.start, start);
|
|
4900
|
+
span.end = Math.max(span.end, end);
|
|
4901
|
+
} else {
|
|
4902
|
+
spans.set(refName, {
|
|
4903
|
+
refName,
|
|
4904
|
+
contig: panSNContig(refName),
|
|
4905
|
+
start,
|
|
4906
|
+
end
|
|
4907
|
+
});
|
|
4908
|
+
}
|
|
4909
|
+
}
|
|
4910
|
+
}
|
|
4911
|
+
const contigs = [...spans.values()];
|
|
4912
|
+
return contigs.length ? { contigs, prefixes: sharedPrefixes(contigs.map((c) => c.refName)) } : void 0;
|
|
4840
4913
|
}
|
|
4841
|
-
function
|
|
4842
|
-
|
|
4914
|
+
function backboneAssembly(backbone, assemblies) {
|
|
4915
|
+
const prefixes = new Set(backbone?.prefixes.map((p) => p.toLowerCase()));
|
|
4916
|
+
const names = (a) => [a.name, ...a.aliases ?? []];
|
|
4917
|
+
const named = (candidates) => candidates.some((n) => n !== void 0 && prefixes.has(n.toLowerCase()));
|
|
4918
|
+
return assemblies.find((a) => named(names(a))) ?? assemblies.find((a) => named(names(a).map(wellKnownSample)));
|
|
4843
4919
|
}
|
|
4844
|
-
function
|
|
4845
|
-
const
|
|
4846
|
-
const
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4920
|
+
function refNameBinding(refNames) {
|
|
4921
|
+
const exact = new Set(refNames);
|
|
4922
|
+
const byContig = /* @__PURE__ */ new Map();
|
|
4923
|
+
for (const refName of exact) {
|
|
4924
|
+
const contig = panSNContig(refName);
|
|
4925
|
+
byContig.set(contig, byContig.has(contig) ? void 0 : refName);
|
|
4926
|
+
}
|
|
4927
|
+
return (name) => exact.has(name) ? name : byContig.get(name);
|
|
4851
4928
|
}
|
|
4852
|
-
function
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
const
|
|
4856
|
-
|
|
4857
|
-
|
|
4929
|
+
function featuresOnBackbone(features, backbone) {
|
|
4930
|
+
const bind = refNameBinding(backbone.contigs.map((c) => c.refName));
|
|
4931
|
+
return features.flatMap((f) => {
|
|
4932
|
+
const refName = bind(f.refName);
|
|
4933
|
+
return refName === void 0 ? [] : [refName === f.refName ? f : { ...f, refName }];
|
|
4934
|
+
});
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4937
|
+
// src/tubeMap/genes.ts
|
|
4938
|
+
function tubeMapGenes(byRefName, genes) {
|
|
4939
|
+
const out = [];
|
|
4940
|
+
const bind = refNameBinding(byRefName.keys());
|
|
4941
|
+
for (const gene of genes) {
|
|
4942
|
+
const refName = bind(gene.refName);
|
|
4943
|
+
const boxes = refName === void 0 ? void 0 : byRefName.get(refName);
|
|
4944
|
+
const first = boxes?.[0];
|
|
4945
|
+
const last = boxes?.at(-1);
|
|
4946
|
+
if (!boxes || !first || !last) {
|
|
4947
|
+
continue;
|
|
4948
|
+
}
|
|
4949
|
+
const start = Math.max(gene.start, first.bp0);
|
|
4950
|
+
const end = Math.min(gene.end, last.bp1);
|
|
4951
|
+
if (end <= start) {
|
|
4952
|
+
continue;
|
|
4953
|
+
}
|
|
4954
|
+
out.push({
|
|
4955
|
+
gene,
|
|
4956
|
+
...tubeSpan(boxes, start, end),
|
|
4957
|
+
exons: gene.exons.filter((e) => e.end > start && e.start < end).map(
|
|
4958
|
+
(e) => tubeSpan(boxes, Math.max(e.start, start), Math.min(e.end, end))
|
|
4959
|
+
)
|
|
4960
|
+
});
|
|
4858
4961
|
}
|
|
4859
|
-
return
|
|
4962
|
+
return out;
|
|
4860
4963
|
}
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4964
|
+
var ROW_PX = 22;
|
|
4965
|
+
var EXON_PX = 8;
|
|
4966
|
+
var NAME_FONT = "11px sans-serif";
|
|
4967
|
+
var NAME_GAP_PX = 8;
|
|
4968
|
+
function drawTubeMapGenes(ctx, genes, frame, bottom, top = 0) {
|
|
4969
|
+
const { x, width, darkMode } = frame;
|
|
4970
|
+
const rows = Math.max(1, Math.floor((bottom - top) / ROW_PX));
|
|
4971
|
+
const rowEnds = [];
|
|
4972
|
+
const ink = darkMode ? "#e0e0e6" : "#1c1c22";
|
|
4973
|
+
ctx.font = NAME_FONT;
|
|
4974
|
+
ctx.textAlign = "center";
|
|
4975
|
+
ctx.textBaseline = "alphabetic";
|
|
4976
|
+
ctx.fillStyle = ink;
|
|
4977
|
+
ctx.strokeStyle = ink;
|
|
4978
|
+
ctx.lineWidth = 1;
|
|
4979
|
+
const placed = genes.map((g) => ({ g, s0: x(g.x0), s1: x(g.x1) })).filter(({ s0, s1 }) => s1 >= 0 && s0 <= width).sort((a, b) => a.s0 - b.s0);
|
|
4980
|
+
for (const { g, s0, s1 } of placed) {
|
|
4981
|
+
const arrow = g.gene.strand > 0 ? " \u2192" : g.gene.strand < 0 ? "\u2190 " : "";
|
|
4982
|
+
const label = g.gene.strand < 0 ? `${arrow}${g.gene.name}` : `${g.gene.name}${arrow}`;
|
|
4983
|
+
const mid = (Math.max(s0, 0) + Math.min(s1, width)) / 2;
|
|
4984
|
+
const half = ctx.measureText(label).width / 2;
|
|
4985
|
+
const left = Math.min(s0, mid - half);
|
|
4986
|
+
const right = Math.max(s1, mid + half);
|
|
4987
|
+
let row = rowEnds.findIndex((end) => end + NAME_GAP_PX <= left);
|
|
4988
|
+
if (row === -1) {
|
|
4989
|
+
row = Math.min(rowEnds.length, rows - 1);
|
|
4990
|
+
}
|
|
4991
|
+
rowEnds[row] = Math.max(rowEnds[row] ?? -Infinity, right);
|
|
4992
|
+
const glyphY = bottom - row * ROW_PX - EXON_PX / 2;
|
|
4993
|
+
ctx.beginPath();
|
|
4994
|
+
ctx.moveTo(s0, glyphY + 0.5);
|
|
4995
|
+
ctx.lineTo(s1, glyphY + 0.5);
|
|
4996
|
+
ctx.stroke();
|
|
4997
|
+
for (const e of g.exons) {
|
|
4998
|
+
const e0 = x(e.x0);
|
|
4999
|
+
ctx.fillRect(e0, glyphY - EXON_PX / 2, Math.max(1, x(e.x1) - e0), EXON_PX);
|
|
4872
5000
|
}
|
|
5001
|
+
ctx.fillText(label, mid, glyphY - EXON_PX / 2 - 2);
|
|
4873
5002
|
}
|
|
4874
|
-
return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
|
|
4875
5003
|
}
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
5004
|
+
|
|
5005
|
+
// src/tubeMap/connectors.ts
|
|
5006
|
+
function referenceNodes(graph, layout) {
|
|
5007
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5008
|
+
const out = [];
|
|
5009
|
+
layout.nodes.forEach((node) => {
|
|
5010
|
+
const graphNode = nodeById.get(node.name);
|
|
5011
|
+
if (node.order >= 0 && graphNode && isBackbone(graphNode)) {
|
|
5012
|
+
out.push({
|
|
5013
|
+
node: node.name,
|
|
5014
|
+
bp0: graphNode.stable.start,
|
|
5015
|
+
bp1: graphNode.stable.start + node.sequenceLength,
|
|
5016
|
+
x0: node.x,
|
|
5017
|
+
x1: node.x + node.pixelWidth
|
|
5018
|
+
});
|
|
5019
|
+
}
|
|
5020
|
+
});
|
|
5021
|
+
return out;
|
|
5022
|
+
}
|
|
5023
|
+
function tubeMapConnectors(nodes, bpToScreen, tubeToScreen, width) {
|
|
5024
|
+
const out = [];
|
|
5025
|
+
for (const n of nodes) {
|
|
5026
|
+
const bottom0 = tubeToScreen(n.x0);
|
|
5027
|
+
const bottom1 = tubeToScreen(n.x1);
|
|
5028
|
+
if (bottom1 >= 0 && bottom0 <= width) {
|
|
5029
|
+
out.push({
|
|
5030
|
+
node: n.node,
|
|
5031
|
+
top0: bpToScreen(n.bp0),
|
|
5032
|
+
top1: bpToScreen(n.bp1),
|
|
5033
|
+
bottom0,
|
|
5034
|
+
bottom1
|
|
5035
|
+
});
|
|
5036
|
+
}
|
|
4881
5037
|
}
|
|
4882
|
-
|
|
4883
|
-
nodeOrderCache.set(nodePositions, { nodeLengths, order });
|
|
4884
|
-
return order;
|
|
5038
|
+
return out;
|
|
4885
5039
|
}
|
|
4886
|
-
var
|
|
4887
|
-
function
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
|
|
4891
|
-
return cached2.arcs;
|
|
5040
|
+
var HIT_SLOP_PX = 3;
|
|
5041
|
+
function connectorAt(connectors, zoneBottom, sx, sy) {
|
|
5042
|
+
if (zoneBottom <= 0 || sy < 0 || sy > zoneBottom) {
|
|
5043
|
+
return void 0;
|
|
4892
5044
|
}
|
|
4893
|
-
const
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
// in screen px, so the two axes' units cannot be added by accident
|
|
4904
|
-
extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
|
|
4905
|
-
});
|
|
5045
|
+
const t = sy / zoneBottom;
|
|
5046
|
+
let hit;
|
|
5047
|
+
let best = HIT_SLOP_PX;
|
|
5048
|
+
for (const c of connectors) {
|
|
5049
|
+
const a = c.top0 + (c.bottom0 - c.top0) * t;
|
|
5050
|
+
const b = c.top1 + (c.bottom1 - c.top1) * t;
|
|
5051
|
+
const dist = Math.max(0, Math.min(a, b) - sx, sx - Math.max(a, b));
|
|
5052
|
+
if (dist < best) {
|
|
5053
|
+
best = dist;
|
|
5054
|
+
hit = c.node;
|
|
4906
5055
|
}
|
|
4907
5056
|
}
|
|
4908
|
-
|
|
4909
|
-
return arcs;
|
|
5057
|
+
return hit;
|
|
4910
5058
|
}
|
|
4911
|
-
function
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
5059
|
+
function band(ctx, c, bottom) {
|
|
5060
|
+
ctx.moveTo(c.top0, 0);
|
|
5061
|
+
ctx.lineTo(c.top1, 0);
|
|
5062
|
+
ctx.lineTo(c.bottom1, bottom);
|
|
5063
|
+
ctx.lineTo(c.bottom0, bottom);
|
|
5064
|
+
ctx.closePath();
|
|
5065
|
+
}
|
|
5066
|
+
function sides(ctx, c, bottom) {
|
|
5067
|
+
ctx.moveTo(c.top0, 0);
|
|
5068
|
+
ctx.lineTo(c.bottom0, bottom);
|
|
5069
|
+
ctx.moveTo(c.top1, 0);
|
|
5070
|
+
ctx.lineTo(c.bottom1, bottom);
|
|
5071
|
+
}
|
|
5072
|
+
function drawTubeMapConnectors(ctx, connectors, bottom, {
|
|
5073
|
+
highlightNode,
|
|
5074
|
+
darkMode
|
|
4922
5075
|
}) {
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4957
|
-
|
|
5076
|
+
if (bottom <= 0 || connectors.length === 0) {
|
|
5077
|
+
return;
|
|
5078
|
+
}
|
|
5079
|
+
const ink = darkMode ? "255,255,255" : "0,0,0";
|
|
5080
|
+
ctx.beginPath();
|
|
5081
|
+
for (const c of connectors) {
|
|
5082
|
+
band(ctx, c, bottom);
|
|
5083
|
+
}
|
|
5084
|
+
ctx.fillStyle = `rgba(${ink},0.07)`;
|
|
5085
|
+
ctx.fill();
|
|
5086
|
+
ctx.beginPath();
|
|
5087
|
+
for (const c of connectors) {
|
|
5088
|
+
sides(ctx, c, bottom);
|
|
5089
|
+
}
|
|
5090
|
+
ctx.strokeStyle = `rgba(${ink},0.35)`;
|
|
5091
|
+
ctx.lineWidth = 0.5;
|
|
5092
|
+
ctx.stroke();
|
|
5093
|
+
const lit = connectors.find((c) => c.node === highlightNode);
|
|
5094
|
+
if (lit) {
|
|
5095
|
+
ctx.beginPath();
|
|
5096
|
+
band(ctx, lit, bottom);
|
|
5097
|
+
ctx.fillStyle = "rgba(255,192,203,0.5)";
|
|
5098
|
+
ctx.fill();
|
|
5099
|
+
ctx.beginPath();
|
|
5100
|
+
sides(ctx, lit, bottom);
|
|
5101
|
+
ctx.strokeStyle = "#ff0000";
|
|
5102
|
+
ctx.lineWidth = 1;
|
|
5103
|
+
ctx.stroke();
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
|
|
5107
|
+
// src/walkHighlight.ts
|
|
5108
|
+
function walkHighlight(graph, name) {
|
|
5109
|
+
const path = graph.paths?.find((p) => p.name === name);
|
|
5110
|
+
if (!path) {
|
|
5111
|
+
return void 0;
|
|
5112
|
+
}
|
|
5113
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5114
|
+
const edgeAt = /* @__PURE__ */ new Map();
|
|
5115
|
+
graph.edges.forEach((e, i) => {
|
|
5116
|
+
edgeAt.set(`${e.from}>${e.to}`, i);
|
|
5117
|
+
});
|
|
5118
|
+
const nodeIds = new Set(path.nodeIds);
|
|
5119
|
+
const edgeIndexes = /* @__PURE__ */ new Set();
|
|
5120
|
+
for (let i = 1; i < path.nodeIds.length; i++) {
|
|
5121
|
+
const a = path.nodeIds[i - 1];
|
|
5122
|
+
const b = path.nodeIds[i];
|
|
5123
|
+
const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
|
|
5124
|
+
if (index !== void 0) {
|
|
5125
|
+
edgeIndexes.add(index);
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
const bpOf = (ids) => ids.reduce((sum, id) => sum + (byId.get(id)?.length ?? 0), 0);
|
|
5129
|
+
const reference = graph.referencePath ? graph.paths.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
|
|
5130
|
+
return {
|
|
5131
|
+
name,
|
|
5132
|
+
nodeIds,
|
|
5133
|
+
edgeIndexes,
|
|
5134
|
+
steps: path.nodeIds.length,
|
|
5135
|
+
bp: bpOf(path.nodeIds),
|
|
5136
|
+
referenceBp: reference && reference !== path ? bpOf(reference.nodeIds) : void 0
|
|
5137
|
+
};
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
// src/bubbles/bubbleLine.ts
|
|
5141
|
+
var SATURATED_PATH_COUNT = 2147483647;
|
|
5142
|
+
|
|
5143
|
+
// src/bubbles/bubblesFromGraph.ts
|
|
5144
|
+
function bubblesFromGraph(graph) {
|
|
5145
|
+
if (!graph.nodes.some(isBackbone)) {
|
|
5146
|
+
return [];
|
|
5147
|
+
}
|
|
5148
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5149
|
+
const order = referenceOrder(graph);
|
|
5150
|
+
const indexOf = new Map(order.map((id, i) => [id, i]));
|
|
5151
|
+
const reach = new Int32Array(order.length + 1);
|
|
5152
|
+
const succ = /* @__PURE__ */ new Map();
|
|
5153
|
+
const linked = /* @__PURE__ */ new Set();
|
|
5154
|
+
for (const e of graph.edges) {
|
|
5155
|
+
const a = indexOf.get(e.from);
|
|
5156
|
+
const b = indexOf.get(e.to);
|
|
5157
|
+
if (a === void 0 || b === void 0 || a === b) {
|
|
5158
|
+
continue;
|
|
5159
|
+
}
|
|
5160
|
+
linked.add(`${e.from}>${e.to}`).add(`${e.to}>${e.from}`);
|
|
5161
|
+
const [lo, hi] = a < b ? [a, b] : [b, a];
|
|
5162
|
+
reach[lo + 1]++;
|
|
5163
|
+
reach[hi]--;
|
|
5164
|
+
const [from, to] = a < b ? [e.from, e.to] : [e.to, e.from];
|
|
5165
|
+
(succ.get(from) ?? succ.set(from, /* @__PURE__ */ new Set()).get(from)).add(to);
|
|
5166
|
+
}
|
|
5167
|
+
for (let i = 1; i < reach.length; i++) {
|
|
5168
|
+
reach[i] += reach[i - 1];
|
|
5169
|
+
}
|
|
5170
|
+
const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
|
|
5171
|
+
const ends = /* @__PURE__ */ new Set([backbone[0].id, backbone.at(-1).id]);
|
|
5172
|
+
const boundaries = backbone.map((node) => [node, indexOf.get(node.id)]).filter(([node, i]) => ends.has(node.id) || reach[i] === 0).sort(([a, ia], [b, ib]) => ia - ib || a.stable.start - b.stable.start);
|
|
5173
|
+
const walkIndex = graph.paths?.map((p) => {
|
|
5174
|
+
const at = /* @__PURE__ */ new Map();
|
|
5175
|
+
p.nodeIds.forEach((id, i) => {
|
|
5176
|
+
if (!at.has(id)) {
|
|
5177
|
+
at.set(id, i);
|
|
4958
5178
|
}
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
});
|
|
5179
|
+
});
|
|
5180
|
+
return at;
|
|
5181
|
+
});
|
|
5182
|
+
const bubbles = [];
|
|
5183
|
+
for (let i = 0; i + 1 < boundaries.length; i++) {
|
|
5184
|
+
const [start, i0] = boundaries[i];
|
|
5185
|
+
const [end, i1] = boundaries[i + 1];
|
|
5186
|
+
const interior = order.slice(i0 + 1, i1);
|
|
5187
|
+
if (interior.length === 0) {
|
|
5188
|
+
continue;
|
|
4970
5189
|
}
|
|
5190
|
+
const best = /* @__PURE__ */ new Map([[start.id, { min: 0, max: 0, n: 1 }]]);
|
|
5191
|
+
for (const id of [start.id, ...interior]) {
|
|
5192
|
+
const cur = best.get(id);
|
|
5193
|
+
if (!cur) {
|
|
5194
|
+
continue;
|
|
5195
|
+
}
|
|
5196
|
+
for (const t of succ.get(id) ?? []) {
|
|
5197
|
+
const ti = indexOf.get(t);
|
|
5198
|
+
if (ti > i1) {
|
|
5199
|
+
continue;
|
|
5200
|
+
}
|
|
5201
|
+
const add = t === end.id ? 0 : byId.get(t).length;
|
|
5202
|
+
const prev = best.get(t);
|
|
5203
|
+
best.set(t, {
|
|
5204
|
+
min: Math.min(prev?.min ?? Infinity, cur.min + add),
|
|
5205
|
+
max: Math.max(prev?.max ?? -Infinity, cur.max + add),
|
|
5206
|
+
n: Math.min(SATURATED_PATH_COUNT, cur.n + (prev?.n ?? 0))
|
|
5207
|
+
});
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
const walked = walkIndex && walkRoutes(graph, byId, walkIndex, start.id, end.id);
|
|
5211
|
+
const crossed = walked?.n ? walked : void 0;
|
|
5212
|
+
const routes2 = crossed ?? best.get(end.id) ?? { min: 0, max: 0, n: 0 };
|
|
5213
|
+
const walksLeave = walked !== void 0 && walked.left > 0;
|
|
5214
|
+
const refStart = start.stable.start + start.length;
|
|
5215
|
+
const refEnd = end.stable.start;
|
|
5216
|
+
const chain = [
|
|
5217
|
+
start.id,
|
|
5218
|
+
...interior.filter((id) => isBackbone(byId.get(id))),
|
|
5219
|
+
end.id
|
|
5220
|
+
];
|
|
5221
|
+
const chainBroken = chain.some(
|
|
5222
|
+
(id, k) => k > 0 && !linked.has(`${chain[k - 1]}>${id}`)
|
|
5223
|
+
);
|
|
5224
|
+
const partial = chainBroken || walksLeave;
|
|
5225
|
+
const fallback = chainBroken && !crossed;
|
|
5226
|
+
bubbles.push({
|
|
5227
|
+
refName: start.stable.refName,
|
|
5228
|
+
start: refStart,
|
|
5229
|
+
end: refEnd,
|
|
5230
|
+
segmentCount: interior.length + 2,
|
|
5231
|
+
pathCount: routes2.n,
|
|
5232
|
+
// a real inversion test needs each walk's direction relative to the
|
|
5233
|
+
// reference; the first visit's strand is just whichever path anchored the
|
|
5234
|
+
// node
|
|
5235
|
+
inversion: false,
|
|
5236
|
+
shortestAlleleLength: fallback ? refEnd - refStart : routes2.min,
|
|
5237
|
+
longestAlleleLength: fallback ? refEnd - refStart : routes2.max,
|
|
5238
|
+
segments: [start, ...interior.map((id) => byId.get(id)), end].map((n) => n.name).join(","),
|
|
5239
|
+
shortestAllele: void 0,
|
|
5240
|
+
longestAllele: void 0,
|
|
5241
|
+
partial,
|
|
5242
|
+
routes: crossed?.routes
|
|
5243
|
+
});
|
|
5244
|
+
}
|
|
5245
|
+
return bubbles;
|
|
5246
|
+
}
|
|
5247
|
+
function walkRoutes(graph, byId, walkIndex, startId, endId) {
|
|
5248
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5249
|
+
let min = Infinity;
|
|
5250
|
+
let max = -Infinity;
|
|
5251
|
+
let left = 0;
|
|
5252
|
+
graph.paths.forEach((p, k) => {
|
|
5253
|
+
const i0 = walkIndex[k].get(startId);
|
|
5254
|
+
const i1 = walkIndex[k].get(endId);
|
|
5255
|
+
if (i0 === void 0 || i1 === void 0) {
|
|
5256
|
+
if (i0 !== void 0 || i1 !== void 0) {
|
|
5257
|
+
left++;
|
|
5258
|
+
}
|
|
5259
|
+
return;
|
|
5260
|
+
}
|
|
5261
|
+
const steps = p.nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
|
|
5262
|
+
if (i1 < i0) {
|
|
5263
|
+
steps.reverse();
|
|
5264
|
+
}
|
|
5265
|
+
let bp = 0;
|
|
5266
|
+
for (const id of steps) {
|
|
5267
|
+
bp += byId.get(id)?.length ?? 0;
|
|
5268
|
+
}
|
|
5269
|
+
const key = steps.join(",");
|
|
5270
|
+
const route = seen.get(key) ?? { steps, bp, walks: [] };
|
|
5271
|
+
route.walks.push(p.name);
|
|
5272
|
+
seen.set(key, route);
|
|
5273
|
+
min = Math.min(min, bp);
|
|
5274
|
+
max = Math.max(max, bp);
|
|
5275
|
+
});
|
|
5276
|
+
return seen.size || left ? { min, max, n: seen.size, left, routes: [...seen.values()] } : void 0;
|
|
5277
|
+
}
|
|
5278
|
+
|
|
5279
|
+
// src/bubbles/classifyBubble.ts
|
|
5280
|
+
var REPEAT_GROWTH = 10;
|
|
5281
|
+
function coveringRepeat(b, repeats) {
|
|
5282
|
+
const span = b.end - b.start;
|
|
5283
|
+
const growth = b.longestAlleleLength - b.shortestAlleleLength;
|
|
5284
|
+
return repeats.find(
|
|
5285
|
+
(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)
|
|
5286
|
+
);
|
|
5287
|
+
}
|
|
5288
|
+
var SUPERBUBBLE_SEGMENTS = 40;
|
|
5289
|
+
function formatBp2(bp) {
|
|
5290
|
+
if (bp >= 1e4) {
|
|
5291
|
+
return `${(bp / 1e3).toFixed(0)} kb`;
|
|
5292
|
+
}
|
|
5293
|
+
if (bp >= 1e3) {
|
|
5294
|
+
return `${(bp / 1e3).toFixed(1)} kb`;
|
|
5295
|
+
}
|
|
5296
|
+
return `${bp} bp`;
|
|
5297
|
+
}
|
|
5298
|
+
function formatBpRange(shortest, longest) {
|
|
5299
|
+
const low = formatBp2(shortest);
|
|
5300
|
+
const high = formatBp2(longest);
|
|
5301
|
+
const unit = high.slice(high.indexOf(" "));
|
|
5302
|
+
return low.endsWith(unit) ? `${low.slice(0, -unit.length)}\u2013${high}` : `${low}\u2013${high}`;
|
|
5303
|
+
}
|
|
5304
|
+
function routes(count) {
|
|
5305
|
+
if (count >= SATURATED_PATH_COUNT) {
|
|
5306
|
+
return "\u22652.1B routes";
|
|
5307
|
+
}
|
|
5308
|
+
if (count >= 1e5) {
|
|
5309
|
+
return `${(count / 1e3).toFixed(0)}k routes`;
|
|
5310
|
+
}
|
|
5311
|
+
return `${count} routes`;
|
|
5312
|
+
}
|
|
5313
|
+
function classifyBubble(b, repeats = []) {
|
|
5314
|
+
const c = classifyShape(b, repeats);
|
|
5315
|
+
return b.partial ? { ...c, label: `${c.label}, partial` } : c;
|
|
5316
|
+
}
|
|
5317
|
+
function classifyShape(b, repeats) {
|
|
5318
|
+
const refSpan = b.end - b.start;
|
|
5319
|
+
const {
|
|
5320
|
+
shortestAlleleLength: shortest,
|
|
5321
|
+
longestAlleleLength: longest,
|
|
5322
|
+
pathCount,
|
|
5323
|
+
segmentCount,
|
|
5324
|
+
inversion
|
|
5325
|
+
} = b;
|
|
5326
|
+
const range = formatBpRange(shortest, longest);
|
|
5327
|
+
if (segmentCount >= SUPERBUBBLE_SEGMENTS) {
|
|
5328
|
+
return {
|
|
5329
|
+
kind: "superbubble",
|
|
5330
|
+
label: `${range} superbubble, ${segmentCount} segments, ${routes(pathCount)}${inversion ? ", inv" : ""}`
|
|
5331
|
+
};
|
|
4971
5332
|
}
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
for (const run of alleleRuns) {
|
|
4975
|
-
const drawn = runBounds(run.nodeIds, nodePositions);
|
|
4976
|
-
if (!drawn) {
|
|
4977
|
-
continue;
|
|
4978
|
-
}
|
|
4979
|
-
const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
|
|
4980
|
-
const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
|
|
4981
|
-
const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
|
|
4982
|
-
const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
|
|
4983
|
-
if (shownRight < shownLeft || shownBottom < shownTop) {
|
|
4984
|
-
continue;
|
|
4985
|
-
}
|
|
4986
|
-
const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
|
|
4987
|
-
const text = deletionText(run.bp);
|
|
4988
|
-
const halfW = labelHalfWidth(text);
|
|
4989
|
-
if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
|
|
4990
|
-
for (const id of run.nodeIds) {
|
|
4991
|
-
labelledByDeletion.add(id);
|
|
4992
|
-
}
|
|
4993
|
-
const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
|
|
4994
|
-
const y = clamp(
|
|
4995
|
-
(shownTop + shownBottom) / 2,
|
|
4996
|
-
LABEL_HALF_HEIGHT,
|
|
4997
|
-
height - LABEL_HALF_HEIGHT
|
|
4998
|
-
);
|
|
4999
|
-
deletionCandidates.push({
|
|
5000
|
-
label: {
|
|
5001
|
-
key: `alleledel:${run.nodeIds.join(",")}`,
|
|
5002
|
-
text,
|
|
5003
|
-
x,
|
|
5004
|
-
y,
|
|
5005
|
-
kind: "deletion"
|
|
5006
|
-
},
|
|
5007
|
-
box: boxAt(halfW, x, y)
|
|
5008
|
-
});
|
|
5009
|
-
}
|
|
5333
|
+
if (inversion) {
|
|
5334
|
+
return { kind: "inversion", label: `${formatBp2(longest)} inv` };
|
|
5010
5335
|
}
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5336
|
+
const repeat = longest !== shortest ? coveringRepeat(b, repeats) : void 0;
|
|
5337
|
+
if (repeat) {
|
|
5338
|
+
const name = repeat.name ? ` (${repeat.name})` : "";
|
|
5339
|
+
return {
|
|
5340
|
+
kind: "repeat",
|
|
5341
|
+
label: `${range} repeat array${name}, ${routes(pathCount)}`
|
|
5342
|
+
};
|
|
5343
|
+
}
|
|
5344
|
+
const alleles = pathCount > 2 ? `, ${pathCount} alleles` : "";
|
|
5345
|
+
if (shortest === longest && shortest === refSpan) {
|
|
5346
|
+
return refSpan <= 1 ? { kind: "snp", label: "SNP" } : { kind: "substitution", label: `${formatBp2(refSpan)} sub` };
|
|
5347
|
+
}
|
|
5348
|
+
if (refSpan === 0 || shortest === refSpan) {
|
|
5349
|
+
return {
|
|
5350
|
+
kind: "insertion",
|
|
5351
|
+
label: `\u2264${formatBp2(longest - refSpan)} ins${alleles}`
|
|
5352
|
+
};
|
|
5353
|
+
}
|
|
5354
|
+
if (longest === refSpan) {
|
|
5355
|
+
return {
|
|
5356
|
+
kind: "deletion",
|
|
5357
|
+
label: `${formatBp2(refSpan - shortest)} del${alleles}`
|
|
5358
|
+
};
|
|
5359
|
+
}
|
|
5360
|
+
return {
|
|
5361
|
+
kind: "complex",
|
|
5362
|
+
label: `${formatBp2(refSpan)} ref \u2192 ${range}, ${pathCount} alleles`
|
|
5363
|
+
};
|
|
5364
|
+
}
|
|
5365
|
+
var BUBBLE_KIND_COLORS = {
|
|
5366
|
+
snp: "#5b6b7a",
|
|
5367
|
+
substitution: "#5b6b7a",
|
|
5368
|
+
insertion: "#2f8fd6",
|
|
5369
|
+
deletion: "#c94040",
|
|
5370
|
+
inversion: "#e07b00",
|
|
5371
|
+
repeat: "#8e3fbf",
|
|
5372
|
+
superbubble: "#8e3fbf",
|
|
5373
|
+
complex: "#8e3fbf"
|
|
5374
|
+
};
|
|
5375
|
+
var BUBBLE_KIND_NAMES = {
|
|
5376
|
+
snp: "SNP",
|
|
5377
|
+
substitution: "substitution",
|
|
5378
|
+
insertion: "insertion",
|
|
5379
|
+
deletion: "deletion",
|
|
5380
|
+
inversion: "inversion",
|
|
5381
|
+
repeat: "repeat array",
|
|
5382
|
+
superbubble: "superbubble",
|
|
5383
|
+
complex: "site"
|
|
5384
|
+
};
|
|
5385
|
+
function bubbleSegmentIds(b) {
|
|
5386
|
+
return b.segments.split(",").filter(Boolean);
|
|
5387
|
+
}
|
|
5388
|
+
|
|
5389
|
+
// src/bubbles/bubbleHalos.ts
|
|
5390
|
+
var WHOLE_FRACTION = 0.9;
|
|
5391
|
+
var NAMED_WALKS = 2;
|
|
5392
|
+
var MIN_CHIPPED_BP = 1e3;
|
|
5393
|
+
function bubbleHalos(graph, bubbles, positions, walkLabel = (name) => name, repeats = []) {
|
|
5394
|
+
const byName = new Map(graph.nodes.map((n) => [n.name, n]));
|
|
5395
|
+
const halos = [];
|
|
5396
|
+
for (const bubble of bubbles) {
|
|
5397
|
+
const parts = [];
|
|
5398
|
+
let minX = Infinity;
|
|
5399
|
+
let maxX = -Infinity;
|
|
5400
|
+
let minY = Infinity;
|
|
5401
|
+
const nodeIds = [];
|
|
5402
|
+
const ends = [];
|
|
5403
|
+
for (const name of bubbleSegmentIds(bubble)) {
|
|
5404
|
+
const node = byName.get(name);
|
|
5405
|
+
const line = node && positions[node.id];
|
|
5406
|
+
if (!node || !line?.length) {
|
|
5407
|
+
continue;
|
|
5408
|
+
}
|
|
5409
|
+
if (isBackbone(node) && (node.stable.start < bubble.start || node.stable.start >= bubble.end)) {
|
|
5410
|
+
ends.push(line[Math.floor(line.length / 2)]);
|
|
5411
|
+
continue;
|
|
5412
|
+
}
|
|
5413
|
+
nodeIds.push(node.id);
|
|
5414
|
+
parts.push(svgPath(line.length === 1 ? [line[0], line[0]] : line));
|
|
5415
|
+
for (const p of line) {
|
|
5416
|
+
minX = Math.min(minX, p.x);
|
|
5417
|
+
maxX = Math.max(maxX, p.x);
|
|
5418
|
+
minY = Math.min(minY, p.y);
|
|
5419
|
+
}
|
|
5014
5420
|
}
|
|
5015
|
-
|
|
5016
|
-
const screenY = y * scaleY + translateY;
|
|
5017
|
-
if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
|
|
5421
|
+
if (nodeIds.length === 0) {
|
|
5018
5422
|
continue;
|
|
5019
5423
|
}
|
|
5020
|
-
const
|
|
5021
|
-
|
|
5022
|
-
|
|
5424
|
+
const labelAt = { x: (minX + maxX) / 2, y: minY };
|
|
5425
|
+
const anchor = ends.length ? {
|
|
5426
|
+
x: ends.reduce((s, p) => s + p.x, 0) / ends.length,
|
|
5427
|
+
y: ends.reduce((s, p) => s + p.y, 0) / ends.length
|
|
5428
|
+
} : labelAt;
|
|
5429
|
+
const sharing = /* @__PURE__ */ new Map();
|
|
5430
|
+
for (const route of bubble.routes ?? []) {
|
|
5431
|
+
for (const id of new Set(route.steps)) {
|
|
5432
|
+
sharing.set(id, (sharing.get(id) ?? 0) + 1);
|
|
5433
|
+
}
|
|
5023
5434
|
}
|
|
5024
|
-
const
|
|
5025
|
-
const
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5435
|
+
const routes2 = [];
|
|
5436
|
+
for (const route of bubble.routes ?? []) {
|
|
5437
|
+
if (bubble.longestAlleleLength < MIN_CHIPPED_BP) {
|
|
5438
|
+
break;
|
|
5439
|
+
}
|
|
5440
|
+
if (route.steps.length === 0) {
|
|
5441
|
+
continue;
|
|
5442
|
+
}
|
|
5443
|
+
const rarest = Math.min(...route.steps.map((id) => sharing.get(id)));
|
|
5444
|
+
let at;
|
|
5445
|
+
let far = -1;
|
|
5446
|
+
for (const id of route.steps) {
|
|
5447
|
+
if (sharing.get(id) !== rarest) {
|
|
5448
|
+
continue;
|
|
5449
|
+
}
|
|
5450
|
+
for (const p of positions[id] ?? []) {
|
|
5451
|
+
const d = Math.hypot(p.x - anchor.x, p.y - anchor.y);
|
|
5452
|
+
if (d > far) {
|
|
5453
|
+
far = d;
|
|
5454
|
+
at = p;
|
|
5455
|
+
}
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
if (at) {
|
|
5459
|
+
routes2.push({ route, at, text: routeText(route, walkLabel) });
|
|
5460
|
+
}
|
|
5038
5461
|
}
|
|
5462
|
+
halos.push({
|
|
5463
|
+
bubble,
|
|
5464
|
+
...classifyBubble(bubble, repeats),
|
|
5465
|
+
path: parts.join(""),
|
|
5466
|
+
labelAt,
|
|
5467
|
+
members: nodeIds.length,
|
|
5468
|
+
nodeIds,
|
|
5469
|
+
whole: nodeIds.length >= WHOLE_FRACTION * graph.nodes.length,
|
|
5470
|
+
routes: routes2
|
|
5471
|
+
});
|
|
5039
5472
|
}
|
|
5040
|
-
return
|
|
5473
|
+
return halos;
|
|
5041
5474
|
}
|
|
5042
|
-
function
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
)
|
|
5475
|
+
function routeText(route, walkLabel) {
|
|
5476
|
+
const names = [...new Set(route.walks.map(walkLabel))].sort();
|
|
5477
|
+
const shown = names.slice(0, NAMED_WALKS).join(", ");
|
|
5478
|
+
const more = names.length > NAMED_WALKS ? ` +${names.length - NAMED_WALKS}` : "";
|
|
5479
|
+
return `${shown}${more} \xB7 ${formatBp2(route.bp)}`;
|
|
5047
5480
|
}
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5481
|
+
|
|
5482
|
+
// src/bubbles/popBubble.ts
|
|
5483
|
+
function bubbleSubgraph(graph, segmentIds) {
|
|
5484
|
+
const keep = new Set(segmentIds);
|
|
5485
|
+
const nodes = graph.nodes.filter((n) => keep.has(n.name));
|
|
5486
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
5487
|
+
const paths = graph.paths?.map((p) => ({ ...p, nodeIds: p.nodeIds.filter((id) => ids.has(id)) })).filter((p) => p.nodeIds.length > 0);
|
|
5488
|
+
return {
|
|
5489
|
+
name: graph.name,
|
|
5490
|
+
nodes,
|
|
5491
|
+
edges: graph.edges.filter((e) => ids.has(e.from) && ids.has(e.to)),
|
|
5492
|
+
...paths?.length ? { paths } : {},
|
|
5493
|
+
anchorPaths: graph.anchorPaths,
|
|
5494
|
+
pathVisits: graph.pathVisits,
|
|
5495
|
+
anchoredBy: graph.anchoredBy,
|
|
5496
|
+
referencePath: graph.referencePath
|
|
5497
|
+
};
|
|
5059
5498
|
}
|
|
5060
5499
|
|
|
5061
5500
|
// src/labelLayout.ts
|
|
@@ -5202,6 +5641,87 @@ function layoutLabels(m) {
|
|
|
5202
5641
|
};
|
|
5203
5642
|
}
|
|
5204
5643
|
|
|
5644
|
+
// src/genes/genePins.ts
|
|
5645
|
+
function genePins(graph, genes, positions) {
|
|
5646
|
+
const byRefName = /* @__PURE__ */ new Map();
|
|
5647
|
+
const refNames = /* @__PURE__ */ new Set();
|
|
5648
|
+
for (const node of graph.nodes) {
|
|
5649
|
+
if (isBackbone(node)) {
|
|
5650
|
+
const { refName } = node.stable;
|
|
5651
|
+
refNames.add(refName);
|
|
5652
|
+
if (positions[node.id]?.length) {
|
|
5653
|
+
const entry = byRefName.get(refName) ?? byRefName.set(refName, { nodes: [], reach: 0 }).get(refName);
|
|
5654
|
+
entry.nodes.push(node);
|
|
5655
|
+
entry.reach = Math.max(entry.reach, node.length);
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
}
|
|
5659
|
+
for (const { nodes } of byRefName.values()) {
|
|
5660
|
+
nodes.sort((a, b) => a.stable.start - b.stable.start);
|
|
5661
|
+
}
|
|
5662
|
+
const bind = refNameBinding(refNames);
|
|
5663
|
+
const pins = [];
|
|
5664
|
+
for (const gene of genes) {
|
|
5665
|
+
const refName = bind(gene.refName);
|
|
5666
|
+
const backbone = refName === void 0 ? void 0 : byRefName.get(refName);
|
|
5667
|
+
if (!backbone) {
|
|
5668
|
+
continue;
|
|
5669
|
+
}
|
|
5670
|
+
const parts = [];
|
|
5671
|
+
let at;
|
|
5672
|
+
let atDistance = Infinity;
|
|
5673
|
+
let covered = 0;
|
|
5674
|
+
const mid = (gene.start + gene.end) / 2;
|
|
5675
|
+
const { nodes, reach } = backbone;
|
|
5676
|
+
for (let i = firstNodeAtOrAfter(nodes, gene.start - reach); i < nodes.length && nodes[i].stable.start < gene.end; i++) {
|
|
5677
|
+
const node = nodes[i];
|
|
5678
|
+
const nodeStart = node.stable.start;
|
|
5679
|
+
const nodeEnd = nodeStart + node.length;
|
|
5680
|
+
if (nodeEnd <= gene.start) {
|
|
5681
|
+
continue;
|
|
5682
|
+
}
|
|
5683
|
+
const line = positions[node.id];
|
|
5684
|
+
covered += Math.min(nodeEnd, gene.end) - Math.max(nodeStart, gene.start);
|
|
5685
|
+
for (const exon of gene.exons) {
|
|
5686
|
+
const a = Math.max(exon.start, nodeStart);
|
|
5687
|
+
const b = Math.min(exon.end, nodeEnd);
|
|
5688
|
+
if (b <= a) {
|
|
5689
|
+
continue;
|
|
5690
|
+
}
|
|
5691
|
+
const stretch = polylineSlice(
|
|
5692
|
+
line,
|
|
5693
|
+
(a - nodeStart) / node.length,
|
|
5694
|
+
(b - nodeStart) / node.length
|
|
5695
|
+
);
|
|
5696
|
+
if (stretch.length === 1) {
|
|
5697
|
+
stretch.push({ ...stretch[0] });
|
|
5698
|
+
}
|
|
5699
|
+
parts.push(svgPath(stretch));
|
|
5700
|
+
}
|
|
5701
|
+
const pinBp = Math.min(Math.max(mid, nodeStart), nodeEnd);
|
|
5702
|
+
const distance = Math.abs(pinBp - mid);
|
|
5703
|
+
if (distance < atDistance) {
|
|
5704
|
+
atDistance = distance;
|
|
5705
|
+
const [p] = polylineSlice(
|
|
5706
|
+
line,
|
|
5707
|
+
(pinBp - nodeStart) / node.length,
|
|
5708
|
+
(pinBp - nodeStart) / node.length
|
|
5709
|
+
);
|
|
5710
|
+
at = p;
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
if (at) {
|
|
5714
|
+
pins.push({
|
|
5715
|
+
gene,
|
|
5716
|
+
exons: parts.join(""),
|
|
5717
|
+
at,
|
|
5718
|
+
covered: covered / (gene.end - gene.start)
|
|
5719
|
+
});
|
|
5720
|
+
}
|
|
5721
|
+
}
|
|
5722
|
+
return pins;
|
|
5723
|
+
}
|
|
5724
|
+
|
|
5205
5725
|
// src/gbzWindow.ts
|
|
5206
5726
|
var NoReferenceSampleError = class extends Error {
|
|
5207
5727
|
constructor(anchor, referenceSamples) {
|
|
@@ -5229,16 +5749,9 @@ function haplotypeWanted(name, wanted) {
|
|
|
5229
5749
|
async function referenceSamplesOf(db) {
|
|
5230
5750
|
return (await db.tag("gbwt_reference_samples") ?? "").split(/\s+/).filter((sample) => sample !== "");
|
|
5231
5751
|
}
|
|
5232
|
-
var SAMPLE_ALIASES = {
|
|
5233
|
-
hg38: "grch38",
|
|
5234
|
-
hg19: "grch37",
|
|
5235
|
-
hs1: "chm13",
|
|
5236
|
-
"t2t-chm13": "chm13",
|
|
5237
|
-
chm13v2: "chm13"
|
|
5238
|
-
};
|
|
5239
5752
|
function aliasedSample(anchorSample, referenceSamples) {
|
|
5240
5753
|
const lower = anchorSample.toLowerCase();
|
|
5241
|
-
const alias =
|
|
5754
|
+
const alias = wellKnownSample(anchorSample)?.toLowerCase();
|
|
5242
5755
|
return referenceSamples.find((s) => s === anchorSample) ?? referenceSamples.find((s) => s.toLowerCase() === lower) ?? referenceSamples.find((s) => s.toLowerCase() === alias);
|
|
5243
5756
|
}
|
|
5244
5757
|
function resolveReferenceSample({
|
|
@@ -5353,7 +5866,9 @@ export {
|
|
|
5353
5866
|
NODE_WIDTHS,
|
|
5354
5867
|
REFERENCE_RAMP_MAX_HUE,
|
|
5355
5868
|
ROW_HEIGHT_PX,
|
|
5869
|
+
WELL_KNOWN_SAMPLES,
|
|
5356
5870
|
axisScaleOf,
|
|
5871
|
+
backboneAssembly,
|
|
5357
5872
|
bubbleHalos,
|
|
5358
5873
|
bubbleSegmentIds,
|
|
5359
5874
|
bubbleSubgraph,
|
|
@@ -5362,19 +5877,26 @@ export {
|
|
|
5362
5877
|
clampZoom,
|
|
5363
5878
|
classifyBubble,
|
|
5364
5879
|
computeReferenceRamp,
|
|
5880
|
+
connectorAt,
|
|
5365
5881
|
contains,
|
|
5366
5882
|
cutWindowGFA,
|
|
5367
5883
|
deletionEdges,
|
|
5368
5884
|
drawTubeMap,
|
|
5885
|
+
drawTubeMapConnectors,
|
|
5886
|
+
drawTubeMapGenes,
|
|
5887
|
+
drawTubeMapRuler,
|
|
5369
5888
|
drawingBounds,
|
|
5370
5889
|
engineKey,
|
|
5371
5890
|
engineRequest,
|
|
5891
|
+
featuresOnBackbone,
|
|
5372
5892
|
findHoveredEdge,
|
|
5373
5893
|
findHoveredNode,
|
|
5374
5894
|
fitTransform,
|
|
5375
5895
|
forceLayout,
|
|
5376
|
-
|
|
5896
|
+
formatBp,
|
|
5897
|
+
genePins,
|
|
5377
5898
|
getDpr,
|
|
5899
|
+
graphBackbone,
|
|
5378
5900
|
haplotypeWanted,
|
|
5379
5901
|
layoutExtent,
|
|
5380
5902
|
layoutLabels,
|
|
@@ -5392,20 +5914,28 @@ export {
|
|
|
5392
5914
|
panSNSample,
|
|
5393
5915
|
pathColorsLegible,
|
|
5394
5916
|
pathLegend,
|
|
5917
|
+
refNameBinding,
|
|
5918
|
+
referenceBoxes,
|
|
5395
5919
|
referenceKnots,
|
|
5920
|
+
referenceNodes,
|
|
5396
5921
|
referencePathQuery,
|
|
5397
5922
|
referenceSamplesOf,
|
|
5398
5923
|
resolveColorScheme,
|
|
5399
5924
|
resolveReferenceSample,
|
|
5925
|
+
rulerBoxes,
|
|
5400
5926
|
screenToLayout,
|
|
5927
|
+
tubeMapConnectors,
|
|
5401
5928
|
tubeMapFrame,
|
|
5929
|
+
tubeMapGenes,
|
|
5402
5930
|
tubeMapNodeAt,
|
|
5403
5931
|
tubeMapPicture,
|
|
5932
|
+
tubeX,
|
|
5404
5933
|
viewportOf,
|
|
5405
5934
|
walkHighlight,
|
|
5406
5935
|
walkRows,
|
|
5407
5936
|
walkRowsExtent,
|
|
5408
5937
|
warpX,
|
|
5938
|
+
wellKnownSample,
|
|
5409
5939
|
wheelZoomFactor,
|
|
5410
5940
|
zoomAbout
|
|
5411
5941
|
};
|