@jbrowse/bandage-core 0.1.19 → 4.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/chunks/chunk-4AOHEU76.js +4982 -0
- package/dist/cli/figure.js +152 -0
- package/dist/index.js +201 -4640
- package/dist/types/cli/figure.d.ts +20 -0
- package/dist/types/facetGrid.d.ts +10 -0
- package/dist/types/figure.d.ts +24 -0
- package/dist/types/genes/genePins.d.ts +4 -0
- package/dist/types/index.d.ts +8 -5
- package/dist/types/labelLayout.d.ts +1 -0
- package/dist/types/overlayLabels.d.ts +1 -0
- package/dist/types/referenceStrip.d.ts +15 -6
- package/dist/types/renderer/svgCanvas.d.ts +4 -0
- package/dist/types/walkEncoding.d.ts +5 -1
- package/dist/types/walkKey.d.ts +1 -0
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1,3717 +1,101 @@
|
|
|
1
|
-
// src/anchoredNodes.ts
|
|
2
|
-
var REFERENCE_RANK = 0;
|
|
3
|
-
function isAnchored(node) {
|
|
4
|
-
return node.stable !== void 0;
|
|
5
|
-
}
|
|
6
|
-
function isBackbone(node) {
|
|
7
|
-
return node.stable?.rank === REFERENCE_RANK;
|
|
8
|
-
}
|
|
9
|
-
function isOffReference(node) {
|
|
10
|
-
return node.stable !== void 0 && node.stable.rank > REFERENCE_RANK;
|
|
11
|
-
}
|
|
12
|
-
function backboneNodes(graph) {
|
|
13
|
-
return graph.nodes.filter(isBackbone);
|
|
14
|
-
}
|
|
15
|
-
function backbonePositions(backbone) {
|
|
16
|
-
const positions = {};
|
|
17
|
-
for (const node of backbone) {
|
|
18
|
-
positions[node.id] = [
|
|
19
|
-
{ x: node.stable.start, y: 0 },
|
|
20
|
-
{ x: node.stable.start + node.length, y: 0 }
|
|
21
|
-
];
|
|
22
|
-
}
|
|
23
|
-
return positions;
|
|
24
|
-
}
|
|
25
|
-
function firstNodeAtOrAfter(nodes, bp) {
|
|
26
|
-
let lo = 0;
|
|
27
|
-
let hi = nodes.length;
|
|
28
|
-
while (lo < hi) {
|
|
29
|
-
const mid = lo + hi >> 1;
|
|
30
|
-
if (nodes[mid].stable.start < bp) {
|
|
31
|
-
lo = mid + 1;
|
|
32
|
-
} else {
|
|
33
|
-
hi = mid;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return lo;
|
|
37
|
-
}
|
|
38
|
-
function backboneSpan(backbone) {
|
|
39
|
-
let min = Infinity;
|
|
40
|
-
let max = -Infinity;
|
|
41
|
-
for (const node of backbone) {
|
|
42
|
-
min = Math.min(min, node.stable.start);
|
|
43
|
-
max = Math.max(max, node.stable.start + node.length);
|
|
44
|
-
}
|
|
45
|
-
return max - min;
|
|
46
|
-
}
|
|
47
|
-
function referenceSpan(backbone, region) {
|
|
48
|
-
return region && region.end > region.start ? region.end - region.start : backboneSpan(backbone);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// src/layout/drawnScale.ts
|
|
52
|
-
var MEAN_NODE_LENGTH = 40;
|
|
53
|
-
var MIN_TOTAL_GRAPH_LENGTH = 500;
|
|
54
|
-
var BANDAGE_MINIMUM_NODE_LENGTH = 5;
|
|
55
|
-
function drawnNodeLength(opts, length) {
|
|
56
|
-
return Math.max(
|
|
57
|
-
opts.nodeLengthPerMegabase * length / 1e6,
|
|
58
|
-
opts.minimumNodeLength
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
function bandageAutoScale(graph, minNodeLength = BANDAGE_MINIMUM_NODE_LENGTH) {
|
|
62
|
-
const totalLength = graph.nodes.reduce((sum, n) => sum + n.length, 0);
|
|
63
|
-
const megabases = totalLength / 1e6;
|
|
64
|
-
const target = Math.max(
|
|
65
|
-
graph.nodes.length * MEAN_NODE_LENGTH,
|
|
66
|
-
MIN_TOTAL_GRAPH_LENGTH
|
|
67
|
-
);
|
|
68
|
-
return {
|
|
69
|
-
nodeLengthPerMegabase: megabases > 0 ? target / megabases : 1e4,
|
|
70
|
-
minimumNodeLength: Math.max(BANDAGE_MINIMUM_NODE_LENGTH, minNodeLength),
|
|
71
|
-
edgeLength: 5,
|
|
72
|
-
nodeSegmentLength: 20
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
var PROPORTIONAL_LENGTH = { power: 1, effect: 1 };
|
|
76
|
-
function isProportional(law) {
|
|
77
|
-
return law.power === 1 && law.effect === 1;
|
|
78
|
-
}
|
|
79
|
-
function drawnLengthFor(graph, law) {
|
|
80
|
-
const count = graph.nodes.length;
|
|
81
|
-
const totalLength = graph.nodes.reduce((sum, n) => sum + n.length, 0);
|
|
82
|
-
const meanBp = count > 0 ? totalLength / count : 0;
|
|
83
|
-
const meanDrawn = count > 0 ? Math.max(count * MEAN_NODE_LENGTH, MIN_TOTAL_GRAPH_LENGTH) / count : MEAN_NODE_LENGTH;
|
|
84
|
-
return (bp) => {
|
|
85
|
-
if (meanBp <= 0) {
|
|
86
|
-
return meanDrawn;
|
|
87
|
-
}
|
|
88
|
-
const relative = (bp / meanBp) ** law.power;
|
|
89
|
-
return Math.max(
|
|
90
|
-
meanDrawn * ((relative - 1) * law.effect + 1),
|
|
91
|
-
BANDAGE_MINIMUM_NODE_LENGTH
|
|
92
|
-
);
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
var LAYOUT_LENGTH_PRECISION = 1e3;
|
|
96
|
-
var PROPORTIONAL_SPREAD = {
|
|
97
|
-
law: PROPORTIONAL_LENGTH,
|
|
98
|
-
minNodeLength: 0
|
|
99
|
-
};
|
|
100
|
-
function layoutScaling(graph, spread = PROPORTIONAL_SPREAD) {
|
|
101
|
-
const { law, minNodeLength } = spread;
|
|
102
|
-
const opts = bandageAutoScale(graph, minNodeLength);
|
|
103
|
-
if (isProportional(law)) {
|
|
104
|
-
return { nodes: graph.nodes, opts };
|
|
105
|
-
}
|
|
106
|
-
const drawn = drawnLengthFor(graph, law);
|
|
107
|
-
return {
|
|
108
|
-
nodes: graph.nodes.map((node) => ({
|
|
109
|
-
...node,
|
|
110
|
-
length: Math.round(drawn(node.length) * LAYOUT_LENGTH_PRECISION)
|
|
111
|
-
})),
|
|
112
|
-
opts: {
|
|
113
|
-
...opts,
|
|
114
|
-
nodeLengthPerMegabase: 1e6 / LAYOUT_LENGTH_PRECISION,
|
|
115
|
-
// Bandage's own floor only. A law and a raised floor are alternatives
|
|
116
|
-
// rather than a stack: the law has already lifted the small end, and a
|
|
117
|
-
// floor on top of it would flatten exactly what it just spread.
|
|
118
|
-
minimumNodeLength: BANDAGE_MINIMUM_NODE_LENGTH
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// src/bubbleSpreads.ts
|
|
124
|
-
var BUBBLE_SPREADS = [
|
|
125
|
-
{
|
|
126
|
-
value: "auto",
|
|
127
|
-
label: "Proportional",
|
|
128
|
-
description: "Bandage's own scale: a node draws proportional to its length, so a 1 bp allele is a speck.",
|
|
129
|
-
law: PROPORTIONAL_LENGTH,
|
|
130
|
-
minNodeFactor: 0
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
value: "open",
|
|
134
|
-
label: "Open bubbles",
|
|
135
|
-
description: "Give every allele a drawn length, so a bubble reads as two arms rather than a knot.",
|
|
136
|
-
law: PROPORTIONAL_LENGTH,
|
|
137
|
-
minNodeFactor: 2.5
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
value: "wide",
|
|
141
|
-
label: "Wide bubbles",
|
|
142
|
-
description: "As open, spread further. Best on a window of a dozen bubbles.",
|
|
143
|
-
law: PROPORTIONAL_LENGTH,
|
|
144
|
-
minNodeFactor: 10
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
value: "compress",
|
|
148
|
-
label: "Compress lengths",
|
|
149
|
-
description: "Pull the longest and shortest nodes towards the graph\u2019s mean, so a cut spanning kb and bp fits one pane.",
|
|
150
|
-
law: { power: 0.5, effect: 0.5 },
|
|
151
|
-
minNodeFactor: 0
|
|
152
|
-
}
|
|
153
|
-
];
|
|
154
|
-
var BUBBLE_SPREAD_VALUES = BUBBLE_SPREADS.map((s) => s.value);
|
|
155
|
-
function spreadFor(spread) {
|
|
156
|
-
const entry = BUBBLE_SPREADS.find((s) => s.value === spread);
|
|
157
|
-
return {
|
|
158
|
-
law: entry?.law ?? PROPORTIONAL_LENGTH,
|
|
159
|
-
minNodeLength: (entry?.minNodeFactor ?? 0) * MEAN_NODE_LENGTH
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// src/gfa-core/gfaParser.ts
|
|
164
|
-
function parseTag(tag, tags) {
|
|
165
|
-
const nameEnd = tag.indexOf(":");
|
|
166
|
-
const typeEnd = tag.indexOf(":", nameEnd + 1);
|
|
167
|
-
if (nameEnd !== -1 && typeEnd !== -1) {
|
|
168
|
-
const name = tag.slice(0, nameEnd);
|
|
169
|
-
const type = tag.slice(nameEnd + 1, typeEnd);
|
|
170
|
-
const val = tag.slice(typeEnd + 1);
|
|
171
|
-
if (type === "i" || type === "f") {
|
|
172
|
-
tags[name] = +val;
|
|
173
|
-
} else if (type === "Z") {
|
|
174
|
-
tags[name] = val;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
function parseTags(fields) {
|
|
179
|
-
const tags = {};
|
|
180
|
-
for (const field of fields) {
|
|
181
|
-
parseTag(field, tags);
|
|
182
|
-
}
|
|
183
|
-
return tags;
|
|
184
|
-
}
|
|
185
|
-
function stableCoordinate(node) {
|
|
186
|
-
const { SN, SO, SR } = node.tags;
|
|
187
|
-
return typeof SN === "string" && typeof SO === "number" && typeof SR === "number" ? { refName: SN, start: SO, rank: SR } : void 0;
|
|
188
|
-
}
|
|
189
|
-
var GT = 62;
|
|
190
|
-
var LT = 60;
|
|
191
|
-
function parseWalkBody(body) {
|
|
192
|
-
const segments = [];
|
|
193
|
-
let strand = "+";
|
|
194
|
-
let start = 0;
|
|
195
|
-
function push(end) {
|
|
196
|
-
if (end > start) {
|
|
197
|
-
segments.push({ id: body.slice(start, end), strand });
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
for (let i = 0; i < body.length; i++) {
|
|
201
|
-
const ch = body.charCodeAt(i);
|
|
202
|
-
if (ch === GT || ch === LT) {
|
|
203
|
-
push(i);
|
|
204
|
-
strand = ch === GT ? "+" : "-";
|
|
205
|
-
start = i + 1;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
push(body.length);
|
|
209
|
-
return segments;
|
|
210
|
-
}
|
|
211
|
-
function parseGFA(file) {
|
|
212
|
-
const graph = {
|
|
213
|
-
nodes: [],
|
|
214
|
-
links: [],
|
|
215
|
-
paths: [],
|
|
216
|
-
walks: [],
|
|
217
|
-
header: [],
|
|
218
|
-
id: ""
|
|
219
|
-
};
|
|
220
|
-
for (const rawLine of file.split("\n")) {
|
|
221
|
-
const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
|
|
222
|
-
if (line.startsWith("H")) {
|
|
223
|
-
const [, ...rest] = line.split(" ");
|
|
224
|
-
graph.header.push(parseTags(rest));
|
|
225
|
-
} else if (line.startsWith("S")) {
|
|
226
|
-
const [, name, ...rest] = line.split(" ");
|
|
227
|
-
let len;
|
|
228
|
-
let seq;
|
|
229
|
-
let tagfields;
|
|
230
|
-
let gfa1 = false;
|
|
231
|
-
const first = rest[0] ?? "*";
|
|
232
|
-
if (/^\d+$/.test(first)) {
|
|
233
|
-
len = +first;
|
|
234
|
-
seq = rest[1] ?? "*";
|
|
235
|
-
tagfields = rest.slice(2);
|
|
236
|
-
} else {
|
|
237
|
-
gfa1 = true;
|
|
238
|
-
seq = first;
|
|
239
|
-
len = seq === "*" ? 0 : seq.length;
|
|
240
|
-
tagfields = rest.slice(1);
|
|
241
|
-
}
|
|
242
|
-
const tags = parseTags(tagfields);
|
|
243
|
-
if (gfa1 && "LN" in tags) {
|
|
244
|
-
len = +tags.LN;
|
|
245
|
-
}
|
|
246
|
-
graph.nodes.push({ id: name, length: len, sequence: seq, tags });
|
|
247
|
-
} else if (line.startsWith("E")) {
|
|
248
|
-
const [, , source, target, , , , , cigar, ...rest] = line.split(" ");
|
|
249
|
-
if (source && target) {
|
|
250
|
-
graph.links.push({
|
|
251
|
-
source: source.slice(0, -1),
|
|
252
|
-
target: target.slice(0, -1),
|
|
253
|
-
strand1: source.at(-1),
|
|
254
|
-
strand2: target.at(-1),
|
|
255
|
-
cigar: cigar ?? "*",
|
|
256
|
-
tags: parseTags(rest)
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
} else if (line.startsWith("L")) {
|
|
260
|
-
const [, source, strand1, target, strand2, cigar, ...rest] = line.split(" ");
|
|
261
|
-
if (source && target && strand2) {
|
|
262
|
-
graph.links.push({
|
|
263
|
-
source,
|
|
264
|
-
target,
|
|
265
|
-
strand1,
|
|
266
|
-
strand2,
|
|
267
|
-
cigar: cigar ?? "*",
|
|
268
|
-
tags: parseTags(rest)
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
} else if (line.startsWith("W")) {
|
|
272
|
-
const [, sample, hap, contig, start, end, body, ...rest] = line.split(" ");
|
|
273
|
-
if (sample && hap && contig && start && end && body) {
|
|
274
|
-
graph.walks.push({
|
|
275
|
-
sample,
|
|
276
|
-
haplotype: +hap,
|
|
277
|
-
contig,
|
|
278
|
-
start: start === "*" ? -1 : +start,
|
|
279
|
-
end: end === "*" ? -1 : +end,
|
|
280
|
-
segments: parseWalkBody(body),
|
|
281
|
-
tags: parseTags(rest)
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
} else if (line.startsWith("P")) {
|
|
285
|
-
const [, name, path, ...rest] = line.split(" ");
|
|
286
|
-
if (name && path) {
|
|
287
|
-
graph.paths.push({ name, path, rest });
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return graph;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// src/pansn.ts
|
|
295
|
-
var SEP = "#";
|
|
296
|
-
function panSNSample(refName) {
|
|
297
|
-
return refName.split(SEP)[0];
|
|
298
|
-
}
|
|
299
|
-
var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
|
|
300
|
-
function isGenericSample(sample) {
|
|
301
|
-
return GENERIC_SAMPLES.has(sample);
|
|
302
|
-
}
|
|
303
|
-
function panSNHaplotype(refName) {
|
|
304
|
-
const parts = refName.split(SEP);
|
|
305
|
-
return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : void 0;
|
|
306
|
-
}
|
|
307
|
-
function panSNContig(refName) {
|
|
308
|
-
const parts = refName.split(SEP);
|
|
309
|
-
return parts.length >= 3 ? parts.slice(2).join(SEP) : parts.length === 2 ? parts[1] : refName;
|
|
310
|
-
}
|
|
311
|
-
function panSNMatchesPrefix(refName, prefix) {
|
|
312
|
-
return prefix !== void 0 && (refName === prefix || refName.startsWith(prefix + SEP));
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// src/pathAnchoring.ts
|
|
316
|
-
var RANGE_SUFFIX = /:(\d+)-\d+$/;
|
|
317
|
-
function pathOrigin(pathName) {
|
|
318
|
-
const match = RANGE_SUFFIX.exec(pathName);
|
|
319
|
-
return match ? { name: pathName.slice(0, match.index), start: +match[1] } : { name: pathName, start: 0 };
|
|
320
|
-
}
|
|
321
|
-
function surveyPaths(paths, lengthOf) {
|
|
322
|
-
const anchorPaths = [];
|
|
323
|
-
const pathVisits = /* @__PURE__ */ new Map();
|
|
324
|
-
for (const path of paths) {
|
|
325
|
-
const sample = panSNSample(path.name);
|
|
326
|
-
let pos = path.start;
|
|
327
|
-
for (const step of path.steps) {
|
|
328
|
-
const visit = {
|
|
329
|
-
path: path.name,
|
|
330
|
-
sample,
|
|
331
|
-
start: pos,
|
|
332
|
-
strand: step.strand
|
|
333
|
-
};
|
|
334
|
-
const existing = pathVisits.get(step.id);
|
|
335
|
-
if (existing) {
|
|
336
|
-
existing.push(visit);
|
|
337
|
-
} else {
|
|
338
|
-
pathVisits.set(step.id, [visit]);
|
|
339
|
-
}
|
|
340
|
-
pos += lengthOf(step.id);
|
|
341
|
-
}
|
|
342
|
-
anchorPaths.push({
|
|
343
|
-
name: path.name,
|
|
344
|
-
sample,
|
|
345
|
-
start: path.start,
|
|
346
|
-
length: pos - path.start
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
return { anchorPaths, pathVisits };
|
|
350
|
-
}
|
|
351
|
-
function chooseReferencePath(anchorPaths, preferred) {
|
|
352
|
-
const named = anchorPaths.find(
|
|
353
|
-
(p) => p.sample === preferred || p.name === preferred
|
|
354
|
-
);
|
|
355
|
-
return named ?? anchorPaths[0];
|
|
356
|
-
}
|
|
357
|
-
var OFF_REFERENCE_RANK = 1;
|
|
358
|
-
function anchorNode(node, visits, reference) {
|
|
359
|
-
let anchored = node;
|
|
360
|
-
if (visits && visits.length > 0) {
|
|
361
|
-
const anchor = visits.find((v) => v.path === reference) ?? visits[0];
|
|
362
|
-
anchored = {
|
|
363
|
-
...node,
|
|
364
|
-
stable: {
|
|
365
|
-
refName: anchor.path,
|
|
366
|
-
start: anchor.start,
|
|
367
|
-
rank: anchor.path === reference ? REFERENCE_RANK : OFF_REFERENCE_RANK,
|
|
368
|
-
strand: anchor.strand
|
|
369
|
-
},
|
|
370
|
-
samples: [...new Set(visits.map((v) => v.sample))].sort()
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
return anchored;
|
|
374
|
-
}
|
|
375
|
-
function anchorFromPaths(graph, preferred) {
|
|
376
|
-
const { anchorPaths, pathVisits } = graph;
|
|
377
|
-
const reference = anchorPaths && pathVisits ? chooseReferencePath(anchorPaths, preferred) : void 0;
|
|
378
|
-
return reference && pathVisits ? {
|
|
379
|
-
...graph,
|
|
380
|
-
nodes: graph.nodes.map(
|
|
381
|
-
(node) => anchorNode(node, pathVisits.get(node.name), reference.name)
|
|
382
|
-
),
|
|
383
|
-
anchoredBy: "paths",
|
|
384
|
-
referencePath: reference.name
|
|
385
|
-
} : graph;
|
|
386
|
-
}
|
|
387
|
-
function anchorGraph(graph, preferred) {
|
|
388
|
-
return graph.anchoredBy === "tags" ? graph : anchorFromPaths(graph, preferred);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// src/gfa/gfaConverter.ts
|
|
392
|
-
function surveySegments(gfaGraph) {
|
|
393
|
-
const canonical = /* @__PURE__ */ new Map();
|
|
394
|
-
const traversals = /* @__PURE__ */ new Map();
|
|
395
|
-
function claim(id, strand) {
|
|
396
|
-
if (!canonical.has(id)) {
|
|
397
|
-
canonical.set(id, strand);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
function traverse(id, strand) {
|
|
401
|
-
claim(id, strand);
|
|
402
|
-
traversals.set(id, (traversals.get(id) ?? 0) + 1);
|
|
403
|
-
}
|
|
404
|
-
for (const link of gfaGraph.links) {
|
|
405
|
-
claim(link.source, link.strand1 === "-" ? "-" : "+");
|
|
406
|
-
claim(link.target, link.strand2 === "-" ? "-" : "+");
|
|
407
|
-
}
|
|
408
|
-
for (const gfaPath of gfaGraph.paths) {
|
|
409
|
-
for (const segment of gfaPath.path.split(",")) {
|
|
410
|
-
traverse(segment.slice(0, -1), segment.endsWith("-") ? "-" : "+");
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
for (const walk of gfaGraph.walks) {
|
|
414
|
-
for (const seg of walk.segments) {
|
|
415
|
-
traverse(seg.id, seg.strand === "-" ? "-" : "+");
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return { canonical, traversals };
|
|
419
|
-
}
|
|
420
|
-
function anchorablePaths(gfaGraph) {
|
|
421
|
-
return [
|
|
422
|
-
...gfaGraph.paths.map((p) => ({
|
|
423
|
-
...pathOrigin(p.name),
|
|
424
|
-
steps: p.path.split(",").map((segment) => ({
|
|
425
|
-
id: segment.slice(0, -1),
|
|
426
|
-
strand: segment.endsWith("-") ? "-" : "+"
|
|
427
|
-
}))
|
|
428
|
-
})),
|
|
429
|
-
...gfaGraph.walks.map((w) => ({
|
|
430
|
-
name: `${w.sample}#${w.haplotype}#${w.contig}`,
|
|
431
|
-
// `*` parses to -1, meaning the record declines to say; 0 is then the
|
|
432
|
-
// only offset that can be assumed, and it makes the walk self-relative
|
|
433
|
-
// rather than wrong
|
|
434
|
-
start: w.start === -1 ? 0 : w.start,
|
|
435
|
-
steps: w.segments.map((s) => ({
|
|
436
|
-
id: s.id,
|
|
437
|
-
strand: s.strand === "-" ? "-" : "+"
|
|
438
|
-
}))
|
|
439
|
-
}))
|
|
440
|
-
];
|
|
441
|
-
}
|
|
442
|
-
function carriedSamples(gfaNode) {
|
|
443
|
-
const sm = gfaNode.tags.SM;
|
|
444
|
-
if (typeof sm !== "string") {
|
|
445
|
-
return void 0;
|
|
446
|
-
}
|
|
447
|
-
const samples = sm.split(",").map((s) => s.trim()).filter(Boolean);
|
|
448
|
-
return samples.length > 0 ? samples : void 0;
|
|
449
|
-
}
|
|
450
|
-
var positive = (v) => typeof v === "number" && v > 0 ? v : void 0;
|
|
451
|
-
function statedDepth({ tags, length }) {
|
|
452
|
-
const count = positive(tags.RC) ?? positive(tags.FC) ?? positive(tags.KC);
|
|
453
|
-
return positive(tags.dp) ?? positive(tags.DP) ?? (count !== void 0 && length > 0 ? count / length : void 0);
|
|
454
|
-
}
|
|
455
|
-
function makeNode(gfaNode, strand, depth) {
|
|
456
|
-
return {
|
|
457
|
-
id: `${gfaNode.id}${strand}`,
|
|
458
|
-
name: gfaNode.id,
|
|
459
|
-
length: gfaNode.length,
|
|
460
|
-
depth,
|
|
461
|
-
stable: stableCoordinate(gfaNode),
|
|
462
|
-
samples: carriedSamples(gfaNode)
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
function convertGFAToGraph(gfaGraph, name = "Imported GFA") {
|
|
466
|
-
const nodes = [];
|
|
467
|
-
const edges = [];
|
|
468
|
-
const { canonical, traversals } = surveySegments(gfaGraph);
|
|
469
|
-
const nodeId = (segmentId) => `${segmentId}${canonical.get(segmentId) ?? "+"}`;
|
|
470
|
-
for (const gfaNode of gfaGraph.nodes) {
|
|
471
|
-
const depth = statedDepth(gfaNode) ?? Math.max(traversals.get(gfaNode.id) ?? 1, 1);
|
|
472
|
-
nodes.push(makeNode(gfaNode, canonical.get(gfaNode.id) ?? "+", depth));
|
|
473
|
-
}
|
|
474
|
-
for (const link of gfaGraph.links) {
|
|
475
|
-
edges.push({
|
|
476
|
-
from: nodeId(link.source),
|
|
477
|
-
to: nodeId(link.target),
|
|
478
|
-
fromStrand: link.strand1 === "-" ? "-" : "+",
|
|
479
|
-
toStrand: link.strand2 === "-" ? "-" : "+"
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
const paths = [];
|
|
483
|
-
const edgeToPathsMap = /* @__PURE__ */ new Map();
|
|
484
|
-
const edgeKey = (a, b) => [a, b].sort().join("--");
|
|
485
|
-
function recordPathEdges(nodeIds, name2) {
|
|
486
|
-
for (let i = 0; i < nodeIds.length - 1; i++) {
|
|
487
|
-
const key = edgeKey(nodeIds[i], nodeIds[i + 1]);
|
|
488
|
-
if (!edgeToPathsMap.has(key)) {
|
|
489
|
-
edgeToPathsMap.set(key, /* @__PURE__ */ new Set());
|
|
490
|
-
}
|
|
491
|
-
edgeToPathsMap.get(key).add(name2);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
for (const gfaPath of gfaGraph.paths) {
|
|
495
|
-
const nodeIds = gfaPath.path.split(",").map((segment) => nodeId(segment.slice(0, -1)));
|
|
496
|
-
paths.push({ name: gfaPath.name, nodeIds });
|
|
497
|
-
recordPathEdges(nodeIds, gfaPath.name);
|
|
498
|
-
}
|
|
499
|
-
for (const walk of gfaGraph.walks) {
|
|
500
|
-
const nodeIds = walk.segments.map((seg) => nodeId(seg.id));
|
|
501
|
-
const name2 = `${walk.sample}#${walk.haplotype}#${walk.contig}`;
|
|
502
|
-
paths.push({
|
|
503
|
-
name: name2,
|
|
504
|
-
nodeIds,
|
|
505
|
-
sample: walk.sample,
|
|
506
|
-
haplotype: walk.haplotype,
|
|
507
|
-
contig: walk.contig
|
|
508
|
-
});
|
|
509
|
-
recordPathEdges(nodeIds, name2);
|
|
510
|
-
}
|
|
511
|
-
for (const edge of edges) {
|
|
512
|
-
const pathIds = edgeToPathsMap.get(edgeKey(edge.from, edge.to));
|
|
513
|
-
if (pathIds && pathIds.size > 0) {
|
|
514
|
-
edge.pathIds = [...pathIds];
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
const lengths = new Map(gfaGraph.nodes.map((n) => [n.id, n.length]));
|
|
518
|
-
const anchoring = gfaGraph.paths.length > 0 || gfaGraph.walks.length > 0 ? surveyPaths(anchorablePaths(gfaGraph), (id) => lengths.get(id) ?? 0) : void 0;
|
|
519
|
-
return {
|
|
520
|
-
name,
|
|
521
|
-
nodes,
|
|
522
|
-
edges,
|
|
523
|
-
paths: paths.length > 0 ? paths : void 0,
|
|
524
|
-
anchorPaths: anchoring?.anchorPaths,
|
|
525
|
-
pathVisits: anchoring?.pathVisits,
|
|
526
|
-
// rGFA's own tags win wherever they are present; pathAnchoring only ever
|
|
527
|
-
// fills in for a graph that carries none.
|
|
528
|
-
anchoredBy: nodes.some((n) => n.stable) ? "tags" : void 0
|
|
529
|
-
};
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// src/layout/mergeRuns.ts
|
|
533
|
-
function ownStrand(id) {
|
|
534
|
-
return id.endsWith("-") ? "-" : "+";
|
|
535
|
-
}
|
|
536
|
-
function flip(strand) {
|
|
537
|
-
return strand === "+" ? "-" : "+";
|
|
538
|
-
}
|
|
539
|
-
function sidesOf(edge) {
|
|
540
|
-
const from = (edge.fromStrand ?? ownStrand(edge.from)) === ownStrand(edge.from) ? "end" : "start";
|
|
541
|
-
const to = (edge.toStrand ?? ownStrand(edge.to)) === ownStrand(edge.to) ? "start" : "end";
|
|
542
|
-
return { from, to };
|
|
543
|
-
}
|
|
544
|
-
function mergeRuns(graph) {
|
|
545
|
-
const sides2 = graph.edges.map(sidesOf);
|
|
546
|
-
const at = /* @__PURE__ */ new Map();
|
|
547
|
-
const attach = (id, side, ei) => {
|
|
548
|
-
const key = `${id}|${side}`;
|
|
549
|
-
const list = at.get(key);
|
|
550
|
-
if (list) {
|
|
551
|
-
list.push(ei);
|
|
552
|
-
} else {
|
|
553
|
-
at.set(key, [ei]);
|
|
554
|
-
}
|
|
555
|
-
};
|
|
556
|
-
graph.edges.forEach((edge, ei) => {
|
|
557
|
-
attach(edge.from, sides2[ei].from, ei);
|
|
558
|
-
attach(edge.to, sides2[ei].to, ei);
|
|
559
|
-
});
|
|
560
|
-
const joinAfter = (a) => {
|
|
561
|
-
const out = at.get(`${a}|end`);
|
|
562
|
-
if (out?.length !== 1) {
|
|
563
|
-
return void 0;
|
|
564
|
-
}
|
|
565
|
-
const ei = out[0];
|
|
566
|
-
const edge = graph.edges[ei];
|
|
567
|
-
return edge.from === a && edge.to !== a && sides2[ei].from === "end" && sides2[ei].to === "start" && at.get(`${edge.to}|start`)?.length === 1 ? ei : void 0;
|
|
568
|
-
};
|
|
569
|
-
const joined = /* @__PURE__ */ new Set();
|
|
570
|
-
const runOf = /* @__PURE__ */ new Map();
|
|
571
|
-
const runs = /* @__PURE__ */ new Map();
|
|
572
|
-
for (const node of graph.nodes) {
|
|
573
|
-
const into = at.get(`${node.id}|start`);
|
|
574
|
-
const pred = into?.length === 1 ? graph.edges[into[0]].from : void 0;
|
|
575
|
-
if (runOf.has(node.id) || pred !== void 0 && joinAfter(pred) === into[0]) {
|
|
576
|
-
continue;
|
|
577
|
-
}
|
|
578
|
-
const run = [node.id];
|
|
579
|
-
for (; ; ) {
|
|
580
|
-
const ei = joinAfter(run.at(-1));
|
|
581
|
-
const next = ei === void 0 ? void 0 : graph.edges[ei].to;
|
|
582
|
-
if (next === void 0 || runOf.has(next)) {
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
joined.add(ei);
|
|
586
|
-
run.push(next);
|
|
587
|
-
}
|
|
588
|
-
for (const id of run) {
|
|
589
|
-
runOf.set(id, node.id);
|
|
590
|
-
}
|
|
591
|
-
if (run.length > 1) {
|
|
592
|
-
runs.set(node.id, run);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
for (const node of graph.nodes) {
|
|
596
|
-
if (!runOf.has(node.id)) {
|
|
597
|
-
runOf.set(node.id, node.id);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
601
|
-
const nodes = [];
|
|
602
|
-
for (const node of graph.nodes) {
|
|
603
|
-
if (runOf.get(node.id) !== node.id) {
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
const run = runs.get(node.id);
|
|
607
|
-
if (!run) {
|
|
608
|
-
nodes.push(node);
|
|
609
|
-
continue;
|
|
610
|
-
}
|
|
611
|
-
const members = run.map((id) => byId.get(id));
|
|
612
|
-
const backbone = members.filter(isBackbone);
|
|
613
|
-
const first = backbone.length ? backbone.reduce((a, b) => a.stable.start <= b.stable.start ? a : b) : members[0];
|
|
614
|
-
nodes.push({
|
|
615
|
-
id: node.id,
|
|
616
|
-
name: node.name,
|
|
617
|
-
length: members.reduce((sum, n) => sum + n.length, 0),
|
|
618
|
-
depth: node.depth,
|
|
619
|
-
stable: first.stable
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
const seen = /* @__PURE__ */ new Set();
|
|
623
|
-
const edges = [];
|
|
624
|
-
graph.edges.forEach((edge, ei) => {
|
|
625
|
-
const from = runOf.get(edge.from);
|
|
626
|
-
const to = runOf.get(edge.to);
|
|
627
|
-
const side = sides2[ei];
|
|
628
|
-
const key = `${from}|${side.from}>${to}|${side.to}`;
|
|
629
|
-
if (joined.has(ei) || seen.has(key)) {
|
|
630
|
-
return;
|
|
631
|
-
}
|
|
632
|
-
seen.add(key);
|
|
633
|
-
edges.push({
|
|
634
|
-
from,
|
|
635
|
-
to,
|
|
636
|
-
...side.from === "start" ? { fromStrand: flip(ownStrand(from)) } : {},
|
|
637
|
-
...side.to === "end" ? { toStrand: flip(ownStrand(to)) } : {}
|
|
638
|
-
});
|
|
639
|
-
});
|
|
640
|
-
return {
|
|
641
|
-
graph: { ...graph, nodes, edges, paths: void 0, pathVisits: void 0 },
|
|
642
|
-
runs
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
function splitRuns(positions, runs, weightOf) {
|
|
646
|
-
const out = { ...positions };
|
|
647
|
-
for (const [id, members] of runs) {
|
|
648
|
-
const line = positions[id];
|
|
649
|
-
if (!line?.length) {
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
delete out[id];
|
|
653
|
-
const arc = arcPrefix(line);
|
|
654
|
-
const total = arc.at(-1);
|
|
655
|
-
const weights = members.map((m) => Math.max(weightOf(m), 0));
|
|
656
|
-
const weightSum = weights.reduce((a, b) => a + b, 0) || members.length;
|
|
657
|
-
let from = 0;
|
|
658
|
-
members.forEach((member, k) => {
|
|
659
|
-
const share = weightSum ? weights[k] / weightSum * total : total / members.length;
|
|
660
|
-
const to = k === members.length - 1 ? total : from + share;
|
|
661
|
-
out[member] = slice(line, arc, from, to);
|
|
662
|
-
from = to;
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
return out;
|
|
666
|
-
}
|
|
667
|
-
function polylineSlice(line, from, to) {
|
|
668
|
-
if (line.length === 0) {
|
|
669
|
-
return [];
|
|
670
|
-
}
|
|
671
|
-
const arc = arcPrefix(line);
|
|
672
|
-
const total = arc.at(-1);
|
|
673
|
-
return slice(line, arc, from * total, to * total);
|
|
674
|
-
}
|
|
675
|
-
function polylineMidpoint(line) {
|
|
676
|
-
const arc = arcPrefix(line);
|
|
677
|
-
return pointAt(line, arc, arc.at(-1) / 2);
|
|
678
|
-
}
|
|
679
|
-
function arcPrefix(line) {
|
|
680
|
-
const arc = [0];
|
|
681
|
-
for (let i = 1; i < line.length; i++) {
|
|
682
|
-
arc.push(
|
|
683
|
-
arc[i - 1] + Math.hypot(line[i].x - line[i - 1].x, line[i].y - line[i - 1].y)
|
|
684
|
-
);
|
|
685
|
-
}
|
|
686
|
-
return arc;
|
|
687
|
-
}
|
|
688
|
-
function pointAt(line, arc, s) {
|
|
689
|
-
if (s <= 0) {
|
|
690
|
-
return { ...line[0] };
|
|
691
|
-
}
|
|
692
|
-
let i = 1;
|
|
693
|
-
while (i < arc.length && arc[i] < s) {
|
|
694
|
-
i++;
|
|
695
|
-
}
|
|
696
|
-
if (i >= arc.length) {
|
|
697
|
-
return { ...line.at(-1) };
|
|
698
|
-
}
|
|
699
|
-
const span2 = arc[i] - arc[i - 1];
|
|
700
|
-
const t = span2 > 0 ? (s - arc[i - 1]) / span2 : 0;
|
|
701
|
-
const a = line[i - 1];
|
|
702
|
-
const b = line[i];
|
|
703
|
-
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
|
704
|
-
}
|
|
705
|
-
function slice(line, arc, from, to) {
|
|
706
|
-
const points = [pointAt(line, arc, from)];
|
|
707
|
-
for (let i = 0; i < line.length; i++) {
|
|
708
|
-
if (arc[i] > from && arc[i] < to) {
|
|
709
|
-
points.push({ ...line[i] });
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
points.push(pointAt(line, arc, to));
|
|
713
|
-
return points;
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// src/layout/orientToReference.ts
|
|
717
|
-
function orientToReference(graph, positions) {
|
|
718
|
-
const backbone = graph.nodes.filter(
|
|
719
|
-
(node) => isBackbone(node) && (positions[node.id]?.length ?? 0) > 0
|
|
720
|
-
);
|
|
721
|
-
if (backbone.length < 2) {
|
|
722
|
-
return positions;
|
|
723
|
-
}
|
|
724
|
-
const points = backbone.map((node) => {
|
|
725
|
-
const segments = positions[node.id];
|
|
726
|
-
const mid = segments[Math.floor(segments.length / 2)];
|
|
727
|
-
return {
|
|
728
|
-
x: mid.x,
|
|
729
|
-
y: mid.y,
|
|
730
|
-
r: node.stable.start + node.length / 2,
|
|
731
|
-
w: node.length
|
|
732
|
-
};
|
|
733
|
-
});
|
|
734
|
-
const weight = points.reduce((sum, p) => sum + p.w, 0);
|
|
735
|
-
const cx = points.reduce((sum, p) => sum + p.x * p.w, 0) / weight;
|
|
736
|
-
const cy = points.reduce((sum, p) => sum + p.y * p.w, 0) / weight;
|
|
737
|
-
const cr = points.reduce((sum, p) => sum + p.r * p.w, 0) / weight;
|
|
738
|
-
let sxr = 0;
|
|
739
|
-
let syr = 0;
|
|
740
|
-
for (const p of points) {
|
|
741
|
-
sxr += p.w * (p.x - cx) * (p.r - cr);
|
|
742
|
-
syr += p.w * (p.y - cy) * (p.r - cr);
|
|
743
|
-
}
|
|
744
|
-
const angle = -Math.atan2(syr, sxr);
|
|
745
|
-
const c = Math.cos(angle);
|
|
746
|
-
const s = Math.sin(angle);
|
|
747
|
-
const out = {};
|
|
748
|
-
for (const [id, segments] of Object.entries(positions)) {
|
|
749
|
-
out[id] = segments.map((p) => {
|
|
750
|
-
const x = p.x - cx;
|
|
751
|
-
const y = p.y - cy;
|
|
752
|
-
return { x: cx + c * x - s * y, y: cy + s * x + c * y };
|
|
753
|
-
});
|
|
754
|
-
}
|
|
755
|
-
let below = 0;
|
|
756
|
-
let above = 0;
|
|
757
|
-
for (const node of graph.nodes) {
|
|
758
|
-
if (isBackbone(node)) {
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
for (const p of out[node.id] ?? []) {
|
|
762
|
-
if (p.y > cy) {
|
|
763
|
-
below++;
|
|
764
|
-
} else {
|
|
765
|
-
above++;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
if (above > below) {
|
|
770
|
-
for (const segments of Object.values(out)) {
|
|
771
|
-
for (const p of segments) {
|
|
772
|
-
p.y = 2 * cy - p.y;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
return out;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// src/referenceSpan.ts
|
|
780
|
-
var MAX_BACKBONE_HOPS = 4;
|
|
781
|
-
function buildNeighbors(graph) {
|
|
782
|
-
const neighbors = /* @__PURE__ */ new Map();
|
|
783
|
-
function add(from, to) {
|
|
784
|
-
const list = neighbors.get(from);
|
|
785
|
-
if (list) {
|
|
786
|
-
list.push(to);
|
|
787
|
-
} else {
|
|
788
|
-
neighbors.set(from, [to]);
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
for (const edge of graph.edges) {
|
|
792
|
-
add(edge.from, edge.to);
|
|
793
|
-
add(edge.to, edge.from);
|
|
794
|
-
}
|
|
795
|
-
return neighbors;
|
|
796
|
-
}
|
|
797
|
-
function flankingSpan({
|
|
798
|
-
nodeId,
|
|
799
|
-
nodeById,
|
|
800
|
-
neighbors
|
|
801
|
-
}) {
|
|
802
|
-
const seen = /* @__PURE__ */ new Set([nodeId]);
|
|
803
|
-
let frontier = [nodeId];
|
|
804
|
-
let minEnd = Infinity;
|
|
805
|
-
let maxStart = -Infinity;
|
|
806
|
-
for (let hop = 0; hop < MAX_BACKBONE_HOPS && frontier.length > 0; hop++) {
|
|
807
|
-
const next = [];
|
|
808
|
-
for (const id of frontier) {
|
|
809
|
-
const adjacent = neighbors.get(id);
|
|
810
|
-
if (adjacent) {
|
|
811
|
-
for (const neighborId of adjacent) {
|
|
812
|
-
if (!seen.has(neighborId)) {
|
|
813
|
-
seen.add(neighborId);
|
|
814
|
-
const neighbor = nodeById.get(neighborId);
|
|
815
|
-
if (neighbor && isBackbone(neighbor)) {
|
|
816
|
-
minEnd = Math.min(minEnd, neighbor.stable.start + neighbor.length);
|
|
817
|
-
maxStart = Math.max(maxStart, neighbor.stable.start);
|
|
818
|
-
} else {
|
|
819
|
-
next.push(neighborId);
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
frontier = next;
|
|
826
|
-
}
|
|
827
|
-
return minEnd === Infinity ? void 0 : {
|
|
828
|
-
start: Math.min(minEnd, maxStart),
|
|
829
|
-
end: Math.max(minEnd, maxStart)
|
|
830
|
-
};
|
|
831
|
-
}
|
|
832
|
-
function nodeReferenceSpan({
|
|
833
|
-
nodeId,
|
|
834
|
-
nodeById,
|
|
835
|
-
neighbors
|
|
836
|
-
}) {
|
|
837
|
-
const node = nodeById.get(nodeId);
|
|
838
|
-
return node === void 0 ? void 0 : isBackbone(node) ? { start: node.stable.start, end: node.stable.start + node.length } : flankingSpan({ nodeId, nodeById, neighbors });
|
|
839
|
-
}
|
|
840
|
-
function referenceMidpoints(graph) {
|
|
841
|
-
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
842
|
-
const neighbors = buildNeighbors(graph);
|
|
843
|
-
const midpoints = /* @__PURE__ */ new Map();
|
|
844
|
-
for (const node of graph.nodes) {
|
|
845
|
-
const span2 = nodeReferenceSpan({ nodeId: node.id, nodeById, neighbors });
|
|
846
|
-
if (span2) {
|
|
847
|
-
midpoints.set(node.id, (span2.start + span2.end) / 2);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
return midpoints;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// src/layout/referenceSeeds.ts
|
|
854
|
-
var LANE_GAP = 30;
|
|
855
|
-
function referenceSeeds(graph, scaling) {
|
|
856
|
-
const { opts } = scaling;
|
|
857
|
-
const drawn = new Map(
|
|
858
|
-
scaling.nodes.map((node) => [node.id, drawnNodeLength(opts, node.length)])
|
|
859
|
-
);
|
|
860
|
-
const drawnOf = (id) => drawn.get(id) ?? opts.minimumNodeLength;
|
|
861
|
-
const backbone = graph.nodes.filter(isBackbone).sort((a, b) => a.stable.start - b.stable.start);
|
|
862
|
-
const seeds = /* @__PURE__ */ new Map();
|
|
863
|
-
let x = 0;
|
|
864
|
-
for (const node of backbone) {
|
|
865
|
-
seeds.set(node.id, { x, y: 0 });
|
|
866
|
-
x += drawnOf(node.id) + opts.edgeLength;
|
|
867
|
-
}
|
|
868
|
-
const adjacent = buildNeighbors(graph);
|
|
869
|
-
const depth = new Map(backbone.map((node) => [node.id, 0]));
|
|
870
|
-
const queue = backbone.map((node) => node.id);
|
|
871
|
-
for (const id of queue) {
|
|
872
|
-
const at = seeds.get(id);
|
|
873
|
-
const next = depth.get(id) + 1;
|
|
874
|
-
for (const neighbour of adjacent.get(id) ?? []) {
|
|
875
|
-
if (depth.has(neighbour)) {
|
|
876
|
-
continue;
|
|
877
|
-
}
|
|
878
|
-
depth.set(neighbour, next);
|
|
879
|
-
seeds.set(neighbour, { x: at.x + drawnOf(id) / 2, y: LANE_GAP * next });
|
|
880
|
-
queue.push(neighbour);
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
let unreached = 0;
|
|
884
|
-
for (const node of graph.nodes) {
|
|
885
|
-
if (!seeds.has(node.id)) {
|
|
886
|
-
seeds.set(node.id, {
|
|
887
|
-
x: unreached++ * x / graph.nodes.length,
|
|
888
|
-
y: LANE_GAP * 3
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
return seeds;
|
|
893
|
-
}
|
|
894
|
-
function seededNodes(graph, scaling) {
|
|
895
|
-
const seeds = referenceSeeds(graph, scaling);
|
|
896
|
-
return scaling.nodes.map((node) => ({ ...node, ...seeds.get(node.id) }));
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// src/pipeline.ts
|
|
900
|
-
function loadGraph(text, name, opts = {}) {
|
|
901
|
-
const graph = anchorGraph(
|
|
902
|
-
convertGFAToGraph(parseGFA(text), name),
|
|
903
|
-
opts.referencePath
|
|
904
|
-
);
|
|
905
|
-
if (graph.nodes.length === 0) {
|
|
906
|
-
throw new Error(`No graph segments in ${name}`);
|
|
907
|
-
}
|
|
908
|
-
if (opts.maxNodes !== void 0 && graph.nodes.length > opts.maxNodes) {
|
|
909
|
-
throw new Error(
|
|
910
|
-
`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.`
|
|
911
|
-
);
|
|
912
|
-
}
|
|
913
|
-
return graph;
|
|
914
|
-
}
|
|
915
|
-
function engineRequest(graph, scaling, settings) {
|
|
916
|
-
const anchored = graph.nodes.some(isBackbone);
|
|
917
|
-
return {
|
|
918
|
-
graph: {
|
|
919
|
-
nodes: anchored ? seededNodes(graph, scaling) : scaling.nodes,
|
|
920
|
-
edges: graph.edges
|
|
921
|
-
},
|
|
922
|
-
options: {
|
|
923
|
-
quality: settings.quality,
|
|
924
|
-
linearLayout: settings.linearLayout,
|
|
925
|
-
...scaling.opts,
|
|
926
|
-
...anchored ? { rotateComponents: false } : {}
|
|
927
|
-
}
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
function engineKey(graph, settings) {
|
|
931
|
-
const anchored = graph.nodes.some(isBackbone);
|
|
932
|
-
return `${settings.quality}|${settings.linearLayout}|${settings.bubbleSpread}|${anchored}|${graph.referencePath ?? ""}`;
|
|
933
|
-
}
|
|
934
|
-
async function forceLayout(graph, settings, engine) {
|
|
935
|
-
const spread = spreadFor(settings.bubbleSpread);
|
|
936
|
-
const merged = mergeRuns(graph);
|
|
937
|
-
const { result, duration } = await engine(
|
|
938
|
-
engineRequest(merged.graph, layoutScaling(merged.graph, spread), settings)
|
|
939
|
-
);
|
|
940
|
-
const scaling = layoutScaling(graph, spread);
|
|
941
|
-
const drawn = new Map(
|
|
942
|
-
scaling.nodes.map((n) => [n.id, drawnNodeLength(scaling.opts, n.length)])
|
|
943
|
-
);
|
|
944
|
-
const positions = splitRuns(
|
|
945
|
-
result.nodePositions,
|
|
946
|
-
merged.runs,
|
|
947
|
-
(id) => drawn.get(id) ?? 0
|
|
948
|
-
);
|
|
949
|
-
return {
|
|
950
|
-
result: {
|
|
951
|
-
...result,
|
|
952
|
-
nodePositions: graph.nodes.some(isBackbone) ? orientToReference(graph, positions) : positions
|
|
953
|
-
},
|
|
954
|
-
duration
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
function layoutExtent(nodePositions) {
|
|
958
|
-
let minX = Infinity;
|
|
959
|
-
let minY = Infinity;
|
|
960
|
-
let maxX = -Infinity;
|
|
961
|
-
let maxY = -Infinity;
|
|
962
|
-
for (const segments of Object.values(nodePositions)) {
|
|
963
|
-
for (const seg of segments) {
|
|
964
|
-
minX = Math.min(minX, seg.x);
|
|
965
|
-
minY = Math.min(minY, seg.y);
|
|
966
|
-
maxX = Math.max(maxX, seg.x);
|
|
967
|
-
maxY = Math.max(maxY, seg.y);
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
return { minX, minY, maxX, maxY };
|
|
971
|
-
}
|
|
972
|
-
var FIT_PADDING = 40;
|
|
973
|
-
var MIN_ZOOM = 1e-6;
|
|
974
|
-
var MAX_ZOOM = 100;
|
|
975
|
-
function clampZoom(zoom) {
|
|
976
|
-
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, zoom));
|
|
977
|
-
}
|
|
978
|
-
function fittedTranslateY(bounds, usableHeight, yScale, padTop = FIT_PADDING) {
|
|
979
|
-
const leftoverY = usableHeight - bounds.h * yScale;
|
|
980
|
-
return padTop - bounds.minY * yScale + Math.max(0, leftoverY) / 2;
|
|
981
|
-
}
|
|
982
|
-
function fitTransform(bounds, paneWidth, paneHeight, pixelRows, {
|
|
983
|
-
minScale = 0,
|
|
984
|
-
padLeft = FIT_PADDING,
|
|
985
|
-
padTop = FIT_PADDING,
|
|
986
|
-
padRight = FIT_PADDING,
|
|
987
|
-
padBottom = FIT_PADDING
|
|
988
|
-
} = {}) {
|
|
989
|
-
const usableWidth = paneWidth - padLeft - padRight;
|
|
990
|
-
const usableHeight = paneHeight - padTop - padBottom;
|
|
991
|
-
if (usableWidth <= 0 || usableHeight <= 0 || !(bounds.w > 0 || !pixelRows && bounds.h > 0)) {
|
|
992
|
-
return void 0;
|
|
993
|
-
}
|
|
994
|
-
const fitX = bounds.w > 0 ? usableWidth / bounds.w : Infinity;
|
|
995
|
-
const fitY = bounds.h > 0 ? usableHeight / bounds.h : Infinity;
|
|
996
|
-
const scale = clampZoom(
|
|
997
|
-
Math.max(minScale, pixelRows ? fitX : Math.min(fitX, fitY))
|
|
998
|
-
);
|
|
999
|
-
const leftoverX = usableWidth - bounds.w * scale;
|
|
1000
|
-
return {
|
|
1001
|
-
scale,
|
|
1002
|
-
translateX: padLeft - bounds.minX * scale + Math.max(0, leftoverX) / 2,
|
|
1003
|
-
translateY: fittedTranslateY(
|
|
1004
|
-
bounds,
|
|
1005
|
-
usableHeight,
|
|
1006
|
-
pixelRows ? 1 : scale,
|
|
1007
|
-
padTop
|
|
1008
|
-
)
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
function drawingBounds(layout, opts = {}) {
|
|
1012
|
-
let { minX, minY, maxX, maxY } = layoutExtent(layout.nodePositions);
|
|
1013
|
-
const { region } = opts;
|
|
1014
|
-
if (layout.referenceAxis && region && region.end > region.start) {
|
|
1015
|
-
minX = region.start;
|
|
1016
|
-
maxX = region.end;
|
|
1017
|
-
}
|
|
1018
|
-
const extent = opts.extent ?? layout.extent;
|
|
1019
|
-
if (extent) {
|
|
1020
|
-
minX = Math.min(minX, extent.minX ?? minX);
|
|
1021
|
-
minY = Math.min(minY, extent.minY ?? minY);
|
|
1022
|
-
maxX = Math.max(maxX, extent.maxX ?? maxX);
|
|
1023
|
-
maxY = Math.max(maxY, extent.maxY ?? maxY);
|
|
1024
|
-
}
|
|
1025
|
-
return { minX, minY, w: maxX - minX, h: maxY - minY };
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
// src/alleleProjection/projectAlleles.ts
|
|
1029
|
-
function parsePanSN(refName) {
|
|
1030
|
-
const parts = refName.split("#");
|
|
1031
|
-
const haplotype = parts.length >= 2 ? Number(parts[1]) : Number.NaN;
|
|
1032
|
-
return {
|
|
1033
|
-
sample: parts[0] ?? refName,
|
|
1034
|
-
haplotype: Number.isFinite(haplotype) ? haplotype : void 0
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
function collectRun(seed, byId, adj, claimed) {
|
|
1038
|
-
const members = [];
|
|
1039
|
-
const anchors = [];
|
|
1040
|
-
const queue = [seed];
|
|
1041
|
-
claimed.add(seed);
|
|
1042
|
-
while (queue.length > 0) {
|
|
1043
|
-
const id = queue.pop();
|
|
1044
|
-
const node = byId.get(id);
|
|
1045
|
-
if (node) {
|
|
1046
|
-
members.push(node);
|
|
1047
|
-
for (const neighborId of adj.get(id) ?? []) {
|
|
1048
|
-
const neighbor = byId.get(neighborId);
|
|
1049
|
-
if (neighbor) {
|
|
1050
|
-
if (neighbor.stable.rank === REFERENCE_RANK) {
|
|
1051
|
-
anchors.push(neighbor);
|
|
1052
|
-
} else if (!claimed.has(neighborId)) {
|
|
1053
|
-
claimed.add(neighborId);
|
|
1054
|
-
queue.push(neighborId);
|
|
1055
|
-
}
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
return { members, anchors };
|
|
1061
|
-
}
|
|
1062
|
-
function anchorSpan(anchors) {
|
|
1063
|
-
const refNames = new Set(anchors.map((a) => a.stable.refName));
|
|
1064
|
-
if (refNames.size !== 1 || anchors.length < 2) {
|
|
1065
|
-
return void 0;
|
|
1066
|
-
}
|
|
1067
|
-
const sorted = [...anchors].sort((a, b) => a.stable.start - b.stable.start);
|
|
1068
|
-
const first = sorted[0];
|
|
1069
|
-
const last = sorted.at(-1);
|
|
1070
|
-
const start = first.stable.start + first.length;
|
|
1071
|
-
const end = last.stable.start;
|
|
1072
|
-
return end >= start ? { start, end, entry: first } : void 0;
|
|
1073
|
-
}
|
|
1074
|
-
function runDistances(members, entry, adj) {
|
|
1075
|
-
const byId = new Map(members.map((m) => [m.id, m]));
|
|
1076
|
-
const dist = /* @__PURE__ */ new Map();
|
|
1077
|
-
const queue = [];
|
|
1078
|
-
for (const neighborId of adj.get(entry.id) ?? []) {
|
|
1079
|
-
if (byId.has(neighborId)) {
|
|
1080
|
-
dist.set(neighborId, 0);
|
|
1081
|
-
queue.push(neighborId);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
if (queue.length === 0) {
|
|
1085
|
-
for (const member of members) {
|
|
1086
|
-
dist.set(member.id, 0);
|
|
1087
|
-
queue.push(member.id);
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
for (const id of queue) {
|
|
1091
|
-
const reach = dist.get(id) + byId.get(id).length;
|
|
1092
|
-
for (const neighborId of adj.get(id) ?? []) {
|
|
1093
|
-
if (byId.has(neighborId)) {
|
|
1094
|
-
const known = dist.get(neighborId);
|
|
1095
|
-
if (known === void 0 || reach < known) {
|
|
1096
|
-
dist.set(neighborId, reach);
|
|
1097
|
-
queue.push(neighborId);
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
return dist;
|
|
1103
|
-
}
|
|
1104
|
-
function runOffsets(members, entry, adj) {
|
|
1105
|
-
const dist = runDistances(members, entry, adj);
|
|
1106
|
-
const ordered = [...members].sort((a, b) => dist.get(a.id) - dist.get(b.id));
|
|
1107
|
-
const rank = new Map(ordered.map((m, i) => [m.id, i]));
|
|
1108
|
-
const byId = new Map(members.map((m) => [m.id, m]));
|
|
1109
|
-
const offsets = /* @__PURE__ */ new Map();
|
|
1110
|
-
for (const [i, member] of ordered.entries()) {
|
|
1111
|
-
let offset = 0;
|
|
1112
|
-
for (const neighborId of adj.get(member.id) ?? []) {
|
|
1113
|
-
const neighbor = byId.get(neighborId);
|
|
1114
|
-
if (neighbor && rank.get(neighborId) < i) {
|
|
1115
|
-
offset = Math.max(offset, offsets.get(neighborId) + neighbor.length);
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
offsets.set(member.id, offset);
|
|
1119
|
-
}
|
|
1120
|
-
return { offsets, ordered };
|
|
1121
|
-
}
|
|
1122
|
-
function projectAlleles(graph) {
|
|
1123
|
-
const byId = new Map(
|
|
1124
|
-
graph.nodes.filter(isAnchored).map((n) => [n.id, n])
|
|
1125
|
-
);
|
|
1126
|
-
const adj = buildNeighbors(graph);
|
|
1127
|
-
const claimed = /* @__PURE__ */ new Set();
|
|
1128
|
-
const alleles = [];
|
|
1129
|
-
for (const node of byId.values()) {
|
|
1130
|
-
if (node.stable.rank !== REFERENCE_RANK && !claimed.has(node.id)) {
|
|
1131
|
-
const { members, anchors } = collectRun(node.id, byId, adj, claimed);
|
|
1132
|
-
const span2 = anchorSpan(anchors);
|
|
1133
|
-
if (span2) {
|
|
1134
|
-
const { offsets, ordered } = runOffsets(members, span2.entry, adj);
|
|
1135
|
-
alleles.push({
|
|
1136
|
-
start: span2.start,
|
|
1137
|
-
refSpan: span2.end - span2.start,
|
|
1138
|
-
nodeIds: ordered.map((m) => m.id),
|
|
1139
|
-
nodeOffsets: ordered.map((m) => offsets.get(m.id)),
|
|
1140
|
-
pathLength: ordered.reduce(
|
|
1141
|
-
(max, m) => Math.max(max, offsets.get(m.id) + m.length),
|
|
1142
|
-
0
|
|
1143
|
-
)
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
return alleles.sort((a, b) => a.start - b.start);
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// src/layout/placeOffReference.ts
|
|
1152
|
-
var MIN_DELETION_BP = 1;
|
|
1153
|
-
var MIN_ALLELE_SPAN_FRACTION = 0.015;
|
|
1154
|
-
function placeOffReference({
|
|
1155
|
-
graph,
|
|
1156
|
-
span: span2,
|
|
1157
|
-
rowY,
|
|
1158
|
-
positions
|
|
1159
|
-
}) {
|
|
1160
|
-
const minSpan = span2 * MIN_ALLELE_SPAN_FRACTION;
|
|
1161
|
-
const alleles = projectAlleles(graph);
|
|
1162
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
1163
|
-
const alleleDeletions = [];
|
|
1164
|
-
for (const allele of alleles) {
|
|
1165
|
-
const skipped = allele.refSpan - allele.pathLength;
|
|
1166
|
-
if (skipped >= MIN_DELETION_BP) {
|
|
1167
|
-
alleleDeletions.push({ nodeIds: allele.nodeIds, bp: skipped });
|
|
1168
|
-
}
|
|
1169
|
-
const drawn = Math.max(allele.refSpan, minSpan);
|
|
1170
|
-
const scale = allele.pathLength > 0 ? drawn / allele.pathLength : 0;
|
|
1171
|
-
allele.nodeIds.forEach((nodeId, i) => {
|
|
1172
|
-
const node = byId.get(nodeId);
|
|
1173
|
-
if (node) {
|
|
1174
|
-
const offset = allele.nodeOffsets[i];
|
|
1175
|
-
const even = drawn / allele.nodeIds.length;
|
|
1176
|
-
const at = (bp) => allele.start + bp * scale;
|
|
1177
|
-
const y = rowY(node);
|
|
1178
|
-
positions[nodeId] = scale ? [
|
|
1179
|
-
{ x: at(offset), y },
|
|
1180
|
-
{ x: at(offset + node.length), y }
|
|
1181
|
-
] : [
|
|
1182
|
-
{ x: allele.start + i * even, y },
|
|
1183
|
-
{ x: allele.start + (i + 1) * even, y }
|
|
1184
|
-
];
|
|
1185
|
-
}
|
|
1186
|
-
});
|
|
1187
|
-
}
|
|
1188
|
-
const neighbors = buildNeighbors(graph);
|
|
1189
|
-
const queue = Object.keys(positions);
|
|
1190
|
-
for (const fromId of queue) {
|
|
1191
|
-
const fromEnd = positions[fromId].at(-1).x;
|
|
1192
|
-
for (const nextId of neighbors.get(fromId) ?? []) {
|
|
1193
|
-
const next = byId.get(nextId);
|
|
1194
|
-
if (next && !(nextId in positions)) {
|
|
1195
|
-
const y = rowY(next);
|
|
1196
|
-
positions[nextId] = [
|
|
1197
|
-
{ x: fromEnd, y },
|
|
1198
|
-
{ x: fromEnd + minSpan, y }
|
|
1199
|
-
];
|
|
1200
|
-
queue.push(nextId);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
return alleleDeletions;
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
// src/layout/rowSpacing.ts
|
|
1208
|
-
var ROW_HEIGHT_PX = 20;
|
|
1209
|
-
|
|
1210
|
-
// src/layout/anchoredLayout.ts
|
|
1211
|
-
function rankRows(graph) {
|
|
1212
|
-
const present = /* @__PURE__ */ new Set();
|
|
1213
|
-
for (const node of graph.nodes) {
|
|
1214
|
-
if (node.stable) {
|
|
1215
|
-
present.add(node.stable.rank);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
return new Map([...present].sort((a, b) => a - b).map((r, i) => [r, i]));
|
|
1219
|
-
}
|
|
1220
|
-
function anchoredLayout(graph, region) {
|
|
1221
|
-
const backbone = backboneNodes(graph);
|
|
1222
|
-
if (backbone.length === 0) {
|
|
1223
|
-
return void 0;
|
|
1224
|
-
}
|
|
1225
|
-
const span2 = referenceSpan(backbone, region);
|
|
1226
|
-
const rows = rankRows(graph);
|
|
1227
|
-
const nodePositions = backbonePositions(backbone);
|
|
1228
|
-
const alleleDeletions = placeOffReference({
|
|
1229
|
-
graph,
|
|
1230
|
-
span: span2,
|
|
1231
|
-
rowY: (node) => (node.stable ? rows.get(node.stable.rank) ?? rows.size : rows.size) * ROW_HEIGHT_PX,
|
|
1232
|
-
positions: nodePositions
|
|
1233
|
-
});
|
|
1234
|
-
const rowLabels = [...rows].map(([rank, row]) => ({
|
|
1235
|
-
label: rank === 0 ? "Reference (rank 0)" : `Rank ${rank}`,
|
|
1236
|
-
y: row * ROW_HEIGHT_PX
|
|
1237
|
-
}));
|
|
1238
|
-
return {
|
|
1239
|
-
nodePositions,
|
|
1240
|
-
rowLabels,
|
|
1241
|
-
referenceAxis: true,
|
|
1242
|
-
pixelRows: true,
|
|
1243
|
-
alleleDeletions
|
|
1244
|
-
};
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
// src/layout/orderedLayout.ts
|
|
1248
|
-
var GUTTER = 12;
|
|
1249
|
-
var LANE_PX = ROW_HEIGHT_PX;
|
|
1250
|
-
function nodeWidth(node) {
|
|
1251
|
-
return 10 + 8 * Math.log2(1 + node.length);
|
|
1252
|
-
}
|
|
1253
|
-
function compare(a, b) {
|
|
1254
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
1255
|
-
}
|
|
1256
|
-
function compareKeys(a, b) {
|
|
1257
|
-
return compare(a.at, b.at) || compare(a.side, b.side) || compare(a.hops, b.hops);
|
|
1258
|
-
}
|
|
1259
|
-
var UNREACHED = { at: Infinity, side: 0, hops: 0 };
|
|
1260
|
-
function referenceOrder(graph) {
|
|
1261
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
1262
|
-
const key = orderKeys(graph);
|
|
1263
|
-
return graph.nodes.map((n) => n.id).sort(
|
|
1264
|
-
(a, b) => compareKeys(key.get(a) ?? UNREACHED, key.get(b) ?? UNREACHED) || compare(byId.get(a).name, byId.get(b).name) || compare(a, b)
|
|
1265
|
-
);
|
|
1266
|
-
}
|
|
1267
|
-
function placeByWalks(graph, backbone) {
|
|
1268
|
-
const isNode = new Set(graph.nodes.map((n) => n.id));
|
|
1269
|
-
const startOf = new Map(backbone.map((n) => [n.id, n.stable.start]));
|
|
1270
|
-
const next = /* @__PURE__ */ new Map();
|
|
1271
|
-
const prev = /* @__PURE__ */ new Map();
|
|
1272
|
-
backbone.forEach((n, i) => {
|
|
1273
|
-
prev.set(n.id, backbone[i - 1]?.id);
|
|
1274
|
-
next.set(n.id, backbone[i + 1]?.id);
|
|
1275
|
-
});
|
|
1276
|
-
let head = backbone[0]?.id;
|
|
1277
|
-
const insertAfter = (at, id) => {
|
|
1278
|
-
const after = next.get(at);
|
|
1279
|
-
next.set(at, id);
|
|
1280
|
-
prev.set(id, at);
|
|
1281
|
-
next.set(id, after);
|
|
1282
|
-
if (after !== void 0) {
|
|
1283
|
-
prev.set(after, id);
|
|
1284
|
-
}
|
|
1285
|
-
};
|
|
1286
|
-
const insertBefore = (at, id) => {
|
|
1287
|
-
const before = prev.get(at);
|
|
1288
|
-
if (before === void 0) {
|
|
1289
|
-
head = id;
|
|
1290
|
-
prev.set(id, void 0);
|
|
1291
|
-
next.set(id, at);
|
|
1292
|
-
prev.set(at, id);
|
|
1293
|
-
} else {
|
|
1294
|
-
insertAfter(before, id);
|
|
1295
|
-
}
|
|
1296
|
-
};
|
|
1297
|
-
for (const path of graph.paths ?? []) {
|
|
1298
|
-
let rising = 0;
|
|
1299
|
-
let falling = 0;
|
|
1300
|
-
let last;
|
|
1301
|
-
for (const id of path.nodeIds) {
|
|
1302
|
-
const start = startOf.get(id);
|
|
1303
|
-
if (start !== void 0) {
|
|
1304
|
-
if (last !== void 0 && start !== last) {
|
|
1305
|
-
if (start > last) {
|
|
1306
|
-
rising++;
|
|
1307
|
-
} else {
|
|
1308
|
-
falling++;
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
last = start;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
const ids = falling > rising ? [...path.nodeIds].reverse() : path.nodeIds;
|
|
1315
|
-
let from;
|
|
1316
|
-
const leading = /* @__PURE__ */ new Set();
|
|
1317
|
-
for (const id of ids) {
|
|
1318
|
-
if (next.has(id)) {
|
|
1319
|
-
if (from === void 0) {
|
|
1320
|
-
for (const lead of leading) {
|
|
1321
|
-
insertBefore(id, lead);
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
from = id;
|
|
1325
|
-
} else if (isNode.has(id)) {
|
|
1326
|
-
if (from === void 0) {
|
|
1327
|
-
leading.add(id);
|
|
1328
|
-
} else {
|
|
1329
|
-
insertAfter(from, id);
|
|
1330
|
-
from = id;
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
const placed = [];
|
|
1336
|
-
for (let id = head; id !== void 0; id = next.get(id)) {
|
|
1337
|
-
placed.push(id);
|
|
1338
|
-
}
|
|
1339
|
-
return placed;
|
|
1340
|
-
}
|
|
1341
|
-
function placeBySearch(adjacent, key) {
|
|
1342
|
-
const queue = [...key.entries()].sort(([a, ka], [b, kb]) => compareKeys(ka, kb) || compare(a, b)).map(([id]) => id);
|
|
1343
|
-
const placed = queue.length;
|
|
1344
|
-
for (const id of queue) {
|
|
1345
|
-
const from = key.get(id);
|
|
1346
|
-
for (const nb of adjacent.get(id) ?? []) {
|
|
1347
|
-
if (!key.has(nb)) {
|
|
1348
|
-
key.set(nb, { at: from.at, side: 1, hops: from.hops + 1 });
|
|
1349
|
-
queue.push(nb);
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
const searched = new Set(queue.slice(placed));
|
|
1354
|
-
const sided = /* @__PURE__ */ new Set();
|
|
1355
|
-
for (const seed of searched) {
|
|
1356
|
-
if (sided.has(seed)) {
|
|
1357
|
-
continue;
|
|
1358
|
-
}
|
|
1359
|
-
const { at } = key.get(seed);
|
|
1360
|
-
const run = [seed];
|
|
1361
|
-
sided.add(seed);
|
|
1362
|
-
let farHalf = true;
|
|
1363
|
-
for (const id of run) {
|
|
1364
|
-
const { hops } = key.get(id);
|
|
1365
|
-
let deadEnd = true;
|
|
1366
|
-
let touchesLeft = false;
|
|
1367
|
-
for (const nb of adjacent.get(id) ?? []) {
|
|
1368
|
-
const other = key.get(nb);
|
|
1369
|
-
if (other.at === at && searched.has(nb)) {
|
|
1370
|
-
deadEnd &&= other.hops <= hops;
|
|
1371
|
-
if (!sided.has(nb)) {
|
|
1372
|
-
sided.add(nb);
|
|
1373
|
-
run.push(nb);
|
|
1374
|
-
}
|
|
1375
|
-
} else if (other.at !== at) {
|
|
1376
|
-
touchesLeft ||= other.at < at;
|
|
1377
|
-
farHalf &&= other.at < at;
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
farHalf &&= !deadEnd || touchesLeft;
|
|
1381
|
-
}
|
|
1382
|
-
if (farHalf) {
|
|
1383
|
-
for (const id of run) {
|
|
1384
|
-
const k = key.get(id);
|
|
1385
|
-
key.set(id, { ...k, side: -1, hops: -k.hops });
|
|
1386
|
-
}
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
function orderKeys(graph) {
|
|
1391
|
-
const ids = new Set(graph.nodes.map((n) => n.id));
|
|
1392
|
-
const adjacent = /* @__PURE__ */ new Map();
|
|
1393
|
-
for (const e of graph.edges) {
|
|
1394
|
-
if (!ids.has(e.from) || !ids.has(e.to)) {
|
|
1395
|
-
continue;
|
|
1396
|
-
}
|
|
1397
|
-
;
|
|
1398
|
-
(adjacent.get(e.from) ?? adjacent.set(e.from, []).get(e.from)).push(e.to);
|
|
1399
|
-
(adjacent.get(e.to) ?? adjacent.set(e.to, []).get(e.to)).push(e.from);
|
|
1400
|
-
}
|
|
1401
|
-
const backbone = graph.nodes.filter(isBackbone).sort(
|
|
1402
|
-
(a, b) => compare(a.stable.start, b.stable.start) || compare(a.name, b.name)
|
|
1403
|
-
);
|
|
1404
|
-
const key = /* @__PURE__ */ new Map();
|
|
1405
|
-
placeByWalks(graph, backbone).forEach((id, rank) => {
|
|
1406
|
-
key.set(id, { at: rank, side: 0, hops: 0 });
|
|
1407
|
-
});
|
|
1408
|
-
placeBySearch(adjacent, key);
|
|
1409
|
-
return key;
|
|
1410
|
-
}
|
|
1411
|
-
function layerGraph(graph) {
|
|
1412
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
1413
|
-
const order = referenceOrder(graph);
|
|
1414
|
-
const key = new Map(order.map((id, i) => [id, i]));
|
|
1415
|
-
const isRef = (id) => isBackbone(byId.get(id));
|
|
1416
|
-
const preds = new Map(order.map((id) => [id, []]));
|
|
1417
|
-
const succs = new Map(order.map((id) => [id, []]));
|
|
1418
|
-
const link = (a, b) => {
|
|
1419
|
-
preds.get(b).push(a);
|
|
1420
|
-
succs.get(a).push(b);
|
|
1421
|
-
};
|
|
1422
|
-
for (const e of graph.edges) {
|
|
1423
|
-
if (e.from === e.to || !key.has(e.from) || !key.has(e.to)) {
|
|
1424
|
-
continue;
|
|
1425
|
-
}
|
|
1426
|
-
if (key.get(e.from) < key.get(e.to)) {
|
|
1427
|
-
link(e.from, e.to);
|
|
1428
|
-
} else {
|
|
1429
|
-
link(e.to, e.from);
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
const backbone = order.filter(isRef);
|
|
1433
|
-
for (let i = 1; i < backbone.length; i++) {
|
|
1434
|
-
link(backbone[i - 1], backbone[i]);
|
|
1435
|
-
}
|
|
1436
|
-
const layerOf2 = /* @__PURE__ */ new Map();
|
|
1437
|
-
for (const id of order) {
|
|
1438
|
-
let l = 0;
|
|
1439
|
-
for (const p of preds.get(id)) {
|
|
1440
|
-
l = Math.max(l, layerOf2.get(p) + 1);
|
|
1441
|
-
}
|
|
1442
|
-
layerOf2.set(id, l);
|
|
1443
|
-
}
|
|
1444
|
-
for (let i = order.length - 1; i >= 0; i--) {
|
|
1445
|
-
const id = order[i];
|
|
1446
|
-
const s = succs.get(id);
|
|
1447
|
-
if (isRef(id) || s.length === 0) {
|
|
1448
|
-
continue;
|
|
1449
|
-
}
|
|
1450
|
-
let minSucc = Infinity;
|
|
1451
|
-
for (const t of s) {
|
|
1452
|
-
minSucc = Math.min(minSucc, layerOf2.get(t));
|
|
1453
|
-
}
|
|
1454
|
-
const lo = layerOf2.get(id);
|
|
1455
|
-
const hi = Math.max(lo, minSucc - 1);
|
|
1456
|
-
layerOf2.set(id, Math.floor((lo + hi) / 2));
|
|
1457
|
-
}
|
|
1458
|
-
const layers = [];
|
|
1459
|
-
for (const id of order) {
|
|
1460
|
-
;
|
|
1461
|
-
(layers[layerOf2.get(id)] ??= []).push(id);
|
|
1462
|
-
}
|
|
1463
|
-
return { layers, layerOf: layerOf2, preds };
|
|
1464
|
-
}
|
|
1465
|
-
function orderedLayout(graph) {
|
|
1466
|
-
if (!graph.nodes.some(isBackbone)) {
|
|
1467
|
-
return void 0;
|
|
1468
|
-
}
|
|
1469
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
1470
|
-
const isRef = (id) => isBackbone(byId.get(id));
|
|
1471
|
-
const { layers, layerOf: layer, preds } = layerGraph(graph);
|
|
1472
|
-
const layerWidth = layers.map((ids) => {
|
|
1473
|
-
let w = 0;
|
|
1474
|
-
for (const id of ids) {
|
|
1475
|
-
w = Math.max(w, nodeWidth(byId.get(id)));
|
|
1476
|
-
}
|
|
1477
|
-
return w;
|
|
1478
|
-
});
|
|
1479
|
-
const layerX = [];
|
|
1480
|
-
let x = 0;
|
|
1481
|
-
for (let l = 0; l < layers.length; l++) {
|
|
1482
|
-
layerX[l] = x;
|
|
1483
|
-
x += layerWidth[l] + GUTTER;
|
|
1484
|
-
}
|
|
1485
|
-
const y = /* @__PURE__ */ new Map();
|
|
1486
|
-
for (const ids of layers) {
|
|
1487
|
-
const free = [];
|
|
1488
|
-
for (const id of ids) {
|
|
1489
|
-
if (isRef(id)) {
|
|
1490
|
-
y.set(id, 0);
|
|
1491
|
-
continue;
|
|
1492
|
-
}
|
|
1493
|
-
const placed = preds.get(id).filter((p) => y.has(p));
|
|
1494
|
-
let ideal = 0;
|
|
1495
|
-
for (const p of placed) {
|
|
1496
|
-
ideal += y.get(p) / placed.length;
|
|
1497
|
-
}
|
|
1498
|
-
free.push({ id, ideal: ideal === 0 ? LANE_PX : ideal });
|
|
1499
|
-
}
|
|
1500
|
-
free.sort((a, b) => a.ideal - b.ideal);
|
|
1501
|
-
const taken = /* @__PURE__ */ new Set([0]);
|
|
1502
|
-
for (const { id, ideal } of free) {
|
|
1503
|
-
let lane = Math.round(ideal / LANE_PX);
|
|
1504
|
-
if (lane === 0) {
|
|
1505
|
-
lane = ideal >= 0 ? 1 : -1;
|
|
1506
|
-
}
|
|
1507
|
-
for (let d = 1; taken.has(lane); d++) {
|
|
1508
|
-
if (!taken.has(lane + d)) {
|
|
1509
|
-
lane += d;
|
|
1510
|
-
} else if (!taken.has(lane - d)) {
|
|
1511
|
-
lane -= d;
|
|
1512
|
-
}
|
|
1513
|
-
}
|
|
1514
|
-
taken.add(lane);
|
|
1515
|
-
y.set(id, lane * LANE_PX);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
const nodePositions = {};
|
|
1519
|
-
for (const n of graph.nodes) {
|
|
1520
|
-
const l = layer.get(n.id);
|
|
1521
|
-
const w = nodeWidth(n);
|
|
1522
|
-
const cx = layerX[l] + layerWidth[l] / 2;
|
|
1523
|
-
const ny = y.get(n.id);
|
|
1524
|
-
nodePositions[n.id] = [
|
|
1525
|
-
{ x: cx - w / 2, y: ny },
|
|
1526
|
-
{ x: cx + w / 2, y: ny }
|
|
1527
|
-
];
|
|
1528
|
-
}
|
|
1529
|
-
return { nodePositions, referenceAxis: false, pixelRows: true };
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// src/layout/sampleRowLayout.ts
|
|
1533
|
-
function contributingSamples(graph, keep) {
|
|
1534
|
-
const carried = /* @__PURE__ */ new Map();
|
|
1535
|
-
for (const node of graph.nodes) {
|
|
1536
|
-
if (isOffReference(node)) {
|
|
1537
|
-
const { sample } = parsePanSN(node.stable.refName);
|
|
1538
|
-
carried.set(sample, (carried.get(sample) ?? 0) + node.length);
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
const held = new Map(keep.map((sample, row) => [sample, row]));
|
|
1542
|
-
return [...carried.keys()].sort(
|
|
1543
|
-
(a, b) => (held.get(a) ?? Infinity) - (held.get(b) ?? Infinity) || carried.get(b) - carried.get(a) || a.localeCompare(b)
|
|
1544
|
-
);
|
|
1545
|
-
}
|
|
1546
|
-
function sampleRowLayout(graph, region, keep = []) {
|
|
1547
|
-
const backbone = backboneNodes(graph);
|
|
1548
|
-
const samples = contributingSamples(graph, keep);
|
|
1549
|
-
if (backbone.length === 0 || samples.length === 0) {
|
|
1550
|
-
return void 0;
|
|
1551
|
-
}
|
|
1552
|
-
const span2 = referenceSpan(backbone, region);
|
|
1553
|
-
const rowOf = new Map(samples.map((s, i) => [s, i + 1]));
|
|
1554
|
-
const nodePositions = backbonePositions(backbone);
|
|
1555
|
-
const spareRow = samples.length + 1;
|
|
1556
|
-
const alleleDeletions = placeOffReference({
|
|
1557
|
-
graph,
|
|
1558
|
-
span: span2,
|
|
1559
|
-
rowY: (node) => {
|
|
1560
|
-
const stable = node.stable;
|
|
1561
|
-
const row = stable ? rowOf.get(parsePanSN(stable.refName).sample) : void 0;
|
|
1562
|
-
return (row ?? spareRow) * ROW_HEIGHT_PX;
|
|
1563
|
-
},
|
|
1564
|
-
positions: nodePositions
|
|
1565
|
-
});
|
|
1566
|
-
const referenceSample = parsePanSN(backbone[0].stable.refName).sample;
|
|
1567
|
-
const rowLabels = [
|
|
1568
|
-
{ label: referenceSample, y: 0 },
|
|
1569
|
-
...samples.map((sample) => ({
|
|
1570
|
-
label: sample,
|
|
1571
|
-
y: rowOf.get(sample) * ROW_HEIGHT_PX
|
|
1572
|
-
}))
|
|
1573
|
-
];
|
|
1574
|
-
return {
|
|
1575
|
-
nodePositions,
|
|
1576
|
-
rowLabels,
|
|
1577
|
-
sampleRows: samples,
|
|
1578
|
-
referenceAxis: true,
|
|
1579
|
-
pixelRows: true,
|
|
1580
|
-
alleleDeletions
|
|
1581
|
-
};
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
// src/layout/tubeMapLayout.ts
|
|
1585
|
-
import { layoutTubeMap } from "@gmod/tubemap-core";
|
|
1586
|
-
|
|
1587
|
-
// src/pathColors.ts
|
|
1588
|
-
function nameHue(name) {
|
|
1589
|
-
let hash = 0;
|
|
1590
|
-
for (let i = 0; i < name.length; i++) {
|
|
1591
|
-
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
1592
|
-
}
|
|
1593
|
-
return Math.abs(hash % 360);
|
|
1594
|
-
}
|
|
1595
|
-
function pathHueAt(index, count) {
|
|
1596
|
-
return index * 360 / count;
|
|
1597
|
-
}
|
|
1598
|
-
var PATH_SATURATION = 0.7;
|
|
1599
|
-
var PATH_LIGHTNESS = 0.5;
|
|
1600
|
-
var MAX_PATH_COLORS = 16;
|
|
1601
|
-
function pathColorsLegible(pathCount) {
|
|
1602
|
-
return pathCount > 0 && pathCount <= MAX_PATH_COLORS;
|
|
1603
|
-
}
|
|
1604
|
-
function pathCssColor(index, count) {
|
|
1605
|
-
return `hsl(${pathHueAt(index, count)}, ${PATH_SATURATION * 100}%, ${PATH_LIGHTNESS * 100}%)`;
|
|
1606
|
-
}
|
|
1607
|
-
function pathGreyCssColor(index, count) {
|
|
1608
|
-
const lightness = count > 1 ? 25 + 45 * index / (count - 1) : 45;
|
|
1609
|
-
return `hsl(0, 0%, ${lightness}%)`;
|
|
1610
|
-
}
|
|
1611
|
-
function labelTiers(name) {
|
|
1612
|
-
const parts = pathOrigin(name).name.split("#");
|
|
1613
|
-
const full = parts.join("#");
|
|
1614
|
-
if (parts.length >= 3 && isGenericSample(parts[0])) {
|
|
1615
|
-
const contig = parts.slice(2).join("#");
|
|
1616
|
-
return [contig, contig, full];
|
|
1617
|
-
}
|
|
1618
|
-
return [parts[0], parts.slice(0, 2).join("#"), full];
|
|
1619
|
-
}
|
|
1620
|
-
function groupDigits(n) {
|
|
1621
|
-
return String(n).replace(/\B(?=(\d{3})+$)/g, ",");
|
|
1622
|
-
}
|
|
1623
|
-
function offsetTiers(name, tiers) {
|
|
1624
|
-
const { start } = pathOrigin(name);
|
|
1625
|
-
return tiers.map((t) => `${t} @${groupDigits(start)}`);
|
|
1626
|
-
}
|
|
1627
|
-
function pathLegend(paths, colors) {
|
|
1628
|
-
const named = paths.map((p) => labelTiers(p.name));
|
|
1629
|
-
const candidates = [0, 1, 2].flatMap((t) => [
|
|
1630
|
-
paths.map((_, i) => named[i][t]),
|
|
1631
|
-
paths.map((p, i) => offsetTiers(p.name, named[i])[t])
|
|
1632
|
-
]);
|
|
1633
|
-
const distinct = candidates.find(
|
|
1634
|
-
(labels2) => new Set(labels2).size === paths.length
|
|
1635
|
-
);
|
|
1636
|
-
const labels = distinct ?? candidates.at(-1);
|
|
1637
|
-
return paths.map((path, i) => ({
|
|
1638
|
-
name: path.name,
|
|
1639
|
-
label: labels[i],
|
|
1640
|
-
color: colors?.[i] ?? pathCssColor(i, paths.length)
|
|
1641
|
-
}));
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
// src/gaf/parseGaf.ts
|
|
1645
|
-
var CS_OP = /:(\d+)|=([A-Za-z]+)|\*[a-zA-Z]([a-zA-Z])|\+([A-Za-z]+)|-([A-Za-z]+)|~[a-z]{2}(\d+)[a-z]{2}/g;
|
|
1646
|
-
function parseCs(cs) {
|
|
1647
|
-
const ops = [];
|
|
1648
|
-
for (const m of cs.matchAll(CS_OP)) {
|
|
1649
|
-
if (m[1] !== void 0) {
|
|
1650
|
-
ops.push({ op: "match", length: +m[1] });
|
|
1651
|
-
} else if (m[2] !== void 0) {
|
|
1652
|
-
ops.push({ op: "match", length: m[2].length });
|
|
1653
|
-
} else if (m[3] !== void 0) {
|
|
1654
|
-
ops.push({ op: "substitution", seq: m[3].toUpperCase() });
|
|
1655
|
-
} else if (m[4] !== void 0) {
|
|
1656
|
-
ops.push({ op: "insertion", seq: m[4].toUpperCase() });
|
|
1657
|
-
} else if (m[5] !== void 0) {
|
|
1658
|
-
ops.push({ op: "deletion", length: m[5].length });
|
|
1659
|
-
} else if (m[6] !== void 0) {
|
|
1660
|
-
ops.push({ op: "match", length: +m[6] });
|
|
1661
|
-
}
|
|
1662
|
-
}
|
|
1663
|
-
return ops;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
// src/tubeMap/reads.ts
|
|
1667
|
-
function canonicalStrand(node) {
|
|
1668
|
-
return node.id.endsWith("-") ? "-" : "+";
|
|
1669
|
-
}
|
|
1670
|
-
function stepName(node, strand) {
|
|
1671
|
-
return strand === canonicalStrand(node) ? node.id : `-${node.id}`;
|
|
1672
|
-
}
|
|
1673
|
-
function stepStarts(lengths, total) {
|
|
1674
|
-
const starts = lengths.map(() => void 0);
|
|
1675
|
-
let head = 0;
|
|
1676
|
-
let i = 0;
|
|
1677
|
-
for (; i < lengths.length && lengths[i] !== void 0; i++) {
|
|
1678
|
-
starts[i] = head;
|
|
1679
|
-
head += lengths[i];
|
|
1680
|
-
}
|
|
1681
|
-
let tail = total;
|
|
1682
|
-
for (let j = lengths.length - 1; j > i && lengths[j] !== void 0; j--) {
|
|
1683
|
-
tail -= lengths[j];
|
|
1684
|
-
starts[j] = tail;
|
|
1685
|
-
}
|
|
1686
|
-
return starts;
|
|
1687
|
-
}
|
|
1688
|
-
function mismatchesByStep(record, lengths) {
|
|
1689
|
-
const byStep = record.path.map(() => []);
|
|
1690
|
-
if (record.cs === void 0) {
|
|
1691
|
-
return byStep;
|
|
1692
|
-
}
|
|
1693
|
-
const starts = stepStarts(lengths, record.pathLength);
|
|
1694
|
-
const stepAt = (pos2, atEnd = false) => {
|
|
1695
|
-
for (let s = 0; s < starts.length; s++) {
|
|
1696
|
-
const start = starts[s];
|
|
1697
|
-
const length = lengths[s];
|
|
1698
|
-
if (start !== void 0 && length !== void 0 && start <= pos2 && (pos2 < start + length || atEnd && pos2 === start + length)) {
|
|
1699
|
-
return { s, local: pos2 - start, room: start + length - pos2 };
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
return void 0;
|
|
1703
|
-
};
|
|
1704
|
-
const add = (s, mismatch) => {
|
|
1705
|
-
const list = byStep[s];
|
|
1706
|
-
const last = list.at(-1);
|
|
1707
|
-
if (mismatch.type === "substitution" && last?.type === "substitution" && last.pos + last.seq.length === mismatch.pos) {
|
|
1708
|
-
last.seq += mismatch.seq;
|
|
1709
|
-
} else {
|
|
1710
|
-
list.push(mismatch);
|
|
1711
|
-
}
|
|
1712
|
-
};
|
|
1713
|
-
let pos = record.pathStart;
|
|
1714
|
-
for (const op of parseCs(record.cs)) {
|
|
1715
|
-
if (op.op === "match") {
|
|
1716
|
-
pos += op.length;
|
|
1717
|
-
} else if (op.op === "substitution") {
|
|
1718
|
-
const at = stepAt(pos);
|
|
1719
|
-
if (at) {
|
|
1720
|
-
add(at.s, { type: "substitution", pos: at.local, seq: op.seq });
|
|
1721
|
-
}
|
|
1722
|
-
pos += 1;
|
|
1723
|
-
} else if (op.op === "insertion") {
|
|
1724
|
-
const at = stepAt(pos, pos === record.pathEnd);
|
|
1725
|
-
if (at) {
|
|
1726
|
-
add(at.s, { type: "insertion", pos: at.local, seq: op.seq });
|
|
1727
|
-
}
|
|
1728
|
-
} else {
|
|
1729
|
-
let left = op.length;
|
|
1730
|
-
while (left > 0) {
|
|
1731
|
-
const at = stepAt(pos);
|
|
1732
|
-
const take = at ? Math.min(left, at.room) : left;
|
|
1733
|
-
if (at) {
|
|
1734
|
-
add(at.s, { type: "deletion", pos: at.local, length: take });
|
|
1735
|
-
}
|
|
1736
|
-
pos += take;
|
|
1737
|
-
left -= take;
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
return byStep;
|
|
1742
|
-
}
|
|
1743
|
-
function tubeMapReads(graph, records, firstId) {
|
|
1744
|
-
const nodeByName = new Map(graph.nodes.map((n) => [n.name, n]));
|
|
1745
|
-
const reads = [];
|
|
1746
|
-
for (const record of records) {
|
|
1747
|
-
const nodes = record.path.map((step) => nodeByName.get(step.name));
|
|
1748
|
-
const kept = nodes.flatMap((node, i) => node ? [i] : []);
|
|
1749
|
-
const first = kept[0];
|
|
1750
|
-
const last = kept.at(-1);
|
|
1751
|
-
if (first !== void 0 && last !== void 0) {
|
|
1752
|
-
const mismatches = mismatchesByStep(
|
|
1753
|
-
record,
|
|
1754
|
-
nodes.map((n) => n?.length)
|
|
1755
|
-
);
|
|
1756
|
-
const names = kept.map((i) => stepName(nodes[i], record.path[i].strand));
|
|
1757
|
-
const lastLength = nodes[last].length;
|
|
1758
|
-
reads.push({
|
|
1759
|
-
id: firstId + reads.length,
|
|
1760
|
-
name: record.name,
|
|
1761
|
-
type: "read",
|
|
1762
|
-
sourceTrackID: 1,
|
|
1763
|
-
sequence: names,
|
|
1764
|
-
sequenceNew: kept.map((i, k) => ({
|
|
1765
|
-
nodeName: names[k],
|
|
1766
|
-
mismatches: mismatches[i]
|
|
1767
|
-
})),
|
|
1768
|
-
firstNodeOffset: first === 0 ? record.pathStart : 0,
|
|
1769
|
-
finalNodeCoverLength: last === record.path.length - 1 ? lastLength - (record.pathLength - record.pathEnd) : lastLength,
|
|
1770
|
-
mapping_quality: record.mappingQuality === 255 ? 0 : record.mappingQuality,
|
|
1771
|
-
is_secondary: record.secondary,
|
|
1772
|
-
is_reverse: record.strand === "-"
|
|
1773
|
-
});
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
return reads;
|
|
1777
|
-
}
|
|
1778
|
-
var FORWARD_READ_COLORS = ["#6baed6", "#4292c6", "#2171b5", "#08519c"];
|
|
1779
|
-
var REVERSE_READ_COLORS = ["#fb6a4a", "#ef3b2c", "#cb181d", "#a50f15"];
|
|
1780
|
-
function readColor(read) {
|
|
1781
|
-
const colors = read.is_reverse ? REVERSE_READ_COLORS : FORWARD_READ_COLORS;
|
|
1782
|
-
return colors[read.id % colors.length];
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
// src/layout/tubeMapLayout.ts
|
|
1786
|
-
function visitStrands(graph) {
|
|
1787
|
-
const strands = /* @__PURE__ */ new Map();
|
|
1788
|
-
for (const [segment, visits] of graph.pathVisits ?? []) {
|
|
1789
|
-
for (const visit of visits) {
|
|
1790
|
-
const key = `${visit.path} ${segment}`;
|
|
1791
|
-
const list = strands.get(key);
|
|
1792
|
-
if (list) {
|
|
1793
|
-
list.push(visit.strand);
|
|
1794
|
-
} else {
|
|
1795
|
-
strands.set(key, [visit.strand]);
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
}
|
|
1799
|
-
return strands;
|
|
1800
|
-
}
|
|
1801
|
-
function tubeMapTracks(graph) {
|
|
1802
|
-
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
1803
|
-
const strands = visitStrands(graph);
|
|
1804
|
-
const paths = (graph.paths ?? []).map((path, index) => ({ path, index }));
|
|
1805
|
-
const reference = paths.findIndex(
|
|
1806
|
-
(p) => pathOrigin(p.path.name).name === graph.referencePath
|
|
1807
|
-
);
|
|
1808
|
-
if (reference > 0) {
|
|
1809
|
-
paths.unshift(...paths.splice(reference, 1));
|
|
1810
|
-
}
|
|
1811
|
-
return paths.flatMap(({ path, index }) => {
|
|
1812
|
-
const visitName = pathOrigin(path.name).name;
|
|
1813
|
-
const seen = /* @__PURE__ */ new Map();
|
|
1814
|
-
const sequence = path.nodeIds.flatMap((id) => {
|
|
1815
|
-
const node = nodeById.get(id);
|
|
1816
|
-
if (!node) {
|
|
1817
|
-
return [];
|
|
1818
|
-
}
|
|
1819
|
-
const k = seen.get(node.name) ?? 0;
|
|
1820
|
-
seen.set(node.name, k + 1);
|
|
1821
|
-
const strand = strands.get(`${visitName} ${node.name}`)?.[k] ?? canonicalStrand(node);
|
|
1822
|
-
return [stepName(node, strand)];
|
|
1823
|
-
});
|
|
1824
|
-
return sequence.length > 0 ? [
|
|
1825
|
-
{
|
|
1826
|
-
id: index,
|
|
1827
|
-
name: path.name,
|
|
1828
|
-
sequence,
|
|
1829
|
-
sourceTrackID: 0,
|
|
1830
|
-
type: "haplotype"
|
|
1831
|
-
}
|
|
1832
|
-
] : [];
|
|
1833
|
-
});
|
|
1834
|
-
}
|
|
1835
|
-
function tubeMapNodes(graph) {
|
|
1836
|
-
return graph.nodes.map((n) => ({
|
|
1837
|
-
name: n.id,
|
|
1838
|
-
seq: "",
|
|
1839
|
-
sequenceLength: Math.max(1, n.length)
|
|
1840
|
-
}));
|
|
1841
|
-
}
|
|
1842
|
-
function hasTubeMapPaths(graph) {
|
|
1843
|
-
return (graph.paths?.length ?? 0) > 0;
|
|
1844
|
-
}
|
|
1845
|
-
var STACK_PX = 600;
|
|
1846
|
-
var MIN_TUBE_PX = 3;
|
|
1847
|
-
var MAX_TUBE_PX = 15;
|
|
1848
|
-
function tubeWidth(trackCount) {
|
|
1849
|
-
return Math.max(
|
|
1850
|
-
MIN_TUBE_PX,
|
|
1851
|
-
Math.min(MAX_TUBE_PX, Math.floor(STACK_PX / Math.max(1, trackCount)))
|
|
1852
|
-
);
|
|
1853
|
-
}
|
|
1854
|
-
function runTubeMap(graph) {
|
|
1855
|
-
const paths = graph.paths ?? [];
|
|
1856
|
-
const tracks = tubeMapTracks(graph);
|
|
1857
|
-
const reads = graph.reads ? tubeMapReads(graph, graph.reads, paths.length) : [];
|
|
1858
|
-
const tubeColor = reads.length > 0 ? pathGreyCssColor : pathCssColor;
|
|
1859
|
-
const pathColors = paths.map((_, i) => tubeColor(i, paths.length));
|
|
1860
|
-
const layout = layoutTubeMap(tubeMapNodes(graph), tracks, reads, {
|
|
1861
|
-
nodeWidthOption: "compressed",
|
|
1862
|
-
trackWidth: tubeWidth(tracks.length),
|
|
1863
|
-
trackColor: (track) => track.type === "read" ? readColor(track) : pathColors[track.id]
|
|
1864
|
-
});
|
|
1865
|
-
return layout ? { layout, pathColors } : void 0;
|
|
1866
|
-
}
|
|
1867
|
-
function drawnNodes(layout) {
|
|
1868
|
-
const nodes = [];
|
|
1869
|
-
layout.nodes.forEach((node) => {
|
|
1870
|
-
if (node.order >= 0) {
|
|
1871
|
-
nodes.push(node);
|
|
1872
|
-
}
|
|
1873
|
-
});
|
|
1874
|
-
return nodes;
|
|
1875
|
-
}
|
|
1876
|
-
function centreline(node, yOffset, x0, x1) {
|
|
1877
|
-
const y = node.y + node.contentHeight / 2 - yOffset;
|
|
1878
|
-
return [
|
|
1879
|
-
{ x: x0, y },
|
|
1880
|
-
{ x: x1, y }
|
|
1881
|
-
];
|
|
1882
|
-
}
|
|
1883
|
-
function extentOf(layout, yOffset) {
|
|
1884
|
-
return {
|
|
1885
|
-
minY: layout.bounds.minY - yOffset,
|
|
1886
|
-
maxY: layout.bounds.maxY - yOffset
|
|
1887
|
-
};
|
|
1888
|
-
}
|
|
1889
|
-
function tubeMapLayout(graph) {
|
|
1890
|
-
const run = hasTubeMapPaths(graph) ? runTubeMap(graph) : void 0;
|
|
1891
|
-
if (!run) {
|
|
1892
|
-
return void 0;
|
|
1893
|
-
}
|
|
1894
|
-
const { layout, pathColors } = run;
|
|
1895
|
-
const yOffset = layout.bounds.minY;
|
|
1896
|
-
const nodePositions = {};
|
|
1897
|
-
for (const node of drawnNodes(layout)) {
|
|
1898
|
-
nodePositions[node.name] = centreline(
|
|
1899
|
-
node,
|
|
1900
|
-
yOffset,
|
|
1901
|
-
node.x,
|
|
1902
|
-
node.x + node.pixelWidth
|
|
1903
|
-
);
|
|
1904
|
-
}
|
|
1905
|
-
const { minY, maxY } = extentOf(layout, yOffset);
|
|
1906
|
-
return {
|
|
1907
|
-
nodePositions,
|
|
1908
|
-
tubeMap: { layout, yOffset, pathColors },
|
|
1909
|
-
extent: {
|
|
1910
|
-
minX: layout.bounds.minX,
|
|
1911
|
-
maxX: layout.bounds.maxX,
|
|
1912
|
-
minY,
|
|
1913
|
-
maxY
|
|
1914
|
-
}
|
|
1915
|
-
};
|
|
1916
|
-
}
|
|
1917
|
-
function tubeMapColumns(graph, layout) {
|
|
1918
|
-
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
1919
|
-
const byOrder = /* @__PURE__ */ new Map();
|
|
1920
|
-
for (const node of drawnNodes(layout)) {
|
|
1921
|
-
const list = byOrder.get(node.order);
|
|
1922
|
-
if (list) {
|
|
1923
|
-
list.push(node);
|
|
1924
|
-
} else {
|
|
1925
|
-
byOrder.set(node.order, [node]);
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
const columns = [];
|
|
1929
|
-
let end = -Infinity;
|
|
1930
|
-
for (const order of [...byOrder.keys()].sort((a, b) => a - b)) {
|
|
1931
|
-
const nodes = byOrder.get(order);
|
|
1932
|
-
let bp0 = Infinity;
|
|
1933
|
-
let bp1 = -Infinity;
|
|
1934
|
-
for (const node of nodes) {
|
|
1935
|
-
const graphNode = nodeById.get(node.name);
|
|
1936
|
-
if (graphNode && isBackbone(graphNode)) {
|
|
1937
|
-
bp0 = Math.min(bp0, graphNode.stable.start);
|
|
1938
|
-
bp1 = Math.max(bp1, graphNode.stable.start + node.sequenceLength);
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
if (bp0 === Infinity) {
|
|
1942
|
-
bp0 = bp1 = end;
|
|
1943
|
-
}
|
|
1944
|
-
if (bp0 < end) {
|
|
1945
|
-
bp0 = end;
|
|
1946
|
-
bp1 = Math.max(end, bp1);
|
|
1947
|
-
}
|
|
1948
|
-
end = Math.max(end, bp1);
|
|
1949
|
-
columns.push({
|
|
1950
|
-
order,
|
|
1951
|
-
x0: Math.min(...nodes.map((n) => n.x)),
|
|
1952
|
-
x1: Math.max(...nodes.map((n) => n.x + n.pixelWidth)),
|
|
1953
|
-
bp0,
|
|
1954
|
-
bp1
|
|
1955
|
-
});
|
|
1956
|
-
}
|
|
1957
|
-
const first = columns.find((c) => c.bp0 !== -Infinity);
|
|
1958
|
-
for (const column of columns) {
|
|
1959
|
-
if (column.bp0 === -Infinity) {
|
|
1960
|
-
column.bp0 = column.bp1 = first?.bp0 ?? 0;
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
return columns;
|
|
1964
|
-
}
|
|
1965
|
-
function pathsReachBackbone(graph) {
|
|
1966
|
-
return graph.nodes.some(isBackbone) && hasTubeMapPaths(graph);
|
|
1967
|
-
}
|
|
1968
|
-
function tubeMapReferenceLayout(graph) {
|
|
1969
|
-
const run = pathsReachBackbone(graph) ? runTubeMap(graph) : void 0;
|
|
1970
|
-
if (!run) {
|
|
1971
|
-
return void 0;
|
|
1972
|
-
}
|
|
1973
|
-
const { layout, pathColors } = run;
|
|
1974
|
-
const columns = tubeMapColumns(graph, layout);
|
|
1975
|
-
const byOrder = new Map(columns.map((c) => [c.order, c]));
|
|
1976
|
-
const yOffset = layout.bounds.minY;
|
|
1977
|
-
const nodePositions = {};
|
|
1978
|
-
for (const node of drawnNodes(layout)) {
|
|
1979
|
-
const column = byOrder.get(node.order);
|
|
1980
|
-
nodePositions[node.name] = centreline(node, yOffset, column.bp0, column.bp1);
|
|
1981
|
-
}
|
|
1982
|
-
return {
|
|
1983
|
-
nodePositions,
|
|
1984
|
-
tubeMap: { layout, yOffset, columns, pathColors },
|
|
1985
|
-
referenceAxis: true,
|
|
1986
|
-
pixelRows: true,
|
|
1987
|
-
extent: extentOf(layout, yOffset)
|
|
1988
|
-
};
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
// src/layout/walkRows.ts
|
|
1992
|
-
function sampleOf(path) {
|
|
1993
|
-
return path.sample ?? panSNSample(path.name);
|
|
1994
|
-
}
|
|
1995
|
-
function labelOf(path) {
|
|
1996
|
-
return path.haplotype !== void 0 && path.sample !== void 0 ? `${path.sample}#${path.haplotype}` : sampleOf(path);
|
|
1997
|
-
}
|
|
1998
|
-
function sliceBetween(nodeIds, span2, region, flanked = true) {
|
|
1999
|
-
if (!region || !flanked) {
|
|
2000
|
-
return { ids: nodeIds, complete: true };
|
|
2001
|
-
}
|
|
2002
|
-
let i0 = -1;
|
|
2003
|
-
let i1 = -1;
|
|
2004
|
-
let bestEnd = -Infinity;
|
|
2005
|
-
let bestStart = Infinity;
|
|
2006
|
-
nodeIds.forEach((id, i) => {
|
|
2007
|
-
const s = span2.get(id);
|
|
2008
|
-
if (s) {
|
|
2009
|
-
if (s.end <= region.start && s.end > bestEnd) {
|
|
2010
|
-
bestEnd = s.end;
|
|
2011
|
-
i0 = i;
|
|
2012
|
-
}
|
|
2013
|
-
if (s.start >= region.end && s.start < bestStart) {
|
|
2014
|
-
bestStart = s.start;
|
|
2015
|
-
i1 = i;
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
});
|
|
2019
|
-
if (i0 < 0 || i1 < 0) {
|
|
2020
|
-
return { ids: nodeIds, complete: false };
|
|
2021
|
-
}
|
|
2022
|
-
const ids = nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
|
|
2023
|
-
return { ids: i0 < i1 ? ids : ids.reverse(), complete: true };
|
|
2024
|
-
}
|
|
2025
|
-
function walkRows(graph, region, unit) {
|
|
2026
|
-
const paths = graph.paths ?? [];
|
|
2027
|
-
const reference = paths.find((p) => pathOrigin(p.name).name === graph.referencePath) ?? paths[0];
|
|
2028
|
-
if (!reference || paths.length < 2) {
|
|
2029
|
-
return void 0;
|
|
2030
|
-
}
|
|
2031
|
-
const lengthOf = new Map(graph.nodes.map((n) => [n.id, n.length]));
|
|
2032
|
-
const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
|
|
2033
|
-
const cut = region && region.end > region.start ? region : void 0;
|
|
2034
|
-
const span2 = /* @__PURE__ */ new Map();
|
|
2035
|
-
let pos = referenceStart;
|
|
2036
|
-
for (const id of reference.nodeIds) {
|
|
2037
|
-
const len = lengthOf.get(id) ?? 0;
|
|
2038
|
-
if (!span2.has(id)) {
|
|
2039
|
-
span2.set(id, { start: pos, end: pos + len });
|
|
2040
|
-
}
|
|
2041
|
-
pos += len;
|
|
2042
|
-
}
|
|
2043
|
-
const flanked = cut === void 0 || [...span2.values()].some((s) => s.end <= cut.start) && [...span2.values()].some((s) => s.start >= cut.end);
|
|
2044
|
-
const rowOf = (path) => {
|
|
2045
|
-
const { ids, complete } = sliceBetween(path.nodeIds, span2, cut, flanked);
|
|
2046
|
-
const runs = [];
|
|
2047
|
-
let bp = 0;
|
|
2048
|
-
let offReferenceBp = 0;
|
|
2049
|
-
let step = 0;
|
|
2050
|
-
for (const id of ids) {
|
|
2051
|
-
const len = lengthOf.get(id) ?? 0;
|
|
2052
|
-
const s = span2.get(id);
|
|
2053
|
-
const last = runs.at(-1);
|
|
2054
|
-
const at = last?.referenceStart;
|
|
2055
|
-
const forward2 = s && at !== void 0 && step >= 0 && at + last.bp === s.start;
|
|
2056
|
-
const backward = s && at !== void 0 && step <= 0 && s.end === at;
|
|
2057
|
-
if (!s && last && !last.onReference) {
|
|
2058
|
-
last.bp += len;
|
|
2059
|
-
} else if (forward2 || backward) {
|
|
2060
|
-
last.bp += len;
|
|
2061
|
-
if (!forward2) {
|
|
2062
|
-
last.referenceStart = s.start;
|
|
2063
|
-
last.reversed = true;
|
|
2064
|
-
}
|
|
2065
|
-
step = forward2 ? 1 : -1;
|
|
2066
|
-
} else {
|
|
2067
|
-
runs.push({
|
|
2068
|
-
start: bp,
|
|
2069
|
-
bp: len,
|
|
2070
|
-
onReference: s !== void 0,
|
|
2071
|
-
referenceStart: s?.start
|
|
2072
|
-
});
|
|
2073
|
-
step = 0;
|
|
2074
|
-
}
|
|
2075
|
-
bp += len;
|
|
2076
|
-
if (!s) {
|
|
2077
|
-
offReferenceBp += len;
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
return {
|
|
2081
|
-
name: path.name,
|
|
2082
|
-
label: labelOf(path),
|
|
2083
|
-
sample: sampleOf(path),
|
|
2084
|
-
haplotype: path.haplotype,
|
|
2085
|
-
bp,
|
|
2086
|
-
offReferenceBp,
|
|
2087
|
-
complete,
|
|
2088
|
-
runs
|
|
2089
|
-
};
|
|
2090
|
-
};
|
|
2091
|
-
const meetsReference = (p) => p.nodeIds.some((id) => span2.has(id));
|
|
2092
|
-
const placedWalks = new Set(
|
|
2093
|
-
paths.filter((p) => p !== reference && meetsReference(p)).map((p) => p.name)
|
|
2094
|
-
);
|
|
2095
|
-
const origin = cut ? cut.start : referenceStart;
|
|
2096
|
-
return {
|
|
2097
|
-
origin,
|
|
2098
|
-
unit,
|
|
2099
|
-
reference: rowOf(reference),
|
|
2100
|
-
rows: paths.filter(
|
|
2101
|
-
(p) => p !== reference && (meetsReference(p) || !placedWalks.has(p.name))
|
|
2102
|
-
).map(rowOf).sort(
|
|
2103
|
-
(a, b) => Number(b.complete) - Number(a.complete) || b.bp - a.bp || a.label.localeCompare(b.label)
|
|
2104
|
-
)
|
|
2105
|
-
};
|
|
2106
|
-
}
|
|
2107
|
-
|
|
2108
|
-
// src/layout/walkRowLayout.ts
|
|
2109
|
-
var READOUT_ROOM = 1.3;
|
|
2110
|
-
function walkRowsExtent(walks) {
|
|
2111
|
-
let longest = walks.reference.bp;
|
|
2112
|
-
for (const row of walks.rows) {
|
|
2113
|
-
longest = Math.max(longest, row.bp);
|
|
2114
|
-
}
|
|
2115
|
-
return {
|
|
2116
|
-
maxX: walks.origin + longest * READOUT_ROOM,
|
|
2117
|
-
maxY: walks.rows.length * ROW_HEIGHT_PX
|
|
2118
|
-
};
|
|
2119
|
-
}
|
|
2120
|
-
function walkRowLayout(graph, region) {
|
|
2121
|
-
const backbone = backboneNodes(graph);
|
|
2122
|
-
const walks = walkRows(graph, region);
|
|
2123
|
-
if (backbone.length === 0 || !walks) {
|
|
2124
|
-
return void 0;
|
|
2125
|
-
}
|
|
2126
|
-
const nodePositions = backbonePositions(backbone);
|
|
2127
|
-
const rowLabels = [
|
|
2128
|
-
{ label: walks.reference.label, y: 0 },
|
|
2129
|
-
...walks.rows.map((row, i) => ({
|
|
2130
|
-
label: row.label,
|
|
2131
|
-
y: (i + 1) * ROW_HEIGHT_PX
|
|
2132
|
-
}))
|
|
2133
|
-
];
|
|
2134
|
-
return {
|
|
2135
|
-
nodePositions,
|
|
2136
|
-
rowLabels,
|
|
2137
|
-
referenceAxis: true,
|
|
2138
|
-
pixelRows: true,
|
|
2139
|
-
extent: walkRowsExtent(walks)
|
|
2140
|
-
};
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
// src/layoutModes.ts
|
|
2144
|
-
var FORCE_LAYOUT_LABEL = "Force-directed layout";
|
|
2145
|
-
var hasBackbone = (graph) => graph.nodes.some(isBackbone);
|
|
2146
|
-
var hasWalks = (graph) => hasBackbone(graph) && (graph.paths?.length ?? 0) > 1;
|
|
2147
|
-
var hasTubeMapBackbone = (graph) => hasBackbone(graph) && hasTubeMapPaths(graph);
|
|
2148
|
-
var hasAlleles = (graph) => hasBackbone(graph) && graph.nodes.some(isOffReference);
|
|
2149
|
-
var LAYOUT_MODES = [
|
|
2150
|
-
{
|
|
2151
|
-
value: "auto",
|
|
2152
|
-
label: "Anchored",
|
|
2153
|
-
description: "x is reference bp, one row per stable rank. Needs rGFA tags or a reference path.",
|
|
2154
|
-
run: anchoredLayout,
|
|
2155
|
-
cutMargins: true,
|
|
2156
|
-
wholeWalks: false,
|
|
2157
|
-
available: hasBackbone,
|
|
2158
|
-
drawsLocally: hasBackbone
|
|
2159
|
-
},
|
|
2160
|
-
{
|
|
2161
|
-
value: "samplerows",
|
|
2162
|
-
label: "Sample rows",
|
|
2163
|
-
description: "x is reference bp, one row per contributing assembly. Needs rGFA tags or a reference path.",
|
|
2164
|
-
run: sampleRowLayout,
|
|
2165
|
-
cutMargins: true,
|
|
2166
|
-
wholeWalks: false,
|
|
2167
|
-
available: hasAlleles,
|
|
2168
|
-
drawsLocally: hasAlleles
|
|
2169
|
-
},
|
|
2170
|
-
{
|
|
2171
|
-
value: "walkrows",
|
|
2172
|
-
label: "Walk rows",
|
|
2173
|
-
description: "x is each walk's own bp: one bar per haplotype, sequence the reference also carries in blue and sequence it does not in purple, so a repeat expansion reads as bar length. Needs W or P lines.",
|
|
2174
|
-
run: walkRowLayout,
|
|
2175
|
-
cutMargins: false,
|
|
2176
|
-
wholeWalks: true,
|
|
2177
|
-
available: hasWalks,
|
|
2178
|
-
drawsLocally: hasWalks
|
|
2179
|
-
},
|
|
2180
|
-
{
|
|
2181
|
-
value: "ordered",
|
|
2182
|
-
label: "Ordered",
|
|
2183
|
-
description: "x is reference order, not bp: every node gets room, bubbles read as lenses. Needs rGFA tags or a reference path.",
|
|
2184
|
-
run: orderedLayout,
|
|
2185
|
-
cutMargins: false,
|
|
2186
|
-
wholeWalks: false,
|
|
2187
|
-
available: hasBackbone,
|
|
2188
|
-
drawsLocally: hasBackbone
|
|
2189
|
-
},
|
|
2190
|
-
{
|
|
2191
|
-
value: "tubemap",
|
|
2192
|
-
label: "Tube map",
|
|
2193
|
-
description: "sequenceTubeMap: every path a coloured tube through boxed nodes, columns in node order and node widths log-scaled. Needs P or W lines.",
|
|
2194
|
-
run: tubeMapLayout,
|
|
2195
|
-
cutMargins: false,
|
|
2196
|
-
wholeWalks: false,
|
|
2197
|
-
available: hasTubeMapPaths,
|
|
2198
|
-
drawsLocally: hasTubeMapPaths
|
|
2199
|
-
},
|
|
2200
|
-
{
|
|
2201
|
-
value: "tubemapref",
|
|
2202
|
-
label: "Tube map on reference",
|
|
2203
|
-
description: "The tube map with each column at the reference bp it covers, so the tubes line up with the tracks around them. Needs P or W lines and a reference path.",
|
|
2204
|
-
run: tubeMapReferenceLayout,
|
|
2205
|
-
cutMargins: true,
|
|
2206
|
-
wholeWalks: false,
|
|
2207
|
-
available: hasTubeMapBackbone,
|
|
2208
|
-
drawsLocally: hasTubeMapBackbone
|
|
2209
|
-
},
|
|
2210
|
-
{
|
|
2211
|
-
value: "force",
|
|
2212
|
-
label: FORCE_LAYOUT_LABEL,
|
|
2213
|
-
description: "OGDF FMMM, via the external Bandage engine.",
|
|
2214
|
-
run: () => void 0,
|
|
2215
|
-
cutMargins: false,
|
|
2216
|
-
wholeWalks: false,
|
|
2217
|
-
available: () => true,
|
|
2218
|
-
drawsLocally: () => false
|
|
2219
|
-
}
|
|
2220
|
-
];
|
|
2221
|
-
var LAYOUT_MODE_VALUES = LAYOUT_MODES.map((m) => m.value);
|
|
2222
|
-
function modeUsesLayoutEngine(value, graph) {
|
|
2223
|
-
return !layoutModeByValue(value).drawsLocally(graph);
|
|
2224
|
-
}
|
|
2225
|
-
function layoutModeByValue(value) {
|
|
2226
|
-
return LAYOUT_MODES.find((m) => m.value === value) ?? LAYOUT_MODES[0];
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
// src/colorSchemes.ts
|
|
2230
|
-
var COLOR_SCHEMES = [
|
|
2231
|
-
// Mirrors `layoutModes`' 'auto': a value the model resolves rather than a
|
|
2232
|
-
// colour of its own. It exists because 'uniform' was the default, so a
|
|
2233
|
-
// launched graph opened flat grey and both tutorials spent a step saying "now
|
|
2234
|
-
// pick a colour" — one figure literally drives that click. A graph with
|
|
2235
|
-
// reference coordinates resolves to 'reference-position', which is the one
|
|
2236
|
-
// ramp a linear track beside it can be painted with too; a graph with none
|
|
2237
|
-
// keeps 'uniform', because there the ramp would be a hue nothing earned.
|
|
2238
|
-
//
|
|
2239
|
-
// Resolve through `effectiveColorScheme`; the dropdown reads the raw prop, so
|
|
2240
|
-
// "Auto" stays selectable and visible as what it is.
|
|
2241
|
-
{ value: "auto", label: "Auto" },
|
|
2242
|
-
{ value: "uniform", label: "Uniform" },
|
|
2243
|
-
{ value: "random", label: "Random" },
|
|
2244
|
-
{ value: "rainbow", label: "Rainbow" },
|
|
2245
|
-
{ value: "depth", label: "Depth" },
|
|
2246
|
-
{ value: "node-length", label: "Node Length" },
|
|
2247
|
-
{ value: "stable-rank", label: "Stable rank" },
|
|
2248
|
-
{ value: "reference-position", label: "Reference position" },
|
|
2249
|
-
{ value: "grey", label: "Grey" }
|
|
2250
|
-
];
|
|
2251
|
-
var COLOR_SCHEME_VALUES = COLOR_SCHEMES.map((s) => s.value);
|
|
2252
|
-
function resolveColorScheme(scheme, graph) {
|
|
2253
|
-
return scheme !== "auto" ? scheme : graph?.anchoredBy ? "reference-position" : "uniform";
|
|
2254
|
-
}
|
|
2255
|
-
|
|
2256
|
-
// src/nodeWidths.ts
|
|
2257
|
-
var NODE_WIDTHS = [
|
|
2258
|
-
{
|
|
2259
|
-
value: "depth",
|
|
2260
|
-
label: "By depth",
|
|
2261
|
-
description: "Thicker the more paths carry a node, on the square root of its depth against the mean. A graph with one depth everywhere draws uniform."
|
|
2262
|
-
},
|
|
2263
|
-
{
|
|
2264
|
-
value: "uniform",
|
|
2265
|
-
label: "Uniform",
|
|
2266
|
-
description: "Every node the same thickness."
|
|
2267
|
-
}
|
|
2268
|
-
];
|
|
2269
|
-
var NODE_WIDTH_VALUES = NODE_WIDTHS.map((w) => w.value);
|
|
2270
|
-
var MIN_FACTOR = 0.3;
|
|
2271
|
-
var MAX_FACTOR = 3;
|
|
2272
|
-
function meanDepth(graph) {
|
|
2273
|
-
let weighted = 0;
|
|
2274
|
-
let length = 0;
|
|
2275
|
-
for (const node of graph.nodes) {
|
|
2276
|
-
weighted += node.depth * node.length;
|
|
2277
|
-
length += node.length;
|
|
2278
|
-
}
|
|
2279
|
-
return length > 0 ? weighted / length : 0;
|
|
2280
|
-
}
|
|
2281
|
-
function depthWidthFactor(node, mean) {
|
|
2282
|
-
if (!(mean > 0) || !(node.depth > 0)) {
|
|
2283
|
-
return 1;
|
|
2284
|
-
}
|
|
2285
|
-
return Math.min(
|
|
2286
|
-
MAX_FACTOR,
|
|
2287
|
-
Math.max(MIN_FACTOR, Math.sqrt(node.depth / mean))
|
|
2288
|
-
);
|
|
2289
|
-
}
|
|
2290
|
-
function nodeWidthPx(node, contigThickness, nodeWidth2, mean) {
|
|
2291
|
-
return contigThickness * (nodeWidth2 === "depth" ? depthWidthFactor(node, mean) : 1);
|
|
2292
|
-
}
|
|
2293
|
-
function maxNodeWidthPx(contigThickness, nodeWidth2) {
|
|
2294
|
-
return contigThickness * (nodeWidth2 === "depth" ? MAX_FACTOR : 1);
|
|
2295
|
-
}
|
|
2296
|
-
function nodeInk(graph, nodeById, contigThickness, nodeWidth2) {
|
|
2297
|
-
const mean = graph ? meanDepth(graph) : 0;
|
|
2298
|
-
return {
|
|
2299
|
-
maxHalfWidthPx: maxNodeWidthPx(contigThickness, nodeWidth2) / 2,
|
|
2300
|
-
halfWidthPx: (id) => {
|
|
2301
|
-
const node = nodeById?.get(id);
|
|
2302
|
-
return node ? nodeWidthPx(node, contigThickness, nodeWidth2, mean) / 2 : contigThickness / 2;
|
|
2303
|
-
}
|
|
2304
|
-
};
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
// src/renderer/GeometryBuilder.ts
|
|
2308
|
-
import { packAbgr as packAbgr2 } from "@jbrowse/core/util/colorBits";
|
|
2309
|
-
|
|
2310
|
-
// src/renderer/colorBits.ts
|
|
2311
|
-
import {
|
|
2312
|
-
abgrAlpha,
|
|
2313
|
-
abgrBlue,
|
|
2314
|
-
abgrGreen,
|
|
2315
|
-
abgrRed,
|
|
2316
|
-
packAbgr
|
|
2317
|
-
} from "@jbrowse/core/util/colorBits";
|
|
2318
|
-
function fadeAbgr(c, alpha) {
|
|
2319
|
-
return packAbgr(
|
|
2320
|
-
abgrRed(c),
|
|
2321
|
-
abgrGreen(c),
|
|
2322
|
-
abgrBlue(c),
|
|
2323
|
-
Math.round(abgrAlpha(c) * alpha)
|
|
2324
|
-
);
|
|
2325
|
-
}
|
|
2326
|
-
function brightenAbgr(c, factor) {
|
|
2327
|
-
return packAbgr(
|
|
2328
|
-
Math.min(255, Math.round(abgrRed(c) * factor)),
|
|
2329
|
-
Math.min(255, Math.round(abgrGreen(c) * factor)),
|
|
2330
|
-
Math.min(255, Math.round(abgrBlue(c) * factor)),
|
|
2331
|
-
abgrAlpha(c)
|
|
2332
|
-
);
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
// src/util/geometry.ts
|
|
2336
|
-
function svgPath(points) {
|
|
2337
|
-
const round = (v) => Math.round(v * 100) / 100;
|
|
2338
|
-
return points.map((p, i) => `${i ? "L" : "M"}${round(p.x)},${round(p.y)}`).join("");
|
|
2339
|
-
}
|
|
2340
|
-
function projectLine(x1, y1, x2, y2, distance) {
|
|
2341
|
-
const d = Math.hypot(y2 - y1, x2 - x1);
|
|
2342
|
-
if (d === 0) {
|
|
2343
|
-
return [x2, y2];
|
|
2344
|
-
}
|
|
2345
|
-
const vx = (x2 - x1) / d;
|
|
2346
|
-
const vy = (y2 - y1) / d;
|
|
2347
|
-
return [x2 + distance * vx, y2 + distance * vy];
|
|
2348
|
-
}
|
|
2349
|
-
function curvePointAt(c, t) {
|
|
2350
|
-
const u = 1 - t;
|
|
2351
|
-
const a = u * u * u;
|
|
2352
|
-
const b = 3 * u * u * t;
|
|
2353
|
-
const d = 3 * u * t * t;
|
|
2354
|
-
const e = t * t * t;
|
|
2355
|
-
return {
|
|
2356
|
-
x: a * c.x0 + b * c.cx0 + d * c.cx1 + e * c.x1,
|
|
2357
|
-
y: a * c.y0 + b * c.cy0 + d * c.cy1 + e * c.y1
|
|
2358
|
-
};
|
|
2359
|
-
}
|
|
2360
|
-
function curveMidpoint(curves) {
|
|
2361
|
-
const mid = curves[Math.floor(curves.length / 2)];
|
|
2362
|
-
return mid ? curvePointAt(mid, 0.5) : void 0;
|
|
2363
|
-
}
|
|
2364
|
-
function curveBounds(curves) {
|
|
2365
|
-
let minX = Infinity;
|
|
2366
|
-
let minY = Infinity;
|
|
2367
|
-
let maxX = -Infinity;
|
|
2368
|
-
let maxY = -Infinity;
|
|
2369
|
-
for (const c of curves) {
|
|
2370
|
-
minX = Math.min(minX, c.x0, c.cx0, c.cx1, c.x1);
|
|
2371
|
-
maxX = Math.max(maxX, c.x0, c.cx0, c.cx1, c.x1);
|
|
2372
|
-
minY = Math.min(minY, c.y0, c.cy0, c.cy1, c.y1);
|
|
2373
|
-
maxY = Math.max(maxY, c.y0, c.cy0, c.cy1, c.y1);
|
|
2374
|
-
}
|
|
2375
|
-
return { minX, minY, maxX, maxY };
|
|
2376
|
-
}
|
|
2377
|
-
function subCurve(c, t0, t1) {
|
|
2378
|
-
const p0 = curvePointAt(c, t0);
|
|
2379
|
-
const p1 = curvePointAt(c, t1);
|
|
2380
|
-
const dt = (t1 - t0) / 3;
|
|
2381
|
-
const d0 = curveTangentAt(c, t0);
|
|
2382
|
-
const d1 = curveTangentAt(c, t1);
|
|
2383
|
-
return {
|
|
2384
|
-
x0: p0.x,
|
|
2385
|
-
y0: p0.y,
|
|
2386
|
-
cx0: p0.x + d0.x * dt,
|
|
2387
|
-
cy0: p0.y + d0.y * dt,
|
|
2388
|
-
cx1: p1.x - d1.x * dt,
|
|
2389
|
-
cy1: p1.y - d1.y * dt,
|
|
2390
|
-
x1: p1.x,
|
|
2391
|
-
y1: p1.y
|
|
2392
|
-
};
|
|
2393
|
-
}
|
|
2394
|
-
function curveTangentAt(c, t) {
|
|
2395
|
-
const u = 1 - t;
|
|
2396
|
-
const a = 3 * u * u;
|
|
2397
|
-
const b = 6 * u * t;
|
|
2398
|
-
const d = 3 * t * t;
|
|
2399
|
-
return {
|
|
2400
|
-
x: a * (c.cx0 - c.x0) + b * (c.cx1 - c.cx0) + d * (c.x1 - c.cx1),
|
|
2401
|
-
y: a * (c.cy0 - c.y0) + b * (c.cy1 - c.cy0) + d * (c.y1 - c.cy1)
|
|
2402
|
-
};
|
|
2403
|
-
}
|
|
2404
|
-
function dashCurves(curves, dashWorld) {
|
|
2405
|
-
const out = [];
|
|
2406
|
-
for (const c of curves) {
|
|
2407
|
-
const polyLen = 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);
|
|
2408
|
-
const spans = Math.max(3, Math.round(polyLen / dashWorld) | 1);
|
|
2409
|
-
for (let i = 0; i < spans; i += 2) {
|
|
2410
|
-
out.push([subCurve(c, i / spans, (i + 1) / spans)]);
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
return out;
|
|
2414
|
-
}
|
|
2415
|
-
function translateCurves(curves, dx, dy) {
|
|
2416
|
-
const out = new Array(curves.length);
|
|
2417
|
-
for (let i = 0; i < curves.length; i++) {
|
|
2418
|
-
const c = curves[i];
|
|
2419
|
-
out[i] = {
|
|
2420
|
-
x0: c.x0 + dx,
|
|
2421
|
-
y0: c.y0 + dy,
|
|
2422
|
-
cx0: c.cx0 + dx,
|
|
2423
|
-
cy0: c.cy0 + dy,
|
|
2424
|
-
cx1: c.cx1 + dx,
|
|
2425
|
-
cy1: c.cy1 + dy,
|
|
2426
|
-
x1: c.x1 + dx,
|
|
2427
|
-
y1: c.y1 + dy
|
|
2428
|
-
};
|
|
2429
|
-
}
|
|
2430
|
-
return out;
|
|
2431
|
-
}
|
|
2432
|
-
function attachment(segments, side) {
|
|
2433
|
-
const last = segments.length - 1;
|
|
2434
|
-
return side === "end" ? { at: segments[last], inward: segments[last - 1] } : { at: segments[0], inward: segments[1] };
|
|
2435
|
-
}
|
|
2436
|
-
var TANGENT_SPAN_FACTOR = 0.5;
|
|
2437
|
-
var MAX_TANGENT = 80;
|
|
2438
|
-
function spanOf(segments) {
|
|
2439
|
-
let min = Infinity;
|
|
2440
|
-
let max = -Infinity;
|
|
2441
|
-
for (const s of segments) {
|
|
2442
|
-
min = Math.min(min, s.x);
|
|
2443
|
-
max = Math.max(max, s.x);
|
|
2444
|
-
}
|
|
2445
|
-
return { min, max };
|
|
2446
|
-
}
|
|
2447
|
-
function distanceToSpan(x, span2) {
|
|
2448
|
-
return Math.max(0, span2.min - x, x - span2.max);
|
|
2449
|
-
}
|
|
2450
|
-
function facingSide(segments, other, tieBreak) {
|
|
2451
|
-
const startGap = distanceToSpan(segments[0].x, other);
|
|
2452
|
-
const endGap = distanceToSpan(segments[segments.length - 1].x, other);
|
|
2453
|
-
return startGap === endGap ? tieBreak : startGap < endGap ? "start" : "end";
|
|
2454
|
-
}
|
|
2455
|
-
function facingSides(fromSegments, toSegments) {
|
|
2456
|
-
return {
|
|
2457
|
-
from: facingSide(fromSegments, spanOf(toSegments), "end"),
|
|
2458
|
-
to: facingSide(toSegments, spanOf(fromSegments), "start")
|
|
2459
|
-
};
|
|
2460
|
-
}
|
|
2461
|
-
var APEX_FRACTION = 0.75;
|
|
2462
|
-
var BOW_CLEARANCE = 1.15;
|
|
2463
|
-
var ALONG_FRACTION = 0.35;
|
|
2464
|
-
var MAX_ROW_BOW_PX = 3 * ROW_HEIGHT_PX;
|
|
2465
|
-
function bowAround(p1x, p1y, p2x, p2y, toward, maxBow) {
|
|
2466
|
-
const chord = Math.hypot(p2x - p1x, p2y - p1y);
|
|
2467
|
-
const ux = chord === 0 ? 1 : (p2x - p1x) / chord;
|
|
2468
|
-
const uy = chord === 0 ? 0 : (p2y - p1y) / chord;
|
|
2469
|
-
let reach = 0;
|
|
2470
|
-
let minAlong = Infinity;
|
|
2471
|
-
let maxAlong = -Infinity;
|
|
2472
|
-
for (const p of toward) {
|
|
2473
|
-
const dx = p.x - p1x;
|
|
2474
|
-
const dy = p.y - p1y;
|
|
2475
|
-
const perp = dx * -uy + dy * ux;
|
|
2476
|
-
if (Math.abs(perp) > Math.abs(reach)) {
|
|
2477
|
-
reach = perp;
|
|
2478
|
-
}
|
|
2479
|
-
const along = dx * ux + dy * uy;
|
|
2480
|
-
minAlong = Math.min(minAlong, along);
|
|
2481
|
-
maxAlong = Math.max(maxAlong, along);
|
|
2482
|
-
}
|
|
2483
|
-
const span2 = toward.length ? maxAlong - minAlong : 0;
|
|
2484
|
-
const uncapped = Math.max(
|
|
2485
|
-
Math.abs(reach) * BOW_CLEARANCE / APEX_FRACTION,
|
|
2486
|
-
span2 * ALONG_FRACTION
|
|
2487
|
-
);
|
|
2488
|
-
const size = maxBow === void 0 ? uncapped : Math.min(uncapped, maxBow);
|
|
2489
|
-
return reach < 0 ? -size : size;
|
|
2490
|
-
}
|
|
2491
|
-
function yToXOf({ scaleX, scaleY }) {
|
|
2492
|
-
return scaleY / scaleX;
|
|
2493
|
-
}
|
|
2494
|
-
var PATH_RIBBON_SPACING = 3;
|
|
2495
|
-
function drawnDirection(a, b, yToX) {
|
|
2496
|
-
const dx = b.x - a.x;
|
|
2497
|
-
const dy = (b.y - a.y) * yToX;
|
|
2498
|
-
const len = Math.hypot(dx, dy);
|
|
2499
|
-
return len === 0 ? void 0 : { x: dx / len, y: dy / len };
|
|
2500
|
-
}
|
|
2501
|
-
function pathRibbonOffsets(fromSegments, toSegments, count, axis, spacing = PATH_RIBBON_SPACING) {
|
|
2502
|
-
const yToX = yToXOf(axis);
|
|
2503
|
-
const dir = drawnDirection(
|
|
2504
|
-
fromSegments[fromSegments.length - 1],
|
|
2505
|
-
toSegments[0],
|
|
2506
|
-
yToX
|
|
2507
|
-
) ?? drawnDirection(fromSegments[0], fromSegments.at(-1), yToX) ?? drawnDirection(toSegments[0], toSegments.at(-1), yToX) ?? { x: 1, y: 0 };
|
|
2508
|
-
const offsets = [];
|
|
2509
|
-
for (let i = 0; i < count; i++) {
|
|
2510
|
-
const distance = (i - (count - 1) / 2) * spacing;
|
|
2511
|
-
offsets.push({ x: -dir.y * distance, y: dir.x * distance / yToX });
|
|
2512
|
-
}
|
|
2513
|
-
return offsets;
|
|
2514
|
-
}
|
|
2515
|
-
function scaleYOf(points, yToX) {
|
|
2516
|
-
return points.map((p) => ({ x: p.x, y: p.y * yToX }));
|
|
2517
|
-
}
|
|
2518
|
-
function unscaleYOf(curves, yToX) {
|
|
2519
|
-
for (const c of curves) {
|
|
2520
|
-
c.y0 /= yToX;
|
|
2521
|
-
c.cy0 /= yToX;
|
|
2522
|
-
c.cy1 /= yToX;
|
|
2523
|
-
c.y1 /= yToX;
|
|
2524
|
-
}
|
|
2525
|
-
return curves;
|
|
2526
|
-
}
|
|
2527
|
-
var SELF_LOOP_MIN_PX = 12;
|
|
2528
|
-
var SELF_LOOP_MAX_PX = 40;
|
|
2529
|
-
function selfLoopReach(segments, scale) {
|
|
2530
|
-
let length = 0;
|
|
2531
|
-
for (let i = 1; i < segments.length; i++) {
|
|
2532
|
-
length += Math.hypot(
|
|
2533
|
-
segments[i].x - segments[i - 1].x,
|
|
2534
|
-
segments[i].y - segments[i - 1].y
|
|
2535
|
-
);
|
|
2536
|
-
}
|
|
2537
|
-
const nodePx = length * scale;
|
|
2538
|
-
const reachPx = Math.min(
|
|
2539
|
-
SELF_LOOP_MAX_PX,
|
|
2540
|
-
Math.max(SELF_LOOP_MIN_PX, nodePx / 2)
|
|
2541
|
-
);
|
|
2542
|
-
return reachPx / Math.max(scale, Number.EPSILON);
|
|
2543
|
-
}
|
|
2544
|
-
function selfLinkOf(edge) {
|
|
2545
|
-
if (edge.from !== edge.to) {
|
|
2546
|
-
return false;
|
|
2547
|
-
}
|
|
2548
|
-
return edge.fromStrand && edge.toStrand && edge.fromStrand !== edge.toStrand ? edge.fromStrand === "+" ? "end" : "start" : true;
|
|
2549
|
-
}
|
|
2550
|
-
function hairpinCurves(attach, reach, offsetX, offsetY) {
|
|
2551
|
-
const { at, inward } = attach;
|
|
2552
|
-
const dx = inward ? at.x - inward.x : 1;
|
|
2553
|
-
const dy = inward ? at.y - inward.y : 0;
|
|
2554
|
-
const len = Math.hypot(dx, dy) || 1;
|
|
2555
|
-
const ux = dx / len;
|
|
2556
|
-
const uy = dy / len;
|
|
2557
|
-
const px = at.x + offsetX;
|
|
2558
|
-
const py = at.y + offsetY;
|
|
2559
|
-
const nearX = px + ux * reach / 3;
|
|
2560
|
-
const nearY = py + uy * reach / 3;
|
|
2561
|
-
const apexX = px + ux * reach;
|
|
2562
|
-
const apexY = py + uy * reach;
|
|
2563
|
-
const sideX = -uy * reach / 2;
|
|
2564
|
-
const sideY = ux * reach / 2;
|
|
2565
|
-
return [
|
|
2566
|
-
{
|
|
2567
|
-
x0: px,
|
|
2568
|
-
y0: py,
|
|
2569
|
-
cx0: nearX,
|
|
2570
|
-
cy0: nearY,
|
|
2571
|
-
cx1: apexX + sideX,
|
|
2572
|
-
cy1: apexY + sideY,
|
|
2573
|
-
x1: apexX,
|
|
2574
|
-
y1: apexY
|
|
2575
|
-
},
|
|
2576
|
-
{
|
|
2577
|
-
x0: apexX,
|
|
2578
|
-
y0: apexY,
|
|
2579
|
-
cx0: apexX - sideX,
|
|
2580
|
-
cy0: apexY - sideY,
|
|
2581
|
-
cx1: nearX,
|
|
2582
|
-
cy1: nearY,
|
|
2583
|
-
x1: px,
|
|
2584
|
-
y1: py
|
|
2585
|
-
}
|
|
2586
|
-
];
|
|
2587
|
-
}
|
|
2588
|
-
function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY, axis, bypassed = [], maxBowIn) {
|
|
2589
|
-
const scale = axis.scaleX;
|
|
2590
|
-
const yToX = yToXOf(axis);
|
|
2591
|
-
const maxBow = maxBowIn ?? (axis.pixelRows ? MAX_ROW_BOW_PX : void 0);
|
|
2592
|
-
if (yToX !== 1) {
|
|
2593
|
-
return unscaleYOf(
|
|
2594
|
-
computeEdgeCurves(
|
|
2595
|
-
scaleYOf(fromSegments, yToX),
|
|
2596
|
-
scaleYOf(toSegments, yToX),
|
|
2597
|
-
selfLink,
|
|
2598
|
-
offsetX,
|
|
2599
|
-
offsetY * yToX,
|
|
2600
|
-
{ scaleX: scale, scaleY: scale },
|
|
2601
|
-
scaleYOf(bypassed, yToX),
|
|
2602
|
-
// the cap is a y measure and the body works in x units, so it converts
|
|
2603
|
-
// with everything else that crosses this boundary
|
|
2604
|
-
maxBow === void 0 ? void 0 : maxBow * yToX
|
|
2605
|
-
),
|
|
2606
|
-
yToX
|
|
2607
|
-
);
|
|
2608
|
-
}
|
|
2609
|
-
if (typeof selfLink === "string") {
|
|
2610
|
-
return hairpinCurves(
|
|
2611
|
-
attachment(fromSegments, selfLink),
|
|
2612
|
-
selfLoopReach(fromSegments, scale),
|
|
2613
|
-
offsetX,
|
|
2614
|
-
offsetY
|
|
2615
|
-
);
|
|
2616
|
-
}
|
|
2617
|
-
const sides2 = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
|
|
2618
|
-
const fromAttach = attachment(fromSegments, sides2.from);
|
|
2619
|
-
const toAttach = attachment(toSegments, sides2.to);
|
|
2620
|
-
const fromEnd = fromAttach.at;
|
|
2621
|
-
const toStart = toAttach.at;
|
|
2622
|
-
const p1x = fromEnd.x + offsetX;
|
|
2623
|
-
const p1y = fromEnd.y + offsetY;
|
|
2624
|
-
const p2x = toStart.x + offsetX;
|
|
2625
|
-
const p2y = toStart.y + offsetY;
|
|
2626
|
-
if (selfLink) {
|
|
2627
|
-
let segDirX = 1;
|
|
2628
|
-
let segDirY = 0;
|
|
2629
|
-
if (fromAttach.inward) {
|
|
2630
|
-
const dx = fromEnd.x - fromAttach.inward.x;
|
|
2631
|
-
const dy = fromEnd.y - fromAttach.inward.y;
|
|
2632
|
-
const len = Math.hypot(dx, dy);
|
|
2633
|
-
if (len > 0) {
|
|
2634
|
-
segDirX = dx / len;
|
|
2635
|
-
segDirY = dy / len;
|
|
2636
|
-
}
|
|
2637
|
-
}
|
|
2638
|
-
const ext = selfLoopReach(fromSegments, scale);
|
|
2639
|
-
const perpX = -segDirY;
|
|
2640
|
-
const perpY = segDirX;
|
|
2641
|
-
const midX = (fromEnd.x + toStart.x) / 2 + offsetX + perpX * ext;
|
|
2642
|
-
const midY = (fromEnd.y + toStart.y) / 2 + offsetY + perpY * ext;
|
|
2643
|
-
const cp1x = p1x + segDirX * ext;
|
|
2644
|
-
const cp1y = p1y + segDirY * ext;
|
|
2645
|
-
const cp2x = p2x - segDirX * ext;
|
|
2646
|
-
const cp2y = p2y - segDirY * ext;
|
|
2647
|
-
return [
|
|
2648
|
-
{
|
|
2649
|
-
x0: p1x,
|
|
2650
|
-
y0: p1y,
|
|
2651
|
-
cx0: cp1x,
|
|
2652
|
-
cy0: cp1y,
|
|
2653
|
-
cx1: cp1x + perpX * ext,
|
|
2654
|
-
cy1: cp1y + perpY * ext,
|
|
2655
|
-
x1: midX,
|
|
2656
|
-
y1: midY
|
|
2657
|
-
},
|
|
2658
|
-
{
|
|
2659
|
-
x0: midX,
|
|
2660
|
-
y0: midY,
|
|
2661
|
-
cx0: cp2x + perpX * ext,
|
|
2662
|
-
cy0: cp2y + perpY * ext,
|
|
2663
|
-
cx1: cp2x,
|
|
2664
|
-
cy1: cp2y,
|
|
2665
|
-
x1: p2x,
|
|
2666
|
-
y1: p2y
|
|
2667
|
-
}
|
|
2668
|
-
];
|
|
2669
|
-
} else {
|
|
2670
|
-
const fromPrev = fromAttach.inward ?? {
|
|
2671
|
-
x: fromEnd.x - (toStart.x - fromEnd.x),
|
|
2672
|
-
y: fromEnd.y - (toStart.y - fromEnd.y)
|
|
2673
|
-
};
|
|
2674
|
-
const toNext = toAttach.inward ?? {
|
|
2675
|
-
x: toStart.x - (fromEnd.x - toStart.x),
|
|
2676
|
-
y: toStart.y - (fromEnd.y - toStart.y)
|
|
2677
|
-
};
|
|
2678
|
-
const dist = Math.hypot(p2x - p1x, p2y - p1y);
|
|
2679
|
-
const tangentProj = (prev, at, towardX, towardY) => {
|
|
2680
|
-
const dx = at.x - prev.x;
|
|
2681
|
-
const dy = at.y - prev.y;
|
|
2682
|
-
const len = Math.hypot(dx, dy);
|
|
2683
|
-
const along = len === 0 ? 0 : (towardX * dx + towardY * dy) / len;
|
|
2684
|
-
return Math.max(
|
|
2685
|
-
0,
|
|
2686
|
-
Math.min(dist * TANGENT_SPAN_FACTOR, MAX_TANGENT, along * 0.5)
|
|
2687
|
-
);
|
|
2688
|
-
};
|
|
2689
|
-
const towardToX = toStart.x - fromEnd.x;
|
|
2690
|
-
const towardToY = toStart.y - fromEnd.y;
|
|
2691
|
-
const [cx1, cy1] = projectLine(
|
|
2692
|
-
fromPrev.x,
|
|
2693
|
-
fromPrev.y,
|
|
2694
|
-
fromEnd.x,
|
|
2695
|
-
fromEnd.y,
|
|
2696
|
-
tangentProj(fromPrev, fromEnd, towardToX, towardToY)
|
|
2697
|
-
);
|
|
2698
|
-
const [cx2, cy2] = projectLine(
|
|
2699
|
-
toNext.x,
|
|
2700
|
-
toNext.y,
|
|
2701
|
-
toStart.x,
|
|
2702
|
-
toStart.y,
|
|
2703
|
-
tangentProj(toNext, toStart, -towardToX, -towardToY)
|
|
2704
|
-
);
|
|
2705
|
-
const bulge = bypassed.length ? bowAround(p1x, p1y, p2x, p2y, bypassed, maxBow) : 0;
|
|
2706
|
-
const chordLen = dist === 0 ? 1 : dist;
|
|
2707
|
-
const ux = (p2x - p1x) / chordLen;
|
|
2708
|
-
const uy = (p2y - p1y) / chordLen;
|
|
2709
|
-
const bulgeX = -uy * bulge;
|
|
2710
|
-
const bulgeY = ux * bulge;
|
|
2711
|
-
const spreadX = ux * Math.abs(bulge);
|
|
2712
|
-
const spreadY = uy * Math.abs(bulge);
|
|
2713
|
-
return [
|
|
2714
|
-
{
|
|
2715
|
-
x0: p1x,
|
|
2716
|
-
y0: p1y,
|
|
2717
|
-
cx0: cx1 + offsetX + bulgeX - spreadX,
|
|
2718
|
-
cy0: cy1 + offsetY + bulgeY - spreadY,
|
|
2719
|
-
cx1: cx2 + offsetX + bulgeX + spreadX,
|
|
2720
|
-
cy1: cy2 + offsetY + bulgeY + spreadY,
|
|
2721
|
-
x1: p2x,
|
|
2722
|
-
y1: p2y
|
|
2723
|
-
}
|
|
2724
|
-
];
|
|
2725
|
-
}
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
// src/deletionEdges.ts
|
|
2729
|
-
var MIN_DELETION_BP2 = 1;
|
|
2730
|
-
function bypassedPoints(nodePositions, bypassed) {
|
|
2731
|
-
return bypassed.flatMap((id) => nodePositions[id] ?? []);
|
|
2732
|
-
}
|
|
2733
|
-
function deletionArcCurves(nodePositions, deletion, axis) {
|
|
2734
|
-
const from = nodePositions[deletion.from];
|
|
2735
|
-
const to = nodePositions[deletion.to];
|
|
2736
|
-
return from?.length && to?.length ? computeEdgeCurves(
|
|
2737
|
-
from,
|
|
2738
|
-
to,
|
|
2739
|
-
deletion.from === deletion.to,
|
|
2740
|
-
0,
|
|
2741
|
-
0,
|
|
2742
|
-
axis,
|
|
2743
|
-
bypassedPoints(nodePositions, deletion.bypassed)
|
|
2744
|
-
) : void 0;
|
|
2745
|
-
}
|
|
2746
|
-
function backboneBySequence(graph) {
|
|
2747
|
-
const bySequence = /* @__PURE__ */ new Map();
|
|
2748
|
-
for (const node of graph.nodes) {
|
|
2749
|
-
if (isBackbone(node)) {
|
|
2750
|
-
const list = bySequence.get(node.stable.refName);
|
|
2751
|
-
if (list) {
|
|
2752
|
-
list.push(node);
|
|
2753
|
-
} else {
|
|
2754
|
-
bySequence.set(node.stable.refName, [node]);
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
for (const list of bySequence.values()) {
|
|
2759
|
-
list.sort((a, b) => a.stable.start - b.stable.start);
|
|
2760
|
-
}
|
|
2761
|
-
return bySequence;
|
|
2762
|
-
}
|
|
2763
|
-
function bypassedNodes(nodes, start, end) {
|
|
2764
|
-
const bypassed = [];
|
|
2765
|
-
if (nodes) {
|
|
2766
|
-
for (let i = firstNodeAtOrAfter(nodes, start); i < nodes.length; i++) {
|
|
2767
|
-
const node = nodes[i];
|
|
2768
|
-
if (node.stable.start >= end) {
|
|
2769
|
-
break;
|
|
2770
|
-
}
|
|
2771
|
-
if (node.stable.start + node.length <= end) {
|
|
2772
|
-
bypassed.push(node.id);
|
|
2773
|
-
}
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
return bypassed;
|
|
2777
|
-
}
|
|
2778
|
-
function skipsBetween(edge, from, to, forwards) {
|
|
2779
|
-
const { fromStrand, toStrand } = edge;
|
|
2780
|
-
if (fromStrand === void 0 || toStrand === void 0) {
|
|
2781
|
-
return true;
|
|
2782
|
-
}
|
|
2783
|
-
const leavesRight = fromStrand === (from.stable.strand ?? "+");
|
|
2784
|
-
const arrivesLeft = toStrand === (to.stable.strand ?? "+");
|
|
2785
|
-
return forwards ? leavesRight && arrivesLeft : !leavesRight && !arrivesLeft;
|
|
2786
|
-
}
|
|
2787
|
-
function deletionEdges(graph) {
|
|
2788
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
2789
|
-
const backbone = backboneBySequence(graph);
|
|
2790
|
-
const found = [];
|
|
2791
|
-
for (let edgeIndex = 0; edgeIndex < graph.edges.length; edgeIndex++) {
|
|
2792
|
-
const edge = graph.edges[edgeIndex];
|
|
2793
|
-
const from = byId.get(edge.from);
|
|
2794
|
-
const to = byId.get(edge.to);
|
|
2795
|
-
if (from !== void 0 && to !== void 0 && isBackbone(from) && isBackbone(to) && from.stable.refName === to.stable.refName) {
|
|
2796
|
-
const forwards = from.stable.start <= to.stable.start;
|
|
2797
|
-
const [left, right] = forwards ? [from, to] : [to, from];
|
|
2798
|
-
const start = left.stable.start + left.length;
|
|
2799
|
-
const end = right.stable.start;
|
|
2800
|
-
const bp = end - start;
|
|
2801
|
-
if (bp >= MIN_DELETION_BP2 && skipsBetween(edge, from, to, forwards)) {
|
|
2802
|
-
const refName = left.stable.refName;
|
|
2803
|
-
found.push({
|
|
2804
|
-
edgeIndex,
|
|
2805
|
-
from: edge.from,
|
|
2806
|
-
to: edge.to,
|
|
2807
|
-
refName,
|
|
2808
|
-
start,
|
|
2809
|
-
end,
|
|
2810
|
-
bp,
|
|
2811
|
-
bypassed: bypassedNodes(backbone.get(refName), start, end)
|
|
2812
|
-
});
|
|
2813
|
-
}
|
|
2814
|
-
}
|
|
2815
|
-
}
|
|
2816
|
-
return found;
|
|
2817
|
-
}
|
|
2818
|
-
|
|
2819
|
-
// src/util/edgeCurves.ts
|
|
2820
|
-
var cache = /* @__PURE__ */ new WeakMap();
|
|
2821
|
-
function baseEdgeCurves(nodePositions, graph, axis, deletions, version) {
|
|
2822
|
-
const { scaleX, scaleY } = axis;
|
|
2823
|
-
const cached2 = cache.get(nodePositions);
|
|
2824
|
-
if (cached2?.graph === graph && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.deletions === deletions && cached2.version === version) {
|
|
2825
|
-
return cached2.curves;
|
|
2826
|
-
}
|
|
2827
|
-
const curves = /* @__PURE__ */ new Map();
|
|
2828
|
-
for (let ei = 0; ei < graph.edges.length; ei++) {
|
|
2829
|
-
const edge = graph.edges[ei];
|
|
2830
|
-
const from = nodePositions[edge.from];
|
|
2831
|
-
const to = nodePositions[edge.to];
|
|
2832
|
-
if (from?.length && to?.length) {
|
|
2833
|
-
const bypassed = deletions?.get(ei);
|
|
2834
|
-
curves.set(
|
|
2835
|
-
ei,
|
|
2836
|
-
computeEdgeCurves(
|
|
2837
|
-
from,
|
|
2838
|
-
to,
|
|
2839
|
-
selfLinkOf(edge),
|
|
2840
|
-
0,
|
|
2841
|
-
0,
|
|
2842
|
-
axis,
|
|
2843
|
-
bypassed ? bypassedPoints(nodePositions, bypassed) : []
|
|
2844
|
-
)
|
|
2845
|
-
);
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
cache.set(nodePositions, {
|
|
2849
|
-
graph,
|
|
2850
|
-
scaleX,
|
|
2851
|
-
scaleY,
|
|
2852
|
-
deletions,
|
|
2853
|
-
version,
|
|
2854
|
-
curves
|
|
2855
|
-
});
|
|
2856
|
-
return curves;
|
|
2857
|
-
}
|
|
2858
|
-
|
|
2859
|
-
// src/renderer/GeometryBuilder.ts
|
|
2860
|
-
var EDGE_DEFAULT_COLOR = packAbgr2(119, 119, 119, 217);
|
|
2861
|
-
var EDGE_PATH_FALLBACK_COLOR = packAbgr2(136, 136, 136, 217);
|
|
2862
|
-
var EDGE_DELETION_COLOR = packAbgr2(24, 24, 28, 240);
|
|
2863
|
-
var DELETION_THICKNESS_FACTOR = 2.2;
|
|
2864
|
-
var LIFT_BACKDROP_COLOR = packAbgr2(160, 160, 160, 255);
|
|
2865
|
-
var FADED_ALPHA = 0.18;
|
|
2866
|
-
var DELETION_DASH_PX = 11;
|
|
2867
|
-
var MIN_PATH_STRIPE_PX = 1.2;
|
|
2868
|
-
var MIN_WALK_LANE_PX = 4;
|
|
2869
|
-
var MIN_SCALE_FOR_OFFSET = 1e-6;
|
|
2870
|
-
var ARROW_LENGTH_BASE_PX = 7;
|
|
2871
|
-
var ARROW_LENGTH_PER_EDGE_PX = 3;
|
|
2872
|
-
var ARROW_HALF_WIDTH_BASE_PX = 3;
|
|
2873
|
-
var ARROW_HALF_WIDTH_PER_EDGE_PX = 1.5;
|
|
2874
|
-
var MIN_ARROW_FIT = 0.5;
|
|
2875
|
-
var MIN_ARROW_SCALE = 0.45;
|
|
2876
|
-
var ARROW_CURVE_STEPS = 12;
|
|
2877
|
-
var ABUTTING_PX = 1e-6;
|
|
2878
|
-
var walkX = new Float64Array(64);
|
|
2879
|
-
var walkY = new Float64Array(64);
|
|
2880
|
-
var walkPx = new Float64Array(64);
|
|
2881
|
-
function backFromEnd(curves, scale, yToX, untilPx) {
|
|
2882
|
-
const most = curves.length * ARROW_CURVE_STEPS + 1;
|
|
2883
|
-
if (walkX.length < most) {
|
|
2884
|
-
walkX = new Float64Array(most);
|
|
2885
|
-
walkY = new Float64Array(most);
|
|
2886
|
-
walkPx = new Float64Array(most);
|
|
2887
|
-
}
|
|
2888
|
-
const last = curves[curves.length - 1];
|
|
2889
|
-
walkX[0] = last.x1;
|
|
2890
|
-
walkY[0] = last.y1;
|
|
2891
|
-
walkPx[0] = 0;
|
|
2892
|
-
let n = 1;
|
|
2893
|
-
let px = 0;
|
|
2894
|
-
for (let ci = curves.length - 1; ci >= 0 && px < untilPx; ci--) {
|
|
2895
|
-
const c = curves[ci];
|
|
2896
|
-
for (let k = 1; k <= ARROW_CURVE_STEPS && px < untilPx; k++) {
|
|
2897
|
-
const t = 1 - (k / ARROW_CURVE_STEPS) ** 2;
|
|
2898
|
-
const u = 1 - t;
|
|
2899
|
-
const a = u * u * u;
|
|
2900
|
-
const b = 3 * u * u * t;
|
|
2901
|
-
const d = 3 * u * t * t;
|
|
2902
|
-
const e = t * t * t;
|
|
2903
|
-
const x = a * c.x0 + b * c.cx0 + d * c.cx1 + e * c.x1;
|
|
2904
|
-
const y = a * c.y0 + b * c.cy0 + d * c.cy1 + e * c.y1;
|
|
2905
|
-
px += Math.hypot(x - walkX[n - 1], (y - walkY[n - 1]) * yToX) * scale;
|
|
2906
|
-
walkX[n] = x;
|
|
2907
|
-
walkY[n] = y;
|
|
2908
|
-
walkPx[n] = px;
|
|
2909
|
-
n++;
|
|
2910
|
-
}
|
|
2911
|
-
}
|
|
2912
|
-
return n;
|
|
2913
|
-
}
|
|
2914
|
-
function walkPointAt(n, px) {
|
|
2915
|
-
let i = 1;
|
|
2916
|
-
while (i < n - 1 && walkPx[i] < px) {
|
|
2917
|
-
i++;
|
|
2918
|
-
}
|
|
2919
|
-
const from = walkPx[i - 1];
|
|
2920
|
-
const to = walkPx[i];
|
|
2921
|
-
const f = to > from ? Math.min(1, Math.max(0, (px - from) / (to - from))) : 0;
|
|
2922
|
-
return {
|
|
2923
|
-
x: walkX[i - 1] + (walkX[i] - walkX[i - 1]) * f,
|
|
2924
|
-
y: walkY[i - 1] + (walkY[i] - walkY[i - 1]) * f
|
|
2925
|
-
};
|
|
2926
|
-
}
|
|
2927
|
-
function arrowheadFor({
|
|
2928
|
-
curves,
|
|
2929
|
-
edgeHalfWidth,
|
|
2930
|
-
nodeHalfWidth,
|
|
2931
|
-
scale,
|
|
2932
|
-
yToX,
|
|
2933
|
-
color,
|
|
2934
|
-
fromSegments,
|
|
2935
|
-
toSegments
|
|
2936
|
-
}) {
|
|
2937
|
-
const length = ARROW_LENGTH_BASE_PX + ARROW_LENGTH_PER_EDGE_PX * edgeHalfWidth;
|
|
2938
|
-
const n = backFromEnd(curves, scale, yToX, nodeHalfWidth + length);
|
|
2939
|
-
const lengthPx = walkPx[n - 1];
|
|
2940
|
-
const inset = Math.min(nodeHalfWidth, lengthPx);
|
|
2941
|
-
const room = lengthPx < ABUTTING_PX ? polylinePx(fromSegments, scale, yToX) : lengthPx - inset;
|
|
2942
|
-
const fit = Math.min(1, room / length);
|
|
2943
|
-
if (fit < MIN_ARROW_FIT) {
|
|
2944
|
-
return void 0;
|
|
2945
|
-
}
|
|
2946
|
-
const tip = walkPointAt(n, inset);
|
|
2947
|
-
const base = walkPointAt(n, inset + length * fit);
|
|
2948
|
-
const dx = tip.x - base.x;
|
|
2949
|
-
const dy = (tip.y - base.y) * yToX;
|
|
2950
|
-
const last = curves[curves.length - 1];
|
|
2951
|
-
return {
|
|
2952
|
-
x: tip.x,
|
|
2953
|
-
y: tip.y,
|
|
2954
|
-
angle: Math.hypot(dx, dy) > 0 ? Math.atan2(dy, dx) : endTangent(last, yToX, intoDirection(last, toSegments)),
|
|
2955
|
-
length: length * fit,
|
|
2956
|
-
halfWidth: (ARROW_HALF_WIDTH_BASE_PX + ARROW_HALF_WIDTH_PER_EDGE_PX * edgeHalfWidth) * fit,
|
|
2957
|
-
color
|
|
2958
|
-
};
|
|
2959
|
-
}
|
|
2960
|
-
function polylinePx(segments, scale, yToX) {
|
|
2961
|
-
let length = 0;
|
|
2962
|
-
for (let i = 1; i < segments.length; i++) {
|
|
2963
|
-
length += Math.hypot(
|
|
2964
|
-
segments[i].x - segments[i - 1].x,
|
|
2965
|
-
(segments[i].y - segments[i - 1].y) * yToX
|
|
2966
|
-
);
|
|
2967
|
-
}
|
|
2968
|
-
return length * scale;
|
|
2969
|
-
}
|
|
2970
|
-
function intoDirection(last, toSegments) {
|
|
2971
|
-
if (toSegments.length < 2) {
|
|
2972
|
-
return void 0;
|
|
2973
|
-
}
|
|
2974
|
-
const first = toSegments[0];
|
|
2975
|
-
const final = toSegments[toSegments.length - 1];
|
|
2976
|
-
const atStart = Math.hypot(last.x1 - first.x, last.y1 - first.y) <= Math.hypot(last.x1 - final.x, last.y1 - final.y);
|
|
2977
|
-
return atStart ? { from: first, to: toSegments[1] } : { from: final, to: toSegments[toSegments.length - 2] };
|
|
2978
|
-
}
|
|
2979
|
-
function pointNormalsOf(rawPoints, yToX = 1) {
|
|
2980
|
-
const points = yToX === 1 ? rawPoints : rawPoints.map((p) => ({ x: p.x, y: p.y * yToX }));
|
|
2981
|
-
const pointNormals = [];
|
|
2982
|
-
for (let i = 0; i < points.length; i++) {
|
|
2983
|
-
let nx = 0;
|
|
2984
|
-
let ny = 0;
|
|
2985
|
-
if (i === 0) {
|
|
2986
|
-
const dx = points[1].x - points[0].x;
|
|
2987
|
-
const dy = points[1].y - points[0].y;
|
|
2988
|
-
const len = Math.hypot(dx, dy);
|
|
2989
|
-
if (len > 0) {
|
|
2990
|
-
nx = -dy / len;
|
|
2991
|
-
ny = dx / len;
|
|
2992
|
-
}
|
|
2993
|
-
} else if (i === points.length - 1) {
|
|
2994
|
-
const dx = points[i].x - points[i - 1].x;
|
|
2995
|
-
const dy = points[i].y - points[i - 1].y;
|
|
2996
|
-
const len = Math.hypot(dx, dy);
|
|
2997
|
-
if (len > 0) {
|
|
2998
|
-
nx = -dy / len;
|
|
2999
|
-
ny = dx / len;
|
|
3000
|
-
}
|
|
3001
|
-
} else {
|
|
3002
|
-
const dx1 = points[i].x - points[i - 1].x;
|
|
3003
|
-
const dy1 = points[i].y - points[i - 1].y;
|
|
3004
|
-
const len1 = Math.hypot(dx1, dy1);
|
|
3005
|
-
const dx2 = points[i + 1].x - points[i].x;
|
|
3006
|
-
const dy2 = points[i + 1].y - points[i].y;
|
|
3007
|
-
const len2 = Math.hypot(dx2, dy2);
|
|
3008
|
-
if (len1 > 0 && len2 > 0) {
|
|
3009
|
-
const nx1 = -dy1 / len1;
|
|
3010
|
-
const ny1 = dx1 / len1;
|
|
3011
|
-
const nx2 = -dy2 / len2;
|
|
3012
|
-
const ny2 = dx2 / len2;
|
|
3013
|
-
nx = (nx1 + nx2) / 2;
|
|
3014
|
-
ny = (ny1 + ny2) / 2;
|
|
3015
|
-
const dot = nx1 * nx + ny1 * ny;
|
|
3016
|
-
if (dot > 0.1) {
|
|
3017
|
-
nx /= dot;
|
|
3018
|
-
ny /= dot;
|
|
3019
|
-
}
|
|
3020
|
-
} else if (len1 > 0) {
|
|
3021
|
-
nx = -dy1 / len1;
|
|
3022
|
-
ny = dx1 / len1;
|
|
3023
|
-
} else if (len2 > 0) {
|
|
3024
|
-
nx = -dy2 / len2;
|
|
3025
|
-
ny = dx2 / len2;
|
|
3026
|
-
}
|
|
3027
|
-
}
|
|
3028
|
-
pointNormals.push({ nx, ny });
|
|
3029
|
-
}
|
|
3030
|
-
return pointNormals;
|
|
3031
|
-
}
|
|
3032
|
-
function offsetPolyline(points, normals, distance, yToX = 1) {
|
|
3033
|
-
return points.map((p, i) => ({
|
|
3034
|
-
x: p.x + normals[i].nx * distance,
|
|
3035
|
-
y: p.y + normals[i].ny * distance / yToX
|
|
3036
|
-
}));
|
|
3037
|
-
}
|
|
3038
|
-
function packNorm(r, g, b, a) {
|
|
3039
|
-
return packAbgr2(
|
|
3040
|
-
Math.round(r * 255),
|
|
3041
|
-
Math.round(g * 255),
|
|
3042
|
-
Math.round(b * 255),
|
|
3043
|
-
Math.round(a * 255)
|
|
3044
|
-
);
|
|
3045
|
-
}
|
|
3046
|
-
function hslToRgb(h, s, l) {
|
|
3047
|
-
const c = (1 - Math.abs(2 * l - 1)) * s;
|
|
3048
|
-
const x = c * (1 - Math.abs(h / 60 % 2 - 1));
|
|
3049
|
-
const m = l - c / 2;
|
|
3050
|
-
let r = 0;
|
|
3051
|
-
let g = 0;
|
|
3052
|
-
let b = 0;
|
|
3053
|
-
if (h < 60) {
|
|
3054
|
-
r = c;
|
|
3055
|
-
g = x;
|
|
3056
|
-
} else if (h < 120) {
|
|
3057
|
-
r = x;
|
|
3058
|
-
g = c;
|
|
3059
|
-
} else if (h < 180) {
|
|
3060
|
-
g = c;
|
|
3061
|
-
b = x;
|
|
3062
|
-
} else if (h < 240) {
|
|
3063
|
-
g = x;
|
|
3064
|
-
b = c;
|
|
3065
|
-
} else if (h < 300) {
|
|
3066
|
-
r = x;
|
|
3067
|
-
b = c;
|
|
3068
|
-
} else {
|
|
3069
|
-
r = c;
|
|
3070
|
-
b = x;
|
|
3071
|
-
}
|
|
3072
|
-
return [r + m, g + m, b + m];
|
|
3073
|
-
}
|
|
3074
|
-
var DEPTH_GRADIENT = [
|
|
3075
|
-
[68, 1, 84],
|
|
3076
|
-
[59, 82, 139],
|
|
3077
|
-
[33, 145, 140],
|
|
3078
|
-
[94, 201, 98],
|
|
3079
|
-
[253, 231, 37]
|
|
3080
|
-
];
|
|
3081
|
-
var STABLE_RANK_GRADIENT = [
|
|
3082
|
-
[237, 137, 44],
|
|
3083
|
-
[158, 42, 122]
|
|
3084
|
-
];
|
|
3085
|
-
var NODE_LENGTH_GRADIENT = [
|
|
3086
|
-
[220, 50, 50],
|
|
3087
|
-
[50, 120, 220]
|
|
3088
|
-
];
|
|
3089
|
-
function sampleGradient(stops, t) {
|
|
3090
|
-
const pos = Math.max(0, Math.min(1, t)) * (stops.length - 1);
|
|
3091
|
-
const i = Math.min(stops.length - 2, Math.floor(pos));
|
|
3092
|
-
const f = pos - i;
|
|
3093
|
-
const a = stops[i];
|
|
3094
|
-
const b = stops[i + 1];
|
|
3095
|
-
return packAbgr2(
|
|
3096
|
-
Math.round(a[0] + (b[0] - a[0]) * f),
|
|
3097
|
-
Math.round(a[1] + (b[1] - a[1]) * f),
|
|
3098
|
-
Math.round(a[2] + (b[2] - a[2]) * f),
|
|
3099
|
-
255
|
|
3100
|
-
);
|
|
3101
|
-
}
|
|
3102
|
-
function hashColor(str, alpha) {
|
|
3103
|
-
const [r, g, b] = hslToRgb(nameHue(str), PATH_SATURATION, PATH_LIGHTNESS);
|
|
3104
|
-
return packNorm(r, g, b, alpha);
|
|
3105
|
-
}
|
|
3106
|
-
var REFERENCE_RAMP_MAX_HUE = 300;
|
|
3107
|
-
var REFERENCE_RAMP_SATURATION = 0.7;
|
|
3108
|
-
var REFERENCE_RAMP_LIGHTNESS = 0.5;
|
|
3109
|
-
var REFERENCE_RAMP_ALT_RGB = [60, 65, 72];
|
|
3110
|
-
var REFERENCE_RAMP_ALT_COLOR = packAbgr2(...REFERENCE_RAMP_ALT_RGB, 255);
|
|
3111
|
-
var REFERENCE_RAMP_ALT_CSS = `rgb(${REFERENCE_RAMP_ALT_RGB.join(", ")})`;
|
|
3112
|
-
function computeReferenceRamp(graph, domain) {
|
|
3113
|
-
const midpoints = referenceMidpoints(graph);
|
|
3114
|
-
if (domain && domain.end > domain.start) {
|
|
3115
|
-
return {
|
|
3116
|
-
start: domain.start,
|
|
3117
|
-
span: domain.end - domain.start,
|
|
3118
|
-
midpoints
|
|
3119
|
-
};
|
|
3120
|
-
}
|
|
3121
|
-
let min = Infinity;
|
|
3122
|
-
let max = -Infinity;
|
|
3123
|
-
for (const mid of midpoints.values()) {
|
|
3124
|
-
min = Math.min(min, mid);
|
|
3125
|
-
max = Math.max(max, mid);
|
|
3126
|
-
}
|
|
3127
|
-
return {
|
|
3128
|
-
start: min === Infinity ? 0 : min,
|
|
3129
|
-
span: max > min ? max - min : 1,
|
|
3130
|
-
midpoints
|
|
3131
|
-
};
|
|
3132
|
-
}
|
|
3133
|
-
function computeColorSchemeRange(graph) {
|
|
3134
|
-
let minDepth = Infinity;
|
|
3135
|
-
let maxDepth = -Infinity;
|
|
3136
|
-
let minLength = Infinity;
|
|
3137
|
-
let maxLength = -Infinity;
|
|
3138
|
-
let maxRank = 0;
|
|
3139
|
-
for (const n of graph.nodes) {
|
|
3140
|
-
if (n.stable !== void 0 && n.stable.rank > maxRank) {
|
|
3141
|
-
maxRank = n.stable.rank;
|
|
3142
|
-
}
|
|
3143
|
-
if (n.depth < minDepth) {
|
|
3144
|
-
minDepth = n.depth;
|
|
3145
|
-
}
|
|
3146
|
-
if (n.depth > maxDepth) {
|
|
3147
|
-
maxDepth = n.depth;
|
|
3148
|
-
}
|
|
3149
|
-
if (n.length < minLength) {
|
|
3150
|
-
minLength = n.length;
|
|
3151
|
-
}
|
|
3152
|
-
if (n.length > maxLength) {
|
|
3153
|
-
maxLength = n.length;
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
return {
|
|
3157
|
-
minDepth,
|
|
3158
|
-
maxDepth,
|
|
3159
|
-
minLength,
|
|
3160
|
-
maxLength,
|
|
3161
|
-
maxRank,
|
|
3162
|
-
nodeCount: graph.nodes.length
|
|
3163
|
-
};
|
|
3164
|
-
}
|
|
3165
|
-
function getNodeColor(node, nodeIndex, colorScheme, range) {
|
|
3166
|
-
switch (colorScheme) {
|
|
3167
|
-
case "random":
|
|
3168
|
-
return hashColor(node.id, 1);
|
|
3169
|
-
case "depth":
|
|
3170
|
-
return sampleGradient(
|
|
3171
|
-
DEPTH_GRADIENT,
|
|
3172
|
-
range.maxDepth > range.minDepth ? (node.depth - range.minDepth) / (range.maxDepth - range.minDepth) : 0.5
|
|
3173
|
-
);
|
|
3174
|
-
case "node-length":
|
|
3175
|
-
return sampleGradient(
|
|
3176
|
-
NODE_LENGTH_GRADIENT,
|
|
3177
|
-
range.maxLength > range.minLength ? (node.length - range.minLength) / (range.maxLength - range.minLength) : 0.5
|
|
3178
|
-
);
|
|
3179
|
-
// rGFA states the backbone rather than leaving it to be inferred: rank 0 is
|
|
3180
|
-
// the reference, higher ranks are the sequence that diverges from it. A
|
|
3181
|
-
// graph with no SR tags draws every node as unranked.
|
|
3182
|
-
case "stable-rank":
|
|
3183
|
-
return node.stable === void 0 ? packAbgr2(160, 160, 160, 255) : node.stable.rank === 0 ? packAbgr2(52, 152, 219, 255) : sampleGradient(
|
|
3184
|
-
STABLE_RANK_GRADIENT,
|
|
3185
|
-
range.maxRank > 1 ? (node.stable.rank - 1) / (range.maxRank - 1) : 0
|
|
3186
|
-
);
|
|
3187
|
-
// Position on the reference, the one quantity both panels of a
|
|
3188
|
-
// graph-over-linear figure can state. A node with no reference position at
|
|
3189
|
-
// all — nothing anchored, or an allele whose flanks fell outside the cut —
|
|
3190
|
-
// is grey rather than a hue it has not earned.
|
|
3191
|
-
case "reference-position": {
|
|
3192
|
-
const ramp = range.referenceRamp;
|
|
3193
|
-
const mid = ramp?.midpoints.get(node.id);
|
|
3194
|
-
if (ramp === void 0 || mid === void 0) {
|
|
3195
|
-
return packAbgr2(160, 160, 160, 255);
|
|
3196
|
-
}
|
|
3197
|
-
if (node.stable !== void 0 && node.stable.rank > 0) {
|
|
3198
|
-
return REFERENCE_RAMP_ALT_COLOR;
|
|
3199
|
-
}
|
|
3200
|
-
const frac = Math.max(0, Math.min(1, (mid - ramp.start) / ramp.span));
|
|
3201
|
-
const [r, g, b] = hslToRgb(
|
|
3202
|
-
frac * REFERENCE_RAMP_MAX_HUE,
|
|
3203
|
-
REFERENCE_RAMP_SATURATION,
|
|
3204
|
-
REFERENCE_RAMP_LIGHTNESS
|
|
3205
|
-
);
|
|
3206
|
-
return packNorm(r, g, b, 1);
|
|
3207
|
-
}
|
|
3208
|
-
case "grey":
|
|
3209
|
-
return packAbgr2(160, 160, 160, 255);
|
|
3210
|
-
case "rainbow": {
|
|
3211
|
-
const hue = range.nodeCount > 1 ? nodeIndex / range.nodeCount * 360 : 0;
|
|
3212
|
-
const [r, g, b] = hslToRgb(hue, 0.75, 0.5);
|
|
3213
|
-
return packNorm(r, g, b, 1);
|
|
3214
|
-
}
|
|
3215
|
-
default:
|
|
3216
|
-
return packAbgr2(52, 152, 219, 255);
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
function endTangent(c, yToX = 1, into) {
|
|
3220
|
-
const candidates = [
|
|
3221
|
-
[c.cx1, c.cy1],
|
|
3222
|
-
[c.cx0, c.cy0],
|
|
3223
|
-
[c.x0, c.y0]
|
|
3224
|
-
];
|
|
3225
|
-
for (const [x, y] of candidates) {
|
|
3226
|
-
const dx = c.x1 - x;
|
|
3227
|
-
const dy = (c.y1 - y) * yToX;
|
|
3228
|
-
if (Math.hypot(dx, dy) > 0) {
|
|
3229
|
-
return Math.atan2(dy, dx);
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
|
-
return into ? Math.atan2((into.to.y - into.from.y) * yToX, into.to.x - into.from.x) : 0;
|
|
3233
|
-
}
|
|
3234
|
-
function isPolylineInBounds(segments, bounds) {
|
|
3235
|
-
let hit = false;
|
|
3236
|
-
for (let i = 0; i < segments.length - 1 && !hit; i++) {
|
|
3237
|
-
const a = segments[i];
|
|
3238
|
-
const b = segments[i + 1];
|
|
3239
|
-
hit = Math.max(a.x, b.x) >= bounds.minX && Math.min(a.x, b.x) <= bounds.maxX && Math.max(a.y, b.y) >= bounds.minY && Math.min(a.y, b.y) <= bounds.maxY;
|
|
3240
|
-
}
|
|
3241
|
-
return hit;
|
|
3242
|
-
}
|
|
3243
|
-
function isBezierInBounds(curves, bounds) {
|
|
3244
|
-
for (const c of curves) {
|
|
3245
|
-
const cMinX = Math.min(c.x0, c.cx0, c.cx1, c.x1);
|
|
3246
|
-
const cMaxX = Math.max(c.x0, c.cx0, c.cx1, c.x1);
|
|
3247
|
-
const cMinY = Math.min(c.y0, c.cy0, c.cy1, c.y1);
|
|
3248
|
-
const cMaxY = Math.max(c.y0, c.cy0, c.cy1, c.y1);
|
|
3249
|
-
if (cMaxX >= bounds.minX && cMinX <= bounds.maxX && cMaxY >= bounds.minY && cMinY <= bounds.maxY) {
|
|
3250
|
-
return true;
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
return false;
|
|
3254
|
-
}
|
|
3255
|
-
var paletteCache = /* @__PURE__ */ new WeakMap();
|
|
3256
|
-
function pathPaletteOf(graph) {
|
|
3257
|
-
let palette = paletteCache.get(graph);
|
|
3258
|
-
if (!palette) {
|
|
3259
|
-
const pathColorByIndex = [];
|
|
3260
|
-
const pathIndexByName = /* @__PURE__ */ new Map();
|
|
3261
|
-
const paths = graph.paths ?? [];
|
|
3262
|
-
for (let i = 0; i < paths.length; i++) {
|
|
3263
|
-
const [r, g, b] = hslToRgb(
|
|
3264
|
-
pathHueAt(i, paths.length),
|
|
3265
|
-
PATH_SATURATION,
|
|
3266
|
-
PATH_LIGHTNESS
|
|
3267
|
-
);
|
|
3268
|
-
pathColorByIndex.push(packNorm(r, g, b, 0.85));
|
|
3269
|
-
if (!pathIndexByName.has(paths[i].name)) {
|
|
3270
|
-
pathIndexByName.set(paths[i].name, i);
|
|
3271
|
-
}
|
|
3272
|
-
}
|
|
3273
|
-
palette = { pathColorByIndex, pathIndexByName };
|
|
3274
|
-
paletteCache.set(graph, palette);
|
|
3275
|
-
}
|
|
3276
|
-
return palette;
|
|
3277
|
-
}
|
|
3278
|
-
var slotsCache = /* @__PURE__ */ new WeakMap();
|
|
3279
|
-
var NO_SLOTS = /* @__PURE__ */ new Map();
|
|
3280
|
-
function pathSlotsOf(graph) {
|
|
3281
|
-
let slots = slotsCache.get(graph);
|
|
3282
|
-
if (!slots) {
|
|
3283
|
-
slots = /* @__PURE__ */ new Map();
|
|
3284
|
-
for (const [pathIdx, path] of (graph.paths ?? []).entries()) {
|
|
3285
|
-
for (const nodeId of new Set(path.nodeIds)) {
|
|
3286
|
-
const visits = slots.get(nodeId);
|
|
3287
|
-
if (visits) {
|
|
3288
|
-
visits.push(pathIdx);
|
|
3289
|
-
} else {
|
|
3290
|
-
slots.set(nodeId, [pathIdx]);
|
|
3291
|
-
}
|
|
3292
|
-
}
|
|
3293
|
-
}
|
|
3294
|
-
slotsCache.set(graph, slots);
|
|
3295
|
-
}
|
|
3296
|
-
return slots;
|
|
3297
|
-
}
|
|
3298
|
-
function buildGeometry(options) {
|
|
3299
|
-
const {
|
|
3300
|
-
nodePositions,
|
|
3301
|
-
graph,
|
|
3302
|
-
nodeById,
|
|
3303
|
-
colorScheme,
|
|
3304
|
-
contigThickness,
|
|
3305
|
-
connectorThickness,
|
|
3306
|
-
drawPaths,
|
|
3307
|
-
nodeWidth: nodeWidth2 = "uniform",
|
|
3308
|
-
highlight,
|
|
3309
|
-
axis,
|
|
3310
|
-
linearLayout,
|
|
3311
|
-
viewportBounds,
|
|
3312
|
-
referenceRamp,
|
|
3313
|
-
deletions,
|
|
3314
|
-
hiddenEdges,
|
|
3315
|
-
version = 0
|
|
3316
|
-
} = options;
|
|
3317
|
-
const depthNorm = nodeWidth2 === "depth" ? meanDepth(graph) : 0;
|
|
3318
|
-
const scale = axis.scaleX;
|
|
3319
|
-
const yToX = yToXOf(axis);
|
|
3320
|
-
const sharedCurves = baseEdgeCurves(
|
|
3321
|
-
nodePositions,
|
|
3322
|
-
graph,
|
|
3323
|
-
axis,
|
|
3324
|
-
deletions,
|
|
3325
|
-
version
|
|
3326
|
-
);
|
|
3327
|
-
const nodeStrokes = [];
|
|
3328
|
-
const nodeStrokeRuns = /* @__PURE__ */ new Map();
|
|
3329
|
-
const arrows = [];
|
|
3330
|
-
const arrowRuns = /* @__PURE__ */ new Map();
|
|
3331
|
-
const edgeCurves = [];
|
|
3332
|
-
const edgeCurveRuns = /* @__PURE__ */ new Map();
|
|
3333
|
-
const colorRange = { ...computeColorSchemeRange(graph), referenceRamp };
|
|
3334
|
-
const nodeIndexMap = /* @__PURE__ */ new Map();
|
|
3335
|
-
if (colorScheme === "rainbow") {
|
|
3336
|
-
for (let i = 0; i < graph.nodes.length; i++) {
|
|
3337
|
-
nodeIndexMap.set(graph.nodes[i].id, i);
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
const pathCount = graph.paths?.length ?? 0;
|
|
3341
|
-
const { pathColorByIndex, pathIndexByName } = pathPaletteOf(graph);
|
|
3342
|
-
const pathColorByName = (name) => pathColorByIndex[pathIndexByName.get(name) ?? -1] ?? EDGE_PATH_FALLBACK_COLOR;
|
|
3343
|
-
const nodePathSlots = drawPaths && pathCount > 1 ? pathSlotsOf(graph) : NO_SLOTS;
|
|
3344
|
-
const showArrows = scale > (linearLayout ? 1 : MIN_ARROW_SCALE);
|
|
3345
|
-
for (let ei = 0; ei < graph.edges.length; ei++) {
|
|
3346
|
-
const edge = graph.edges[ei];
|
|
3347
|
-
const fromSegments = nodePositions[edge.from];
|
|
3348
|
-
const toSegments = nodePositions[edge.to];
|
|
3349
|
-
const baseCurves = sharedCurves.get(ei);
|
|
3350
|
-
if (!fromSegments?.length || !toSegments?.length || !baseCurves || hiddenEdges?.has(ei)) {
|
|
3351
|
-
continue;
|
|
3352
|
-
}
|
|
3353
|
-
const numPaths = edge.pathIds?.length ?? 0;
|
|
3354
|
-
const bypassed = deletions?.get(ei);
|
|
3355
|
-
const isDeletion = bypassed !== void 0;
|
|
3356
|
-
const onWalk = highlight?.edgeIndexes.has(ei) ?? false;
|
|
3357
|
-
const edgeThickness = connectorThickness / 2 * (isDeletion ? DELETION_THICKNESS_FACTOR : 1);
|
|
3358
|
-
const ribbons = drawPaths && numPaths > 0;
|
|
3359
|
-
const plainEdgeColor = isDeletion ? EDGE_DELETION_COLOR : EDGE_DEFAULT_COLOR;
|
|
3360
|
-
const edgeColor = !highlight || onWalk ? plainEdgeColor : fadeAbgr(plainEdgeColor, FADED_ALPHA);
|
|
3361
|
-
const toNode = nodeById.get(edge.to);
|
|
3362
|
-
const intoHalfWidth = (toNode ? nodeWidthPx(toNode, contigThickness, nodeWidth2, depthNorm) : contigThickness) / 2;
|
|
3363
|
-
const buildSingleEdge = (offsetX, offsetY, color, arrowColor) => {
|
|
3364
|
-
const curves = offsetX === 0 && offsetY === 0 ? baseCurves : translateCurves(baseCurves, offsetX, offsetY);
|
|
3365
|
-
if (viewportBounds && !isBezierInBounds(curves, viewportBounds)) {
|
|
3366
|
-
return;
|
|
3367
|
-
}
|
|
3368
|
-
if (isDeletion) {
|
|
3369
|
-
for (const dash of dashCurves(curves, DELETION_DASH_PX / scale)) {
|
|
3370
|
-
edgeCurves.push({ curves: dash, thickness: edgeThickness, color });
|
|
3371
|
-
}
|
|
3372
|
-
} else {
|
|
3373
|
-
edgeCurves.push({ curves, thickness: edgeThickness, color });
|
|
3374
|
-
}
|
|
3375
|
-
const head = showArrows && arrowColor !== void 0 ? arrowheadFor({
|
|
3376
|
-
curves,
|
|
3377
|
-
edgeHalfWidth: edgeThickness,
|
|
3378
|
-
nodeHalfWidth: intoHalfWidth,
|
|
3379
|
-
scale,
|
|
3380
|
-
yToX,
|
|
3381
|
-
color: arrowColor,
|
|
3382
|
-
fromSegments,
|
|
3383
|
-
toSegments
|
|
3384
|
-
}) : void 0;
|
|
3385
|
-
if (head) {
|
|
3386
|
-
arrows.push(head);
|
|
3387
|
-
}
|
|
3388
|
-
};
|
|
3389
|
-
const edgeCurveStart = edgeCurves.length;
|
|
3390
|
-
const arrowStart = arrows.length;
|
|
3391
|
-
const laneWalks = highlight && onWalk && !ribbons ? highlight.walks : void 0;
|
|
3392
|
-
if (laneWalks) {
|
|
3393
|
-
const fromNode = nodeById.get(edge.from);
|
|
3394
|
-
const width = fromNode ? nodeWidthPx(fromNode, contigThickness, nodeWidth2, depthNorm) : contigThickness;
|
|
3395
|
-
const laneWidth = Math.max(width / laneWalks.length, MIN_WALK_LANE_PX);
|
|
3396
|
-
const normals = pointNormalsOf(fromSegments, yToX);
|
|
3397
|
-
const { nx, ny } = (edge.fromStrand === "-" ? normals[0] : normals.at(-1)) ?? {
|
|
3398
|
-
nx: 0,
|
|
3399
|
-
ny: 0
|
|
3400
|
-
};
|
|
3401
|
-
const worldPerScreenPx = 1 / Math.max(scale, MIN_SCALE_FOR_OFFSET);
|
|
3402
|
-
laneWalks.forEach((walk, lane) => {
|
|
3403
|
-
if (!walk.edgeIndexes.has(ei)) {
|
|
3404
|
-
return;
|
|
3405
|
-
}
|
|
3406
|
-
const offset = (lane - (laneWalks.length - 1) / 2) * laneWidth * worldPerScreenPx;
|
|
3407
|
-
const curves = offset === 0 ? baseCurves : translateCurves(baseCurves, nx * offset, ny * offset / yToX);
|
|
3408
|
-
if (viewportBounds && !isBezierInBounds(curves, viewportBounds)) {
|
|
3409
|
-
return;
|
|
3410
|
-
}
|
|
3411
|
-
edgeCurves.push({
|
|
3412
|
-
curves,
|
|
3413
|
-
thickness: laneWidth / 2,
|
|
3414
|
-
color: walk.colors.get(edge.from) ?? walk.colors.get(edge.to) ?? plainEdgeColor
|
|
3415
|
-
});
|
|
3416
|
-
});
|
|
3417
|
-
} else if (!ribbons) {
|
|
3418
|
-
buildSingleEdge(0, 0, edgeColor, edgeColor);
|
|
3419
|
-
} else {
|
|
3420
|
-
const offsets = pathRibbonOffsets(
|
|
3421
|
-
fromSegments,
|
|
3422
|
-
toSegments,
|
|
3423
|
-
numPaths,
|
|
3424
|
-
axis
|
|
3425
|
-
);
|
|
3426
|
-
const arrowRibbon = Math.floor((numPaths - 1) / 2);
|
|
3427
|
-
for (let pathIdx = 0; pathIdx < numPaths; pathIdx++) {
|
|
3428
|
-
const offset = offsets[pathIdx];
|
|
3429
|
-
const ribbonColor = pathColorByName(edge.pathIds[pathIdx]);
|
|
3430
|
-
buildSingleEdge(
|
|
3431
|
-
offset.x,
|
|
3432
|
-
offset.y,
|
|
3433
|
-
highlight && !highlight.names.has(edge.pathIds[pathIdx]) ? fadeAbgr(ribbonColor, FADED_ALPHA) : ribbonColor,
|
|
3434
|
-
pathIdx === arrowRibbon ? edgeColor : void 0
|
|
3435
|
-
);
|
|
3436
|
-
}
|
|
3437
|
-
}
|
|
3438
|
-
const edgeCurveCount = edgeCurves.length - edgeCurveStart;
|
|
3439
|
-
if (edgeCurveCount > 0) {
|
|
3440
|
-
edgeCurveRuns.set(ei, { start: edgeCurveStart, count: edgeCurveCount });
|
|
3441
|
-
}
|
|
3442
|
-
const arrowCount = arrows.length - arrowStart;
|
|
3443
|
-
if (arrowCount > 0) {
|
|
3444
|
-
arrowRuns.set(ei, { start: arrowStart, count: arrowCount });
|
|
3445
|
-
}
|
|
3446
|
-
}
|
|
3447
|
-
for (const [nodeId, segments] of Object.entries(nodePositions)) {
|
|
3448
|
-
const node = nodeById.get(nodeId);
|
|
3449
|
-
if (!node) {
|
|
3450
|
-
continue;
|
|
3451
|
-
}
|
|
3452
|
-
if (viewportBounds && !isPolylineInBounds(segments, viewportBounds)) {
|
|
3453
|
-
continue;
|
|
3454
|
-
}
|
|
3455
|
-
const faded = highlight !== void 0 && !highlight.nodeIds.has(nodeId);
|
|
3456
|
-
const own = faded ? LIFT_BACKDROP_COLOR : getNodeColor(
|
|
3457
|
-
node,
|
|
3458
|
-
nodeIndexMap.get(nodeId) ?? 0,
|
|
3459
|
-
colorScheme,
|
|
3460
|
-
colorRange
|
|
3461
|
-
);
|
|
3462
|
-
const color = faded ? fadeAbgr(own, FADED_ALPHA) : own;
|
|
3463
|
-
const width = nodeWidthPx(node, contigThickness, nodeWidth2, depthNorm);
|
|
3464
|
-
const nodeThickness = width / 2;
|
|
3465
|
-
const start = nodeStrokes.length;
|
|
3466
|
-
const slots = nodePathSlots.get(nodeId);
|
|
3467
|
-
const slotWidth = width / pathCount;
|
|
3468
|
-
const drawable = segments.length >= 2;
|
|
3469
|
-
const lanes = highlight && !faded && !slots?.length ? highlight.walks : void 0;
|
|
3470
|
-
if (drawable && lanes) {
|
|
3471
|
-
const laneWidth = Math.max(width / lanes.length, MIN_WALK_LANE_PX);
|
|
3472
|
-
const normals = pointNormalsOf(segments, yToX);
|
|
3473
|
-
const worldPerScreenPx = 1 / Math.max(scale, MIN_SCALE_FOR_OFFSET);
|
|
3474
|
-
lanes.forEach((walk, lane) => {
|
|
3475
|
-
const laneColor = walk.colors.get(nodeId);
|
|
3476
|
-
if (laneColor === void 0) {
|
|
3477
|
-
return;
|
|
3478
|
-
}
|
|
3479
|
-
const offset = (lane - (lanes.length - 1) / 2) * laneWidth * worldPerScreenPx;
|
|
3480
|
-
nodeStrokes.push({
|
|
3481
|
-
points: lanes.length === 1 ? segments : offsetPolyline(segments, normals, offset, yToX),
|
|
3482
|
-
thickness: laneWidth / 2,
|
|
3483
|
-
color: laneColor
|
|
3484
|
-
});
|
|
3485
|
-
});
|
|
3486
|
-
} else if (drawable && slots?.length && slotWidth >= MIN_PATH_STRIPE_PX) {
|
|
3487
|
-
const normals = pointNormalsOf(segments, yToX);
|
|
3488
|
-
const worldPerScreenPx = 1 / Math.max(scale, MIN_SCALE_FOR_OFFSET);
|
|
3489
|
-
for (const slot of slots) {
|
|
3490
|
-
const offset = (slot - (pathCount - 1) / 2) * slotWidth * worldPerScreenPx;
|
|
3491
|
-
const stripe = pathColorByIndex[slot] ?? EDGE_PATH_FALLBACK_COLOR;
|
|
3492
|
-
nodeStrokes.push({
|
|
3493
|
-
points: offsetPolyline(segments, normals, offset, yToX),
|
|
3494
|
-
thickness: slotWidth / 2,
|
|
3495
|
-
// a node off the lifted walk fades whichever way it is painted
|
|
3496
|
-
color: faded ? fadeAbgr(stripe, FADED_ALPHA) : stripe
|
|
3497
|
-
});
|
|
3498
|
-
}
|
|
3499
|
-
} else if (drawable) {
|
|
3500
|
-
nodeStrokes.push({ points: segments, thickness: nodeThickness, color });
|
|
3501
|
-
}
|
|
3502
|
-
const count = nodeStrokes.length - start;
|
|
3503
|
-
if (count > 0) {
|
|
3504
|
-
nodeStrokeRuns.set(nodeId, { start, count });
|
|
3505
|
-
}
|
|
3506
|
-
}
|
|
3507
|
-
return {
|
|
3508
|
-
nodeStrokes,
|
|
3509
|
-
nodeStrokeRuns,
|
|
3510
|
-
arrows,
|
|
3511
|
-
arrowRuns,
|
|
3512
|
-
edgeCurves,
|
|
3513
|
-
edgeCurveRuns
|
|
3514
|
-
};
|
|
3515
|
-
}
|
|
3516
|
-
|
|
3517
|
-
// src/renderer/Canvas2DRenderer.ts
|
|
3518
1
|
import {
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
lifted.set(i, factor);
|
|
3617
|
-
}
|
|
3618
|
-
}
|
|
3619
|
-
}
|
|
3620
|
-
for (const { color, items } of groupByPaint(batch.nodeStrokes, (s, i) => {
|
|
3621
|
-
const factor = lifted.get(i);
|
|
3622
|
-
return {
|
|
3623
|
-
color: factor === void 0 ? s.color : brightenAbgr(s.color, factor),
|
|
3624
|
-
weight: s.thickness,
|
|
3625
|
-
last: factor !== void 0
|
|
3626
|
-
};
|
|
3627
|
-
})) {
|
|
3628
|
-
ctx.strokeStyle = abgrToCssRgba(color);
|
|
3629
|
-
ctx.lineWidth = items[0].thickness * 2 * t.dpr;
|
|
3630
|
-
ctx.beginPath();
|
|
3631
|
-
for (const s of items) {
|
|
3632
|
-
const p0 = s.points[0];
|
|
3633
|
-
ctx.moveTo(
|
|
3634
|
-
p0.x * t.scaleX + t.translateX,
|
|
3635
|
-
p0.y * t.scaleY + t.translateY
|
|
3636
|
-
);
|
|
3637
|
-
for (let i = 1, l = s.points.length; i < l; i++) {
|
|
3638
|
-
const p = s.points[i];
|
|
3639
|
-
ctx.lineTo(
|
|
3640
|
-
p.x * t.scaleX + t.translateX,
|
|
3641
|
-
p.y * t.scaleY + t.translateY
|
|
3642
|
-
);
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3645
|
-
ctx.stroke();
|
|
3646
|
-
}
|
|
3647
|
-
}
|
|
3648
|
-
renderArrows(batch) {
|
|
3649
|
-
const t = this.transform;
|
|
3650
|
-
const ctx = this.ctx;
|
|
3651
|
-
const hl = this.highlightedEdge === null ? void 0 : batch.arrowRuns.get(this.highlightedEdge);
|
|
3652
|
-
for (const { color, items } of groupByPaint(batch.arrows, (a, i) => ({
|
|
3653
|
-
color: inRun(hl, i) ? brightenAbgr(a.color, this.highlightFactor) : a.color,
|
|
3654
|
-
weight: 0,
|
|
3655
|
-
last: inRun(hl, i)
|
|
3656
|
-
}))) {
|
|
3657
|
-
ctx.fillStyle = abgrToCssRgba(color);
|
|
3658
|
-
ctx.beginPath();
|
|
3659
|
-
for (const a of items) {
|
|
3660
|
-
const [tip, left, notch, right] = arrowheadOutline(a, t);
|
|
3661
|
-
ctx.moveTo(tip.x, tip.y);
|
|
3662
|
-
ctx.lineTo(left.x, left.y);
|
|
3663
|
-
ctx.lineTo(notch.x, notch.y);
|
|
3664
|
-
ctx.lineTo(right.x, right.y);
|
|
3665
|
-
ctx.closePath();
|
|
3666
|
-
}
|
|
3667
|
-
ctx.fill();
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3670
|
-
dispose() {
|
|
3671
|
-
super.dispose();
|
|
3672
|
-
this.batch = null;
|
|
3673
|
-
this.nodeHighlights = /* @__PURE__ */ new Map();
|
|
3674
|
-
this.highlightedEdge = null;
|
|
3675
|
-
}
|
|
3676
|
-
};
|
|
3677
|
-
var ARROW_NOTCH = 0.25;
|
|
3678
|
-
function arrowheadOutline(a, t) {
|
|
3679
|
-
const ux = Math.cos(a.angle);
|
|
3680
|
-
const uy = Math.sin(a.angle);
|
|
3681
|
-
const tipX = a.x * t.scaleX + t.translateX;
|
|
3682
|
-
const tipY = a.y * t.scaleY + t.translateY;
|
|
3683
|
-
const length = a.length * t.dpr;
|
|
3684
|
-
const halfWidth = a.halfWidth * t.dpr;
|
|
3685
|
-
const backX = tipX - ux * length;
|
|
3686
|
-
const backY = tipY - uy * length;
|
|
3687
|
-
const notchBack = length * (1 - ARROW_NOTCH);
|
|
3688
|
-
return [
|
|
3689
|
-
{ x: tipX, y: tipY },
|
|
3690
|
-
{ x: backX - uy * halfWidth, y: backY + ux * halfWidth },
|
|
3691
|
-
{ x: tipX - ux * notchBack, y: tipY - uy * notchBack },
|
|
3692
|
-
{ x: backX + uy * halfWidth, y: backY - ux * halfWidth }
|
|
3693
|
-
];
|
|
3694
|
-
}
|
|
3695
|
-
function inRun(run, i) {
|
|
3696
|
-
return run !== void 0 && i >= run.start && i < run.start + run.count;
|
|
3697
|
-
}
|
|
3698
|
-
function groupByPaint(items, paintOf) {
|
|
3699
|
-
const groups = /* @__PURE__ */ new Map();
|
|
3700
|
-
const deferred = /* @__PURE__ */ new Map();
|
|
3701
|
-
for (let i = 0, l = items.length; i < l; i++) {
|
|
3702
|
-
const item = items[i];
|
|
3703
|
-
const { color, weight, last } = paintOf(item, i);
|
|
3704
|
-
const into = last ? deferred : groups;
|
|
3705
|
-
const key = `${color}:${weight}`;
|
|
3706
|
-
const group = into.get(key);
|
|
3707
|
-
if (group) {
|
|
3708
|
-
group.items.push(item);
|
|
3709
|
-
} else {
|
|
3710
|
-
into.set(key, { color, items: [item] });
|
|
3711
|
-
}
|
|
3712
|
-
}
|
|
3713
|
-
return [...groups.values(), ...deferred.values()];
|
|
3714
|
-
}
|
|
2
|
+
BUBBLE_SPREADS,
|
|
3
|
+
COLOR_SCHEMES,
|
|
4
|
+
Canvas2DRenderer,
|
|
5
|
+
FACET_GAP_PX,
|
|
6
|
+
FACET_PAD_PX,
|
|
7
|
+
FACET_TITLE_PX,
|
|
8
|
+
FADED_ALPHA,
|
|
9
|
+
FIT_PADDING,
|
|
10
|
+
FORCE_LAYOUT_LABEL,
|
|
11
|
+
LABEL_CHAR_PX,
|
|
12
|
+
LABEL_PAD,
|
|
13
|
+
LABEL_PX,
|
|
14
|
+
LAYOUT_MODES,
|
|
15
|
+
LIFT_BACKDROP_COLOR,
|
|
16
|
+
NODE_WIDTHS,
|
|
17
|
+
REFERENCE_RAMP_MAX_HUE,
|
|
18
|
+
ROW_HEIGHT_PX,
|
|
19
|
+
WALK_FIELDS,
|
|
20
|
+
WALK_SCHEMES,
|
|
21
|
+
WELL_KNOWN_SAMPLES,
|
|
22
|
+
assemblyWalk,
|
|
23
|
+
axisScaleOf,
|
|
24
|
+
backboneAssembly,
|
|
25
|
+
baseEdgeCurves,
|
|
26
|
+
buildGeometry,
|
|
27
|
+
clampZoom,
|
|
28
|
+
computeColorSchemeRange,
|
|
29
|
+
computeReferenceRamp,
|
|
30
|
+
contains,
|
|
31
|
+
curveBounds,
|
|
32
|
+
cutWindowGFA,
|
|
33
|
+
deletionEdges,
|
|
34
|
+
drawingBounds,
|
|
35
|
+
encodingStops,
|
|
36
|
+
encodingSwatchCss,
|
|
37
|
+
engineKey,
|
|
38
|
+
engineRequest,
|
|
39
|
+
facetCells,
|
|
40
|
+
facetGrid,
|
|
41
|
+
facetLifts,
|
|
42
|
+
fadeAbgr,
|
|
43
|
+
featuresOnBackbone,
|
|
44
|
+
figureSvg,
|
|
45
|
+
firstNodeAtOrAfter,
|
|
46
|
+
fitTransform,
|
|
47
|
+
forceLayout,
|
|
48
|
+
formatBp,
|
|
49
|
+
getNodeColor,
|
|
50
|
+
graphBackbone,
|
|
51
|
+
haplotypeWanted,
|
|
52
|
+
isBackbone,
|
|
53
|
+
layoutExtent,
|
|
54
|
+
layoutModeByValue,
|
|
55
|
+
loadBandage,
|
|
56
|
+
loadGraph,
|
|
57
|
+
maxNodeWidthPx,
|
|
58
|
+
meanDepth,
|
|
59
|
+
modeUsesLayoutEngine,
|
|
60
|
+
nodeInk,
|
|
61
|
+
nodeLabelBudget,
|
|
62
|
+
nodeWidthPx,
|
|
63
|
+
occupancy,
|
|
64
|
+
padded,
|
|
65
|
+
panSNContig,
|
|
66
|
+
panSNHaplotype,
|
|
67
|
+
panSNSample,
|
|
68
|
+
pathColorsLegible,
|
|
69
|
+
pathLegend,
|
|
70
|
+
pathRibbonOffsets,
|
|
71
|
+
placeLabels,
|
|
72
|
+
placeSizeLabels,
|
|
73
|
+
polylineSlice,
|
|
74
|
+
rangeText,
|
|
75
|
+
refNameBinding,
|
|
76
|
+
referenceOrder,
|
|
77
|
+
referencePathQuery,
|
|
78
|
+
referenceSamplesOf,
|
|
79
|
+
resolveColorScheme,
|
|
80
|
+
resolveEncoding,
|
|
81
|
+
resolveReferenceSample,
|
|
82
|
+
rowLabelBox,
|
|
83
|
+
schemeCss,
|
|
84
|
+
screenToLayout,
|
|
85
|
+
sizeLabelCandidates,
|
|
86
|
+
svgCanvas,
|
|
87
|
+
svgPath,
|
|
88
|
+
translateCurves,
|
|
89
|
+
viewportOf,
|
|
90
|
+
walkHighlight,
|
|
91
|
+
walkKey,
|
|
92
|
+
walkLift,
|
|
93
|
+
walkRows,
|
|
94
|
+
walkRowsExtent,
|
|
95
|
+
wellKnownSample,
|
|
96
|
+
yToXOf,
|
|
97
|
+
zoomAbout
|
|
98
|
+
} from "./chunks/chunk-4AOHEU76.js";
|
|
3715
99
|
|
|
3716
100
|
// src/index.ts
|
|
3717
101
|
import { getDpr } from "@jbrowse/render-core/canvas2dUtils";
|
|
@@ -3949,9 +333,9 @@ function distanceToEdgeCurves(px, py, curves, yToX = 1) {
|
|
|
3949
333
|
}
|
|
3950
334
|
var nodeCache = /* @__PURE__ */ new WeakMap();
|
|
3951
335
|
function getSpatialIndex(nodePositions, version) {
|
|
3952
|
-
const
|
|
3953
|
-
if (
|
|
3954
|
-
return
|
|
336
|
+
const cached = nodeCache.get(nodePositions);
|
|
337
|
+
if (cached?.version === version) {
|
|
338
|
+
return cached.index;
|
|
3955
339
|
}
|
|
3956
340
|
const index = new SpatialIndex(nodePositions);
|
|
3957
341
|
nodeCache.set(nodePositions, { version, index });
|
|
@@ -3959,9 +343,9 @@ function getSpatialIndex(nodePositions, version) {
|
|
|
3959
343
|
}
|
|
3960
344
|
var edgeCache = /* @__PURE__ */ new WeakMap();
|
|
3961
345
|
function getEdgeSpatialIndex(nodePositions, graph, drawPaths, axis, version, deletions) {
|
|
3962
|
-
const
|
|
3963
|
-
if (
|
|
3964
|
-
return
|
|
346
|
+
const cached = edgeCache.get(nodePositions);
|
|
347
|
+
if (cached?.graph === graph && cached.drawPaths === drawPaths && cached.scaleX === axis.scaleX && cached.scaleY === axis.scaleY && cached.version === version && cached.deletions === deletions) {
|
|
348
|
+
return cached.index;
|
|
3965
349
|
}
|
|
3966
350
|
const index = new EdgeSpatialIndex(
|
|
3967
351
|
nodePositions,
|
|
@@ -4081,28 +465,31 @@ function wheelZoomFactor(e) {
|
|
|
4081
465
|
}
|
|
4082
466
|
|
|
4083
467
|
// src/referenceStrip.ts
|
|
4084
|
-
import { abgrToCssRgba
|
|
4085
|
-
var REFERENCE_STRIP_PX =
|
|
468
|
+
import { abgrToCssRgba } from "@jbrowse/core/util/colorBits";
|
|
469
|
+
var REFERENCE_STRIP_PX = 12;
|
|
4086
470
|
var REFERENCE_STRIP_ZONE_PX = REFERENCE_STRIP_PX + 8;
|
|
4087
|
-
var OVERHANG_PX =
|
|
471
|
+
var OVERHANG_PX = 8;
|
|
472
|
+
var OVERHANG_CAP_PX = OVERHANG_PX + 3;
|
|
473
|
+
var PALE = abgrToCssRgba(fadeAbgr(LIFT_BACKDROP_COLOR, FADED_ALPHA));
|
|
4088
474
|
function referenceStripBlocks(graph, {
|
|
4089
475
|
colorScheme,
|
|
4090
476
|
referenceRamp,
|
|
4091
|
-
|
|
4092
|
-
walkColors
|
|
477
|
+
walks
|
|
4093
478
|
}) {
|
|
4094
479
|
const range = { ...computeColorSchemeRange(graph), referenceRamp };
|
|
4095
480
|
const out = [];
|
|
4096
481
|
graph.nodes.forEach((node, index) => {
|
|
4097
482
|
if (isBackbone(node)) {
|
|
4098
|
-
const
|
|
483
|
+
const colors = walks ? walks.map((w) => {
|
|
484
|
+
const lane = w.colors.get(node.id);
|
|
485
|
+
return lane === void 0 ? PALE : abgrToCssRgba(lane);
|
|
486
|
+
}) : [abgrToCssRgba(getNodeColor(node, index, colorScheme, range))];
|
|
4099
487
|
out.push({
|
|
4100
488
|
node: node.id,
|
|
4101
489
|
bp0: node.stable.start,
|
|
4102
490
|
bp1: node.stable.start + node.length,
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
)
|
|
491
|
+
colors,
|
|
492
|
+
faded: colors.includes(PALE)
|
|
4106
493
|
});
|
|
4107
494
|
}
|
|
4108
495
|
});
|
|
@@ -4143,43 +530,70 @@ function stripBlockAt(blocks, frame, sx, sy) {
|
|
|
4143
530
|
}
|
|
4144
531
|
return hit;
|
|
4145
532
|
}
|
|
4146
|
-
function
|
|
4147
|
-
const
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
533
|
+
function stripPixels(blocks, frame, width, dpr) {
|
|
534
|
+
const placed = blocks.map((b) => {
|
|
535
|
+
const [x0, x1] = span(frame, b.bp0, b.bp1);
|
|
536
|
+
const px0 = Math.round(x0 * dpr);
|
|
537
|
+
return {
|
|
538
|
+
colors: b.colors,
|
|
539
|
+
x0,
|
|
540
|
+
px0,
|
|
541
|
+
px1: Math.max(Math.round(x1 * dpr), px0 + 1)
|
|
542
|
+
};
|
|
543
|
+
}).filter((b) => b.px1 > 0 && b.px0 < width * dpr).sort((a, b) => a.x0 - b.x0);
|
|
544
|
+
const out = [];
|
|
545
|
+
let claimed = -Infinity;
|
|
546
|
+
const tierEnds = [-Infinity, -Infinity];
|
|
547
|
+
for (const { colors, px0, px1 } of placed) {
|
|
548
|
+
const start = Math.max(px0, claimed);
|
|
549
|
+
if (px1 > start) {
|
|
550
|
+
const tier = tierEnds[0] < start ? 0 : 1;
|
|
551
|
+
tierEnds[tier] = px1;
|
|
552
|
+
out.push({ colors, x0: start / dpr, x1: px1 / dpr, tier });
|
|
553
|
+
claimed = px1;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
558
|
+
function drawOverhang(ctx, edgeX, inward, ink) {
|
|
559
|
+
const capX = inward > 0 ? edgeX : edgeX - OVERHANG_CAP_PX;
|
|
560
|
+
ctx.clearRect(capX, 0, OVERHANG_CAP_PX, REFERENCE_STRIP_PX);
|
|
561
|
+
const tipX = edgeX + inward;
|
|
562
|
+
const baseX = tipX + inward * OVERHANG_PX;
|
|
563
|
+
ctx.beginPath();
|
|
564
|
+
ctx.moveTo(tipX, REFERENCE_STRIP_PX / 2);
|
|
565
|
+
ctx.lineTo(baseX, 0);
|
|
566
|
+
ctx.lineTo(baseX, REFERENCE_STRIP_PX);
|
|
4152
567
|
ctx.closePath();
|
|
568
|
+
ctx.fillStyle = ink;
|
|
569
|
+
ctx.fill();
|
|
570
|
+
}
|
|
571
|
+
function rowEdge(row, rows, dpr) {
|
|
572
|
+
return Math.round(row * REFERENCE_STRIP_PX * dpr / rows) / dpr;
|
|
4153
573
|
}
|
|
4154
574
|
function drawReferenceStrip(ctx, blocks, frame, {
|
|
4155
575
|
width,
|
|
4156
576
|
lit,
|
|
4157
|
-
darkMode
|
|
577
|
+
darkMode,
|
|
578
|
+
dpr = 1
|
|
4158
579
|
}) {
|
|
4159
580
|
const ink = darkMode ? "#ffffff" : "#18181c";
|
|
4160
|
-
ctx.
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
ctx.
|
|
4167
|
-
|
|
581
|
+
ctx.clearRect(0, 0, width, REFERENCE_STRIP_ZONE_PX);
|
|
582
|
+
for (const b of stripPixels(blocks, frame, width, dpr)) {
|
|
583
|
+
const rows = b.colors.length > 1 ? b.colors.length : 2;
|
|
584
|
+
b.colors.forEach((color, i) => {
|
|
585
|
+
const row = b.colors.length > 1 ? i : b.tier;
|
|
586
|
+
const y0 = rowEdge(row, rows, dpr);
|
|
587
|
+
ctx.fillStyle = color;
|
|
588
|
+
ctx.fillRect(b.x0, y0, b.x1 - b.x0, rowEdge(row + 1, rows, dpr) - y0);
|
|
589
|
+
});
|
|
4168
590
|
}
|
|
4169
591
|
const overhang = stripOverhang(blocks, frame, width);
|
|
4170
|
-
if (overhang.left > 0
|
|
4171
|
-
ctx
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
if (overhang.right > 0) {
|
|
4176
|
-
drawOverhang(ctx, width, -1);
|
|
4177
|
-
}
|
|
4178
|
-
ctx.fillStyle = ink;
|
|
4179
|
-
ctx.fill();
|
|
4180
|
-
ctx.strokeStyle = darkMode ? "#1f1f1f" : "#ffffff";
|
|
4181
|
-
ctx.lineWidth = 1;
|
|
4182
|
-
ctx.stroke();
|
|
592
|
+
if (overhang.left > 0) {
|
|
593
|
+
drawOverhang(ctx, 0, 1, ink);
|
|
594
|
+
}
|
|
595
|
+
if (overhang.right > 0) {
|
|
596
|
+
drawOverhang(ctx, width, -1, ink);
|
|
4183
597
|
}
|
|
4184
598
|
if (lit) {
|
|
4185
599
|
const [x0, x1] = span(frame, lit.start, lit.end);
|
|
@@ -4282,7 +696,7 @@ function parsePath(d) {
|
|
|
4282
696
|
}
|
|
4283
697
|
return commands;
|
|
4284
698
|
}
|
|
4285
|
-
function
|
|
699
|
+
function extentOf(commands) {
|
|
4286
700
|
let x0 = Infinity;
|
|
4287
701
|
let x1 = -Infinity;
|
|
4288
702
|
for (const c of commands) {
|
|
@@ -4299,7 +713,7 @@ function extentOf2(commands) {
|
|
|
4299
713
|
}
|
|
4300
714
|
function shapeOf(d, color, alpha) {
|
|
4301
715
|
const commands = parsePath(d);
|
|
4302
|
-
return { commands, color, alpha, ...
|
|
716
|
+
return { commands, color, alpha, ...extentOf(commands) };
|
|
4303
717
|
}
|
|
4304
718
|
function layerOf(layout, type) {
|
|
4305
719
|
const { shapes } = layout;
|
|
@@ -4322,7 +736,7 @@ function tubeMapPicture(layout) {
|
|
|
4322
736
|
layout.nodes.forEach((node) => {
|
|
4323
737
|
if (node.order >= 0) {
|
|
4324
738
|
const commands = parsePath(nodeOutlinePath(node));
|
|
4325
|
-
nodes.push({ name: node.name, commands, ...
|
|
739
|
+
nodes.push({ name: node.name, commands, ...extentOf(commands) });
|
|
4326
740
|
}
|
|
4327
741
|
});
|
|
4328
742
|
return {
|
|
@@ -4571,329 +985,6 @@ function tubeMapNodeAt(drawing, { x, y }, sx, sy) {
|
|
|
4571
985
|
return hit;
|
|
4572
986
|
}
|
|
4573
987
|
|
|
4574
|
-
// src/overlayLabels.ts
|
|
4575
|
-
var LABEL_PX = 11;
|
|
4576
|
-
var LABEL_CHAR_PX = 6.2;
|
|
4577
|
-
var LABEL_PAD = 4;
|
|
4578
|
-
function labelWidth(text) {
|
|
4579
|
-
return text.length * LABEL_CHAR_PX + LABEL_PAD * 2;
|
|
4580
|
-
}
|
|
4581
|
-
function overlaps(a, b) {
|
|
4582
|
-
return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
|
|
4583
|
-
}
|
|
4584
|
-
function offFrame(box, frame) {
|
|
4585
|
-
return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
|
|
4586
|
-
}
|
|
4587
|
-
function insideFrame(box, frame) {
|
|
4588
|
-
return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
|
|
4589
|
-
}
|
|
4590
|
-
function occupancy(frame, reserved = []) {
|
|
4591
|
-
const placed = [...reserved];
|
|
4592
|
-
return (box) => {
|
|
4593
|
-
if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
|
|
4594
|
-
return false;
|
|
4595
|
-
}
|
|
4596
|
-
placed.push(box);
|
|
4597
|
-
return true;
|
|
4598
|
-
};
|
|
4599
|
-
}
|
|
4600
|
-
function placeLabels(candidates, frame, take) {
|
|
4601
|
-
const out = [];
|
|
4602
|
-
const row = LABEL_PX + LABEL_PAD * 2;
|
|
4603
|
-
for (const c of candidates) {
|
|
4604
|
-
const w = labelWidth(c.text);
|
|
4605
|
-
const direction = c.y > frame.height / 2 ? -1 : 1;
|
|
4606
|
-
for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
|
|
4607
|
-
const y = c.y + direction * tries * row;
|
|
4608
|
-
const box = {
|
|
4609
|
-
x0: c.x - w / 2,
|
|
4610
|
-
x1: c.x + w / 2,
|
|
4611
|
-
y0: y - LABEL_PX - LABEL_PAD,
|
|
4612
|
-
y1: y + LABEL_PAD
|
|
4613
|
-
};
|
|
4614
|
-
if (offFrame(box, frame)) {
|
|
4615
|
-
break;
|
|
4616
|
-
}
|
|
4617
|
-
if (insideFrame(box, frame) && take(box)) {
|
|
4618
|
-
out.push({ ...c, y, w });
|
|
4619
|
-
break;
|
|
4620
|
-
}
|
|
4621
|
-
}
|
|
4622
|
-
}
|
|
4623
|
-
return out;
|
|
4624
|
-
}
|
|
4625
|
-
|
|
4626
|
-
// src/graphLabels.ts
|
|
4627
|
-
var MIN_DELETION_LABEL_PX = 26;
|
|
4628
|
-
var PX_PER_NODE_LABEL = 4e4;
|
|
4629
|
-
var MIN_NODE_LABELS = 8;
|
|
4630
|
-
var LEADER_GAP_PX = 8;
|
|
4631
|
-
var LABEL_CHAR_PX2 = 5.7;
|
|
4632
|
-
var LABEL_PAD_PX = 6;
|
|
4633
|
-
var LABEL_HEIGHT_PX = 13;
|
|
4634
|
-
var LABEL_GAP_PX = 3;
|
|
4635
|
-
var MAX_LABEL_OVERHANG = 2;
|
|
4636
|
-
function labelHalfWidth(text) {
|
|
4637
|
-
return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
|
|
4638
|
-
}
|
|
4639
|
-
var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
|
|
4640
|
-
var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
|
|
4641
|
-
function boxAt(halfW, x, y) {
|
|
4642
|
-
return {
|
|
4643
|
-
x0: x - halfW,
|
|
4644
|
-
x1: x + halfW,
|
|
4645
|
-
y0: y - LABEL_HALF_HEIGHT,
|
|
4646
|
-
y1: y + LABEL_HALF_HEIGHT
|
|
4647
|
-
};
|
|
4648
|
-
}
|
|
4649
|
-
function rowLabelBox(text, screenY) {
|
|
4650
|
-
return {
|
|
4651
|
-
x0: 6,
|
|
4652
|
-
x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
|
|
4653
|
-
y0: screenY - 8 - LABEL_GAP_PX / 2,
|
|
4654
|
-
y1: screenY + 8 + LABEL_GAP_PX / 2
|
|
4655
|
-
};
|
|
4656
|
-
}
|
|
4657
|
-
function clamp(v, lo, hi) {
|
|
4658
|
-
return hi < lo ? v : Math.min(Math.max(v, lo), hi);
|
|
4659
|
-
}
|
|
4660
|
-
function formatBp(bp) {
|
|
4661
|
-
if (bp >= 1e6) {
|
|
4662
|
-
return `${+(bp / 1e6).toFixed(1)} Mb`;
|
|
4663
|
-
}
|
|
4664
|
-
return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
|
|
4665
|
-
}
|
|
4666
|
-
function deletionText(bp) {
|
|
4667
|
-
return `${formatBp(bp)} del`;
|
|
4668
|
-
}
|
|
4669
|
-
function arcOutward(curves, apex, scaleX, scaleY) {
|
|
4670
|
-
const first = curves[0];
|
|
4671
|
-
const last = curves[curves.length - 1];
|
|
4672
|
-
const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
|
|
4673
|
-
const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
|
|
4674
|
-
const len = Math.hypot(dx, dy);
|
|
4675
|
-
return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
|
|
4676
|
-
}
|
|
4677
|
-
function boxSupport(box, dir) {
|
|
4678
|
-
return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
|
|
4679
|
-
}
|
|
4680
|
-
function boxRayCrossing(box, dir) {
|
|
4681
|
-
const halfW = (box.x1 - box.x0) / 2;
|
|
4682
|
-
const halfH = (box.y1 - box.y0) / 2;
|
|
4683
|
-
return Math.min(
|
|
4684
|
-
dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
|
|
4685
|
-
dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
|
|
4686
|
-
);
|
|
4687
|
-
}
|
|
4688
|
-
function drawnLength(segments, scaleX, scaleY) {
|
|
4689
|
-
let total = 0;
|
|
4690
|
-
for (let i = 1; i < segments.length; i++) {
|
|
4691
|
-
const a = segments[i - 1];
|
|
4692
|
-
const b = segments[i];
|
|
4693
|
-
total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
|
|
4694
|
-
}
|
|
4695
|
-
return total;
|
|
4696
|
-
}
|
|
4697
|
-
function runBounds(nodeIds, nodePositions) {
|
|
4698
|
-
let minX = Infinity;
|
|
4699
|
-
let minY = Infinity;
|
|
4700
|
-
let maxX = -Infinity;
|
|
4701
|
-
let maxY = -Infinity;
|
|
4702
|
-
for (const id of nodeIds) {
|
|
4703
|
-
for (const p of nodePositions[id] ?? []) {
|
|
4704
|
-
minX = Math.min(minX, p.x);
|
|
4705
|
-
maxX = Math.max(maxX, p.x);
|
|
4706
|
-
minY = Math.min(minY, p.y);
|
|
4707
|
-
maxY = Math.max(maxY, p.y);
|
|
4708
|
-
}
|
|
4709
|
-
}
|
|
4710
|
-
return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
|
|
4711
|
-
}
|
|
4712
|
-
var nodeOrderCache = /* @__PURE__ */ new WeakMap();
|
|
4713
|
-
function labelOrder(nodePositions, nodeLengths) {
|
|
4714
|
-
const cached2 = nodeOrderCache.get(nodePositions);
|
|
4715
|
-
if (cached2?.nodeLengths === nodeLengths) {
|
|
4716
|
-
return cached2.order;
|
|
4717
|
-
}
|
|
4718
|
-
const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
|
|
4719
|
-
nodeOrderCache.set(nodePositions, { nodeLengths, order });
|
|
4720
|
-
return order;
|
|
4721
|
-
}
|
|
4722
|
-
var arcCache = /* @__PURE__ */ new WeakMap();
|
|
4723
|
-
function arcPlacements(nodePositions, deletions, axis, version) {
|
|
4724
|
-
const { scaleX, scaleY } = axis;
|
|
4725
|
-
const cached2 = arcCache.get(nodePositions);
|
|
4726
|
-
if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
|
|
4727
|
-
return cached2.arcs;
|
|
4728
|
-
}
|
|
4729
|
-
const arcs = [];
|
|
4730
|
-
for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
|
|
4731
|
-
const curves = deletionArcCurves(nodePositions, deletion, axis);
|
|
4732
|
-
const apex = curves ? curveMidpoint(curves) : void 0;
|
|
4733
|
-
if (curves && apex) {
|
|
4734
|
-
const { minX, minY, maxX, maxY } = curveBounds(curves);
|
|
4735
|
-
arcs.push({
|
|
4736
|
-
deletion,
|
|
4737
|
-
curves,
|
|
4738
|
-
apex,
|
|
4739
|
-
// in screen px, so the two axes' units cannot be added by accident
|
|
4740
|
-
extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
|
|
4741
|
-
});
|
|
4742
|
-
}
|
|
4743
|
-
}
|
|
4744
|
-
arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
|
|
4745
|
-
return arcs;
|
|
4746
|
-
}
|
|
4747
|
-
function sizeLabelCandidates({
|
|
4748
|
-
nodePositions,
|
|
4749
|
-
nodeLengths,
|
|
4750
|
-
deletions,
|
|
4751
|
-
alleleDeletions,
|
|
4752
|
-
axis,
|
|
4753
|
-
translateX,
|
|
4754
|
-
translateY,
|
|
4755
|
-
width,
|
|
4756
|
-
height,
|
|
4757
|
-
version = 0
|
|
4758
|
-
}) {
|
|
4759
|
-
const { scaleX, scaleY } = axis;
|
|
4760
|
-
const deletionCandidates = [];
|
|
4761
|
-
const nodeCandidates = [];
|
|
4762
|
-
for (const { deletion, curves, apex, extent } of arcPlacements(
|
|
4763
|
-
nodePositions,
|
|
4764
|
-
deletions,
|
|
4765
|
-
axis,
|
|
4766
|
-
version
|
|
4767
|
-
)) {
|
|
4768
|
-
if (extent >= MIN_DELETION_LABEL_PX) {
|
|
4769
|
-
const text = deletionText(deletion.bp);
|
|
4770
|
-
const halfW = labelHalfWidth(text);
|
|
4771
|
-
const box = boxAt(halfW, 0, 0);
|
|
4772
|
-
const arcX = apex.x * scaleX + translateX;
|
|
4773
|
-
const arcY = apex.y * scaleY + translateY;
|
|
4774
|
-
const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
|
|
4775
|
-
const bow = arcOutward(curves, apex, scaleX, scaleY);
|
|
4776
|
-
const offset = boxSupport(box, bow) + LEADER_GAP_PX;
|
|
4777
|
-
const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
|
|
4778
|
-
const y = fits ? arcY : clamp(
|
|
4779
|
-
arcY + bow.y * offset,
|
|
4780
|
-
LABEL_HALF_HEIGHT,
|
|
4781
|
-
height - LABEL_HALF_HEIGHT
|
|
4782
|
-
);
|
|
4783
|
-
const away = Math.hypot(x - arcX, y - arcY);
|
|
4784
|
-
let leader;
|
|
4785
|
-
if (away > 0) {
|
|
4786
|
-
const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
|
|
4787
|
-
const crossing = boxRayCrossing(box, dir);
|
|
4788
|
-
leader = {
|
|
4789
|
-
arcX,
|
|
4790
|
-
arcY,
|
|
4791
|
-
labelX: x - dir.x * crossing,
|
|
4792
|
-
labelY: y - dir.y * crossing
|
|
4793
|
-
};
|
|
4794
|
-
}
|
|
4795
|
-
deletionCandidates.push({
|
|
4796
|
-
label: {
|
|
4797
|
-
key: `del:${deletion.edgeIndex}`,
|
|
4798
|
-
text,
|
|
4799
|
-
x,
|
|
4800
|
-
y,
|
|
4801
|
-
kind: "deletion",
|
|
4802
|
-
leader
|
|
4803
|
-
},
|
|
4804
|
-
box: boxAt(halfW, x, y)
|
|
4805
|
-
});
|
|
4806
|
-
}
|
|
4807
|
-
}
|
|
4808
|
-
const labelledByDeletion = /* @__PURE__ */ new Set();
|
|
4809
|
-
const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
|
|
4810
|
-
for (const run of alleleRuns) {
|
|
4811
|
-
const drawn = runBounds(run.nodeIds, nodePositions);
|
|
4812
|
-
if (!drawn) {
|
|
4813
|
-
continue;
|
|
4814
|
-
}
|
|
4815
|
-
const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
|
|
4816
|
-
const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
|
|
4817
|
-
const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
|
|
4818
|
-
const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
|
|
4819
|
-
if (shownRight < shownLeft || shownBottom < shownTop) {
|
|
4820
|
-
continue;
|
|
4821
|
-
}
|
|
4822
|
-
const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
|
|
4823
|
-
const text = deletionText(run.bp);
|
|
4824
|
-
const halfW = labelHalfWidth(text);
|
|
4825
|
-
if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
|
|
4826
|
-
for (const id of run.nodeIds) {
|
|
4827
|
-
labelledByDeletion.add(id);
|
|
4828
|
-
}
|
|
4829
|
-
const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
|
|
4830
|
-
const y = clamp(
|
|
4831
|
-
(shownTop + shownBottom) / 2,
|
|
4832
|
-
LABEL_HALF_HEIGHT,
|
|
4833
|
-
height - LABEL_HALF_HEIGHT
|
|
4834
|
-
);
|
|
4835
|
-
deletionCandidates.push({
|
|
4836
|
-
label: {
|
|
4837
|
-
key: `alleledel:${run.nodeIds.join(",")}`,
|
|
4838
|
-
text,
|
|
4839
|
-
x,
|
|
4840
|
-
y,
|
|
4841
|
-
kind: "deletion"
|
|
4842
|
-
},
|
|
4843
|
-
box: boxAt(halfW, x, y)
|
|
4844
|
-
});
|
|
4845
|
-
}
|
|
4846
|
-
}
|
|
4847
|
-
for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
|
|
4848
|
-
if (labelledByDeletion.has(id)) {
|
|
4849
|
-
continue;
|
|
4850
|
-
}
|
|
4851
|
-
const { x, y } = polylineMidpoint(segments);
|
|
4852
|
-
const screenY = y * scaleY + translateY;
|
|
4853
|
-
if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
|
|
4854
|
-
continue;
|
|
4855
|
-
}
|
|
4856
|
-
const drawn = drawnLength(segments, scaleX, scaleY);
|
|
4857
|
-
if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
|
|
4858
|
-
continue;
|
|
4859
|
-
}
|
|
4860
|
-
const text = formatBp(nodeLengths.get(id));
|
|
4861
|
-
const halfW = labelHalfWidth(text);
|
|
4862
|
-
if (drawn >= halfW * 2 / MAX_LABEL_OVERHANG) {
|
|
4863
|
-
const screenX = x * scaleX + translateX;
|
|
4864
|
-
nodeCandidates.push({
|
|
4865
|
-
label: {
|
|
4866
|
-
key: `node:${id}`,
|
|
4867
|
-
text,
|
|
4868
|
-
x: screenX,
|
|
4869
|
-
y: screenY,
|
|
4870
|
-
kind: "node"
|
|
4871
|
-
},
|
|
4872
|
-
box: boxAt(halfW, screenX, screenY)
|
|
4873
|
-
});
|
|
4874
|
-
}
|
|
4875
|
-
}
|
|
4876
|
-
return { deletions: deletionCandidates, nodes: nodeCandidates };
|
|
4877
|
-
}
|
|
4878
|
-
function nodeLabelBudget(width, height) {
|
|
4879
|
-
return Math.max(
|
|
4880
|
-
MIN_NODE_LABELS,
|
|
4881
|
-
Math.floor(width * height / PX_PER_NODE_LABEL)
|
|
4882
|
-
);
|
|
4883
|
-
}
|
|
4884
|
-
function placeSizeLabels(candidates, take, budget = Infinity) {
|
|
4885
|
-
const labels = [];
|
|
4886
|
-
for (const { label, box } of candidates) {
|
|
4887
|
-
if (labels.length >= budget) {
|
|
4888
|
-
break;
|
|
4889
|
-
}
|
|
4890
|
-
if (take(box)) {
|
|
4891
|
-
labels.push(label);
|
|
4892
|
-
}
|
|
4893
|
-
}
|
|
4894
|
-
return labels;
|
|
4895
|
-
}
|
|
4896
|
-
|
|
4897
988
|
// src/tubeMap/axis.ts
|
|
4898
989
|
function referenceBoxes(graph, layout) {
|
|
4899
990
|
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
@@ -4950,7 +1041,7 @@ function rulerBoxes(byRefName) {
|
|
|
4950
1041
|
return best;
|
|
4951
1042
|
}
|
|
4952
1043
|
var TICK_PX = 5;
|
|
4953
|
-
var
|
|
1044
|
+
var LABEL_GAP_PX = 10;
|
|
4954
1045
|
var TARGET_TICK_PX = 110;
|
|
4955
1046
|
var MIN_BRACKET_PX = 3;
|
|
4956
1047
|
function rulerInk(darkMode) {
|
|
@@ -4965,7 +1056,7 @@ function placeLabels2(candidates) {
|
|
|
4965
1056
|
const placed = [];
|
|
4966
1057
|
for (const a of candidates) {
|
|
4967
1058
|
if (placed.every(
|
|
4968
|
-
(b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 +
|
|
1059
|
+
(b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX
|
|
4969
1060
|
)) {
|
|
4970
1061
|
placed.push(a);
|
|
4971
1062
|
}
|
|
@@ -5248,304 +1339,6 @@ function drawTubeMapConnectors(ctx, connectors, bottom, {
|
|
|
5248
1339
|
}
|
|
5249
1340
|
}
|
|
5250
1341
|
|
|
5251
|
-
// src/walkEncoding.ts
|
|
5252
|
-
import { packAbgr as packAbgr3 } from "@jbrowse/core/util/colorBits";
|
|
5253
|
-
var WALK_FIELDS = [
|
|
5254
|
-
{ value: "progress", label: "Progress along the walk" },
|
|
5255
|
-
{ value: "walk", label: "One colour for the walk" },
|
|
5256
|
-
{ value: "reference", label: "Reference position" }
|
|
5257
|
-
];
|
|
5258
|
-
var WALK_SCHEMES = [
|
|
5259
|
-
{ value: "grey", label: "Grey to black", ends: ["#a3a3a3", "#151515"] },
|
|
5260
|
-
{ value: "blue", label: "Cyan to navy", ends: ["#6ee0f0", "#0b2c7a"] },
|
|
5261
|
-
{ value: "red", label: "Yellow to red", ends: ["#ffd24a", "#b3121b"] },
|
|
5262
|
-
{ value: "purple", label: "Pink to plum", ends: ["#f5a3d8", "#5e1a6e"] },
|
|
5263
|
-
{ value: "green", label: "Lime to forest", ends: ["#b6e36c", "#0b5d3b"] },
|
|
5264
|
-
{ value: "rainbow", label: "Rainbow" }
|
|
5265
|
-
];
|
|
5266
|
-
var FAMILIES = ["blue", "red", "purple", "green"];
|
|
5267
|
-
var isField = (v) => WALK_FIELDS.some((f) => f.value === v);
|
|
5268
|
-
var isScheme = (v) => WALK_SCHEMES.some((s) => s.value === v);
|
|
5269
|
-
function resolveEncoding(layer, reference, picked, alone = true) {
|
|
5270
|
-
const { field: askedField, scheme: askedScheme } = layer.color ?? {};
|
|
5271
|
-
const field = isField(askedField) ? askedField : alone ? "progress" : "walk";
|
|
5272
|
-
const family = reference ? "grey" : FAMILIES[picked % FAMILIES.length];
|
|
5273
|
-
const scheme = isScheme(askedScheme) ? askedScheme === "rainbow" && field !== "reference" ? family : askedScheme : family;
|
|
5274
|
-
return { field, scheme };
|
|
5275
|
-
}
|
|
5276
|
-
var BASE_T = 0.5;
|
|
5277
|
-
var toLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
5278
|
-
var toGamma = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
|
|
5279
|
-
function oklab(hex) {
|
|
5280
|
-
const [r, g, b] = [1, 3, 5].map(
|
|
5281
|
-
(i) => toLinear(Number.parseInt(hex.slice(i, i + 2), 16) / 255)
|
|
5282
|
-
);
|
|
5283
|
-
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
5284
|
-
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
5285
|
-
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
5286
|
-
return [
|
|
5287
|
-
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
5288
|
-
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
5289
|
-
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
|
|
5290
|
-
];
|
|
5291
|
-
}
|
|
5292
|
-
function oklabToRgb([L, a, b]) {
|
|
5293
|
-
const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
|
|
5294
|
-
const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
|
|
5295
|
-
const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
|
|
5296
|
-
return [
|
|
5297
|
-
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
5298
|
-
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
5299
|
-
-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
|
|
5300
|
-
].map((c) => toGamma(Math.max(0, Math.min(1, c))) * 255);
|
|
5301
|
-
}
|
|
5302
|
-
function schemeRgb(scheme, t) {
|
|
5303
|
-
const x = Math.max(0, Math.min(1, t));
|
|
5304
|
-
const family = WALK_SCHEMES.find((s) => s.value === scheme);
|
|
5305
|
-
if (!family || !("ends" in family)) {
|
|
5306
|
-
return hslToRgb(x * REFERENCE_RAMP_MAX_HUE, 0.7, 0.5).map((c) => c * 255);
|
|
5307
|
-
}
|
|
5308
|
-
const from = oklab(family.ends[0]);
|
|
5309
|
-
const to = oklab(family.ends[1]);
|
|
5310
|
-
return oklabToRgb(from.map((v, i) => v + (to[i] - v) * x));
|
|
5311
|
-
}
|
|
5312
|
-
function schemeCss(scheme, t) {
|
|
5313
|
-
const [r, g, b] = schemeRgb(scheme, t).map(Math.round);
|
|
5314
|
-
return `rgb(${r}, ${g}, ${b})`;
|
|
5315
|
-
}
|
|
5316
|
-
function schemeColor(scheme, t) {
|
|
5317
|
-
const [r, g, b] = schemeRgb(scheme, t).map(Math.round);
|
|
5318
|
-
return packAbgr3(r, g, b, 255);
|
|
5319
|
-
}
|
|
5320
|
-
function encodedColor({ field, scheme }, t) {
|
|
5321
|
-
return field === "walk" ? schemeColor(scheme, BASE_T) : t === void 0 ? NO_VALUE_COLOR : schemeColor(scheme, t);
|
|
5322
|
-
}
|
|
5323
|
-
var NO_VALUE_COLOR = REFERENCE_RAMP_ALT_COLOR;
|
|
5324
|
-
function encodingSwatchCss({ field, scheme }) {
|
|
5325
|
-
if (field === "walk") {
|
|
5326
|
-
return schemeCss(scheme, BASE_T);
|
|
5327
|
-
}
|
|
5328
|
-
const stops = Array.from({ length: 7 }, (_, i) => {
|
|
5329
|
-
const t = i / 6;
|
|
5330
|
-
return `${schemeCss(scheme, t)} ${t * 100}%`;
|
|
5331
|
-
});
|
|
5332
|
-
return `linear-gradient(to right, ${stops.join(", ")})`;
|
|
5333
|
-
}
|
|
5334
|
-
|
|
5335
|
-
// src/walkHighlight.ts
|
|
5336
|
-
function referenceOf(graph) {
|
|
5337
|
-
return graph.referencePath ? graph.paths?.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
|
|
5338
|
-
}
|
|
5339
|
-
function walkHighlight(graph, name) {
|
|
5340
|
-
const path = graph.paths?.find((p) => p.name === name);
|
|
5341
|
-
if (!path) {
|
|
5342
|
-
return void 0;
|
|
5343
|
-
}
|
|
5344
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5345
|
-
const edgeAt = /* @__PURE__ */ new Map();
|
|
5346
|
-
graph.edges.forEach((e, i) => {
|
|
5347
|
-
edgeAt.set(`${e.from}>${e.to}`, i);
|
|
5348
|
-
});
|
|
5349
|
-
const edgeIndexes = /* @__PURE__ */ new Set();
|
|
5350
|
-
for (let i = 1; i < path.nodeIds.length; i++) {
|
|
5351
|
-
const a = path.nodeIds[i - 1];
|
|
5352
|
-
const b = path.nodeIds[i];
|
|
5353
|
-
const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
|
|
5354
|
-
if (index !== void 0) {
|
|
5355
|
-
edgeIndexes.add(index);
|
|
5356
|
-
}
|
|
5357
|
-
}
|
|
5358
|
-
const lengthOf = (id) => byId.get(id)?.length ?? 0;
|
|
5359
|
-
const bp = path.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0);
|
|
5360
|
-
const progress = /* @__PURE__ */ new Map();
|
|
5361
|
-
let before = 0;
|
|
5362
|
-
for (const id of path.nodeIds) {
|
|
5363
|
-
const length = lengthOf(id);
|
|
5364
|
-
if (!progress.has(id)) {
|
|
5365
|
-
progress.set(id, bp > 0 ? (before + length / 2) / bp : 0);
|
|
5366
|
-
}
|
|
5367
|
-
before += length;
|
|
5368
|
-
}
|
|
5369
|
-
const reference = referenceOf(graph);
|
|
5370
|
-
const originName = pathOrigin(name).name;
|
|
5371
|
-
const origin = graph.anchorPaths?.find((a) => a.name === originName && a.length === bp) ?? graph.anchorPaths?.find((a) => a.name === originName);
|
|
5372
|
-
return {
|
|
5373
|
-
name,
|
|
5374
|
-
reference: reference === path,
|
|
5375
|
-
range: origin ? {
|
|
5376
|
-
contig: path.contig ?? panSNContig(origin.name),
|
|
5377
|
-
start: origin.start,
|
|
5378
|
-
end: origin.start + origin.length
|
|
5379
|
-
} : void 0,
|
|
5380
|
-
nodeIds: new Set(path.nodeIds),
|
|
5381
|
-
edgeIndexes,
|
|
5382
|
-
progress,
|
|
5383
|
-
steps: path.nodeIds.length,
|
|
5384
|
-
bp,
|
|
5385
|
-
referenceBp: reference && reference !== path ? reference.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0) : void 0
|
|
5386
|
-
};
|
|
5387
|
-
}
|
|
5388
|
-
function laneColors(graph, walk, encoding, ramp) {
|
|
5389
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5390
|
-
const colors = /* @__PURE__ */ new Map();
|
|
5391
|
-
for (const [id, progress] of walk.progress) {
|
|
5392
|
-
const node = byId.get(id);
|
|
5393
|
-
const mid = ramp?.midpoints.get(id);
|
|
5394
|
-
const t = encoding.field === "progress" ? progress : ramp && mid !== void 0 && !(node?.stable && node.stable.rank > 0) ? (mid - ramp.start) / ramp.span : void 0;
|
|
5395
|
-
colors.set(id, encodedColor(encoding, t));
|
|
5396
|
-
}
|
|
5397
|
-
return colors;
|
|
5398
|
-
}
|
|
5399
|
-
function reversedNodes(graph, walk) {
|
|
5400
|
-
const reference = referenceOf(graph);
|
|
5401
|
-
const reversed = /* @__PURE__ */ new Set();
|
|
5402
|
-
let bp = 0;
|
|
5403
|
-
if (!reference || walk.reference) {
|
|
5404
|
-
return { reversed, bp };
|
|
5405
|
-
}
|
|
5406
|
-
const walkPath = pathOrigin(walk.name).name;
|
|
5407
|
-
const referencePath = pathOrigin(reference.name).name;
|
|
5408
|
-
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
5409
|
-
for (const id of walk.progress.keys()) {
|
|
5410
|
-
const node = byId.get(id);
|
|
5411
|
-
const visits = node ? graph.pathVisits?.get(node.name) : void 0;
|
|
5412
|
-
const own = visits?.find((v) => v.path === walkPath)?.strand;
|
|
5413
|
-
const theirs = visits?.find((v) => v.path === referencePath)?.strand;
|
|
5414
|
-
if (own && theirs && own !== theirs) {
|
|
5415
|
-
reversed.add(id);
|
|
5416
|
-
bp += node.length;
|
|
5417
|
-
}
|
|
5418
|
-
}
|
|
5419
|
-
return { reversed, bp };
|
|
5420
|
-
}
|
|
5421
|
-
function walkLift(graph, layers, ramp) {
|
|
5422
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5423
|
-
const lifted = layers.filter((layer) => !seen.has(layer.walk) && seen.add(layer.walk)).flatMap((layer) => {
|
|
5424
|
-
const walk = walkHighlight(graph, layer.walk);
|
|
5425
|
-
return walk ? [{ walk, layer }] : [];
|
|
5426
|
-
}).sort((a, b) => Number(b.walk.reference) - Number(a.walk.reference));
|
|
5427
|
-
if (lifted.length === 0) {
|
|
5428
|
-
return void 0;
|
|
5429
|
-
}
|
|
5430
|
-
let picked = 0;
|
|
5431
|
-
const walks = lifted.map(({ walk, layer }) => {
|
|
5432
|
-
const encoding = resolveEncoding(
|
|
5433
|
-
layer,
|
|
5434
|
-
walk.reference,
|
|
5435
|
-
walk.reference ? 0 : picked++,
|
|
5436
|
-
lifted.length === 1
|
|
5437
|
-
);
|
|
5438
|
-
const { reversed, bp } = reversedNodes(graph, walk);
|
|
5439
|
-
return {
|
|
5440
|
-
...walk,
|
|
5441
|
-
encoding,
|
|
5442
|
-
colors: laneColors(graph, walk, encoding, ramp),
|
|
5443
|
-
reversed,
|
|
5444
|
-
reversedBp: bp
|
|
5445
|
-
};
|
|
5446
|
-
});
|
|
5447
|
-
return liftOf(walks, ramp);
|
|
5448
|
-
}
|
|
5449
|
-
var FACET_COLOR = { field: "progress", scheme: "red" };
|
|
5450
|
-
function facetLifts(graph, lift, layers, ramp) {
|
|
5451
|
-
return lift.walks.map((walk) => {
|
|
5452
|
-
const stated = layers.find((l) => l.walk === walk.name)?.color;
|
|
5453
|
-
const encoding = resolveEncoding(
|
|
5454
|
-
{ walk: walk.name, color: { ...FACET_COLOR, ...stated } },
|
|
5455
|
-
walk.reference,
|
|
5456
|
-
0
|
|
5457
|
-
);
|
|
5458
|
-
return liftOf(
|
|
5459
|
-
[
|
|
5460
|
-
{
|
|
5461
|
-
...walk,
|
|
5462
|
-
encoding,
|
|
5463
|
-
colors: laneColors(graph, walk, encoding, ramp)
|
|
5464
|
-
}
|
|
5465
|
-
],
|
|
5466
|
-
ramp
|
|
5467
|
-
);
|
|
5468
|
-
});
|
|
5469
|
-
}
|
|
5470
|
-
function liftOf(walks, ramp) {
|
|
5471
|
-
return {
|
|
5472
|
-
walks,
|
|
5473
|
-
referenceDomain: ramp ? { start: ramp.start, end: ramp.start + ramp.span } : void 0,
|
|
5474
|
-
nodeIds: new Set(walks.flatMap((w) => [...w.nodeIds])),
|
|
5475
|
-
edgeIndexes: new Set(walks.flatMap((w) => [...w.edgeIndexes])),
|
|
5476
|
-
names: new Set(walks.map((w) => w.name))
|
|
5477
|
-
};
|
|
5478
|
-
}
|
|
5479
|
-
|
|
5480
|
-
// src/walkKey.ts
|
|
5481
|
-
function rangeText(contig, start, end) {
|
|
5482
|
-
const s = Math.round(start);
|
|
5483
|
-
const e = Math.round(end);
|
|
5484
|
-
return `${contig ? `${contig}:` : ""}${s.toLocaleString()}-${e.toLocaleString()} (${formatBp(e - s)})`;
|
|
5485
|
-
}
|
|
5486
|
-
function walkKey(walk, reference) {
|
|
5487
|
-
const { field } = walk.encoding;
|
|
5488
|
-
const own = walk.range ? rangeText(walk.range.contig, walk.range.start, walk.range.end) : void 0;
|
|
5489
|
-
const scale = field === "progress" ? own : field === "reference" && reference ? rangeText(reference.name, reference.start, reference.end) : void 0;
|
|
5490
|
-
return {
|
|
5491
|
-
delta: walk.referenceBp === void 0 || walk.bp === walk.referenceBp ? "" : ` ${walk.bp > walk.referenceBp ? "+" : "\u2212"}${formatBp(Math.abs(walk.bp - walk.referenceBp))}`,
|
|
5492
|
-
reversed: walk.reversedBp > 0 ? `, ${formatBp(walk.reversedBp)} reversed` : "",
|
|
5493
|
-
shades: field !== "walk",
|
|
5494
|
-
scale,
|
|
5495
|
-
hover: scale === own ? void 0 : own
|
|
5496
|
-
};
|
|
5497
|
-
}
|
|
5498
|
-
|
|
5499
|
-
// src/facetGrid.ts
|
|
5500
|
-
var FACET_GAP_PX = 8;
|
|
5501
|
-
var FACET_TITLE_PX = 34;
|
|
5502
|
-
var FACET_PAD_PX = 12;
|
|
5503
|
-
var MIN_FACET_PANEL_PX = 60;
|
|
5504
|
-
function facetGrid({
|
|
5505
|
-
count,
|
|
5506
|
-
bounds,
|
|
5507
|
-
pixelRows,
|
|
5508
|
-
width,
|
|
5509
|
-
room,
|
|
5510
|
-
columns
|
|
5511
|
-
}) {
|
|
5512
|
-
function tile(across) {
|
|
5513
|
-
const rows = Math.ceil(count / across);
|
|
5514
|
-
const panelWidth = Math.floor(
|
|
5515
|
-
(width - (across - 1) * FACET_GAP_PX) / across
|
|
5516
|
-
);
|
|
5517
|
-
const panelRoom = (room - rows * FACET_TITLE_PX - (rows - 1) * FACET_GAP_PX) / rows;
|
|
5518
|
-
const fitX = (panelWidth - 2 * FACET_PAD_PX) / bounds.w;
|
|
5519
|
-
const fitY = bounds.h > 0 ? (panelRoom - 2 * FACET_PAD_PX) / bounds.h : Infinity;
|
|
5520
|
-
const scale = pixelRows ? fitX : Math.min(fitX, fitY);
|
|
5521
|
-
const drawn = pixelRows ? bounds.h : bounds.h * scale;
|
|
5522
|
-
const height = Math.floor(
|
|
5523
|
-
Math.max(
|
|
5524
|
-
MIN_FACET_PANEL_PX,
|
|
5525
|
-
Math.min(drawn + 2 * FACET_PAD_PX, panelRoom)
|
|
5526
|
-
)
|
|
5527
|
-
);
|
|
5528
|
-
return {
|
|
5529
|
-
columns: across,
|
|
5530
|
-
width: panelWidth,
|
|
5531
|
-
height,
|
|
5532
|
-
scale,
|
|
5533
|
-
total: rows * (height + FACET_TITLE_PX) + (rows - 1) * FACET_GAP_PX
|
|
5534
|
-
};
|
|
5535
|
-
}
|
|
5536
|
-
if (columns !== void 0) {
|
|
5537
|
-
return tile(Math.max(1, Math.min(count, Math.round(columns))));
|
|
5538
|
-
}
|
|
5539
|
-
let best = tile(1);
|
|
5540
|
-
for (let across = 2; across <= count; across++) {
|
|
5541
|
-
const grid = tile(across);
|
|
5542
|
-
if (grid.scale > best.scale) {
|
|
5543
|
-
best = grid;
|
|
5544
|
-
}
|
|
5545
|
-
}
|
|
5546
|
-
return best;
|
|
5547
|
-
}
|
|
5548
|
-
|
|
5549
1342
|
// src/bubbles/bubbleLine.ts
|
|
5550
1343
|
var SATURATED_PATH_COUNT = 2147483647;
|
|
5551
1344
|
|
|
@@ -5913,6 +1706,15 @@ var ROUTE_STACK = 8;
|
|
|
5913
1706
|
var GENE_PIN_DROP_PX = 18;
|
|
5914
1707
|
var GENE_STACK = 2;
|
|
5915
1708
|
var TOPMOST_BASELINE = LABEL_PX + LABEL_PAD;
|
|
1709
|
+
function geneCoverageNote(pin) {
|
|
1710
|
+
const length = pin.gene.end - pin.gene.start;
|
|
1711
|
+
if (pin.covered >= 0.98) {
|
|
1712
|
+
return void 0;
|
|
1713
|
+
}
|
|
1714
|
+
const [per, unit] = length >= 1e6 ? [1e6, "Mb"] : length >= 1e3 ? [1e3, "kb"] : [1, "bp"];
|
|
1715
|
+
const amount = (bp) => +(bp / per).toFixed(per === 1 ? 0 : 1);
|
|
1716
|
+
return `${amount(pin.covered * length)} of ${amount(length)} ${unit}`;
|
|
1717
|
+
}
|
|
5916
1718
|
function byExtent(items, extent) {
|
|
5917
1719
|
return [...items].sort((a, b) => extent(b) - extent(a));
|
|
5918
1720
|
}
|
|
@@ -5972,11 +1774,13 @@ function layoutLabels(m) {
|
|
|
5972
1774
|
const genes = placeLabels(
|
|
5973
1775
|
byExtent(m.genePins, (pin) => pin.gene.end - pin.gene.start).map((pin) => {
|
|
5974
1776
|
const { x, y } = screen(pin.at);
|
|
1777
|
+
const note = geneCoverageNote(pin);
|
|
5975
1778
|
return {
|
|
5976
1779
|
item: pin,
|
|
5977
1780
|
x,
|
|
5978
1781
|
y: y + m.contigThickness + GENE_PIN_DROP_PX,
|
|
5979
|
-
text:
|
|
1782
|
+
text: note ? `${pin.gene.name} \xB7 ${note}` : pin.gene.name,
|
|
1783
|
+
fallback: note ? `${pin.gene.name} \u2026` : void 0,
|
|
5980
1784
|
stack: GENE_STACK
|
|
5981
1785
|
};
|
|
5982
1786
|
}),
|
|
@@ -6048,6 +1852,7 @@ function genePins(graph, genes, positions) {
|
|
|
6048
1852
|
continue;
|
|
6049
1853
|
}
|
|
6050
1854
|
const parts = [];
|
|
1855
|
+
const exonsByNode = [];
|
|
6051
1856
|
let at;
|
|
6052
1857
|
let atDistance = Infinity;
|
|
6053
1858
|
let covered = 0;
|
|
@@ -6062,6 +1867,7 @@ function genePins(graph, genes, positions) {
|
|
|
6062
1867
|
}
|
|
6063
1868
|
const line = positions[node.id];
|
|
6064
1869
|
covered += Math.min(nodeEnd, gene.end) - Math.max(nodeStart, gene.start);
|
|
1870
|
+
const nodeParts = [];
|
|
6065
1871
|
for (const exon of gene.exons) {
|
|
6066
1872
|
const a = Math.max(exon.start, nodeStart);
|
|
6067
1873
|
const b = Math.min(exon.end, nodeEnd);
|
|
@@ -6076,7 +1882,11 @@ function genePins(graph, genes, positions) {
|
|
|
6076
1882
|
if (stretch.length === 1) {
|
|
6077
1883
|
stretch.push({ ...stretch[0] });
|
|
6078
1884
|
}
|
|
6079
|
-
|
|
1885
|
+
nodeParts.push(svgPath(stretch));
|
|
1886
|
+
}
|
|
1887
|
+
if (nodeParts.length > 0) {
|
|
1888
|
+
parts.push(...nodeParts);
|
|
1889
|
+
exonsByNode.push({ nodeId: node.id, d: nodeParts.join("") });
|
|
6080
1890
|
}
|
|
6081
1891
|
const pinBp = Math.min(Math.max(mid, nodeStart), nodeEnd);
|
|
6082
1892
|
const distance = Math.abs(pinBp - mid);
|
|
@@ -6094,6 +1904,7 @@ function genePins(graph, genes, positions) {
|
|
|
6094
1904
|
pins.push({
|
|
6095
1905
|
gene,
|
|
6096
1906
|
exons: parts.join(""),
|
|
1907
|
+
exonsByNode,
|
|
6097
1908
|
at,
|
|
6098
1909
|
covered: covered / (gene.end - gene.start)
|
|
6099
1910
|
});
|
|
@@ -6101,262 +1912,6 @@ function genePins(graph, genes, positions) {
|
|
|
6101
1912
|
}
|
|
6102
1913
|
return pins;
|
|
6103
1914
|
}
|
|
6104
|
-
|
|
6105
|
-
// src/reference.ts
|
|
6106
|
-
var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
|
|
6107
|
-
["hg38", "GRCh38"],
|
|
6108
|
-
["hg19", "GRCh37"],
|
|
6109
|
-
["hs1", "CHM13"],
|
|
6110
|
-
["t2t-chm13", "CHM13"],
|
|
6111
|
-
["chm13v2", "CHM13"]
|
|
6112
|
-
]);
|
|
6113
|
-
function wellKnownSample(assemblyName) {
|
|
6114
|
-
return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
|
|
6115
|
-
}
|
|
6116
|
-
function sampleOf2(refName) {
|
|
6117
|
-
const sample = refName.includes("#") ? panSNSample(refName) : void 0;
|
|
6118
|
-
return sample === void 0 || isGenericSample(sample) ? void 0 : sample;
|
|
6119
|
-
}
|
|
6120
|
-
function haplotypeOf(refName) {
|
|
6121
|
-
return panSNHaplotype(refName) ?? panSNSample(refName);
|
|
6122
|
-
}
|
|
6123
|
-
function graphHaplotypes(graph) {
|
|
6124
|
-
const refNames = new Set((graph.anchorPaths ?? []).map((p) => p.name));
|
|
6125
|
-
for (const node of graph.nodes) {
|
|
6126
|
-
if (node.stable) {
|
|
6127
|
-
refNames.add(node.stable.refName);
|
|
6128
|
-
}
|
|
6129
|
-
}
|
|
6130
|
-
const bySample = /* @__PURE__ */ new Map();
|
|
6131
|
-
for (const refName of refNames) {
|
|
6132
|
-
const sample = sampleOf2(refName);
|
|
6133
|
-
if (sample !== void 0) {
|
|
6134
|
-
const haplotypes = bySample.get(sample) ?? bySample.set(sample, /* @__PURE__ */ new Set()).get(sample);
|
|
6135
|
-
haplotypes.add(haplotypeOf(refName));
|
|
6136
|
-
}
|
|
6137
|
-
}
|
|
6138
|
-
return bySample;
|
|
6139
|
-
}
|
|
6140
|
-
function backboneFrom(contigs, haplotypes) {
|
|
6141
|
-
const samples = new Set(contigs.map((c) => sampleOf2(c.refName)));
|
|
6142
|
-
const [sample] = samples;
|
|
6143
|
-
if (samples.size !== 1 || sample === void 0) {
|
|
6144
|
-
return { contigs, prefixes: [], named: false };
|
|
6145
|
-
}
|
|
6146
|
-
const own = new Set(contigs.map((c) => haplotypeOf(c.refName)));
|
|
6147
|
-
const unique = [...haplotypes.get(sample) ?? []].every((h) => own.has(h));
|
|
6148
|
-
const shared = new Set(contigs.map((c) => panSNHaplotype(c.refName)));
|
|
6149
|
-
const [haplotype] = shared;
|
|
6150
|
-
return {
|
|
6151
|
-
contigs,
|
|
6152
|
-
prefixes: [
|
|
6153
|
-
...unique ? [sample] : [],
|
|
6154
|
-
...shared.size === 1 && haplotype ? [haplotype] : []
|
|
6155
|
-
],
|
|
6156
|
-
named: true
|
|
6157
|
-
};
|
|
6158
|
-
}
|
|
6159
|
-
function graphBackbone(graph) {
|
|
6160
|
-
const spans = /* @__PURE__ */ new Map();
|
|
6161
|
-
for (const node of graph.nodes) {
|
|
6162
|
-
if (isBackbone(node)) {
|
|
6163
|
-
const { refName, start } = node.stable;
|
|
6164
|
-
const end = start + node.length;
|
|
6165
|
-
const span2 = spans.get(refName);
|
|
6166
|
-
if (span2) {
|
|
6167
|
-
span2.start = Math.min(span2.start, start);
|
|
6168
|
-
span2.end = Math.max(span2.end, end);
|
|
6169
|
-
} else {
|
|
6170
|
-
spans.set(refName, {
|
|
6171
|
-
refName,
|
|
6172
|
-
contig: panSNContig(refName),
|
|
6173
|
-
start,
|
|
6174
|
-
end
|
|
6175
|
-
});
|
|
6176
|
-
}
|
|
6177
|
-
}
|
|
6178
|
-
}
|
|
6179
|
-
const contigs = [...spans.values()];
|
|
6180
|
-
return contigs.length ? backboneFrom(contigs, graphHaplotypes(graph)) : void 0;
|
|
6181
|
-
}
|
|
6182
|
-
function backboneAssembly(backbone, assemblies) {
|
|
6183
|
-
const names = (a) => [a.name, ...a.aliases ?? []];
|
|
6184
|
-
const naming = (prefix) => (a) => names(a).some((n) => n.toLowerCase() === prefix.toLowerCase());
|
|
6185
|
-
const prefixes = [...backbone?.prefixes ?? []].reverse();
|
|
6186
|
-
const sample = backbone?.prefixes.find((p) => !p.includes("#"))?.toLowerCase();
|
|
6187
|
-
for (const prefix of prefixes) {
|
|
6188
|
-
const assembly = assemblies.find(naming(prefix));
|
|
6189
|
-
if (assembly) {
|
|
6190
|
-
return assembly;
|
|
6191
|
-
}
|
|
6192
|
-
}
|
|
6193
|
-
return sample === void 0 ? void 0 : assemblies.find(
|
|
6194
|
-
(a) => names(a).some((n) => wellKnownSample(n)?.toLowerCase() === sample)
|
|
6195
|
-
);
|
|
6196
|
-
}
|
|
6197
|
-
function assemblyWalk(graph, assembly) {
|
|
6198
|
-
const haplotypes = graphHaplotypes(graph);
|
|
6199
|
-
return graph.anchorPaths?.find(
|
|
6200
|
-
(walk) => backboneAssembly(
|
|
6201
|
-
backboneFrom(
|
|
6202
|
-
[
|
|
6203
|
-
{
|
|
6204
|
-
refName: walk.name,
|
|
6205
|
-
contig: panSNContig(walk.name),
|
|
6206
|
-
start: walk.start,
|
|
6207
|
-
end: walk.start + walk.length
|
|
6208
|
-
}
|
|
6209
|
-
],
|
|
6210
|
-
haplotypes
|
|
6211
|
-
),
|
|
6212
|
-
[assembly]
|
|
6213
|
-
)
|
|
6214
|
-
);
|
|
6215
|
-
}
|
|
6216
|
-
function refNameBinding(refNames) {
|
|
6217
|
-
const exact = new Set(refNames);
|
|
6218
|
-
const byContig = /* @__PURE__ */ new Map();
|
|
6219
|
-
for (const refName of exact) {
|
|
6220
|
-
const contig = panSNContig(refName);
|
|
6221
|
-
byContig.set(contig, byContig.has(contig) ? void 0 : refName);
|
|
6222
|
-
}
|
|
6223
|
-
return (name) => exact.has(name) ? name : byContig.get(name);
|
|
6224
|
-
}
|
|
6225
|
-
function featuresOnBackbone(features, backbone) {
|
|
6226
|
-
const bind = refNameBinding(backbone.contigs.map((c) => c.refName));
|
|
6227
|
-
return features.flatMap((f) => {
|
|
6228
|
-
const refName = bind(f.refName);
|
|
6229
|
-
return refName === void 0 ? [] : [refName === f.refName ? f : { ...f, refName }];
|
|
6230
|
-
});
|
|
6231
|
-
}
|
|
6232
|
-
|
|
6233
|
-
// src/gbzWindow.ts
|
|
6234
|
-
var NoReferenceSampleError = class extends Error {
|
|
6235
|
-
constructor(anchor, referenceSamples) {
|
|
6236
|
-
super(
|
|
6237
|
-
referenceSamples.length === 0 ? `the graph names no reference sample (gbwt_reference_samples) and the anchor "${anchor}" maps to none; set referenceSample` : `the anchor "${anchor}" is none of the graph's reference samples (${referenceSamples.join(", ")}); set referenceSample or map it through assemblyNameToPanSN`
|
|
6238
|
-
);
|
|
6239
|
-
this.name = "NoReferenceSampleError";
|
|
6240
|
-
}
|
|
6241
|
-
};
|
|
6242
|
-
var NodeLimitError = class extends Error {
|
|
6243
|
-
constructor(limit, windowBp, fitsBp) {
|
|
6244
|
-
super(
|
|
6245
|
-
`this ${windowBp.toLocaleString()} bp window reads more than nodeLimit (${limit.toLocaleString()}) graph nodes; zoom in to about ${fitsBp.toLocaleString()} bp or raise nodeLimit`
|
|
6246
|
-
);
|
|
6247
|
-
this.name = "NodeLimitError";
|
|
6248
|
-
}
|
|
6249
|
-
};
|
|
6250
|
-
function haplotypePrefix(name) {
|
|
6251
|
-
return `${name.sample}#${name.haplotype}`;
|
|
6252
|
-
}
|
|
6253
|
-
function haplotypeWanted(name, wanted) {
|
|
6254
|
-
const prefix = haplotypePrefix(name);
|
|
6255
|
-
return wanted === void 0 || wanted.some((candidate) => panSNMatchesPrefix(prefix, candidate));
|
|
6256
|
-
}
|
|
6257
|
-
async function referenceSamplesOf(db) {
|
|
6258
|
-
return (await db.tag("gbwt_reference_samples") ?? "").split(/\s+/).filter((sample) => sample !== "");
|
|
6259
|
-
}
|
|
6260
|
-
function aliasedSample(anchorSample, referenceSamples) {
|
|
6261
|
-
const lower = anchorSample.toLowerCase();
|
|
6262
|
-
const alias = wellKnownSample(anchorSample)?.toLowerCase();
|
|
6263
|
-
return referenceSamples.find((s) => s === anchorSample) ?? referenceSamples.find((s) => s.toLowerCase() === lower) ?? referenceSamples.find((s) => s.toLowerCase() === alias);
|
|
6264
|
-
}
|
|
6265
|
-
function resolveReferenceSample({
|
|
6266
|
-
configured,
|
|
6267
|
-
anchorPrefix,
|
|
6268
|
-
referenceSamples
|
|
6269
|
-
}) {
|
|
6270
|
-
if (configured !== "") {
|
|
6271
|
-
return configured;
|
|
6272
|
-
}
|
|
6273
|
-
const sample = aliasedSample(panSNSample(anchorPrefix), referenceSamples) ?? (referenceSamples.length === 1 ? referenceSamples[0] : void 0);
|
|
6274
|
-
if (sample === void 0) {
|
|
6275
|
-
throw new NoReferenceSampleError(anchorPrefix, referenceSamples);
|
|
6276
|
-
}
|
|
6277
|
-
return sample;
|
|
6278
|
-
}
|
|
6279
|
-
async function referencePathQuery(db, referenceSample, refName) {
|
|
6280
|
-
const path = (await db.paths()).find(
|
|
6281
|
-
(p) => p.isIndexed && p.name.sample === referenceSample && p.name.contig === refName
|
|
6282
|
-
);
|
|
6283
|
-
return path ? {
|
|
6284
|
-
sample: path.name.sample,
|
|
6285
|
-
contig: refName,
|
|
6286
|
-
haplotype: path.name.haplotype
|
|
6287
|
-
} : void 0;
|
|
6288
|
-
}
|
|
6289
|
-
function nodeLimitError(error, limit, windowBp) {
|
|
6290
|
-
const isLimit = error instanceof Error && (error.name === "SubgraphLimitError" || /^Subgraph size limit of \d+ nodes exceeded/.test(error.message));
|
|
6291
|
-
if (!isLimit) {
|
|
6292
|
-
return void 0;
|
|
6293
|
-
} else {
|
|
6294
|
-
const walked = error.walkedBp;
|
|
6295
|
-
const fits = typeof walked === "number" && walked > 0 ? Math.floor(walked * 0.8) : Math.floor(windowBp / 2);
|
|
6296
|
-
return new NodeLimitError(limit, windowBp, Math.max(fits, 1));
|
|
6297
|
-
}
|
|
6298
|
-
}
|
|
6299
|
-
async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
|
|
6300
|
-
const subgraph = query ? await db.getSubgraphForRange(query, start, end, {
|
|
6301
|
-
...opts,
|
|
6302
|
-
haplotypes: "all",
|
|
6303
|
-
...keep === void 0 ? {} : { keep }
|
|
6304
|
-
}).catch((error) => {
|
|
6305
|
-
throw nodeLimitError(error, opts.limit, end - start) ?? error;
|
|
6306
|
-
}) : void 0;
|
|
6307
|
-
return subgraph ? subgraph.toGFA({ names: "resolved" }) : "";
|
|
6308
|
-
}
|
|
6309
|
-
|
|
6310
|
-
// src/viewport.ts
|
|
6311
|
-
function axisScaleOf(scale, pixelRows) {
|
|
6312
|
-
return { scaleX: scale, scaleY: pixelRows ? 1 : scale, pixelRows };
|
|
6313
|
-
}
|
|
6314
|
-
function viewportOf(t, axis, width, height) {
|
|
6315
|
-
return {
|
|
6316
|
-
minX: -t.translateX / axis.scaleX,
|
|
6317
|
-
minY: -t.translateY / axis.scaleY,
|
|
6318
|
-
maxX: (width - t.translateX) / axis.scaleX,
|
|
6319
|
-
maxY: (height - t.translateY) / axis.scaleY
|
|
6320
|
-
};
|
|
6321
|
-
}
|
|
6322
|
-
function padded(v, panes) {
|
|
6323
|
-
const w = (v.maxX - v.minX) * panes;
|
|
6324
|
-
const h = (v.maxY - v.minY) * panes;
|
|
6325
|
-
return {
|
|
6326
|
-
minX: v.minX - w,
|
|
6327
|
-
minY: v.minY - h,
|
|
6328
|
-
maxX: v.maxX + w,
|
|
6329
|
-
maxY: v.maxY + h
|
|
6330
|
-
};
|
|
6331
|
-
}
|
|
6332
|
-
function contains(outer, inner) {
|
|
6333
|
-
return inner.minX >= outer.minX && inner.maxX <= outer.maxX && inner.minY >= outer.minY && inner.maxY <= outer.maxY;
|
|
6334
|
-
}
|
|
6335
|
-
function zoomAbout(t, factor, centerX, centerY, pixelRows) {
|
|
6336
|
-
const scale = clampZoom(t.scale * factor);
|
|
6337
|
-
const ratio = scale / t.scale;
|
|
6338
|
-
return {
|
|
6339
|
-
scale,
|
|
6340
|
-
translateX: centerX - (centerX - t.translateX) * ratio,
|
|
6341
|
-
translateY: pixelRows ? t.translateY : centerY - (centerY - t.translateY) * ratio
|
|
6342
|
-
};
|
|
6343
|
-
}
|
|
6344
|
-
function screenToLayout(x, y, t, axis) {
|
|
6345
|
-
return {
|
|
6346
|
-
x: (x - t.translateX) / axis.scaleX,
|
|
6347
|
-
y: (y - t.translateY) / axis.scaleY
|
|
6348
|
-
};
|
|
6349
|
-
}
|
|
6350
|
-
|
|
6351
|
-
// src/loadBandage.ts
|
|
6352
|
-
var cached;
|
|
6353
|
-
function loadBandage() {
|
|
6354
|
-
cached ??= import("./chunks/bandage-layout-3AR6BCUD.js").then((mod) => mod.default()).catch((e) => {
|
|
6355
|
-
cached = void 0;
|
|
6356
|
-
throw e;
|
|
6357
|
-
});
|
|
6358
|
-
return cached;
|
|
6359
|
-
}
|
|
6360
1915
|
export {
|
|
6361
1916
|
BUBBLE_KIND_COLORS,
|
|
6362
1917
|
BUBBLE_KIND_NAMES,
|
|
@@ -6402,17 +1957,21 @@ export {
|
|
|
6402
1957
|
drawTubeMapGenes,
|
|
6403
1958
|
drawTubeMapRuler,
|
|
6404
1959
|
drawingBounds,
|
|
1960
|
+
encodingStops,
|
|
6405
1961
|
encodingSwatchCss,
|
|
6406
1962
|
engineKey,
|
|
6407
1963
|
engineRequest,
|
|
1964
|
+
facetCells,
|
|
6408
1965
|
facetGrid,
|
|
6409
1966
|
facetLifts,
|
|
6410
1967
|
featuresOnBackbone,
|
|
1968
|
+
figureSvg,
|
|
6411
1969
|
findHoveredEdge,
|
|
6412
1970
|
findHoveredNode,
|
|
6413
1971
|
fitTransform,
|
|
6414
1972
|
forceLayout,
|
|
6415
1973
|
formatBp,
|
|
1974
|
+
geneCoverageNote,
|
|
6416
1975
|
genePins,
|
|
6417
1976
|
getDpr,
|
|
6418
1977
|
graphBackbone,
|
|
@@ -6433,6 +1992,7 @@ export {
|
|
|
6433
1992
|
panSNSample,
|
|
6434
1993
|
pathColorsLegible,
|
|
6435
1994
|
pathLegend,
|
|
1995
|
+
rangeText,
|
|
6436
1996
|
refNameBinding,
|
|
6437
1997
|
referenceBoxes,
|
|
6438
1998
|
referenceKnots,
|
|
@@ -6447,6 +2007,7 @@ export {
|
|
|
6447
2007
|
schemeCss,
|
|
6448
2008
|
screenToLayout,
|
|
6449
2009
|
stripBlockAt,
|
|
2010
|
+
svgCanvas,
|
|
6450
2011
|
tubeMapConnectors,
|
|
6451
2012
|
tubeMapFrame,
|
|
6452
2013
|
tubeMapGenes,
|