@jbrowse/bandage-core 4.2.0 → 4.4.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-FPBV6GNR.js → chunk-YNLMROBC.js} +1768 -1274
- package/dist/cli/figure.js +10 -4
- package/dist/index.js +33 -9
- package/dist/types/cli/figure.d.ts +2 -1
- package/dist/types/deletionEdges.d.ts +2 -2
- package/dist/types/figure.d.ts +6 -0
- 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 +10 -1
- package/dist/types/layout/walkStrip.d.ts +32 -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,341 +173,924 @@ 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
|
-
|
|
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
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (typeof selfLink === "string") {
|
|
472
|
+
return hairpinCurves(
|
|
473
|
+
attachment(fromSegments, selfLink),
|
|
474
|
+
selfLoopReach(fromSegments, scale),
|
|
475
|
+
offsetX,
|
|
476
|
+
offsetY
|
|
477
|
+
);
|
|
478
|
+
}
|
|
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
|
+
}
|
|
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
|
+
];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
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";
|
|
593
|
+
}
|
|
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]);
|
|
614
|
+
}
|
|
615
|
+
if (i === points.length - 1) {
|
|
616
|
+
const out = along(toAttach.inward, points[i - 1]);
|
|
617
|
+
return { x: -out.x, y: -out.y };
|
|
618
|
+
}
|
|
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
|
+
};
|
|
623
|
+
};
|
|
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
|
+
}
|
|
675
|
+
}
|
|
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) {
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
if (node.stable.start + node.length <= end) {
|
|
691
|
+
bypassed.push(node.id);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
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;
|
|
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) {
|
|
707
|
+
const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
|
|
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
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return found;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// src/pansn.ts
|
|
739
|
+
var SEP = "#";
|
|
740
|
+
function panSNSample(refName) {
|
|
741
|
+
return refName.split(SEP)[0];
|
|
742
|
+
}
|
|
743
|
+
var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
|
|
744
|
+
function isGenericSample(sample) {
|
|
745
|
+
return GENERIC_SAMPLES.has(sample);
|
|
746
|
+
}
|
|
747
|
+
function panSNHaplotype(refName) {
|
|
748
|
+
const parts = refName.split(SEP);
|
|
749
|
+
return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : void 0;
|
|
750
|
+
}
|
|
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;
|
|
754
|
+
}
|
|
755
|
+
function panSNMatchesPrefix(refName, prefix) {
|
|
756
|
+
return prefix !== void 0 && (refName === prefix || refName.startsWith(prefix + SEP));
|
|
757
|
+
}
|
|
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;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
function parseTags(fields) {
|
|
775
|
+
const tags = {};
|
|
776
|
+
for (const field of fields) {
|
|
777
|
+
parseTag(field, tags);
|
|
778
|
+
}
|
|
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
|
+
}
|
|
795
|
+
}
|
|
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;
|
|
802
|
+
}
|
|
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);
|
|
832
|
+
} else {
|
|
833
|
+
gfa1 = true;
|
|
834
|
+
seq = first;
|
|
835
|
+
len = seq === "*" ? 0 : seq.length;
|
|
836
|
+
tagfields = rest.slice(1);
|
|
837
|
+
}
|
|
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 });
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
return graph;
|
|
888
|
+
}
|
|
889
|
+
|
|
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 };
|
|
895
|
+
}
|
|
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]);
|
|
914
|
+
}
|
|
915
|
+
pos += lengthOf(step.id);
|
|
916
|
+
}
|
|
917
|
+
anchorPaths.push({
|
|
918
|
+
name: path.name,
|
|
919
|
+
sample,
|
|
920
|
+
start: path.start,
|
|
921
|
+
length: pos - path.start
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
return { anchorPaths, pathVisits };
|
|
925
|
+
}
|
|
926
|
+
function chooseReferencePath(anchorPaths, preferred) {
|
|
927
|
+
const named = anchorPaths.find(
|
|
928
|
+
(p) => p.sample === preferred || p.name === preferred
|
|
929
|
+
);
|
|
930
|
+
return named ?? anchorPaths[0];
|
|
931
|
+
}
|
|
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
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return anchored;
|
|
949
|
+
}
|
|
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;
|
|
968
|
+
}
|
|
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;
|
|
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]);
|
|
981
|
+
}
|
|
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
|
+
});
|
|
992
|
+
}
|
|
993
|
+
|
|
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
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function traverse(id, strand) {
|
|
1004
|
+
claim(id, strand);
|
|
1005
|
+
traversals.set(id, (traversals.get(id) ?? 0) + 1);
|
|
1006
|
+
}
|
|
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("-") ? "-" : "+");
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
for (const walk of gfaGraph.walks) {
|
|
1017
|
+
for (const seg of walk.segments) {
|
|
1018
|
+
traverse(seg.id, seg.strand === "-" ? "-" : "+");
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return { canonical, traversals };
|
|
1022
|
+
}
|
|
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
|
+
];
|
|
1042
|
+
}
|
|
1043
|
+
function carriedSamples(gfaNode) {
|
|
1044
|
+
const sm = gfaNode.tags.SM;
|
|
1045
|
+
if (typeof sm !== "string") {
|
|
1046
|
+
return void 0;
|
|
1047
|
+
}
|
|
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) {
|
|
1057
|
+
return {
|
|
1058
|
+
id: `${gfaNode.id}${strand}`,
|
|
1059
|
+
name: gfaNode.id,
|
|
1060
|
+
length: gfaNode.length,
|
|
1061
|
+
depth,
|
|
1062
|
+
stable: stableCoordinate(gfaNode),
|
|
1063
|
+
samples: carriedSamples(gfaNode)
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
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));
|
|
1074
|
+
}
|
|
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
|
+
});
|
|
1082
|
+
}
|
|
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
|
+
}
|
|
522
1094
|
}
|
|
523
1095
|
for (const gfaPath of gfaGraph.paths) {
|
|
524
1096
|
const nodeIds = gfaPath.path.split(",").map((segment) => nodeId(segment.slice(0, -1)));
|
|
@@ -559,991 +1131,603 @@ function convertGFAToGraph(gfaGraph, name = "Imported GFA") {
|
|
|
559
1131
|
pathVisits: anchoring?.pathVisits,
|
|
560
1132
|
// rGFA's own tags win wherever they are present; pathAnchoring only ever
|
|
561
1133
|
// 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
|
-
}
|
|
573
|
-
function sidesOf(edge) {
|
|
574
|
-
const from = (edge.fromStrand ?? ownStrand(edge.from)) === ownStrand(edge.from) ? "end" : "start";
|
|
575
|
-
const to = (edge.toStrand ?? ownStrand(edge.to)) === ownStrand(edge.to) ? "start" : "end";
|
|
576
|
-
return { from, to };
|
|
577
|
-
}
|
|
578
|
-
function mergeRuns(graph) {
|
|
579
|
-
const sides = graph.edges.map(sidesOf);
|
|
580
|
-
const at = /* @__PURE__ */ new Map();
|
|
581
|
-
const attach = (id, side, ei) => {
|
|
582
|
-
const key = `${id}|${side}`;
|
|
583
|
-
const list = at.get(key);
|
|
584
|
-
if (list) {
|
|
585
|
-
list.push(ei);
|
|
586
|
-
} else {
|
|
587
|
-
at.set(key, [ei]);
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
graph.edges.forEach((edge, ei) => {
|
|
591
|
-
attach(edge.from, sides[ei].from, ei);
|
|
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;
|
|
598
|
-
}
|
|
599
|
-
const ei = out[0];
|
|
600
|
-
const edge = graph.edges[ei];
|
|
601
|
-
return edge.from === a && edge.to !== a && sides[ei].from === "end" && sides[ei].to === "start" && at.get(`${edge.to}|start`)?.length === 1 ? ei : void 0;
|
|
602
|
-
};
|
|
603
|
-
const joined = /* @__PURE__ */ new Set();
|
|
604
|
-
const runOf = /* @__PURE__ */ new Map();
|
|
605
|
-
const runs = /* @__PURE__ */ new Map();
|
|
606
|
-
for (const node of graph.nodes) {
|
|
607
|
-
const into = at.get(`${node.id}|start`);
|
|
608
|
-
const pred = into?.length === 1 ? graph.edges[into[0]].from : void 0;
|
|
609
|
-
if (runOf.has(node.id) || pred !== void 0 && joinAfter(pred) === into[0]) {
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
const run = [node.id];
|
|
613
|
-
for (; ; ) {
|
|
614
|
-
const ei = joinAfter(run.at(-1));
|
|
615
|
-
const next = ei === void 0 ? void 0 : graph.edges[ei].to;
|
|
616
|
-
if (next === void 0 || runOf.has(next)) {
|
|
617
|
-
break;
|
|
618
|
-
}
|
|
619
|
-
joined.add(ei);
|
|
620
|
-
run.push(next);
|
|
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);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
for (const node of graph.nodes) {
|
|
630
|
-
if (!runOf.has(node.id)) {
|
|
631
|
-
runOf.set(node.id, node.id);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
|
|
635
|
-
const nodes = [];
|
|
636
|
-
for (const node of graph.nodes) {
|
|
637
|
-
if (runOf.get(node.id) !== node.id) {
|
|
638
|
-
continue;
|
|
639
|
-
}
|
|
640
|
-
const run = runs.get(node.id);
|
|
641
|
-
if (!run) {
|
|
642
|
-
nodes.push(node);
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
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
|
-
}
|
|
656
|
-
const seen = /* @__PURE__ */ new Set();
|
|
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
|
-
};
|
|
678
|
-
}
|
|
679
|
-
function splitRuns(positions, runs, weightOf) {
|
|
680
|
-
const out = { ...positions };
|
|
681
|
-
for (const [id, members] of runs) {
|
|
682
|
-
const line = positions[id];
|
|
683
|
-
if (!line?.length) {
|
|
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;
|
|
700
|
-
}
|
|
701
|
-
function polylineSlice(line, from, to) {
|
|
702
|
-
if (line.length === 0) {
|
|
703
|
-
return [];
|
|
704
|
-
}
|
|
705
|
-
const arc = arcPrefix(line);
|
|
706
|
-
const total = arc.at(-1);
|
|
707
|
-
return slice(line, arc, from * total, to * total);
|
|
708
|
-
}
|
|
709
|
-
function polylineMidpoint(line) {
|
|
710
|
-
const arc = arcPrefix(line);
|
|
711
|
-
return pointAt(line, arc, arc.at(-1) / 2);
|
|
712
|
-
}
|
|
713
|
-
function arcPrefix(line) {
|
|
714
|
-
const arc = [0];
|
|
715
|
-
for (let i = 1; i < line.length; i++) {
|
|
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;
|
|
721
|
-
}
|
|
722
|
-
function pointAt(line, arc, s) {
|
|
723
|
-
if (s <= 0) {
|
|
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 };
|
|
738
|
-
}
|
|
739
|
-
function slice(line, arc, from, to) {
|
|
740
|
-
const points = [pointAt(line, arc, from)];
|
|
741
|
-
for (let i = 0; i < line.length; i++) {
|
|
742
|
-
if (arc[i] > from && arc[i] < to) {
|
|
743
|
-
points.push({ ...line[i] });
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
points.push(pointAt(line, arc, to));
|
|
747
|
-
return points;
|
|
1134
|
+
anchoredBy: nodes.some((n2) => n2.stable) ? "tags" : void 0
|
|
1135
|
+
};
|
|
748
1136
|
}
|
|
749
1137
|
|
|
750
|
-
// src/layout/
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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}`])
|
|
754
1144
|
);
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
+
});
|
|
1158
|
+
return {
|
|
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())
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
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);
|
|
761
1188
|
return {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1189
|
+
id,
|
|
1190
|
+
name: id,
|
|
1191
|
+
length: Math.round(drawn * 1e6 / opts.nodeLengthPerMegabase),
|
|
1192
|
+
depth: 0
|
|
766
1193
|
};
|
|
767
1194
|
});
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
});
|
|
788
|
-
}
|
|
789
|
-
let below = 0;
|
|
790
|
-
let above = 0;
|
|
791
|
-
for (const node of graph.nodes) {
|
|
792
|
-
if (isBackbone(node)) {
|
|
793
|
-
continue;
|
|
794
|
-
}
|
|
795
|
-
for (const p of out[node.id] ?? []) {
|
|
796
|
-
if (p.y > cy) {
|
|
797
|
-
below++;
|
|
798
|
-
} else {
|
|
799
|
-
above++;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
if (above > below) {
|
|
804
|
-
for (const segments of Object.values(out)) {
|
|
805
|
-
for (const p of segments) {
|
|
806
|
-
p.y = 2 * cy - p.y;
|
|
807
|
-
}
|
|
1195
|
+
}
|
|
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;
|
|
808
1204
|
}
|
|
809
1205
|
}
|
|
810
|
-
return
|
|
1206
|
+
return { nodePositions, deletionRoutes };
|
|
811
1207
|
}
|
|
812
1208
|
|
|
813
|
-
// src/
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1209
|
+
// src/layout/mergeRuns.ts
|
|
1210
|
+
function ownStrand(id) {
|
|
1211
|
+
return id.endsWith("-") ? "-" : "+";
|
|
1212
|
+
}
|
|
1213
|
+
function flip(strand) {
|
|
1214
|
+
return strand === "+" ? "-" : "+";
|
|
1215
|
+
}
|
|
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 };
|
|
1220
|
+
}
|
|
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);
|
|
819
1227
|
if (list) {
|
|
820
|
-
list.push(
|
|
1228
|
+
list.push(ei);
|
|
821
1229
|
} else {
|
|
822
|
-
|
|
1230
|
+
at.set(key, [ei]);
|
|
823
1231
|
}
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
nodeById,
|
|
834
|
-
neighbors
|
|
835
|
-
}) {
|
|
836
|
-
const seen = /* @__PURE__ */ new Set([nodeId]);
|
|
837
|
-
let frontier = [nodeId];
|
|
838
|
-
let minEnd = Infinity;
|
|
839
|
-
let maxStart = -Infinity;
|
|
840
|
-
for (let hop = 0; hop < MAX_BACKBONE_HOPS && frontier.length > 0; hop++) {
|
|
841
|
-
const next = [];
|
|
842
|
-
for (const id of frontier) {
|
|
843
|
-
const adjacent = neighbors.get(id);
|
|
844
|
-
if (adjacent) {
|
|
845
|
-
for (const neighborId of adjacent) {
|
|
846
|
-
if (!seen.has(neighborId)) {
|
|
847
|
-
seen.add(neighborId);
|
|
848
|
-
const neighbor = nodeById.get(neighborId);
|
|
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
|
-
}
|
|
857
|
-
}
|
|
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;
|
|
858
1241
|
}
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
start: Math.min(minEnd, maxStart),
|
|
863
|
-
end: Math.max(minEnd, maxStart)
|
|
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;
|
|
864
1245
|
};
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
nodeById,
|
|
869
|
-
neighbors
|
|
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 });
|
|
873
|
-
}
|
|
874
|
-
function referenceMidpoints(graph) {
|
|
875
|
-
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
876
|
-
const neighbors = buildNeighbors(graph);
|
|
877
|
-
const midpoints = /* @__PURE__ */ new Map();
|
|
1246
|
+
const joined = /* @__PURE__ */ new Set();
|
|
1247
|
+
const runOf = /* @__PURE__ */ new Map();
|
|
1248
|
+
const runs = /* @__PURE__ */ new Map();
|
|
878
1249
|
for (const node of graph.nodes) {
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
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;
|
|
882
1254
|
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
function referenceSeeds(graph, scaling) {
|
|
890
|
-
const { opts } = scaling;
|
|
891
|
-
const drawn = new Map(
|
|
892
|
-
scaling.nodes.map((node) => [node.id, drawnNodeLength(opts, node.length)])
|
|
893
|
-
);
|
|
894
|
-
const drawnOf = (id) => drawn.get(id) ?? opts.minimumNodeLength;
|
|
895
|
-
const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
|
|
896
|
-
const seeds = /* @__PURE__ */ new Map();
|
|
897
|
-
let x = 0;
|
|
898
|
-
for (const node of backbone) {
|
|
899
|
-
seeds.set(node.id, { x, y: 0 });
|
|
900
|
-
x += drawnOf(node.id) + opts.edgeLength;
|
|
901
|
-
}
|
|
902
|
-
const adjacent = buildNeighbors(graph);
|
|
903
|
-
const depth = new Map(backbone.map((node) => [node.id, 0]));
|
|
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;
|
|
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;
|
|
911
1261
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
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);
|
|
915
1270
|
}
|
|
916
1271
|
}
|
|
917
|
-
let unreached = 0;
|
|
918
1272
|
for (const node of graph.nodes) {
|
|
919
|
-
if (!
|
|
920
|
-
|
|
921
|
-
x: unreached++ * x / graph.nodes.length,
|
|
922
|
-
y: LANE_GAP * 3
|
|
923
|
-
});
|
|
1273
|
+
if (!runOf.has(node.id)) {
|
|
1274
|
+
runOf.set(node.id, node.id);
|
|
924
1275
|
}
|
|
925
1276
|
}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
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
|
+
});
|
|
946
1298
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
options: {
|
|
957
|
-
quality: settings.quality,
|
|
958
|
-
linearLayout: settings.linearLayout,
|
|
959
|
-
...scaling.opts,
|
|
960
|
-
...anchored ? { rotateComponents: false } : {}
|
|
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;
|
|
961
1308
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
|
|
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
|
-
);
|
|
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
|
+
});
|
|
983
1317
|
return {
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
nodePositions: graph.nodes.some(isBackbone) ? orientToReference(graph, positions) : positions
|
|
987
|
-
},
|
|
988
|
-
duration
|
|
1318
|
+
graph: { ...graph, nodes, edges, paths: void 0, pathVisits: void 0 },
|
|
1319
|
+
runs
|
|
989
1320
|
};
|
|
990
1321
|
}
|
|
991
|
-
function
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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);
|
|
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;
|
|
1002
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
|
+
});
|
|
1003
1341
|
}
|
|
1004
|
-
return
|
|
1342
|
+
return out;
|
|
1005
1343
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
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);
|
|
1011
1351
|
}
|
|
1012
|
-
function
|
|
1013
|
-
const
|
|
1014
|
-
return
|
|
1352
|
+
function polylineMidpoint(line) {
|
|
1353
|
+
const arc = arcPrefix(line);
|
|
1354
|
+
return pointAt(line, arc, arc.at(-1) / 2);
|
|
1015
1355
|
}
|
|
1016
|
-
function
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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)) {
|
|
1026
|
-
return void 0;
|
|
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)
|
|
1361
|
+
);
|
|
1027
1362
|
}
|
|
1028
|
-
|
|
1029
|
-
const fitY = bounds.h > 0 ? usableHeight / bounds.h : Infinity;
|
|
1030
|
-
const scale = clampZoom(
|
|
1031
|
-
Math.max(minScale, pixelRows ? fitX : Math.min(fitX, fitY))
|
|
1032
|
-
);
|
|
1033
|
-
const leftoverX = usableWidth - bounds.w * scale;
|
|
1034
|
-
return {
|
|
1035
|
-
scale,
|
|
1036
|
-
translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
|
|
1037
|
-
translateY: fittedTranslateY(
|
|
1038
|
-
bounds,
|
|
1039
|
-
usableHeight,
|
|
1040
|
-
pixelRows ? 1 : scale,
|
|
1041
|
-
padTop
|
|
1042
|
-
)
|
|
1043
|
-
};
|
|
1363
|
+
return arc;
|
|
1044
1364
|
}
|
|
1045
|
-
function
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (layout.referenceAxis && region && region.end > region.start) {
|
|
1049
|
-
minX = region.start;
|
|
1050
|
-
maxX = region.end;
|
|
1051
|
-
}
|
|
1052
|
-
const extent = opts.extent ?? layout.extent;
|
|
1053
|
-
if (extent) {
|
|
1054
|
-
minX = Math.min(minX, extent.minX ?? minX);
|
|
1055
|
-
minY = Math.min(minY, extent.minY ?? minY);
|
|
1056
|
-
maxX = Math.max(maxX, extent.maxX ?? maxX);
|
|
1057
|
-
maxY = Math.max(maxY, extent.maxY ?? maxY);
|
|
1365
|
+
function pointAt(line, arc, s) {
|
|
1366
|
+
if (s <= 0) {
|
|
1367
|
+
return { ...line[0] };
|
|
1058
1368
|
}
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
// src/layout/rowSpacing.ts
|
|
1063
|
-
var ROW_HEIGHT_PX = 20;
|
|
1064
|
-
|
|
1065
|
-
// src/util/geometry.ts
|
|
1066
|
-
function svgPath(points) {
|
|
1067
|
-
const round = (v) => Math.round(v * 100) / 100;
|
|
1068
|
-
return points.map((p, i) => `${i ? "L" : "M"}${round(p.x)},${round(p.y)}`).join("");
|
|
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];
|
|
1369
|
+
let i = 1;
|
|
1370
|
+
while (i < arc.length && arc[i] < s) {
|
|
1371
|
+
i++;
|
|
1074
1372
|
}
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
return [x2 + distance * vx, y2 + distance * vy];
|
|
1078
|
-
}
|
|
1079
|
-
function curvePointAt(c, t) {
|
|
1080
|
-
const u = 1 - t;
|
|
1081
|
-
const a = u * u * u;
|
|
1082
|
-
const b = 3 * u * u * t;
|
|
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);
|
|
1373
|
+
if (i >= arc.length) {
|
|
1374
|
+
return { ...line.at(-1) };
|
|
1104
1375
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
const
|
|
1109
|
-
|
|
1110
|
-
const dt = (t1 - t0) / 3;
|
|
1111
|
-
const d0 = curveTangentAt(c, t0);
|
|
1112
|
-
const d1 = curveTangentAt(c, t1);
|
|
1113
|
-
return {
|
|
1114
|
-
x0: p0.x,
|
|
1115
|
-
y0: p0.y,
|
|
1116
|
-
cx0: p0.x + d0.x * dt,
|
|
1117
|
-
cy0: p0.y + d0.y * dt,
|
|
1118
|
-
cx1: p1.x - d1.x * dt,
|
|
1119
|
-
cy1: p1.y - d1.y * dt,
|
|
1120
|
-
x1: p1.x,
|
|
1121
|
-
y1: p1.y
|
|
1122
|
-
};
|
|
1123
|
-
}
|
|
1124
|
-
function curveTangentAt(c, t) {
|
|
1125
|
-
const u = 1 - t;
|
|
1126
|
-
const a = 3 * u * u;
|
|
1127
|
-
const b = 6 * u * t;
|
|
1128
|
-
const d = 3 * t * t;
|
|
1129
|
-
return {
|
|
1130
|
-
x: a * (c.cx0 - c.x0) + b * (c.cx1 - c.cx0) + d * (c.x1 - c.cx1),
|
|
1131
|
-
y: a * (c.cy0 - c.y0) + b * (c.cy1 - c.cy0) + d * (c.y1 - c.cy1)
|
|
1132
|
-
};
|
|
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 };
|
|
1133
1381
|
}
|
|
1134
|
-
function
|
|
1135
|
-
const
|
|
1136
|
-
for (
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
for (let i = 0; i < spans; i += 2) {
|
|
1140
|
-
out.push([subCurve(c, i / spans, (i + 1) / spans)]);
|
|
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] });
|
|
1141
1387
|
}
|
|
1142
1388
|
}
|
|
1143
|
-
|
|
1389
|
+
points.push(pointAt(line, arc, to));
|
|
1390
|
+
return points;
|
|
1144
1391
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
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;
|
|
1400
|
+
}
|
|
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
|
|
1158
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);
|
|
1420
|
+
}
|
|
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++;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
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;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1159
1452
|
}
|
|
1160
1453
|
return out;
|
|
1161
1454
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
+
}
|
|
1174
1467
|
}
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
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
|
-
};
|
|
1468
|
+
for (const edge of graph.edges) {
|
|
1469
|
+
add(edge.from, edge.to);
|
|
1470
|
+
add(edge.to, edge.from);
|
|
1471
|
+
}
|
|
1472
|
+
return neighbors;
|
|
1190
1473
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
let
|
|
1200
|
-
let
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
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
|
+
}
|
|
1208
1501
|
}
|
|
1209
|
-
|
|
1210
|
-
minAlong = Math.min(minAlong, along);
|
|
1211
|
-
maxAlong = Math.max(maxAlong, along);
|
|
1502
|
+
frontier = next;
|
|
1212
1503
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
Math.
|
|
1216
|
-
|
|
1217
|
-
);
|
|
1218
|
-
const size = maxBow === void 0 ? uncapped : Math.min(uncapped, maxBow);
|
|
1219
|
-
return reach < 0 ? -size : size;
|
|
1504
|
+
return minEnd === Infinity ? void 0 : {
|
|
1505
|
+
start: Math.min(minEnd, maxStart),
|
|
1506
|
+
end: Math.max(minEnd, maxStart)
|
|
1507
|
+
};
|
|
1220
1508
|
}
|
|
1221
|
-
function
|
|
1222
|
-
|
|
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 });
|
|
1223
1516
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
const
|
|
1227
|
-
const
|
|
1228
|
-
const
|
|
1229
|
-
|
|
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;
|
|
1230
1528
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
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;
|
|
1242
1544
|
}
|
|
1243
|
-
|
|
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;
|
|
1554
|
+
}
|
|
1555
|
+
depth.set(neighbour, next);
|
|
1556
|
+
seeds.set(neighbour, { x: at.x + drawnOf(id) / 2, y: LANE_GAP * next });
|
|
1557
|
+
queue.push(neighbour);
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
let unreached = 0;
|
|
1561
|
+
for (const node of graph.nodes) {
|
|
1562
|
+
if (!seeds.has(node.id)) {
|
|
1563
|
+
seeds.set(node.id, {
|
|
1564
|
+
x: unreached++ * x / graph.nodes.length,
|
|
1565
|
+
y: LANE_GAP * 3
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return seeds;
|
|
1244
1570
|
}
|
|
1245
|
-
function
|
|
1246
|
-
|
|
1571
|
+
function seededNodes(graph, scaling) {
|
|
1572
|
+
const seeds = referenceSeeds(graph, scaling);
|
|
1573
|
+
return scaling.nodes.map((node) => ({ ...node, ...seeds.get(node.id) }));
|
|
1247
1574
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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}`);
|
|
1254
1584
|
}
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
var SELF_LOOP_MAX_PX = 40;
|
|
1259
|
-
function selfLoopReach(segments, scale) {
|
|
1260
|
-
let length = 0;
|
|
1261
|
-
for (let i = 1; i < segments.length; i++) {
|
|
1262
|
-
length += Math.hypot(
|
|
1263
|
-
segments[i].x - segments[i - 1].x,
|
|
1264
|
-
segments[i].y - segments[i - 1].y
|
|
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.`
|
|
1265
1588
|
);
|
|
1266
1589
|
}
|
|
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);
|
|
1273
|
-
}
|
|
1274
|
-
function selfLinkOf(edge) {
|
|
1275
|
-
if (edge.from !== edge.to) {
|
|
1276
|
-
return false;
|
|
1277
|
-
}
|
|
1278
|
-
return edge.fromStrand && edge.toStrand && edge.fromStrand !== edge.toStrand ? edge.fromStrand === "+" ? "end" : "start" : true;
|
|
1590
|
+
return graph;
|
|
1279
1591
|
}
|
|
1280
|
-
function
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
|
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
|
|
1305
1598
|
},
|
|
1306
|
-
{
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
cx1: nearX,
|
|
1312
|
-
cy1: nearY,
|
|
1313
|
-
x1: px,
|
|
1314
|
-
y1: py
|
|
1599
|
+
options: {
|
|
1600
|
+
quality: settings.quality,
|
|
1601
|
+
linearLayout: settings.linearLayout,
|
|
1602
|
+
...scaling.opts,
|
|
1603
|
+
...anchored ? { rotateComponents: false } : {}
|
|
1315
1604
|
}
|
|
1316
|
-
|
|
1605
|
+
};
|
|
1317
1606
|
}
|
|
1318
|
-
function
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
),
|
|
1336
|
-
yToX
|
|
1337
|
-
);
|
|
1338
|
-
}
|
|
1339
|
-
if (typeof selfLink === "string") {
|
|
1340
|
-
return hairpinCurves(
|
|
1341
|
-
attachment(fromSegments, selfLink),
|
|
1342
|
-
selfLoopReach(fromSegments, scale),
|
|
1343
|
-
offsetX,
|
|
1344
|
-
offsetY
|
|
1345
|
-
);
|
|
1346
|
-
}
|
|
1347
|
-
const sides = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
|
|
1348
|
-
const fromAttach = attachment(fromSegments, sides.from);
|
|
1349
|
-
const toAttach = attachment(toSegments, sides.to);
|
|
1350
|
-
const fromEnd = fromAttach.at;
|
|
1351
|
-
const toStart = toAttach.at;
|
|
1352
|
-
const p1x = fromEnd.x + offsetX;
|
|
1353
|
-
const p1y = fromEnd.y + offsetY;
|
|
1354
|
-
const p2x = toStart.x + offsetX;
|
|
1355
|
-
const p2y = toStart.y + offsetY;
|
|
1356
|
-
if (selfLink) {
|
|
1357
|
-
let segDirX = 1;
|
|
1358
|
-
let segDirY = 0;
|
|
1359
|
-
if (fromAttach.inward) {
|
|
1360
|
-
const dx = fromEnd.x - fromAttach.inward.x;
|
|
1361
|
-
const dy = fromEnd.y - fromAttach.inward.y;
|
|
1362
|
-
const len = Math.hypot(dx, dy);
|
|
1363
|
-
if (len > 0) {
|
|
1364
|
-
segDirX = dx / len;
|
|
1365
|
-
segDirY = dy / len;
|
|
1366
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
const ext = selfLoopReach(fromSegments, scale);
|
|
1369
|
-
const perpX = -segDirY;
|
|
1370
|
-
const perpY = segDirX;
|
|
1371
|
-
const midX = (fromEnd.x + toStart.x) / 2 + offsetX + perpX * ext;
|
|
1372
|
-
const midY = (fromEnd.y + toStart.y) / 2 + offsetY + perpY * ext;
|
|
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 [
|
|
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,
|
|
1378
1624
|
{
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
+
]
|
|
1387
1636
|
},
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
const len = Math.hypot(dx, dy);
|
|
1413
|
-
const along = len === 0 ? 0 : (towardX * dx + towardY * dy) / len;
|
|
1414
|
-
return Math.max(
|
|
1415
|
-
0,
|
|
1416
|
-
Math.min(dist * TANGENT_SPAN_FACTOR, MAX_TANGENT, along * 0.5)
|
|
1417
|
-
);
|
|
1418
|
-
};
|
|
1419
|
-
const towardToX = toStart.x - fromEnd.x;
|
|
1420
|
-
const towardToY = toStart.y - fromEnd.y;
|
|
1421
|
-
const [cx1, cy1] = projectLine(
|
|
1422
|
-
fromPrev.x,
|
|
1423
|
-
fromPrev.y,
|
|
1424
|
-
fromEnd.x,
|
|
1425
|
-
fromEnd.y,
|
|
1426
|
-
tangentProj(fromPrev, fromEnd, towardToX, towardToY)
|
|
1427
|
-
);
|
|
1428
|
-
const [cx2, cy2] = projectLine(
|
|
1429
|
-
toNext.x,
|
|
1430
|
-
toNext.y,
|
|
1431
|
-
toStart.x,
|
|
1432
|
-
toStart.y,
|
|
1433
|
-
tangentProj(toNext, toStart, -towardToX, -towardToY)
|
|
1434
|
-
);
|
|
1435
|
-
const bulge = bypassed.length ? bowAround(p1x, p1y, p2x, p2y, bypassed, maxBow) : 0;
|
|
1436
|
-
const chordLen = dist === 0 ? 1 : dist;
|
|
1437
|
-
const ux = (p2x - p1x) / chordLen;
|
|
1438
|
-
const uy = (p2y - p1y) / chordLen;
|
|
1439
|
-
const bulgeX = -uy * bulge;
|
|
1440
|
-
const bulgeY = ux * bulge;
|
|
1441
|
-
const spreadX = ux * Math.abs(bulge);
|
|
1442
|
-
const spreadY = uy * Math.abs(bulge);
|
|
1443
|
-
return [
|
|
1444
|
-
{
|
|
1445
|
-
x0: p1x,
|
|
1446
|
-
y0: p1y,
|
|
1447
|
-
cx0: cx1 + offsetX + bulgeX - spreadX,
|
|
1448
|
-
cy0: cy1 + offsetY + bulgeY - spreadY,
|
|
1449
|
-
cx1: cx2 + offsetX + bulgeX + spreadX,
|
|
1450
|
-
cy1: cy2 + offsetY + bulgeY + spreadY,
|
|
1451
|
-
x1: p2x,
|
|
1452
|
-
y1: p2y
|
|
1453
|
-
}
|
|
1454
|
-
];
|
|
1455
|
-
}
|
|
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;
|
|
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
|
+
};
|
|
1475
1661
|
}
|
|
1476
|
-
function
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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);
|
|
1486
1673
|
}
|
|
1487
1674
|
}
|
|
1488
|
-
|
|
1489
|
-
list.sort((a, b) => a.stable.start - b.stable.start);
|
|
1490
|
-
}
|
|
1491
|
-
return bySequence;
|
|
1675
|
+
return { minX, minY, maxX, maxY };
|
|
1492
1676
|
}
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
if (node.stable.start >= end) {
|
|
1499
|
-
break;
|
|
1500
|
-
}
|
|
1501
|
-
if (node.stable.start + node.length <= end) {
|
|
1502
|
-
bypassed.push(node.id);
|
|
1503
|
-
}
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
return bypassed;
|
|
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));
|
|
1507
1682
|
}
|
|
1508
|
-
function
|
|
1509
|
-
const
|
|
1510
|
-
|
|
1511
|
-
|
|
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,
|
|
@@ -4872,7 +5075,16 @@ function placeRowGenes(rows, byRow) {
|
|
|
4872
5075
|
}
|
|
4873
5076
|
var GENE_FONT_PX = 9;
|
|
4874
5077
|
var GENE_CHAR_PX = 5.2;
|
|
4875
|
-
|
|
5078
|
+
var MIN_LETTERED_BAR_PX = 8;
|
|
5079
|
+
var genePad = (rowPx, barPx) => Math.min(2, (rowPx - barPx) / 2 - 1);
|
|
5080
|
+
function boxesGenes(rowPx, barPx) {
|
|
5081
|
+
return genePad(rowPx, barPx) >= 1;
|
|
5082
|
+
}
|
|
5083
|
+
function rowGeneBoxes(genes, X, y, { rowPx = ROW_HEIGHT_PX, barPx = BAR_PX } = {}) {
|
|
5084
|
+
if (!boxesGenes(rowPx, barPx)) {
|
|
5085
|
+
return [];
|
|
5086
|
+
}
|
|
5087
|
+
const pad = genePad(rowPx, barPx);
|
|
4876
5088
|
const boxes = genes.map((g) => {
|
|
4877
5089
|
const x = X(g.start);
|
|
4878
5090
|
return {
|
|
@@ -4883,7 +5095,7 @@ function rowGeneBoxes(genes, X, y) {
|
|
|
4883
5095
|
});
|
|
4884
5096
|
const taken = [];
|
|
4885
5097
|
const named = new Set(
|
|
4886
|
-
[...boxes].sort((a, b) => a.w - b.w).filter(({ g, x, w }) => {
|
|
5098
|
+
(barPx >= MIN_LETTERED_BAR_PX ? [...boxes] : []).sort((a, b) => a.w - b.w).filter(({ g, x, w }) => {
|
|
4887
5099
|
const half = g.name.length * GENE_CHAR_PX / 2;
|
|
4888
5100
|
const at = [x + w / 2 - half, x + w / 2 + half];
|
|
4889
5101
|
if (2 * half > w + 6 || taken.some(([a, b]) => at[0] < b + 2 && at[1] > a - 2)) {
|
|
@@ -4896,9 +5108,9 @@ function rowGeneBoxes(genes, X, y) {
|
|
|
4896
5108
|
return boxes.map((box) => ({
|
|
4897
5109
|
name: box.g.name,
|
|
4898
5110
|
x: box.x,
|
|
4899
|
-
y: y -
|
|
5111
|
+
y: y - barPx / 2 - pad,
|
|
4900
5112
|
w: box.w,
|
|
4901
|
-
h:
|
|
5113
|
+
h: barPx + 2 * pad,
|
|
4902
5114
|
exons: box.g.exons.map((e) => ({
|
|
4903
5115
|
x: X(e.start),
|
|
4904
5116
|
w: Math.max(1, X(e.end) - X(e.start))
|
|
@@ -4930,7 +5142,13 @@ function walkRowsKey(bars, o = {}) {
|
|
|
4930
5142
|
label: "walked outside the cut"
|
|
4931
5143
|
});
|
|
4932
5144
|
}
|
|
4933
|
-
if (o.genes) {
|
|
5145
|
+
if (o.genes?.crowded) {
|
|
5146
|
+
entries.push({
|
|
5147
|
+
swatch: { kind: "gene" },
|
|
5148
|
+
label: "genes left out: too many rows to box them in",
|
|
5149
|
+
note: true
|
|
5150
|
+
});
|
|
5151
|
+
} else if (o.genes) {
|
|
4934
5152
|
entries.push({
|
|
4935
5153
|
swatch: { kind: "gene" },
|
|
4936
5154
|
label: "genes, each row's own annotation"
|
|
@@ -5007,7 +5225,57 @@ function walkRowsKeyTree(entries, x, y) {
|
|
|
5007
5225
|
)
|
|
5008
5226
|
};
|
|
5009
5227
|
}
|
|
5010
|
-
function
|
|
5228
|
+
function coalesceRuns(runs, pxPerBp) {
|
|
5229
|
+
const minBp = 1 / pxPerBp;
|
|
5230
|
+
const out = [];
|
|
5231
|
+
let bucket = [];
|
|
5232
|
+
const flush = () => {
|
|
5233
|
+
if (bucket.length === 0) {
|
|
5234
|
+
return;
|
|
5235
|
+
}
|
|
5236
|
+
if (bucket.length === 1) {
|
|
5237
|
+
out.push(bucket[0]);
|
|
5238
|
+
} else {
|
|
5239
|
+
let on = 0;
|
|
5240
|
+
let off = 0;
|
|
5241
|
+
for (const r of bucket) {
|
|
5242
|
+
if (r.onReference) {
|
|
5243
|
+
on += r.bp;
|
|
5244
|
+
} else {
|
|
5245
|
+
off += r.bp;
|
|
5246
|
+
}
|
|
5247
|
+
}
|
|
5248
|
+
const onReference = on >= off;
|
|
5249
|
+
const lead = bucket.find((r) => r.onReference === onReference);
|
|
5250
|
+
out.push({
|
|
5251
|
+
start: bucket[0].start,
|
|
5252
|
+
bp: on + off,
|
|
5253
|
+
onReference,
|
|
5254
|
+
referenceStart: onReference ? lead.referenceStart : void 0,
|
|
5255
|
+
...lead.reversed ? { reversed: true } : {}
|
|
5256
|
+
});
|
|
5257
|
+
}
|
|
5258
|
+
bucket = [];
|
|
5259
|
+
};
|
|
5260
|
+
let held = 0;
|
|
5261
|
+
for (const run of runs) {
|
|
5262
|
+
if (run.gap || run.bp >= minBp) {
|
|
5263
|
+
flush();
|
|
5264
|
+
held = 0;
|
|
5265
|
+
out.push(run);
|
|
5266
|
+
continue;
|
|
5267
|
+
}
|
|
5268
|
+
bucket.push(run);
|
|
5269
|
+
held += run.bp;
|
|
5270
|
+
if (held >= minBp) {
|
|
5271
|
+
flush();
|
|
5272
|
+
held = 0;
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
flush();
|
|
5276
|
+
return out;
|
|
5277
|
+
}
|
|
5278
|
+
function geneTree(box, y, barPx) {
|
|
5011
5279
|
return el(
|
|
5012
5280
|
"g",
|
|
5013
5281
|
{ class: "row-gene", "data-testid": "graph-walk-gene" },
|
|
@@ -5015,9 +5283,9 @@ function geneTree(box, y) {
|
|
|
5015
5283
|
...box.exons.map(
|
|
5016
5284
|
(e) => el("rect", {
|
|
5017
5285
|
x: e.x,
|
|
5018
|
-
y: y -
|
|
5286
|
+
y: y - barPx / 2,
|
|
5019
5287
|
width: e.w,
|
|
5020
|
-
height:
|
|
5288
|
+
height: barPx,
|
|
5021
5289
|
fill: GENE_INK,
|
|
5022
5290
|
opacity: 0.35
|
|
5023
5291
|
})
|
|
@@ -5051,18 +5319,20 @@ function geneTree(box, y) {
|
|
|
5051
5319
|
}
|
|
5052
5320
|
function walkRowsTree(bars, frame, o = {}) {
|
|
5053
5321
|
const X = (bp) => bp * frame.scaleX + frame.translateX;
|
|
5054
|
-
const
|
|
5322
|
+
const rowPx = frame.rowPx ?? ROW_HEIGHT_PX;
|
|
5323
|
+
const barPx = frame.barPx ?? BAR_PX;
|
|
5324
|
+
const Y = (row) => row * rowPx * frame.scaleY + frame.translateY;
|
|
5055
5325
|
const { origin, unit, reference, rows } = bars;
|
|
5056
5326
|
const along = (offset) => X(origin + offset);
|
|
5057
5327
|
const prefix = o.idPrefix ?? "walkrow";
|
|
5058
5328
|
const rowTree = (row, i) => {
|
|
5059
5329
|
const y = Y(i);
|
|
5060
|
-
if (y < -
|
|
5330
|
+
if (y < -barPx || y > frame.height + barPx) {
|
|
5061
5331
|
return void 0;
|
|
5062
5332
|
}
|
|
5063
|
-
const runs = row.runs.flatMap((run) => {
|
|
5333
|
+
const runs = coalesceRuns(row.runs, frame.scaleX).flatMap((run) => {
|
|
5064
5334
|
const paint2 = runPaint(run, o.ramp);
|
|
5065
|
-
const h = run.gap ? GAP_PX :
|
|
5335
|
+
const h = run.gap ? Math.min(GAP_PX, barPx) : barPx;
|
|
5066
5336
|
const id = `${prefix}-${i}-${run.start}`;
|
|
5067
5337
|
const rect = el("rect", {
|
|
5068
5338
|
x: along(run.start),
|
|
@@ -5089,8 +5359,8 @@ function walkRowsTree(bars, frame, o = {}) {
|
|
|
5089
5359
|
(k) => el("line", {
|
|
5090
5360
|
x1: along(k),
|
|
5091
5361
|
x2: along(k),
|
|
5092
|
-
y1: y -
|
|
5093
|
-
y2: y +
|
|
5362
|
+
y1: y - barPx / 2,
|
|
5363
|
+
y2: y + barPx / 2,
|
|
5094
5364
|
stroke: "white",
|
|
5095
5365
|
"stroke-width": 1
|
|
5096
5366
|
})
|
|
@@ -5103,6 +5373,7 @@ function walkRowsTree(bars, frame, o = {}) {
|
|
|
5103
5373
|
call
|
|
5104
5374
|
);
|
|
5105
5375
|
const at = readoutPlacement(text, along(row.bp), frame.width);
|
|
5376
|
+
const readouts = frame.readouts ?? true;
|
|
5106
5377
|
return el(
|
|
5107
5378
|
"g",
|
|
5108
5379
|
{
|
|
@@ -5110,18 +5381,19 @@ function walkRowsTree(bars, frame, o = {}) {
|
|
|
5110
5381
|
},
|
|
5111
5382
|
...runs,
|
|
5112
5383
|
...ticks,
|
|
5113
|
-
...rowGeneBoxes(o.rowGenes?.get(row.name) ?? [], along, y
|
|
5114
|
-
|
|
5115
|
-
|
|
5384
|
+
...rowGeneBoxes(o.rowGenes?.get(row.name) ?? [], along, y, {
|
|
5385
|
+
rowPx,
|
|
5386
|
+
barPx
|
|
5387
|
+
}).map((box) => geneTree(box, y, barPx)),
|
|
5116
5388
|
call && el("rect", {
|
|
5117
5389
|
"data-testid": "graph-walk-call",
|
|
5118
5390
|
x: along(call.bp) - 1,
|
|
5119
|
-
y: y -
|
|
5391
|
+
y: y - barPx / 2 - 3,
|
|
5120
5392
|
width: 2,
|
|
5121
|
-
height:
|
|
5393
|
+
height: barPx + 6,
|
|
5122
5394
|
fill: call.spanningReads === 0 ? UNBACKED_TICK : CALL_TICK
|
|
5123
5395
|
}),
|
|
5124
|
-
el(
|
|
5396
|
+
readouts && el(
|
|
5125
5397
|
"text",
|
|
5126
5398
|
{
|
|
5127
5399
|
x: at.x,
|
|
@@ -5141,6 +5413,180 @@ function walkRowsTree(bars, frame, o = {}) {
|
|
|
5141
5413
|
return el("g", {}, ...[reference, ...rows].map(rowTree));
|
|
5142
5414
|
}
|
|
5143
5415
|
|
|
5416
|
+
// src/layout/walkStrip.ts
|
|
5417
|
+
var PAD_PX = 6;
|
|
5418
|
+
var WALK_STRIP_CEILING_PX = 260;
|
|
5419
|
+
var LABEL_FONT_PX = 11;
|
|
5420
|
+
var MIN_ROW_PX = 3;
|
|
5421
|
+
var LABELLED_ROW_PX = 12;
|
|
5422
|
+
var READOUT_ROOM2 = 1.3;
|
|
5423
|
+
var MARK_INK = "#111";
|
|
5424
|
+
var TICK_PX = 3;
|
|
5425
|
+
function stripMarks(graph, rows, node) {
|
|
5426
|
+
const visits = graph.pathVisits?.get(node.name) ?? [];
|
|
5427
|
+
const marks = [];
|
|
5428
|
+
for (const row of rows) {
|
|
5429
|
+
const axis = row.axis;
|
|
5430
|
+
if (!axis) {
|
|
5431
|
+
continue;
|
|
5432
|
+
}
|
|
5433
|
+
const path = pathOrigin(row.name).name;
|
|
5434
|
+
for (const v of visits) {
|
|
5435
|
+
if (v.path !== path) {
|
|
5436
|
+
continue;
|
|
5437
|
+
}
|
|
5438
|
+
const { start, end } = alongRow(axis, v.start, v.start + node.length);
|
|
5439
|
+
if (end > 0 && start < row.bp) {
|
|
5440
|
+
marks.push({ row: row.name, start, bp: end - start });
|
|
5441
|
+
}
|
|
5442
|
+
}
|
|
5443
|
+
}
|
|
5444
|
+
return marks;
|
|
5445
|
+
}
|
|
5446
|
+
var spanCache = /* @__PURE__ */ new WeakMap();
|
|
5447
|
+
function spansOf(graph, name) {
|
|
5448
|
+
let byName = spanCache.get(graph);
|
|
5449
|
+
if (!byName) {
|
|
5450
|
+
byName = /* @__PURE__ */ new Map();
|
|
5451
|
+
spanCache.set(graph, byName);
|
|
5452
|
+
}
|
|
5453
|
+
if (byName.has(name)) {
|
|
5454
|
+
return byName.get(name);
|
|
5455
|
+
}
|
|
5456
|
+
const lengthOf = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
|
|
5457
|
+
const pieces = (graph.paths ?? []).filter((p) => p.name === name);
|
|
5458
|
+
let spans;
|
|
5459
|
+
if (pieces.every((p) => p.start !== void 0)) {
|
|
5460
|
+
spans = { ids: [], starts: [], ends: [] };
|
|
5461
|
+
for (const piece of [...pieces].sort((a, b) => a.start - b.start)) {
|
|
5462
|
+
let pos = piece.start;
|
|
5463
|
+
for (const id of piece.nodeIds) {
|
|
5464
|
+
const len = lengthOf.get(id) ?? 0;
|
|
5465
|
+
spans.ids.push(id);
|
|
5466
|
+
spans.starts.push(pos);
|
|
5467
|
+
spans.ends.push(pos + len);
|
|
5468
|
+
pos += len;
|
|
5469
|
+
}
|
|
5470
|
+
}
|
|
5471
|
+
}
|
|
5472
|
+
byName.set(name, spans);
|
|
5473
|
+
return spans;
|
|
5474
|
+
}
|
|
5475
|
+
function segmentAt(graph, row, offset, pxPerBp) {
|
|
5476
|
+
const axis = row.axis;
|
|
5477
|
+
const spans = axis && spansOf(graph, row.name);
|
|
5478
|
+
if (!axis || !spans || offset < 0 || offset > row.bp) {
|
|
5479
|
+
return void 0;
|
|
5480
|
+
}
|
|
5481
|
+
const at = axis.reversed ? axis.start - offset : axis.start + offset;
|
|
5482
|
+
const tol = 0.5 / pxPerBp;
|
|
5483
|
+
const bar = axis.reversed ? { start: axis.start - row.bp, end: axis.start } : { start: axis.start, end: axis.start + row.bp };
|
|
5484
|
+
const from = Math.max(at - tol, bar.start);
|
|
5485
|
+
const to = Math.min(at + tol, bar.end);
|
|
5486
|
+
let lo = 0;
|
|
5487
|
+
let hi = spans.starts.length;
|
|
5488
|
+
while (lo < hi) {
|
|
5489
|
+
const mid = lo + hi >> 1;
|
|
5490
|
+
if (spans.ends[mid] <= from) {
|
|
5491
|
+
lo = mid + 1;
|
|
5492
|
+
} else {
|
|
5493
|
+
hi = mid;
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
let best;
|
|
5497
|
+
let bestLen = -1;
|
|
5498
|
+
let bestHolds = false;
|
|
5499
|
+
for (let i = lo; i < spans.starts.length && spans.starts[i] < to; i++) {
|
|
5500
|
+
const len = spans.ends[i] - spans.starts[i];
|
|
5501
|
+
const holds = spans.starts[i] <= at && at < spans.ends[i];
|
|
5502
|
+
if (len > bestLen || len === bestLen && holds && !bestHolds) {
|
|
5503
|
+
bestLen = len;
|
|
5504
|
+
best = spans.ids[i];
|
|
5505
|
+
bestHolds = holds;
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
return best;
|
|
5509
|
+
}
|
|
5510
|
+
function walkStripFrame(bars, o) {
|
|
5511
|
+
const maxHeight = o.maxHeight ?? WALK_STRIP_CEILING_PX;
|
|
5512
|
+
const labelPx = o.labelPx ?? Math.max(...[bars.reference, ...bars.rows].map((r) => r.label.length)) * LABEL_CHAR_PX + 16;
|
|
5513
|
+
const count = bars.rows.length + 1;
|
|
5514
|
+
const rowPx = Math.max(
|
|
5515
|
+
MIN_ROW_PX,
|
|
5516
|
+
Math.min(ROW_HEIGHT_PX, Math.floor((maxHeight - 2 * PAD_PX) / count))
|
|
5517
|
+
);
|
|
5518
|
+
const labelled = rowPx >= LABELLED_ROW_PX;
|
|
5519
|
+
const readouts = rowPx >= 14;
|
|
5520
|
+
const barPx = Math.max(2, Math.round(rowPx * 0.6));
|
|
5521
|
+
const longest = Math.max(bars.reference.bp, ...bars.rows.map((r) => r.bp), 1);
|
|
5522
|
+
const left = labelled ? labelPx : PAD_PX;
|
|
5523
|
+
const usable = Math.max(1, o.width - left - PAD_PX);
|
|
5524
|
+
const scaleX = usable / (longest * (readouts ? READOUT_ROOM2 : 1));
|
|
5525
|
+
return {
|
|
5526
|
+
scaleX,
|
|
5527
|
+
scaleY: 1,
|
|
5528
|
+
translateX: left - bars.origin * scaleX,
|
|
5529
|
+
translateY: PAD_PX + rowPx / 2,
|
|
5530
|
+
width: o.width,
|
|
5531
|
+
height: count * rowPx + 2 * PAD_PX,
|
|
5532
|
+
rowPx,
|
|
5533
|
+
barPx,
|
|
5534
|
+
readouts,
|
|
5535
|
+
labelled,
|
|
5536
|
+
boxesGenes: boxesGenes(rowPx, barPx)
|
|
5537
|
+
};
|
|
5538
|
+
}
|
|
5539
|
+
function stripRowAt(bars, frame, x, y) {
|
|
5540
|
+
const i = Math.round((y - frame.translateY) / frame.rowPx);
|
|
5541
|
+
const row = [bars.reference, ...bars.rows][i];
|
|
5542
|
+
const offset = (x - frame.translateX) / frame.scaleX - bars.origin;
|
|
5543
|
+
return row && i >= 0 && offset >= 0 && offset <= row.bp ? { index: i, row, offset } : void 0;
|
|
5544
|
+
}
|
|
5545
|
+
function walkMarksTree(bars, frame, marks) {
|
|
5546
|
+
const index = new Map(
|
|
5547
|
+
[bars.reference, ...bars.rows].map((row, i) => [row.name, i])
|
|
5548
|
+
);
|
|
5549
|
+
const X = (offset) => (bars.origin + offset) * frame.scaleX + frame.translateX;
|
|
5550
|
+
return el(
|
|
5551
|
+
"g",
|
|
5552
|
+
{ "data-testid": "graph-walk-marks" },
|
|
5553
|
+
...marks.flatMap((m) => {
|
|
5554
|
+
const i = index.get(m.row);
|
|
5555
|
+
if (i === void 0) {
|
|
5556
|
+
return [];
|
|
5557
|
+
}
|
|
5558
|
+
const y = i * frame.rowPx + frame.translateY;
|
|
5559
|
+
const x = X(m.start);
|
|
5560
|
+
const w = Math.max(2, m.bp * frame.scaleX);
|
|
5561
|
+
const tick = (top) => el("rect", { x, y: top, width: w, height: TICK_PX, fill: MARK_INK });
|
|
5562
|
+
return [
|
|
5563
|
+
tick(y - frame.barPx / 2 - TICK_PX - 1),
|
|
5564
|
+
tick(y + frame.barPx / 2 + 1)
|
|
5565
|
+
];
|
|
5566
|
+
})
|
|
5567
|
+
);
|
|
5568
|
+
}
|
|
5569
|
+
function walkStripLabelsTree(bars, frame, bold = /* @__PURE__ */ new Set()) {
|
|
5570
|
+
return el(
|
|
5571
|
+
"g",
|
|
5572
|
+
{ "font-family": "sans-serif", "font-size": LABEL_FONT_PX, fill: "#333" },
|
|
5573
|
+
...(frame.labelled ? [bars.reference, ...bars.rows] : []).map(
|
|
5574
|
+
(row, i) => el(
|
|
5575
|
+
"text",
|
|
5576
|
+
{
|
|
5577
|
+
x: 6,
|
|
5578
|
+
y: i * frame.rowPx + frame.translateY + 4,
|
|
5579
|
+
"font-weight": bold.has(row.name) ? 600 : void 0
|
|
5580
|
+
},
|
|
5581
|
+
row.label
|
|
5582
|
+
)
|
|
5583
|
+
)
|
|
5584
|
+
);
|
|
5585
|
+
}
|
|
5586
|
+
function stripGeneGaps(frame, gaps) {
|
|
5587
|
+
return gaps && !frame.boxesGenes ? { ...gaps, crowded: true } : gaps;
|
|
5588
|
+
}
|
|
5589
|
+
|
|
5144
5590
|
// src/renderer/svgCanvas.ts
|
|
5145
5591
|
var n = (v) => +v.toFixed(1);
|
|
5146
5592
|
function paint(css) {
|
|
@@ -5254,7 +5700,7 @@ function svgCanvas(width, height) {
|
|
|
5254
5700
|
}
|
|
5255
5701
|
|
|
5256
5702
|
// src/version.ts
|
|
5257
|
-
var version = "4.
|
|
5703
|
+
var version = "4.4.0";
|
|
5258
5704
|
|
|
5259
5705
|
// src/viewport.ts
|
|
5260
5706
|
function axisScaleOf(scale, pixelRows) {
|
|
@@ -5302,6 +5748,7 @@ var FONT = 'font-family="Helvetica, Arial, sans-serif" font-size="11"';
|
|
|
5302
5748
|
var BAR_PX2 = 48;
|
|
5303
5749
|
var SWATCH_PX = 18;
|
|
5304
5750
|
var KEY_GAP_PX = 16;
|
|
5751
|
+
var STRIP_KEY_PX = 22;
|
|
5305
5752
|
var FADED = "rgb(160,160,160)";
|
|
5306
5753
|
var EXON_COLOR = "#daa520";
|
|
5307
5754
|
var GENE_INK2 = "#1c1c22";
|
|
@@ -5482,6 +5929,7 @@ function figureSvg(graph, layout, o = {}) {
|
|
|
5482
5929
|
axis,
|
|
5483
5930
|
referenceRamp,
|
|
5484
5931
|
deletions: new Map(deletions.map((d) => [d.edgeIndex, d.bypassed])),
|
|
5932
|
+
deletionRoutes: layout.deletionRoutes,
|
|
5485
5933
|
hiddenEdges: new Set(
|
|
5486
5934
|
o.showDeletionEdges ? [] : deletions.map((d) => d.edgeIndex)
|
|
5487
5935
|
)
|
|
@@ -5589,6 +6037,42 @@ function figureSvg(graph, layout, o = {}) {
|
|
|
5589
6037
|
);
|
|
5590
6038
|
height = header + h;
|
|
5591
6039
|
}
|
|
6040
|
+
const strip = o.walkStrip;
|
|
6041
|
+
if (strip && !rows && !layout.tubeMap) {
|
|
6042
|
+
const frame = walkStripFrame(strip.rows, { width });
|
|
6043
|
+
const key = walkRowsKeyTree(
|
|
6044
|
+
walkRowsKey(strip.rows, {
|
|
6045
|
+
ramp: referenceRamp && {
|
|
6046
|
+
start: referenceRamp.start,
|
|
6047
|
+
end: referenceRamp.start + referenceRamp.span
|
|
6048
|
+
},
|
|
6049
|
+
genes: strip.rowGenes?.size ? stripGeneGaps(frame, strip.rowGeneGaps) : void 0
|
|
6050
|
+
}),
|
|
6051
|
+
6,
|
|
6052
|
+
height + frame.height
|
|
6053
|
+
);
|
|
6054
|
+
parts.push(
|
|
6055
|
+
`<line x1="0" x2="${width}" y1="${height + 0.5}" y2="${height + 0.5}" stroke="#ddd"/>`,
|
|
6056
|
+
`<svg y="${height}" width="${width}" height="${frame.height}">${serializeEl(
|
|
6057
|
+
walkRowsTree(strip.rows, frame, {
|
|
6058
|
+
ramp: referenceRamp && {
|
|
6059
|
+
start: referenceRamp.start,
|
|
6060
|
+
end: referenceRamp.start + referenceRamp.span
|
|
6061
|
+
},
|
|
6062
|
+
rowGenes: strip.rowGenes,
|
|
6063
|
+
idPrefix: "strip"
|
|
6064
|
+
})
|
|
6065
|
+
)}${serializeEl(
|
|
6066
|
+
walkStripLabelsTree(
|
|
6067
|
+
strip.rows,
|
|
6068
|
+
frame,
|
|
6069
|
+
new Set(layers.map((l) => l.walk))
|
|
6070
|
+
)
|
|
6071
|
+
)}</svg>`,
|
|
6072
|
+
serializeEl(key.tree)
|
|
6073
|
+
);
|
|
6074
|
+
height += frame.height + STRIP_KEY_PX;
|
|
6075
|
+
}
|
|
5592
6076
|
const metadata = JSON.stringify({
|
|
5593
6077
|
generator: `@jbrowse/bandage-core@${version}`,
|
|
5594
6078
|
...o.spec === void 0 ? {} : { spec: o.spec }
|
|
@@ -6189,6 +6673,13 @@ function loadBandage() {
|
|
|
6189
6673
|
export {
|
|
6190
6674
|
isBackbone,
|
|
6191
6675
|
BUBBLE_SPREADS,
|
|
6676
|
+
ROW_HEIGHT_PX,
|
|
6677
|
+
svgPath,
|
|
6678
|
+
curveBounds,
|
|
6679
|
+
translateCurves,
|
|
6680
|
+
yToXOf,
|
|
6681
|
+
pathRibbonOffsets,
|
|
6682
|
+
deletionEdges,
|
|
6192
6683
|
panSNSample,
|
|
6193
6684
|
panSNHaplotype,
|
|
6194
6685
|
panSNContig,
|
|
@@ -6202,14 +6693,7 @@ export {
|
|
|
6202
6693
|
clampZoom,
|
|
6203
6694
|
fitTransform,
|
|
6204
6695
|
drawingBounds,
|
|
6205
|
-
ROW_HEIGHT_PX,
|
|
6206
6696
|
referenceOrder,
|
|
6207
|
-
svgPath,
|
|
6208
|
-
curveBounds,
|
|
6209
|
-
translateCurves,
|
|
6210
|
-
yToXOf,
|
|
6211
|
-
pathRibbonOffsets,
|
|
6212
|
-
deletionEdges,
|
|
6213
6697
|
LABEL_PX,
|
|
6214
6698
|
LABEL_CHAR_PX,
|
|
6215
6699
|
LABEL_PAD,
|
|
@@ -6283,7 +6767,17 @@ export {
|
|
|
6283
6767
|
walkRowsKey,
|
|
6284
6768
|
DISAGREES,
|
|
6285
6769
|
walkRowsKeyTree,
|
|
6770
|
+
coalesceRuns,
|
|
6286
6771
|
walkRowsTree,
|
|
6772
|
+
WALK_STRIP_CEILING_PX,
|
|
6773
|
+
LABELLED_ROW_PX,
|
|
6774
|
+
stripMarks,
|
|
6775
|
+
segmentAt,
|
|
6776
|
+
walkStripFrame,
|
|
6777
|
+
stripRowAt,
|
|
6778
|
+
walkMarksTree,
|
|
6779
|
+
walkStripLabelsTree,
|
|
6780
|
+
stripGeneGaps,
|
|
6287
6781
|
svgCanvas,
|
|
6288
6782
|
version,
|
|
6289
6783
|
axisScaleOf,
|