@jbrowse/bandage-core 0.1.19 → 4.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
- abgrToCssRgba,
3520
- normalizedRgbToCssRgba
3521
- } from "@jbrowse/core/util/colorBits";
3522
- import { syncCanvasSize } from "@jbrowse/render-core/canvas2dUtils";
3523
- import { Canvas2DRenderingBackendBase } from "@jbrowse/render-core/renderingBackendBase";
3524
- var Canvas2DRenderer = class extends Canvas2DRenderingBackendBase {
3525
- constructor() {
3526
- super(...arguments);
3527
- this.transform = null;
3528
- this.batch = null;
3529
- this.nodeHighlights = /* @__PURE__ */ new Map();
3530
- this.highlightedEdge = null;
3531
- this.highlightFactor = 1;
3532
- }
3533
- // render-core's, not a local `width * devicePixelRatio`: it clamps the
3534
- // backing store at MAX_CANVAS_DIM_PX, reads the ratio through `getDpr()` so
3535
- // this agrees with the transform the model builds, and writes the css size
3536
- // independently of the backing size.
3537
- resize(width, height) {
3538
- syncCanvasSize(this.ctx.canvas, width, height);
3539
- }
3540
- uploadGeometry(batch) {
3541
- this.batch = batch;
3542
- this.highlightedEdge = null;
3543
- }
3544
- setNodeHighlights(factors) {
3545
- this.nodeHighlights = factors;
3546
- }
3547
- setEdgeHighlight(edgeIndex, factor) {
3548
- this.highlightedEdge = edgeIndex;
3549
- this.highlightFactor = factor;
3550
- }
3551
- updateTransform(transform) {
3552
- this.transform = transform;
3553
- }
3554
- render(clearColor) {
3555
- const t = this.transform;
3556
- if (!t) {
3557
- return;
3558
- }
3559
- const ctx = this.ctx;
3560
- const { width, height } = ctx.canvas;
3561
- ctx.fillStyle = normalizedRgbToCssRgba(
3562
- [clearColor[0], clearColor[1], clearColor[2]],
3563
- clearColor[3]
3564
- );
3565
- ctx.fillRect(0, 0, width, height);
3566
- const batch = this.batch;
3567
- if (!batch) {
3568
- return;
3569
- }
3570
- ctx.lineCap = "round";
3571
- ctx.lineJoin = "round";
3572
- this.renderEdges(batch);
3573
- this.renderNodes(batch);
3574
- this.renderArrows(batch);
3575
- }
3576
- renderEdges(batch) {
3577
- const t = this.transform;
3578
- const ctx = this.ctx;
3579
- const hl = this.highlightedEdge === null ? void 0 : batch.edgeCurveRuns.get(this.highlightedEdge);
3580
- for (const { color, items } of groupByPaint(batch.edgeCurves, (e, i) => ({
3581
- color: inRun(hl, i) ? brightenAbgr(e.color, this.highlightFactor) : e.color,
3582
- weight: e.thickness,
3583
- last: inRun(hl, i)
3584
- }))) {
3585
- ctx.strokeStyle = abgrToCssRgba(color);
3586
- ctx.lineWidth = items[0].thickness * 2 * t.dpr;
3587
- ctx.beginPath();
3588
- for (const e of items) {
3589
- const first = e.curves[0];
3590
- ctx.moveTo(
3591
- first.x0 * t.scaleX + t.translateX,
3592
- first.y0 * t.scaleY + t.translateY
3593
- );
3594
- for (const c of e.curves) {
3595
- ctx.bezierCurveTo(
3596
- c.cx0 * t.scaleX + t.translateX,
3597
- c.cy0 * t.scaleY + t.translateY,
3598
- c.cx1 * t.scaleX + t.translateX,
3599
- c.cy1 * t.scaleY + t.translateY,
3600
- c.x1 * t.scaleX + t.translateX,
3601
- c.y1 * t.scaleY + t.translateY
3602
- );
3603
- }
3604
- }
3605
- ctx.stroke();
3606
- }
3607
- }
3608
- renderNodes(batch) {
3609
- const t = this.transform;
3610
- const ctx = this.ctx;
3611
- const lifted = /* @__PURE__ */ new Map();
3612
- for (const [nodeId, factor] of this.nodeHighlights) {
3613
- const run = batch.nodeStrokeRuns.get(nodeId);
3614
- if (run) {
3615
- for (let i = run.start; i < run.start + run.count; i++) {
3616
- lifted.set(i, factor);
3617
- }
3618
- }
3619
- }
3620
- for (const { color, items } of groupByPaint(batch.nodeStrokes, (s, i) => {
3621
- const factor = lifted.get(i);
3622
- return {
3623
- color: factor === void 0 ? s.color : brightenAbgr(s.color, factor),
3624
- weight: s.thickness,
3625
- last: factor !== void 0
3626
- };
3627
- })) {
3628
- ctx.strokeStyle = abgrToCssRgba(color);
3629
- ctx.lineWidth = items[0].thickness * 2 * t.dpr;
3630
- ctx.beginPath();
3631
- for (const s of items) {
3632
- const p0 = s.points[0];
3633
- ctx.moveTo(
3634
- p0.x * t.scaleX + t.translateX,
3635
- p0.y * t.scaleY + t.translateY
3636
- );
3637
- for (let i = 1, l = s.points.length; i < l; i++) {
3638
- const p = s.points[i];
3639
- ctx.lineTo(
3640
- p.x * t.scaleX + t.translateX,
3641
- p.y * t.scaleY + t.translateY
3642
- );
3643
- }
3644
- }
3645
- ctx.stroke();
3646
- }
3647
- }
3648
- renderArrows(batch) {
3649
- const t = this.transform;
3650
- const ctx = this.ctx;
3651
- const hl = this.highlightedEdge === null ? void 0 : batch.arrowRuns.get(this.highlightedEdge);
3652
- for (const { color, items } of groupByPaint(batch.arrows, (a, i) => ({
3653
- color: inRun(hl, i) ? brightenAbgr(a.color, this.highlightFactor) : a.color,
3654
- weight: 0,
3655
- last: inRun(hl, i)
3656
- }))) {
3657
- ctx.fillStyle = abgrToCssRgba(color);
3658
- ctx.beginPath();
3659
- for (const a of items) {
3660
- const [tip, left, notch, right] = arrowheadOutline(a, t);
3661
- ctx.moveTo(tip.x, tip.y);
3662
- ctx.lineTo(left.x, left.y);
3663
- ctx.lineTo(notch.x, notch.y);
3664
- ctx.lineTo(right.x, right.y);
3665
- ctx.closePath();
3666
- }
3667
- ctx.fill();
3668
- }
3669
- }
3670
- dispose() {
3671
- super.dispose();
3672
- this.batch = null;
3673
- this.nodeHighlights = /* @__PURE__ */ new Map();
3674
- this.highlightedEdge = null;
3675
- }
3676
- };
3677
- var ARROW_NOTCH = 0.25;
3678
- function arrowheadOutline(a, t) {
3679
- const ux = Math.cos(a.angle);
3680
- const uy = Math.sin(a.angle);
3681
- const tipX = a.x * t.scaleX + t.translateX;
3682
- const tipY = a.y * t.scaleY + t.translateY;
3683
- const length = a.length * t.dpr;
3684
- const halfWidth = a.halfWidth * t.dpr;
3685
- const backX = tipX - ux * length;
3686
- const backY = tipY - uy * length;
3687
- const notchBack = length * (1 - ARROW_NOTCH);
3688
- return [
3689
- { x: tipX, y: tipY },
3690
- { x: backX - uy * halfWidth, y: backY + ux * halfWidth },
3691
- { x: tipX - ux * notchBack, y: tipY - uy * notchBack },
3692
- { x: backX + uy * halfWidth, y: backY - ux * halfWidth }
3693
- ];
3694
- }
3695
- function inRun(run, i) {
3696
- return run !== void 0 && i >= run.start && i < run.start + run.count;
3697
- }
3698
- function groupByPaint(items, paintOf) {
3699
- const groups = /* @__PURE__ */ new Map();
3700
- const deferred = /* @__PURE__ */ new Map();
3701
- for (let i = 0, l = items.length; i < l; i++) {
3702
- const item = items[i];
3703
- const { color, weight, last } = paintOf(item, i);
3704
- const into = last ? deferred : groups;
3705
- const key = `${color}:${weight}`;
3706
- const group = into.get(key);
3707
- if (group) {
3708
- group.items.push(item);
3709
- } else {
3710
- into.set(key, { color, items: [item] });
3711
- }
3712
- }
3713
- return [...groups.values(), ...deferred.values()];
3714
- }
2
+ BUBBLE_SPREADS,
3
+ COLOR_SCHEMES,
4
+ Canvas2DRenderer,
5
+ FACET_GAP_PX,
6
+ FACET_PAD_PX,
7
+ FACET_TITLE_PX,
8
+ FADED_ALPHA,
9
+ FIT_PADDING,
10
+ FORCE_LAYOUT_LABEL,
11
+ LABEL_CHAR_PX,
12
+ LABEL_PAD,
13
+ LABEL_PX,
14
+ LAYOUT_MODES,
15
+ LIFT_BACKDROP_COLOR,
16
+ NODE_WIDTHS,
17
+ REFERENCE_RAMP_MAX_HUE,
18
+ ROW_HEIGHT_PX,
19
+ WALK_FIELDS,
20
+ WALK_SCHEMES,
21
+ WELL_KNOWN_SAMPLES,
22
+ assemblyWalk,
23
+ axisScaleOf,
24
+ backboneAssembly,
25
+ baseEdgeCurves,
26
+ buildGeometry,
27
+ clampZoom,
28
+ computeColorSchemeRange,
29
+ computeReferenceRamp,
30
+ contains,
31
+ curveBounds,
32
+ cutWindowGFA,
33
+ deletionEdges,
34
+ drawingBounds,
35
+ encodingStops,
36
+ encodingSwatchCss,
37
+ engineKey,
38
+ engineRequest,
39
+ facetCells,
40
+ facetGrid,
41
+ facetLifts,
42
+ fadeAbgr,
43
+ featuresOnBackbone,
44
+ figureSvg,
45
+ firstNodeAtOrAfter,
46
+ fitTransform,
47
+ forceLayout,
48
+ formatBp,
49
+ getNodeColor,
50
+ graphBackbone,
51
+ haplotypeWanted,
52
+ isBackbone,
53
+ layoutExtent,
54
+ layoutModeByValue,
55
+ loadBandage,
56
+ loadGraph,
57
+ maxNodeWidthPx,
58
+ meanDepth,
59
+ modeUsesLayoutEngine,
60
+ nodeInk,
61
+ nodeLabelBudget,
62
+ nodeWidthPx,
63
+ occupancy,
64
+ padded,
65
+ panSNContig,
66
+ panSNHaplotype,
67
+ panSNSample,
68
+ pathColorsLegible,
69
+ pathLegend,
70
+ pathRibbonOffsets,
71
+ placeLabels,
72
+ placeSizeLabels,
73
+ polylineSlice,
74
+ rangeText,
75
+ refNameBinding,
76
+ referenceOrder,
77
+ referencePathQuery,
78
+ referenceSamplesOf,
79
+ resolveColorScheme,
80
+ resolveEncoding,
81
+ resolveReferenceSample,
82
+ rowLabelBox,
83
+ schemeCss,
84
+ screenToLayout,
85
+ sizeLabelCandidates,
86
+ svgCanvas,
87
+ svgPath,
88
+ translateCurves,
89
+ viewportOf,
90
+ walkHighlight,
91
+ walkKey,
92
+ walkLift,
93
+ walkRows,
94
+ walkRowsExtent,
95
+ wellKnownSample,
96
+ yToXOf,
97
+ zoomAbout
98
+ } from "./chunks/chunk-4AOHEU76.js";
3715
99
 
3716
100
  // src/index.ts
3717
101
  import { getDpr } from "@jbrowse/render-core/canvas2dUtils";
@@ -3949,9 +333,9 @@ function distanceToEdgeCurves(px, py, curves, yToX = 1) {
3949
333
  }
3950
334
  var nodeCache = /* @__PURE__ */ new WeakMap();
3951
335
  function getSpatialIndex(nodePositions, version) {
3952
- const cached2 = nodeCache.get(nodePositions);
3953
- if (cached2?.version === version) {
3954
- return cached2.index;
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 cached2 = edgeCache.get(nodePositions);
3963
- if (cached2?.graph === graph && cached2.drawPaths === drawPaths && cached2.scaleX === axis.scaleX && cached2.scaleY === axis.scaleY && cached2.version === version && cached2.deletions === deletions) {
3964
- return cached2.index;
346
+ const cached = edgeCache.get(nodePositions);
347
+ if (cached?.graph === graph && cached.drawPaths === drawPaths && cached.scaleX === axis.scaleX && cached.scaleY === axis.scaleY && cached.version === version && cached.deletions === deletions) {
348
+ return cached.index;
3965
349
  }
3966
350
  const index = new EdgeSpatialIndex(
3967
351
  nodePositions,
@@ -4081,28 +465,31 @@ function wheelZoomFactor(e) {
4081
465
  }
4082
466
 
4083
467
  // src/referenceStrip.ts
4084
- import { abgrToCssRgba as abgrToCssRgba2 } from "@jbrowse/core/util/colorBits";
4085
- var REFERENCE_STRIP_PX = 10;
468
+ import { abgrToCssRgba } from "@jbrowse/core/util/colorBits";
469
+ var REFERENCE_STRIP_PX = 12;
4086
470
  var REFERENCE_STRIP_ZONE_PX = REFERENCE_STRIP_PX + 8;
4087
- var OVERHANG_PX = 5;
471
+ var OVERHANG_PX = 8;
472
+ var OVERHANG_CAP_PX = OVERHANG_PX + 3;
473
+ var PALE = abgrToCssRgba(fadeAbgr(LIFT_BACKDROP_COLOR, FADED_ALPHA));
4088
474
  function referenceStripBlocks(graph, {
4089
475
  colorScheme,
4090
476
  referenceRamp,
4091
- walkNodes,
4092
- walkColors
477
+ walks
4093
478
  }) {
4094
479
  const range = { ...computeColorSchemeRange(graph), referenceRamp };
4095
480
  const out = [];
4096
481
  graph.nodes.forEach((node, index) => {
4097
482
  if (isBackbone(node)) {
4098
- const own = walkNodes ? walkColors?.get(node.id) ?? LIFT_BACKDROP_COLOR : getNodeColor(node, index, colorScheme, range);
483
+ const colors = walks ? walks.map((w) => {
484
+ const lane = w.colors.get(node.id);
485
+ return lane === void 0 ? PALE : abgrToCssRgba(lane);
486
+ }) : [abgrToCssRgba(getNodeColor(node, index, colorScheme, range))];
4099
487
  out.push({
4100
488
  node: node.id,
4101
489
  bp0: node.stable.start,
4102
490
  bp1: node.stable.start + node.length,
4103
- color: abgrToCssRgba2(
4104
- walkNodes && !walkNodes.has(node.id) ? fadeAbgr(own, FADED_ALPHA) : own
4105
- )
491
+ colors,
492
+ faded: colors.includes(PALE)
4106
493
  });
4107
494
  }
4108
495
  });
@@ -4143,43 +530,70 @@ function stripBlockAt(blocks, frame, sx, sy) {
4143
530
  }
4144
531
  return hit;
4145
532
  }
4146
- function drawOverhang(ctx, tipX, inward) {
4147
- const mid = REFERENCE_STRIP_PX / 2;
4148
- const baseX = tipX + inward * (OVERHANG_PX + 1);
4149
- ctx.moveTo(tipX + inward, mid);
4150
- ctx.lineTo(baseX, mid - OVERHANG_PX / 1.4);
4151
- ctx.lineTo(baseX, mid + OVERHANG_PX / 1.4);
533
+ function stripPixels(blocks, frame, width, dpr) {
534
+ const placed = blocks.map((b) => {
535
+ const [x0, x1] = span(frame, b.bp0, b.bp1);
536
+ const px0 = Math.round(x0 * dpr);
537
+ return {
538
+ colors: b.colors,
539
+ x0,
540
+ px0,
541
+ px1: Math.max(Math.round(x1 * dpr), px0 + 1)
542
+ };
543
+ }).filter((b) => b.px1 > 0 && b.px0 < width * dpr).sort((a, b) => a.x0 - b.x0);
544
+ const out = [];
545
+ let claimed = -Infinity;
546
+ const tierEnds = [-Infinity, -Infinity];
547
+ for (const { colors, px0, px1 } of placed) {
548
+ const start = Math.max(px0, claimed);
549
+ if (px1 > start) {
550
+ const tier = tierEnds[0] < start ? 0 : 1;
551
+ tierEnds[tier] = px1;
552
+ out.push({ colors, x0: start / dpr, x1: px1 / dpr, tier });
553
+ claimed = px1;
554
+ }
555
+ }
556
+ return out;
557
+ }
558
+ function drawOverhang(ctx, edgeX, inward, ink) {
559
+ const capX = inward > 0 ? edgeX : edgeX - OVERHANG_CAP_PX;
560
+ ctx.clearRect(capX, 0, OVERHANG_CAP_PX, REFERENCE_STRIP_PX);
561
+ const tipX = edgeX + inward;
562
+ const baseX = tipX + inward * OVERHANG_PX;
563
+ ctx.beginPath();
564
+ ctx.moveTo(tipX, REFERENCE_STRIP_PX / 2);
565
+ ctx.lineTo(baseX, 0);
566
+ ctx.lineTo(baseX, REFERENCE_STRIP_PX);
4152
567
  ctx.closePath();
568
+ ctx.fillStyle = ink;
569
+ ctx.fill();
570
+ }
571
+ function rowEdge(row, rows, dpr) {
572
+ return Math.round(row * REFERENCE_STRIP_PX * dpr / rows) / dpr;
4153
573
  }
4154
574
  function drawReferenceStrip(ctx, blocks, frame, {
4155
575
  width,
4156
576
  lit,
4157
- darkMode
577
+ darkMode,
578
+ dpr = 1
4158
579
  }) {
4159
580
  const ink = darkMode ? "#ffffff" : "#18181c";
4160
- ctx.fillStyle = darkMode ? "#1f1f1f" : "#ffffff";
4161
- ctx.fillRect(0, 0, width, REFERENCE_STRIP_ZONE_PX);
4162
- for (const b of blocks) {
4163
- const [x0, x1] = span(frame, b.bp0, b.bp1);
4164
- if (x1 >= 0 && x0 <= width) {
4165
- ctx.fillStyle = b.color;
4166
- ctx.fillRect(x0, 0, Math.max(x1 - x0, 1), REFERENCE_STRIP_PX);
4167
- }
581
+ ctx.clearRect(0, 0, width, REFERENCE_STRIP_ZONE_PX);
582
+ for (const b of stripPixels(blocks, frame, width, dpr)) {
583
+ const rows = b.colors.length > 1 ? b.colors.length : 2;
584
+ b.colors.forEach((color, i) => {
585
+ const row = b.colors.length > 1 ? i : b.tier;
586
+ const y0 = rowEdge(row, rows, dpr);
587
+ ctx.fillStyle = color;
588
+ ctx.fillRect(b.x0, y0, b.x1 - b.x0, rowEdge(row + 1, rows, dpr) - y0);
589
+ });
4168
590
  }
4169
591
  const overhang = stripOverhang(blocks, frame, width);
4170
- if (overhang.left > 0 || overhang.right > 0) {
4171
- ctx.beginPath();
4172
- if (overhang.left > 0) {
4173
- drawOverhang(ctx, 0, 1);
4174
- }
4175
- if (overhang.right > 0) {
4176
- drawOverhang(ctx, width, -1);
4177
- }
4178
- ctx.fillStyle = ink;
4179
- ctx.fill();
4180
- ctx.strokeStyle = darkMode ? "#1f1f1f" : "#ffffff";
4181
- ctx.lineWidth = 1;
4182
- ctx.stroke();
592
+ if (overhang.left > 0) {
593
+ drawOverhang(ctx, 0, 1, ink);
594
+ }
595
+ if (overhang.right > 0) {
596
+ drawOverhang(ctx, width, -1, ink);
4183
597
  }
4184
598
  if (lit) {
4185
599
  const [x0, x1] = span(frame, lit.start, lit.end);
@@ -4282,7 +696,7 @@ function parsePath(d) {
4282
696
  }
4283
697
  return commands;
4284
698
  }
4285
- function extentOf2(commands) {
699
+ function extentOf(commands) {
4286
700
  let x0 = Infinity;
4287
701
  let x1 = -Infinity;
4288
702
  for (const c of commands) {
@@ -4299,7 +713,7 @@ function extentOf2(commands) {
4299
713
  }
4300
714
  function shapeOf(d, color, alpha) {
4301
715
  const commands = parsePath(d);
4302
- return { commands, color, alpha, ...extentOf2(commands) };
716
+ return { commands, color, alpha, ...extentOf(commands) };
4303
717
  }
4304
718
  function layerOf(layout, type) {
4305
719
  const { shapes } = layout;
@@ -4322,7 +736,7 @@ function tubeMapPicture(layout) {
4322
736
  layout.nodes.forEach((node) => {
4323
737
  if (node.order >= 0) {
4324
738
  const commands = parsePath(nodeOutlinePath(node));
4325
- nodes.push({ name: node.name, commands, ...extentOf2(commands) });
739
+ nodes.push({ name: node.name, commands, ...extentOf(commands) });
4326
740
  }
4327
741
  });
4328
742
  return {
@@ -4571,329 +985,6 @@ function tubeMapNodeAt(drawing, { x, y }, sx, sy) {
4571
985
  return hit;
4572
986
  }
4573
987
 
4574
- // src/overlayLabels.ts
4575
- var LABEL_PX = 11;
4576
- var LABEL_CHAR_PX = 6.2;
4577
- var LABEL_PAD = 4;
4578
- function labelWidth(text) {
4579
- return text.length * LABEL_CHAR_PX + LABEL_PAD * 2;
4580
- }
4581
- function overlaps(a, b) {
4582
- return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
4583
- }
4584
- function offFrame(box, frame) {
4585
- return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
4586
- }
4587
- function insideFrame(box, frame) {
4588
- return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
4589
- }
4590
- function occupancy(frame, reserved = []) {
4591
- const placed = [...reserved];
4592
- return (box) => {
4593
- if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
4594
- return false;
4595
- }
4596
- placed.push(box);
4597
- return true;
4598
- };
4599
- }
4600
- function placeLabels(candidates, frame, take) {
4601
- const out = [];
4602
- const row = LABEL_PX + LABEL_PAD * 2;
4603
- for (const c of candidates) {
4604
- const w = labelWidth(c.text);
4605
- const direction = c.y > frame.height / 2 ? -1 : 1;
4606
- for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
4607
- const y = c.y + direction * tries * row;
4608
- const box = {
4609
- x0: c.x - w / 2,
4610
- x1: c.x + w / 2,
4611
- y0: y - LABEL_PX - LABEL_PAD,
4612
- y1: y + LABEL_PAD
4613
- };
4614
- if (offFrame(box, frame)) {
4615
- break;
4616
- }
4617
- if (insideFrame(box, frame) && take(box)) {
4618
- out.push({ ...c, y, w });
4619
- break;
4620
- }
4621
- }
4622
- }
4623
- return out;
4624
- }
4625
-
4626
- // src/graphLabels.ts
4627
- var MIN_DELETION_LABEL_PX = 26;
4628
- var PX_PER_NODE_LABEL = 4e4;
4629
- var MIN_NODE_LABELS = 8;
4630
- var LEADER_GAP_PX = 8;
4631
- var LABEL_CHAR_PX2 = 5.7;
4632
- var LABEL_PAD_PX = 6;
4633
- var LABEL_HEIGHT_PX = 13;
4634
- var LABEL_GAP_PX = 3;
4635
- var MAX_LABEL_OVERHANG = 2;
4636
- function labelHalfWidth(text) {
4637
- return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
4638
- }
4639
- var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
4640
- var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
4641
- function boxAt(halfW, x, y) {
4642
- return {
4643
- x0: x - halfW,
4644
- x1: x + halfW,
4645
- y0: y - LABEL_HALF_HEIGHT,
4646
- y1: y + LABEL_HALF_HEIGHT
4647
- };
4648
- }
4649
- function rowLabelBox(text, screenY) {
4650
- return {
4651
- x0: 6,
4652
- x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
4653
- y0: screenY - 8 - LABEL_GAP_PX / 2,
4654
- y1: screenY + 8 + LABEL_GAP_PX / 2
4655
- };
4656
- }
4657
- function clamp(v, lo, hi) {
4658
- return hi < lo ? v : Math.min(Math.max(v, lo), hi);
4659
- }
4660
- function formatBp(bp) {
4661
- if (bp >= 1e6) {
4662
- return `${+(bp / 1e6).toFixed(1)} Mb`;
4663
- }
4664
- return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
4665
- }
4666
- function deletionText(bp) {
4667
- return `${formatBp(bp)} del`;
4668
- }
4669
- function arcOutward(curves, apex, scaleX, scaleY) {
4670
- const first = curves[0];
4671
- const last = curves[curves.length - 1];
4672
- const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
4673
- const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
4674
- const len = Math.hypot(dx, dy);
4675
- return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
4676
- }
4677
- function boxSupport(box, dir) {
4678
- return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
4679
- }
4680
- function boxRayCrossing(box, dir) {
4681
- const halfW = (box.x1 - box.x0) / 2;
4682
- const halfH = (box.y1 - box.y0) / 2;
4683
- return Math.min(
4684
- dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
4685
- dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
4686
- );
4687
- }
4688
- function drawnLength(segments, scaleX, scaleY) {
4689
- let total = 0;
4690
- for (let i = 1; i < segments.length; i++) {
4691
- const a = segments[i - 1];
4692
- const b = segments[i];
4693
- total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
4694
- }
4695
- return total;
4696
- }
4697
- function runBounds(nodeIds, nodePositions) {
4698
- let minX = Infinity;
4699
- let minY = Infinity;
4700
- let maxX = -Infinity;
4701
- let maxY = -Infinity;
4702
- for (const id of nodeIds) {
4703
- for (const p of nodePositions[id] ?? []) {
4704
- minX = Math.min(minX, p.x);
4705
- maxX = Math.max(maxX, p.x);
4706
- minY = Math.min(minY, p.y);
4707
- maxY = Math.max(maxY, p.y);
4708
- }
4709
- }
4710
- return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
4711
- }
4712
- var nodeOrderCache = /* @__PURE__ */ new WeakMap();
4713
- function labelOrder(nodePositions, nodeLengths) {
4714
- const cached2 = nodeOrderCache.get(nodePositions);
4715
- if (cached2?.nodeLengths === nodeLengths) {
4716
- return cached2.order;
4717
- }
4718
- const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
4719
- nodeOrderCache.set(nodePositions, { nodeLengths, order });
4720
- return order;
4721
- }
4722
- var arcCache = /* @__PURE__ */ new WeakMap();
4723
- function arcPlacements(nodePositions, deletions, axis, version) {
4724
- const { scaleX, scaleY } = axis;
4725
- const cached2 = arcCache.get(nodePositions);
4726
- if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
4727
- return cached2.arcs;
4728
- }
4729
- const arcs = [];
4730
- for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
4731
- const curves = deletionArcCurves(nodePositions, deletion, axis);
4732
- const apex = curves ? curveMidpoint(curves) : void 0;
4733
- if (curves && apex) {
4734
- const { minX, minY, maxX, maxY } = curveBounds(curves);
4735
- arcs.push({
4736
- deletion,
4737
- curves,
4738
- apex,
4739
- // in screen px, so the two axes' units cannot be added by accident
4740
- extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
4741
- });
4742
- }
4743
- }
4744
- arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
4745
- return arcs;
4746
- }
4747
- function sizeLabelCandidates({
4748
- nodePositions,
4749
- nodeLengths,
4750
- deletions,
4751
- alleleDeletions,
4752
- axis,
4753
- translateX,
4754
- translateY,
4755
- width,
4756
- height,
4757
- version = 0
4758
- }) {
4759
- const { scaleX, scaleY } = axis;
4760
- const deletionCandidates = [];
4761
- const nodeCandidates = [];
4762
- for (const { deletion, curves, apex, extent } of arcPlacements(
4763
- nodePositions,
4764
- deletions,
4765
- axis,
4766
- version
4767
- )) {
4768
- if (extent >= MIN_DELETION_LABEL_PX) {
4769
- const text = deletionText(deletion.bp);
4770
- const halfW = labelHalfWidth(text);
4771
- const box = boxAt(halfW, 0, 0);
4772
- const arcX = apex.x * scaleX + translateX;
4773
- const arcY = apex.y * scaleY + translateY;
4774
- const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
4775
- const bow = arcOutward(curves, apex, scaleX, scaleY);
4776
- const offset = boxSupport(box, bow) + LEADER_GAP_PX;
4777
- const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
4778
- const y = fits ? arcY : clamp(
4779
- arcY + bow.y * offset,
4780
- LABEL_HALF_HEIGHT,
4781
- height - LABEL_HALF_HEIGHT
4782
- );
4783
- const away = Math.hypot(x - arcX, y - arcY);
4784
- let leader;
4785
- if (away > 0) {
4786
- const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
4787
- const crossing = boxRayCrossing(box, dir);
4788
- leader = {
4789
- arcX,
4790
- arcY,
4791
- labelX: x - dir.x * crossing,
4792
- labelY: y - dir.y * crossing
4793
- };
4794
- }
4795
- deletionCandidates.push({
4796
- label: {
4797
- key: `del:${deletion.edgeIndex}`,
4798
- text,
4799
- x,
4800
- y,
4801
- kind: "deletion",
4802
- leader
4803
- },
4804
- box: boxAt(halfW, x, y)
4805
- });
4806
- }
4807
- }
4808
- const labelledByDeletion = /* @__PURE__ */ new Set();
4809
- const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
4810
- for (const run of alleleRuns) {
4811
- const drawn = runBounds(run.nodeIds, nodePositions);
4812
- if (!drawn) {
4813
- continue;
4814
- }
4815
- const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
4816
- const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
4817
- const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
4818
- const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
4819
- if (shownRight < shownLeft || shownBottom < shownTop) {
4820
- continue;
4821
- }
4822
- const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
4823
- const text = deletionText(run.bp);
4824
- const halfW = labelHalfWidth(text);
4825
- if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
4826
- for (const id of run.nodeIds) {
4827
- labelledByDeletion.add(id);
4828
- }
4829
- const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
4830
- const y = clamp(
4831
- (shownTop + shownBottom) / 2,
4832
- LABEL_HALF_HEIGHT,
4833
- height - LABEL_HALF_HEIGHT
4834
- );
4835
- deletionCandidates.push({
4836
- label: {
4837
- key: `alleledel:${run.nodeIds.join(",")}`,
4838
- text,
4839
- x,
4840
- y,
4841
- kind: "deletion"
4842
- },
4843
- box: boxAt(halfW, x, y)
4844
- });
4845
- }
4846
- }
4847
- for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
4848
- if (labelledByDeletion.has(id)) {
4849
- continue;
4850
- }
4851
- const { x, y } = polylineMidpoint(segments);
4852
- const screenY = y * scaleY + translateY;
4853
- if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
4854
- continue;
4855
- }
4856
- const drawn = drawnLength(segments, scaleX, scaleY);
4857
- if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
4858
- continue;
4859
- }
4860
- const text = formatBp(nodeLengths.get(id));
4861
- const halfW = labelHalfWidth(text);
4862
- if (drawn >= halfW * 2 / MAX_LABEL_OVERHANG) {
4863
- const screenX = x * scaleX + translateX;
4864
- nodeCandidates.push({
4865
- label: {
4866
- key: `node:${id}`,
4867
- text,
4868
- x: screenX,
4869
- y: screenY,
4870
- kind: "node"
4871
- },
4872
- box: boxAt(halfW, screenX, screenY)
4873
- });
4874
- }
4875
- }
4876
- return { deletions: deletionCandidates, nodes: nodeCandidates };
4877
- }
4878
- function nodeLabelBudget(width, height) {
4879
- return Math.max(
4880
- MIN_NODE_LABELS,
4881
- Math.floor(width * height / PX_PER_NODE_LABEL)
4882
- );
4883
- }
4884
- function placeSizeLabels(candidates, take, budget = Infinity) {
4885
- const labels = [];
4886
- for (const { label, box } of candidates) {
4887
- if (labels.length >= budget) {
4888
- break;
4889
- }
4890
- if (take(box)) {
4891
- labels.push(label);
4892
- }
4893
- }
4894
- return labels;
4895
- }
4896
-
4897
988
  // src/tubeMap/axis.ts
4898
989
  function referenceBoxes(graph, layout) {
4899
990
  const nodeById = new Map(graph.nodes.map((n) => [n.id, n]));
@@ -4950,7 +1041,7 @@ function rulerBoxes(byRefName) {
4950
1041
  return best;
4951
1042
  }
4952
1043
  var TICK_PX = 5;
4953
- var LABEL_GAP_PX2 = 10;
1044
+ var LABEL_GAP_PX = 10;
4954
1045
  var TARGET_TICK_PX = 110;
4955
1046
  var MIN_BRACKET_PX = 3;
4956
1047
  function rulerInk(darkMode) {
@@ -4965,7 +1056,7 @@ function placeLabels2(candidates) {
4965
1056
  const placed = [];
4966
1057
  for (const a of candidates) {
4967
1058
  if (placed.every(
4968
- (b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX2
1059
+ (b) => Math.abs(a.at - b.at) >= (a.width + b.width) / 2 + LABEL_GAP_PX
4969
1060
  )) {
4970
1061
  placed.push(a);
4971
1062
  }
@@ -5248,304 +1339,6 @@ function drawTubeMapConnectors(ctx, connectors, bottom, {
5248
1339
  }
5249
1340
  }
5250
1341
 
5251
- // src/walkEncoding.ts
5252
- import { packAbgr as packAbgr3 } from "@jbrowse/core/util/colorBits";
5253
- var WALK_FIELDS = [
5254
- { value: "progress", label: "Progress along the walk" },
5255
- { value: "walk", label: "One colour for the walk" },
5256
- { value: "reference", label: "Reference position" }
5257
- ];
5258
- var WALK_SCHEMES = [
5259
- { value: "grey", label: "Grey to black", ends: ["#a3a3a3", "#151515"] },
5260
- { value: "blue", label: "Cyan to navy", ends: ["#6ee0f0", "#0b2c7a"] },
5261
- { value: "red", label: "Yellow to red", ends: ["#ffd24a", "#b3121b"] },
5262
- { value: "purple", label: "Pink to plum", ends: ["#f5a3d8", "#5e1a6e"] },
5263
- { value: "green", label: "Lime to forest", ends: ["#b6e36c", "#0b5d3b"] },
5264
- { value: "rainbow", label: "Rainbow" }
5265
- ];
5266
- var FAMILIES = ["blue", "red", "purple", "green"];
5267
- var isField = (v) => WALK_FIELDS.some((f) => f.value === v);
5268
- var isScheme = (v) => WALK_SCHEMES.some((s) => s.value === v);
5269
- function resolveEncoding(layer, reference, picked, alone = true) {
5270
- const { field: askedField, scheme: askedScheme } = layer.color ?? {};
5271
- const field = isField(askedField) ? askedField : alone ? "progress" : "walk";
5272
- const family = reference ? "grey" : FAMILIES[picked % FAMILIES.length];
5273
- const scheme = isScheme(askedScheme) ? askedScheme === "rainbow" && field !== "reference" ? family : askedScheme : family;
5274
- return { field, scheme };
5275
- }
5276
- var BASE_T = 0.5;
5277
- var toLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
5278
- var toGamma = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
5279
- function oklab(hex) {
5280
- const [r, g, b] = [1, 3, 5].map(
5281
- (i) => toLinear(Number.parseInt(hex.slice(i, i + 2), 16) / 255)
5282
- );
5283
- const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
5284
- const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
5285
- const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
5286
- return [
5287
- 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
5288
- 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
5289
- 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
5290
- ];
5291
- }
5292
- function oklabToRgb([L, a, b]) {
5293
- const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
5294
- const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
5295
- const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
5296
- return [
5297
- 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
5298
- -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
5299
- -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
5300
- ].map((c) => toGamma(Math.max(0, Math.min(1, c))) * 255);
5301
- }
5302
- function schemeRgb(scheme, t) {
5303
- const x = Math.max(0, Math.min(1, t));
5304
- const family = WALK_SCHEMES.find((s) => s.value === scheme);
5305
- if (!family || !("ends" in family)) {
5306
- return hslToRgb(x * REFERENCE_RAMP_MAX_HUE, 0.7, 0.5).map((c) => c * 255);
5307
- }
5308
- const from = oklab(family.ends[0]);
5309
- const to = oklab(family.ends[1]);
5310
- return oklabToRgb(from.map((v, i) => v + (to[i] - v) * x));
5311
- }
5312
- function schemeCss(scheme, t) {
5313
- const [r, g, b] = schemeRgb(scheme, t).map(Math.round);
5314
- return `rgb(${r}, ${g}, ${b})`;
5315
- }
5316
- function schemeColor(scheme, t) {
5317
- const [r, g, b] = schemeRgb(scheme, t).map(Math.round);
5318
- return packAbgr3(r, g, b, 255);
5319
- }
5320
- function encodedColor({ field, scheme }, t) {
5321
- return field === "walk" ? schemeColor(scheme, BASE_T) : t === void 0 ? NO_VALUE_COLOR : schemeColor(scheme, t);
5322
- }
5323
- var NO_VALUE_COLOR = REFERENCE_RAMP_ALT_COLOR;
5324
- function encodingSwatchCss({ field, scheme }) {
5325
- if (field === "walk") {
5326
- return schemeCss(scheme, BASE_T);
5327
- }
5328
- const stops = Array.from({ length: 7 }, (_, i) => {
5329
- const t = i / 6;
5330
- return `${schemeCss(scheme, t)} ${t * 100}%`;
5331
- });
5332
- return `linear-gradient(to right, ${stops.join(", ")})`;
5333
- }
5334
-
5335
- // src/walkHighlight.ts
5336
- function referenceOf(graph) {
5337
- return graph.referencePath ? graph.paths?.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
5338
- }
5339
- function walkHighlight(graph, name) {
5340
- const path = graph.paths?.find((p) => p.name === name);
5341
- if (!path) {
5342
- return void 0;
5343
- }
5344
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
5345
- const edgeAt = /* @__PURE__ */ new Map();
5346
- graph.edges.forEach((e, i) => {
5347
- edgeAt.set(`${e.from}>${e.to}`, i);
5348
- });
5349
- const edgeIndexes = /* @__PURE__ */ new Set();
5350
- for (let i = 1; i < path.nodeIds.length; i++) {
5351
- const a = path.nodeIds[i - 1];
5352
- const b = path.nodeIds[i];
5353
- const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
5354
- if (index !== void 0) {
5355
- edgeIndexes.add(index);
5356
- }
5357
- }
5358
- const lengthOf = (id) => byId.get(id)?.length ?? 0;
5359
- const bp = path.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0);
5360
- const progress = /* @__PURE__ */ new Map();
5361
- let before = 0;
5362
- for (const id of path.nodeIds) {
5363
- const length = lengthOf(id);
5364
- if (!progress.has(id)) {
5365
- progress.set(id, bp > 0 ? (before + length / 2) / bp : 0);
5366
- }
5367
- before += length;
5368
- }
5369
- const reference = referenceOf(graph);
5370
- const originName = pathOrigin(name).name;
5371
- const origin = graph.anchorPaths?.find((a) => a.name === originName && a.length === bp) ?? graph.anchorPaths?.find((a) => a.name === originName);
5372
- return {
5373
- name,
5374
- reference: reference === path,
5375
- range: origin ? {
5376
- contig: path.contig ?? panSNContig(origin.name),
5377
- start: origin.start,
5378
- end: origin.start + origin.length
5379
- } : void 0,
5380
- nodeIds: new Set(path.nodeIds),
5381
- edgeIndexes,
5382
- progress,
5383
- steps: path.nodeIds.length,
5384
- bp,
5385
- referenceBp: reference && reference !== path ? reference.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0) : void 0
5386
- };
5387
- }
5388
- function laneColors(graph, walk, encoding, ramp) {
5389
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
5390
- const colors = /* @__PURE__ */ new Map();
5391
- for (const [id, progress] of walk.progress) {
5392
- const node = byId.get(id);
5393
- const mid = ramp?.midpoints.get(id);
5394
- const t = encoding.field === "progress" ? progress : ramp && mid !== void 0 && !(node?.stable && node.stable.rank > 0) ? (mid - ramp.start) / ramp.span : void 0;
5395
- colors.set(id, encodedColor(encoding, t));
5396
- }
5397
- return colors;
5398
- }
5399
- function reversedNodes(graph, walk) {
5400
- const reference = referenceOf(graph);
5401
- const reversed = /* @__PURE__ */ new Set();
5402
- let bp = 0;
5403
- if (!reference || walk.reference) {
5404
- return { reversed, bp };
5405
- }
5406
- const walkPath = pathOrigin(walk.name).name;
5407
- const referencePath = pathOrigin(reference.name).name;
5408
- const byId = new Map(graph.nodes.map((n) => [n.id, n]));
5409
- for (const id of walk.progress.keys()) {
5410
- const node = byId.get(id);
5411
- const visits = node ? graph.pathVisits?.get(node.name) : void 0;
5412
- const own = visits?.find((v) => v.path === walkPath)?.strand;
5413
- const theirs = visits?.find((v) => v.path === referencePath)?.strand;
5414
- if (own && theirs && own !== theirs) {
5415
- reversed.add(id);
5416
- bp += node.length;
5417
- }
5418
- }
5419
- return { reversed, bp };
5420
- }
5421
- function walkLift(graph, layers, ramp) {
5422
- const seen = /* @__PURE__ */ new Set();
5423
- const lifted = layers.filter((layer) => !seen.has(layer.walk) && seen.add(layer.walk)).flatMap((layer) => {
5424
- const walk = walkHighlight(graph, layer.walk);
5425
- return walk ? [{ walk, layer }] : [];
5426
- }).sort((a, b) => Number(b.walk.reference) - Number(a.walk.reference));
5427
- if (lifted.length === 0) {
5428
- return void 0;
5429
- }
5430
- let picked = 0;
5431
- const walks = lifted.map(({ walk, layer }) => {
5432
- const encoding = resolveEncoding(
5433
- layer,
5434
- walk.reference,
5435
- walk.reference ? 0 : picked++,
5436
- lifted.length === 1
5437
- );
5438
- const { reversed, bp } = reversedNodes(graph, walk);
5439
- return {
5440
- ...walk,
5441
- encoding,
5442
- colors: laneColors(graph, walk, encoding, ramp),
5443
- reversed,
5444
- reversedBp: bp
5445
- };
5446
- });
5447
- return liftOf(walks, ramp);
5448
- }
5449
- var FACET_COLOR = { field: "progress", scheme: "red" };
5450
- function facetLifts(graph, lift, layers, ramp) {
5451
- return lift.walks.map((walk) => {
5452
- const stated = layers.find((l) => l.walk === walk.name)?.color;
5453
- const encoding = resolveEncoding(
5454
- { walk: walk.name, color: { ...FACET_COLOR, ...stated } },
5455
- walk.reference,
5456
- 0
5457
- );
5458
- return liftOf(
5459
- [
5460
- {
5461
- ...walk,
5462
- encoding,
5463
- colors: laneColors(graph, walk, encoding, ramp)
5464
- }
5465
- ],
5466
- ramp
5467
- );
5468
- });
5469
- }
5470
- function liftOf(walks, ramp) {
5471
- return {
5472
- walks,
5473
- referenceDomain: ramp ? { start: ramp.start, end: ramp.start + ramp.span } : void 0,
5474
- nodeIds: new Set(walks.flatMap((w) => [...w.nodeIds])),
5475
- edgeIndexes: new Set(walks.flatMap((w) => [...w.edgeIndexes])),
5476
- names: new Set(walks.map((w) => w.name))
5477
- };
5478
- }
5479
-
5480
- // src/walkKey.ts
5481
- function rangeText(contig, start, end) {
5482
- const s = Math.round(start);
5483
- const e = Math.round(end);
5484
- return `${contig ? `${contig}:` : ""}${s.toLocaleString()}-${e.toLocaleString()} (${formatBp(e - s)})`;
5485
- }
5486
- function walkKey(walk, reference) {
5487
- const { field } = walk.encoding;
5488
- const own = walk.range ? rangeText(walk.range.contig, walk.range.start, walk.range.end) : void 0;
5489
- const scale = field === "progress" ? own : field === "reference" && reference ? rangeText(reference.name, reference.start, reference.end) : void 0;
5490
- return {
5491
- delta: walk.referenceBp === void 0 || walk.bp === walk.referenceBp ? "" : ` ${walk.bp > walk.referenceBp ? "+" : "\u2212"}${formatBp(Math.abs(walk.bp - walk.referenceBp))}`,
5492
- reversed: walk.reversedBp > 0 ? `, ${formatBp(walk.reversedBp)} reversed` : "",
5493
- shades: field !== "walk",
5494
- scale,
5495
- hover: scale === own ? void 0 : own
5496
- };
5497
- }
5498
-
5499
- // src/facetGrid.ts
5500
- var FACET_GAP_PX = 8;
5501
- var FACET_TITLE_PX = 34;
5502
- var FACET_PAD_PX = 12;
5503
- var MIN_FACET_PANEL_PX = 60;
5504
- function facetGrid({
5505
- count,
5506
- bounds,
5507
- pixelRows,
5508
- width,
5509
- room,
5510
- columns
5511
- }) {
5512
- function tile(across) {
5513
- const rows = Math.ceil(count / across);
5514
- const panelWidth = Math.floor(
5515
- (width - (across - 1) * FACET_GAP_PX) / across
5516
- );
5517
- const panelRoom = (room - rows * FACET_TITLE_PX - (rows - 1) * FACET_GAP_PX) / rows;
5518
- const fitX = (panelWidth - 2 * FACET_PAD_PX) / bounds.w;
5519
- const fitY = bounds.h > 0 ? (panelRoom - 2 * FACET_PAD_PX) / bounds.h : Infinity;
5520
- const scale = pixelRows ? fitX : Math.min(fitX, fitY);
5521
- const drawn = pixelRows ? bounds.h : bounds.h * scale;
5522
- const height = Math.floor(
5523
- Math.max(
5524
- MIN_FACET_PANEL_PX,
5525
- Math.min(drawn + 2 * FACET_PAD_PX, panelRoom)
5526
- )
5527
- );
5528
- return {
5529
- columns: across,
5530
- width: panelWidth,
5531
- height,
5532
- scale,
5533
- total: rows * (height + FACET_TITLE_PX) + (rows - 1) * FACET_GAP_PX
5534
- };
5535
- }
5536
- if (columns !== void 0) {
5537
- return tile(Math.max(1, Math.min(count, Math.round(columns))));
5538
- }
5539
- let best = tile(1);
5540
- for (let across = 2; across <= count; across++) {
5541
- const grid = tile(across);
5542
- if (grid.scale > best.scale) {
5543
- best = grid;
5544
- }
5545
- }
5546
- return best;
5547
- }
5548
-
5549
1342
  // src/bubbles/bubbleLine.ts
5550
1343
  var SATURATED_PATH_COUNT = 2147483647;
5551
1344
 
@@ -5913,6 +1706,15 @@ var ROUTE_STACK = 8;
5913
1706
  var GENE_PIN_DROP_PX = 18;
5914
1707
  var GENE_STACK = 2;
5915
1708
  var TOPMOST_BASELINE = LABEL_PX + LABEL_PAD;
1709
+ function geneCoverageNote(pin) {
1710
+ const length = pin.gene.end - pin.gene.start;
1711
+ if (pin.covered >= 0.98) {
1712
+ return void 0;
1713
+ }
1714
+ const [per, unit] = length >= 1e6 ? [1e6, "Mb"] : length >= 1e3 ? [1e3, "kb"] : [1, "bp"];
1715
+ const amount = (bp) => +(bp / per).toFixed(per === 1 ? 0 : 1);
1716
+ return `${amount(pin.covered * length)} of ${amount(length)} ${unit}`;
1717
+ }
5916
1718
  function byExtent(items, extent) {
5917
1719
  return [...items].sort((a, b) => extent(b) - extent(a));
5918
1720
  }
@@ -5972,11 +1774,13 @@ function layoutLabels(m) {
5972
1774
  const genes = placeLabels(
5973
1775
  byExtent(m.genePins, (pin) => pin.gene.end - pin.gene.start).map((pin) => {
5974
1776
  const { x, y } = screen(pin.at);
1777
+ const note = geneCoverageNote(pin);
5975
1778
  return {
5976
1779
  item: pin,
5977
1780
  x,
5978
1781
  y: y + m.contigThickness + GENE_PIN_DROP_PX,
5979
- text: pin.covered < 0.98 ? `${pin.gene.name} \u2026` : pin.gene.name,
1782
+ text: note ? `${pin.gene.name} \xB7 ${note}` : pin.gene.name,
1783
+ fallback: note ? `${pin.gene.name} \u2026` : void 0,
5980
1784
  stack: GENE_STACK
5981
1785
  };
5982
1786
  }),
@@ -6048,6 +1852,7 @@ function genePins(graph, genes, positions) {
6048
1852
  continue;
6049
1853
  }
6050
1854
  const parts = [];
1855
+ const exonsByNode = [];
6051
1856
  let at;
6052
1857
  let atDistance = Infinity;
6053
1858
  let covered = 0;
@@ -6062,6 +1867,7 @@ function genePins(graph, genes, positions) {
6062
1867
  }
6063
1868
  const line = positions[node.id];
6064
1869
  covered += Math.min(nodeEnd, gene.end) - Math.max(nodeStart, gene.start);
1870
+ const nodeParts = [];
6065
1871
  for (const exon of gene.exons) {
6066
1872
  const a = Math.max(exon.start, nodeStart);
6067
1873
  const b = Math.min(exon.end, nodeEnd);
@@ -6076,7 +1882,11 @@ function genePins(graph, genes, positions) {
6076
1882
  if (stretch.length === 1) {
6077
1883
  stretch.push({ ...stretch[0] });
6078
1884
  }
6079
- parts.push(svgPath(stretch));
1885
+ nodeParts.push(svgPath(stretch));
1886
+ }
1887
+ if (nodeParts.length > 0) {
1888
+ parts.push(...nodeParts);
1889
+ exonsByNode.push({ nodeId: node.id, d: nodeParts.join("") });
6080
1890
  }
6081
1891
  const pinBp = Math.min(Math.max(mid, nodeStart), nodeEnd);
6082
1892
  const distance = Math.abs(pinBp - mid);
@@ -6094,6 +1904,7 @@ function genePins(graph, genes, positions) {
6094
1904
  pins.push({
6095
1905
  gene,
6096
1906
  exons: parts.join(""),
1907
+ exonsByNode,
6097
1908
  at,
6098
1909
  covered: covered / (gene.end - gene.start)
6099
1910
  });
@@ -6101,262 +1912,6 @@ function genePins(graph, genes, positions) {
6101
1912
  }
6102
1913
  return pins;
6103
1914
  }
6104
-
6105
- // src/reference.ts
6106
- var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
6107
- ["hg38", "GRCh38"],
6108
- ["hg19", "GRCh37"],
6109
- ["hs1", "CHM13"],
6110
- ["t2t-chm13", "CHM13"],
6111
- ["chm13v2", "CHM13"]
6112
- ]);
6113
- function wellKnownSample(assemblyName) {
6114
- return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
6115
- }
6116
- function sampleOf2(refName) {
6117
- const sample = refName.includes("#") ? panSNSample(refName) : void 0;
6118
- return sample === void 0 || isGenericSample(sample) ? void 0 : sample;
6119
- }
6120
- function haplotypeOf(refName) {
6121
- return panSNHaplotype(refName) ?? panSNSample(refName);
6122
- }
6123
- function graphHaplotypes(graph) {
6124
- const refNames = new Set((graph.anchorPaths ?? []).map((p) => p.name));
6125
- for (const node of graph.nodes) {
6126
- if (node.stable) {
6127
- refNames.add(node.stable.refName);
6128
- }
6129
- }
6130
- const bySample = /* @__PURE__ */ new Map();
6131
- for (const refName of refNames) {
6132
- const sample = sampleOf2(refName);
6133
- if (sample !== void 0) {
6134
- const haplotypes = bySample.get(sample) ?? bySample.set(sample, /* @__PURE__ */ new Set()).get(sample);
6135
- haplotypes.add(haplotypeOf(refName));
6136
- }
6137
- }
6138
- return bySample;
6139
- }
6140
- function backboneFrom(contigs, haplotypes) {
6141
- const samples = new Set(contigs.map((c) => sampleOf2(c.refName)));
6142
- const [sample] = samples;
6143
- if (samples.size !== 1 || sample === void 0) {
6144
- return { contigs, prefixes: [], named: false };
6145
- }
6146
- const own = new Set(contigs.map((c) => haplotypeOf(c.refName)));
6147
- const unique = [...haplotypes.get(sample) ?? []].every((h) => own.has(h));
6148
- const shared = new Set(contigs.map((c) => panSNHaplotype(c.refName)));
6149
- const [haplotype] = shared;
6150
- return {
6151
- contigs,
6152
- prefixes: [
6153
- ...unique ? [sample] : [],
6154
- ...shared.size === 1 && haplotype ? [haplotype] : []
6155
- ],
6156
- named: true
6157
- };
6158
- }
6159
- function graphBackbone(graph) {
6160
- const spans = /* @__PURE__ */ new Map();
6161
- for (const node of graph.nodes) {
6162
- if (isBackbone(node)) {
6163
- const { refName, start } = node.stable;
6164
- const end = start + node.length;
6165
- const span2 = spans.get(refName);
6166
- if (span2) {
6167
- span2.start = Math.min(span2.start, start);
6168
- span2.end = Math.max(span2.end, end);
6169
- } else {
6170
- spans.set(refName, {
6171
- refName,
6172
- contig: panSNContig(refName),
6173
- start,
6174
- end
6175
- });
6176
- }
6177
- }
6178
- }
6179
- const contigs = [...spans.values()];
6180
- return contigs.length ? backboneFrom(contigs, graphHaplotypes(graph)) : void 0;
6181
- }
6182
- function backboneAssembly(backbone, assemblies) {
6183
- const names = (a) => [a.name, ...a.aliases ?? []];
6184
- const naming = (prefix) => (a) => names(a).some((n) => n.toLowerCase() === prefix.toLowerCase());
6185
- const prefixes = [...backbone?.prefixes ?? []].reverse();
6186
- const sample = backbone?.prefixes.find((p) => !p.includes("#"))?.toLowerCase();
6187
- for (const prefix of prefixes) {
6188
- const assembly = assemblies.find(naming(prefix));
6189
- if (assembly) {
6190
- return assembly;
6191
- }
6192
- }
6193
- return sample === void 0 ? void 0 : assemblies.find(
6194
- (a) => names(a).some((n) => wellKnownSample(n)?.toLowerCase() === sample)
6195
- );
6196
- }
6197
- function assemblyWalk(graph, assembly) {
6198
- const haplotypes = graphHaplotypes(graph);
6199
- return graph.anchorPaths?.find(
6200
- (walk) => backboneAssembly(
6201
- backboneFrom(
6202
- [
6203
- {
6204
- refName: walk.name,
6205
- contig: panSNContig(walk.name),
6206
- start: walk.start,
6207
- end: walk.start + walk.length
6208
- }
6209
- ],
6210
- haplotypes
6211
- ),
6212
- [assembly]
6213
- )
6214
- );
6215
- }
6216
- function refNameBinding(refNames) {
6217
- const exact = new Set(refNames);
6218
- const byContig = /* @__PURE__ */ new Map();
6219
- for (const refName of exact) {
6220
- const contig = panSNContig(refName);
6221
- byContig.set(contig, byContig.has(contig) ? void 0 : refName);
6222
- }
6223
- return (name) => exact.has(name) ? name : byContig.get(name);
6224
- }
6225
- function featuresOnBackbone(features, backbone) {
6226
- const bind = refNameBinding(backbone.contigs.map((c) => c.refName));
6227
- return features.flatMap((f) => {
6228
- const refName = bind(f.refName);
6229
- return refName === void 0 ? [] : [refName === f.refName ? f : { ...f, refName }];
6230
- });
6231
- }
6232
-
6233
- // src/gbzWindow.ts
6234
- var NoReferenceSampleError = class extends Error {
6235
- constructor(anchor, referenceSamples) {
6236
- super(
6237
- referenceSamples.length === 0 ? `the graph names no reference sample (gbwt_reference_samples) and the anchor "${anchor}" maps to none; set referenceSample` : `the anchor "${anchor}" is none of the graph's reference samples (${referenceSamples.join(", ")}); set referenceSample or map it through assemblyNameToPanSN`
6238
- );
6239
- this.name = "NoReferenceSampleError";
6240
- }
6241
- };
6242
- var NodeLimitError = class extends Error {
6243
- constructor(limit, windowBp, fitsBp) {
6244
- super(
6245
- `this ${windowBp.toLocaleString()} bp window reads more than nodeLimit (${limit.toLocaleString()}) graph nodes; zoom in to about ${fitsBp.toLocaleString()} bp or raise nodeLimit`
6246
- );
6247
- this.name = "NodeLimitError";
6248
- }
6249
- };
6250
- function haplotypePrefix(name) {
6251
- return `${name.sample}#${name.haplotype}`;
6252
- }
6253
- function haplotypeWanted(name, wanted) {
6254
- const prefix = haplotypePrefix(name);
6255
- return wanted === void 0 || wanted.some((candidate) => panSNMatchesPrefix(prefix, candidate));
6256
- }
6257
- async function referenceSamplesOf(db) {
6258
- return (await db.tag("gbwt_reference_samples") ?? "").split(/\s+/).filter((sample) => sample !== "");
6259
- }
6260
- function aliasedSample(anchorSample, referenceSamples) {
6261
- const lower = anchorSample.toLowerCase();
6262
- const alias = wellKnownSample(anchorSample)?.toLowerCase();
6263
- return referenceSamples.find((s) => s === anchorSample) ?? referenceSamples.find((s) => s.toLowerCase() === lower) ?? referenceSamples.find((s) => s.toLowerCase() === alias);
6264
- }
6265
- function resolveReferenceSample({
6266
- configured,
6267
- anchorPrefix,
6268
- referenceSamples
6269
- }) {
6270
- if (configured !== "") {
6271
- return configured;
6272
- }
6273
- const sample = aliasedSample(panSNSample(anchorPrefix), referenceSamples) ?? (referenceSamples.length === 1 ? referenceSamples[0] : void 0);
6274
- if (sample === void 0) {
6275
- throw new NoReferenceSampleError(anchorPrefix, referenceSamples);
6276
- }
6277
- return sample;
6278
- }
6279
- async function referencePathQuery(db, referenceSample, refName) {
6280
- const path = (await db.paths()).find(
6281
- (p) => p.isIndexed && p.name.sample === referenceSample && p.name.contig === refName
6282
- );
6283
- return path ? {
6284
- sample: path.name.sample,
6285
- contig: refName,
6286
- haplotype: path.name.haplotype
6287
- } : void 0;
6288
- }
6289
- function nodeLimitError(error, limit, windowBp) {
6290
- const isLimit = error instanceof Error && (error.name === "SubgraphLimitError" || /^Subgraph size limit of \d+ nodes exceeded/.test(error.message));
6291
- if (!isLimit) {
6292
- return void 0;
6293
- } else {
6294
- const walked = error.walkedBp;
6295
- const fits = typeof walked === "number" && walked > 0 ? Math.floor(walked * 0.8) : Math.floor(windowBp / 2);
6296
- return new NodeLimitError(limit, windowBp, Math.max(fits, 1));
6297
- }
6298
- }
6299
- async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
6300
- const subgraph = query ? await db.getSubgraphForRange(query, start, end, {
6301
- ...opts,
6302
- haplotypes: "all",
6303
- ...keep === void 0 ? {} : { keep }
6304
- }).catch((error) => {
6305
- throw nodeLimitError(error, opts.limit, end - start) ?? error;
6306
- }) : void 0;
6307
- return subgraph ? subgraph.toGFA({ names: "resolved" }) : "";
6308
- }
6309
-
6310
- // src/viewport.ts
6311
- function axisScaleOf(scale, pixelRows) {
6312
- return { scaleX: scale, scaleY: pixelRows ? 1 : scale, pixelRows };
6313
- }
6314
- function viewportOf(t, axis, width, height) {
6315
- return {
6316
- minX: -t.translateX / axis.scaleX,
6317
- minY: -t.translateY / axis.scaleY,
6318
- maxX: (width - t.translateX) / axis.scaleX,
6319
- maxY: (height - t.translateY) / axis.scaleY
6320
- };
6321
- }
6322
- function padded(v, panes) {
6323
- const w = (v.maxX - v.minX) * panes;
6324
- const h = (v.maxY - v.minY) * panes;
6325
- return {
6326
- minX: v.minX - w,
6327
- minY: v.minY - h,
6328
- maxX: v.maxX + w,
6329
- maxY: v.maxY + h
6330
- };
6331
- }
6332
- function contains(outer, inner) {
6333
- return inner.minX >= outer.minX && inner.maxX <= outer.maxX && inner.minY >= outer.minY && inner.maxY <= outer.maxY;
6334
- }
6335
- function zoomAbout(t, factor, centerX, centerY, pixelRows) {
6336
- const scale = clampZoom(t.scale * factor);
6337
- const ratio = scale / t.scale;
6338
- return {
6339
- scale,
6340
- translateX: centerX - (centerX - t.translateX) * ratio,
6341
- translateY: pixelRows ? t.translateY : centerY - (centerY - t.translateY) * ratio
6342
- };
6343
- }
6344
- function screenToLayout(x, y, t, axis) {
6345
- return {
6346
- x: (x - t.translateX) / axis.scaleX,
6347
- y: (y - t.translateY) / axis.scaleY
6348
- };
6349
- }
6350
-
6351
- // src/loadBandage.ts
6352
- var cached;
6353
- function loadBandage() {
6354
- cached ??= import("./chunks/bandage-layout-3AR6BCUD.js").then((mod) => mod.default()).catch((e) => {
6355
- cached = void 0;
6356
- throw e;
6357
- });
6358
- return cached;
6359
- }
6360
1915
  export {
6361
1916
  BUBBLE_KIND_COLORS,
6362
1917
  BUBBLE_KIND_NAMES,
@@ -6402,17 +1957,21 @@ export {
6402
1957
  drawTubeMapGenes,
6403
1958
  drawTubeMapRuler,
6404
1959
  drawingBounds,
1960
+ encodingStops,
6405
1961
  encodingSwatchCss,
6406
1962
  engineKey,
6407
1963
  engineRequest,
1964
+ facetCells,
6408
1965
  facetGrid,
6409
1966
  facetLifts,
6410
1967
  featuresOnBackbone,
1968
+ figureSvg,
6411
1969
  findHoveredEdge,
6412
1970
  findHoveredNode,
6413
1971
  fitTransform,
6414
1972
  forceLayout,
6415
1973
  formatBp,
1974
+ geneCoverageNote,
6416
1975
  genePins,
6417
1976
  getDpr,
6418
1977
  graphBackbone,
@@ -6433,6 +1992,7 @@ export {
6433
1992
  panSNSample,
6434
1993
  pathColorsLegible,
6435
1994
  pathLegend,
1995
+ rangeText,
6436
1996
  refNameBinding,
6437
1997
  referenceBoxes,
6438
1998
  referenceKnots,
@@ -6447,6 +2007,7 @@ export {
6447
2007
  schemeCss,
6448
2008
  screenToLayout,
6449
2009
  stripBlockAt,
2010
+ svgCanvas,
6450
2011
  tubeMapConnectors,
6451
2012
  tubeMapFrame,
6452
2013
  tubeMapGenes,