@jbrowse/bandage-core 0.1.5 → 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 +1126 -685
- package/dist/types/index.d.ts +8 -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
|
@@ -269,12 +269,12 @@ function parseGFA(file) {
|
|
|
269
269
|
});
|
|
270
270
|
}
|
|
271
271
|
} else if (line.startsWith("W")) {
|
|
272
|
-
const [, sample, hap,
|
|
273
|
-
if (sample && hap &&
|
|
272
|
+
const [, sample, hap, contig, start, end, body, ...rest] = line.split(" ");
|
|
273
|
+
if (sample && hap && contig && start && end && body) {
|
|
274
274
|
graph.walks.push({
|
|
275
275
|
sample,
|
|
276
276
|
haplotype: +hap,
|
|
277
|
-
contig
|
|
277
|
+
contig,
|
|
278
278
|
start: start === "*" ? -1 : +start,
|
|
279
279
|
end: end === "*" ? -1 : +end,
|
|
280
280
|
segments: parseWalkBody(body),
|
|
@@ -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;
|
|
@@ -975,18 +975,21 @@ function fittedTranslateY(bounds, usableHeight, yScale) {
|
|
|
975
975
|
const leftoverY = usableHeight - bounds.h * yScale;
|
|
976
976
|
return FIT_PADDING - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
|
|
977
977
|
}
|
|
978
|
-
function fitTransform(bounds, paneWidth, paneHeight, pixelRows) {
|
|
979
|
-
const usableWidth = paneWidth -
|
|
978
|
+
function fitTransform(bounds, paneWidth, paneHeight, pixelRows, { minScale = 0, padLeft = FIT_PADDING } = {}) {
|
|
979
|
+
const usableWidth = paneWidth - padLeft - FIT_PADDING;
|
|
980
980
|
const usableHeight = paneHeight - FIT_PADDING * 2;
|
|
981
981
|
if (usableWidth <= 0 || usableHeight <= 0 || !(bounds.w > 0 || !pixelRows && bounds.h > 0)) {
|
|
982
982
|
return void 0;
|
|
983
983
|
}
|
|
984
984
|
const fitX = bounds.w > 0 ? usableWidth / bounds.w : Infinity;
|
|
985
985
|
const fitY = bounds.h > 0 ? usableHeight / bounds.h : Infinity;
|
|
986
|
-
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;
|
|
987
990
|
return {
|
|
988
991
|
scale,
|
|
989
|
-
translateX:
|
|
992
|
+
translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
|
|
990
993
|
translateY: fittedTranslateY(bounds, usableHeight, pixelRows ? 1 : scale)
|
|
991
994
|
};
|
|
992
995
|
}
|
|
@@ -2010,7 +2013,6 @@ function walkRows(graph, region, unit) {
|
|
|
2010
2013
|
return void 0;
|
|
2011
2014
|
}
|
|
2012
2015
|
const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
|
|
2013
|
-
const onReference = new Set(reference.nodeIds);
|
|
2014
2016
|
const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
|
|
2015
2017
|
const cut = region && region.end > region.start ? region : void 0;
|
|
2016
2018
|
const span = /* @__PURE__ */ new Map();
|
|
@@ -2028,17 +2030,34 @@ function walkRows(graph, region, unit) {
|
|
|
2028
2030
|
const runs = [];
|
|
2029
2031
|
let bp = 0;
|
|
2030
2032
|
let offReferenceBp = 0;
|
|
2033
|
+
let step = 0;
|
|
2031
2034
|
for (const id of ids) {
|
|
2032
2035
|
const len = lengthOf.get(id) ?? 0;
|
|
2033
|
-
const
|
|
2036
|
+
const s = span.get(id);
|
|
2034
2037
|
const last = runs.at(-1);
|
|
2035
|
-
|
|
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) {
|
|
2036
2044
|
last.bp += len;
|
|
2045
|
+
if (!forward2) {
|
|
2046
|
+
last.referenceStart = s.start;
|
|
2047
|
+
last.reversed = true;
|
|
2048
|
+
}
|
|
2049
|
+
step = forward2 ? 1 : -1;
|
|
2037
2050
|
} else {
|
|
2038
|
-
runs.push({
|
|
2051
|
+
runs.push({
|
|
2052
|
+
start: bp,
|
|
2053
|
+
bp: len,
|
|
2054
|
+
onReference: s !== void 0,
|
|
2055
|
+
referenceStart: s?.start
|
|
2056
|
+
});
|
|
2057
|
+
step = 0;
|
|
2039
2058
|
}
|
|
2040
2059
|
bp += len;
|
|
2041
|
-
if (!
|
|
2060
|
+
if (!s) {
|
|
2042
2061
|
offReferenceBp += len;
|
|
2043
2062
|
}
|
|
2044
2063
|
}
|
|
@@ -2574,9 +2593,9 @@ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY,
|
|
|
2574
2593
|
offsetY
|
|
2575
2594
|
);
|
|
2576
2595
|
}
|
|
2577
|
-
const
|
|
2578
|
-
const fromAttach = attachment(fromSegments,
|
|
2579
|
-
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);
|
|
2580
2599
|
const fromEnd = fromAttach.at;
|
|
2581
2600
|
const toStart = toAttach.at;
|
|
2582
2601
|
const p1x = fromEnd.x + offsetX;
|
|
@@ -3066,7 +3085,9 @@ function hashColor(str, alpha) {
|
|
|
3066
3085
|
var REFERENCE_RAMP_MAX_HUE = 300;
|
|
3067
3086
|
var REFERENCE_RAMP_SATURATION = 0.7;
|
|
3068
3087
|
var REFERENCE_RAMP_LIGHTNESS = 0.5;
|
|
3069
|
-
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(", ")})`;
|
|
3070
3091
|
function computeReferenceRamp(graph, domain) {
|
|
3071
3092
|
const midpoints = referenceMidpoints(graph);
|
|
3072
3093
|
if (domain && domain.end > domain.start) {
|
|
@@ -4123,6 +4144,7 @@ function tubeMapPicture(layout) {
|
|
|
4123
4144
|
}
|
|
4124
4145
|
});
|
|
4125
4146
|
return {
|
|
4147
|
+
bounds: layout.bounds,
|
|
4126
4148
|
layers: [layerOf(layout, "haplotype"), layerOf(layout, "read")],
|
|
4127
4149
|
nodes,
|
|
4128
4150
|
mismatches: tubeMapMismatches(layout).filter(
|
|
@@ -4193,6 +4215,11 @@ function fillByColor(ctx, items, visible, addTo) {
|
|
|
4193
4215
|
}
|
|
4194
4216
|
ctx.globalAlpha = 1;
|
|
4195
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
|
+
}
|
|
4196
4223
|
function drawTubeMap(ctx, picture, frame) {
|
|
4197
4224
|
const { x, y, width } = frame;
|
|
4198
4225
|
const visible = (item) => x(item.x1) >= 0 && x(item.x0) <= width;
|
|
@@ -4217,7 +4244,9 @@ function drawTubeMap(ctx, picture, frame) {
|
|
|
4217
4244
|
ctx.fillStyle = lit ? "rgba(255,192,203,0.5)" : fill;
|
|
4218
4245
|
ctx.strokeStyle = lit ? "#ff0000" : stroke;
|
|
4219
4246
|
ctx.fill();
|
|
4247
|
+
ctx.globalAlpha = lit ? 1 : outlineAlpha(x(node.x1) - x(node.x0));
|
|
4220
4248
|
ctx.stroke();
|
|
4249
|
+
ctx.globalAlpha = 1;
|
|
4221
4250
|
}
|
|
4222
4251
|
}
|
|
4223
4252
|
drawMismatches(ctx, picture.mismatches, frame);
|
|
@@ -4350,720 +4379,1122 @@ function tubeMapNodeAt(drawing, { x, y }, sx, sy) {
|
|
|
4350
4379
|
return hit;
|
|
4351
4380
|
}
|
|
4352
4381
|
|
|
4353
|
-
// src/
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
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;
|
|
4372
4403
|
}
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
const reference = graph.referencePath ? graph.paths.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
|
|
4376
|
-
return {
|
|
4377
|
-
name,
|
|
4378
|
-
nodeIds,
|
|
4379
|
-
edgeIndexes,
|
|
4380
|
-
steps: path.nodeIds.length,
|
|
4381
|
-
bp: bpOf(path.nodeIds),
|
|
4382
|
-
referenceBp: reference && reference !== path ? bpOf(reference.nodeIds) : void 0
|
|
4404
|
+
placed.push(box);
|
|
4405
|
+
return true;
|
|
4383
4406
|
};
|
|
4384
4407
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
const a = indexOf.get(e.from);
|
|
4402
|
-
const b = indexOf.get(e.to);
|
|
4403
|
-
if (a === void 0 || b === void 0 || a === b) {
|
|
4404
|
-
continue;
|
|
4405
|
-
}
|
|
4406
|
-
linked.add(`${e.from}>${e.to}`).add(`${e.to}>${e.from}`);
|
|
4407
|
-
const [lo, hi] = a < b ? [a, b] : [b, a];
|
|
4408
|
-
reach[lo + 1]++;
|
|
4409
|
-
reach[hi]--;
|
|
4410
|
-
const [from, to] = a < b ? [e.from, e.to] : [e.to, e.from];
|
|
4411
|
-
(succ.get(from) ?? succ.set(from, /* @__PURE__ */ new Set()).get(from)).add(to);
|
|
4412
|
-
}
|
|
4413
|
-
for (let i = 1; i < reach.length; i++) {
|
|
4414
|
-
reach[i] += reach[i - 1];
|
|
4415
|
-
}
|
|
4416
|
-
const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
|
|
4417
|
-
const ends = /* @__PURE__ */ new Set([backbone[0].id, backbone.at(-1).id]);
|
|
4418
|
-
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);
|
|
4419
|
-
const walkIndex = graph.paths?.map((p) => {
|
|
4420
|
-
const at = /* @__PURE__ */ new Map();
|
|
4421
|
-
p.nodeIds.forEach((id, i) => {
|
|
4422
|
-
if (!at.has(id)) {
|
|
4423
|
-
at.set(id, i);
|
|
4424
|
-
}
|
|
4425
|
-
});
|
|
4426
|
-
return at;
|
|
4427
|
-
});
|
|
4428
|
-
const bubbles = [];
|
|
4429
|
-
for (let i = 0; i + 1 < boundaries.length; i++) {
|
|
4430
|
-
const [start, i0] = boundaries[i];
|
|
4431
|
-
const [end, i1] = boundaries[i + 1];
|
|
4432
|
-
const interior = order.slice(i0 + 1, i1);
|
|
4433
|
-
if (interior.length === 0) {
|
|
4434
|
-
continue;
|
|
4435
|
-
}
|
|
4436
|
-
const best = /* @__PURE__ */ new Map([[start.id, { min: 0, max: 0, n: 1 }]]);
|
|
4437
|
-
for (const id of [start.id, ...interior]) {
|
|
4438
|
-
const cur = best.get(id);
|
|
4439
|
-
if (!cur) {
|
|
4440
|
-
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;
|
|
4441
4424
|
}
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
continue;
|
|
4446
|
-
}
|
|
4447
|
-
const add = t === end.id ? 0 : byId.get(t).length;
|
|
4448
|
-
const prev = best.get(t);
|
|
4449
|
-
best.set(t, {
|
|
4450
|
-
min: Math.min(prev?.min ?? Infinity, cur.min + add),
|
|
4451
|
-
max: Math.max(prev?.max ?? -Infinity, cur.max + add),
|
|
4452
|
-
n: Math.min(SATURATED_PATH_COUNT, cur.n + (prev?.n ?? 0))
|
|
4453
|
-
});
|
|
4425
|
+
if (insideFrame(box, frame) && take(box)) {
|
|
4426
|
+
out.push({ ...c, y, w });
|
|
4427
|
+
break;
|
|
4454
4428
|
}
|
|
4455
4429
|
}
|
|
4456
|
-
const walked = walkIndex && walkRoutes(graph, byId, walkIndex, start.id, end.id);
|
|
4457
|
-
const crossed = walked?.n ? walked : void 0;
|
|
4458
|
-
const routes2 = crossed ?? best.get(end.id) ?? { min: 0, max: 0, n: 0 };
|
|
4459
|
-
const walksLeave = walked !== void 0 && walked.left > 0;
|
|
4460
|
-
const refStart = start.stable.start + start.length;
|
|
4461
|
-
const refEnd = end.stable.start;
|
|
4462
|
-
const chain = [
|
|
4463
|
-
start.id,
|
|
4464
|
-
...interior.filter((id) => isBackbone(byId.get(id))),
|
|
4465
|
-
end.id
|
|
4466
|
-
];
|
|
4467
|
-
const chainBroken = chain.some(
|
|
4468
|
-
(id, k) => k > 0 && !linked.has(`${chain[k - 1]}>${id}`)
|
|
4469
|
-
);
|
|
4470
|
-
const partial = chainBroken || walksLeave;
|
|
4471
|
-
const fallback = chainBroken && !crossed;
|
|
4472
|
-
bubbles.push({
|
|
4473
|
-
refName: start.stable.refName,
|
|
4474
|
-
start: refStart,
|
|
4475
|
-
end: refEnd,
|
|
4476
|
-
segmentCount: interior.length + 2,
|
|
4477
|
-
pathCount: routes2.n,
|
|
4478
|
-
// a real inversion test needs each walk's direction relative to the
|
|
4479
|
-
// reference; the first visit's strand is just whichever path anchored the
|
|
4480
|
-
// node
|
|
4481
|
-
inversion: false,
|
|
4482
|
-
shortestAlleleLength: fallback ? refEnd - refStart : routes2.min,
|
|
4483
|
-
longestAlleleLength: fallback ? refEnd - refStart : routes2.max,
|
|
4484
|
-
segments: [start, ...interior.map((id) => byId.get(id)), end].map((n) => n.name).join(","),
|
|
4485
|
-
shortestAllele: void 0,
|
|
4486
|
-
longestAllele: void 0,
|
|
4487
|
-
partial,
|
|
4488
|
-
routes: crossed?.routes
|
|
4489
|
-
});
|
|
4490
4430
|
}
|
|
4491
|
-
return
|
|
4492
|
-
}
|
|
4493
|
-
function walkRoutes(graph, byId, walkIndex, startId, endId) {
|
|
4494
|
-
const seen = /* @__PURE__ */ new Map();
|
|
4495
|
-
let min = Infinity;
|
|
4496
|
-
let max = -Infinity;
|
|
4497
|
-
let left = 0;
|
|
4498
|
-
graph.paths.forEach((p, k) => {
|
|
4499
|
-
const i0 = walkIndex[k].get(startId);
|
|
4500
|
-
const i1 = walkIndex[k].get(endId);
|
|
4501
|
-
if (i0 === void 0 || i1 === void 0) {
|
|
4502
|
-
if (i0 !== void 0 || i1 !== void 0) {
|
|
4503
|
-
left++;
|
|
4504
|
-
}
|
|
4505
|
-
return;
|
|
4506
|
-
}
|
|
4507
|
-
const steps = p.nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
|
|
4508
|
-
if (i1 < i0) {
|
|
4509
|
-
steps.reverse();
|
|
4510
|
-
}
|
|
4511
|
-
let bp = 0;
|
|
4512
|
-
for (const id of steps) {
|
|
4513
|
-
bp += byId.get(id)?.length ?? 0;
|
|
4514
|
-
}
|
|
4515
|
-
const key = steps.join(",");
|
|
4516
|
-
const route = seen.get(key) ?? { steps, bp, walks: [] };
|
|
4517
|
-
route.walks.push(p.name);
|
|
4518
|
-
seen.set(key, route);
|
|
4519
|
-
min = Math.min(min, bp);
|
|
4520
|
-
max = Math.max(max, bp);
|
|
4521
|
-
});
|
|
4522
|
-
return seen.size || left ? { min, max, n: seen.size, left, routes: [...seen.values()] } : void 0;
|
|
4431
|
+
return out;
|
|
4523
4432
|
}
|
|
4524
4433
|
|
|
4525
|
-
// src/
|
|
4526
|
-
var
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
var
|
|
4535
|
-
function
|
|
4536
|
-
|
|
4537
|
-
return `${(bp / 1e3).toFixed(0)} kb`;
|
|
4538
|
-
}
|
|
4539
|
-
if (bp >= 1e3) {
|
|
4540
|
-
return `${(bp / 1e3).toFixed(1)} kb`;
|
|
4541
|
-
}
|
|
4542
|
-
return `${bp} bp`;
|
|
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;
|
|
4543
4446
|
}
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
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
|
+
};
|
|
4549
4456
|
}
|
|
4550
|
-
function
|
|
4551
|
-
|
|
4552
|
-
|
|
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
|
+
};
|
|
4464
|
+
}
|
|
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`;
|
|
4553
4471
|
}
|
|
4554
|
-
|
|
4555
|
-
|
|
4472
|
+
return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
|
|
4473
|
+
}
|
|
4474
|
+
function deletionText(bp) {
|
|
4475
|
+
return `${formatBp(bp)} del`;
|
|
4476
|
+
}
|
|
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);
|
|
4556
4502
|
}
|
|
4557
|
-
return
|
|
4503
|
+
return total;
|
|
4558
4504
|
}
|
|
4559
|
-
function
|
|
4560
|
-
|
|
4561
|
-
|
|
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
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
|
|
4562
4519
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
const
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
pathCount,
|
|
4569
|
-
segmentCount,
|
|
4570
|
-
inversion
|
|
4571
|
-
} = b;
|
|
4572
|
-
const range = formatBpRange(shortest, longest);
|
|
4573
|
-
if (segmentCount >= SUPERBUBBLE_SEGMENTS) {
|
|
4574
|
-
return {
|
|
4575
|
-
kind: "superbubble",
|
|
4576
|
-
label: `${range} superbubble, ${segmentCount} segments, ${routes(pathCount)}${inversion ? ", inv" : ""}`
|
|
4577
|
-
};
|
|
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;
|
|
4578
4525
|
}
|
|
4579
|
-
|
|
4580
|
-
|
|
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;
|
|
4581
4536
|
}
|
|
4582
|
-
const
|
|
4583
|
-
|
|
4584
|
-
const
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
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
|
+
}
|
|
4589
4551
|
}
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4552
|
+
arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
|
|
4553
|
+
return arcs;
|
|
4554
|
+
}
|
|
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
|
+
};
|
|
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
|
+
});
|
|
4614
|
+
}
|
|
4593
4615
|
}
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
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) {
|
|
4621
|
+
continue;
|
|
4622
|
+
}
|
|
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;
|
|
4629
|
+
}
|
|
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);
|
|
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
|
+
});
|
|
4653
|
+
}
|
|
4599
4654
|
}
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
};
|
|
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 };
|
|
4685
|
+
}
|
|
4686
|
+
function nodeLabelBudget(width, height) {
|
|
4687
|
+
return Math.max(
|
|
4688
|
+
MIN_NODE_LABELS,
|
|
4689
|
+
Math.floor(width * height / PX_PER_NODE_LABEL)
|
|
4690
|
+
);
|
|
4691
|
+
}
|
|
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;
|
|
4703
|
+
}
|
|
4704
|
+
|
|
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
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
});
|
|
4722
|
+
for (const boxes of byRefName.values()) {
|
|
4723
|
+
boxes.sort((a, b) => a.bp0 - b.bp0);
|
|
4724
|
+
}
|
|
4725
|
+
return byRefName;
|
|
4726
|
+
}
|
|
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;
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
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;
|
|
4745
|
+
}
|
|
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) };
|
|
4750
|
+
}
|
|
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;
|
|
4759
|
+
}
|
|
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
|
+
}
|
|
4605
4823
|
}
|
|
4606
4824
|
return {
|
|
4607
|
-
|
|
4608
|
-
|
|
4825
|
+
from,
|
|
4826
|
+
to,
|
|
4827
|
+
ticks,
|
|
4828
|
+
squeezed,
|
|
4829
|
+
labels: placed.map(({ text, at }) => ({ text, at }))
|
|
4609
4830
|
};
|
|
4610
4831
|
}
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
superbubble: "#8e3fbf",
|
|
4619
|
-
complex: "#8e3fbf"
|
|
4620
|
-
};
|
|
4621
|
-
var BUBBLE_KIND_NAMES = {
|
|
4622
|
-
snp: "SNP",
|
|
4623
|
-
substitution: "substitution",
|
|
4624
|
-
insertion: "insertion",
|
|
4625
|
-
deletion: "deletion",
|
|
4626
|
-
inversion: "inversion",
|
|
4627
|
-
repeat: "repeat array",
|
|
4628
|
-
superbubble: "superbubble",
|
|
4629
|
-
complex: "site"
|
|
4630
|
-
};
|
|
4631
|
-
function bubbleSegmentIds(b) {
|
|
4632
|
-
return b.segments.split(",").filter(Boolean);
|
|
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);
|
|
4633
4839
|
}
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
const
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
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);
|
|
4868
|
+
}
|
|
4869
|
+
}
|
|
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
|
+
});
|
|
4890
|
+
}
|
|
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
|
+
});
|
|
4665
4908
|
}
|
|
4666
4909
|
}
|
|
4667
|
-
|
|
4910
|
+
}
|
|
4911
|
+
const contigs = [...spans.values()];
|
|
4912
|
+
return contigs.length ? { contigs, prefixes: sharedPrefixes(contigs.map((c) => c.refName)) } : void 0;
|
|
4913
|
+
}
|
|
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)));
|
|
4919
|
+
}
|
|
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);
|
|
4928
|
+
}
|
|
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) {
|
|
4668
4947
|
continue;
|
|
4669
4948
|
}
|
|
4670
|
-
const
|
|
4671
|
-
const
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
} : labelAt;
|
|
4675
|
-
const sharing = /* @__PURE__ */ new Map();
|
|
4676
|
-
for (const route of bubble.routes ?? []) {
|
|
4677
|
-
for (const id of new Set(route.steps)) {
|
|
4678
|
-
sharing.set(id, (sharing.get(id) ?? 0) + 1);
|
|
4679
|
-
}
|
|
4680
|
-
}
|
|
4681
|
-
const routes2 = [];
|
|
4682
|
-
for (const route of bubble.routes ?? []) {
|
|
4683
|
-
if (bubble.longestAlleleLength < MIN_CHIPPED_BP) {
|
|
4684
|
-
break;
|
|
4685
|
-
}
|
|
4686
|
-
if (route.steps.length === 0) {
|
|
4687
|
-
continue;
|
|
4688
|
-
}
|
|
4689
|
-
const rarest = Math.min(...route.steps.map((id) => sharing.get(id)));
|
|
4690
|
-
let at;
|
|
4691
|
-
let far = -1;
|
|
4692
|
-
for (const id of route.steps) {
|
|
4693
|
-
if (sharing.get(id) !== rarest) {
|
|
4694
|
-
continue;
|
|
4695
|
-
}
|
|
4696
|
-
for (const p of positions[id] ?? []) {
|
|
4697
|
-
const d = Math.hypot(p.x - anchor.x, p.y - anchor.y);
|
|
4698
|
-
if (d > far) {
|
|
4699
|
-
far = d;
|
|
4700
|
-
at = p;
|
|
4701
|
-
}
|
|
4702
|
-
}
|
|
4703
|
-
}
|
|
4704
|
-
if (at) {
|
|
4705
|
-
routes2.push({ route, at, text: routeText(route, walkLabel) });
|
|
4706
|
-
}
|
|
4949
|
+
const start = Math.max(gene.start, first.bp0);
|
|
4950
|
+
const end = Math.min(gene.end, last.bp1);
|
|
4951
|
+
if (end <= start) {
|
|
4952
|
+
continue;
|
|
4707
4953
|
}
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
...
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
nodeIds,
|
|
4715
|
-
whole: nodeIds.length >= WHOLE_FRACTION * graph.nodes.length,
|
|
4716
|
-
routes: routes2
|
|
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
|
+
)
|
|
4717
4960
|
});
|
|
4718
4961
|
}
|
|
4719
|
-
return
|
|
4720
|
-
}
|
|
4721
|
-
function routeText(route, walkLabel) {
|
|
4722
|
-
const names = [...new Set(route.walks.map(walkLabel))].sort();
|
|
4723
|
-
const shown = names.slice(0, NAMED_WALKS).join(", ");
|
|
4724
|
-
const more = names.length > NAMED_WALKS ? ` +${names.length - NAMED_WALKS}` : "";
|
|
4725
|
-
return `${shown}${more} \xB7 ${formatBp(route.bp)}`;
|
|
4962
|
+
return out;
|
|
4726
4963
|
}
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
const
|
|
4733
|
-
const
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
}
|
|
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);
|
|
5000
|
+
}
|
|
5001
|
+
ctx.fillText(label, mid, glyphY - EXON_PX / 2 - 2);
|
|
5002
|
+
}
|
|
4744
5003
|
}
|
|
4745
5004
|
|
|
4746
|
-
// src/
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
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;
|
|
4752
5022
|
}
|
|
4753
|
-
function
|
|
4754
|
-
|
|
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
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
return out;
|
|
4755
5039
|
}
|
|
4756
|
-
|
|
4757
|
-
|
|
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;
|
|
5044
|
+
}
|
|
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;
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
return hit;
|
|
4758
5058
|
}
|
|
4759
|
-
function
|
|
4760
|
-
|
|
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
|
|
5075
|
+
}) {
|
|
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
|
+
}
|
|
4761
5105
|
}
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
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);
|
|
4767
5126
|
}
|
|
4768
|
-
|
|
4769
|
-
|
|
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
|
|
4770
5137
|
};
|
|
4771
5138
|
}
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
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);
|
|
4788
5178
|
}
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
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;
|
|
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
|
+
});
|
|
4792
5208
|
}
|
|
4793
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
|
+
});
|
|
4794
5244
|
}
|
|
4795
|
-
return
|
|
4796
|
-
}
|
|
4797
|
-
|
|
4798
|
-
// src/graphLabels.ts
|
|
4799
|
-
var MIN_DELETION_LABEL_PX = 26;
|
|
4800
|
-
var PX_PER_NODE_LABEL = 4e4;
|
|
4801
|
-
var MIN_NODE_LABELS = 8;
|
|
4802
|
-
var LEADER_GAP_PX = 8;
|
|
4803
|
-
var LABEL_CHAR_PX2 = 5.7;
|
|
4804
|
-
var LABEL_PAD_PX = 6;
|
|
4805
|
-
var LABEL_HEIGHT_PX = 13;
|
|
4806
|
-
var LABEL_GAP_PX = 3;
|
|
4807
|
-
var MAX_LABEL_OVERHANG = 2;
|
|
4808
|
-
function labelHalfWidth(text) {
|
|
4809
|
-
return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
|
|
4810
|
-
}
|
|
4811
|
-
var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
|
|
4812
|
-
var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
|
|
4813
|
-
function boxAt(halfW, x, y) {
|
|
4814
|
-
return {
|
|
4815
|
-
x0: x - halfW,
|
|
4816
|
-
x1: x + halfW,
|
|
4817
|
-
y0: y - LABEL_HALF_HEIGHT,
|
|
4818
|
-
y1: y + LABEL_HALF_HEIGHT
|
|
4819
|
-
};
|
|
5245
|
+
return bubbles;
|
|
4820
5246
|
}
|
|
4821
|
-
function
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
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;
|
|
4828
5277
|
}
|
|
4829
|
-
|
|
4830
|
-
|
|
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
|
+
);
|
|
4831
5287
|
}
|
|
5288
|
+
var SUPERBUBBLE_SEGMENTS = 40;
|
|
4832
5289
|
function formatBp2(bp) {
|
|
4833
|
-
if (bp >=
|
|
4834
|
-
return `${
|
|
5290
|
+
if (bp >= 1e4) {
|
|
5291
|
+
return `${(bp / 1e3).toFixed(0)} kb`;
|
|
4835
5292
|
}
|
|
4836
|
-
|
|
4837
|
-
}
|
|
4838
|
-
|
|
4839
|
-
return `${
|
|
5293
|
+
if (bp >= 1e3) {
|
|
5294
|
+
return `${(bp / 1e3).toFixed(1)} kb`;
|
|
5295
|
+
}
|
|
5296
|
+
return `${bp} bp`;
|
|
4840
5297
|
}
|
|
4841
|
-
function
|
|
4842
|
-
const
|
|
4843
|
-
const
|
|
4844
|
-
const
|
|
4845
|
-
|
|
4846
|
-
const len = Math.hypot(dx, dy);
|
|
4847
|
-
return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
|
|
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}`;
|
|
4848
5303
|
}
|
|
4849
|
-
function
|
|
4850
|
-
|
|
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`;
|
|
4851
5312
|
}
|
|
4852
|
-
function
|
|
4853
|
-
const
|
|
4854
|
-
|
|
4855
|
-
return Math.min(
|
|
4856
|
-
dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
|
|
4857
|
-
dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
|
|
4858
|
-
);
|
|
5313
|
+
function classifyBubble(b, repeats = []) {
|
|
5314
|
+
const c = classifyShape(b, repeats);
|
|
5315
|
+
return b.partial ? { ...c, label: `${c.label}, partial` } : c;
|
|
4859
5316
|
}
|
|
4860
|
-
function
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
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
|
+
};
|
|
4866
5332
|
}
|
|
4867
|
-
|
|
4868
|
-
}
|
|
4869
|
-
function runBounds(nodeIds, nodePositions) {
|
|
4870
|
-
let minX = Infinity;
|
|
4871
|
-
let minY = Infinity;
|
|
4872
|
-
let maxX = -Infinity;
|
|
4873
|
-
let maxY = -Infinity;
|
|
4874
|
-
for (const id of nodeIds) {
|
|
4875
|
-
for (const p of nodePositions[id] ?? []) {
|
|
4876
|
-
minX = Math.min(minX, p.x);
|
|
4877
|
-
maxX = Math.max(maxX, p.x);
|
|
4878
|
-
minY = Math.min(minY, p.y);
|
|
4879
|
-
maxY = Math.max(maxY, p.y);
|
|
4880
|
-
}
|
|
5333
|
+
if (inversion) {
|
|
5334
|
+
return { kind: "inversion", label: `${formatBp2(longest)} inv` };
|
|
4881
5335
|
}
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
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
|
+
};
|
|
4889
5343
|
}
|
|
4890
|
-
const
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
}
|
|
4894
|
-
var arcCache = /* @__PURE__ */ new WeakMap();
|
|
4895
|
-
function arcPlacements(nodePositions, deletions, axis, version) {
|
|
4896
|
-
const { scaleX, scaleY } = axis;
|
|
4897
|
-
const cached2 = arcCache.get(nodePositions);
|
|
4898
|
-
if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
|
|
4899
|
-
return cached2.arcs;
|
|
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` };
|
|
4900
5347
|
}
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
const { minX, minY, maxX, maxY } = curveBounds(curves);
|
|
4907
|
-
arcs.push({
|
|
4908
|
-
deletion,
|
|
4909
|
-
curves,
|
|
4910
|
-
apex,
|
|
4911
|
-
// in screen px, so the two axes' units cannot be added by accident
|
|
4912
|
-
extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
|
|
4913
|
-
});
|
|
4914
|
-
}
|
|
5348
|
+
if (refSpan === 0 || shortest === refSpan) {
|
|
5349
|
+
return {
|
|
5350
|
+
kind: "insertion",
|
|
5351
|
+
label: `\u2264${formatBp2(longest - refSpan)} ins${alleles}`
|
|
5352
|
+
};
|
|
4915
5353
|
}
|
|
4916
|
-
|
|
4917
|
-
|
|
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);
|
|
4918
5387
|
}
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
)) {
|
|
4940
|
-
if (extent >= MIN_DELETION_LABEL_PX) {
|
|
4941
|
-
const text = deletionText(deletion.bp);
|
|
4942
|
-
const halfW = labelHalfWidth(text);
|
|
4943
|
-
const box = boxAt(halfW, 0, 0);
|
|
4944
|
-
const arcX = apex.x * scaleX + translateX;
|
|
4945
|
-
const arcY = apex.y * scaleY + translateY;
|
|
4946
|
-
const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
|
|
4947
|
-
const bow = arcOutward(curves, apex, scaleX, scaleY);
|
|
4948
|
-
const offset = boxSupport(box, bow) + LEADER_GAP_PX;
|
|
4949
|
-
const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
|
|
4950
|
-
const y = fits ? arcY : clamp(
|
|
4951
|
-
arcY + bow.y * offset,
|
|
4952
|
-
LABEL_HALF_HEIGHT,
|
|
4953
|
-
height - LABEL_HALF_HEIGHT
|
|
4954
|
-
);
|
|
4955
|
-
const away = Math.hypot(x - arcX, y - arcY);
|
|
4956
|
-
let leader;
|
|
4957
|
-
if (away > 0) {
|
|
4958
|
-
const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
|
|
4959
|
-
const crossing = boxRayCrossing(box, dir);
|
|
4960
|
-
leader = {
|
|
4961
|
-
arcX,
|
|
4962
|
-
arcY,
|
|
4963
|
-
labelX: x - dir.x * crossing,
|
|
4964
|
-
labelY: y - dir.y * crossing
|
|
4965
|
-
};
|
|
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;
|
|
4966
5408
|
}
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
});
|
|
4978
|
-
}
|
|
4979
|
-
}
|
|
4980
|
-
const labelledByDeletion = /* @__PURE__ */ new Set();
|
|
4981
|
-
const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
|
|
4982
|
-
for (const run of alleleRuns) {
|
|
4983
|
-
const drawn = runBounds(run.nodeIds, nodePositions);
|
|
4984
|
-
if (!drawn) {
|
|
4985
|
-
continue;
|
|
4986
|
-
}
|
|
4987
|
-
const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
|
|
4988
|
-
const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
|
|
4989
|
-
const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
|
|
4990
|
-
const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
|
|
4991
|
-
if (shownRight < shownLeft || shownBottom < shownTop) {
|
|
4992
|
-
continue;
|
|
4993
|
-
}
|
|
4994
|
-
const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
|
|
4995
|
-
const text = deletionText(run.bp);
|
|
4996
|
-
const halfW = labelHalfWidth(text);
|
|
4997
|
-
if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
|
|
4998
|
-
for (const id of run.nodeIds) {
|
|
4999
|
-
labelledByDeletion.add(id);
|
|
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);
|
|
5000
5419
|
}
|
|
5001
|
-
const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
|
|
5002
|
-
const y = clamp(
|
|
5003
|
-
(shownTop + shownBottom) / 2,
|
|
5004
|
-
LABEL_HALF_HEIGHT,
|
|
5005
|
-
height - LABEL_HALF_HEIGHT
|
|
5006
|
-
);
|
|
5007
|
-
deletionCandidates.push({
|
|
5008
|
-
label: {
|
|
5009
|
-
key: `alleledel:${run.nodeIds.join(",")}`,
|
|
5010
|
-
text,
|
|
5011
|
-
x,
|
|
5012
|
-
y,
|
|
5013
|
-
kind: "deletion"
|
|
5014
|
-
},
|
|
5015
|
-
box: boxAt(halfW, x, y)
|
|
5016
|
-
});
|
|
5017
|
-
}
|
|
5018
|
-
}
|
|
5019
|
-
for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
|
|
5020
|
-
if (labelledByDeletion.has(id)) {
|
|
5021
|
-
continue;
|
|
5022
5420
|
}
|
|
5023
|
-
|
|
5024
|
-
const screenY = y * scaleY + translateY;
|
|
5025
|
-
if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
|
|
5421
|
+
if (nodeIds.length === 0) {
|
|
5026
5422
|
continue;
|
|
5027
5423
|
}
|
|
5028
|
-
const
|
|
5029
|
-
|
|
5030
|
-
|
|
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
|
+
}
|
|
5031
5434
|
}
|
|
5032
|
-
const
|
|
5033
|
-
const
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
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
|
+
}
|
|
5046
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
|
+
});
|
|
5047
5472
|
}
|
|
5048
|
-
return
|
|
5473
|
+
return halos;
|
|
5049
5474
|
}
|
|
5050
|
-
function
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
)
|
|
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)}`;
|
|
5055
5480
|
}
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
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
|
+
};
|
|
5067
5498
|
}
|
|
5068
5499
|
|
|
5069
5500
|
// src/labelLayout.ts
|
|
@@ -5211,27 +5642,28 @@ function layoutLabels(m) {
|
|
|
5211
5642
|
}
|
|
5212
5643
|
|
|
5213
5644
|
// src/genes/genePins.ts
|
|
5214
|
-
function contig(name) {
|
|
5215
|
-
return name.split("#").at(-1);
|
|
5216
|
-
}
|
|
5217
5645
|
function genePins(graph, genes, positions) {
|
|
5218
|
-
const
|
|
5219
|
-
const
|
|
5646
|
+
const byRefName = /* @__PURE__ */ new Map();
|
|
5647
|
+
const refNames = /* @__PURE__ */ new Set();
|
|
5220
5648
|
for (const node of graph.nodes) {
|
|
5221
|
-
if (isBackbone(node)
|
|
5649
|
+
if (isBackbone(node)) {
|
|
5222
5650
|
const { refName } = node.stable;
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
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
|
+
}
|
|
5227
5657
|
}
|
|
5228
5658
|
}
|
|
5229
|
-
for (const { nodes } of
|
|
5659
|
+
for (const { nodes } of byRefName.values()) {
|
|
5230
5660
|
nodes.sort((a, b) => a.stable.start - b.stable.start);
|
|
5231
5661
|
}
|
|
5662
|
+
const bind = refNameBinding(refNames);
|
|
5232
5663
|
const pins = [];
|
|
5233
5664
|
for (const gene of genes) {
|
|
5234
|
-
const
|
|
5665
|
+
const refName = bind(gene.refName);
|
|
5666
|
+
const backbone = refName === void 0 ? void 0 : byRefName.get(refName);
|
|
5235
5667
|
if (!backbone) {
|
|
5236
5668
|
continue;
|
|
5237
5669
|
}
|
|
@@ -5317,16 +5749,9 @@ function haplotypeWanted(name, wanted) {
|
|
|
5317
5749
|
async function referenceSamplesOf(db) {
|
|
5318
5750
|
return (await db.tag("gbwt_reference_samples") ?? "").split(/\s+/).filter((sample) => sample !== "");
|
|
5319
5751
|
}
|
|
5320
|
-
var SAMPLE_ALIASES = {
|
|
5321
|
-
hg38: "grch38",
|
|
5322
|
-
hg19: "grch37",
|
|
5323
|
-
hs1: "chm13",
|
|
5324
|
-
"t2t-chm13": "chm13",
|
|
5325
|
-
chm13v2: "chm13"
|
|
5326
|
-
};
|
|
5327
5752
|
function aliasedSample(anchorSample, referenceSamples) {
|
|
5328
5753
|
const lower = anchorSample.toLowerCase();
|
|
5329
|
-
const alias =
|
|
5754
|
+
const alias = wellKnownSample(anchorSample)?.toLowerCase();
|
|
5330
5755
|
return referenceSamples.find((s) => s === anchorSample) ?? referenceSamples.find((s) => s.toLowerCase() === lower) ?? referenceSamples.find((s) => s.toLowerCase() === alias);
|
|
5331
5756
|
}
|
|
5332
5757
|
function resolveReferenceSample({
|
|
@@ -5441,7 +5866,9 @@ export {
|
|
|
5441
5866
|
NODE_WIDTHS,
|
|
5442
5867
|
REFERENCE_RAMP_MAX_HUE,
|
|
5443
5868
|
ROW_HEIGHT_PX,
|
|
5869
|
+
WELL_KNOWN_SAMPLES,
|
|
5444
5870
|
axisScaleOf,
|
|
5871
|
+
backboneAssembly,
|
|
5445
5872
|
bubbleHalos,
|
|
5446
5873
|
bubbleSegmentIds,
|
|
5447
5874
|
bubbleSubgraph,
|
|
@@ -5450,20 +5877,26 @@ export {
|
|
|
5450
5877
|
clampZoom,
|
|
5451
5878
|
classifyBubble,
|
|
5452
5879
|
computeReferenceRamp,
|
|
5880
|
+
connectorAt,
|
|
5453
5881
|
contains,
|
|
5454
5882
|
cutWindowGFA,
|
|
5455
5883
|
deletionEdges,
|
|
5456
5884
|
drawTubeMap,
|
|
5885
|
+
drawTubeMapConnectors,
|
|
5886
|
+
drawTubeMapGenes,
|
|
5887
|
+
drawTubeMapRuler,
|
|
5457
5888
|
drawingBounds,
|
|
5458
5889
|
engineKey,
|
|
5459
5890
|
engineRequest,
|
|
5891
|
+
featuresOnBackbone,
|
|
5460
5892
|
findHoveredEdge,
|
|
5461
5893
|
findHoveredNode,
|
|
5462
5894
|
fitTransform,
|
|
5463
5895
|
forceLayout,
|
|
5464
|
-
|
|
5896
|
+
formatBp,
|
|
5465
5897
|
genePins,
|
|
5466
5898
|
getDpr,
|
|
5899
|
+
graphBackbone,
|
|
5467
5900
|
haplotypeWanted,
|
|
5468
5901
|
layoutExtent,
|
|
5469
5902
|
layoutLabels,
|
|
@@ -5481,20 +5914,28 @@ export {
|
|
|
5481
5914
|
panSNSample,
|
|
5482
5915
|
pathColorsLegible,
|
|
5483
5916
|
pathLegend,
|
|
5917
|
+
refNameBinding,
|
|
5918
|
+
referenceBoxes,
|
|
5484
5919
|
referenceKnots,
|
|
5920
|
+
referenceNodes,
|
|
5485
5921
|
referencePathQuery,
|
|
5486
5922
|
referenceSamplesOf,
|
|
5487
5923
|
resolveColorScheme,
|
|
5488
5924
|
resolveReferenceSample,
|
|
5925
|
+
rulerBoxes,
|
|
5489
5926
|
screenToLayout,
|
|
5927
|
+
tubeMapConnectors,
|
|
5490
5928
|
tubeMapFrame,
|
|
5929
|
+
tubeMapGenes,
|
|
5491
5930
|
tubeMapNodeAt,
|
|
5492
5931
|
tubeMapPicture,
|
|
5932
|
+
tubeX,
|
|
5493
5933
|
viewportOf,
|
|
5494
5934
|
walkHighlight,
|
|
5495
5935
|
walkRows,
|
|
5496
5936
|
walkRowsExtent,
|
|
5497
5937
|
warpX,
|
|
5938
|
+
wellKnownSample,
|
|
5498
5939
|
wheelZoomFactor,
|
|
5499
5940
|
zoomAbout
|
|
5500
5941
|
};
|