@jbrowse/bandage-core 0.1.19 → 0.1.20
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 +5 -0
- package/dist/chunks/chunk-3XDR3ZBS.js +4973 -0
- package/dist/cli/figure.js +152 -0
- package/dist/index.js +164 -4614
- 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/referenceStrip.d.ts +8 -1
- 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-3XDR3ZBS.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,7 +465,7 @@ function wheelZoomFactor(e) {
|
|
|
4081
465
|
}
|
|
4082
466
|
|
|
4083
467
|
// src/referenceStrip.ts
|
|
4084
|
-
import { abgrToCssRgba
|
|
468
|
+
import { abgrToCssRgba } from "@jbrowse/core/util/colorBits";
|
|
4085
469
|
var REFERENCE_STRIP_PX = 10;
|
|
4086
470
|
var REFERENCE_STRIP_ZONE_PX = REFERENCE_STRIP_PX + 8;
|
|
4087
471
|
var OVERHANG_PX = 5;
|
|
@@ -4096,13 +480,13 @@ function referenceStripBlocks(graph, {
|
|
|
4096
480
|
graph.nodes.forEach((node, index) => {
|
|
4097
481
|
if (isBackbone(node)) {
|
|
4098
482
|
const own = walkNodes ? walkColors?.get(node.id) ?? LIFT_BACKDROP_COLOR : getNodeColor(node, index, colorScheme, range);
|
|
483
|
+
const faded = !!walkNodes && !walkNodes.has(node.id);
|
|
4099
484
|
out.push({
|
|
4100
485
|
node: node.id,
|
|
4101
486
|
bp0: node.stable.start,
|
|
4102
487
|
bp1: node.stable.start + node.length,
|
|
4103
|
-
color:
|
|
4104
|
-
|
|
4105
|
-
)
|
|
488
|
+
color: abgrToCssRgba(faded ? fadeAbgr(own, FADED_ALPHA) : own),
|
|
489
|
+
faded
|
|
4106
490
|
});
|
|
4107
491
|
}
|
|
4108
492
|
});
|
|
@@ -4143,6 +527,28 @@ function stripBlockAt(blocks, frame, sx, sy) {
|
|
|
4143
527
|
}
|
|
4144
528
|
return hit;
|
|
4145
529
|
}
|
|
530
|
+
function stripPixels(blocks, frame, width, dpr) {
|
|
531
|
+
const placed = blocks.map((b) => {
|
|
532
|
+
const [x0, x1] = span(frame, b.bp0, b.bp1);
|
|
533
|
+
const px0 = Math.round(x0 * dpr);
|
|
534
|
+
return {
|
|
535
|
+
color: b.color,
|
|
536
|
+
x0,
|
|
537
|
+
px0,
|
|
538
|
+
px1: Math.max(Math.round(x1 * dpr), px0 + 1)
|
|
539
|
+
};
|
|
540
|
+
}).filter((b) => b.px1 > 0 && b.px0 < width * dpr).sort((a, b) => a.x0 - b.x0);
|
|
541
|
+
const out = [];
|
|
542
|
+
let claimed = -Infinity;
|
|
543
|
+
for (const { color, px0, px1 } of placed) {
|
|
544
|
+
const start = Math.max(px0, claimed);
|
|
545
|
+
if (px1 > start) {
|
|
546
|
+
out.push({ color, x0: start / dpr, x1: px1 / dpr });
|
|
547
|
+
claimed = px1;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
4146
552
|
function drawOverhang(ctx, tipX, inward) {
|
|
4147
553
|
const mid = REFERENCE_STRIP_PX / 2;
|
|
4148
554
|
const baseX = tipX + inward * (OVERHANG_PX + 1);
|
|
@@ -4154,17 +560,15 @@ function drawOverhang(ctx, tipX, inward) {
|
|
|
4154
560
|
function drawReferenceStrip(ctx, blocks, frame, {
|
|
4155
561
|
width,
|
|
4156
562
|
lit,
|
|
4157
|
-
darkMode
|
|
563
|
+
darkMode,
|
|
564
|
+
dpr = 1
|
|
4158
565
|
}) {
|
|
4159
566
|
const ink = darkMode ? "#ffffff" : "#18181c";
|
|
4160
567
|
ctx.fillStyle = darkMode ? "#1f1f1f" : "#ffffff";
|
|
4161
568
|
ctx.fillRect(0, 0, width, REFERENCE_STRIP_ZONE_PX);
|
|
4162
|
-
for (const b of blocks) {
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
ctx.fillStyle = b.color;
|
|
4166
|
-
ctx.fillRect(x0, 0, Math.max(x1 - x0, 1), REFERENCE_STRIP_PX);
|
|
4167
|
-
}
|
|
569
|
+
for (const b of stripPixels(blocks, frame, width, dpr)) {
|
|
570
|
+
ctx.fillStyle = b.color;
|
|
571
|
+
ctx.fillRect(b.x0, 0, b.x1 - b.x0, REFERENCE_STRIP_PX);
|
|
4168
572
|
}
|
|
4169
573
|
const overhang = stripOverhang(blocks, frame, width);
|
|
4170
574
|
if (overhang.left > 0 || overhang.right > 0) {
|
|
@@ -4282,7 +686,7 @@ function parsePath(d) {
|
|
|
4282
686
|
}
|
|
4283
687
|
return commands;
|
|
4284
688
|
}
|
|
4285
|
-
function
|
|
689
|
+
function extentOf(commands) {
|
|
4286
690
|
let x0 = Infinity;
|
|
4287
691
|
let x1 = -Infinity;
|
|
4288
692
|
for (const c of commands) {
|
|
@@ -4299,7 +703,7 @@ function extentOf2(commands) {
|
|
|
4299
703
|
}
|
|
4300
704
|
function shapeOf(d, color, alpha) {
|
|
4301
705
|
const commands = parsePath(d);
|
|
4302
|
-
return { commands, color, alpha, ...
|
|
706
|
+
return { commands, color, alpha, ...extentOf(commands) };
|
|
4303
707
|
}
|
|
4304
708
|
function layerOf(layout, type) {
|
|
4305
709
|
const { shapes } = layout;
|
|
@@ -4322,7 +726,7 @@ function tubeMapPicture(layout) {
|
|
|
4322
726
|
layout.nodes.forEach((node) => {
|
|
4323
727
|
if (node.order >= 0) {
|
|
4324
728
|
const commands = parsePath(nodeOutlinePath(node));
|
|
4325
|
-
nodes.push({ name: node.name, commands, ...
|
|
729
|
+
nodes.push({ name: node.name, commands, ...extentOf(commands) });
|
|
4326
730
|
}
|
|
4327
731
|
});
|
|
4328
732
|
return {
|
|
@@ -4571,329 +975,6 @@ function tubeMapNodeAt(drawing, { x, y }, sx, sy) {
|
|
|
4571
975
|
return hit;
|
|
4572
976
|
}
|
|
4573
977
|
|
|
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
978
|
// src/tubeMap/axis.ts
|
|
4898
979
|
function referenceBoxes(graph, layout) {
|
|
4899
980
|
const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
@@ -4950,7 +1031,7 @@ function rulerBoxes(byRefName) {
|
|
|
4950
1031
|
return best;
|
|
4951
1032
|
}
|
|
4952
1033
|
var TICK_PX = 5;
|
|
4953
|
-
var
|
|
1034
|
+
var LABEL_GAP_PX = 10;
|
|
4954
1035
|
var TARGET_TICK_PX = 110;
|
|
4955
1036
|
var MIN_BRACKET_PX = 3;
|
|
4956
1037
|
function rulerInk(darkMode) {
|
|
@@ -4965,7 +1046,7 @@ function placeLabels2(candidates) {
|
|
|
4965
1046
|
const placed = [];
|
|
4966
1047
|
for (const a of candidates) {
|
|
4967
1048
|
if (placed.every(
|
|
4968
|
-
(b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 +
|
|
1049
|
+
(b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX
|
|
4969
1050
|
)) {
|
|
4970
1051
|
placed.push(a);
|
|
4971
1052
|
}
|
|
@@ -5248,304 +1329,6 @@ function drawTubeMapConnectors(ctx, connectors, bottom, {
|
|
|
5248
1329
|
}
|
|
5249
1330
|
}
|
|
5250
1331
|
|
|
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
1332
|
// src/bubbles/bubbleLine.ts
|
|
5550
1333
|
var SATURATED_PATH_COUNT = 2147483647;
|
|
5551
1334
|
|
|
@@ -5913,6 +1696,15 @@ var ROUTE_STACK = 8;
|
|
|
5913
1696
|
var GENE_PIN_DROP_PX = 18;
|
|
5914
1697
|
var GENE_STACK = 2;
|
|
5915
1698
|
var TOPMOST_BASELINE = LABEL_PX + LABEL_PAD;
|
|
1699
|
+
function geneCoverageNote(pin) {
|
|
1700
|
+
const length = pin.gene.end - pin.gene.start;
|
|
1701
|
+
if (pin.covered >= 0.98) {
|
|
1702
|
+
return void 0;
|
|
1703
|
+
}
|
|
1704
|
+
const [per, unit] = length >= 1e6 ? [1e6, "Mb"] : length >= 1e3 ? [1e3, "kb"] : [1, "bp"];
|
|
1705
|
+
const amount = (bp) => +(bp / per).toFixed(per === 1 ? 0 : 1);
|
|
1706
|
+
return `${amount(pin.covered * length)} of ${amount(length)} ${unit}`;
|
|
1707
|
+
}
|
|
5916
1708
|
function byExtent(items, extent) {
|
|
5917
1709
|
return [...items].sort((a, b) => extent(b) - extent(a));
|
|
5918
1710
|
}
|
|
@@ -5972,11 +1764,12 @@ function layoutLabels(m) {
|
|
|
5972
1764
|
const genes = placeLabels(
|
|
5973
1765
|
byExtent(m.genePins, (pin) => pin.gene.end - pin.gene.start).map((pin) => {
|
|
5974
1766
|
const { x, y } = screen(pin.at);
|
|
1767
|
+
const note = geneCoverageNote(pin);
|
|
5975
1768
|
return {
|
|
5976
1769
|
item: pin,
|
|
5977
1770
|
x,
|
|
5978
1771
|
y: y + m.contigThickness + GENE_PIN_DROP_PX,
|
|
5979
|
-
text:
|
|
1772
|
+
text: note ? `${pin.gene.name} \xB7 ${note}` : pin.gene.name,
|
|
5980
1773
|
stack: GENE_STACK
|
|
5981
1774
|
};
|
|
5982
1775
|
}),
|
|
@@ -6048,6 +1841,7 @@ function genePins(graph, genes, positions) {
|
|
|
6048
1841
|
continue;
|
|
6049
1842
|
}
|
|
6050
1843
|
const parts = [];
|
|
1844
|
+
const exonsByNode = [];
|
|
6051
1845
|
let at;
|
|
6052
1846
|
let atDistance = Infinity;
|
|
6053
1847
|
let covered = 0;
|
|
@@ -6062,6 +1856,7 @@ function genePins(graph, genes, positions) {
|
|
|
6062
1856
|
}
|
|
6063
1857
|
const line = positions[node.id];
|
|
6064
1858
|
covered += Math.min(nodeEnd, gene.end) - Math.max(nodeStart, gene.start);
|
|
1859
|
+
const nodeParts = [];
|
|
6065
1860
|
for (const exon of gene.exons) {
|
|
6066
1861
|
const a = Math.max(exon.start, nodeStart);
|
|
6067
1862
|
const b = Math.min(exon.end, nodeEnd);
|
|
@@ -6076,7 +1871,11 @@ function genePins(graph, genes, positions) {
|
|
|
6076
1871
|
if (stretch.length === 1) {
|
|
6077
1872
|
stretch.push({ ...stretch[0] });
|
|
6078
1873
|
}
|
|
6079
|
-
|
|
1874
|
+
nodeParts.push(svgPath(stretch));
|
|
1875
|
+
}
|
|
1876
|
+
if (nodeParts.length > 0) {
|
|
1877
|
+
parts.push(...nodeParts);
|
|
1878
|
+
exonsByNode.push({ nodeId: node.id, d: nodeParts.join("") });
|
|
6080
1879
|
}
|
|
6081
1880
|
const pinBp = Math.min(Math.max(mid, nodeStart), nodeEnd);
|
|
6082
1881
|
const distance = Math.abs(pinBp - mid);
|
|
@@ -6094,6 +1893,7 @@ function genePins(graph, genes, positions) {
|
|
|
6094
1893
|
pins.push({
|
|
6095
1894
|
gene,
|
|
6096
1895
|
exons: parts.join(""),
|
|
1896
|
+
exonsByNode,
|
|
6097
1897
|
at,
|
|
6098
1898
|
covered: covered / (gene.end - gene.start)
|
|
6099
1899
|
});
|
|
@@ -6101,262 +1901,6 @@ function genePins(graph, genes, positions) {
|
|
|
6101
1901
|
}
|
|
6102
1902
|
return pins;
|
|
6103
1903
|
}
|
|
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
1904
|
export {
|
|
6361
1905
|
BUBBLE_KIND_COLORS,
|
|
6362
1906
|
BUBBLE_KIND_NAMES,
|
|
@@ -6402,17 +1946,21 @@ export {
|
|
|
6402
1946
|
drawTubeMapGenes,
|
|
6403
1947
|
drawTubeMapRuler,
|
|
6404
1948
|
drawingBounds,
|
|
1949
|
+
encodingStops,
|
|
6405
1950
|
encodingSwatchCss,
|
|
6406
1951
|
engineKey,
|
|
6407
1952
|
engineRequest,
|
|
1953
|
+
facetCells,
|
|
6408
1954
|
facetGrid,
|
|
6409
1955
|
facetLifts,
|
|
6410
1956
|
featuresOnBackbone,
|
|
1957
|
+
figureSvg,
|
|
6411
1958
|
findHoveredEdge,
|
|
6412
1959
|
findHoveredNode,
|
|
6413
1960
|
fitTransform,
|
|
6414
1961
|
forceLayout,
|
|
6415
1962
|
formatBp,
|
|
1963
|
+
geneCoverageNote,
|
|
6416
1964
|
genePins,
|
|
6417
1965
|
getDpr,
|
|
6418
1966
|
graphBackbone,
|
|
@@ -6433,6 +1981,7 @@ export {
|
|
|
6433
1981
|
panSNSample,
|
|
6434
1982
|
pathColorsLegible,
|
|
6435
1983
|
pathLegend,
|
|
1984
|
+
rangeText,
|
|
6436
1985
|
refNameBinding,
|
|
6437
1986
|
referenceBoxes,
|
|
6438
1987
|
referenceKnots,
|
|
@@ -6447,6 +1996,7 @@ export {
|
|
|
6447
1996
|
schemeCss,
|
|
6448
1997
|
screenToLayout,
|
|
6449
1998
|
stripBlockAt,
|
|
1999
|
+
svgCanvas,
|
|
6450
2000
|
tubeMapConnectors,
|
|
6451
2001
|
tubeMapFrame,
|
|
6452
2002
|
tubeMapGenes,
|