@jbrowse/bandage-core 0.1.5 → 0.1.7
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 +3 -0
- package/dist/index.js +1202 -694
- package/dist/types/colorSchemes.d.ts +1 -1
- package/dist/types/index.d.ts +8 -0
- package/dist/types/layout/tubeMapLayout.d.ts +3 -0
- package/dist/types/layout/walkRows.d.ts +3 -0
- package/dist/types/nodeWidths.d.ts +1 -1
- package/dist/types/pipeline.d.ts +6 -2
- package/dist/types/reference.d.ts +25 -0
- package/dist/types/renderer/GeometryBuilder.d.ts +1 -0
- package/dist/types/tubeMap/axis.d.ts +35 -0
- package/dist/types/tubeMap/coarsen.d.ts +16 -0
- package/dist/types/tubeMap/connectors.d.ts +23 -0
- package/dist/types/tubeMap/deviations.d.ts +13 -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 +16 -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;
|
|
@@ -971,23 +971,31 @@ var MAX_ZOOM = 100;
|
|
|
971
971
|
function clampZoom(zoom) {
|
|
972
972
|
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
|
|
973
973
|
}
|
|
974
|
-
function fittedTranslateY(bounds, usableHeight, yScale) {
|
|
974
|
+
function fittedTranslateY(bounds, usableHeight, yScale, padTop = FIT_PADDING) {
|
|
975
975
|
const leftoverY = usableHeight - bounds.h * yScale;
|
|
976
|
-
return
|
|
976
|
+
return padTop - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
|
|
977
977
|
}
|
|
978
|
-
function fitTransform(bounds, paneWidth, paneHeight, pixelRows) {
|
|
979
|
-
const usableWidth = paneWidth -
|
|
980
|
-
const usableHeight = paneHeight -
|
|
978
|
+
function fitTransform(bounds, paneWidth, paneHeight, pixelRows, { minScale = 0, padLeft = FIT_PADDING, padTop = FIT_PADDING } = {}) {
|
|
979
|
+
const usableWidth = paneWidth - padLeft - FIT_PADDING;
|
|
980
|
+
const usableHeight = paneHeight - padTop - FIT_PADDING;
|
|
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:
|
|
990
|
-
translateY: fittedTranslateY(
|
|
992
|
+
translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
|
|
993
|
+
translateY: fittedTranslateY(
|
|
994
|
+
bounds,
|
|
995
|
+
usableHeight,
|
|
996
|
+
pixelRows ? 1 : scale,
|
|
997
|
+
padTop
|
|
998
|
+
)
|
|
991
999
|
};
|
|
992
1000
|
}
|
|
993
1001
|
function drawingBounds(layout, opts = {}) {
|
|
@@ -2010,7 +2018,6 @@ function walkRows(graph, region, unit) {
|
|
|
2010
2018
|
return void 0;
|
|
2011
2019
|
}
|
|
2012
2020
|
const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
|
|
2013
|
-
const onReference = new Set(reference.nodeIds);
|
|
2014
2021
|
const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
|
|
2015
2022
|
const cut = region && region.end > region.start ? region : void 0;
|
|
2016
2023
|
const span = /* @__PURE__ */ new Map();
|
|
@@ -2028,17 +2035,34 @@ function walkRows(graph, region, unit) {
|
|
|
2028
2035
|
const runs = [];
|
|
2029
2036
|
let bp = 0;
|
|
2030
2037
|
let offReferenceBp = 0;
|
|
2038
|
+
let step = 0;
|
|
2031
2039
|
for (const id of ids) {
|
|
2032
2040
|
const len = lengthOf.get(id) ?? 0;
|
|
2033
|
-
const
|
|
2041
|
+
const s = span.get(id);
|
|
2034
2042
|
const last = runs.at(-1);
|
|
2035
|
-
|
|
2043
|
+
const at = last?.referenceStart;
|
|
2044
|
+
const forward2 = s && at !== void 0 && step >= 0 && at + last.bp === s.start;
|
|
2045
|
+
const backward = s && at !== void 0 && step <= 0 && s.end === at;
|
|
2046
|
+
if (!s && last && !last.onReference) {
|
|
2036
2047
|
last.bp += len;
|
|
2048
|
+
} else if (forward2 || backward) {
|
|
2049
|
+
last.bp += len;
|
|
2050
|
+
if (!forward2) {
|
|
2051
|
+
last.referenceStart = s.start;
|
|
2052
|
+
last.reversed = true;
|
|
2053
|
+
}
|
|
2054
|
+
step = forward2 ? 1 : -1;
|
|
2037
2055
|
} else {
|
|
2038
|
-
runs.push({
|
|
2056
|
+
runs.push({
|
|
2057
|
+
start: bp,
|
|
2058
|
+
bp: len,
|
|
2059
|
+
onReference: s !== void 0,
|
|
2060
|
+
referenceStart: s?.start
|
|
2061
|
+
});
|
|
2062
|
+
step = 0;
|
|
2039
2063
|
}
|
|
2040
2064
|
bp += len;
|
|
2041
|
-
if (!
|
|
2065
|
+
if (!s) {
|
|
2042
2066
|
offReferenceBp += len;
|
|
2043
2067
|
}
|
|
2044
2068
|
}
|
|
@@ -2046,6 +2070,7 @@ function walkRows(graph, region, unit) {
|
|
|
2046
2070
|
name: path.name,
|
|
2047
2071
|
label: labelOf(path),
|
|
2048
2072
|
sample: sampleOf(path),
|
|
2073
|
+
haplotype: path.haplotype,
|
|
2049
2074
|
bp,
|
|
2050
2075
|
offReferenceBp,
|
|
2051
2076
|
complete,
|
|
@@ -2574,9 +2599,9 @@ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY,
|
|
|
2574
2599
|
offsetY
|
|
2575
2600
|
);
|
|
2576
2601
|
}
|
|
2577
|
-
const
|
|
2578
|
-
const fromAttach = attachment(fromSegments,
|
|
2579
|
-
const toAttach = attachment(toSegments,
|
|
2602
|
+
const sides2 = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
|
|
2603
|
+
const fromAttach = attachment(fromSegments, sides2.from);
|
|
2604
|
+
const toAttach = attachment(toSegments, sides2.to);
|
|
2580
2605
|
const fromEnd = fromAttach.at;
|
|
2581
2606
|
const toStart = toAttach.at;
|
|
2582
2607
|
const p1x = fromEnd.x + offsetX;
|
|
@@ -3066,7 +3091,9 @@ function hashColor(str, alpha) {
|
|
|
3066
3091
|
var REFERENCE_RAMP_MAX_HUE = 300;
|
|
3067
3092
|
var REFERENCE_RAMP_SATURATION = 0.7;
|
|
3068
3093
|
var REFERENCE_RAMP_LIGHTNESS = 0.5;
|
|
3069
|
-
var
|
|
3094
|
+
var REFERENCE_RAMP_ALT_RGB = [60, 65, 72];
|
|
3095
|
+
var REFERENCE_RAMP_ALT_COLOR = packAbgr2(...REFERENCE_RAMP_ALT_RGB, 255);
|
|
3096
|
+
var REFERENCE_RAMP_ALT_CSS = `rgb(${REFERENCE_RAMP_ALT_RGB.join(", ")})`;
|
|
3070
3097
|
function computeReferenceRamp(graph, domain) {
|
|
3071
3098
|
const midpoints = referenceMidpoints(graph);
|
|
3072
3099
|
if (domain && domain.end > domain.start) {
|
|
@@ -4123,6 +4150,7 @@ function tubeMapPicture(layout) {
|
|
|
4123
4150
|
}
|
|
4124
4151
|
});
|
|
4125
4152
|
return {
|
|
4153
|
+
bounds: layout.bounds,
|
|
4126
4154
|
layers: [layerOf(layout, "haplotype"), layerOf(layout, "read")],
|
|
4127
4155
|
nodes,
|
|
4128
4156
|
mismatches: tubeMapMismatches(layout).filter(
|
|
@@ -4193,6 +4221,11 @@ function fillByColor(ctx, items, visible, addTo) {
|
|
|
4193
4221
|
}
|
|
4194
4222
|
ctx.globalAlpha = 1;
|
|
4195
4223
|
}
|
|
4224
|
+
var FULL_OUTLINE_PX = 12;
|
|
4225
|
+
var MIN_OUTLINE_ALPHA = 0.15;
|
|
4226
|
+
function outlineAlpha(screenWidth) {
|
|
4227
|
+
return Math.max(MIN_OUTLINE_ALPHA, Math.min(1, screenWidth / FULL_OUTLINE_PX));
|
|
4228
|
+
}
|
|
4196
4229
|
function drawTubeMap(ctx, picture, frame) {
|
|
4197
4230
|
const { x, y, width } = frame;
|
|
4198
4231
|
const visible = (item) => x(item.x1) >= 0 && x(item.x0) <= width;
|
|
@@ -4217,7 +4250,9 @@ function drawTubeMap(ctx, picture, frame) {
|
|
|
4217
4250
|
ctx.fillStyle = lit ? "rgba(255,192,203,0.5)" : fill;
|
|
4218
4251
|
ctx.strokeStyle = lit ? "#ff0000" : stroke;
|
|
4219
4252
|
ctx.fill();
|
|
4253
|
+
ctx.globalAlpha = lit ? 1 : outlineAlpha(x(node.x1) - x(node.x0));
|
|
4220
4254
|
ctx.stroke();
|
|
4255
|
+
ctx.globalAlpha = 1;
|
|
4221
4256
|
}
|
|
4222
4257
|
}
|
|
4223
4258
|
drawMismatches(ctx, picture.mismatches, frame);
|
|
@@ -4350,720 +4385,1057 @@ function tubeMapNodeAt(drawing, { x, y }, sx, sy) {
|
|
|
4350
4385
|
return hit;
|
|
4351
4386
|
}
|
|
4352
4387
|
|
|
4353
|
-
// src/
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4388
|
+
// src/overlayLabels.ts
|
|
4389
|
+
var LABEL_PX = 11;
|
|
4390
|
+
var LABEL_CHAR_PX = 6.2;
|
|
4391
|
+
var LABEL_PAD = 4;
|
|
4392
|
+
function labelWidth(text) {
|
|
4393
|
+
return text.length * LABEL_CHAR_PX + LABEL_PAD * 2;
|
|
4394
|
+
}
|
|
4395
|
+
function overlaps(a, b) {
|
|
4396
|
+
return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
|
|
4397
|
+
}
|
|
4398
|
+
function offFrame(box, frame) {
|
|
4399
|
+
return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
|
|
4400
|
+
}
|
|
4401
|
+
function insideFrame(box, frame) {
|
|
4402
|
+
return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
|
|
4403
|
+
}
|
|
4404
|
+
function occupancy(frame, reserved = []) {
|
|
4405
|
+
const placed = [...reserved];
|
|
4406
|
+
return (box) => {
|
|
4407
|
+
if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
|
|
4408
|
+
return false;
|
|
4372
4409
|
}
|
|
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
|
|
4410
|
+
placed.push(box);
|
|
4411
|
+
return true;
|
|
4383
4412
|
};
|
|
4384
4413
|
}
|
|
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;
|
|
4414
|
+
function placeLabels(candidates, frame, take) {
|
|
4415
|
+
const out = [];
|
|
4416
|
+
const row = LABEL_PX + LABEL_PAD * 2;
|
|
4417
|
+
for (const c of candidates) {
|
|
4418
|
+
const w = labelWidth(c.text);
|
|
4419
|
+
const direction = c.y > frame.height / 2 ? -1 : 1;
|
|
4420
|
+
for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
|
|
4421
|
+
const y = c.y + direction * tries * row;
|
|
4422
|
+
const box = {
|
|
4423
|
+
x0: c.x - w / 2,
|
|
4424
|
+
x1: c.x + w / 2,
|
|
4425
|
+
y0: y - LABEL_PX - LABEL_PAD,
|
|
4426
|
+
y1: y + LABEL_PAD
|
|
4427
|
+
};
|
|
4428
|
+
if (offFrame(box, frame)) {
|
|
4429
|
+
break;
|
|
4441
4430
|
}
|
|
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
|
-
});
|
|
4431
|
+
if (insideFrame(box, frame) && take(box)) {
|
|
4432
|
+
out.push({ ...c, y, w });
|
|
4433
|
+
break;
|
|
4454
4434
|
}
|
|
4455
4435
|
}
|
|
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
4436
|
}
|
|
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;
|
|
4437
|
+
return out;
|
|
4523
4438
|
}
|
|
4524
4439
|
|
|
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`;
|
|
4440
|
+
// src/graphLabels.ts
|
|
4441
|
+
var MIN_DELETION_LABEL_PX = 26;
|
|
4442
|
+
var PX_PER_NODE_LABEL = 4e4;
|
|
4443
|
+
var MIN_NODE_LABELS = 8;
|
|
4444
|
+
var LEADER_GAP_PX = 8;
|
|
4445
|
+
var LABEL_CHAR_PX2 = 5.7;
|
|
4446
|
+
var LABEL_PAD_PX = 6;
|
|
4447
|
+
var LABEL_HEIGHT_PX = 13;
|
|
4448
|
+
var LABEL_GAP_PX = 3;
|
|
4449
|
+
var MAX_LABEL_OVERHANG = 2;
|
|
4450
|
+
function labelHalfWidth(text) {
|
|
4451
|
+
return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
|
|
4543
4452
|
}
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4453
|
+
var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
|
|
4454
|
+
var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
|
|
4455
|
+
function boxAt(halfW, x, y) {
|
|
4456
|
+
return {
|
|
4457
|
+
x0: x - halfW,
|
|
4458
|
+
x1: x + halfW,
|
|
4459
|
+
y0: y - LABEL_HALF_HEIGHT,
|
|
4460
|
+
y1: y + LABEL_HALF_HEIGHT
|
|
4461
|
+
};
|
|
4549
4462
|
}
|
|
4550
|
-
function
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
}
|
|
4557
|
-
return `${count} routes`;
|
|
4463
|
+
function rowLabelBox(text, screenY) {
|
|
4464
|
+
return {
|
|
4465
|
+
x0: 6,
|
|
4466
|
+
x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
|
|
4467
|
+
y0: screenY - 8 - LABEL_GAP_PX / 2,
|
|
4468
|
+
y1: screenY + 8 + LABEL_GAP_PX / 2
|
|
4469
|
+
};
|
|
4558
4470
|
}
|
|
4559
|
-
function
|
|
4560
|
-
|
|
4561
|
-
return b.partial ? { ...c, label: `${c.label}, partial` } : c;
|
|
4471
|
+
function clamp(v, lo, hi) {
|
|
4472
|
+
return hi < lo ? v : Math.min(Math.max(v, lo), hi);
|
|
4562
4473
|
}
|
|
4563
|
-
function
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
shortestAlleleLength: shortest,
|
|
4567
|
-
longestAlleleLength: longest,
|
|
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
|
-
};
|
|
4474
|
+
function formatBp(bp) {
|
|
4475
|
+
if (bp >= 1e6) {
|
|
4476
|
+
return `${+(bp / 1e6).toFixed(1)} Mb`;
|
|
4578
4477
|
}
|
|
4579
|
-
|
|
4580
|
-
|
|
4478
|
+
return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
|
|
4479
|
+
}
|
|
4480
|
+
function deletionText(bp) {
|
|
4481
|
+
return `${formatBp(bp)} del`;
|
|
4482
|
+
}
|
|
4483
|
+
function arcOutward(curves, apex, scaleX, scaleY) {
|
|
4484
|
+
const first = curves[0];
|
|
4485
|
+
const last = curves[curves.length - 1];
|
|
4486
|
+
const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
|
|
4487
|
+
const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
|
|
4488
|
+
const len = Math.hypot(dx, dy);
|
|
4489
|
+
return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
|
|
4490
|
+
}
|
|
4491
|
+
function boxSupport(box, dir) {
|
|
4492
|
+
return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
|
|
4493
|
+
}
|
|
4494
|
+
function boxRayCrossing(box, dir) {
|
|
4495
|
+
const halfW = (box.x1 - box.x0) / 2;
|
|
4496
|
+
const halfH = (box.y1 - box.y0) / 2;
|
|
4497
|
+
return Math.min(
|
|
4498
|
+
dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
|
|
4499
|
+
dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
function drawnLength(segments, scaleX, scaleY) {
|
|
4503
|
+
let total = 0;
|
|
4504
|
+
for (let i = 1; i < segments.length; i++) {
|
|
4505
|
+
const a = segments[i - 1];
|
|
4506
|
+
const b = segments[i];
|
|
4507
|
+
total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
|
|
4581
4508
|
}
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4509
|
+
return total;
|
|
4510
|
+
}
|
|
4511
|
+
function runBounds(nodeIds, nodePositions) {
|
|
4512
|
+
let minX = Infinity;
|
|
4513
|
+
let minY = Infinity;
|
|
4514
|
+
let maxX = -Infinity;
|
|
4515
|
+
let maxY = -Infinity;
|
|
4516
|
+
for (const id of nodeIds) {
|
|
4517
|
+
for (const p of nodePositions[id] ?? []) {
|
|
4518
|
+
minX = Math.min(minX, p.x);
|
|
4519
|
+
maxX = Math.max(maxX, p.x);
|
|
4520
|
+
minY = Math.min(minY, p.y);
|
|
4521
|
+
maxY = Math.max(maxY, p.y);
|
|
4522
|
+
}
|
|
4589
4523
|
}
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4524
|
+
return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
|
|
4525
|
+
}
|
|
4526
|
+
var nodeOrderCache = /* @__PURE__ */ new WeakMap();
|
|
4527
|
+
function labelOrder(nodePositions, nodeLengths) {
|
|
4528
|
+
const cached2 = nodeOrderCache.get(nodePositions);
|
|
4529
|
+
if (cached2?.nodeLengths === nodeLengths) {
|
|
4530
|
+
return cached2.order;
|
|
4593
4531
|
}
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4532
|
+
const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
|
|
4533
|
+
nodeOrderCache.set(nodePositions, { nodeLengths, order });
|
|
4534
|
+
return order;
|
|
4535
|
+
}
|
|
4536
|
+
var arcCache = /* @__PURE__ */ new WeakMap();
|
|
4537
|
+
function arcPlacements(nodePositions, deletions, axis, version) {
|
|
4538
|
+
const { scaleX, scaleY } = axis;
|
|
4539
|
+
const cached2 = arcCache.get(nodePositions);
|
|
4540
|
+
if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
|
|
4541
|
+
return cached2.arcs;
|
|
4599
4542
|
}
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4543
|
+
const arcs = [];
|
|
4544
|
+
for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
|
|
4545
|
+
const curves = deletionArcCurves(nodePositions, deletion, axis);
|
|
4546
|
+
const apex = curves ? curveMidpoint(curves) : void 0;
|
|
4547
|
+
if (curves && apex) {
|
|
4548
|
+
const { minX, minY, maxX, maxY } = curveBounds(curves);
|
|
4549
|
+
arcs.push({
|
|
4550
|
+
deletion,
|
|
4551
|
+
curves,
|
|
4552
|
+
apex,
|
|
4553
|
+
// in screen px, so the two axes' units cannot be added by accident
|
|
4554
|
+
extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
|
|
4555
|
+
});
|
|
4556
|
+
}
|
|
4605
4557
|
}
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
label: `${formatBp(refSpan)} ref \u2192 ${range}, ${pathCount} alleles`
|
|
4609
|
-
};
|
|
4610
|
-
}
|
|
4611
|
-
var BUBBLE_KIND_COLORS = {
|
|
4612
|
-
snp: "#5b6b7a",
|
|
4613
|
-
substitution: "#5b6b7a",
|
|
4614
|
-
insertion: "#2f8fd6",
|
|
4615
|
-
deletion: "#c94040",
|
|
4616
|
-
inversion: "#e07b00",
|
|
4617
|
-
repeat: "#8e3fbf",
|
|
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);
|
|
4558
|
+
arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
|
|
4559
|
+
return arcs;
|
|
4633
4560
|
}
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4561
|
+
function sizeLabelCandidates({
|
|
4562
|
+
nodePositions,
|
|
4563
|
+
nodeLengths,
|
|
4564
|
+
deletions,
|
|
4565
|
+
alleleDeletions,
|
|
4566
|
+
axis,
|
|
4567
|
+
translateX,
|
|
4568
|
+
translateY,
|
|
4569
|
+
width,
|
|
4570
|
+
height,
|
|
4571
|
+
version = 0
|
|
4572
|
+
}) {
|
|
4573
|
+
const { scaleX, scaleY } = axis;
|
|
4574
|
+
const deletionCandidates = [];
|
|
4575
|
+
const nodeCandidates = [];
|
|
4576
|
+
for (const { deletion, curves, apex, extent } of arcPlacements(
|
|
4577
|
+
nodePositions,
|
|
4578
|
+
deletions,
|
|
4579
|
+
axis,
|
|
4580
|
+
version
|
|
4581
|
+
)) {
|
|
4582
|
+
if (extent >= MIN_DELETION_LABEL_PX) {
|
|
4583
|
+
const text = deletionText(deletion.bp);
|
|
4584
|
+
const halfW = labelHalfWidth(text);
|
|
4585
|
+
const box = boxAt(halfW, 0, 0);
|
|
4586
|
+
const arcX = apex.x * scaleX + translateX;
|
|
4587
|
+
const arcY = apex.y * scaleY + translateY;
|
|
4588
|
+
const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
|
|
4589
|
+
const bow = arcOutward(curves, apex, scaleX, scaleY);
|
|
4590
|
+
const offset = boxSupport(box, bow) + LEADER_GAP_PX;
|
|
4591
|
+
const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
|
|
4592
|
+
const y = fits ? arcY : clamp(
|
|
4593
|
+
arcY + bow.y * offset,
|
|
4594
|
+
LABEL_HALF_HEIGHT,
|
|
4595
|
+
height - LABEL_HALF_HEIGHT
|
|
4596
|
+
);
|
|
4597
|
+
const away = Math.hypot(x - arcX, y - arcY);
|
|
4598
|
+
let leader;
|
|
4599
|
+
if (away > 0) {
|
|
4600
|
+
const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
|
|
4601
|
+
const crossing = boxRayCrossing(box, dir);
|
|
4602
|
+
leader = {
|
|
4603
|
+
arcX,
|
|
4604
|
+
arcY,
|
|
4605
|
+
labelX: x - dir.x * crossing,
|
|
4606
|
+
labelY: y - dir.y * crossing
|
|
4607
|
+
};
|
|
4665
4608
|
}
|
|
4609
|
+
deletionCandidates.push({
|
|
4610
|
+
label: {
|
|
4611
|
+
key: `del:${deletion.edgeIndex}`,
|
|
4612
|
+
text,
|
|
4613
|
+
x,
|
|
4614
|
+
y,
|
|
4615
|
+
kind: "deletion",
|
|
4616
|
+
leader
|
|
4617
|
+
},
|
|
4618
|
+
box: boxAt(halfW, x, y)
|
|
4619
|
+
});
|
|
4666
4620
|
}
|
|
4667
|
-
|
|
4621
|
+
}
|
|
4622
|
+
const labelledByDeletion = /* @__PURE__ */ new Set();
|
|
4623
|
+
const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
|
|
4624
|
+
for (const run of alleleRuns) {
|
|
4625
|
+
const drawn = runBounds(run.nodeIds, nodePositions);
|
|
4626
|
+
if (!drawn) {
|
|
4668
4627
|
continue;
|
|
4669
4628
|
}
|
|
4670
|
-
const
|
|
4671
|
-
const
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
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
|
-
}
|
|
4629
|
+
const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
|
|
4630
|
+
const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
|
|
4631
|
+
const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
|
|
4632
|
+
const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
|
|
4633
|
+
if (shownRight < shownLeft || shownBottom < shownTop) {
|
|
4634
|
+
continue;
|
|
4680
4635
|
}
|
|
4681
|
-
const
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
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) });
|
|
4636
|
+
const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
|
|
4637
|
+
const text = deletionText(run.bp);
|
|
4638
|
+
const halfW = labelHalfWidth(text);
|
|
4639
|
+
if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
|
|
4640
|
+
for (const id of run.nodeIds) {
|
|
4641
|
+
labelledByDeletion.add(id);
|
|
4706
4642
|
}
|
|
4643
|
+
const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
|
|
4644
|
+
const y = clamp(
|
|
4645
|
+
(shownTop + shownBottom) / 2,
|
|
4646
|
+
LABEL_HALF_HEIGHT,
|
|
4647
|
+
height - LABEL_HALF_HEIGHT
|
|
4648
|
+
);
|
|
4649
|
+
deletionCandidates.push({
|
|
4650
|
+
label: {
|
|
4651
|
+
key: `alleledel:${run.nodeIds.join(",")}`,
|
|
4652
|
+
text,
|
|
4653
|
+
x,
|
|
4654
|
+
y,
|
|
4655
|
+
kind: "deletion"
|
|
4656
|
+
},
|
|
4657
|
+
box: boxAt(halfW, x, y)
|
|
4658
|
+
});
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
|
|
4662
|
+
if (labelledByDeletion.has(id)) {
|
|
4663
|
+
continue;
|
|
4664
|
+
}
|
|
4665
|
+
const { x, y } = polylineMidpoint(segments);
|
|
4666
|
+
const screenY = y * scaleY + translateY;
|
|
4667
|
+
if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
|
|
4668
|
+
continue;
|
|
4669
|
+
}
|
|
4670
|
+
const drawn = drawnLength(segments, scaleX, scaleY);
|
|
4671
|
+
if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
|
|
4672
|
+
continue;
|
|
4673
|
+
}
|
|
4674
|
+
const text = formatBp(nodeLengths.get(id));
|
|
4675
|
+
const halfW = labelHalfWidth(text);
|
|
4676
|
+
if (drawn >= halfW * 2 / MAX_LABEL_OVERHANG) {
|
|
4677
|
+
const screenX = x * scaleX + translateX;
|
|
4678
|
+
nodeCandidates.push({
|
|
4679
|
+
label: {
|
|
4680
|
+
key: `node:${id}`,
|
|
4681
|
+
text,
|
|
4682
|
+
x: screenX,
|
|
4683
|
+
y: screenY,
|
|
4684
|
+
kind: "node"
|
|
4685
|
+
},
|
|
4686
|
+
box: boxAt(halfW, screenX, screenY)
|
|
4687
|
+
});
|
|
4688
|
+
}
|
|
4689
|
+
}
|
|
4690
|
+
return { deletions: deletionCandidates, nodes: nodeCandidates };
|
|
4691
|
+
}
|
|
4692
|
+
function nodeLabelBudget(width, height) {
|
|
4693
|
+
return Math.max(
|
|
4694
|
+
MIN_NODE_LABELS,
|
|
4695
|
+
Math.floor(width * height / PX_PER_NODE_LABEL)
|
|
4696
|
+
);
|
|
4697
|
+
}
|
|
4698
|
+
function placeSizeLabels(candidates, take, budget = Infinity) {
|
|
4699
|
+
const labels = [];
|
|
4700
|
+
for (const { label, box } of candidates) {
|
|
4701
|
+
if (labels.length >= budget) {
|
|
4702
|
+
break;
|
|
4703
|
+
}
|
|
4704
|
+
if (take(box)) {
|
|
4705
|
+
labels.push(label);
|
|
4706
|
+
}
|
|
4707
|
+
}
|
|
4708
|
+
return labels;
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
// src/tubeMap/axis.ts
|
|
4712
|
+
function referenceBoxes(graph, layout) {
|
|
4713
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
4714
|
+
const byRefName = /* @__PURE__ */ new Map();
|
|
4715
|
+
layout.nodes.forEach((node) => {
|
|
4716
|
+
const graphNode = nodeById.get(node.name);
|
|
4717
|
+
if (node.order >= 0 && graphNode && isBackbone(graphNode)) {
|
|
4718
|
+
const { refName, start } = graphNode.stable;
|
|
4719
|
+
const boxes = byRefName.get(refName) ?? byRefName.set(refName, []).get(refName);
|
|
4720
|
+
boxes.push({
|
|
4721
|
+
bp0: start,
|
|
4722
|
+
bp1: start + node.sequenceLength,
|
|
4723
|
+
x0: node.x,
|
|
4724
|
+
x1: node.x + node.pixelWidth
|
|
4725
|
+
});
|
|
4726
|
+
}
|
|
4727
|
+
});
|
|
4728
|
+
for (const boxes of byRefName.values()) {
|
|
4729
|
+
boxes.sort((a, b) => a.bp0 - b.bp0);
|
|
4730
|
+
}
|
|
4731
|
+
return byRefName;
|
|
4732
|
+
}
|
|
4733
|
+
function tubeX(boxes, bp, isEnd = false) {
|
|
4734
|
+
let lo = 0;
|
|
4735
|
+
let hi = boxes.length - 1;
|
|
4736
|
+
while (lo < hi) {
|
|
4737
|
+
const mid = lo + hi >> 1;
|
|
4738
|
+
const bp1 = boxes[mid].bp1;
|
|
4739
|
+
if (isEnd ? bp1 < bp : bp1 <= bp) {
|
|
4740
|
+
lo = mid + 1;
|
|
4741
|
+
} else {
|
|
4742
|
+
hi = mid;
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
const box = boxes[lo];
|
|
4746
|
+
if (bp <= box.bp0) {
|
|
4747
|
+
return box.x0;
|
|
4748
|
+
}
|
|
4749
|
+
const span = box.bp1 - box.bp0;
|
|
4750
|
+
return span > 0 ? box.x0 + (Math.min(bp, box.bp1) - box.bp0) / span * (box.x1 - box.x0) : box.x0;
|
|
4751
|
+
}
|
|
4752
|
+
function tubeSpan(boxes, start, end) {
|
|
4753
|
+
const a = tubeX(boxes, start);
|
|
4754
|
+
const b = tubeX(boxes, end, true);
|
|
4755
|
+
return { x0: Math.min(a, b), x1: Math.max(a, b) };
|
|
4756
|
+
}
|
|
4757
|
+
function rulerBoxes(byRefName) {
|
|
4758
|
+
let best;
|
|
4759
|
+
for (const boxes of byRefName.values()) {
|
|
4760
|
+
if (!best || boxes.length > best.length) {
|
|
4761
|
+
best = boxes;
|
|
4762
|
+
}
|
|
4763
|
+
}
|
|
4764
|
+
return best;
|
|
4765
|
+
}
|
|
4766
|
+
var TICK_PX = 5;
|
|
4767
|
+
var LABEL_GAP_PX2 = 10;
|
|
4768
|
+
var TARGET_TICK_PX = 110;
|
|
4769
|
+
var ZIGZAG_PX = 4;
|
|
4770
|
+
var ZIGZAG_AMPLITUDE_PX = 2.5;
|
|
4771
|
+
function rulerInk(darkMode) {
|
|
4772
|
+
return darkMode ? "#b0b0b8" : "#55555c";
|
|
4773
|
+
}
|
|
4774
|
+
function niceStep(raw) {
|
|
4775
|
+
const pow = 10 ** Math.floor(Math.log10(raw));
|
|
4776
|
+
const m = raw / pow;
|
|
4777
|
+
return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;
|
|
4778
|
+
}
|
|
4779
|
+
function rulerMarks(boxes, x, width, measure) {
|
|
4780
|
+
const first = boxes[0];
|
|
4781
|
+
const last = boxes.at(-1);
|
|
4782
|
+
if (!first || !last) {
|
|
4783
|
+
return void 0;
|
|
4784
|
+
}
|
|
4785
|
+
const from = x(first.x0);
|
|
4786
|
+
const to = x(last.x1);
|
|
4787
|
+
const bpSpan = last.bp1 - first.bp0;
|
|
4788
|
+
if (to <= from || bpSpan <= 0) {
|
|
4789
|
+
return void 0;
|
|
4790
|
+
}
|
|
4791
|
+
const step = niceStep(bpSpan / (to - from) * TARGET_TICK_PX);
|
|
4792
|
+
const squeezedBoxes = boxes.filter((box) => {
|
|
4793
|
+
const bp = box.bp1 - box.bp0;
|
|
4794
|
+
return bp >= step && (x(box.x1) - x(box.x0)) * step / bp < TARGET_TICK_PX / 2;
|
|
4795
|
+
});
|
|
4796
|
+
const squeezed = [];
|
|
4797
|
+
const lengths = [];
|
|
4798
|
+
for (const box of squeezedBoxes) {
|
|
4799
|
+
const x0 = x(box.x0);
|
|
4800
|
+
const x1 = x(box.x1);
|
|
4801
|
+
if (x1 >= 0 && x0 <= width) {
|
|
4802
|
+
squeezed.push({ x0, x1 });
|
|
4803
|
+
const text = formatBp(box.bp1 - box.bp0);
|
|
4804
|
+
lengths.push({ text, at: (x0 + x1) / 2, width: measure(text) });
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
const ticks = [];
|
|
4808
|
+
const positions = [];
|
|
4809
|
+
let k = 0;
|
|
4810
|
+
for (let bp = Math.ceil(first.bp0 / step) * step; bp <= last.bp1; bp += step) {
|
|
4811
|
+
while (k < squeezedBoxes.length && squeezedBoxes[k].bp1 <= bp) {
|
|
4812
|
+
k++;
|
|
4813
|
+
}
|
|
4814
|
+
if (k < squeezedBoxes.length && squeezedBoxes[k].bp0 < bp) {
|
|
4815
|
+
continue;
|
|
4816
|
+
}
|
|
4817
|
+
const sx = Math.round(x(tubeX(boxes, bp))) + 0.5;
|
|
4818
|
+
if (sx >= 0 && sx <= width) {
|
|
4819
|
+
ticks.push(sx);
|
|
4820
|
+
const text = bp.toLocaleString("en-US");
|
|
4821
|
+
positions.push({ text, at: sx, width: measure(text) });
|
|
4707
4822
|
}
|
|
4708
|
-
halos.push({
|
|
4709
|
-
bubble,
|
|
4710
|
-
...classifyBubble(bubble, repeats),
|
|
4711
|
-
path: parts.join(""),
|
|
4712
|
-
labelAt,
|
|
4713
|
-
members: nodeIds.length,
|
|
4714
|
-
nodeIds,
|
|
4715
|
-
whole: nodeIds.length >= WHOLE_FRACTION * graph.nodes.length,
|
|
4716
|
-
routes: routes2
|
|
4717
|
-
});
|
|
4718
4823
|
}
|
|
4719
|
-
|
|
4824
|
+
const placed = [];
|
|
4825
|
+
const clear = (a) => placed.every(
|
|
4826
|
+
(b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX2
|
|
4827
|
+
);
|
|
4828
|
+
for (const label of [...positions, ...lengths]) {
|
|
4829
|
+
if (clear(label)) {
|
|
4830
|
+
placed.push(label);
|
|
4831
|
+
}
|
|
4832
|
+
}
|
|
4833
|
+
return {
|
|
4834
|
+
from,
|
|
4835
|
+
to,
|
|
4836
|
+
ticks,
|
|
4837
|
+
squeezed,
|
|
4838
|
+
labels: placed.map(({ text, at }) => ({ text, at }))
|
|
4839
|
+
};
|
|
4720
4840
|
}
|
|
4721
|
-
function
|
|
4722
|
-
const
|
|
4723
|
-
const
|
|
4724
|
-
|
|
4725
|
-
|
|
4841
|
+
function zigzag(ctx, x0, x1, y) {
|
|
4842
|
+
const teeth = Math.max(1, Math.round((x1 - x0) / ZIGZAG_PX));
|
|
4843
|
+
const dx = (x1 - x0) / teeth;
|
|
4844
|
+
for (let i = 0; i < teeth; i++) {
|
|
4845
|
+
ctx.lineTo(x0 + (i + 0.5) * dx, y + (i % 2 ? 1 : -1) * ZIGZAG_AMPLITUDE_PX);
|
|
4846
|
+
}
|
|
4847
|
+
ctx.lineTo(x1, y);
|
|
4848
|
+
}
|
|
4849
|
+
function drawTubeMapRuler(ctx, boxes, frame, top) {
|
|
4850
|
+
const { x, width, darkMode } = frame;
|
|
4851
|
+
ctx.font = "10px sans-serif";
|
|
4852
|
+
const marks = rulerMarks(boxes, x, width, (text) => ctx.measureText(text).width);
|
|
4853
|
+
if (!marks) {
|
|
4854
|
+
return;
|
|
4855
|
+
}
|
|
4856
|
+
const ink = rulerInk(darkMode);
|
|
4857
|
+
const y = top + 0.5;
|
|
4858
|
+
ctx.strokeStyle = ink;
|
|
4859
|
+
ctx.fillStyle = ink;
|
|
4860
|
+
ctx.lineWidth = 1;
|
|
4861
|
+
ctx.textAlign = "center";
|
|
4862
|
+
ctx.textBaseline = "top";
|
|
4863
|
+
ctx.beginPath();
|
|
4864
|
+
ctx.moveTo(Math.max(0, marks.from), y);
|
|
4865
|
+
for (const { x0, x1 } of marks.squeezed) {
|
|
4866
|
+
ctx.lineTo(x0, y);
|
|
4867
|
+
zigzag(ctx, x0, x1, y);
|
|
4868
|
+
}
|
|
4869
|
+
ctx.lineTo(Math.min(width, marks.to), y);
|
|
4870
|
+
for (const sx of marks.ticks) {
|
|
4871
|
+
ctx.moveTo(sx, top);
|
|
4872
|
+
ctx.lineTo(sx, top + TICK_PX);
|
|
4873
|
+
}
|
|
4874
|
+
ctx.stroke();
|
|
4875
|
+
for (const { text, at } of marks.labels) {
|
|
4876
|
+
ctx.fillText(text, at, top + TICK_PX + 2);
|
|
4877
|
+
}
|
|
4726
4878
|
}
|
|
4727
4879
|
|
|
4728
|
-
// src/
|
|
4729
|
-
function
|
|
4730
|
-
const
|
|
4731
|
-
const
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4880
|
+
// src/tubeMap/genes.ts
|
|
4881
|
+
function tubeMapGenes(byRefName, genes) {
|
|
4882
|
+
const out = [];
|
|
4883
|
+
for (const gene of genes) {
|
|
4884
|
+
const boxes = byRefName.get(gene.refName);
|
|
4885
|
+
const first = boxes?.[0];
|
|
4886
|
+
const last = boxes?.at(-1);
|
|
4887
|
+
if (!boxes || !first || !last) {
|
|
4888
|
+
continue;
|
|
4889
|
+
}
|
|
4890
|
+
const start = Math.max(gene.start, first.bp0);
|
|
4891
|
+
const end = Math.min(gene.end, last.bp1);
|
|
4892
|
+
if (end <= start) {
|
|
4893
|
+
continue;
|
|
4894
|
+
}
|
|
4895
|
+
out.push({
|
|
4896
|
+
gene,
|
|
4897
|
+
...tubeSpan(boxes, start, end),
|
|
4898
|
+
exons: gene.exons.filter((e) => e.end > start && e.start < end).map(
|
|
4899
|
+
(e) => tubeSpan(boxes, Math.max(e.start, start), Math.min(e.end, end))
|
|
4900
|
+
)
|
|
4901
|
+
});
|
|
4902
|
+
}
|
|
4903
|
+
return out;
|
|
4904
|
+
}
|
|
4905
|
+
var GENE_ROW_PX = 22;
|
|
4906
|
+
var EXON_PX = 8;
|
|
4907
|
+
var NAME_FONT = "11px sans-serif";
|
|
4908
|
+
var NAME_GAP_PX = 8;
|
|
4909
|
+
function drawTubeMapGenes(ctx, genes, frame, bottom, top = 0) {
|
|
4910
|
+
const { x, width, darkMode } = frame;
|
|
4911
|
+
const rows = Math.max(1, Math.floor((bottom - top) / GENE_ROW_PX));
|
|
4912
|
+
const rowEnds = [];
|
|
4913
|
+
const ink = darkMode ? "#e0e0e6" : "#1c1c22";
|
|
4914
|
+
ctx.font = NAME_FONT;
|
|
4915
|
+
ctx.textAlign = "center";
|
|
4916
|
+
ctx.textBaseline = "alphabetic";
|
|
4917
|
+
ctx.fillStyle = ink;
|
|
4918
|
+
ctx.strokeStyle = ink;
|
|
4919
|
+
ctx.lineWidth = 1;
|
|
4920
|
+
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);
|
|
4921
|
+
for (const { g, s0, s1 } of placed) {
|
|
4922
|
+
const arrow = g.gene.strand > 0 ? " \u2192" : g.gene.strand < 0 ? "\u2190 " : "";
|
|
4923
|
+
const label = g.gene.strand < 0 ? `${arrow}${g.gene.name}` : `${g.gene.name}${arrow}`;
|
|
4924
|
+
const mid = (Math.max(s0, 0) + Math.min(s1, width)) / 2;
|
|
4925
|
+
const half = ctx.measureText(label).width / 2;
|
|
4926
|
+
const left = Math.min(s0, mid - half);
|
|
4927
|
+
const right = Math.max(s1, mid + half);
|
|
4928
|
+
let row = rowEnds.findIndex((end) => end + NAME_GAP_PX <= left);
|
|
4929
|
+
if (row === -1) {
|
|
4930
|
+
row = Math.min(rowEnds.length, rows - 1);
|
|
4931
|
+
}
|
|
4932
|
+
rowEnds[row] = Math.max(rowEnds[row] ?? -Infinity, right);
|
|
4933
|
+
const glyphY = bottom - row * GENE_ROW_PX - EXON_PX / 2;
|
|
4934
|
+
ctx.beginPath();
|
|
4935
|
+
ctx.moveTo(s0, glyphY + 0.5);
|
|
4936
|
+
ctx.lineTo(s1, glyphY + 0.5);
|
|
4937
|
+
ctx.stroke();
|
|
4938
|
+
for (const e of g.exons) {
|
|
4939
|
+
const e0 = x(e.x0);
|
|
4940
|
+
ctx.fillRect(e0, glyphY - EXON_PX / 2, Math.max(1, x(e.x1) - e0), EXON_PX);
|
|
4941
|
+
}
|
|
4942
|
+
ctx.fillText(label, mid, glyphY - EXON_PX / 2 - 2);
|
|
4943
|
+
}
|
|
4744
4944
|
}
|
|
4745
4945
|
|
|
4746
|
-
// src/
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4946
|
+
// src/tubeMap/connectors.ts
|
|
4947
|
+
function referenceNodes(graph, layout) {
|
|
4948
|
+
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
4949
|
+
const out = [];
|
|
4950
|
+
layout.nodes.forEach((node) => {
|
|
4951
|
+
const graphNode = nodeById.get(node.name);
|
|
4952
|
+
if (node.order >= 0 && graphNode && isBackbone(graphNode)) {
|
|
4953
|
+
out.push({
|
|
4954
|
+
node: node.name,
|
|
4955
|
+
bp0: graphNode.stable.start,
|
|
4956
|
+
bp1: graphNode.stable.start + node.sequenceLength,
|
|
4957
|
+
x0: node.x,
|
|
4958
|
+
x1: node.x + node.pixelWidth
|
|
4959
|
+
});
|
|
4960
|
+
}
|
|
4961
|
+
});
|
|
4962
|
+
return out;
|
|
4752
4963
|
}
|
|
4753
|
-
function
|
|
4754
|
-
|
|
4964
|
+
function tubeMapConnectors(nodes, bpToScreen, tubeToScreen, width) {
|
|
4965
|
+
const out = [];
|
|
4966
|
+
for (const n of nodes) {
|
|
4967
|
+
const bottom0 = tubeToScreen(n.x0);
|
|
4968
|
+
const bottom1 = tubeToScreen(n.x1);
|
|
4969
|
+
if (bottom1 >= 0 && bottom0 <= width) {
|
|
4970
|
+
out.push({
|
|
4971
|
+
node: n.node,
|
|
4972
|
+
top0: bpToScreen(n.bp0),
|
|
4973
|
+
top1: bpToScreen(n.bp1),
|
|
4974
|
+
bottom0,
|
|
4975
|
+
bottom1
|
|
4976
|
+
});
|
|
4977
|
+
}
|
|
4978
|
+
}
|
|
4979
|
+
return out;
|
|
4755
4980
|
}
|
|
4756
|
-
|
|
4757
|
-
|
|
4981
|
+
var HIT_SLOP_PX = 3;
|
|
4982
|
+
function connectorAt(connectors, zoneBottom, sx, sy) {
|
|
4983
|
+
if (zoneBottom <= 0 || sy < 0 || sy > zoneBottom) {
|
|
4984
|
+
return void 0;
|
|
4985
|
+
}
|
|
4986
|
+
const t = sy / zoneBottom;
|
|
4987
|
+
let hit;
|
|
4988
|
+
let best = HIT_SLOP_PX;
|
|
4989
|
+
for (const c of connectors) {
|
|
4990
|
+
const a = c.top0 + (c.bottom0 - c.top0) * t;
|
|
4991
|
+
const b = c.top1 + (c.bottom1 - c.top1) * t;
|
|
4992
|
+
const dist = Math.max(0, Math.min(a, b) - sx, sx - Math.max(a, b));
|
|
4993
|
+
if (dist < best) {
|
|
4994
|
+
best = dist;
|
|
4995
|
+
hit = c.node;
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
return hit;
|
|
4758
4999
|
}
|
|
4759
|
-
function
|
|
4760
|
-
|
|
5000
|
+
function band(ctx, c, bottom) {
|
|
5001
|
+
ctx.moveTo(c.top0, 0);
|
|
5002
|
+
ctx.lineTo(c.top1, 0);
|
|
5003
|
+
ctx.lineTo(c.bottom1, bottom);
|
|
5004
|
+
ctx.lineTo(c.bottom0, bottom);
|
|
5005
|
+
ctx.closePath();
|
|
5006
|
+
}
|
|
5007
|
+
function sides(ctx, c, bottom) {
|
|
5008
|
+
ctx.moveTo(c.top0, 0);
|
|
5009
|
+
ctx.lineTo(c.bottom0, bottom);
|
|
5010
|
+
ctx.moveTo(c.top1, 0);
|
|
5011
|
+
ctx.lineTo(c.bottom1, bottom);
|
|
5012
|
+
}
|
|
5013
|
+
function drawTubeMapConnectors(ctx, connectors, bottom, {
|
|
5014
|
+
highlightNode,
|
|
5015
|
+
darkMode
|
|
5016
|
+
}) {
|
|
5017
|
+
if (bottom <= 0 || connectors.length === 0) {
|
|
5018
|
+
return;
|
|
5019
|
+
}
|
|
5020
|
+
const ink = darkMode ? "255,255,255" : "0,0,0";
|
|
5021
|
+
ctx.beginPath();
|
|
5022
|
+
for (const c of connectors) {
|
|
5023
|
+
band(ctx, c, bottom);
|
|
5024
|
+
}
|
|
5025
|
+
ctx.fillStyle = `rgba(${ink},0.07)`;
|
|
5026
|
+
ctx.fill();
|
|
5027
|
+
ctx.beginPath();
|
|
5028
|
+
for (const c of connectors) {
|
|
5029
|
+
sides(ctx, c, bottom);
|
|
5030
|
+
}
|
|
5031
|
+
ctx.strokeStyle = `rgba(${ink},0.35)`;
|
|
5032
|
+
ctx.lineWidth = 0.5;
|
|
5033
|
+
ctx.stroke();
|
|
5034
|
+
const lit = connectors.find((c) => c.node === highlightNode);
|
|
5035
|
+
if (lit) {
|
|
5036
|
+
ctx.beginPath();
|
|
5037
|
+
band(ctx, lit, bottom);
|
|
5038
|
+
ctx.fillStyle = "rgba(255,192,203,0.5)";
|
|
5039
|
+
ctx.fill();
|
|
5040
|
+
ctx.beginPath();
|
|
5041
|
+
sides(ctx, lit, bottom);
|
|
5042
|
+
ctx.strokeStyle = "#ff0000";
|
|
5043
|
+
ctx.lineWidth = 1;
|
|
5044
|
+
ctx.stroke();
|
|
5045
|
+
}
|
|
4761
5046
|
}
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
5047
|
+
|
|
5048
|
+
// src/walkHighlight.ts
|
|
5049
|
+
function walkHighlight(graph, name) {
|
|
5050
|
+
const path = graph.paths?.find((p) => p.name === name);
|
|
5051
|
+
if (!path) {
|
|
5052
|
+
return void 0;
|
|
5053
|
+
}
|
|
5054
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5055
|
+
const edgeAt = /* @__PURE__ */ new Map();
|
|
5056
|
+
graph.edges.forEach((e, i) => {
|
|
5057
|
+
edgeAt.set(`${e.from}>${e.to}`, i);
|
|
5058
|
+
});
|
|
5059
|
+
const nodeIds = new Set(path.nodeIds);
|
|
5060
|
+
const edgeIndexes = /* @__PURE__ */ new Set();
|
|
5061
|
+
for (let i = 1; i < path.nodeIds.length; i++) {
|
|
5062
|
+
const a = path.nodeIds[i - 1];
|
|
5063
|
+
const b = path.nodeIds[i];
|
|
5064
|
+
const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
|
|
5065
|
+
if (index !== void 0) {
|
|
5066
|
+
edgeIndexes.add(index);
|
|
4767
5067
|
}
|
|
4768
|
-
|
|
4769
|
-
|
|
5068
|
+
}
|
|
5069
|
+
const bpOf = (ids) => ids.reduce((sum, id) => sum + (byId.get(id)?.length ?? 0), 0);
|
|
5070
|
+
const reference = graph.referencePath ? graph.paths.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
|
|
5071
|
+
return {
|
|
5072
|
+
name,
|
|
5073
|
+
nodeIds,
|
|
5074
|
+
edgeIndexes,
|
|
5075
|
+
steps: path.nodeIds.length,
|
|
5076
|
+
bp: bpOf(path.nodeIds),
|
|
5077
|
+
referenceBp: reference && reference !== path ? bpOf(reference.nodeIds) : void 0
|
|
4770
5078
|
};
|
|
4771
5079
|
}
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
5080
|
+
|
|
5081
|
+
// src/bubbles/bubbleLine.ts
|
|
5082
|
+
var SATURATED_PATH_COUNT = 2147483647;
|
|
5083
|
+
|
|
5084
|
+
// src/bubbles/bubblesFromGraph.ts
|
|
5085
|
+
function bubblesFromGraph(graph) {
|
|
5086
|
+
if (!graph.nodes.some(isBackbone)) {
|
|
5087
|
+
return [];
|
|
5088
|
+
}
|
|
5089
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5090
|
+
const order = referenceOrder(graph);
|
|
5091
|
+
const indexOf = new Map(order.map((id, i) => [id, i]));
|
|
5092
|
+
const reach = new Int32Array(order.length + 1);
|
|
5093
|
+
const succ = /* @__PURE__ */ new Map();
|
|
5094
|
+
const linked = /* @__PURE__ */ new Set();
|
|
5095
|
+
for (const e of graph.edges) {
|
|
5096
|
+
const a = indexOf.get(e.from);
|
|
5097
|
+
const b = indexOf.get(e.to);
|
|
5098
|
+
if (a === void 0 || b === void 0 || a === b) {
|
|
5099
|
+
continue;
|
|
5100
|
+
}
|
|
5101
|
+
linked.add(`${e.from}>${e.to}`).add(`${e.to}>${e.from}`);
|
|
5102
|
+
const [lo, hi] = a < b ? [a, b] : [b, a];
|
|
5103
|
+
reach[lo + 1]++;
|
|
5104
|
+
reach[hi]--;
|
|
5105
|
+
const [from, to] = a < b ? [e.from, e.to] : [e.to, e.from];
|
|
5106
|
+
(succ.get(from) ?? succ.set(from, /* @__PURE__ */ new Set()).get(from)).add(to);
|
|
5107
|
+
}
|
|
5108
|
+
for (let i = 1; i < reach.length; i++) {
|
|
5109
|
+
reach[i] += reach[i - 1];
|
|
5110
|
+
}
|
|
5111
|
+
const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
|
|
5112
|
+
const ends = /* @__PURE__ */ new Set([backbone[0].id, backbone.at(-1).id]);
|
|
5113
|
+
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);
|
|
5114
|
+
const walkIndex = graph.paths?.map((p) => {
|
|
5115
|
+
const at = /* @__PURE__ */ new Map();
|
|
5116
|
+
p.nodeIds.forEach((id, i) => {
|
|
5117
|
+
if (!at.has(id)) {
|
|
5118
|
+
at.set(id, i);
|
|
4788
5119
|
}
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
5120
|
+
});
|
|
5121
|
+
return at;
|
|
5122
|
+
});
|
|
5123
|
+
const bubbles = [];
|
|
5124
|
+
for (let i = 0; i + 1 < boundaries.length; i++) {
|
|
5125
|
+
const [start, i0] = boundaries[i];
|
|
5126
|
+
const [end, i1] = boundaries[i + 1];
|
|
5127
|
+
const interior = order.slice(i0 + 1, i1);
|
|
5128
|
+
if (interior.length === 0) {
|
|
5129
|
+
continue;
|
|
5130
|
+
}
|
|
5131
|
+
const best = /* @__PURE__ */ new Map([[start.id, { min: 0, max: 0, n: 1 }]]);
|
|
5132
|
+
for (const id of [start.id, ...interior]) {
|
|
5133
|
+
const cur = best.get(id);
|
|
5134
|
+
if (!cur) {
|
|
5135
|
+
continue;
|
|
5136
|
+
}
|
|
5137
|
+
for (const t of succ.get(id) ?? []) {
|
|
5138
|
+
const ti = indexOf.get(t);
|
|
5139
|
+
if (ti > i1) {
|
|
5140
|
+
continue;
|
|
5141
|
+
}
|
|
5142
|
+
const add = t === end.id ? 0 : byId.get(t).length;
|
|
5143
|
+
const prev = best.get(t);
|
|
5144
|
+
best.set(t, {
|
|
5145
|
+
min: Math.min(prev?.min ?? Infinity, cur.min + add),
|
|
5146
|
+
max: Math.max(prev?.max ?? -Infinity, cur.max + add),
|
|
5147
|
+
n: Math.min(SATURATED_PATH_COUNT, cur.n + (prev?.n ?? 0))
|
|
5148
|
+
});
|
|
5149
|
+
}
|
|
5150
|
+
}
|
|
5151
|
+
const walked = walkIndex && walkRoutes(graph, byId, walkIndex, start.id, end.id);
|
|
5152
|
+
const crossed = walked?.n ? walked : void 0;
|
|
5153
|
+
const routes2 = crossed ?? best.get(end.id) ?? { min: 0, max: 0, n: 0 };
|
|
5154
|
+
const walksLeave = walked !== void 0 && walked.left > 0;
|
|
5155
|
+
const refStart = start.stable.start + start.length;
|
|
5156
|
+
const refEnd = end.stable.start;
|
|
5157
|
+
const chain = [
|
|
5158
|
+
start.id,
|
|
5159
|
+
...interior.filter((id) => isBackbone(byId.get(id))),
|
|
5160
|
+
end.id
|
|
5161
|
+
];
|
|
5162
|
+
const chainBroken = chain.some(
|
|
5163
|
+
(id, k) => k > 0 && !linked.has(`${chain[k - 1]}>${id}`)
|
|
5164
|
+
);
|
|
5165
|
+
const partial = chainBroken || walksLeave;
|
|
5166
|
+
const fallback = chainBroken && !crossed;
|
|
5167
|
+
bubbles.push({
|
|
5168
|
+
refName: start.stable.refName,
|
|
5169
|
+
start: refStart,
|
|
5170
|
+
end: refEnd,
|
|
5171
|
+
segmentCount: interior.length + 2,
|
|
5172
|
+
pathCount: routes2.n,
|
|
5173
|
+
// a real inversion test needs each walk's direction relative to the
|
|
5174
|
+
// reference; the first visit's strand is just whichever path anchored the
|
|
5175
|
+
// node
|
|
5176
|
+
inversion: false,
|
|
5177
|
+
shortestAlleleLength: fallback ? refEnd - refStart : routes2.min,
|
|
5178
|
+
longestAlleleLength: fallback ? refEnd - refStart : routes2.max,
|
|
5179
|
+
segments: [start, ...interior.map((id) => byId.get(id)), end].map((n) => n.name).join(","),
|
|
5180
|
+
shortestAllele: void 0,
|
|
5181
|
+
longestAllele: void 0,
|
|
5182
|
+
partial,
|
|
5183
|
+
routes: crossed?.routes
|
|
5184
|
+
});
|
|
5185
|
+
}
|
|
5186
|
+
return bubbles;
|
|
5187
|
+
}
|
|
5188
|
+
function walkRoutes(graph, byId, walkIndex, startId, endId) {
|
|
5189
|
+
const seen = /* @__PURE__ */ new Map();
|
|
5190
|
+
let min = Infinity;
|
|
5191
|
+
let max = -Infinity;
|
|
5192
|
+
let left = 0;
|
|
5193
|
+
graph.paths.forEach((p, k) => {
|
|
5194
|
+
const i0 = walkIndex[k].get(startId);
|
|
5195
|
+
const i1 = walkIndex[k].get(endId);
|
|
5196
|
+
if (i0 === void 0 || i1 === void 0) {
|
|
5197
|
+
if (i0 !== void 0 || i1 !== void 0) {
|
|
5198
|
+
left++;
|
|
4792
5199
|
}
|
|
5200
|
+
return;
|
|
4793
5201
|
}
|
|
4794
|
-
|
|
4795
|
-
|
|
5202
|
+
const steps = p.nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
|
|
5203
|
+
if (i1 < i0) {
|
|
5204
|
+
steps.reverse();
|
|
5205
|
+
}
|
|
5206
|
+
let bp = 0;
|
|
5207
|
+
for (const id of steps) {
|
|
5208
|
+
bp += byId.get(id)?.length ?? 0;
|
|
5209
|
+
}
|
|
5210
|
+
const key = steps.join(",");
|
|
5211
|
+
const route = seen.get(key) ?? { steps, bp, walks: [] };
|
|
5212
|
+
route.walks.push(p.name);
|
|
5213
|
+
seen.set(key, route);
|
|
5214
|
+
min = Math.min(min, bp);
|
|
5215
|
+
max = Math.max(max, bp);
|
|
5216
|
+
});
|
|
5217
|
+
return seen.size || left ? { min, max, n: seen.size, left, routes: [...seen.values()] } : void 0;
|
|
4796
5218
|
}
|
|
4797
5219
|
|
|
4798
|
-
// src/
|
|
4799
|
-
var
|
|
4800
|
-
|
|
4801
|
-
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
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
|
-
};
|
|
4820
|
-
}
|
|
4821
|
-
function rowLabelBox(text, screenY) {
|
|
4822
|
-
return {
|
|
4823
|
-
x0: 6,
|
|
4824
|
-
x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
|
|
4825
|
-
y0: screenY - 8 - LABEL_GAP_PX / 2,
|
|
4826
|
-
y1: screenY + 8 + LABEL_GAP_PX / 2
|
|
4827
|
-
};
|
|
4828
|
-
}
|
|
4829
|
-
function clamp(v, lo, hi) {
|
|
4830
|
-
return hi < lo ? v : Math.min(Math.max(v, lo), hi);
|
|
5220
|
+
// src/bubbles/classifyBubble.ts
|
|
5221
|
+
var REPEAT_GROWTH = 10;
|
|
5222
|
+
function coveringRepeat(b, repeats) {
|
|
5223
|
+
const span = b.end - b.start;
|
|
5224
|
+
const growth = b.longestAlleleLength - b.shortestAlleleLength;
|
|
5225
|
+
return repeats.find(
|
|
5226
|
+
(r) => growth <= REPEAT_GROWTH * (r.end - r.start) && (span === 0 ? r.start <= b.start && b.start <= r.end : Math.min(b.end, r.end) - Math.max(b.start, r.start) >= span / 2)
|
|
5227
|
+
);
|
|
4831
5228
|
}
|
|
5229
|
+
var SUPERBUBBLE_SEGMENTS = 40;
|
|
4832
5230
|
function formatBp2(bp) {
|
|
4833
|
-
if (bp >=
|
|
4834
|
-
return `${
|
|
5231
|
+
if (bp >= 1e4) {
|
|
5232
|
+
return `${(bp / 1e3).toFixed(0)} kb`;
|
|
4835
5233
|
}
|
|
4836
|
-
|
|
4837
|
-
}
|
|
4838
|
-
|
|
4839
|
-
return `${
|
|
4840
|
-
}
|
|
4841
|
-
function arcOutward(curves, apex, scaleX, scaleY) {
|
|
4842
|
-
const first = curves[0];
|
|
4843
|
-
const last = curves[curves.length - 1];
|
|
4844
|
-
const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
|
|
4845
|
-
const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
|
|
4846
|
-
const len = Math.hypot(dx, dy);
|
|
4847
|
-
return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
|
|
4848
|
-
}
|
|
4849
|
-
function boxSupport(box, dir) {
|
|
4850
|
-
return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
|
|
5234
|
+
if (bp >= 1e3) {
|
|
5235
|
+
return `${(bp / 1e3).toFixed(1)} kb`;
|
|
5236
|
+
}
|
|
5237
|
+
return `${bp} bp`;
|
|
4851
5238
|
}
|
|
4852
|
-
function
|
|
4853
|
-
const
|
|
4854
|
-
const
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
|
|
4858
|
-
);
|
|
5239
|
+
function formatBpRange(shortest, longest) {
|
|
5240
|
+
const low = formatBp2(shortest);
|
|
5241
|
+
const high = formatBp2(longest);
|
|
5242
|
+
const unit = high.slice(high.indexOf(" "));
|
|
5243
|
+
return low.endsWith(unit) ? `${low.slice(0, -unit.length)}\u2013${high}` : `${low}\u2013${high}`;
|
|
4859
5244
|
}
|
|
4860
|
-
function
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
const a = segments[i - 1];
|
|
4864
|
-
const b = segments[i];
|
|
4865
|
-
total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
|
|
5245
|
+
function routes(count) {
|
|
5246
|
+
if (count >= SATURATED_PATH_COUNT) {
|
|
5247
|
+
return "\u22652.1B routes";
|
|
4866
5248
|
}
|
|
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
|
-
}
|
|
5249
|
+
if (count >= 1e5) {
|
|
5250
|
+
return `${(count / 1e3).toFixed(0)}k routes`;
|
|
4881
5251
|
}
|
|
4882
|
-
return
|
|
5252
|
+
return `${count} routes`;
|
|
4883
5253
|
}
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
if (cached2?.nodeLengths === nodeLengths) {
|
|
4888
|
-
return cached2.order;
|
|
4889
|
-
}
|
|
4890
|
-
const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
|
|
4891
|
-
nodeOrderCache.set(nodePositions, { nodeLengths, order });
|
|
4892
|
-
return order;
|
|
5254
|
+
function classifyBubble(b, repeats = []) {
|
|
5255
|
+
const c = classifyShape(b, repeats);
|
|
5256
|
+
return b.partial ? { ...c, label: `${c.label}, partial` } : c;
|
|
4893
5257
|
}
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
const {
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
5258
|
+
function classifyShape(b, repeats) {
|
|
5259
|
+
const refSpan = b.end - b.start;
|
|
5260
|
+
const {
|
|
5261
|
+
shortestAlleleLength: shortest,
|
|
5262
|
+
longestAlleleLength: longest,
|
|
5263
|
+
pathCount,
|
|
5264
|
+
segmentCount,
|
|
5265
|
+
inversion
|
|
5266
|
+
} = b;
|
|
5267
|
+
const range = formatBpRange(shortest, longest);
|
|
5268
|
+
if (segmentCount >= SUPERBUBBLE_SEGMENTS) {
|
|
5269
|
+
return {
|
|
5270
|
+
kind: "superbubble",
|
|
5271
|
+
label: `${range} superbubble, ${segmentCount} segments, ${routes(pathCount)}${inversion ? ", inv" : ""}`
|
|
5272
|
+
};
|
|
4900
5273
|
}
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
const curves = deletionArcCurves(nodePositions, deletion, axis);
|
|
4904
|
-
const apex = curves ? curveMidpoint(curves) : void 0;
|
|
4905
|
-
if (curves && apex) {
|
|
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
|
-
}
|
|
5274
|
+
if (inversion) {
|
|
5275
|
+
return { kind: "inversion", label: `${formatBp2(longest)} inv` };
|
|
4915
5276
|
}
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
}
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
const nodeCandidates = [];
|
|
4934
|
-
for (const { deletion, curves, apex, extent } of arcPlacements(
|
|
4935
|
-
nodePositions,
|
|
4936
|
-
deletions,
|
|
4937
|
-
axis,
|
|
4938
|
-
version
|
|
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
|
-
};
|
|
4966
|
-
}
|
|
4967
|
-
deletionCandidates.push({
|
|
4968
|
-
label: {
|
|
4969
|
-
key: `del:${deletion.edgeIndex}`,
|
|
4970
|
-
text,
|
|
4971
|
-
x,
|
|
4972
|
-
y,
|
|
4973
|
-
kind: "deletion",
|
|
4974
|
-
leader
|
|
4975
|
-
},
|
|
4976
|
-
box: boxAt(halfW, x, y)
|
|
4977
|
-
});
|
|
4978
|
-
}
|
|
5277
|
+
const repeat = longest !== shortest ? coveringRepeat(b, repeats) : void 0;
|
|
5278
|
+
if (repeat) {
|
|
5279
|
+
const name = repeat.name ? ` (${repeat.name})` : "";
|
|
5280
|
+
return {
|
|
5281
|
+
kind: "repeat",
|
|
5282
|
+
label: `${range} repeat array${name}, ${routes(pathCount)}`
|
|
5283
|
+
};
|
|
5284
|
+
}
|
|
5285
|
+
const alleles = pathCount > 2 ? `, ${pathCount} alleles` : "";
|
|
5286
|
+
if (shortest === longest && shortest === refSpan) {
|
|
5287
|
+
return refSpan <= 1 ? { kind: "snp", label: "SNP" } : { kind: "substitution", label: `${formatBp2(refSpan)} sub` };
|
|
5288
|
+
}
|
|
5289
|
+
if (refSpan === 0 || shortest === refSpan) {
|
|
5290
|
+
return {
|
|
5291
|
+
kind: "insertion",
|
|
5292
|
+
label: `\u2264${formatBp2(longest - refSpan)} ins${alleles}`
|
|
5293
|
+
};
|
|
4979
5294
|
}
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
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);
|
|
5000
|
-
}
|
|
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
|
-
}
|
|
5295
|
+
if (longest === refSpan) {
|
|
5296
|
+
return {
|
|
5297
|
+
kind: "deletion",
|
|
5298
|
+
label: `${formatBp2(refSpan - shortest)} del${alleles}`
|
|
5299
|
+
};
|
|
5018
5300
|
}
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5301
|
+
return {
|
|
5302
|
+
kind: "complex",
|
|
5303
|
+
label: `${formatBp2(refSpan)} ref \u2192 ${range}, ${pathCount} alleles`
|
|
5304
|
+
};
|
|
5305
|
+
}
|
|
5306
|
+
var BUBBLE_KIND_COLORS = {
|
|
5307
|
+
snp: "#5b6b7a",
|
|
5308
|
+
substitution: "#5b6b7a",
|
|
5309
|
+
insertion: "#2f8fd6",
|
|
5310
|
+
deletion: "#c94040",
|
|
5311
|
+
inversion: "#e07b00",
|
|
5312
|
+
repeat: "#8e3fbf",
|
|
5313
|
+
superbubble: "#8e3fbf",
|
|
5314
|
+
complex: "#8e3fbf"
|
|
5315
|
+
};
|
|
5316
|
+
var BUBBLE_KIND_NAMES = {
|
|
5317
|
+
snp: "SNP",
|
|
5318
|
+
substitution: "substitution",
|
|
5319
|
+
insertion: "insertion",
|
|
5320
|
+
deletion: "deletion",
|
|
5321
|
+
inversion: "inversion",
|
|
5322
|
+
repeat: "repeat array",
|
|
5323
|
+
superbubble: "superbubble",
|
|
5324
|
+
complex: "site"
|
|
5325
|
+
};
|
|
5326
|
+
function bubbleSegmentIds(b) {
|
|
5327
|
+
return b.segments.split(",").filter(Boolean);
|
|
5328
|
+
}
|
|
5329
|
+
|
|
5330
|
+
// src/bubbles/bubbleHalos.ts
|
|
5331
|
+
var WHOLE_FRACTION = 0.9;
|
|
5332
|
+
var NAMED_WALKS = 2;
|
|
5333
|
+
var MIN_CHIPPED_BP = 1e3;
|
|
5334
|
+
function bubbleHalos(graph, bubbles, positions, walkLabel = (name) => name, repeats = []) {
|
|
5335
|
+
const byName = new Map(graph.nodes.map((n) => [n.name, n]));
|
|
5336
|
+
const halos = [];
|
|
5337
|
+
for (const bubble of bubbles) {
|
|
5338
|
+
const parts = [];
|
|
5339
|
+
let minX = Infinity;
|
|
5340
|
+
let maxX = -Infinity;
|
|
5341
|
+
let minY = Infinity;
|
|
5342
|
+
const nodeIds = [];
|
|
5343
|
+
const ends = [];
|
|
5344
|
+
for (const name of bubbleSegmentIds(bubble)) {
|
|
5345
|
+
const node = byName.get(name);
|
|
5346
|
+
const line = node && positions[node.id];
|
|
5347
|
+
if (!node || !line?.length) {
|
|
5348
|
+
continue;
|
|
5349
|
+
}
|
|
5350
|
+
if (isBackbone(node) && (node.stable.start < bubble.start || node.stable.start >= bubble.end)) {
|
|
5351
|
+
ends.push(line[Math.floor(line.length / 2)]);
|
|
5352
|
+
continue;
|
|
5353
|
+
}
|
|
5354
|
+
nodeIds.push(node.id);
|
|
5355
|
+
parts.push(svgPath(line.length === 1 ? [line[0], line[0]] : line));
|
|
5356
|
+
for (const p of line) {
|
|
5357
|
+
minX = Math.min(minX, p.x);
|
|
5358
|
+
maxX = Math.max(maxX, p.x);
|
|
5359
|
+
minY = Math.min(minY, p.y);
|
|
5360
|
+
}
|
|
5022
5361
|
}
|
|
5023
|
-
|
|
5024
|
-
const screenY = y * scaleY + translateY;
|
|
5025
|
-
if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
|
|
5362
|
+
if (nodeIds.length === 0) {
|
|
5026
5363
|
continue;
|
|
5027
5364
|
}
|
|
5028
|
-
const
|
|
5029
|
-
|
|
5030
|
-
|
|
5365
|
+
const labelAt = { x: (minX + maxX) / 2, y: minY };
|
|
5366
|
+
const anchor = ends.length ? {
|
|
5367
|
+
x: ends.reduce((s, p) => s + p.x, 0) / ends.length,
|
|
5368
|
+
y: ends.reduce((s, p) => s + p.y, 0) / ends.length
|
|
5369
|
+
} : labelAt;
|
|
5370
|
+
const sharing = /* @__PURE__ */ new Map();
|
|
5371
|
+
for (const route of bubble.routes ?? []) {
|
|
5372
|
+
for (const id of new Set(route.steps)) {
|
|
5373
|
+
sharing.set(id, (sharing.get(id) ?? 0) + 1);
|
|
5374
|
+
}
|
|
5031
5375
|
}
|
|
5032
|
-
const
|
|
5033
|
-
const
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5376
|
+
const routes2 = [];
|
|
5377
|
+
for (const route of bubble.routes ?? []) {
|
|
5378
|
+
if (bubble.longestAlleleLength < MIN_CHIPPED_BP) {
|
|
5379
|
+
break;
|
|
5380
|
+
}
|
|
5381
|
+
if (route.steps.length === 0) {
|
|
5382
|
+
continue;
|
|
5383
|
+
}
|
|
5384
|
+
const rarest = Math.min(...route.steps.map((id) => sharing.get(id)));
|
|
5385
|
+
let at;
|
|
5386
|
+
let far = -1;
|
|
5387
|
+
for (const id of route.steps) {
|
|
5388
|
+
if (sharing.get(id) !== rarest) {
|
|
5389
|
+
continue;
|
|
5390
|
+
}
|
|
5391
|
+
for (const p of positions[id] ?? []) {
|
|
5392
|
+
const d = Math.hypot(p.x - anchor.x, p.y - anchor.y);
|
|
5393
|
+
if (d > far) {
|
|
5394
|
+
far = d;
|
|
5395
|
+
at = p;
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
if (at) {
|
|
5400
|
+
routes2.push({ route, at, text: routeText(route, walkLabel) });
|
|
5401
|
+
}
|
|
5046
5402
|
}
|
|
5403
|
+
halos.push({
|
|
5404
|
+
bubble,
|
|
5405
|
+
...classifyBubble(bubble, repeats),
|
|
5406
|
+
path: parts.join(""),
|
|
5407
|
+
labelAt,
|
|
5408
|
+
members: nodeIds.length,
|
|
5409
|
+
nodeIds,
|
|
5410
|
+
whole: nodeIds.length >= WHOLE_FRACTION * graph.nodes.length,
|
|
5411
|
+
routes: routes2
|
|
5412
|
+
});
|
|
5047
5413
|
}
|
|
5048
|
-
return
|
|
5414
|
+
return halos;
|
|
5049
5415
|
}
|
|
5050
|
-
function
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
)
|
|
5416
|
+
function routeText(route, walkLabel) {
|
|
5417
|
+
const names = [...new Set(route.walks.map(walkLabel))].sort();
|
|
5418
|
+
const shown = names.slice(0, NAMED_WALKS).join(", ");
|
|
5419
|
+
const more = names.length > NAMED_WALKS ? ` +${names.length - NAMED_WALKS}` : "";
|
|
5420
|
+
return `${shown}${more} \xB7 ${formatBp2(route.bp)}`;
|
|
5055
5421
|
}
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5422
|
+
|
|
5423
|
+
// src/bubbles/popBubble.ts
|
|
5424
|
+
function bubbleSubgraph(graph, segmentIds) {
|
|
5425
|
+
const keep = new Set(segmentIds);
|
|
5426
|
+
const nodes = graph.nodes.filter((n) => keep.has(n.name));
|
|
5427
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
5428
|
+
const paths = graph.paths?.map((p) => ({ ...p, nodeIds: p.nodeIds.filter((id) => ids.has(id)) })).filter((p) => p.nodeIds.length > 0);
|
|
5429
|
+
return {
|
|
5430
|
+
name: graph.name,
|
|
5431
|
+
nodes,
|
|
5432
|
+
edges: graph.edges.filter((e) => ids.has(e.from) && ids.has(e.to)),
|
|
5433
|
+
...paths?.length ? { paths } : {},
|
|
5434
|
+
anchorPaths: graph.anchorPaths,
|
|
5435
|
+
pathVisits: graph.pathVisits,
|
|
5436
|
+
anchoredBy: graph.anchoredBy,
|
|
5437
|
+
referencePath: graph.referencePath
|
|
5438
|
+
};
|
|
5067
5439
|
}
|
|
5068
5440
|
|
|
5069
5441
|
// src/labelLayout.ts
|
|
@@ -5211,27 +5583,24 @@ function layoutLabels(m) {
|
|
|
5211
5583
|
}
|
|
5212
5584
|
|
|
5213
5585
|
// src/genes/genePins.ts
|
|
5214
|
-
function contig(name) {
|
|
5215
|
-
return name.split("#").at(-1);
|
|
5216
|
-
}
|
|
5217
5586
|
function genePins(graph, genes, positions) {
|
|
5218
|
-
const
|
|
5219
|
-
const contigOf = /* @__PURE__ */ new Map();
|
|
5587
|
+
const byRefName = /* @__PURE__ */ new Map();
|
|
5220
5588
|
for (const node of graph.nodes) {
|
|
5221
|
-
if (isBackbone(node)
|
|
5589
|
+
if (isBackbone(node)) {
|
|
5222
5590
|
const { refName } = node.stable;
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5591
|
+
if (positions[node.id]?.length) {
|
|
5592
|
+
const entry = byRefName.get(refName) ?? byRefName.set(refName, { nodes: [], reach: 0 }).get(refName);
|
|
5593
|
+
entry.nodes.push(node);
|
|
5594
|
+
entry.reach = Math.max(entry.reach, node.length);
|
|
5595
|
+
}
|
|
5227
5596
|
}
|
|
5228
5597
|
}
|
|
5229
|
-
for (const { nodes } of
|
|
5598
|
+
for (const { nodes } of byRefName.values()) {
|
|
5230
5599
|
nodes.sort((a, b) => a.stable.start - b.stable.start);
|
|
5231
5600
|
}
|
|
5232
5601
|
const pins = [];
|
|
5233
5602
|
for (const gene of genes) {
|
|
5234
|
-
const backbone =
|
|
5603
|
+
const backbone = byRefName.get(gene.refName);
|
|
5235
5604
|
if (!backbone) {
|
|
5236
5605
|
continue;
|
|
5237
5606
|
}
|
|
@@ -5290,6 +5659,135 @@ function genePins(graph, genes, positions) {
|
|
|
5290
5659
|
return pins;
|
|
5291
5660
|
}
|
|
5292
5661
|
|
|
5662
|
+
// src/reference.ts
|
|
5663
|
+
var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
|
|
5664
|
+
["hg38", "GRCh38"],
|
|
5665
|
+
["hg19", "GRCh37"],
|
|
5666
|
+
["hs1", "CHM13"],
|
|
5667
|
+
["t2t-chm13", "CHM13"],
|
|
5668
|
+
["chm13v2", "CHM13"]
|
|
5669
|
+
]);
|
|
5670
|
+
function wellKnownSample(assemblyName) {
|
|
5671
|
+
return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
|
|
5672
|
+
}
|
|
5673
|
+
var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
|
|
5674
|
+
function sampleOf2(refName) {
|
|
5675
|
+
const sample = refName.includes("#") ? panSNSample(refName) : void 0;
|
|
5676
|
+
return sample === void 0 || GENERIC_SAMPLES.has(sample) ? void 0 : sample;
|
|
5677
|
+
}
|
|
5678
|
+
function haplotypeOf(refName) {
|
|
5679
|
+
return panSNHaplotype(refName) ?? panSNSample(refName);
|
|
5680
|
+
}
|
|
5681
|
+
function graphHaplotypes(graph) {
|
|
5682
|
+
const refNames = new Set((graph.anchorPaths ?? []).map((p) => p.name));
|
|
5683
|
+
for (const node of graph.nodes) {
|
|
5684
|
+
if (node.stable) {
|
|
5685
|
+
refNames.add(node.stable.refName);
|
|
5686
|
+
}
|
|
5687
|
+
}
|
|
5688
|
+
const bySample = /* @__PURE__ */ new Map();
|
|
5689
|
+
for (const refName of refNames) {
|
|
5690
|
+
const sample = sampleOf2(refName);
|
|
5691
|
+
if (sample !== void 0) {
|
|
5692
|
+
const haplotypes = bySample.get(sample) ?? bySample.set(sample, /* @__PURE__ */ new Set()).get(sample);
|
|
5693
|
+
haplotypes.add(haplotypeOf(refName));
|
|
5694
|
+
}
|
|
5695
|
+
}
|
|
5696
|
+
return bySample;
|
|
5697
|
+
}
|
|
5698
|
+
function backboneFrom(contigs, haplotypes) {
|
|
5699
|
+
const samples = new Set(contigs.map((c) => sampleOf2(c.refName)));
|
|
5700
|
+
const [sample] = samples;
|
|
5701
|
+
if (samples.size !== 1 || sample === void 0) {
|
|
5702
|
+
return { contigs, prefixes: [], named: false };
|
|
5703
|
+
}
|
|
5704
|
+
const own = new Set(contigs.map((c) => haplotypeOf(c.refName)));
|
|
5705
|
+
const unique = [...haplotypes.get(sample) ?? []].every((h) => own.has(h));
|
|
5706
|
+
const shared = new Set(contigs.map((c) => panSNHaplotype(c.refName)));
|
|
5707
|
+
const [haplotype] = shared;
|
|
5708
|
+
return {
|
|
5709
|
+
contigs,
|
|
5710
|
+
prefixes: [
|
|
5711
|
+
...unique ? [sample] : [],
|
|
5712
|
+
...shared.size === 1 && haplotype ? [haplotype] : []
|
|
5713
|
+
],
|
|
5714
|
+
named: true
|
|
5715
|
+
};
|
|
5716
|
+
}
|
|
5717
|
+
function graphBackbone(graph) {
|
|
5718
|
+
const spans = /* @__PURE__ */ new Map();
|
|
5719
|
+
for (const node of graph.nodes) {
|
|
5720
|
+
if (isBackbone(node)) {
|
|
5721
|
+
const { refName, start } = node.stable;
|
|
5722
|
+
const end = start + node.length;
|
|
5723
|
+
const span = spans.get(refName);
|
|
5724
|
+
if (span) {
|
|
5725
|
+
span.start = Math.min(span.start, start);
|
|
5726
|
+
span.end = Math.max(span.end, end);
|
|
5727
|
+
} else {
|
|
5728
|
+
spans.set(refName, {
|
|
5729
|
+
refName,
|
|
5730
|
+
contig: panSNContig(refName),
|
|
5731
|
+
start,
|
|
5732
|
+
end
|
|
5733
|
+
});
|
|
5734
|
+
}
|
|
5735
|
+
}
|
|
5736
|
+
}
|
|
5737
|
+
const contigs = [...spans.values()];
|
|
5738
|
+
return contigs.length ? backboneFrom(contigs, graphHaplotypes(graph)) : void 0;
|
|
5739
|
+
}
|
|
5740
|
+
function backboneAssembly(backbone, assemblies) {
|
|
5741
|
+
const names = (a) => [a.name, ...a.aliases ?? []];
|
|
5742
|
+
const naming = (prefix) => (a) => names(a).some((n) => n.toLowerCase() === prefix.toLowerCase());
|
|
5743
|
+
const prefixes = [...backbone?.prefixes ?? []].reverse();
|
|
5744
|
+
const sample = backbone?.prefixes.find((p) => !p.includes("#"))?.toLowerCase();
|
|
5745
|
+
for (const prefix of prefixes) {
|
|
5746
|
+
const assembly = assemblies.find(naming(prefix));
|
|
5747
|
+
if (assembly) {
|
|
5748
|
+
return assembly;
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
return sample === void 0 ? void 0 : assemblies.find(
|
|
5752
|
+
(a) => names(a).some((n) => wellKnownSample(n)?.toLowerCase() === sample)
|
|
5753
|
+
);
|
|
5754
|
+
}
|
|
5755
|
+
function assemblyWalk(graph, assembly) {
|
|
5756
|
+
const haplotypes = graphHaplotypes(graph);
|
|
5757
|
+
return graph.anchorPaths?.find(
|
|
5758
|
+
(walk) => backboneAssembly(
|
|
5759
|
+
backboneFrom(
|
|
5760
|
+
[
|
|
5761
|
+
{
|
|
5762
|
+
refName: walk.name,
|
|
5763
|
+
contig: panSNContig(walk.name),
|
|
5764
|
+
start: walk.start,
|
|
5765
|
+
end: walk.start + walk.length
|
|
5766
|
+
}
|
|
5767
|
+
],
|
|
5768
|
+
haplotypes
|
|
5769
|
+
),
|
|
5770
|
+
[assembly]
|
|
5771
|
+
)
|
|
5772
|
+
);
|
|
5773
|
+
}
|
|
5774
|
+
function refNameBinding(refNames) {
|
|
5775
|
+
const exact = new Set(refNames);
|
|
5776
|
+
const byContig = /* @__PURE__ */ new Map();
|
|
5777
|
+
for (const refName of exact) {
|
|
5778
|
+
const contig = panSNContig(refName);
|
|
5779
|
+
byContig.set(contig, byContig.has(contig) ? void 0 : refName);
|
|
5780
|
+
}
|
|
5781
|
+
return (name) => exact.has(name) ? name : byContig.get(name);
|
|
5782
|
+
}
|
|
5783
|
+
function featuresOnBackbone(features, backbone) {
|
|
5784
|
+
const bind = refNameBinding(backbone.contigs.map((c) => c.refName));
|
|
5785
|
+
return features.flatMap((f) => {
|
|
5786
|
+
const refName = bind(f.refName);
|
|
5787
|
+
return refName === void 0 ? [] : [refName === f.refName ? f : { ...f, refName }];
|
|
5788
|
+
});
|
|
5789
|
+
}
|
|
5790
|
+
|
|
5293
5791
|
// src/gbzWindow.ts
|
|
5294
5792
|
var NoReferenceSampleError = class extends Error {
|
|
5295
5793
|
constructor(anchor, referenceSamples) {
|
|
@@ -5317,16 +5815,9 @@ function haplotypeWanted(name, wanted) {
|
|
|
5317
5815
|
async function referenceSamplesOf(db) {
|
|
5318
5816
|
return (await db.tag("gbwt_reference_samples") ?? "").split(/\s+/).filter((sample) => sample !== "");
|
|
5319
5817
|
}
|
|
5320
|
-
var SAMPLE_ALIASES = {
|
|
5321
|
-
hg38: "grch38",
|
|
5322
|
-
hg19: "grch37",
|
|
5323
|
-
hs1: "chm13",
|
|
5324
|
-
"t2t-chm13": "chm13",
|
|
5325
|
-
chm13v2: "chm13"
|
|
5326
|
-
};
|
|
5327
5818
|
function aliasedSample(anchorSample, referenceSamples) {
|
|
5328
5819
|
const lower = anchorSample.toLowerCase();
|
|
5329
|
-
const alias =
|
|
5820
|
+
const alias = wellKnownSample(anchorSample)?.toLowerCase();
|
|
5330
5821
|
return referenceSamples.find((s) => s === anchorSample) ?? referenceSamples.find((s) => s.toLowerCase() === lower) ?? referenceSamples.find((s) => s.toLowerCase() === alias);
|
|
5331
5822
|
}
|
|
5332
5823
|
function resolveReferenceSample({
|
|
@@ -5441,7 +5932,10 @@ export {
|
|
|
5441
5932
|
NODE_WIDTHS,
|
|
5442
5933
|
REFERENCE_RAMP_MAX_HUE,
|
|
5443
5934
|
ROW_HEIGHT_PX,
|
|
5935
|
+
WELL_KNOWN_SAMPLES,
|
|
5936
|
+
assemblyWalk,
|
|
5444
5937
|
axisScaleOf,
|
|
5938
|
+
backboneAssembly,
|
|
5445
5939
|
bubbleHalos,
|
|
5446
5940
|
bubbleSegmentIds,
|
|
5447
5941
|
bubbleSubgraph,
|
|
@@ -5450,20 +5944,26 @@ export {
|
|
|
5450
5944
|
clampZoom,
|
|
5451
5945
|
classifyBubble,
|
|
5452
5946
|
computeReferenceRamp,
|
|
5947
|
+
connectorAt,
|
|
5453
5948
|
contains,
|
|
5454
5949
|
cutWindowGFA,
|
|
5455
5950
|
deletionEdges,
|
|
5456
5951
|
drawTubeMap,
|
|
5952
|
+
drawTubeMapConnectors,
|
|
5953
|
+
drawTubeMapGenes,
|
|
5954
|
+
drawTubeMapRuler,
|
|
5457
5955
|
drawingBounds,
|
|
5458
5956
|
engineKey,
|
|
5459
5957
|
engineRequest,
|
|
5958
|
+
featuresOnBackbone,
|
|
5460
5959
|
findHoveredEdge,
|
|
5461
5960
|
findHoveredNode,
|
|
5462
5961
|
fitTransform,
|
|
5463
5962
|
forceLayout,
|
|
5464
|
-
|
|
5963
|
+
formatBp,
|
|
5465
5964
|
genePins,
|
|
5466
5965
|
getDpr,
|
|
5966
|
+
graphBackbone,
|
|
5467
5967
|
haplotypeWanted,
|
|
5468
5968
|
layoutExtent,
|
|
5469
5969
|
layoutLabels,
|
|
@@ -5481,20 +5981,28 @@ export {
|
|
|
5481
5981
|
panSNSample,
|
|
5482
5982
|
pathColorsLegible,
|
|
5483
5983
|
pathLegend,
|
|
5984
|
+
refNameBinding,
|
|
5985
|
+
referenceBoxes,
|
|
5484
5986
|
referenceKnots,
|
|
5987
|
+
referenceNodes,
|
|
5485
5988
|
referencePathQuery,
|
|
5486
5989
|
referenceSamplesOf,
|
|
5487
5990
|
resolveColorScheme,
|
|
5488
5991
|
resolveReferenceSample,
|
|
5992
|
+
rulerBoxes,
|
|
5489
5993
|
screenToLayout,
|
|
5994
|
+
tubeMapConnectors,
|
|
5490
5995
|
tubeMapFrame,
|
|
5996
|
+
tubeMapGenes,
|
|
5491
5997
|
tubeMapNodeAt,
|
|
5492
5998
|
tubeMapPicture,
|
|
5999
|
+
tubeX,
|
|
5493
6000
|
viewportOf,
|
|
5494
6001
|
walkHighlight,
|
|
5495
6002
|
walkRows,
|
|
5496
6003
|
walkRowsExtent,
|
|
5497
6004
|
warpX,
|
|
6005
|
+
wellKnownSample,
|
|
5498
6006
|
wheelZoomFactor,
|
|
5499
6007
|
zoomAbout
|
|
5500
6008
|
};
|