@jbrowse/bandage-core 4.1.1 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/{chunk-QWZLIWOJ.js → chunk-IFVLNU7X.js} +1528 -1269
- package/dist/cli/figure.js +1 -1
- package/dist/index.js +171 -9
- package/dist/types/deletionEdges.d.ts +2 -2
- package/dist/types/graphLabels.d.ts +3 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/labelLayout.d.ts +2 -0
- package/dist/types/layout/deletionRoutes.d.ts +31 -0
- package/dist/types/layout/drawnScale.d.ts +1 -0
- package/dist/types/layout/mergeRuns.d.ts +1 -1
- package/dist/types/layout/walkRowDraw.d.ts +4 -0
- package/dist/types/layout/walkStrip.d.ts +28 -0
- package/dist/types/pipeline.d.ts +10 -7
- package/dist/types/renderer/GeometryBuilder.d.ts +2 -0
- package/dist/types/types.d.ts +1 -0
- package/dist/types/util/SpatialIndex.d.ts +2 -1
- package/dist/types/util/edgeCurves.d.ts +2 -1
- package/dist/types/util/geometry.d.ts +1 -0
- package/dist/types/util/hitDetection.d.ts +2 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -69,6 +69,13 @@ function layoutScaling(graph, spread = PROPORTIONAL_SPREAD) {
|
|
|
69
69
|
}
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
+
function drawnLengthOf(graph, spread = PROPORTIONAL_SPREAD) {
|
|
73
|
+
if (isProportional(spread.law)) {
|
|
74
|
+
const opts = bandageAutoScale(graph, spread.minNodeLength);
|
|
75
|
+
return (bp) => drawnNodeLength(opts, bp);
|
|
76
|
+
}
|
|
77
|
+
return drawnLengthFor(graph, spread.law);
|
|
78
|
+
}
|
|
72
79
|
|
|
73
80
|
// src/bubbleSpreads.ts
|
|
74
81
|
var BUBBLE_SPREADS = [
|
|
@@ -110,26 +117,8 @@ function spreadFor(spread) {
|
|
|
110
117
|
};
|
|
111
118
|
}
|
|
112
119
|
|
|
113
|
-
// src/
|
|
114
|
-
var
|
|
115
|
-
function panSNSample(refName) {
|
|
116
|
-
return refName.split(SEP)[0];
|
|
117
|
-
}
|
|
118
|
-
var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
|
|
119
|
-
function isGenericSample(sample) {
|
|
120
|
-
return GENERIC_SAMPLES.has(sample);
|
|
121
|
-
}
|
|
122
|
-
function panSNHaplotype(refName) {
|
|
123
|
-
const parts = refName.split(SEP);
|
|
124
|
-
return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : void 0;
|
|
125
|
-
}
|
|
126
|
-
function panSNContig(refName) {
|
|
127
|
-
const parts = refName.split(SEP);
|
|
128
|
-
return parts.length >= 3 ? parts.slice(2).join(SEP) : parts.length === 2 ? parts[1] : refName;
|
|
129
|
-
}
|
|
130
|
-
function panSNMatchesPrefix(refName, prefix) {
|
|
131
|
-
return prefix !== void 0 && (refName === prefix || refName.startsWith(prefix + SEP));
|
|
132
|
-
}
|
|
120
|
+
// src/layout/rowSpacing.ts
|
|
121
|
+
var ROW_HEIGHT_PX = 20;
|
|
133
122
|
|
|
134
123
|
// src/anchoredNodes.ts
|
|
135
124
|
var REFERENCE_RANK = 0;
|
|
@@ -184,1366 +173,1561 @@ function referenceSpan(backbone, region) {
|
|
|
184
173
|
return region && region.end > region.start ? region.end - region.start : backboneSpan(backbone);
|
|
185
174
|
}
|
|
186
175
|
|
|
187
|
-
// src/
|
|
188
|
-
function
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
if (nameEnd !== -1 && typeEnd !== -1) {
|
|
192
|
-
const name = tag.slice(0, nameEnd);
|
|
193
|
-
const type = tag.slice(nameEnd + 1, typeEnd);
|
|
194
|
-
const val = tag.slice(typeEnd + 1);
|
|
195
|
-
if (type === "i" || type === "f") {
|
|
196
|
-
tags[name] = +val;
|
|
197
|
-
} else if (type === "Z") {
|
|
198
|
-
tags[name] = val;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
176
|
+
// src/util/geometry.ts
|
|
177
|
+
function svgPath(points) {
|
|
178
|
+
const round = (v) => Math.round(v * 100) / 100;
|
|
179
|
+
return points.map((p, i) => `${i ? "L" : "M"}${round(p.x)},${round(p.y)}`).join("");
|
|
201
180
|
}
|
|
202
|
-
function
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
181
|
+
function projectLine(x1, y1, x2, y2, distance) {
|
|
182
|
+
const d = Math.hypot(y2 - y1, x2 - x1);
|
|
183
|
+
if (d === 0) {
|
|
184
|
+
return [x2, y2];
|
|
206
185
|
}
|
|
207
|
-
|
|
186
|
+
const vx = (x2 - x1) / d;
|
|
187
|
+
const vy = (y2 - y1) / d;
|
|
188
|
+
return [x2 + distance * vx, y2 + distance * vy];
|
|
208
189
|
}
|
|
209
|
-
function
|
|
210
|
-
const
|
|
211
|
-
|
|
190
|
+
function curvePointAt(c, t) {
|
|
191
|
+
const u = 1 - t;
|
|
192
|
+
const a = u * u * u;
|
|
193
|
+
const b = 3 * u * u * t;
|
|
194
|
+
const d = 3 * u * t * t;
|
|
195
|
+
const e = t * t * t;
|
|
196
|
+
return {
|
|
197
|
+
x: a * c.x0 + b * c.cx0 + d * c.cx1 + e * c.x1,
|
|
198
|
+
y: a * c.y0 + b * c.cy0 + d * c.cy1 + e * c.y1
|
|
199
|
+
};
|
|
212
200
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
let
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
push(i);
|
|
228
|
-
strand = ch === GT ? "+" : "-";
|
|
229
|
-
start = i + 1;
|
|
230
|
-
}
|
|
201
|
+
function curveMidpoint(curves) {
|
|
202
|
+
const mid = curves[Math.floor(curves.length / 2)];
|
|
203
|
+
return mid ? curvePointAt(mid, 0.5) : void 0;
|
|
204
|
+
}
|
|
205
|
+
function curveBounds(curves) {
|
|
206
|
+
let minX = Infinity;
|
|
207
|
+
let minY = Infinity;
|
|
208
|
+
let maxX = -Infinity;
|
|
209
|
+
let maxY = -Infinity;
|
|
210
|
+
for (const c of curves) {
|
|
211
|
+
minX = Math.min(minX, c.x0, c.cx0, c.cx1, c.x1);
|
|
212
|
+
maxX = Math.max(maxX, c.x0, c.cx0, c.cx1, c.x1);
|
|
213
|
+
minY = Math.min(minY, c.y0, c.cy0, c.cy1, c.y1);
|
|
214
|
+
maxY = Math.max(maxY, c.y0, c.cy0, c.cy1, c.y1);
|
|
231
215
|
}
|
|
232
|
-
|
|
233
|
-
return segments;
|
|
216
|
+
return { minX, minY, maxX, maxY };
|
|
234
217
|
}
|
|
235
|
-
function
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
218
|
+
function subCurve(c, t0, t1) {
|
|
219
|
+
const p0 = curvePointAt(c, t0);
|
|
220
|
+
const p1 = curvePointAt(c, t1);
|
|
221
|
+
const dt = (t1 - t0) / 3;
|
|
222
|
+
const d0 = curveTangentAt(c, t0);
|
|
223
|
+
const d1 = curveTangentAt(c, t1);
|
|
224
|
+
return {
|
|
225
|
+
x0: p0.x,
|
|
226
|
+
y0: p0.y,
|
|
227
|
+
cx0: p0.x + d0.x * dt,
|
|
228
|
+
cy0: p0.y + d0.y * dt,
|
|
229
|
+
cx1: p1.x - d1.x * dt,
|
|
230
|
+
cy1: p1.y - d1.y * dt,
|
|
231
|
+
x1: p1.x,
|
|
232
|
+
y1: p1.y
|
|
243
233
|
};
|
|
244
|
-
for (const rawLine of file.split("\n")) {
|
|
245
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
246
|
-
if (line.startsWith("H")) {
|
|
247
|
-
const [, ...rest] = line.split(" ");
|
|
248
|
-
graph.header.push(parseTags(rest));
|
|
249
|
-
} else if (line.startsWith("S")) {
|
|
250
|
-
const [, name, ...rest] = line.split(" ");
|
|
251
|
-
let len;
|
|
252
|
-
let seq;
|
|
253
|
-
let tagfields;
|
|
254
|
-
let gfa1 = false;
|
|
255
|
-
const first = rest[0] ?? "*";
|
|
256
|
-
if (/^\d+$/.test(first)) {
|
|
257
|
-
len = +first;
|
|
258
|
-
seq = rest[1] ?? "*";
|
|
259
|
-
tagfields = rest.slice(2);
|
|
260
|
-
} else {
|
|
261
|
-
gfa1 = true;
|
|
262
|
-
seq = first;
|
|
263
|
-
len = seq === "*" ? 0 : seq.length;
|
|
264
|
-
tagfields = rest.slice(1);
|
|
265
|
-
}
|
|
266
|
-
const tags = parseTags(tagfields);
|
|
267
|
-
if (gfa1 && "LN" in tags) {
|
|
268
|
-
len = +tags.LN;
|
|
269
|
-
}
|
|
270
|
-
graph.nodes.push({ id: name, length: len, sequence: seq, tags });
|
|
271
|
-
} else if (line.startsWith("E")) {
|
|
272
|
-
const [, , source, target, , , , , cigar, ...rest] = line.split(" ");
|
|
273
|
-
if (source && target) {
|
|
274
|
-
graph.links.push({
|
|
275
|
-
source: source.slice(0, -1),
|
|
276
|
-
target: target.slice(0, -1),
|
|
277
|
-
strand1: source.at(-1),
|
|
278
|
-
strand2: target.at(-1),
|
|
279
|
-
cigar: cigar ?? "*",
|
|
280
|
-
tags: parseTags(rest)
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
} else if (line.startsWith("L")) {
|
|
284
|
-
const [, source, strand1, target, strand2, cigar, ...rest] = line.split(" ");
|
|
285
|
-
if (source && target && strand2) {
|
|
286
|
-
graph.links.push({
|
|
287
|
-
source,
|
|
288
|
-
target,
|
|
289
|
-
strand1,
|
|
290
|
-
strand2,
|
|
291
|
-
cigar: cigar ?? "*",
|
|
292
|
-
tags: parseTags(rest)
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
} else if (line.startsWith("W")) {
|
|
296
|
-
const [, sample, hap, contig, start, end, body, ...rest] = line.split(" ");
|
|
297
|
-
if (sample && hap && contig && start && end && body) {
|
|
298
|
-
graph.walks.push({
|
|
299
|
-
sample,
|
|
300
|
-
haplotype: +hap,
|
|
301
|
-
contig,
|
|
302
|
-
start: start === "*" ? -1 : +start,
|
|
303
|
-
end: end === "*" ? -1 : +end,
|
|
304
|
-
segments: parseWalkBody(body),
|
|
305
|
-
tags: parseTags(rest)
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
} else if (line.startsWith("P")) {
|
|
309
|
-
const [, name, path, ...rest] = line.split(" ");
|
|
310
|
-
if (name && path) {
|
|
311
|
-
graph.paths.push({ name, path, rest });
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return graph;
|
|
316
234
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
return
|
|
235
|
+
function curveTangentAt(c, t) {
|
|
236
|
+
const u = 1 - t;
|
|
237
|
+
const a = 3 * u * u;
|
|
238
|
+
const b = 6 * u * t;
|
|
239
|
+
const d = 3 * t * t;
|
|
240
|
+
return {
|
|
241
|
+
x: a * (c.cx0 - c.x0) + b * (c.cx1 - c.cx0) + d * (c.x1 - c.cx1),
|
|
242
|
+
y: a * (c.cy0 - c.y0) + b * (c.cy1 - c.cy0) + d * (c.y1 - c.cy1)
|
|
243
|
+
};
|
|
323
244
|
}
|
|
324
|
-
function
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
245
|
+
function dashCurves(curves, dashWorld) {
|
|
246
|
+
const lengths = curves.map(
|
|
247
|
+
(c) => Math.hypot(c.cx0 - c.x0, c.cy0 - c.y0) + Math.hypot(c.cx1 - c.cx0, c.cy1 - c.cy0) + Math.hypot(c.x1 - c.cx1, c.y1 - c.cy1)
|
|
248
|
+
);
|
|
249
|
+
const total = lengths.reduce((a, b) => a + b, 0);
|
|
250
|
+
const spans = Math.max(3, Math.round(total / dashWorld) | 1);
|
|
251
|
+
const out = [];
|
|
252
|
+
for (let i = 0; i < spans; i += 2) {
|
|
253
|
+
const from = i / spans * total;
|
|
254
|
+
const to = (i + 1) / spans * total;
|
|
255
|
+
const dash = [];
|
|
256
|
+
let start = 0;
|
|
257
|
+
curves.forEach((c, k) => {
|
|
258
|
+
const length = lengths[k];
|
|
259
|
+
const end = start + length;
|
|
260
|
+
if (length > 0 && end > from && start < to) {
|
|
261
|
+
dash.push(
|
|
262
|
+
subCurve(
|
|
263
|
+
c,
|
|
264
|
+
Math.max(0, (from - start) / length),
|
|
265
|
+
Math.min(1, (to - start) / length)
|
|
266
|
+
)
|
|
267
|
+
);
|
|
342
268
|
}
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
anchorPaths.push({
|
|
346
|
-
name: path.name,
|
|
347
|
-
sample,
|
|
348
|
-
start: path.start,
|
|
349
|
-
length: pos - path.start
|
|
269
|
+
start = end;
|
|
350
270
|
});
|
|
271
|
+
if (dash.length) {
|
|
272
|
+
out.push(dash);
|
|
273
|
+
}
|
|
351
274
|
}
|
|
352
|
-
return
|
|
353
|
-
}
|
|
354
|
-
function chooseReferencePath(anchorPaths, preferred) {
|
|
355
|
-
const named = anchorPaths.find(
|
|
356
|
-
(p) => p.sample === preferred || p.name === preferred
|
|
357
|
-
);
|
|
358
|
-
return named ?? anchorPaths[0];
|
|
275
|
+
return out;
|
|
359
276
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
let
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
samples: [...new Set(visits.map((v) => v.sample))].sort()
|
|
277
|
+
function translateCurves(curves, dx, dy) {
|
|
278
|
+
const out = new Array(curves.length);
|
|
279
|
+
for (let i = 0; i < curves.length; i++) {
|
|
280
|
+
const c = curves[i];
|
|
281
|
+
out[i] = {
|
|
282
|
+
x0: c.x0 + dx,
|
|
283
|
+
y0: c.y0 + dy,
|
|
284
|
+
cx0: c.cx0 + dx,
|
|
285
|
+
cy0: c.cy0 + dy,
|
|
286
|
+
cx1: c.cx1 + dx,
|
|
287
|
+
cy1: c.cy1 + dy,
|
|
288
|
+
x1: c.x1 + dx,
|
|
289
|
+
y1: c.y1 + dy
|
|
374
290
|
};
|
|
375
291
|
}
|
|
376
|
-
return
|
|
377
|
-
}
|
|
378
|
-
function anchorFromPaths(graph, preferred) {
|
|
379
|
-
const { anchorPaths, pathVisits } = graph;
|
|
380
|
-
const reference = anchorPaths && pathVisits ? chooseReferencePath(anchorPaths, preferred) : void 0;
|
|
381
|
-
return reference && pathVisits ? {
|
|
382
|
-
...graph,
|
|
383
|
-
nodes: graph.nodes.map(
|
|
384
|
-
(node) => anchorNode(node, pathVisits.get(node.name), reference.name)
|
|
385
|
-
),
|
|
386
|
-
anchoredBy: "paths",
|
|
387
|
-
referencePath: reference.name
|
|
388
|
-
} : graph;
|
|
292
|
+
return out;
|
|
389
293
|
}
|
|
390
|
-
function
|
|
391
|
-
|
|
294
|
+
function attachment(segments, side) {
|
|
295
|
+
const last = segments.length - 1;
|
|
296
|
+
return side === "end" ? { at: segments[last], inward: segments[last - 1] } : { at: segments[0], inward: segments[1] };
|
|
392
297
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
298
|
+
var TANGENT_SPAN_FACTOR = 0.5;
|
|
299
|
+
var MAX_TANGENT = 80;
|
|
300
|
+
function spanOf(segments) {
|
|
301
|
+
let min = Infinity;
|
|
302
|
+
let max = -Infinity;
|
|
303
|
+
for (const s of segments) {
|
|
304
|
+
min = Math.min(min, s.x);
|
|
305
|
+
max = Math.max(max, s.x);
|
|
396
306
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
307
|
+
return { min, max };
|
|
308
|
+
}
|
|
309
|
+
function distanceToSpan(x, span) {
|
|
310
|
+
return Math.max(0, span.min - x, x - span.max);
|
|
311
|
+
}
|
|
312
|
+
function facingSide(segments, other, tieBreak) {
|
|
313
|
+
const startGap = distanceToSpan(segments[0].x, other);
|
|
314
|
+
const endGap = distanceToSpan(segments[segments.length - 1].x, other);
|
|
315
|
+
return startGap === endGap ? tieBreak : startGap < endGap ? "start" : "end";
|
|
316
|
+
}
|
|
317
|
+
function facingSides(fromSegments, toSegments) {
|
|
318
|
+
return {
|
|
319
|
+
from: facingSide(fromSegments, spanOf(toSegments), "end"),
|
|
320
|
+
to: facingSide(toSegments, spanOf(fromSegments), "start")
|
|
404
321
|
};
|
|
405
|
-
const byOrigin = /* @__PURE__ */ new Map();
|
|
406
|
-
for (const path of graph.paths ?? []) {
|
|
407
|
-
const origin = pathOrigin(path.name).name;
|
|
408
|
-
byOrigin.set(origin, [...byOrigin.get(origin) ?? [], path]);
|
|
409
|
-
}
|
|
410
|
-
return graph.anchorPaths.map((o) => {
|
|
411
|
-
const pieces = byOrigin.get(o.name);
|
|
412
|
-
const path = pieces?.length === 1 ? pieces[0] : void 0;
|
|
413
|
-
const stretch = path && kept.get(path.name);
|
|
414
|
-
return path && stretch ? {
|
|
415
|
-
...o,
|
|
416
|
-
start: o.start + bp(path.nodeIds, stretch.first),
|
|
417
|
-
length: bp(stretch.nodeIds)
|
|
418
|
-
} : o;
|
|
419
|
-
});
|
|
420
322
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
439
|
-
for (const gfaPath of gfaGraph.paths) {
|
|
440
|
-
for (const segment of gfaPath.path.split(",")) {
|
|
441
|
-
traverse(segment.slice(0, -1), segment.endsWith("-") ? "-" : "+");
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
for (const walk of gfaGraph.walks) {
|
|
445
|
-
for (const seg of walk.segments) {
|
|
446
|
-
traverse(seg.id, seg.strand === "-" ? "-" : "+");
|
|
323
|
+
var APEX_FRACTION = 0.75;
|
|
324
|
+
var BOW_CLEARANCE = 1.15;
|
|
325
|
+
var ALONG_FRACTION = 0.35;
|
|
326
|
+
var MAX_ROW_BOW_PX = 3 * ROW_HEIGHT_PX;
|
|
327
|
+
function bowAround(p1x, p1y, p2x, p2y, toward, maxBow) {
|
|
328
|
+
const chord = Math.hypot(p2x - p1x, p2y - p1y);
|
|
329
|
+
const ux = chord === 0 ? 1 : (p2x - p1x) / chord;
|
|
330
|
+
const uy = chord === 0 ? 0 : (p2y - p1y) / chord;
|
|
331
|
+
let reach = 0;
|
|
332
|
+
let minAlong = Infinity;
|
|
333
|
+
let maxAlong = -Infinity;
|
|
334
|
+
for (const p of toward) {
|
|
335
|
+
const dx = p.x - p1x;
|
|
336
|
+
const dy = p.y - p1y;
|
|
337
|
+
const perp = dx * -uy + dy * ux;
|
|
338
|
+
if (Math.abs(perp) > Math.abs(reach)) {
|
|
339
|
+
reach = perp;
|
|
447
340
|
}
|
|
341
|
+
const along = dx * ux + dy * uy;
|
|
342
|
+
minAlong = Math.min(minAlong, along);
|
|
343
|
+
maxAlong = Math.max(maxAlong, along);
|
|
448
344
|
}
|
|
449
|
-
|
|
345
|
+
const span = toward.length ? maxAlong - minAlong : 0;
|
|
346
|
+
const uncapped = Math.max(
|
|
347
|
+
Math.abs(reach) * BOW_CLEARANCE / APEX_FRACTION,
|
|
348
|
+
span * ALONG_FRACTION
|
|
349
|
+
);
|
|
350
|
+
const size = maxBow === void 0 ? uncapped : Math.min(uncapped, maxBow);
|
|
351
|
+
return reach < 0 ? -size : size;
|
|
450
352
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
return [
|
|
454
|
-
...gfaGraph.paths.map((p) => ({
|
|
455
|
-
...pathOrigin(p.name),
|
|
456
|
-
steps: p.path.split(",").map((segment) => ({
|
|
457
|
-
id: segment.slice(0, -1),
|
|
458
|
-
strand: segment.endsWith("-") ? "-" : "+"
|
|
459
|
-
}))
|
|
460
|
-
})),
|
|
461
|
-
...gfaGraph.walks.map((w) => ({
|
|
462
|
-
name: `${w.sample}#${w.haplotype}#${w.contig}`,
|
|
463
|
-
start: walkStart(w),
|
|
464
|
-
steps: w.segments.map((s) => ({
|
|
465
|
-
id: s.id,
|
|
466
|
-
strand: s.strand === "-" ? "-" : "+"
|
|
467
|
-
}))
|
|
468
|
-
}))
|
|
469
|
-
];
|
|
353
|
+
function yToXOf({ scaleX, scaleY }) {
|
|
354
|
+
return scaleY / scaleX;
|
|
470
355
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
356
|
+
var PATH_RIBBON_SPACING = 3;
|
|
357
|
+
function drawnDirection(a, b, yToX) {
|
|
358
|
+
const dx = b.x - a.x;
|
|
359
|
+
const dy = (b.y - a.y) * yToX;
|
|
360
|
+
const len = Math.hypot(dx, dy);
|
|
361
|
+
return len === 0 ? void 0 : { x: dx / len, y: dy / len };
|
|
362
|
+
}
|
|
363
|
+
function pathRibbonOffsets(fromSegments, toSegments, count, axis, spacing = PATH_RIBBON_SPACING) {
|
|
364
|
+
const yToX = yToXOf(axis);
|
|
365
|
+
const dir = drawnDirection(
|
|
366
|
+
fromSegments[fromSegments.length - 1],
|
|
367
|
+
toSegments[0],
|
|
368
|
+
yToX
|
|
369
|
+
) ?? drawnDirection(fromSegments[0], fromSegments.at(-1), yToX) ?? drawnDirection(toSegments[0], toSegments.at(-1), yToX) ?? { x: 1, y: 0 };
|
|
370
|
+
const offsets = [];
|
|
371
|
+
for (let i = 0; i < count; i++) {
|
|
372
|
+
const distance = (i - (count - 1) / 2) * spacing;
|
|
373
|
+
offsets.push({ x: -dir.y * distance, y: dir.x * distance / yToX });
|
|
475
374
|
}
|
|
476
|
-
|
|
477
|
-
return samples.length > 0 ? samples : void 0;
|
|
375
|
+
return offsets;
|
|
478
376
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const count = positive(tags.RC) ?? positive(tags.FC) ?? positive(tags.KC);
|
|
482
|
-
return positive(tags.dp) ?? positive(tags.DP) ?? (count !== void 0 && length > 0 ? count / length : void 0);
|
|
377
|
+
function scaleYOf(points, yToX) {
|
|
378
|
+
return points.map((p) => ({ x: p.x, y: p.y * yToX }));
|
|
483
379
|
}
|
|
484
|
-
function
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
};
|
|
380
|
+
function unscaleYOf(curves, yToX) {
|
|
381
|
+
for (const c of curves) {
|
|
382
|
+
c.y0 /= yToX;
|
|
383
|
+
c.cy0 /= yToX;
|
|
384
|
+
c.cy1 /= yToX;
|
|
385
|
+
c.y1 /= yToX;
|
|
386
|
+
}
|
|
387
|
+
return curves;
|
|
493
388
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
389
|
+
var SELF_LOOP_MIN_PX = 12;
|
|
390
|
+
var SELF_LOOP_MAX_PX = 40;
|
|
391
|
+
function selfLoopReach(segments, scale) {
|
|
392
|
+
let length = 0;
|
|
393
|
+
for (let i = 1; i < segments.length; i++) {
|
|
394
|
+
length += Math.hypot(
|
|
395
|
+
segments[i].x - segments[i - 1].x,
|
|
396
|
+
segments[i].y - segments[i - 1].y
|
|
397
|
+
);
|
|
502
398
|
}
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
399
|
+
const nodePx = length * scale;
|
|
400
|
+
const reachPx = Math.min(
|
|
401
|
+
SELF_LOOP_MAX_PX,
|
|
402
|
+
Math.max(SELF_LOOP_MIN_PX, nodePx / 2)
|
|
403
|
+
);
|
|
404
|
+
return reachPx / Math.max(scale, Number.EPSILON);
|
|
405
|
+
}
|
|
406
|
+
function selfLinkOf(edge) {
|
|
407
|
+
if (edge.from !== edge.to) {
|
|
408
|
+
return false;
|
|
510
409
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
410
|
+
return edge.fromStrand && edge.toStrand && edge.fromStrand !== edge.toStrand ? edge.fromStrand === "+" ? "end" : "start" : true;
|
|
411
|
+
}
|
|
412
|
+
function hairpinCurves(attach, reach, offsetX, offsetY) {
|
|
413
|
+
const { at, inward } = attach;
|
|
414
|
+
const dx = inward ? at.x - inward.x : 1;
|
|
415
|
+
const dy = inward ? at.y - inward.y : 0;
|
|
416
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
417
|
+
const ux = dx / len;
|
|
418
|
+
const uy = dy / len;
|
|
419
|
+
const px = at.x + offsetX;
|
|
420
|
+
const py = at.y + offsetY;
|
|
421
|
+
const nearX = px + ux * reach / 3;
|
|
422
|
+
const nearY = py + uy * reach / 3;
|
|
423
|
+
const apexX = px + ux * reach;
|
|
424
|
+
const apexY = py + uy * reach;
|
|
425
|
+
const sideX = -uy * reach / 2;
|
|
426
|
+
const sideY = ux * reach / 2;
|
|
427
|
+
return [
|
|
428
|
+
{
|
|
429
|
+
x0: px,
|
|
430
|
+
y0: py,
|
|
431
|
+
cx0: nearX,
|
|
432
|
+
cy0: nearY,
|
|
433
|
+
cx1: apexX + sideX,
|
|
434
|
+
cy1: apexY + sideY,
|
|
435
|
+
x1: apexX,
|
|
436
|
+
y1: apexY
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
x0: apexX,
|
|
440
|
+
y0: apexY,
|
|
441
|
+
cx0: apexX - sideX,
|
|
442
|
+
cy0: apexY - sideY,
|
|
443
|
+
cx1: nearX,
|
|
444
|
+
cy1: nearY,
|
|
445
|
+
x1: px,
|
|
446
|
+
y1: py
|
|
447
|
+
}
|
|
448
|
+
];
|
|
449
|
+
}
|
|
450
|
+
function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY, axis, bypassed = [], maxBowIn) {
|
|
451
|
+
const scale = axis.scaleX;
|
|
452
|
+
const yToX = yToXOf(axis);
|
|
453
|
+
const maxBow = maxBowIn ?? (axis.pixelRows ? MAX_ROW_BOW_PX : void 0);
|
|
454
|
+
if (yToX !== 1) {
|
|
455
|
+
return unscaleYOf(
|
|
456
|
+
computeEdgeCurves(
|
|
457
|
+
scaleYOf(fromSegments, yToX),
|
|
458
|
+
scaleYOf(toSegments, yToX),
|
|
459
|
+
selfLink,
|
|
460
|
+
offsetX,
|
|
461
|
+
offsetY * yToX,
|
|
462
|
+
{ scaleX: scale, scaleY: scale },
|
|
463
|
+
scaleYOf(bypassed, yToX),
|
|
464
|
+
// the cap is a y measure and the body works in x units, so it converts
|
|
465
|
+
// with everything else that crosses this boundary
|
|
466
|
+
maxBow === void 0 ? void 0 : maxBow * yToX
|
|
467
|
+
),
|
|
468
|
+
yToX
|
|
469
|
+
);
|
|
531
470
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
sample: walk.sample,
|
|
540
|
-
haplotype: walk.haplotype,
|
|
541
|
-
contig: walk.contig
|
|
542
|
-
});
|
|
543
|
-
recordPathEdges(nodeIds, name2);
|
|
471
|
+
if (typeof selfLink === "string") {
|
|
472
|
+
return hairpinCurves(
|
|
473
|
+
attachment(fromSegments, selfLink),
|
|
474
|
+
selfLoopReach(fromSegments, scale),
|
|
475
|
+
offsetX,
|
|
476
|
+
offsetY
|
|
477
|
+
);
|
|
544
478
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
479
|
+
const sides = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
|
|
480
|
+
const fromAttach = attachment(fromSegments, sides.from);
|
|
481
|
+
const toAttach = attachment(toSegments, sides.to);
|
|
482
|
+
const fromEnd = fromAttach.at;
|
|
483
|
+
const toStart = toAttach.at;
|
|
484
|
+
const p1x = fromEnd.x + offsetX;
|
|
485
|
+
const p1y = fromEnd.y + offsetY;
|
|
486
|
+
const p2x = toStart.x + offsetX;
|
|
487
|
+
const p2y = toStart.y + offsetY;
|
|
488
|
+
if (selfLink) {
|
|
489
|
+
let segDirX = 1;
|
|
490
|
+
let segDirY = 0;
|
|
491
|
+
if (fromAttach.inward) {
|
|
492
|
+
const dx = fromEnd.x - fromAttach.inward.x;
|
|
493
|
+
const dy = fromEnd.y - fromAttach.inward.y;
|
|
494
|
+
const len = Math.hypot(dx, dy);
|
|
495
|
+
if (len > 0) {
|
|
496
|
+
segDirX = dx / len;
|
|
497
|
+
segDirY = dy / len;
|
|
498
|
+
}
|
|
549
499
|
}
|
|
500
|
+
const ext = selfLoopReach(fromSegments, scale);
|
|
501
|
+
const perpX = -segDirY;
|
|
502
|
+
const perpY = segDirX;
|
|
503
|
+
const midX = (fromEnd.x + toStart.x) / 2 + offsetX + perpX * ext;
|
|
504
|
+
const midY = (fromEnd.y + toStart.y) / 2 + offsetY + perpY * ext;
|
|
505
|
+
const cp1x = p1x + segDirX * ext;
|
|
506
|
+
const cp1y = p1y + segDirY * ext;
|
|
507
|
+
const cp2x = p2x - segDirX * ext;
|
|
508
|
+
const cp2y = p2y - segDirY * ext;
|
|
509
|
+
return [
|
|
510
|
+
{
|
|
511
|
+
x0: p1x,
|
|
512
|
+
y0: p1y,
|
|
513
|
+
cx0: cp1x,
|
|
514
|
+
cy0: cp1y,
|
|
515
|
+
cx1: cp1x + perpX * ext,
|
|
516
|
+
cy1: cp1y + perpY * ext,
|
|
517
|
+
x1: midX,
|
|
518
|
+
y1: midY
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
x0: midX,
|
|
522
|
+
y0: midY,
|
|
523
|
+
cx0: cp2x + perpX * ext,
|
|
524
|
+
cy0: cp2y + perpY * ext,
|
|
525
|
+
cx1: cp2x,
|
|
526
|
+
cy1: cp2y,
|
|
527
|
+
x1: p2x,
|
|
528
|
+
y1: p2y
|
|
529
|
+
}
|
|
530
|
+
];
|
|
531
|
+
} else {
|
|
532
|
+
const fromPrev = fromAttach.inward ?? {
|
|
533
|
+
x: fromEnd.x - (toStart.x - fromEnd.x),
|
|
534
|
+
y: fromEnd.y - (toStart.y - fromEnd.y)
|
|
535
|
+
};
|
|
536
|
+
const toNext = toAttach.inward ?? {
|
|
537
|
+
x: toStart.x - (fromEnd.x - toStart.x),
|
|
538
|
+
y: toStart.y - (fromEnd.y - toStart.y)
|
|
539
|
+
};
|
|
540
|
+
const dist = Math.hypot(p2x - p1x, p2y - p1y);
|
|
541
|
+
const tangentProj = (prev, at, towardX, towardY) => {
|
|
542
|
+
const dx = at.x - prev.x;
|
|
543
|
+
const dy = at.y - prev.y;
|
|
544
|
+
const len = Math.hypot(dx, dy);
|
|
545
|
+
const along = len === 0 ? 0 : (towardX * dx + towardY * dy) / len;
|
|
546
|
+
return Math.max(
|
|
547
|
+
0,
|
|
548
|
+
Math.min(dist * TANGENT_SPAN_FACTOR, MAX_TANGENT, along * 0.5)
|
|
549
|
+
);
|
|
550
|
+
};
|
|
551
|
+
const towardToX = toStart.x - fromEnd.x;
|
|
552
|
+
const towardToY = toStart.y - fromEnd.y;
|
|
553
|
+
const [cx1, cy1] = projectLine(
|
|
554
|
+
fromPrev.x,
|
|
555
|
+
fromPrev.y,
|
|
556
|
+
fromEnd.x,
|
|
557
|
+
fromEnd.y,
|
|
558
|
+
tangentProj(fromPrev, fromEnd, towardToX, towardToY)
|
|
559
|
+
);
|
|
560
|
+
const [cx2, cy2] = projectLine(
|
|
561
|
+
toNext.x,
|
|
562
|
+
toNext.y,
|
|
563
|
+
toStart.x,
|
|
564
|
+
toStart.y,
|
|
565
|
+
tangentProj(toNext, toStart, -towardToX, -towardToY)
|
|
566
|
+
);
|
|
567
|
+
const bulge = bypassed.length ? bowAround(p1x, p1y, p2x, p2y, bypassed, maxBow) : 0;
|
|
568
|
+
const chordLen = dist === 0 ? 1 : dist;
|
|
569
|
+
const ux = (p2x - p1x) / chordLen;
|
|
570
|
+
const uy = (p2y - p1y) / chordLen;
|
|
571
|
+
const bulgeX = -uy * bulge;
|
|
572
|
+
const bulgeY = ux * bulge;
|
|
573
|
+
const spreadX = ux * Math.abs(bulge);
|
|
574
|
+
const spreadY = uy * Math.abs(bulge);
|
|
575
|
+
return [
|
|
576
|
+
{
|
|
577
|
+
x0: p1x,
|
|
578
|
+
y0: p1y,
|
|
579
|
+
cx0: cx1 + offsetX + bulgeX - spreadX,
|
|
580
|
+
cy0: cy1 + offsetY + bulgeY - spreadY,
|
|
581
|
+
cx1: cx2 + offsetX + bulgeX + spreadX,
|
|
582
|
+
cy1: cy2 + offsetY + bulgeY + spreadY,
|
|
583
|
+
x1: p2x,
|
|
584
|
+
y1: p2y
|
|
585
|
+
}
|
|
586
|
+
];
|
|
550
587
|
}
|
|
551
|
-
const lengths = new Map(gfaGraph.nodes.map((n2) => [n2.id, n2.length]));
|
|
552
|
-
const anchoring = gfaGraph.paths.length > 0 || gfaGraph.walks.length > 0 ? surveyPaths(anchorablePaths(gfaGraph), (id) => lengths.get(id) ?? 0) : void 0;
|
|
553
|
-
return {
|
|
554
|
-
name,
|
|
555
|
-
nodes,
|
|
556
|
-
edges,
|
|
557
|
-
paths: paths.length > 0 ? paths : void 0,
|
|
558
|
-
anchorPaths: anchoring?.anchorPaths,
|
|
559
|
-
pathVisits: anchoring?.pathVisits,
|
|
560
|
-
// rGFA's own tags win wherever they are present; pathAnchoring only ever
|
|
561
|
-
// fills in for a graph that carries none.
|
|
562
|
-
anchoredBy: nodes.some((n2) => n2.stable) ? "tags" : void 0
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// src/layout/mergeRuns.ts
|
|
567
|
-
function ownStrand(id) {
|
|
568
|
-
return id.endsWith("-") ? "-" : "+";
|
|
569
|
-
}
|
|
570
|
-
function flip(strand) {
|
|
571
|
-
return strand === "+" ? "-" : "+";
|
|
572
588
|
}
|
|
573
|
-
function
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
return
|
|
589
|
+
function nearerEnd(segments, to) {
|
|
590
|
+
const start = segments[0];
|
|
591
|
+
const end = segments.at(-1);
|
|
592
|
+
return Math.hypot(start.x - to.x, start.y - to.y) < Math.hypot(end.x - to.x, end.y - to.y) ? "start" : "end";
|
|
577
593
|
}
|
|
578
|
-
function
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
594
|
+
function routedEdgeCurves(fromSegments, toSegments, route) {
|
|
595
|
+
const fromAttach = attachment(
|
|
596
|
+
fromSegments,
|
|
597
|
+
nearerEnd(fromSegments, route[0])
|
|
598
|
+
);
|
|
599
|
+
const toAttach = attachment(toSegments, nearerEnd(toSegments, route.at(-1)));
|
|
600
|
+
const points = [fromAttach.at, ...route, toAttach.at];
|
|
601
|
+
const tangent = (i) => {
|
|
602
|
+
const p = points[i];
|
|
603
|
+
const span = (q) => Math.hypot(q.x - p.x, q.y - p.y);
|
|
604
|
+
const along = (inward, next) => {
|
|
605
|
+
const len = inward ? span(inward) : 0;
|
|
606
|
+
const reach = span(next);
|
|
607
|
+
return len === 0 ? { x: next.x - p.x, y: next.y - p.y } : {
|
|
608
|
+
x: (p.x - inward.x) / len * reach,
|
|
609
|
+
y: (p.y - inward.y) / len * reach
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
if (i === 0) {
|
|
613
|
+
return along(fromAttach.inward, points[1]);
|
|
588
614
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
attach(edge.to, sides[ei].to, ei);
|
|
593
|
-
});
|
|
594
|
-
const joinAfter = (a) => {
|
|
595
|
-
const out = at.get(`${a}|end`);
|
|
596
|
-
if (out?.length !== 1) {
|
|
597
|
-
return void 0;
|
|
615
|
+
if (i === points.length - 1) {
|
|
616
|
+
const out = along(toAttach.inward, points[i - 1]);
|
|
617
|
+
return { x: -out.x, y: -out.y };
|
|
598
618
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
619
|
+
return {
|
|
620
|
+
x: (points[i + 1].x - points[i - 1].x) / 2,
|
|
621
|
+
y: (points[i + 1].y - points[i - 1].y) / 2
|
|
622
|
+
};
|
|
602
623
|
};
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
624
|
+
const curves = [];
|
|
625
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
626
|
+
const a = points[i];
|
|
627
|
+
const b = points[i + 1];
|
|
628
|
+
const ta = tangent(i);
|
|
629
|
+
const tb = tangent(i + 1);
|
|
630
|
+
curves.push({
|
|
631
|
+
x0: a.x,
|
|
632
|
+
y0: a.y,
|
|
633
|
+
cx0: a.x + ta.x / 3,
|
|
634
|
+
cy0: a.y + ta.y / 3,
|
|
635
|
+
cx1: b.x - tb.x / 3,
|
|
636
|
+
cy1: b.y - tb.y / 3,
|
|
637
|
+
x1: b.x,
|
|
638
|
+
y1: b.y
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
return curves;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/deletionEdges.ts
|
|
645
|
+
var MIN_DELETION_BP = 1;
|
|
646
|
+
function bypassedPoints(nodePositions, bypassed) {
|
|
647
|
+
return bypassed.flatMap((id) => nodePositions[id] ?? []);
|
|
648
|
+
}
|
|
649
|
+
function deletionArcCurves(nodePositions, deletion, axis, route) {
|
|
650
|
+
const from = nodePositions[deletion.from];
|
|
651
|
+
const to = nodePositions[deletion.to];
|
|
652
|
+
if (!from?.length || !to?.length) {
|
|
653
|
+
return void 0;
|
|
654
|
+
}
|
|
655
|
+
return route ? routedEdgeCurves(from, to, route) : computeEdgeCurves(
|
|
656
|
+
from,
|
|
657
|
+
to,
|
|
658
|
+
deletion.from === deletion.to,
|
|
659
|
+
0,
|
|
660
|
+
0,
|
|
661
|
+
axis,
|
|
662
|
+
bypassedPoints(nodePositions, deletion.bypassed)
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
function backboneBySequence(graph) {
|
|
666
|
+
const bySequence = /* @__PURE__ */ new Map();
|
|
667
|
+
for (const node of graph.nodes) {
|
|
668
|
+
if (isBackbone(node)) {
|
|
669
|
+
const list = bySequence.get(node.stable.refName);
|
|
670
|
+
if (list) {
|
|
671
|
+
list.push(node);
|
|
672
|
+
} else {
|
|
673
|
+
bySequence.set(node.stable.refName, [node]);
|
|
674
|
+
}
|
|
611
675
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
676
|
+
}
|
|
677
|
+
for (const list of bySequence.values()) {
|
|
678
|
+
list.sort((a, b) => a.stable.start - b.stable.start);
|
|
679
|
+
}
|
|
680
|
+
return bySequence;
|
|
681
|
+
}
|
|
682
|
+
function bypassedNodes(nodes, start, end) {
|
|
683
|
+
const bypassed = [];
|
|
684
|
+
if (nodes) {
|
|
685
|
+
for (let i = firstNodeAtOrAfter(nodes, start); i < nodes.length; i++) {
|
|
686
|
+
const node = nodes[i];
|
|
687
|
+
if (node.stable.start >= end) {
|
|
617
688
|
break;
|
|
618
689
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
for (const id of run) {
|
|
623
|
-
runOf.set(id, node.id);
|
|
624
|
-
}
|
|
625
|
-
if (run.length > 1) {
|
|
626
|
-
runs.set(node.id, run);
|
|
690
|
+
if (node.stable.start + node.length <= end) {
|
|
691
|
+
bypassed.push(node.id);
|
|
692
|
+
}
|
|
627
693
|
}
|
|
628
694
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
695
|
+
return bypassed;
|
|
696
|
+
}
|
|
697
|
+
function skipsBetween(edge, from, to, forwards) {
|
|
698
|
+
const { fromStrand, toStrand } = edge;
|
|
699
|
+
if (fromStrand === void 0 || toStrand === void 0) {
|
|
700
|
+
return true;
|
|
633
701
|
}
|
|
702
|
+
const leavesRight = fromStrand === (from.stable.strand ?? "+");
|
|
703
|
+
const arrivesLeft = toStrand === (to.stable.strand ?? "+");
|
|
704
|
+
return forwards ? leavesRight && arrivesLeft : !leavesRight && !arrivesLeft;
|
|
705
|
+
}
|
|
706
|
+
function deletionEdges(graph) {
|
|
634
707
|
const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
if (
|
|
642
|
-
|
|
643
|
-
|
|
708
|
+
const backbone = backboneBySequence(graph);
|
|
709
|
+
const found = [];
|
|
710
|
+
for (let edgeIndex = 0; edgeIndex < graph.edges.length; edgeIndex++) {
|
|
711
|
+
const edge = graph.edges[edgeIndex];
|
|
712
|
+
const from = byId.get(edge.from);
|
|
713
|
+
const to = byId.get(edge.to);
|
|
714
|
+
if (from !== void 0 && to !== void 0 && isBackbone(from) && isBackbone(to) && from.stable.refName === to.stable.refName) {
|
|
715
|
+
const forwards = from.stable.start <= to.stable.start;
|
|
716
|
+
const [left, right] = forwards ? [from, to] : [to, from];
|
|
717
|
+
const start = left.stable.start + left.length;
|
|
718
|
+
const end = right.stable.start;
|
|
719
|
+
const bp = end - start;
|
|
720
|
+
if (bp >= MIN_DELETION_BP && skipsBetween(edge, from, to, forwards)) {
|
|
721
|
+
const refName = left.stable.refName;
|
|
722
|
+
found.push({
|
|
723
|
+
edgeIndex,
|
|
724
|
+
from: edge.from,
|
|
725
|
+
to: edge.to,
|
|
726
|
+
refName,
|
|
727
|
+
start,
|
|
728
|
+
end,
|
|
729
|
+
bp,
|
|
730
|
+
bypassed: bypassedNodes(backbone.get(refName), start, end)
|
|
731
|
+
});
|
|
732
|
+
}
|
|
644
733
|
}
|
|
645
|
-
const members = run.map((id) => byId.get(id));
|
|
646
|
-
const backbone = members.filter(isBackbone);
|
|
647
|
-
const first = backbone.length ? backbone.reduce((a, b) => a.stable.start <= b.stable.start ? a : b) : members[0];
|
|
648
|
-
nodes.push({
|
|
649
|
-
id: node.id,
|
|
650
|
-
name: node.name,
|
|
651
|
-
length: members.reduce((sum, n2) => sum + n2.length, 0),
|
|
652
|
-
depth: node.depth,
|
|
653
|
-
stable: first.stable
|
|
654
|
-
});
|
|
655
734
|
}
|
|
656
|
-
|
|
657
|
-
const edges = [];
|
|
658
|
-
graph.edges.forEach((edge, ei) => {
|
|
659
|
-
const from = runOf.get(edge.from);
|
|
660
|
-
const to = runOf.get(edge.to);
|
|
661
|
-
const side = sides[ei];
|
|
662
|
-
const key = `${from}|${side.from}>${to}|${side.to}`;
|
|
663
|
-
if (joined.has(ei) || seen.has(key)) {
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
seen.add(key);
|
|
667
|
-
edges.push({
|
|
668
|
-
from,
|
|
669
|
-
to,
|
|
670
|
-
...side.from === "start" ? { fromStrand: flip(ownStrand(from)) } : {},
|
|
671
|
-
...side.to === "end" ? { toStrand: flip(ownStrand(to)) } : {}
|
|
672
|
-
});
|
|
673
|
-
});
|
|
674
|
-
return {
|
|
675
|
-
graph: { ...graph, nodes, edges, paths: void 0, pathVisits: void 0 },
|
|
676
|
-
runs
|
|
677
|
-
};
|
|
735
|
+
return found;
|
|
678
736
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
delete out[id];
|
|
687
|
-
const arc = arcPrefix(line);
|
|
688
|
-
const total = arc.at(-1);
|
|
689
|
-
const weights = members.map((m) => Math.max(weightOf(m), 0));
|
|
690
|
-
const weightSum = weights.reduce((a, b) => a + b, 0) || members.length;
|
|
691
|
-
let from = 0;
|
|
692
|
-
members.forEach((member, k) => {
|
|
693
|
-
const share = weightSum ? weights[k] / weightSum * total : total / members.length;
|
|
694
|
-
const to = k === members.length - 1 ? total : from + share;
|
|
695
|
-
out[member] = slice(line, arc, from, to);
|
|
696
|
-
from = to;
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
return out;
|
|
737
|
+
|
|
738
|
+
// src/pansn.ts
|
|
739
|
+
var SEP = "#";
|
|
740
|
+
function panSNSample(refName) {
|
|
741
|
+
return refName.split(SEP)[0];
|
|
700
742
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
}
|
|
705
|
-
const arc = arcPrefix(line);
|
|
706
|
-
const total = arc.at(-1);
|
|
707
|
-
return slice(line, arc, from * total, to * total);
|
|
743
|
+
var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
|
|
744
|
+
function isGenericSample(sample) {
|
|
745
|
+
return GENERIC_SAMPLES.has(sample);
|
|
708
746
|
}
|
|
709
|
-
function
|
|
710
|
-
const
|
|
711
|
-
return
|
|
747
|
+
function panSNHaplotype(refName) {
|
|
748
|
+
const parts = refName.split(SEP);
|
|
749
|
+
return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : void 0;
|
|
712
750
|
}
|
|
713
|
-
function
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
arc.push(
|
|
717
|
-
arc[i - 1] + Math.hypot(line[i].x - line[i - 1].x, line[i].y - line[i - 1].y)
|
|
718
|
-
);
|
|
719
|
-
}
|
|
720
|
-
return arc;
|
|
751
|
+
function panSNContig(refName) {
|
|
752
|
+
const parts = refName.split(SEP);
|
|
753
|
+
return parts.length >= 3 ? parts.slice(2).join(SEP) : parts.length === 2 ? parts[1] : refName;
|
|
721
754
|
}
|
|
722
|
-
function
|
|
723
|
-
|
|
724
|
-
return { ...line[0] };
|
|
725
|
-
}
|
|
726
|
-
let i = 1;
|
|
727
|
-
while (i < arc.length && arc[i] < s) {
|
|
728
|
-
i++;
|
|
729
|
-
}
|
|
730
|
-
if (i >= arc.length) {
|
|
731
|
-
return { ...line.at(-1) };
|
|
732
|
-
}
|
|
733
|
-
const span = arc[i] - arc[i - 1];
|
|
734
|
-
const t = span > 0 ? (s - arc[i - 1]) / span : 0;
|
|
735
|
-
const a = line[i - 1];
|
|
736
|
-
const b = line[i];
|
|
737
|
-
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
755
|
+
function panSNMatchesPrefix(refName, prefix) {
|
|
756
|
+
return prefix !== void 0 && (refName === prefix || refName.startsWith(prefix + SEP));
|
|
738
757
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
758
|
+
|
|
759
|
+
// src/gfa-core/gfaParser.ts
|
|
760
|
+
function parseTag(tag, tags) {
|
|
761
|
+
const nameEnd = tag.indexOf(":");
|
|
762
|
+
const typeEnd = tag.indexOf(":", nameEnd + 1);
|
|
763
|
+
if (nameEnd !== -1 && typeEnd !== -1) {
|
|
764
|
+
const name = tag.slice(0, nameEnd);
|
|
765
|
+
const type = tag.slice(nameEnd + 1, typeEnd);
|
|
766
|
+
const val = tag.slice(typeEnd + 1);
|
|
767
|
+
if (type === "i" || type === "f") {
|
|
768
|
+
tags[name] = +val;
|
|
769
|
+
} else if (type === "Z") {
|
|
770
|
+
tags[name] = val;
|
|
744
771
|
}
|
|
745
772
|
}
|
|
746
|
-
points.push(pointAt(line, arc, to));
|
|
747
|
-
return points;
|
|
748
773
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
(node) => isBackbone(node) && (positions[node.id]?.length ?? 0) > 0
|
|
754
|
-
);
|
|
755
|
-
if (backbone.length < 2) {
|
|
756
|
-
return positions;
|
|
774
|
+
function parseTags(fields) {
|
|
775
|
+
const tags = {};
|
|
776
|
+
for (const field of fields) {
|
|
777
|
+
parseTag(field, tags);
|
|
757
778
|
}
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
for (const p of points) {
|
|
775
|
-
sxr += p.w * (p.x - cx) * (p.r - cr);
|
|
776
|
-
syr += p.w * (p.y - cy) * (p.r - cr);
|
|
777
|
-
}
|
|
778
|
-
const angle = -Math.atan2(syr, sxr);
|
|
779
|
-
const c = Math.cos(angle);
|
|
780
|
-
const s = Math.sin(angle);
|
|
781
|
-
const out = {};
|
|
782
|
-
for (const [id, segments] of Object.entries(positions)) {
|
|
783
|
-
out[id] = segments.map((p) => {
|
|
784
|
-
const x = p.x - cx;
|
|
785
|
-
const y = p.y - cy;
|
|
786
|
-
return { x: cx + c * x - s * y, y: cy + s * x + c * y };
|
|
787
|
-
});
|
|
779
|
+
return tags;
|
|
780
|
+
}
|
|
781
|
+
function stableCoordinate(node) {
|
|
782
|
+
const { SN, SO, SR } = node.tags;
|
|
783
|
+
return typeof SN === "string" && typeof SO === "number" && typeof SR === "number" ? { refName: SN, start: SO, rank: SR } : void 0;
|
|
784
|
+
}
|
|
785
|
+
var GT = 62;
|
|
786
|
+
var LT = 60;
|
|
787
|
+
function parseWalkBody(body) {
|
|
788
|
+
const segments = [];
|
|
789
|
+
let strand = "+";
|
|
790
|
+
let start = 0;
|
|
791
|
+
function push(end) {
|
|
792
|
+
if (end > start) {
|
|
793
|
+
segments.push({ id: body.slice(start, end), strand });
|
|
794
|
+
}
|
|
788
795
|
}
|
|
789
|
-
let
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
796
|
+
for (let i = 0; i < body.length; i++) {
|
|
797
|
+
const ch = body.charCodeAt(i);
|
|
798
|
+
if (ch === GT || ch === LT) {
|
|
799
|
+
push(i);
|
|
800
|
+
strand = ch === GT ? "+" : "-";
|
|
801
|
+
start = i + 1;
|
|
794
802
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
803
|
+
}
|
|
804
|
+
push(body.length);
|
|
805
|
+
return segments;
|
|
806
|
+
}
|
|
807
|
+
function parseGFA(file) {
|
|
808
|
+
const graph = {
|
|
809
|
+
nodes: [],
|
|
810
|
+
links: [],
|
|
811
|
+
paths: [],
|
|
812
|
+
walks: [],
|
|
813
|
+
header: [],
|
|
814
|
+
id: ""
|
|
815
|
+
};
|
|
816
|
+
for (const rawLine of file.split("\n")) {
|
|
817
|
+
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
818
|
+
if (line.startsWith("H")) {
|
|
819
|
+
const [, ...rest] = line.split(" ");
|
|
820
|
+
graph.header.push(parseTags(rest));
|
|
821
|
+
} else if (line.startsWith("S")) {
|
|
822
|
+
const [, name, ...rest] = line.split(" ");
|
|
823
|
+
let len;
|
|
824
|
+
let seq;
|
|
825
|
+
let tagfields;
|
|
826
|
+
let gfa1 = false;
|
|
827
|
+
const first = rest[0] ?? "*";
|
|
828
|
+
if (/^\d+$/.test(first)) {
|
|
829
|
+
len = +first;
|
|
830
|
+
seq = rest[1] ?? "*";
|
|
831
|
+
tagfields = rest.slice(2);
|
|
798
832
|
} else {
|
|
799
|
-
|
|
833
|
+
gfa1 = true;
|
|
834
|
+
seq = first;
|
|
835
|
+
len = seq === "*" ? 0 : seq.length;
|
|
836
|
+
tagfields = rest.slice(1);
|
|
800
837
|
}
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
838
|
+
const tags = parseTags(tagfields);
|
|
839
|
+
if (gfa1 && "LN" in tags) {
|
|
840
|
+
len = +tags.LN;
|
|
841
|
+
}
|
|
842
|
+
graph.nodes.push({ id: name, length: len, sequence: seq, tags });
|
|
843
|
+
} else if (line.startsWith("E")) {
|
|
844
|
+
const [, , source, target, , , , , cigar, ...rest] = line.split(" ");
|
|
845
|
+
if (source && target) {
|
|
846
|
+
graph.links.push({
|
|
847
|
+
source: source.slice(0, -1),
|
|
848
|
+
target: target.slice(0, -1),
|
|
849
|
+
strand1: source.at(-1),
|
|
850
|
+
strand2: target.at(-1),
|
|
851
|
+
cigar: cigar ?? "*",
|
|
852
|
+
tags: parseTags(rest)
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
} else if (line.startsWith("L")) {
|
|
856
|
+
const [, source, strand1, target, strand2, cigar, ...rest] = line.split(" ");
|
|
857
|
+
if (source && target && strand2) {
|
|
858
|
+
graph.links.push({
|
|
859
|
+
source,
|
|
860
|
+
target,
|
|
861
|
+
strand1,
|
|
862
|
+
strand2,
|
|
863
|
+
cigar: cigar ?? "*",
|
|
864
|
+
tags: parseTags(rest)
|
|
865
|
+
});
|
|
866
|
+
}
|
|
867
|
+
} else if (line.startsWith("W")) {
|
|
868
|
+
const [, sample, hap, contig, start, end, body, ...rest] = line.split(" ");
|
|
869
|
+
if (sample && hap && contig && start && end && body) {
|
|
870
|
+
graph.walks.push({
|
|
871
|
+
sample,
|
|
872
|
+
haplotype: +hap,
|
|
873
|
+
contig,
|
|
874
|
+
start: start === "*" ? -1 : +start,
|
|
875
|
+
end: end === "*" ? -1 : +end,
|
|
876
|
+
segments: parseWalkBody(body),
|
|
877
|
+
tags: parseTags(rest)
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
} else if (line.startsWith("P")) {
|
|
881
|
+
const [, name, path, ...rest] = line.split(" ");
|
|
882
|
+
if (name && path) {
|
|
883
|
+
graph.paths.push({ name, path, rest });
|
|
807
884
|
}
|
|
808
885
|
}
|
|
809
886
|
}
|
|
810
|
-
return
|
|
887
|
+
return graph;
|
|
811
888
|
}
|
|
812
889
|
|
|
813
|
-
// src/
|
|
814
|
-
var
|
|
815
|
-
function
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
const list = neighbors.get(from);
|
|
819
|
-
if (list) {
|
|
820
|
-
list.push(to);
|
|
821
|
-
} else {
|
|
822
|
-
neighbors.set(from, [to]);
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
for (const edge of graph.edges) {
|
|
826
|
-
add(edge.from, edge.to);
|
|
827
|
-
add(edge.to, edge.from);
|
|
828
|
-
}
|
|
829
|
-
return neighbors;
|
|
890
|
+
// src/pathAnchoring.ts
|
|
891
|
+
var RANGE_SUFFIX = /:(\d+)-\d+$/;
|
|
892
|
+
function pathOrigin(pathName) {
|
|
893
|
+
const match = RANGE_SUFFIX.exec(pathName);
|
|
894
|
+
return match ? { name: pathName.slice(0, match.index), start: +match[1] } : { name: pathName, start: 0 };
|
|
830
895
|
}
|
|
831
|
-
function
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (neighbor && isBackbone(neighbor)) {
|
|
850
|
-
minEnd = Math.min(minEnd, neighbor.stable.start + neighbor.length);
|
|
851
|
-
maxStart = Math.max(maxStart, neighbor.stable.start);
|
|
852
|
-
} else {
|
|
853
|
-
next.push(neighborId);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
896
|
+
function surveyPaths(paths, lengthOf) {
|
|
897
|
+
const anchorPaths = [];
|
|
898
|
+
const pathVisits = /* @__PURE__ */ new Map();
|
|
899
|
+
for (const path of paths) {
|
|
900
|
+
const sample = panSNSample(path.name);
|
|
901
|
+
let pos = path.start;
|
|
902
|
+
for (const step of path.steps) {
|
|
903
|
+
const visit = {
|
|
904
|
+
path: path.name,
|
|
905
|
+
sample,
|
|
906
|
+
start: pos,
|
|
907
|
+
strand: step.strand
|
|
908
|
+
};
|
|
909
|
+
const existing = pathVisits.get(step.id);
|
|
910
|
+
if (existing) {
|
|
911
|
+
existing.push(visit);
|
|
912
|
+
} else {
|
|
913
|
+
pathVisits.set(step.id, [visit]);
|
|
857
914
|
}
|
|
915
|
+
pos += lengthOf(step.id);
|
|
858
916
|
}
|
|
859
|
-
|
|
917
|
+
anchorPaths.push({
|
|
918
|
+
name: path.name,
|
|
919
|
+
sample,
|
|
920
|
+
start: path.start,
|
|
921
|
+
length: pos - path.start
|
|
922
|
+
});
|
|
860
923
|
}
|
|
861
|
-
return
|
|
862
|
-
start: Math.min(minEnd, maxStart),
|
|
863
|
-
end: Math.max(minEnd, maxStart)
|
|
864
|
-
};
|
|
924
|
+
return { anchorPaths, pathVisits };
|
|
865
925
|
}
|
|
866
|
-
function
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const node = nodeById.get(nodeId);
|
|
872
|
-
return node === void 0 ? void 0 : isBackbone(node) ? { start: node.stable.start, end: node.stable.start + node.length } : flankingSpan({ nodeId, nodeById, neighbors });
|
|
926
|
+
function chooseReferencePath(anchorPaths, preferred) {
|
|
927
|
+
const named = anchorPaths.find(
|
|
928
|
+
(p) => p.sample === preferred || p.name === preferred
|
|
929
|
+
);
|
|
930
|
+
return named ?? anchorPaths[0];
|
|
873
931
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
932
|
+
var OFF_REFERENCE_RANK = 1;
|
|
933
|
+
function anchorNode(node, visits, reference) {
|
|
934
|
+
let anchored = node;
|
|
935
|
+
if (visits && visits.length > 0) {
|
|
936
|
+
const anchor = visits.find((v) => v.path === reference) ?? visits[0];
|
|
937
|
+
anchored = {
|
|
938
|
+
...node,
|
|
939
|
+
stable: {
|
|
940
|
+
refName: anchor.path,
|
|
941
|
+
start: anchor.start,
|
|
942
|
+
rank: anchor.path === reference ? REFERENCE_RANK : OFF_REFERENCE_RANK,
|
|
943
|
+
strand: anchor.strand
|
|
944
|
+
},
|
|
945
|
+
samples: [...new Set(visits.map((v) => v.sample))].sort()
|
|
946
|
+
};
|
|
883
947
|
}
|
|
884
|
-
return
|
|
948
|
+
return anchored;
|
|
885
949
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
const queue = backbone.map((node) => node.id);
|
|
905
|
-
for (const id of queue) {
|
|
906
|
-
const at = seeds.get(id);
|
|
907
|
-
const next = depth.get(id) + 1;
|
|
908
|
-
for (const neighbour of adjacent.get(id) ?? []) {
|
|
909
|
-
if (depth.has(neighbour)) {
|
|
910
|
-
continue;
|
|
911
|
-
}
|
|
912
|
-
depth.set(neighbour, next);
|
|
913
|
-
seeds.set(neighbour, { x: at.x + drawnOf(id) / 2, y: LANE_GAP * next });
|
|
914
|
-
queue.push(neighbour);
|
|
915
|
-
}
|
|
950
|
+
function anchorFromPaths(graph, preferred) {
|
|
951
|
+
const { anchorPaths, pathVisits } = graph;
|
|
952
|
+
const reference = anchorPaths && pathVisits ? chooseReferencePath(anchorPaths, preferred) : void 0;
|
|
953
|
+
return reference && pathVisits ? {
|
|
954
|
+
...graph,
|
|
955
|
+
nodes: graph.nodes.map(
|
|
956
|
+
(node) => anchorNode(node, pathVisits.get(node.name), reference.name)
|
|
957
|
+
),
|
|
958
|
+
anchoredBy: "paths",
|
|
959
|
+
referencePath: reference.name
|
|
960
|
+
} : graph;
|
|
961
|
+
}
|
|
962
|
+
function anchorGraph(graph, preferred) {
|
|
963
|
+
return graph.anchoredBy === "tags" ? graph : anchorFromPaths(graph, preferred);
|
|
964
|
+
}
|
|
965
|
+
function trimOrigins(graph, kept) {
|
|
966
|
+
if (!graph.anchorPaths) {
|
|
967
|
+
return void 0;
|
|
916
968
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
y: LANE_GAP * 3
|
|
923
|
-
});
|
|
969
|
+
const lengthOf = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
|
|
970
|
+
const bp = (ids, end = ids.length) => {
|
|
971
|
+
let sum = 0;
|
|
972
|
+
for (let i = 0; i < end; i++) {
|
|
973
|
+
sum += lengthOf.get(ids[i]) ?? 0;
|
|
924
974
|
}
|
|
975
|
+
return sum;
|
|
976
|
+
};
|
|
977
|
+
const byOrigin = /* @__PURE__ */ new Map();
|
|
978
|
+
for (const path of graph.paths ?? []) {
|
|
979
|
+
const origin = pathOrigin(path.name).name;
|
|
980
|
+
byOrigin.set(origin, [...byOrigin.get(origin) ?? [], path]);
|
|
925
981
|
}
|
|
926
|
-
return
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
982
|
+
return graph.anchorPaths.map((o) => {
|
|
983
|
+
const pieces = byOrigin.get(o.name);
|
|
984
|
+
const path = pieces?.length === 1 ? pieces[0] : void 0;
|
|
985
|
+
const stretch = path && kept.get(path.name);
|
|
986
|
+
return path && stretch ? {
|
|
987
|
+
...o,
|
|
988
|
+
start: o.start + bp(path.nodeIds, stretch.first),
|
|
989
|
+
length: bp(stretch.nodeIds)
|
|
990
|
+
} : o;
|
|
991
|
+
});
|
|
931
992
|
}
|
|
932
993
|
|
|
933
|
-
// src/
|
|
934
|
-
function
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
994
|
+
// src/gfa/gfaConverter.ts
|
|
995
|
+
function surveySegments(gfaGraph) {
|
|
996
|
+
const canonical = /* @__PURE__ */ new Map();
|
|
997
|
+
const traversals = /* @__PURE__ */ new Map();
|
|
998
|
+
function claim(id, strand) {
|
|
999
|
+
if (!canonical.has(id)) {
|
|
1000
|
+
canonical.set(id, strand);
|
|
1001
|
+
}
|
|
941
1002
|
}
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
);
|
|
1003
|
+
function traverse(id, strand) {
|
|
1004
|
+
claim(id, strand);
|
|
1005
|
+
traversals.set(id, (traversals.get(id) ?? 0) + 1);
|
|
946
1006
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
edges: graph.edges
|
|
955
|
-
},
|
|
956
|
-
options: {
|
|
957
|
-
quality: settings.quality,
|
|
958
|
-
linearLayout: settings.linearLayout,
|
|
959
|
-
...scaling.opts,
|
|
960
|
-
...anchored ? { rotateComponents: false } : {}
|
|
1007
|
+
for (const link of gfaGraph.links) {
|
|
1008
|
+
claim(link.source, link.strand1 === "-" ? "-" : "+");
|
|
1009
|
+
claim(link.target, link.strand2 === "-" ? "-" : "+");
|
|
1010
|
+
}
|
|
1011
|
+
for (const gfaPath of gfaGraph.paths) {
|
|
1012
|
+
for (const segment of gfaPath.path.split(",")) {
|
|
1013
|
+
traverse(segment.slice(0, -1), segment.endsWith("-") ? "-" : "+");
|
|
961
1014
|
}
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
return `${settings.quality}|${settings.linearLayout}|${settings.bubbleSpread}|${anchored}|${graph.referencePath ?? ""}`;
|
|
967
|
-
}
|
|
968
|
-
async function forceLayout(graph, settings, engine) {
|
|
969
|
-
const spread = spreadFor(settings.bubbleSpread);
|
|
970
|
-
const merged = mergeRuns(graph);
|
|
971
|
-
const { result, duration } = await engine(
|
|
972
|
-
engineRequest(merged.graph, layoutScaling(merged.graph, spread), settings)
|
|
973
|
-
);
|
|
974
|
-
const scaling = layoutScaling(graph, spread);
|
|
975
|
-
const drawn = new Map(
|
|
976
|
-
scaling.nodes.map((n2) => [n2.id, drawnNodeLength(scaling.opts, n2.length)])
|
|
977
|
-
);
|
|
978
|
-
const positions = splitRuns(
|
|
979
|
-
result.nodePositions,
|
|
980
|
-
merged.runs,
|
|
981
|
-
(id) => drawn.get(id) ?? 0
|
|
982
|
-
);
|
|
983
|
-
return {
|
|
984
|
-
result: {
|
|
985
|
-
...result,
|
|
986
|
-
nodePositions: graph.nodes.some(isBackbone) ? orientToReference(graph, positions) : positions
|
|
987
|
-
},
|
|
988
|
-
duration
|
|
989
|
-
};
|
|
990
|
-
}
|
|
991
|
-
function layoutExtent(nodePositions) {
|
|
992
|
-
let minX = Infinity;
|
|
993
|
-
let minY = Infinity;
|
|
994
|
-
let maxX = -Infinity;
|
|
995
|
-
let maxY = -Infinity;
|
|
996
|
-
for (const segments of Object.values(nodePositions)) {
|
|
997
|
-
for (const seg of segments) {
|
|
998
|
-
minX = Math.min(minX, seg.x);
|
|
999
|
-
minY = Math.min(minY, seg.y);
|
|
1000
|
-
maxX = Math.max(maxX, seg.x);
|
|
1001
|
-
maxY = Math.max(maxY, seg.y);
|
|
1015
|
+
}
|
|
1016
|
+
for (const walk of gfaGraph.walks) {
|
|
1017
|
+
for (const seg of walk.segments) {
|
|
1018
|
+
traverse(seg.id, seg.strand === "-" ? "-" : "+");
|
|
1002
1019
|
}
|
|
1003
1020
|
}
|
|
1004
|
-
return {
|
|
1005
|
-
}
|
|
1006
|
-
var FIT_PADDING = 40;
|
|
1007
|
-
var MIN_ZOOM = 1e-6;
|
|
1008
|
-
var MAX_ZOOM = 100;
|
|
1009
|
-
function clampZoom(zoom) {
|
|
1010
|
-
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
|
|
1021
|
+
return { canonical, traversals };
|
|
1011
1022
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
return
|
|
1023
|
+
var walkStart = (w) => w.start === -1 ? 0 : w.start;
|
|
1024
|
+
function anchorablePaths(gfaGraph) {
|
|
1025
|
+
return [
|
|
1026
|
+
...gfaGraph.paths.map((p) => ({
|
|
1027
|
+
...pathOrigin(p.name),
|
|
1028
|
+
steps: p.path.split(",").map((segment) => ({
|
|
1029
|
+
id: segment.slice(0, -1),
|
|
1030
|
+
strand: segment.endsWith("-") ? "-" : "+"
|
|
1031
|
+
}))
|
|
1032
|
+
})),
|
|
1033
|
+
...gfaGraph.walks.map((w) => ({
|
|
1034
|
+
name: `${w.sample}#${w.haplotype}#${w.contig}`,
|
|
1035
|
+
start: walkStart(w),
|
|
1036
|
+
steps: w.segments.map((s) => ({
|
|
1037
|
+
id: s.id,
|
|
1038
|
+
strand: s.strand === "-" ? "-" : "+"
|
|
1039
|
+
}))
|
|
1040
|
+
}))
|
|
1041
|
+
];
|
|
1015
1042
|
}
|
|
1016
|
-
function
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
padTop = FIT_PADDING,
|
|
1020
|
-
padRight = FIT_PADDING,
|
|
1021
|
-
padBottom = FIT_PADDING
|
|
1022
|
-
} = {}) {
|
|
1023
|
-
const usableWidth = paneWidth - padLeft - padRight;
|
|
1024
|
-
const usableHeight = paneHeight - padTop - padBottom;
|
|
1025
|
-
if (usableWidth <= 0 || usableHeight <= 0 || !(bounds.w > 0 || !pixelRows && bounds.h > 0)) {
|
|
1043
|
+
function carriedSamples(gfaNode) {
|
|
1044
|
+
const sm = gfaNode.tags.SM;
|
|
1045
|
+
if (typeof sm !== "string") {
|
|
1026
1046
|
return void 0;
|
|
1027
1047
|
}
|
|
1028
|
-
const
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
const
|
|
1048
|
+
const samples = sm.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1049
|
+
return samples.length > 0 ? samples : void 0;
|
|
1050
|
+
}
|
|
1051
|
+
var positive = (v) => typeof v === "number" && v > 0 ? v : void 0;
|
|
1052
|
+
function statedDepth({ tags, length }) {
|
|
1053
|
+
const count = positive(tags.RC) ?? positive(tags.FC) ?? positive(tags.KC);
|
|
1054
|
+
return positive(tags.dp) ?? positive(tags.DP) ?? (count !== void 0 && length > 0 ? count / length : void 0);
|
|
1055
|
+
}
|
|
1056
|
+
function makeNode(gfaNode, strand, depth) {
|
|
1034
1057
|
return {
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
padTop
|
|
1042
|
-
)
|
|
1058
|
+
id: `${gfaNode.id}${strand}`,
|
|
1059
|
+
name: gfaNode.id,
|
|
1060
|
+
length: gfaNode.length,
|
|
1061
|
+
depth,
|
|
1062
|
+
stable: stableCoordinate(gfaNode),
|
|
1063
|
+
samples: carriedSamples(gfaNode)
|
|
1043
1064
|
};
|
|
1044
1065
|
}
|
|
1045
|
-
function
|
|
1046
|
-
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1066
|
+
function convertGFAToGraph(gfaGraph, name = "Imported GFA") {
|
|
1067
|
+
const nodes = [];
|
|
1068
|
+
const edges = [];
|
|
1069
|
+
const { canonical, traversals } = surveySegments(gfaGraph);
|
|
1070
|
+
const nodeId = (segmentId) => `${segmentId}${canonical.get(segmentId) ?? "+"}`;
|
|
1071
|
+
for (const gfaNode of gfaGraph.nodes) {
|
|
1072
|
+
const depth = statedDepth(gfaNode) ?? Math.max(traversals.get(gfaNode.id) ?? 1, 1);
|
|
1073
|
+
nodes.push(makeNode(gfaNode, canonical.get(gfaNode.id) ?? "+", depth));
|
|
1051
1074
|
}
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1075
|
+
for (const link of gfaGraph.links) {
|
|
1076
|
+
edges.push({
|
|
1077
|
+
from: nodeId(link.source),
|
|
1078
|
+
to: nodeId(link.target),
|
|
1079
|
+
fromStrand: link.strand1 === "-" ? "-" : "+",
|
|
1080
|
+
toStrand: link.strand2 === "-" ? "-" : "+"
|
|
1081
|
+
});
|
|
1058
1082
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
1070
|
-
function projectLine(x1, y1, x2, y2, distance) {
|
|
1071
|
-
const d = Math.hypot(y2 - y1, x2 - x1);
|
|
1072
|
-
if (d === 0) {
|
|
1073
|
-
return [x2, y2];
|
|
1083
|
+
const paths = [];
|
|
1084
|
+
const edgeToPathsMap = /* @__PURE__ */ new Map();
|
|
1085
|
+
const edgeKey = (a, b) => [a, b].sort().join("--");
|
|
1086
|
+
function recordPathEdges(nodeIds, name2) {
|
|
1087
|
+
for (let i = 0; i < nodeIds.length - 1; i++) {
|
|
1088
|
+
const key = edgeKey(nodeIds[i], nodeIds[i + 1]);
|
|
1089
|
+
if (!edgeToPathsMap.has(key)) {
|
|
1090
|
+
edgeToPathsMap.set(key, /* @__PURE__ */ new Set());
|
|
1091
|
+
}
|
|
1092
|
+
edgeToPathsMap.get(key).add(name2);
|
|
1093
|
+
}
|
|
1074
1094
|
}
|
|
1075
|
-
const
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
const d = 3 * u * t * t;
|
|
1084
|
-
const e = t * t * t;
|
|
1085
|
-
return {
|
|
1086
|
-
x: a * c.x0 + b * c.cx0 + d * c.cx1 + e * c.x1,
|
|
1087
|
-
y: a * c.y0 + b * c.cy0 + d * c.cy1 + e * c.y1
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
function curveMidpoint(curves) {
|
|
1091
|
-
const mid = curves[Math.floor(curves.length / 2)];
|
|
1092
|
-
return mid ? curvePointAt(mid, 0.5) : void 0;
|
|
1093
|
-
}
|
|
1094
|
-
function curveBounds(curves) {
|
|
1095
|
-
let minX = Infinity;
|
|
1096
|
-
let minY = Infinity;
|
|
1097
|
-
let maxX = -Infinity;
|
|
1098
|
-
let maxY = -Infinity;
|
|
1099
|
-
for (const c of curves) {
|
|
1100
|
-
minX = Math.min(minX, c.x0, c.cx0, c.cx1, c.x1);
|
|
1101
|
-
maxX = Math.max(maxX, c.x0, c.cx0, c.cx1, c.x1);
|
|
1102
|
-
minY = Math.min(minY, c.y0, c.cy0, c.cy1, c.y1);
|
|
1103
|
-
maxY = Math.max(maxY, c.y0, c.cy0, c.cy1, c.y1);
|
|
1095
|
+
for (const gfaPath of gfaGraph.paths) {
|
|
1096
|
+
const nodeIds = gfaPath.path.split(",").map((segment) => nodeId(segment.slice(0, -1)));
|
|
1097
|
+
paths.push({
|
|
1098
|
+
name: gfaPath.name,
|
|
1099
|
+
nodeIds,
|
|
1100
|
+
start: pathOrigin(gfaPath.name).start
|
|
1101
|
+
});
|
|
1102
|
+
recordPathEdges(nodeIds, gfaPath.name);
|
|
1104
1103
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1104
|
+
for (const walk of gfaGraph.walks) {
|
|
1105
|
+
const nodeIds = walk.segments.map((seg) => nodeId(seg.id));
|
|
1106
|
+
const name2 = `${walk.sample}#${walk.haplotype}#${walk.contig}`;
|
|
1107
|
+
paths.push({
|
|
1108
|
+
name: name2,
|
|
1109
|
+
nodeIds,
|
|
1110
|
+
start: walkStart(walk),
|
|
1111
|
+
sample: walk.sample,
|
|
1112
|
+
haplotype: walk.haplotype,
|
|
1113
|
+
contig: walk.contig
|
|
1114
|
+
});
|
|
1115
|
+
recordPathEdges(nodeIds, name2);
|
|
1116
|
+
}
|
|
1117
|
+
for (const edge of edges) {
|
|
1118
|
+
const pathIds = edgeToPathsMap.get(edgeKey(edge.from, edge.to));
|
|
1119
|
+
if (pathIds && pathIds.size > 0) {
|
|
1120
|
+
edge.pathIds = [...pathIds];
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const lengths = new Map(gfaGraph.nodes.map((n2) => [n2.id, n2.length]));
|
|
1124
|
+
const anchoring = gfaGraph.paths.length > 0 || gfaGraph.walks.length > 0 ? surveyPaths(anchorablePaths(gfaGraph), (id) => lengths.get(id) ?? 0) : void 0;
|
|
1113
1125
|
return {
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1126
|
+
name,
|
|
1127
|
+
nodes,
|
|
1128
|
+
edges,
|
|
1129
|
+
paths: paths.length > 0 ? paths : void 0,
|
|
1130
|
+
anchorPaths: anchoring?.anchorPaths,
|
|
1131
|
+
pathVisits: anchoring?.pathVisits,
|
|
1132
|
+
// rGFA's own tags win wherever they are present; pathAnchoring only ever
|
|
1133
|
+
// fills in for a graph that carries none.
|
|
1134
|
+
anchoredBy: nodes.some((n2) => n2.stable) ? "tags" : void 0
|
|
1122
1135
|
};
|
|
1123
1136
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1137
|
+
|
|
1138
|
+
// src/layout/deletionRoutes.ts
|
|
1139
|
+
var ROUTE_ID = "\0deletion:";
|
|
1140
|
+
var ROUTE_FRACTION = 0.5;
|
|
1141
|
+
function withDeletionRoutes(graph, deletions) {
|
|
1142
|
+
const routes = new Map(
|
|
1143
|
+
deletions.map((d) => [d.edgeIndex, `${ROUTE_ID}${d.edgeIndex}`])
|
|
1144
|
+
);
|
|
1145
|
+
const edges = [];
|
|
1146
|
+
graph.edges.forEach((edge, ei) => {
|
|
1147
|
+
const id = routes.get(ei);
|
|
1148
|
+
if (id === void 0) {
|
|
1149
|
+
edges.push(edge);
|
|
1150
|
+
} else {
|
|
1151
|
+
const { fromStrand, toStrand } = edge;
|
|
1152
|
+
edges.push(
|
|
1153
|
+
{ from: edge.from, to: id, ...fromStrand ? { fromStrand } : {} },
|
|
1154
|
+
{ from: id, to: edge.to, ...toStrand ? { toStrand } : {} }
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
});
|
|
1129
1158
|
return {
|
|
1130
|
-
|
|
1131
|
-
|
|
1159
|
+
graph: {
|
|
1160
|
+
...graph,
|
|
1161
|
+
nodes: [
|
|
1162
|
+
...graph.nodes,
|
|
1163
|
+
...[...routes.values()].map((id) => ({
|
|
1164
|
+
id,
|
|
1165
|
+
name: id,
|
|
1166
|
+
length: 0,
|
|
1167
|
+
depth: 0
|
|
1168
|
+
}))
|
|
1169
|
+
],
|
|
1170
|
+
edges
|
|
1171
|
+
},
|
|
1172
|
+
routes,
|
|
1173
|
+
ids: new Set(routes.values())
|
|
1132
1174
|
};
|
|
1133
1175
|
}
|
|
1134
|
-
function
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
y0: c.y0 + dy,
|
|
1152
|
-
cx0: c.cx0 + dx,
|
|
1153
|
-
cy0: c.cy0 + dy,
|
|
1154
|
-
cx1: c.cx1 + dx,
|
|
1155
|
-
cy1: c.cy1 + dy,
|
|
1156
|
-
x1: c.x1 + dx,
|
|
1157
|
-
y1: c.y1 + dy
|
|
1176
|
+
function routeNodes(graph, deletions, routes, drawnOf, opts) {
|
|
1177
|
+
const lengths = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
|
|
1178
|
+
return deletions.map((d) => {
|
|
1179
|
+
const skipped = d.bypassed.reduce(
|
|
1180
|
+
(sum, id2) => sum + (lengths.get(id2) ?? 0),
|
|
1181
|
+
0
|
|
1182
|
+
);
|
|
1183
|
+
const drawn = Math.max(
|
|
1184
|
+
opts.minimumNodeLength,
|
|
1185
|
+
drawnOf(skipped) * ROUTE_FRACTION
|
|
1186
|
+
);
|
|
1187
|
+
const id = routes.get(d.edgeIndex);
|
|
1188
|
+
return {
|
|
1189
|
+
id,
|
|
1190
|
+
name: id,
|
|
1191
|
+
length: Math.round(drawn * 1e6 / opts.nodeLengthPerMegabase),
|
|
1192
|
+
depth: 0
|
|
1158
1193
|
};
|
|
1159
|
-
}
|
|
1160
|
-
return out;
|
|
1161
|
-
}
|
|
1162
|
-
function attachment(segments, side) {
|
|
1163
|
-
const last = segments.length - 1;
|
|
1164
|
-
return side === "end" ? { at: segments[last], inward: segments[last - 1] } : { at: segments[0], inward: segments[1] };
|
|
1165
|
-
}
|
|
1166
|
-
var TANGENT_SPAN_FACTOR = 0.5;
|
|
1167
|
-
var MAX_TANGENT = 80;
|
|
1168
|
-
function spanOf(segments) {
|
|
1169
|
-
let min = Infinity;
|
|
1170
|
-
let max = -Infinity;
|
|
1171
|
-
for (const s of segments) {
|
|
1172
|
-
min = Math.min(min, s.x);
|
|
1173
|
-
max = Math.max(max, s.x);
|
|
1174
|
-
}
|
|
1175
|
-
return { min, max };
|
|
1176
|
-
}
|
|
1177
|
-
function distanceToSpan(x, span) {
|
|
1178
|
-
return Math.max(0, span.min - x, x - span.max);
|
|
1179
|
-
}
|
|
1180
|
-
function facingSide(segments, other, tieBreak) {
|
|
1181
|
-
const startGap = distanceToSpan(segments[0].x, other);
|
|
1182
|
-
const endGap = distanceToSpan(segments[segments.length - 1].x, other);
|
|
1183
|
-
return startGap === endGap ? tieBreak : startGap < endGap ? "start" : "end";
|
|
1184
|
-
}
|
|
1185
|
-
function facingSides(fromSegments, toSegments) {
|
|
1186
|
-
return {
|
|
1187
|
-
from: facingSide(fromSegments, spanOf(toSegments), "end"),
|
|
1188
|
-
to: facingSide(toSegments, spanOf(fromSegments), "start")
|
|
1189
|
-
};
|
|
1194
|
+
});
|
|
1190
1195
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
let reach = 0;
|
|
1200
|
-
let minAlong = Infinity;
|
|
1201
|
-
let maxAlong = -Infinity;
|
|
1202
|
-
for (const p of toward) {
|
|
1203
|
-
const dx = p.x - p1x;
|
|
1204
|
-
const dy = p.y - p1y;
|
|
1205
|
-
const perp = dx * -uy + dy * ux;
|
|
1206
|
-
if (Math.abs(perp) > Math.abs(reach)) {
|
|
1207
|
-
reach = perp;
|
|
1196
|
+
function takeRoutes(positions, routes) {
|
|
1197
|
+
const nodePositions = { ...positions };
|
|
1198
|
+
const deletionRoutes = {};
|
|
1199
|
+
for (const [edgeIndex, id] of routes) {
|
|
1200
|
+
const route = nodePositions[id];
|
|
1201
|
+
delete nodePositions[id];
|
|
1202
|
+
if (route?.length) {
|
|
1203
|
+
deletionRoutes[edgeIndex] = route;
|
|
1208
1204
|
}
|
|
1209
|
-
const along = dx * ux + dy * uy;
|
|
1210
|
-
minAlong = Math.min(minAlong, along);
|
|
1211
|
-
maxAlong = Math.max(maxAlong, along);
|
|
1212
1205
|
}
|
|
1213
|
-
|
|
1214
|
-
const uncapped = Math.max(
|
|
1215
|
-
Math.abs(reach) * BOW_CLEARANCE / APEX_FRACTION,
|
|
1216
|
-
span * ALONG_FRACTION
|
|
1217
|
-
);
|
|
1218
|
-
const size = maxBow === void 0 ? uncapped : Math.min(uncapped, maxBow);
|
|
1219
|
-
return reach < 0 ? -size : size;
|
|
1220
|
-
}
|
|
1221
|
-
function yToXOf({ scaleX, scaleY }) {
|
|
1222
|
-
return scaleY / scaleX;
|
|
1206
|
+
return { nodePositions, deletionRoutes };
|
|
1223
1207
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
const len = Math.hypot(dx, dy);
|
|
1229
|
-
return len === 0 ? void 0 : { x: dx / len, y: dy / len };
|
|
1208
|
+
|
|
1209
|
+
// src/layout/mergeRuns.ts
|
|
1210
|
+
function ownStrand(id) {
|
|
1211
|
+
return id.endsWith("-") ? "-" : "+";
|
|
1230
1212
|
}
|
|
1231
|
-
function
|
|
1232
|
-
|
|
1233
|
-
const dir = drawnDirection(
|
|
1234
|
-
fromSegments[fromSegments.length - 1],
|
|
1235
|
-
toSegments[0],
|
|
1236
|
-
yToX
|
|
1237
|
-
) ?? drawnDirection(fromSegments[0], fromSegments.at(-1), yToX) ?? drawnDirection(toSegments[0], toSegments.at(-1), yToX) ?? { x: 1, y: 0 };
|
|
1238
|
-
const offsets = [];
|
|
1239
|
-
for (let i = 0; i < count; i++) {
|
|
1240
|
-
const distance = (i - (count - 1) / 2) * spacing;
|
|
1241
|
-
offsets.push({ x: -dir.y * distance, y: dir.x * distance / yToX });
|
|
1242
|
-
}
|
|
1243
|
-
return offsets;
|
|
1213
|
+
function flip(strand) {
|
|
1214
|
+
return strand === "+" ? "-" : "+";
|
|
1244
1215
|
}
|
|
1245
|
-
function
|
|
1246
|
-
|
|
1216
|
+
function sidesOf(edge) {
|
|
1217
|
+
const from = (edge.fromStrand ?? ownStrand(edge.from)) === ownStrand(edge.from) ? "end" : "start";
|
|
1218
|
+
const to = (edge.toStrand ?? ownStrand(edge.to)) === ownStrand(edge.to) ? "start" : "end";
|
|
1219
|
+
return { from, to };
|
|
1247
1220
|
}
|
|
1248
|
-
function
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1221
|
+
function mergeRuns(graph, alone = /* @__PURE__ */ new Set()) {
|
|
1222
|
+
const sides = graph.edges.map(sidesOf);
|
|
1223
|
+
const at = /* @__PURE__ */ new Map();
|
|
1224
|
+
const attach = (id, side, ei) => {
|
|
1225
|
+
const key = `${id}|${side}`;
|
|
1226
|
+
const list = at.get(key);
|
|
1227
|
+
if (list) {
|
|
1228
|
+
list.push(ei);
|
|
1229
|
+
} else {
|
|
1230
|
+
at.set(key, [ei]);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
graph.edges.forEach((edge, ei) => {
|
|
1234
|
+
attach(edge.from, sides[ei].from, ei);
|
|
1235
|
+
attach(edge.to, sides[ei].to, ei);
|
|
1236
|
+
});
|
|
1237
|
+
const joinAfter = (a) => {
|
|
1238
|
+
const out = at.get(`${a}|end`);
|
|
1239
|
+
if (out?.length !== 1) {
|
|
1240
|
+
return void 0;
|
|
1241
|
+
}
|
|
1242
|
+
const ei = out[0];
|
|
1243
|
+
const edge = graph.edges[ei];
|
|
1244
|
+
return edge.from === a && edge.to !== a && !alone.has(a) && !alone.has(edge.to) && sides[ei].from === "end" && sides[ei].to === "start" && at.get(`${edge.to}|start`)?.length === 1 ? ei : void 0;
|
|
1245
|
+
};
|
|
1246
|
+
const joined = /* @__PURE__ */ new Set();
|
|
1247
|
+
const runOf = /* @__PURE__ */ new Map();
|
|
1248
|
+
const runs = /* @__PURE__ */ new Map();
|
|
1249
|
+
for (const node of graph.nodes) {
|
|
1250
|
+
const into = at.get(`${node.id}|start`);
|
|
1251
|
+
const pred = into?.length === 1 ? graph.edges[into[0]].from : void 0;
|
|
1252
|
+
if (runOf.has(node.id) || pred !== void 0 && joinAfter(pred) === into[0]) {
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
const run = [node.id];
|
|
1256
|
+
for (; ; ) {
|
|
1257
|
+
const ei = joinAfter(run.at(-1));
|
|
1258
|
+
const next = ei === void 0 ? void 0 : graph.edges[ei].to;
|
|
1259
|
+
if (next === void 0 || runOf.has(next)) {
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
joined.add(ei);
|
|
1263
|
+
run.push(next);
|
|
1264
|
+
}
|
|
1265
|
+
for (const id of run) {
|
|
1266
|
+
runOf.set(id, node.id);
|
|
1267
|
+
}
|
|
1268
|
+
if (run.length > 1) {
|
|
1269
|
+
runs.set(node.id, run);
|
|
1270
|
+
}
|
|
1254
1271
|
}
|
|
1255
|
-
|
|
1272
|
+
for (const node of graph.nodes) {
|
|
1273
|
+
if (!runOf.has(node.id)) {
|
|
1274
|
+
runOf.set(node.id, node.id);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
|
|
1278
|
+
const nodes = [];
|
|
1279
|
+
for (const node of graph.nodes) {
|
|
1280
|
+
if (runOf.get(node.id) !== node.id) {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
const run = runs.get(node.id);
|
|
1284
|
+
if (!run) {
|
|
1285
|
+
nodes.push(node);
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
const members = run.map((id) => byId.get(id));
|
|
1289
|
+
const backbone = members.filter(isBackbone);
|
|
1290
|
+
const first = backbone.length ? backbone.reduce((a, b) => a.stable.start <= b.stable.start ? a : b) : members[0];
|
|
1291
|
+
nodes.push({
|
|
1292
|
+
id: node.id,
|
|
1293
|
+
name: node.name,
|
|
1294
|
+
length: members.reduce((sum, n2) => sum + n2.length, 0),
|
|
1295
|
+
depth: node.depth,
|
|
1296
|
+
stable: first.stable
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1300
|
+
const edges = [];
|
|
1301
|
+
graph.edges.forEach((edge, ei) => {
|
|
1302
|
+
const from = runOf.get(edge.from);
|
|
1303
|
+
const to = runOf.get(edge.to);
|
|
1304
|
+
const side = sides[ei];
|
|
1305
|
+
const key = `${from}|${side.from}>${to}|${side.to}`;
|
|
1306
|
+
if (joined.has(ei) || seen.has(key)) {
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
seen.add(key);
|
|
1310
|
+
edges.push({
|
|
1311
|
+
from,
|
|
1312
|
+
to,
|
|
1313
|
+
...side.from === "start" ? { fromStrand: flip(ownStrand(from)) } : {},
|
|
1314
|
+
...side.to === "end" ? { toStrand: flip(ownStrand(to)) } : {}
|
|
1315
|
+
});
|
|
1316
|
+
});
|
|
1317
|
+
return {
|
|
1318
|
+
graph: { ...graph, nodes, edges, paths: void 0, pathVisits: void 0 },
|
|
1319
|
+
runs
|
|
1320
|
+
};
|
|
1256
1321
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1322
|
+
function splitRuns(positions, runs, weightOf) {
|
|
1323
|
+
const out = { ...positions };
|
|
1324
|
+
for (const [id, members] of runs) {
|
|
1325
|
+
const line = positions[id];
|
|
1326
|
+
if (!line?.length) {
|
|
1327
|
+
continue;
|
|
1328
|
+
}
|
|
1329
|
+
delete out[id];
|
|
1330
|
+
const arc = arcPrefix(line);
|
|
1331
|
+
const total = arc.at(-1);
|
|
1332
|
+
const weights = members.map((m) => Math.max(weightOf(m), 0));
|
|
1333
|
+
const weightSum = weights.reduce((a, b) => a + b, 0) || members.length;
|
|
1334
|
+
let from = 0;
|
|
1335
|
+
members.forEach((member, k) => {
|
|
1336
|
+
const share = weightSum ? weights[k] / weightSum * total : total / members.length;
|
|
1337
|
+
const to = k === members.length - 1 ? total : from + share;
|
|
1338
|
+
out[member] = slice(line, arc, from, to);
|
|
1339
|
+
from = to;
|
|
1340
|
+
});
|
|
1341
|
+
}
|
|
1342
|
+
return out;
|
|
1343
|
+
}
|
|
1344
|
+
function polylineSlice(line, from, to) {
|
|
1345
|
+
if (line.length === 0) {
|
|
1346
|
+
return [];
|
|
1347
|
+
}
|
|
1348
|
+
const arc = arcPrefix(line);
|
|
1349
|
+
const total = arc.at(-1);
|
|
1350
|
+
return slice(line, arc, from * total, to * total);
|
|
1351
|
+
}
|
|
1352
|
+
function polylineMidpoint(line) {
|
|
1353
|
+
const arc = arcPrefix(line);
|
|
1354
|
+
return pointAt(line, arc, arc.at(-1) / 2);
|
|
1355
|
+
}
|
|
1356
|
+
function arcPrefix(line) {
|
|
1357
|
+
const arc = [0];
|
|
1358
|
+
for (let i = 1; i < line.length; i++) {
|
|
1359
|
+
arc.push(
|
|
1360
|
+
arc[i - 1] + Math.hypot(line[i].x - line[i - 1].x, line[i].y - line[i - 1].y)
|
|
1265
1361
|
);
|
|
1266
1362
|
}
|
|
1267
|
-
|
|
1268
|
-
const reachPx = Math.min(
|
|
1269
|
-
SELF_LOOP_MAX_PX,
|
|
1270
|
-
Math.max(SELF_LOOP_MIN_PX, nodePx / 2)
|
|
1271
|
-
);
|
|
1272
|
-
return reachPx / Math.max(scale, Number.EPSILON);
|
|
1363
|
+
return arc;
|
|
1273
1364
|
}
|
|
1274
|
-
function
|
|
1275
|
-
if (
|
|
1276
|
-
return
|
|
1365
|
+
function pointAt(line, arc, s) {
|
|
1366
|
+
if (s <= 0) {
|
|
1367
|
+
return { ...line[0] };
|
|
1277
1368
|
}
|
|
1278
|
-
|
|
1369
|
+
let i = 1;
|
|
1370
|
+
while (i < arc.length && arc[i] < s) {
|
|
1371
|
+
i++;
|
|
1372
|
+
}
|
|
1373
|
+
if (i >= arc.length) {
|
|
1374
|
+
return { ...line.at(-1) };
|
|
1375
|
+
}
|
|
1376
|
+
const span = arc[i] - arc[i - 1];
|
|
1377
|
+
const t = span > 0 ? (s - arc[i - 1]) / span : 0;
|
|
1378
|
+
const a = line[i - 1];
|
|
1379
|
+
const b = line[i];
|
|
1380
|
+
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
1279
1381
|
}
|
|
1280
|
-
function
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
const ux = dx / len;
|
|
1286
|
-
const uy = dy / len;
|
|
1287
|
-
const px = at.x + offsetX;
|
|
1288
|
-
const py = at.y + offsetY;
|
|
1289
|
-
const nearX = px + ux * reach / 3;
|
|
1290
|
-
const nearY = py + uy * reach / 3;
|
|
1291
|
-
const apexX = px + ux * reach;
|
|
1292
|
-
const apexY = py + uy * reach;
|
|
1293
|
-
const sideX = -uy * reach / 2;
|
|
1294
|
-
const sideY = ux * reach / 2;
|
|
1295
|
-
return [
|
|
1296
|
-
{
|
|
1297
|
-
x0: px,
|
|
1298
|
-
y0: py,
|
|
1299
|
-
cx0: nearX,
|
|
1300
|
-
cy0: nearY,
|
|
1301
|
-
cx1: apexX + sideX,
|
|
1302
|
-
cy1: apexY + sideY,
|
|
1303
|
-
x1: apexX,
|
|
1304
|
-
y1: apexY
|
|
1305
|
-
},
|
|
1306
|
-
{
|
|
1307
|
-
x0: apexX,
|
|
1308
|
-
y0: apexY,
|
|
1309
|
-
cx0: apexX - sideX,
|
|
1310
|
-
cy0: apexY - sideY,
|
|
1311
|
-
cx1: nearX,
|
|
1312
|
-
cy1: nearY,
|
|
1313
|
-
x1: px,
|
|
1314
|
-
y1: py
|
|
1382
|
+
function slice(line, arc, from, to) {
|
|
1383
|
+
const points = [pointAt(line, arc, from)];
|
|
1384
|
+
for (let i = 0; i < line.length; i++) {
|
|
1385
|
+
if (arc[i] > from && arc[i] < to) {
|
|
1386
|
+
points.push({ ...line[i] });
|
|
1315
1387
|
}
|
|
1316
|
-
|
|
1388
|
+
}
|
|
1389
|
+
points.push(pointAt(line, arc, to));
|
|
1390
|
+
return points;
|
|
1317
1391
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
const
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
scaleYOf(toSegments, yToX),
|
|
1327
|
-
selfLink,
|
|
1328
|
-
offsetX,
|
|
1329
|
-
offsetY * yToX,
|
|
1330
|
-
{ scaleX: scale, scaleY: scale },
|
|
1331
|
-
scaleYOf(bypassed, yToX),
|
|
1332
|
-
// the cap is a y measure and the body works in x units, so it converts
|
|
1333
|
-
// with everything else that crosses this boundary
|
|
1334
|
-
maxBow === void 0 ? void 0 : maxBow * yToX
|
|
1335
|
-
),
|
|
1336
|
-
yToX
|
|
1337
|
-
);
|
|
1392
|
+
|
|
1393
|
+
// src/layout/orientToReference.ts
|
|
1394
|
+
function orientToReference(graph, positions) {
|
|
1395
|
+
const backbone = graph.nodes.filter(
|
|
1396
|
+
(node) => isBackbone(node) && (positions[node.id]?.length ?? 0) > 0
|
|
1397
|
+
);
|
|
1398
|
+
if (backbone.length < 2) {
|
|
1399
|
+
return positions;
|
|
1338
1400
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1401
|
+
const points = backbone.map((node) => {
|
|
1402
|
+
const segments = positions[node.id];
|
|
1403
|
+
const mid = segments[Math.floor(segments.length / 2)];
|
|
1404
|
+
return {
|
|
1405
|
+
x: mid.x,
|
|
1406
|
+
y: mid.y,
|
|
1407
|
+
r: node.stable.start + node.length / 2,
|
|
1408
|
+
w: node.length
|
|
1409
|
+
};
|
|
1410
|
+
});
|
|
1411
|
+
const weight = points.reduce((sum, p) => sum + p.w, 0);
|
|
1412
|
+
const cx = points.reduce((sum, p) => sum + p.x * p.w, 0) / weight;
|
|
1413
|
+
const cy = points.reduce((sum, p) => sum + p.y * p.w, 0) / weight;
|
|
1414
|
+
const cr = points.reduce((sum, p) => sum + p.r * p.w, 0) / weight;
|
|
1415
|
+
let sxr = 0;
|
|
1416
|
+
let syr = 0;
|
|
1417
|
+
for (const p of points) {
|
|
1418
|
+
sxr += p.w * (p.x - cx) * (p.r - cr);
|
|
1419
|
+
syr += p.w * (p.y - cy) * (p.r - cr);
|
|
1346
1420
|
}
|
|
1347
|
-
const
|
|
1348
|
-
const
|
|
1349
|
-
const
|
|
1350
|
-
const
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1421
|
+
const angle = -Math.atan2(syr, sxr);
|
|
1422
|
+
const c = Math.cos(angle);
|
|
1423
|
+
const s = Math.sin(angle);
|
|
1424
|
+
const out = {};
|
|
1425
|
+
for (const [id, segments] of Object.entries(positions)) {
|
|
1426
|
+
out[id] = segments.map((p) => {
|
|
1427
|
+
const x = p.x - cx;
|
|
1428
|
+
const y = p.y - cy;
|
|
1429
|
+
return { x: cx + c * x - s * y, y: cy + s * x + c * y };
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
let below = 0;
|
|
1433
|
+
let above = 0;
|
|
1434
|
+
for (const node of graph.nodes) {
|
|
1435
|
+
if (isBackbone(node)) {
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
for (const p of out[node.id] ?? []) {
|
|
1439
|
+
if (p.y > cy) {
|
|
1440
|
+
below++;
|
|
1441
|
+
} else {
|
|
1442
|
+
above++;
|
|
1366
1443
|
}
|
|
1367
1444
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
const cp1x = p1x + segDirX * ext;
|
|
1374
|
-
const cp1y = p1y + segDirY * ext;
|
|
1375
|
-
const cp2x = p2x - segDirX * ext;
|
|
1376
|
-
const cp2y = p2y - segDirY * ext;
|
|
1377
|
-
return [
|
|
1378
|
-
{
|
|
1379
|
-
x0: p1x,
|
|
1380
|
-
y0: p1y,
|
|
1381
|
-
cx0: cp1x,
|
|
1382
|
-
cy0: cp1y,
|
|
1383
|
-
cx1: cp1x + perpX * ext,
|
|
1384
|
-
cy1: cp1y + perpY * ext,
|
|
1385
|
-
x1: midX,
|
|
1386
|
-
y1: midY
|
|
1387
|
-
},
|
|
1388
|
-
{
|
|
1389
|
-
x0: midX,
|
|
1390
|
-
y0: midY,
|
|
1391
|
-
cx0: cp2x + perpX * ext,
|
|
1392
|
-
cy0: cp2y + perpY * ext,
|
|
1393
|
-
cx1: cp2x,
|
|
1394
|
-
cy1: cp2y,
|
|
1395
|
-
x1: p2x,
|
|
1396
|
-
y1: p2y
|
|
1445
|
+
}
|
|
1446
|
+
if (above > below) {
|
|
1447
|
+
for (const segments of Object.values(out)) {
|
|
1448
|
+
for (const p of segments) {
|
|
1449
|
+
p.y = 2 * cy - p.y;
|
|
1397
1450
|
}
|
|
1398
|
-
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return out;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/referenceSpan.ts
|
|
1457
|
+
var MAX_BACKBONE_HOPS = 4;
|
|
1458
|
+
function buildNeighbors(graph) {
|
|
1459
|
+
const neighbors = /* @__PURE__ */ new Map();
|
|
1460
|
+
function add(from, to) {
|
|
1461
|
+
const list = neighbors.get(from);
|
|
1462
|
+
if (list) {
|
|
1463
|
+
list.push(to);
|
|
1464
|
+
} else {
|
|
1465
|
+
neighbors.set(from, [to]);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
for (const edge of graph.edges) {
|
|
1469
|
+
add(edge.from, edge.to);
|
|
1470
|
+
add(edge.to, edge.from);
|
|
1471
|
+
}
|
|
1472
|
+
return neighbors;
|
|
1473
|
+
}
|
|
1474
|
+
function flankingSpan({
|
|
1475
|
+
nodeId,
|
|
1476
|
+
nodeById,
|
|
1477
|
+
neighbors
|
|
1478
|
+
}) {
|
|
1479
|
+
const seen = /* @__PURE__ */ new Set([nodeId]);
|
|
1480
|
+
let frontier = [nodeId];
|
|
1481
|
+
let minEnd = Infinity;
|
|
1482
|
+
let maxStart = -Infinity;
|
|
1483
|
+
for (let hop = 0; hop < MAX_BACKBONE_HOPS && frontier.length > 0; hop++) {
|
|
1484
|
+
const next = [];
|
|
1485
|
+
for (const id of frontier) {
|
|
1486
|
+
const adjacent = neighbors.get(id);
|
|
1487
|
+
if (adjacent) {
|
|
1488
|
+
for (const neighborId of adjacent) {
|
|
1489
|
+
if (!seen.has(neighborId)) {
|
|
1490
|
+
seen.add(neighborId);
|
|
1491
|
+
const neighbor = nodeById.get(neighborId);
|
|
1492
|
+
if (neighbor && isBackbone(neighbor)) {
|
|
1493
|
+
minEnd = Math.min(minEnd, neighbor.stable.start + neighbor.length);
|
|
1494
|
+
maxStart = Math.max(maxStart, neighbor.stable.start);
|
|
1495
|
+
} else {
|
|
1496
|
+
next.push(neighborId);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
frontier = next;
|
|
1503
|
+
}
|
|
1504
|
+
return minEnd === Infinity ? void 0 : {
|
|
1505
|
+
start: Math.min(minEnd, maxStart),
|
|
1506
|
+
end: Math.max(minEnd, maxStart)
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
function nodeReferenceSpan({
|
|
1510
|
+
nodeId,
|
|
1511
|
+
nodeById,
|
|
1512
|
+
neighbors
|
|
1513
|
+
}) {
|
|
1514
|
+
const node = nodeById.get(nodeId);
|
|
1515
|
+
return node === void 0 ? void 0 : isBackbone(node) ? { start: node.stable.start, end: node.stable.start + node.length } : flankingSpan({ nodeId, nodeById, neighbors });
|
|
1516
|
+
}
|
|
1517
|
+
function referenceMidpoints(graph) {
|
|
1518
|
+
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1519
|
+
const neighbors = buildNeighbors(graph);
|
|
1520
|
+
const midpoints = /* @__PURE__ */ new Map();
|
|
1521
|
+
for (const node of graph.nodes) {
|
|
1522
|
+
const span = nodeReferenceSpan({ nodeId: node.id, nodeById, neighbors });
|
|
1523
|
+
if (span) {
|
|
1524
|
+
midpoints.set(node.id, (span.start + span.end) / 2);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return midpoints;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/layout/referenceSeeds.ts
|
|
1531
|
+
var LANE_GAP = 30;
|
|
1532
|
+
function referenceSeeds(graph, scaling) {
|
|
1533
|
+
const { opts } = scaling;
|
|
1534
|
+
const drawn = new Map(
|
|
1535
|
+
scaling.nodes.map((node) => [node.id, drawnNodeLength(opts, node.length)])
|
|
1536
|
+
);
|
|
1537
|
+
const drawnOf = (id) => drawn.get(id) ?? opts.minimumNodeLength;
|
|
1538
|
+
const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
|
|
1539
|
+
const seeds = /* @__PURE__ */ new Map();
|
|
1540
|
+
let x = 0;
|
|
1541
|
+
for (const node of backbone) {
|
|
1542
|
+
seeds.set(node.id, { x, y: 0 });
|
|
1543
|
+
x += drawnOf(node.id) + opts.edgeLength;
|
|
1544
|
+
}
|
|
1545
|
+
const adjacent = buildNeighbors(graph);
|
|
1546
|
+
const depth = new Map(backbone.map((node) => [node.id, 0]));
|
|
1547
|
+
const queue = backbone.map((node) => node.id);
|
|
1548
|
+
for (const id of queue) {
|
|
1549
|
+
const at = seeds.get(id);
|
|
1550
|
+
const next = depth.get(id) + 1;
|
|
1551
|
+
for (const neighbour of adjacent.get(id) ?? []) {
|
|
1552
|
+
if (depth.has(neighbour)) {
|
|
1553
|
+
continue;
|
|
1453
1554
|
}
|
|
1454
|
-
|
|
1555
|
+
depth.set(neighbour, next);
|
|
1556
|
+
seeds.set(neighbour, { x: at.x + drawnOf(id) / 2, y: LANE_GAP * next });
|
|
1557
|
+
queue.push(neighbour);
|
|
1558
|
+
}
|
|
1455
1559
|
}
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
// src/deletionEdges.ts
|
|
1459
|
-
var MIN_DELETION_BP = 1;
|
|
1460
|
-
function bypassedPoints(nodePositions, bypassed) {
|
|
1461
|
-
return bypassed.flatMap((id) => nodePositions[id] ?? []);
|
|
1462
|
-
}
|
|
1463
|
-
function deletionArcCurves(nodePositions, deletion, axis) {
|
|
1464
|
-
const from = nodePositions[deletion.from];
|
|
1465
|
-
const to = nodePositions[deletion.to];
|
|
1466
|
-
return from?.length && to?.length ? computeEdgeCurves(
|
|
1467
|
-
from,
|
|
1468
|
-
to,
|
|
1469
|
-
deletion.from === deletion.to,
|
|
1470
|
-
0,
|
|
1471
|
-
0,
|
|
1472
|
-
axis,
|
|
1473
|
-
bypassedPoints(nodePositions, deletion.bypassed)
|
|
1474
|
-
) : void 0;
|
|
1475
|
-
}
|
|
1476
|
-
function backboneBySequence(graph) {
|
|
1477
|
-
const bySequence = /* @__PURE__ */ new Map();
|
|
1560
|
+
let unreached = 0;
|
|
1478
1561
|
for (const node of graph.nodes) {
|
|
1479
|
-
if (
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
}
|
|
1484
|
-
bySequence.set(node.stable.refName, [node]);
|
|
1485
|
-
}
|
|
1562
|
+
if (!seeds.has(node.id)) {
|
|
1563
|
+
seeds.set(node.id, {
|
|
1564
|
+
x: unreached++ * x / graph.nodes.length,
|
|
1565
|
+
y: LANE_GAP * 3
|
|
1566
|
+
});
|
|
1486
1567
|
}
|
|
1487
1568
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1569
|
+
return seeds;
|
|
1570
|
+
}
|
|
1571
|
+
function seededNodes(graph, scaling) {
|
|
1572
|
+
const seeds = referenceSeeds(graph, scaling);
|
|
1573
|
+
return scaling.nodes.map((node) => ({ ...node, ...seeds.get(node.id) }));
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/pipeline.ts
|
|
1577
|
+
function loadGraph(text, name, opts = {}) {
|
|
1578
|
+
const graph = anchorGraph(
|
|
1579
|
+
convertGFAToGraph(parseGFA(text), name),
|
|
1580
|
+
opts.referencePath
|
|
1581
|
+
);
|
|
1582
|
+
if (graph.nodes.length === 0) {
|
|
1583
|
+
throw new Error(`No graph segments in ${name}`);
|
|
1490
1584
|
}
|
|
1491
|
-
|
|
1585
|
+
if (opts.maxNodes !== void 0 && graph.nodes.length > opts.maxNodes) {
|
|
1586
|
+
throw new Error(
|
|
1587
|
+
`Graph too large to draw: ${graph.nodes.length.toLocaleString()} nodes (limit ${opts.maxNodes.toLocaleString()}). Zoom in to a smaller region, or raise maxGraphNodes on this view.`
|
|
1588
|
+
);
|
|
1589
|
+
}
|
|
1590
|
+
return graph;
|
|
1492
1591
|
}
|
|
1493
|
-
function
|
|
1494
|
-
const
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1592
|
+
function engineRequest(graph, scaling, settings) {
|
|
1593
|
+
const anchored = graph.nodes.some(isBackbone);
|
|
1594
|
+
return {
|
|
1595
|
+
graph: {
|
|
1596
|
+
nodes: anchored ? seededNodes(graph, scaling) : scaling.nodes,
|
|
1597
|
+
edges: graph.edges
|
|
1598
|
+
},
|
|
1599
|
+
options: {
|
|
1600
|
+
quality: settings.quality,
|
|
1601
|
+
linearLayout: settings.linearLayout,
|
|
1602
|
+
...scaling.opts,
|
|
1603
|
+
...anchored ? { rotateComponents: false } : {}
|
|
1604
|
+
}
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
function engineKey(graph, settings) {
|
|
1608
|
+
const anchored = graph.nodes.some(isBackbone);
|
|
1609
|
+
return `${settings.quality}|${settings.linearLayout}|${settings.bubbleSpread}|${anchored}|${graph.referencePath ?? ""}`;
|
|
1610
|
+
}
|
|
1611
|
+
async function forceLayout(graph, settings, engine) {
|
|
1612
|
+
const spread = spreadFor(settings.bubbleSpread);
|
|
1613
|
+
const deletions = deletionEdges(graph);
|
|
1614
|
+
const routed = withDeletionRoutes(graph, deletions);
|
|
1615
|
+
const merged = mergeRuns(routed.graph, routed.ids);
|
|
1616
|
+
const runs = {
|
|
1617
|
+
...merged.graph,
|
|
1618
|
+
nodes: merged.graph.nodes.filter((n2) => !routed.ids.has(n2.id))
|
|
1619
|
+
};
|
|
1620
|
+
const engineScaling = layoutScaling(runs, spread);
|
|
1621
|
+
const { result, duration } = await engine(
|
|
1622
|
+
engineRequest(
|
|
1623
|
+
merged.graph,
|
|
1624
|
+
{
|
|
1625
|
+
...engineScaling,
|
|
1626
|
+
nodes: [
|
|
1627
|
+
...engineScaling.nodes,
|
|
1628
|
+
...routeNodes(
|
|
1629
|
+
graph,
|
|
1630
|
+
deletions,
|
|
1631
|
+
routed.routes,
|
|
1632
|
+
drawnLengthOf(runs, spread),
|
|
1633
|
+
engineScaling.opts
|
|
1634
|
+
)
|
|
1635
|
+
]
|
|
1636
|
+
},
|
|
1637
|
+
settings
|
|
1638
|
+
)
|
|
1639
|
+
);
|
|
1640
|
+
const scaling = layoutScaling(graph, spread);
|
|
1641
|
+
const drawn = new Map(
|
|
1642
|
+
scaling.nodes.map((n2) => [n2.id, drawnNodeLength(scaling.opts, n2.length)])
|
|
1643
|
+
);
|
|
1644
|
+
const positions = splitRuns(
|
|
1645
|
+
result.nodePositions,
|
|
1646
|
+
merged.runs,
|
|
1647
|
+
(id) => drawn.get(id) ?? 0
|
|
1648
|
+
);
|
|
1649
|
+
const { nodePositions, deletionRoutes } = takeRoutes(
|
|
1650
|
+
graph.nodes.some(isBackbone) ? orientToReference(graph, positions) : positions,
|
|
1651
|
+
routed.routes
|
|
1652
|
+
);
|
|
1653
|
+
return {
|
|
1654
|
+
result: {
|
|
1655
|
+
...result,
|
|
1656
|
+
nodePositions,
|
|
1657
|
+
...deletions.length ? { deletionRoutes, extent: layoutExtent(deletionRoutes) } : {}
|
|
1658
|
+
},
|
|
1659
|
+
duration
|
|
1660
|
+
};
|
|
1661
|
+
}
|
|
1662
|
+
function layoutExtent(nodePositions) {
|
|
1663
|
+
let minX = Infinity;
|
|
1664
|
+
let minY = Infinity;
|
|
1665
|
+
let maxX = -Infinity;
|
|
1666
|
+
let maxY = -Infinity;
|
|
1667
|
+
for (const segments of Object.values(nodePositions)) {
|
|
1668
|
+
for (const seg of segments) {
|
|
1669
|
+
minX = Math.min(minX, seg.x);
|
|
1670
|
+
minY = Math.min(minY, seg.y);
|
|
1671
|
+
maxX = Math.max(maxX, seg.x);
|
|
1672
|
+
maxY = Math.max(maxY, seg.y);
|
|
1504
1673
|
}
|
|
1505
1674
|
}
|
|
1506
|
-
return
|
|
1675
|
+
return { minX, minY, maxX, maxY };
|
|
1507
1676
|
}
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1677
|
+
var FIT_PADDING = 40;
|
|
1678
|
+
var MIN_ZOOM = 1e-6;
|
|
1679
|
+
var MAX_ZOOM = 100;
|
|
1680
|
+
function clampZoom(zoom) {
|
|
1681
|
+
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
|
|
1682
|
+
}
|
|
1683
|
+
function fittedTranslateY(bounds, usableHeight, yScale, padTop = FIT_PADDING) {
|
|
1684
|
+
const leftoverY = usableHeight - bounds.h * yScale;
|
|
1685
|
+
return padTop - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
|
|
1686
|
+
}
|
|
1687
|
+
function fitTransform(bounds, paneWidth, paneHeight, pixelRows, {
|
|
1688
|
+
minScale = 0,
|
|
1689
|
+
padLeft = FIT_PADDING,
|
|
1690
|
+
padTop = FIT_PADDING,
|
|
1691
|
+
padRight = FIT_PADDING,
|
|
1692
|
+
padBottom = FIT_PADDING
|
|
1693
|
+
} = {}) {
|
|
1694
|
+
const usableWidth = paneWidth - padLeft - padRight;
|
|
1695
|
+
const usableHeight = paneHeight - padTop - padBottom;
|
|
1696
|
+
if (usableWidth <= 0 || usableHeight <= 0 || !(bounds.w > 0 || !pixelRows && bounds.h > 0)) {
|
|
1697
|
+
return void 0;
|
|
1512
1698
|
}
|
|
1513
|
-
const
|
|
1514
|
-
const
|
|
1515
|
-
|
|
1699
|
+
const fitX = bounds.w > 0 ? usableWidth / bounds.w : Infinity;
|
|
1700
|
+
const fitY = bounds.h > 0 ? usableHeight / bounds.h : Infinity;
|
|
1701
|
+
const scale = clampZoom(
|
|
1702
|
+
Math.max(minScale, pixelRows ? fitX : Math.min(fitX, fitY))
|
|
1703
|
+
);
|
|
1704
|
+
const leftoverX = usableWidth - bounds.w * scale;
|
|
1705
|
+
return {
|
|
1706
|
+
scale,
|
|
1707
|
+
translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
|
|
1708
|
+
translateY: fittedTranslateY(
|
|
1709
|
+
bounds,
|
|
1710
|
+
usableHeight,
|
|
1711
|
+
pixelRows ? 1 : scale,
|
|
1712
|
+
padTop
|
|
1713
|
+
)
|
|
1714
|
+
};
|
|
1516
1715
|
}
|
|
1517
|
-
function
|
|
1518
|
-
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
const from = byId.get(edge.from);
|
|
1524
|
-
const to = byId.get(edge.to);
|
|
1525
|
-
if (from !== void 0 && to !== void 0 && isBackbone(from) && isBackbone(to) && from.stable.refName === to.stable.refName) {
|
|
1526
|
-
const forwards = from.stable.start <= to.stable.start;
|
|
1527
|
-
const [left, right] = forwards ? [from, to] : [to, from];
|
|
1528
|
-
const start = left.stable.start + left.length;
|
|
1529
|
-
const end = right.stable.start;
|
|
1530
|
-
const bp = end - start;
|
|
1531
|
-
if (bp >= MIN_DELETION_BP && skipsBetween(edge, from, to, forwards)) {
|
|
1532
|
-
const refName = left.stable.refName;
|
|
1533
|
-
found.push({
|
|
1534
|
-
edgeIndex,
|
|
1535
|
-
from: edge.from,
|
|
1536
|
-
to: edge.to,
|
|
1537
|
-
refName,
|
|
1538
|
-
start,
|
|
1539
|
-
end,
|
|
1540
|
-
bp,
|
|
1541
|
-
bypassed: bypassedNodes(backbone.get(refName), start, end)
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
}
|
|
1716
|
+
function drawingBounds(layout, opts = {}) {
|
|
1717
|
+
let { minX, minY, maxX, maxY } = layoutExtent(layout.nodePositions);
|
|
1718
|
+
const { region } = opts;
|
|
1719
|
+
if (layout.referenceAxis && region && region.end > region.start) {
|
|
1720
|
+
minX = region.start;
|
|
1721
|
+
maxX = region.end;
|
|
1545
1722
|
}
|
|
1546
|
-
|
|
1723
|
+
const extent = opts.extent ?? layout.extent;
|
|
1724
|
+
if (extent) {
|
|
1725
|
+
minX = Math.min(minX, extent.minX ?? minX);
|
|
1726
|
+
minY = Math.min(minY, extent.minY ?? minY);
|
|
1727
|
+
maxX = Math.max(maxX, extent.maxX ?? maxX);
|
|
1728
|
+
maxY = Math.max(maxY, extent.maxY ?? maxY);
|
|
1729
|
+
}
|
|
1730
|
+
return { minX, minY, w: maxX - minX, h: maxY - minY };
|
|
1547
1731
|
}
|
|
1548
1732
|
|
|
1549
1733
|
// src/overlayLabels.ts
|
|
@@ -1704,15 +1888,20 @@ function labelOrder(nodePositions, nodeLengths) {
|
|
|
1704
1888
|
return order;
|
|
1705
1889
|
}
|
|
1706
1890
|
var arcCache = /* @__PURE__ */ new WeakMap();
|
|
1707
|
-
function arcPlacements(nodePositions, deletions, axis, version2) {
|
|
1891
|
+
function arcPlacements(nodePositions, deletions, axis, version2, routes) {
|
|
1708
1892
|
const { scaleX, scaleY } = axis;
|
|
1709
1893
|
const cached2 = arcCache.get(nodePositions);
|
|
1710
|
-
if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version2) {
|
|
1894
|
+
if (cached2?.deletions === deletions && cached2.routes === routes && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version2) {
|
|
1711
1895
|
return cached2.arcs;
|
|
1712
1896
|
}
|
|
1713
1897
|
const arcs = [];
|
|
1714
1898
|
for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
|
|
1715
|
-
const curves = deletionArcCurves(
|
|
1899
|
+
const curves = deletionArcCurves(
|
|
1900
|
+
nodePositions,
|
|
1901
|
+
deletion,
|
|
1902
|
+
axis,
|
|
1903
|
+
routes?.[deletion.edgeIndex]
|
|
1904
|
+
);
|
|
1716
1905
|
const apex = curves ? curveMidpoint(curves) : void 0;
|
|
1717
1906
|
if (curves && apex) {
|
|
1718
1907
|
const { minX, minY, maxX, maxY } = curveBounds(curves);
|
|
@@ -1725,13 +1914,21 @@ function arcPlacements(nodePositions, deletions, axis, version2) {
|
|
|
1725
1914
|
});
|
|
1726
1915
|
}
|
|
1727
1916
|
}
|
|
1728
|
-
arcCache.set(nodePositions, {
|
|
1917
|
+
arcCache.set(nodePositions, {
|
|
1918
|
+
deletions,
|
|
1919
|
+
routes,
|
|
1920
|
+
scaleX,
|
|
1921
|
+
scaleY,
|
|
1922
|
+
version: version2,
|
|
1923
|
+
arcs
|
|
1924
|
+
});
|
|
1729
1925
|
return arcs;
|
|
1730
1926
|
}
|
|
1731
1927
|
function sizeLabelCandidates({
|
|
1732
1928
|
nodePositions,
|
|
1733
1929
|
nodeLengths,
|
|
1734
1930
|
deletions,
|
|
1931
|
+
deletionRoutes,
|
|
1735
1932
|
alleleDeletions,
|
|
1736
1933
|
axis,
|
|
1737
1934
|
translateX,
|
|
@@ -1747,7 +1944,8 @@ function sizeLabelCandidates({
|
|
|
1747
1944
|
nodePositions,
|
|
1748
1945
|
deletions,
|
|
1749
1946
|
axis,
|
|
1750
|
-
version2
|
|
1947
|
+
version2,
|
|
1948
|
+
deletionRoutes
|
|
1751
1949
|
)) {
|
|
1752
1950
|
if (extent >= MIN_DELETION_LABEL_PX) {
|
|
1753
1951
|
const text = deletionText(deletion.bp);
|
|
@@ -3262,10 +3460,10 @@ function brightenAbgr(c, factor) {
|
|
|
3262
3460
|
|
|
3263
3461
|
// src/util/edgeCurves.ts
|
|
3264
3462
|
var cache = /* @__PURE__ */ new WeakMap();
|
|
3265
|
-
function baseEdgeCurves(nodePositions, graph, axis, deletions, version2) {
|
|
3463
|
+
function baseEdgeCurves(nodePositions, graph, axis, deletions, version2, routes) {
|
|
3266
3464
|
const { scaleX, scaleY } = axis;
|
|
3267
3465
|
const cached2 = cache.get(nodePositions);
|
|
3268
|
-
if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.version === version2) {
|
|
3466
|
+
if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.routes === routes && cached2.version === version2) {
|
|
3269
3467
|
return cached2.curves;
|
|
3270
3468
|
}
|
|
3271
3469
|
const curves = /* @__PURE__ */ new Map();
|
|
@@ -3275,9 +3473,10 @@ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2) {
|
|
|
3275
3473
|
const to = nodePositions[edge.to];
|
|
3276
3474
|
if (from?.length && to?.length) {
|
|
3277
3475
|
const bypassed = deletions?.get(ei);
|
|
3476
|
+
const route = bypassed && routes?.[ei];
|
|
3278
3477
|
curves.set(
|
|
3279
3478
|
ei,
|
|
3280
|
-
computeEdgeCurves(
|
|
3479
|
+
route ? routedEdgeCurves(from, to, route) : computeEdgeCurves(
|
|
3281
3480
|
from,
|
|
3282
3481
|
to,
|
|
3283
3482
|
selfLinkOf(edge),
|
|
@@ -3294,6 +3493,7 @@ function baseEdgeCurves(nodePositions, graph, axis, deletions, version2) {
|
|
|
3294
3493
|
scaleX,
|
|
3295
3494
|
scaleY,
|
|
3296
3495
|
deletions,
|
|
3496
|
+
routes,
|
|
3297
3497
|
version: version2,
|
|
3298
3498
|
curves
|
|
3299
3499
|
});
|
|
@@ -3758,6 +3958,7 @@ function buildGeometry(options) {
|
|
|
3758
3958
|
viewportBounds,
|
|
3759
3959
|
referenceRamp,
|
|
3760
3960
|
deletions,
|
|
3961
|
+
deletionRoutes,
|
|
3761
3962
|
hiddenEdges,
|
|
3762
3963
|
version: version2 = 0
|
|
3763
3964
|
} = options;
|
|
@@ -3769,7 +3970,8 @@ function buildGeometry(options) {
|
|
|
3769
3970
|
graph,
|
|
3770
3971
|
axis,
|
|
3771
3972
|
deletions,
|
|
3772
|
-
version2
|
|
3973
|
+
version2,
|
|
3974
|
+
deletionRoutes
|
|
3773
3975
|
);
|
|
3774
3976
|
const nodeStrokes = [];
|
|
3775
3977
|
const nodeStrokeRuns = /* @__PURE__ */ new Map();
|
|
@@ -4724,6 +4926,7 @@ function layoutLabels(m) {
|
|
|
4724
4926
|
nodePositions: m.nodePositions,
|
|
4725
4927
|
nodeLengths: m.nodeLengths,
|
|
4726
4928
|
deletions: m.showDeletionEdges ? m.deletions : [],
|
|
4929
|
+
deletionRoutes: m.deletionRoutes,
|
|
4727
4930
|
alleleDeletions: m.alleleDeletions,
|
|
4728
4931
|
axis: m.axisScale,
|
|
4729
4932
|
translateX,
|
|
@@ -5007,6 +5210,56 @@ function walkRowsKeyTree(entries, x, y) {
|
|
|
5007
5210
|
)
|
|
5008
5211
|
};
|
|
5009
5212
|
}
|
|
5213
|
+
function coalesceRuns(runs, pxPerBp) {
|
|
5214
|
+
const minBp = 1 / pxPerBp;
|
|
5215
|
+
const out = [];
|
|
5216
|
+
let bucket = [];
|
|
5217
|
+
const flush = () => {
|
|
5218
|
+
if (bucket.length === 0) {
|
|
5219
|
+
return;
|
|
5220
|
+
}
|
|
5221
|
+
if (bucket.length === 1) {
|
|
5222
|
+
out.push(bucket[0]);
|
|
5223
|
+
} else {
|
|
5224
|
+
let on = 0;
|
|
5225
|
+
let off = 0;
|
|
5226
|
+
for (const r of bucket) {
|
|
5227
|
+
if (r.onReference) {
|
|
5228
|
+
on += r.bp;
|
|
5229
|
+
} else {
|
|
5230
|
+
off += r.bp;
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
const onReference = on >= off;
|
|
5234
|
+
const lead = bucket.find((r) => r.onReference === onReference);
|
|
5235
|
+
out.push({
|
|
5236
|
+
start: bucket[0].start,
|
|
5237
|
+
bp: on + off,
|
|
5238
|
+
onReference,
|
|
5239
|
+
referenceStart: onReference ? lead.referenceStart : void 0,
|
|
5240
|
+
...lead.reversed ? { reversed: true } : {}
|
|
5241
|
+
});
|
|
5242
|
+
}
|
|
5243
|
+
bucket = [];
|
|
5244
|
+
};
|
|
5245
|
+
let held = 0;
|
|
5246
|
+
for (const run of runs) {
|
|
5247
|
+
if (run.gap || run.bp >= minBp) {
|
|
5248
|
+
flush();
|
|
5249
|
+
held = 0;
|
|
5250
|
+
out.push(run);
|
|
5251
|
+
continue;
|
|
5252
|
+
}
|
|
5253
|
+
bucket.push(run);
|
|
5254
|
+
held += run.bp;
|
|
5255
|
+
if (held >= minBp) {
|
|
5256
|
+
flush();
|
|
5257
|
+
held = 0;
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
flush();
|
|
5261
|
+
return out;
|
|
5262
|
+
}
|
|
5010
5263
|
function geneTree(box, y) {
|
|
5011
5264
|
return el(
|
|
5012
5265
|
"g",
|
|
@@ -5051,18 +5304,20 @@ function geneTree(box, y) {
|
|
|
5051
5304
|
}
|
|
5052
5305
|
function walkRowsTree(bars, frame, o = {}) {
|
|
5053
5306
|
const X = (bp) => bp * frame.scaleX + frame.translateX;
|
|
5054
|
-
const
|
|
5307
|
+
const rowPx = frame.rowPx ?? ROW_HEIGHT_PX;
|
|
5308
|
+
const barPx = frame.barPx ?? BAR_PX;
|
|
5309
|
+
const Y = (row) => row * rowPx * frame.scaleY + frame.translateY;
|
|
5055
5310
|
const { origin, unit, reference, rows } = bars;
|
|
5056
5311
|
const along = (offset) => X(origin + offset);
|
|
5057
5312
|
const prefix = o.idPrefix ?? "walkrow";
|
|
5058
5313
|
const rowTree = (row, i) => {
|
|
5059
5314
|
const y = Y(i);
|
|
5060
|
-
if (y < -
|
|
5315
|
+
if (y < -barPx || y > frame.height + barPx) {
|
|
5061
5316
|
return void 0;
|
|
5062
5317
|
}
|
|
5063
|
-
const runs = row.runs.flatMap((run) => {
|
|
5318
|
+
const runs = coalesceRuns(row.runs, frame.scaleX).flatMap((run) => {
|
|
5064
5319
|
const paint2 = runPaint(run, o.ramp);
|
|
5065
|
-
const h = run.gap ? GAP_PX :
|
|
5320
|
+
const h = run.gap ? Math.min(GAP_PX, barPx) : barPx;
|
|
5066
5321
|
const id = `${prefix}-${i}-${run.start}`;
|
|
5067
5322
|
const rect = el("rect", {
|
|
5068
5323
|
x: along(run.start),
|
|
@@ -5089,8 +5344,8 @@ function walkRowsTree(bars, frame, o = {}) {
|
|
|
5089
5344
|
(k) => el("line", {
|
|
5090
5345
|
x1: along(k),
|
|
5091
5346
|
x2: along(k),
|
|
5092
|
-
y1: y -
|
|
5093
|
-
y2: y +
|
|
5347
|
+
y1: y - barPx / 2,
|
|
5348
|
+
y2: y + barPx / 2,
|
|
5094
5349
|
stroke: "white",
|
|
5095
5350
|
"stroke-width": 1
|
|
5096
5351
|
})
|
|
@@ -5103,6 +5358,7 @@ function walkRowsTree(bars, frame, o = {}) {
|
|
|
5103
5358
|
call
|
|
5104
5359
|
);
|
|
5105
5360
|
const at = readoutPlacement(text, along(row.bp), frame.width);
|
|
5361
|
+
const readouts = frame.readouts ?? true;
|
|
5106
5362
|
return el(
|
|
5107
5363
|
"g",
|
|
5108
5364
|
{
|
|
@@ -5116,12 +5372,12 @@ function walkRowsTree(bars, frame, o = {}) {
|
|
|
5116
5372
|
call && el("rect", {
|
|
5117
5373
|
"data-testid": "graph-walk-call",
|
|
5118
5374
|
x: along(call.bp) - 1,
|
|
5119
|
-
y: y -
|
|
5375
|
+
y: y - barPx / 2 - 3,
|
|
5120
5376
|
width: 2,
|
|
5121
|
-
height:
|
|
5377
|
+
height: barPx + 6,
|
|
5122
5378
|
fill: call.spanningReads === 0 ? UNBACKED_TICK : CALL_TICK
|
|
5123
5379
|
}),
|
|
5124
|
-
el(
|
|
5380
|
+
readouts && el(
|
|
5125
5381
|
"text",
|
|
5126
5382
|
{
|
|
5127
5383
|
x: at.x,
|
|
@@ -5254,7 +5510,7 @@ function svgCanvas(width, height) {
|
|
|
5254
5510
|
}
|
|
5255
5511
|
|
|
5256
5512
|
// src/version.ts
|
|
5257
|
-
var version = "4.
|
|
5513
|
+
var version = "4.3.0";
|
|
5258
5514
|
|
|
5259
5515
|
// src/viewport.ts
|
|
5260
5516
|
function axisScaleOf(scale, pixelRows) {
|
|
@@ -5482,6 +5738,7 @@ function figureSvg(graph, layout, o = {}) {
|
|
|
5482
5738
|
axis,
|
|
5483
5739
|
referenceRamp,
|
|
5484
5740
|
deletions: new Map(deletions.map((d) => [d.edgeIndex, d.bypassed])),
|
|
5741
|
+
deletionRoutes: layout.deletionRoutes,
|
|
5485
5742
|
hiddenEdges: new Set(
|
|
5486
5743
|
o.showDeletionEdges ? [] : deletions.map((d) => d.edgeIndex)
|
|
5487
5744
|
)
|
|
@@ -6189,9 +6446,17 @@ function loadBandage() {
|
|
|
6189
6446
|
export {
|
|
6190
6447
|
isBackbone,
|
|
6191
6448
|
BUBBLE_SPREADS,
|
|
6449
|
+
ROW_HEIGHT_PX,
|
|
6450
|
+
svgPath,
|
|
6451
|
+
curveBounds,
|
|
6452
|
+
translateCurves,
|
|
6453
|
+
yToXOf,
|
|
6454
|
+
pathRibbonOffsets,
|
|
6455
|
+
deletionEdges,
|
|
6192
6456
|
panSNSample,
|
|
6193
6457
|
panSNHaplotype,
|
|
6194
6458
|
panSNContig,
|
|
6459
|
+
pathOrigin,
|
|
6195
6460
|
trimOrigins,
|
|
6196
6461
|
loadGraph,
|
|
6197
6462
|
engineRequest,
|
|
@@ -6202,14 +6467,7 @@ export {
|
|
|
6202
6467
|
clampZoom,
|
|
6203
6468
|
fitTransform,
|
|
6204
6469
|
drawingBounds,
|
|
6205
|
-
ROW_HEIGHT_PX,
|
|
6206
6470
|
referenceOrder,
|
|
6207
|
-
svgPath,
|
|
6208
|
-
curveBounds,
|
|
6209
|
-
translateCurves,
|
|
6210
|
-
yToXOf,
|
|
6211
|
-
pathRibbonOffsets,
|
|
6212
|
-
deletionEdges,
|
|
6213
6471
|
LABEL_PX,
|
|
6214
6472
|
LABEL_CHAR_PX,
|
|
6215
6473
|
LABEL_PAD,
|
|
@@ -6283,6 +6541,7 @@ export {
|
|
|
6283
6541
|
walkRowsKey,
|
|
6284
6542
|
DISAGREES,
|
|
6285
6543
|
walkRowsKeyTree,
|
|
6544
|
+
coalesceRuns,
|
|
6286
6545
|
walkRowsTree,
|
|
6287
6546
|
svgCanvas,
|
|
6288
6547
|
version,
|