@jbrowse/bandage-core 0.1.19 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4973 @@
1
+ // src/layout/drawnScale.ts
2
+ var MEAN_NODE_LENGTH = 40;
3
+ var MIN_TOTAL_GRAPH_LENGTH = 500;
4
+ var BANDAGE_MINIMUM_NODE_LENGTH = 5;
5
+ function drawnNodeLength(opts, length) {
6
+ return Math.max(
7
+ opts.nodeLengthPerMegabase * length / 1e6,
8
+ opts.minimumNodeLength
9
+ );
10
+ }
11
+ function bandageAutoScale(graph, minNodeLength = BANDAGE_MINIMUM_NODE_LENGTH) {
12
+ const totalLength = graph.nodes.reduce((sum, n2) => sum + n2.length, 0);
13
+ const megabases = totalLength / 1e6;
14
+ const target = Math.max(
15
+ graph.nodes.length * MEAN_NODE_LENGTH,
16
+ MIN_TOTAL_GRAPH_LENGTH
17
+ );
18
+ return {
19
+ nodeLengthPerMegabase: megabases > 0 ? target / megabases : 1e4,
20
+ minimumNodeLength: Math.max(BANDAGE_MINIMUM_NODE_LENGTH, minNodeLength),
21
+ edgeLength: 5,
22
+ nodeSegmentLength: 20
23
+ };
24
+ }
25
+ var PROPORTIONAL_LENGTH = { power: 1, effect: 1 };
26
+ function isProportional(law) {
27
+ return law.power === 1 && law.effect === 1;
28
+ }
29
+ function drawnLengthFor(graph, law) {
30
+ const count = graph.nodes.length;
31
+ const totalLength = graph.nodes.reduce((sum, n2) => sum + n2.length, 0);
32
+ const meanBp = count > 0 ? totalLength / count : 0;
33
+ const meanDrawn = count > 0 ? Math.max(count * MEAN_NODE_LENGTH, MIN_TOTAL_GRAPH_LENGTH) / count : MEAN_NODE_LENGTH;
34
+ return (bp) => {
35
+ if (meanBp <= 0) {
36
+ return meanDrawn;
37
+ }
38
+ const relative = (bp / meanBp) ** law.power;
39
+ return Math.max(
40
+ meanDrawn * ((relative - 1) * law.effect + 1),
41
+ BANDAGE_MINIMUM_NODE_LENGTH
42
+ );
43
+ };
44
+ }
45
+ var LAYOUT_LENGTH_PRECISION = 1e3;
46
+ var PROPORTIONAL_SPREAD = {
47
+ law: PROPORTIONAL_LENGTH,
48
+ minNodeLength: 0
49
+ };
50
+ function layoutScaling(graph, spread = PROPORTIONAL_SPREAD) {
51
+ const { law, minNodeLength } = spread;
52
+ const opts = bandageAutoScale(graph, minNodeLength);
53
+ if (isProportional(law)) {
54
+ return { nodes: graph.nodes, opts };
55
+ }
56
+ const drawn = drawnLengthFor(graph, law);
57
+ return {
58
+ nodes: graph.nodes.map((node) => ({
59
+ ...node,
60
+ length: Math.round(drawn(node.length) * LAYOUT_LENGTH_PRECISION)
61
+ })),
62
+ opts: {
63
+ ...opts,
64
+ nodeLengthPerMegabase: 1e6 / LAYOUT_LENGTH_PRECISION,
65
+ // Bandage's own floor only. A law and a raised floor are alternatives
66
+ // rather than a stack: the law has already lifted the small end, and a
67
+ // floor on top of it would flatten exactly what it just spread.
68
+ minimumNodeLength: BANDAGE_MINIMUM_NODE_LENGTH
69
+ }
70
+ };
71
+ }
72
+
73
+ // src/bubbleSpreads.ts
74
+ var BUBBLE_SPREADS = [
75
+ {
76
+ value: "auto",
77
+ label: "Proportional",
78
+ description: "Bandage's own scale: a node draws proportional to its length, so a 1 bp allele is a speck.",
79
+ law: PROPORTIONAL_LENGTH,
80
+ minNodeFactor: 0
81
+ },
82
+ {
83
+ value: "open",
84
+ label: "Open bubbles",
85
+ description: "Give every allele a drawn length, so a bubble reads as two arms rather than a knot.",
86
+ law: PROPORTIONAL_LENGTH,
87
+ minNodeFactor: 2.5
88
+ },
89
+ {
90
+ value: "wide",
91
+ label: "Wide bubbles",
92
+ description: "As open, spread further. Best on a window of a dozen bubbles.",
93
+ law: PROPORTIONAL_LENGTH,
94
+ minNodeFactor: 10
95
+ },
96
+ {
97
+ value: "compress",
98
+ label: "Compress lengths",
99
+ description: "Pull the longest and shortest nodes towards the graph\u2019s mean, so a cut spanning kb and bp fits one pane.",
100
+ law: { power: 0.5, effect: 0.5 },
101
+ minNodeFactor: 0
102
+ }
103
+ ];
104
+ var BUBBLE_SPREAD_VALUES = BUBBLE_SPREADS.map((s) => s.value);
105
+ function spreadFor(spread) {
106
+ const entry = BUBBLE_SPREADS.find((s) => s.value === spread);
107
+ return {
108
+ law: entry?.law ?? PROPORTIONAL_LENGTH,
109
+ minNodeLength: (entry?.minNodeFactor ?? 0) * MEAN_NODE_LENGTH
110
+ };
111
+ }
112
+
113
+ // src/pansn.ts
114
+ var SEP = "#";
115
+ function panSNSample(refName) {
116
+ return refName.split(SEP)[0];
117
+ }
118
+ var GENERIC_SAMPLES = /* @__PURE__ */ new Set(["_gbwt_ref"]);
119
+ function isGenericSample(sample) {
120
+ return GENERIC_SAMPLES.has(sample);
121
+ }
122
+ function panSNHaplotype(refName) {
123
+ const parts = refName.split(SEP);
124
+ return parts.length >= 3 ? `${parts[0]}${SEP}${parts[1]}` : void 0;
125
+ }
126
+ function panSNContig(refName) {
127
+ const parts = refName.split(SEP);
128
+ return parts.length >= 3 ? parts.slice(2).join(SEP) : parts.length === 2 ? parts[1] : refName;
129
+ }
130
+ function panSNMatchesPrefix(refName, prefix) {
131
+ return prefix !== void 0 && (refName === prefix || refName.startsWith(prefix + SEP));
132
+ }
133
+
134
+ // src/anchoredNodes.ts
135
+ var REFERENCE_RANK = 0;
136
+ function isAnchored(node) {
137
+ return node.stable !== void 0;
138
+ }
139
+ function isBackbone(node) {
140
+ return node.stable?.rank === REFERENCE_RANK;
141
+ }
142
+ function isOffReference(node) {
143
+ return node.stable !== void 0 && node.stable.rank > REFERENCE_RANK;
144
+ }
145
+ function backboneNodes(graph) {
146
+ return graph.nodes.filter(isBackbone);
147
+ }
148
+ function backbonePositions(backbone) {
149
+ const positions = {};
150
+ for (const node of backbone) {
151
+ positions[node.id] = [
152
+ { x: node.stable.start, y: 0 },
153
+ { x: node.stable.start + node.length, y: 0 }
154
+ ];
155
+ }
156
+ return positions;
157
+ }
158
+ function firstNodeAtOrAfter(nodes, bp) {
159
+ let lo = 0;
160
+ let hi = nodes.length;
161
+ while (lo < hi) {
162
+ const mid = lo + hi >> 1;
163
+ if (nodes[mid].stable.start < bp) {
164
+ lo = mid + 1;
165
+ } else {
166
+ hi = mid;
167
+ }
168
+ }
169
+ return lo;
170
+ }
171
+ function backboneSpan(backbone) {
172
+ let min = Infinity;
173
+ let max = -Infinity;
174
+ for (const node of backbone) {
175
+ min = Math.min(min, node.stable.start);
176
+ max = Math.max(max, node.stable.start + node.length);
177
+ }
178
+ return max - min;
179
+ }
180
+ function referenceSpan(backbone, region) {
181
+ return region && region.end > region.start ? region.end - region.start : backboneSpan(backbone);
182
+ }
183
+
184
+ // src/gfa-core/gfaParser.ts
185
+ function parseTag(tag, tags) {
186
+ const nameEnd = tag.indexOf(":");
187
+ const typeEnd = tag.indexOf(":", nameEnd + 1);
188
+ if (nameEnd !== -1 && typeEnd !== -1) {
189
+ const name = tag.slice(0, nameEnd);
190
+ const type = tag.slice(nameEnd + 1, typeEnd);
191
+ const val = tag.slice(typeEnd + 1);
192
+ if (type === "i" || type === "f") {
193
+ tags[name] = +val;
194
+ } else if (type === "Z") {
195
+ tags[name] = val;
196
+ }
197
+ }
198
+ }
199
+ function parseTags(fields) {
200
+ const tags = {};
201
+ for (const field of fields) {
202
+ parseTag(field, tags);
203
+ }
204
+ return tags;
205
+ }
206
+ function stableCoordinate(node) {
207
+ const { SN, SO, SR } = node.tags;
208
+ return typeof SN === "string" && typeof SO === "number" && typeof SR === "number" ? { refName: SN, start: SO, rank: SR } : void 0;
209
+ }
210
+ var GT = 62;
211
+ var LT = 60;
212
+ function parseWalkBody(body) {
213
+ const segments = [];
214
+ let strand = "+";
215
+ let start = 0;
216
+ function push(end) {
217
+ if (end > start) {
218
+ segments.push({ id: body.slice(start, end), strand });
219
+ }
220
+ }
221
+ for (let i = 0; i < body.length; i++) {
222
+ const ch = body.charCodeAt(i);
223
+ if (ch === GT || ch === LT) {
224
+ push(i);
225
+ strand = ch === GT ? "+" : "-";
226
+ start = i + 1;
227
+ }
228
+ }
229
+ push(body.length);
230
+ return segments;
231
+ }
232
+ function parseGFA(file) {
233
+ const graph = {
234
+ nodes: [],
235
+ links: [],
236
+ paths: [],
237
+ walks: [],
238
+ header: [],
239
+ id: ""
240
+ };
241
+ for (const rawLine of file.split("\n")) {
242
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
243
+ if (line.startsWith("H")) {
244
+ const [, ...rest] = line.split(" ");
245
+ graph.header.push(parseTags(rest));
246
+ } else if (line.startsWith("S")) {
247
+ const [, name, ...rest] = line.split(" ");
248
+ let len;
249
+ let seq;
250
+ let tagfields;
251
+ let gfa1 = false;
252
+ const first = rest[0] ?? "*";
253
+ if (/^\d+$/.test(first)) {
254
+ len = +first;
255
+ seq = rest[1] ?? "*";
256
+ tagfields = rest.slice(2);
257
+ } else {
258
+ gfa1 = true;
259
+ seq = first;
260
+ len = seq === "*" ? 0 : seq.length;
261
+ tagfields = rest.slice(1);
262
+ }
263
+ const tags = parseTags(tagfields);
264
+ if (gfa1 && "LN" in tags) {
265
+ len = +tags.LN;
266
+ }
267
+ graph.nodes.push({ id: name, length: len, sequence: seq, tags });
268
+ } else if (line.startsWith("E")) {
269
+ const [, , source, target, , , , , cigar, ...rest] = line.split(" ");
270
+ if (source && target) {
271
+ graph.links.push({
272
+ source: source.slice(0, -1),
273
+ target: target.slice(0, -1),
274
+ strand1: source.at(-1),
275
+ strand2: target.at(-1),
276
+ cigar: cigar ?? "*",
277
+ tags: parseTags(rest)
278
+ });
279
+ }
280
+ } else if (line.startsWith("L")) {
281
+ const [, source, strand1, target, strand2, cigar, ...rest] = line.split(" ");
282
+ if (source && target && strand2) {
283
+ graph.links.push({
284
+ source,
285
+ target,
286
+ strand1,
287
+ strand2,
288
+ cigar: cigar ?? "*",
289
+ tags: parseTags(rest)
290
+ });
291
+ }
292
+ } else if (line.startsWith("W")) {
293
+ const [, sample, hap, contig, start, end, body, ...rest] = line.split(" ");
294
+ if (sample && hap && contig && start && end && body) {
295
+ graph.walks.push({
296
+ sample,
297
+ haplotype: +hap,
298
+ contig,
299
+ start: start === "*" ? -1 : +start,
300
+ end: end === "*" ? -1 : +end,
301
+ segments: parseWalkBody(body),
302
+ tags: parseTags(rest)
303
+ });
304
+ }
305
+ } else if (line.startsWith("P")) {
306
+ const [, name, path, ...rest] = line.split(" ");
307
+ if (name && path) {
308
+ graph.paths.push({ name, path, rest });
309
+ }
310
+ }
311
+ }
312
+ return graph;
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((n2) => [n2.id, n2.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((n2) => n2.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 sides = 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, sides[ei].from, ei);
558
+ attach(edge.to, sides[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 && sides[ei].from === "end" && sides[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((n2) => [n2.id, n2]));
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, n2) => sum + n2.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 = sides[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 span = arc[i] - arc[i - 1];
700
+ const t = span > 0 ? (s - arc[i - 1]) / span : 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 span = nodeReferenceSpan({ nodeId: node.id, nodeById, neighbors });
846
+ if (span) {
847
+ midpoints.set(node.id, (span.start + span.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((n2) => [n2.id, drawnNodeLength(scaling.opts, n2.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/layout/rowSpacing.ts
1029
+ var ROW_HEIGHT_PX = 20;
1030
+
1031
+ // src/pathColors.ts
1032
+ function nameHue(name) {
1033
+ let hash = 0;
1034
+ for (let i = 0; i < name.length; i++) {
1035
+ hash = name.charCodeAt(i) + ((hash << 5) - hash);
1036
+ }
1037
+ return Math.abs(hash % 360);
1038
+ }
1039
+ function pathHueAt(index, count) {
1040
+ return index * 360 / count;
1041
+ }
1042
+ var PATH_SATURATION = 0.7;
1043
+ var PATH_LIGHTNESS = 0.5;
1044
+ var MAX_PATH_COLORS = 16;
1045
+ function pathColorsLegible(pathCount) {
1046
+ return pathCount > 0 && pathCount <= MAX_PATH_COLORS;
1047
+ }
1048
+ function pathCssColor(index, count) {
1049
+ return `hsl(${pathHueAt(index, count)}, ${PATH_SATURATION * 100}%, ${PATH_LIGHTNESS * 100}%)`;
1050
+ }
1051
+ function pathGreyCssColor(index, count) {
1052
+ const lightness = count > 1 ? 25 + 45 * index / (count - 1) : 45;
1053
+ return `hsl(0, 0%, ${lightness}%)`;
1054
+ }
1055
+ function labelTiers(name) {
1056
+ const parts = pathOrigin(name).name.split("#");
1057
+ const full = parts.join("#");
1058
+ if (parts.length >= 3 && isGenericSample(parts[0])) {
1059
+ const contig = parts.slice(2).join("#");
1060
+ return [contig, contig, full];
1061
+ }
1062
+ return [parts[0], parts.slice(0, 2).join("#"), full];
1063
+ }
1064
+ function groupDigits(n2) {
1065
+ return String(n2).replace(/\B(?=(\d{3})+$)/g, ",");
1066
+ }
1067
+ function offsetTiers(name, tiers) {
1068
+ const { start } = pathOrigin(name);
1069
+ return tiers.map((t) => `${t} @${groupDigits(start)}`);
1070
+ }
1071
+ function pathLegend(paths, colors) {
1072
+ const named = paths.map((p) => labelTiers(p.name));
1073
+ const candidates = [0, 1, 2].flatMap((t) => [
1074
+ paths.map((_, i) => named[i][t]),
1075
+ paths.map((p, i) => offsetTiers(p.name, named[i])[t])
1076
+ ]);
1077
+ const distinct = candidates.find(
1078
+ (labels2) => new Set(labels2).size === paths.length
1079
+ );
1080
+ const labels = distinct ?? candidates.at(-1);
1081
+ return paths.map((path, i) => ({
1082
+ name: path.name,
1083
+ label: labels[i],
1084
+ color: colors?.[i] ?? pathCssColor(i, paths.length)
1085
+ }));
1086
+ }
1087
+
1088
+ // src/layout/walkRows.ts
1089
+ function sampleOf(path) {
1090
+ return path.sample ?? panSNSample(path.name);
1091
+ }
1092
+ function labelOf(path) {
1093
+ return path.haplotype !== void 0 && path.sample !== void 0 ? `${path.sample}#${path.haplotype}` : sampleOf(path);
1094
+ }
1095
+ function sliceBetween(nodeIds, span, region, flanked = true) {
1096
+ if (!region || !flanked) {
1097
+ return { ids: nodeIds, complete: true };
1098
+ }
1099
+ let i0 = -1;
1100
+ let i1 = -1;
1101
+ let bestEnd = -Infinity;
1102
+ let bestStart = Infinity;
1103
+ nodeIds.forEach((id, i) => {
1104
+ const s = span.get(id);
1105
+ if (s) {
1106
+ if (s.end <= region.start && s.end > bestEnd) {
1107
+ bestEnd = s.end;
1108
+ i0 = i;
1109
+ }
1110
+ if (s.start >= region.end && s.start < bestStart) {
1111
+ bestStart = s.start;
1112
+ i1 = i;
1113
+ }
1114
+ }
1115
+ });
1116
+ if (i0 < 0 || i1 < 0) {
1117
+ return { ids: nodeIds, complete: false };
1118
+ }
1119
+ const ids = nodeIds.slice(Math.min(i0, i1) + 1, Math.max(i0, i1));
1120
+ return { ids: i0 < i1 ? ids : ids.reverse(), complete: true };
1121
+ }
1122
+ function walkRows(graph, region, unit) {
1123
+ const paths = graph.paths ?? [];
1124
+ const reference = paths.find((p) => pathOrigin(p.name).name === graph.referencePath) ?? paths[0];
1125
+ if (!reference || paths.length < 2) {
1126
+ return void 0;
1127
+ }
1128
+ const lengthOf = new Map(graph.nodes.map((n2) => [n2.id, n2.length]));
1129
+ const referenceStart = graph.anchorPaths?.find((p) => p.name === pathOrigin(reference.name).name)?.start ?? 0;
1130
+ const cut = region && region.end > region.start ? region : void 0;
1131
+ const span = /* @__PURE__ */ new Map();
1132
+ let pos = referenceStart;
1133
+ for (const id of reference.nodeIds) {
1134
+ const len = lengthOf.get(id) ?? 0;
1135
+ if (!span.has(id)) {
1136
+ span.set(id, { start: pos, end: pos + len });
1137
+ }
1138
+ pos += len;
1139
+ }
1140
+ const flanked = cut === void 0 || [...span.values()].some((s) => s.end <= cut.start) && [...span.values()].some((s) => s.start >= cut.end);
1141
+ const rowOf = (path) => {
1142
+ const { ids, complete } = sliceBetween(path.nodeIds, span, cut, flanked);
1143
+ const runs = [];
1144
+ let bp = 0;
1145
+ let offReferenceBp = 0;
1146
+ let step = 0;
1147
+ for (const id of ids) {
1148
+ const len = lengthOf.get(id) ?? 0;
1149
+ const s = span.get(id);
1150
+ const last = runs.at(-1);
1151
+ const at = last?.referenceStart;
1152
+ const forward = s && at !== void 0 && step >= 0 && at + last.bp === s.start;
1153
+ const backward = s && at !== void 0 && step <= 0 && s.end === at;
1154
+ if (!s && last && !last.onReference) {
1155
+ last.bp += len;
1156
+ } else if (forward || backward) {
1157
+ last.bp += len;
1158
+ if (!forward) {
1159
+ last.referenceStart = s.start;
1160
+ last.reversed = true;
1161
+ }
1162
+ step = forward ? 1 : -1;
1163
+ } else {
1164
+ runs.push({
1165
+ start: bp,
1166
+ bp: len,
1167
+ onReference: s !== void 0,
1168
+ referenceStart: s?.start
1169
+ });
1170
+ step = 0;
1171
+ }
1172
+ bp += len;
1173
+ if (!s) {
1174
+ offReferenceBp += len;
1175
+ }
1176
+ }
1177
+ return {
1178
+ name: path.name,
1179
+ label: labelOf(path),
1180
+ sample: sampleOf(path),
1181
+ haplotype: path.haplotype,
1182
+ bp,
1183
+ offReferenceBp,
1184
+ complete,
1185
+ runs
1186
+ };
1187
+ };
1188
+ const meetsReference = (p) => p.nodeIds.some((id) => span.has(id));
1189
+ const placedWalks = new Set(
1190
+ paths.filter((p) => p !== reference && meetsReference(p)).map((p) => p.name)
1191
+ );
1192
+ const origin = cut ? cut.start : referenceStart;
1193
+ return {
1194
+ origin,
1195
+ unit,
1196
+ reference: rowOf(reference),
1197
+ rows: paths.filter(
1198
+ (p) => p !== reference && (meetsReference(p) || !placedWalks.has(p.name))
1199
+ ).map(rowOf).sort(
1200
+ (a, b) => Number(b.complete) - Number(a.complete) || b.bp - a.bp || a.label.localeCompare(b.label)
1201
+ )
1202
+ };
1203
+ }
1204
+
1205
+ // src/layout/walkRowLayout.ts
1206
+ var READOUT_ROOM = 1.3;
1207
+ function walkRowsExtent(walks) {
1208
+ let longest = walks.reference.bp;
1209
+ for (const row of walks.rows) {
1210
+ longest = Math.max(longest, row.bp);
1211
+ }
1212
+ return {
1213
+ maxX: walks.origin + longest * READOUT_ROOM,
1214
+ maxY: walks.rows.length * ROW_HEIGHT_PX
1215
+ };
1216
+ }
1217
+ function walkRowLayout(graph, region) {
1218
+ const backbone = backboneNodes(graph);
1219
+ const walks = walkRows(graph, region);
1220
+ if (backbone.length === 0 || !walks) {
1221
+ return void 0;
1222
+ }
1223
+ const nodePositions = backbonePositions(backbone);
1224
+ const rowLabels = [
1225
+ { label: walks.reference.label, y: 0 },
1226
+ ...walks.rows.map((row, i) => ({
1227
+ label: row.label,
1228
+ y: (i + 1) * ROW_HEIGHT_PX
1229
+ }))
1230
+ ];
1231
+ return {
1232
+ nodePositions,
1233
+ rowLabels,
1234
+ referenceAxis: true,
1235
+ pixelRows: true,
1236
+ extent: walkRowsExtent(walks)
1237
+ };
1238
+ }
1239
+
1240
+ // src/alleleProjection/projectAlleles.ts
1241
+ function parsePanSN(refName) {
1242
+ const parts = refName.split("#");
1243
+ const haplotype = parts.length >= 2 ? Number(parts[1]) : Number.NaN;
1244
+ return {
1245
+ sample: parts[0] ?? refName,
1246
+ haplotype: Number.isFinite(haplotype) ? haplotype : void 0
1247
+ };
1248
+ }
1249
+ function collectRun(seed, byId, adj, claimed) {
1250
+ const members = [];
1251
+ const anchors = [];
1252
+ const queue = [seed];
1253
+ claimed.add(seed);
1254
+ while (queue.length > 0) {
1255
+ const id = queue.pop();
1256
+ const node = byId.get(id);
1257
+ if (node) {
1258
+ members.push(node);
1259
+ for (const neighborId of adj.get(id) ?? []) {
1260
+ const neighbor = byId.get(neighborId);
1261
+ if (neighbor) {
1262
+ if (neighbor.stable.rank === REFERENCE_RANK) {
1263
+ anchors.push(neighbor);
1264
+ } else if (!claimed.has(neighborId)) {
1265
+ claimed.add(neighborId);
1266
+ queue.push(neighborId);
1267
+ }
1268
+ }
1269
+ }
1270
+ }
1271
+ }
1272
+ return { members, anchors };
1273
+ }
1274
+ function anchorSpan(anchors) {
1275
+ const refNames = new Set(anchors.map((a) => a.stable.refName));
1276
+ if (refNames.size !== 1 || anchors.length < 2) {
1277
+ return void 0;
1278
+ }
1279
+ const sorted = [...anchors].sort((a, b) => a.stable.start - b.stable.start);
1280
+ const first = sorted[0];
1281
+ const last = sorted.at(-1);
1282
+ const start = first.stable.start + first.length;
1283
+ const end = last.stable.start;
1284
+ return end >= start ? { start, end, entry: first } : void 0;
1285
+ }
1286
+ function runDistances(members, entry, adj) {
1287
+ const byId = new Map(members.map((m) => [m.id, m]));
1288
+ const dist = /* @__PURE__ */ new Map();
1289
+ const queue = [];
1290
+ for (const neighborId of adj.get(entry.id) ?? []) {
1291
+ if (byId.has(neighborId)) {
1292
+ dist.set(neighborId, 0);
1293
+ queue.push(neighborId);
1294
+ }
1295
+ }
1296
+ if (queue.length === 0) {
1297
+ for (const member of members) {
1298
+ dist.set(member.id, 0);
1299
+ queue.push(member.id);
1300
+ }
1301
+ }
1302
+ for (const id of queue) {
1303
+ const reach = dist.get(id) + byId.get(id).length;
1304
+ for (const neighborId of adj.get(id) ?? []) {
1305
+ if (byId.has(neighborId)) {
1306
+ const known = dist.get(neighborId);
1307
+ if (known === void 0 || reach < known) {
1308
+ dist.set(neighborId, reach);
1309
+ queue.push(neighborId);
1310
+ }
1311
+ }
1312
+ }
1313
+ }
1314
+ return dist;
1315
+ }
1316
+ function runOffsets(members, entry, adj) {
1317
+ const dist = runDistances(members, entry, adj);
1318
+ const ordered = [...members].sort((a, b) => dist.get(a.id) - dist.get(b.id));
1319
+ const rank = new Map(ordered.map((m, i) => [m.id, i]));
1320
+ const byId = new Map(members.map((m) => [m.id, m]));
1321
+ const offsets = /* @__PURE__ */ new Map();
1322
+ for (const [i, member] of ordered.entries()) {
1323
+ let offset = 0;
1324
+ for (const neighborId of adj.get(member.id) ?? []) {
1325
+ const neighbor = byId.get(neighborId);
1326
+ if (neighbor && rank.get(neighborId) < i) {
1327
+ offset = Math.max(offset, offsets.get(neighborId) + neighbor.length);
1328
+ }
1329
+ }
1330
+ offsets.set(member.id, offset);
1331
+ }
1332
+ return { offsets, ordered };
1333
+ }
1334
+ function projectAlleles(graph) {
1335
+ const byId = new Map(
1336
+ graph.nodes.filter(isAnchored).map((n2) => [n2.id, n2])
1337
+ );
1338
+ const adj = buildNeighbors(graph);
1339
+ const claimed = /* @__PURE__ */ new Set();
1340
+ const alleles = [];
1341
+ for (const node of byId.values()) {
1342
+ if (node.stable.rank !== REFERENCE_RANK && !claimed.has(node.id)) {
1343
+ const { members, anchors } = collectRun(node.id, byId, adj, claimed);
1344
+ const span = anchorSpan(anchors);
1345
+ if (span) {
1346
+ const { offsets, ordered } = runOffsets(members, span.entry, adj);
1347
+ alleles.push({
1348
+ start: span.start,
1349
+ refSpan: span.end - span.start,
1350
+ nodeIds: ordered.map((m) => m.id),
1351
+ nodeOffsets: ordered.map((m) => offsets.get(m.id)),
1352
+ pathLength: ordered.reduce(
1353
+ (max, m) => Math.max(max, offsets.get(m.id) + m.length),
1354
+ 0
1355
+ )
1356
+ });
1357
+ }
1358
+ }
1359
+ }
1360
+ return alleles.sort((a, b) => a.start - b.start);
1361
+ }
1362
+
1363
+ // src/layout/placeOffReference.ts
1364
+ var MIN_DELETION_BP = 1;
1365
+ var MIN_ALLELE_SPAN_FRACTION = 0.015;
1366
+ function placeOffReference({
1367
+ graph,
1368
+ span,
1369
+ rowY,
1370
+ positions
1371
+ }) {
1372
+ const minSpan = span * MIN_ALLELE_SPAN_FRACTION;
1373
+ const alleles = projectAlleles(graph);
1374
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
1375
+ const alleleDeletions = [];
1376
+ for (const allele of alleles) {
1377
+ const skipped = allele.refSpan - allele.pathLength;
1378
+ if (skipped >= MIN_DELETION_BP) {
1379
+ alleleDeletions.push({ nodeIds: allele.nodeIds, bp: skipped });
1380
+ }
1381
+ const drawn = Math.max(allele.refSpan, minSpan);
1382
+ const scale = allele.pathLength > 0 ? drawn / allele.pathLength : 0;
1383
+ allele.nodeIds.forEach((nodeId, i) => {
1384
+ const node = byId.get(nodeId);
1385
+ if (node) {
1386
+ const offset = allele.nodeOffsets[i];
1387
+ const even = drawn / allele.nodeIds.length;
1388
+ const at = (bp) => allele.start + bp * scale;
1389
+ const y = rowY(node);
1390
+ positions[nodeId] = scale ? [
1391
+ { x: at(offset), y },
1392
+ { x: at(offset + node.length), y }
1393
+ ] : [
1394
+ { x: allele.start + i * even, y },
1395
+ { x: allele.start + (i + 1) * even, y }
1396
+ ];
1397
+ }
1398
+ });
1399
+ }
1400
+ const neighbors = buildNeighbors(graph);
1401
+ const queue = Object.keys(positions);
1402
+ for (const fromId of queue) {
1403
+ const fromEnd = positions[fromId].at(-1).x;
1404
+ for (const nextId of neighbors.get(fromId) ?? []) {
1405
+ const next = byId.get(nextId);
1406
+ if (next && !(nextId in positions)) {
1407
+ const y = rowY(next);
1408
+ positions[nextId] = [
1409
+ { x: fromEnd, y },
1410
+ { x: fromEnd + minSpan, y }
1411
+ ];
1412
+ queue.push(nextId);
1413
+ }
1414
+ }
1415
+ }
1416
+ return alleleDeletions;
1417
+ }
1418
+
1419
+ // src/layout/anchoredLayout.ts
1420
+ function rankRows(graph) {
1421
+ const present = /* @__PURE__ */ new Set();
1422
+ for (const node of graph.nodes) {
1423
+ if (node.stable) {
1424
+ present.add(node.stable.rank);
1425
+ }
1426
+ }
1427
+ return new Map([...present].sort((a, b) => a - b).map((r, i) => [r, i]));
1428
+ }
1429
+ function anchoredLayout(graph, region) {
1430
+ const backbone = backboneNodes(graph);
1431
+ if (backbone.length === 0) {
1432
+ return void 0;
1433
+ }
1434
+ const span = referenceSpan(backbone, region);
1435
+ const rows = rankRows(graph);
1436
+ const nodePositions = backbonePositions(backbone);
1437
+ const alleleDeletions = placeOffReference({
1438
+ graph,
1439
+ span,
1440
+ rowY: (node) => (node.stable ? rows.get(node.stable.rank) ?? rows.size : rows.size) * ROW_HEIGHT_PX,
1441
+ positions: nodePositions
1442
+ });
1443
+ const rowLabels = [...rows].map(([rank, row]) => ({
1444
+ label: rank === 0 ? "Reference (rank 0)" : `Rank ${rank}`,
1445
+ y: row * ROW_HEIGHT_PX
1446
+ }));
1447
+ return {
1448
+ nodePositions,
1449
+ rowLabels,
1450
+ referenceAxis: true,
1451
+ pixelRows: true,
1452
+ alleleDeletions
1453
+ };
1454
+ }
1455
+
1456
+ // src/layout/orderedLayout.ts
1457
+ var GUTTER = 12;
1458
+ var LANE_PX = ROW_HEIGHT_PX;
1459
+ function nodeWidth(node) {
1460
+ return 10 + 8 * Math.log2(1 + node.length);
1461
+ }
1462
+ function compare(a, b) {
1463
+ return a < b ? -1 : a > b ? 1 : 0;
1464
+ }
1465
+ function compareKeys(a, b) {
1466
+ return compare(a.at, b.at) || compare(a.side, b.side) || compare(a.hops, b.hops);
1467
+ }
1468
+ var UNREACHED = { at: Infinity, side: 0, hops: 0 };
1469
+ function referenceOrder(graph) {
1470
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
1471
+ const key = orderKeys(graph);
1472
+ return graph.nodes.map((n2) => n2.id).sort(
1473
+ (a, b) => compareKeys(key.get(a) ?? UNREACHED, key.get(b) ?? UNREACHED) || compare(byId.get(a).name, byId.get(b).name) || compare(a, b)
1474
+ );
1475
+ }
1476
+ function placeByWalks(graph, backbone) {
1477
+ const isNode = new Set(graph.nodes.map((n2) => n2.id));
1478
+ const startOf = new Map(backbone.map((n2) => [n2.id, n2.stable.start]));
1479
+ const next = /* @__PURE__ */ new Map();
1480
+ const prev = /* @__PURE__ */ new Map();
1481
+ backbone.forEach((n2, i) => {
1482
+ prev.set(n2.id, backbone[i - 1]?.id);
1483
+ next.set(n2.id, backbone[i + 1]?.id);
1484
+ });
1485
+ let head = backbone[0]?.id;
1486
+ const insertAfter = (at, id) => {
1487
+ const after = next.get(at);
1488
+ next.set(at, id);
1489
+ prev.set(id, at);
1490
+ next.set(id, after);
1491
+ if (after !== void 0) {
1492
+ prev.set(after, id);
1493
+ }
1494
+ };
1495
+ const insertBefore = (at, id) => {
1496
+ const before = prev.get(at);
1497
+ if (before === void 0) {
1498
+ head = id;
1499
+ prev.set(id, void 0);
1500
+ next.set(id, at);
1501
+ prev.set(at, id);
1502
+ } else {
1503
+ insertAfter(before, id);
1504
+ }
1505
+ };
1506
+ for (const path of graph.paths ?? []) {
1507
+ let rising = 0;
1508
+ let falling = 0;
1509
+ let last;
1510
+ for (const id of path.nodeIds) {
1511
+ const start = startOf.get(id);
1512
+ if (start !== void 0) {
1513
+ if (last !== void 0 && start !== last) {
1514
+ if (start > last) {
1515
+ rising++;
1516
+ } else {
1517
+ falling++;
1518
+ }
1519
+ }
1520
+ last = start;
1521
+ }
1522
+ }
1523
+ const ids = falling > rising ? [...path.nodeIds].reverse() : path.nodeIds;
1524
+ let from;
1525
+ const leading = /* @__PURE__ */ new Set();
1526
+ for (const id of ids) {
1527
+ if (next.has(id)) {
1528
+ if (from === void 0) {
1529
+ for (const lead of leading) {
1530
+ insertBefore(id, lead);
1531
+ }
1532
+ }
1533
+ from = id;
1534
+ } else if (isNode.has(id)) {
1535
+ if (from === void 0) {
1536
+ leading.add(id);
1537
+ } else {
1538
+ insertAfter(from, id);
1539
+ from = id;
1540
+ }
1541
+ }
1542
+ }
1543
+ }
1544
+ const placed = [];
1545
+ for (let id = head; id !== void 0; id = next.get(id)) {
1546
+ placed.push(id);
1547
+ }
1548
+ return placed;
1549
+ }
1550
+ function placeBySearch(adjacent, key) {
1551
+ const queue = [...key.entries()].sort(([a, ka], [b, kb]) => compareKeys(ka, kb) || compare(a, b)).map(([id]) => id);
1552
+ const placed = queue.length;
1553
+ for (const id of queue) {
1554
+ const from = key.get(id);
1555
+ for (const nb of adjacent.get(id) ?? []) {
1556
+ if (!key.has(nb)) {
1557
+ key.set(nb, { at: from.at, side: 1, hops: from.hops + 1 });
1558
+ queue.push(nb);
1559
+ }
1560
+ }
1561
+ }
1562
+ const searched = new Set(queue.slice(placed));
1563
+ const sided = /* @__PURE__ */ new Set();
1564
+ for (const seed of searched) {
1565
+ if (sided.has(seed)) {
1566
+ continue;
1567
+ }
1568
+ const { at } = key.get(seed);
1569
+ const run = [seed];
1570
+ sided.add(seed);
1571
+ let farHalf = true;
1572
+ for (const id of run) {
1573
+ const { hops } = key.get(id);
1574
+ let deadEnd = true;
1575
+ let touchesLeft = false;
1576
+ for (const nb of adjacent.get(id) ?? []) {
1577
+ const other = key.get(nb);
1578
+ if (other.at === at && searched.has(nb)) {
1579
+ deadEnd &&= other.hops <= hops;
1580
+ if (!sided.has(nb)) {
1581
+ sided.add(nb);
1582
+ run.push(nb);
1583
+ }
1584
+ } else if (other.at !== at) {
1585
+ touchesLeft ||= other.at < at;
1586
+ farHalf &&= other.at < at;
1587
+ }
1588
+ }
1589
+ farHalf &&= !deadEnd || touchesLeft;
1590
+ }
1591
+ if (farHalf) {
1592
+ for (const id of run) {
1593
+ const k = key.get(id);
1594
+ key.set(id, { ...k, side: -1, hops: -k.hops });
1595
+ }
1596
+ }
1597
+ }
1598
+ }
1599
+ function orderKeys(graph) {
1600
+ const ids = new Set(graph.nodes.map((n2) => n2.id));
1601
+ const adjacent = /* @__PURE__ */ new Map();
1602
+ for (const e of graph.edges) {
1603
+ if (!ids.has(e.from) || !ids.has(e.to)) {
1604
+ continue;
1605
+ }
1606
+ ;
1607
+ (adjacent.get(e.from) ?? adjacent.set(e.from, []).get(e.from)).push(e.to);
1608
+ (adjacent.get(e.to) ?? adjacent.set(e.to, []).get(e.to)).push(e.from);
1609
+ }
1610
+ const backbone = graph.nodes.filter(isBackbone).sort(
1611
+ (a, b) => compare(a.stable.start, b.stable.start) || compare(a.name, b.name)
1612
+ );
1613
+ const key = /* @__PURE__ */ new Map();
1614
+ placeByWalks(graph, backbone).forEach((id, rank) => {
1615
+ key.set(id, { at: rank, side: 0, hops: 0 });
1616
+ });
1617
+ placeBySearch(adjacent, key);
1618
+ return key;
1619
+ }
1620
+ function layerGraph(graph) {
1621
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
1622
+ const order = referenceOrder(graph);
1623
+ const key = new Map(order.map((id, i) => [id, i]));
1624
+ const isRef = (id) => isBackbone(byId.get(id));
1625
+ const preds = new Map(order.map((id) => [id, []]));
1626
+ const succs = new Map(order.map((id) => [id, []]));
1627
+ const link = (a, b) => {
1628
+ preds.get(b).push(a);
1629
+ succs.get(a).push(b);
1630
+ };
1631
+ for (const e of graph.edges) {
1632
+ if (e.from === e.to || !key.has(e.from) || !key.has(e.to)) {
1633
+ continue;
1634
+ }
1635
+ if (key.get(e.from) < key.get(e.to)) {
1636
+ link(e.from, e.to);
1637
+ } else {
1638
+ link(e.to, e.from);
1639
+ }
1640
+ }
1641
+ const backbone = order.filter(isRef);
1642
+ for (let i = 1; i < backbone.length; i++) {
1643
+ link(backbone[i - 1], backbone[i]);
1644
+ }
1645
+ const layerOf = /* @__PURE__ */ new Map();
1646
+ for (const id of order) {
1647
+ let l = 0;
1648
+ for (const p of preds.get(id)) {
1649
+ l = Math.max(l, layerOf.get(p) + 1);
1650
+ }
1651
+ layerOf.set(id, l);
1652
+ }
1653
+ for (let i = order.length - 1; i >= 0; i--) {
1654
+ const id = order[i];
1655
+ const s = succs.get(id);
1656
+ if (isRef(id) || s.length === 0) {
1657
+ continue;
1658
+ }
1659
+ let minSucc = Infinity;
1660
+ for (const t of s) {
1661
+ minSucc = Math.min(minSucc, layerOf.get(t));
1662
+ }
1663
+ const lo = layerOf.get(id);
1664
+ const hi = Math.max(lo, minSucc - 1);
1665
+ layerOf.set(id, Math.floor((lo + hi) / 2));
1666
+ }
1667
+ const layers = [];
1668
+ for (const id of order) {
1669
+ ;
1670
+ (layers[layerOf.get(id)] ??= []).push(id);
1671
+ }
1672
+ return { layers, layerOf, preds };
1673
+ }
1674
+ function orderedLayout(graph) {
1675
+ if (!graph.nodes.some(isBackbone)) {
1676
+ return void 0;
1677
+ }
1678
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
1679
+ const isRef = (id) => isBackbone(byId.get(id));
1680
+ const { layers, layerOf: layer, preds } = layerGraph(graph);
1681
+ const layerWidth = layers.map((ids) => {
1682
+ let w = 0;
1683
+ for (const id of ids) {
1684
+ w = Math.max(w, nodeWidth(byId.get(id)));
1685
+ }
1686
+ return w;
1687
+ });
1688
+ const layerX = [];
1689
+ let x = 0;
1690
+ for (let l = 0; l < layers.length; l++) {
1691
+ layerX[l] = x;
1692
+ x += layerWidth[l] + GUTTER;
1693
+ }
1694
+ const y = /* @__PURE__ */ new Map();
1695
+ for (const ids of layers) {
1696
+ const free = [];
1697
+ for (const id of ids) {
1698
+ if (isRef(id)) {
1699
+ y.set(id, 0);
1700
+ continue;
1701
+ }
1702
+ const placed = preds.get(id).filter((p) => y.has(p));
1703
+ let ideal = 0;
1704
+ for (const p of placed) {
1705
+ ideal += y.get(p) / placed.length;
1706
+ }
1707
+ free.push({ id, ideal: ideal === 0 ? LANE_PX : ideal });
1708
+ }
1709
+ free.sort((a, b) => a.ideal - b.ideal);
1710
+ const taken = /* @__PURE__ */ new Set([0]);
1711
+ for (const { id, ideal } of free) {
1712
+ let lane = Math.round(ideal / LANE_PX);
1713
+ if (lane === 0) {
1714
+ lane = ideal >= 0 ? 1 : -1;
1715
+ }
1716
+ for (let d = 1; taken.has(lane); d++) {
1717
+ if (!taken.has(lane + d)) {
1718
+ lane += d;
1719
+ } else if (!taken.has(lane - d)) {
1720
+ lane -= d;
1721
+ }
1722
+ }
1723
+ taken.add(lane);
1724
+ y.set(id, lane * LANE_PX);
1725
+ }
1726
+ }
1727
+ const nodePositions = {};
1728
+ for (const n2 of graph.nodes) {
1729
+ const l = layer.get(n2.id);
1730
+ const w = nodeWidth(n2);
1731
+ const cx = layerX[l] + layerWidth[l] / 2;
1732
+ const ny = y.get(n2.id);
1733
+ nodePositions[n2.id] = [
1734
+ { x: cx - w / 2, y: ny },
1735
+ { x: cx + w / 2, y: ny }
1736
+ ];
1737
+ }
1738
+ return { nodePositions, referenceAxis: false, pixelRows: true };
1739
+ }
1740
+
1741
+ // src/layout/sampleRowLayout.ts
1742
+ function contributingSamples(graph, keep) {
1743
+ const carried = /* @__PURE__ */ new Map();
1744
+ for (const node of graph.nodes) {
1745
+ if (isOffReference(node)) {
1746
+ const { sample } = parsePanSN(node.stable.refName);
1747
+ carried.set(sample, (carried.get(sample) ?? 0) + node.length);
1748
+ }
1749
+ }
1750
+ const held = new Map(keep.map((sample, row) => [sample, row]));
1751
+ return [...carried.keys()].sort(
1752
+ (a, b) => (held.get(a) ?? Infinity) - (held.get(b) ?? Infinity) || carried.get(b) - carried.get(a) || a.localeCompare(b)
1753
+ );
1754
+ }
1755
+ function sampleRowLayout(graph, region, keep = []) {
1756
+ const backbone = backboneNodes(graph);
1757
+ const samples = contributingSamples(graph, keep);
1758
+ if (backbone.length === 0 || samples.length === 0) {
1759
+ return void 0;
1760
+ }
1761
+ const span = referenceSpan(backbone, region);
1762
+ const rowOf = new Map(samples.map((s, i) => [s, i + 1]));
1763
+ const nodePositions = backbonePositions(backbone);
1764
+ const spareRow = samples.length + 1;
1765
+ const alleleDeletions = placeOffReference({
1766
+ graph,
1767
+ span,
1768
+ rowY: (node) => {
1769
+ const stable = node.stable;
1770
+ const row = stable ? rowOf.get(parsePanSN(stable.refName).sample) : void 0;
1771
+ return (row ?? spareRow) * ROW_HEIGHT_PX;
1772
+ },
1773
+ positions: nodePositions
1774
+ });
1775
+ const referenceSample = parsePanSN(backbone[0].stable.refName).sample;
1776
+ const rowLabels = [
1777
+ { label: referenceSample, y: 0 },
1778
+ ...samples.map((sample) => ({
1779
+ label: sample,
1780
+ y: rowOf.get(sample) * ROW_HEIGHT_PX
1781
+ }))
1782
+ ];
1783
+ return {
1784
+ nodePositions,
1785
+ rowLabels,
1786
+ sampleRows: samples,
1787
+ referenceAxis: true,
1788
+ pixelRows: true,
1789
+ alleleDeletions
1790
+ };
1791
+ }
1792
+
1793
+ // src/layout/tubeMapLayout.ts
1794
+ import { layoutTubeMap } from "@gmod/tubemap-core";
1795
+
1796
+ // src/gaf/parseGaf.ts
1797
+ 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;
1798
+ function parseCs(cs) {
1799
+ const ops = [];
1800
+ for (const m of cs.matchAll(CS_OP)) {
1801
+ if (m[1] !== void 0) {
1802
+ ops.push({ op: "match", length: +m[1] });
1803
+ } else if (m[2] !== void 0) {
1804
+ ops.push({ op: "match", length: m[2].length });
1805
+ } else if (m[3] !== void 0) {
1806
+ ops.push({ op: "substitution", seq: m[3].toUpperCase() });
1807
+ } else if (m[4] !== void 0) {
1808
+ ops.push({ op: "insertion", seq: m[4].toUpperCase() });
1809
+ } else if (m[5] !== void 0) {
1810
+ ops.push({ op: "deletion", length: m[5].length });
1811
+ } else if (m[6] !== void 0) {
1812
+ ops.push({ op: "match", length: +m[6] });
1813
+ }
1814
+ }
1815
+ return ops;
1816
+ }
1817
+
1818
+ // src/tubeMap/reads.ts
1819
+ function canonicalStrand(node) {
1820
+ return node.id.endsWith("-") ? "-" : "+";
1821
+ }
1822
+ function stepName(node, strand) {
1823
+ return strand === canonicalStrand(node) ? node.id : `-${node.id}`;
1824
+ }
1825
+ function stepStarts(lengths, total) {
1826
+ const starts = lengths.map(() => void 0);
1827
+ let head = 0;
1828
+ let i = 0;
1829
+ for (; i < lengths.length && lengths[i] !== void 0; i++) {
1830
+ starts[i] = head;
1831
+ head += lengths[i];
1832
+ }
1833
+ let tail = total;
1834
+ for (let j = lengths.length - 1; j > i && lengths[j] !== void 0; j--) {
1835
+ tail -= lengths[j];
1836
+ starts[j] = tail;
1837
+ }
1838
+ return starts;
1839
+ }
1840
+ function mismatchesByStep(record, lengths) {
1841
+ const byStep = record.path.map(() => []);
1842
+ if (record.cs === void 0) {
1843
+ return byStep;
1844
+ }
1845
+ const starts = stepStarts(lengths, record.pathLength);
1846
+ const stepAt = (pos2, atEnd = false) => {
1847
+ for (let s = 0; s < starts.length; s++) {
1848
+ const start = starts[s];
1849
+ const length = lengths[s];
1850
+ if (start !== void 0 && length !== void 0 && start <= pos2 && (pos2 < start + length || atEnd && pos2 === start + length)) {
1851
+ return { s, local: pos2 - start, room: start + length - pos2 };
1852
+ }
1853
+ }
1854
+ return void 0;
1855
+ };
1856
+ const add = (s, mismatch) => {
1857
+ const list = byStep[s];
1858
+ const last = list.at(-1);
1859
+ if (mismatch.type === "substitution" && last?.type === "substitution" && last.pos + last.seq.length === mismatch.pos) {
1860
+ last.seq += mismatch.seq;
1861
+ } else {
1862
+ list.push(mismatch);
1863
+ }
1864
+ };
1865
+ let pos = record.pathStart;
1866
+ for (const op of parseCs(record.cs)) {
1867
+ if (op.op === "match") {
1868
+ pos += op.length;
1869
+ } else if (op.op === "substitution") {
1870
+ const at = stepAt(pos);
1871
+ if (at) {
1872
+ add(at.s, { type: "substitution", pos: at.local, seq: op.seq });
1873
+ }
1874
+ pos += 1;
1875
+ } else if (op.op === "insertion") {
1876
+ const at = stepAt(pos, pos === record.pathEnd);
1877
+ if (at) {
1878
+ add(at.s, { type: "insertion", pos: at.local, seq: op.seq });
1879
+ }
1880
+ } else {
1881
+ let left = op.length;
1882
+ while (left > 0) {
1883
+ const at = stepAt(pos);
1884
+ const take = at ? Math.min(left, at.room) : left;
1885
+ if (at) {
1886
+ add(at.s, { type: "deletion", pos: at.local, length: take });
1887
+ }
1888
+ pos += take;
1889
+ left -= take;
1890
+ }
1891
+ }
1892
+ }
1893
+ return byStep;
1894
+ }
1895
+ function tubeMapReads(graph, records, firstId) {
1896
+ const nodeByName = new Map(graph.nodes.map((n2) => [n2.name, n2]));
1897
+ const reads = [];
1898
+ for (const record of records) {
1899
+ const nodes = record.path.map((step) => nodeByName.get(step.name));
1900
+ const kept = nodes.flatMap((node, i) => node ? [i] : []);
1901
+ const first = kept[0];
1902
+ const last = kept.at(-1);
1903
+ if (first !== void 0 && last !== void 0) {
1904
+ const mismatches = mismatchesByStep(
1905
+ record,
1906
+ nodes.map((n2) => n2?.length)
1907
+ );
1908
+ const names = kept.map((i) => stepName(nodes[i], record.path[i].strand));
1909
+ const lastLength = nodes[last].length;
1910
+ reads.push({
1911
+ id: firstId + reads.length,
1912
+ name: record.name,
1913
+ type: "read",
1914
+ sourceTrackID: 1,
1915
+ sequence: names,
1916
+ sequenceNew: kept.map((i, k) => ({
1917
+ nodeName: names[k],
1918
+ mismatches: mismatches[i]
1919
+ })),
1920
+ firstNodeOffset: first === 0 ? record.pathStart : 0,
1921
+ finalNodeCoverLength: last === record.path.length - 1 ? lastLength - (record.pathLength - record.pathEnd) : lastLength,
1922
+ mapping_quality: record.mappingQuality === 255 ? 0 : record.mappingQuality,
1923
+ is_secondary: record.secondary,
1924
+ is_reverse: record.strand === "-"
1925
+ });
1926
+ }
1927
+ }
1928
+ return reads;
1929
+ }
1930
+ var FORWARD_READ_COLORS = ["#6baed6", "#4292c6", "#2171b5", "#08519c"];
1931
+ var REVERSE_READ_COLORS = ["#fb6a4a", "#ef3b2c", "#cb181d", "#a50f15"];
1932
+ function readColor(read) {
1933
+ const colors = read.is_reverse ? REVERSE_READ_COLORS : FORWARD_READ_COLORS;
1934
+ return colors[read.id % colors.length];
1935
+ }
1936
+
1937
+ // src/layout/tubeMapLayout.ts
1938
+ function visitStrands(graph) {
1939
+ const strands = /* @__PURE__ */ new Map();
1940
+ for (const [segment, visits] of graph.pathVisits ?? []) {
1941
+ for (const visit of visits) {
1942
+ const key = `${visit.path} ${segment}`;
1943
+ const list = strands.get(key);
1944
+ if (list) {
1945
+ list.push(visit.strand);
1946
+ } else {
1947
+ strands.set(key, [visit.strand]);
1948
+ }
1949
+ }
1950
+ }
1951
+ return strands;
1952
+ }
1953
+ function tubeMapTracks(graph) {
1954
+ const nodeById = new Map(graph.nodes.map((n2) => [n2.id, n2]));
1955
+ const strands = visitStrands(graph);
1956
+ const paths = (graph.paths ?? []).map((path, index) => ({ path, index }));
1957
+ const reference = paths.findIndex(
1958
+ (p) => pathOrigin(p.path.name).name === graph.referencePath
1959
+ );
1960
+ if (reference > 0) {
1961
+ paths.unshift(...paths.splice(reference, 1));
1962
+ }
1963
+ return paths.flatMap(({ path, index }) => {
1964
+ const visitName = pathOrigin(path.name).name;
1965
+ const seen = /* @__PURE__ */ new Map();
1966
+ const sequence = path.nodeIds.flatMap((id) => {
1967
+ const node = nodeById.get(id);
1968
+ if (!node) {
1969
+ return [];
1970
+ }
1971
+ const k = seen.get(node.name) ?? 0;
1972
+ seen.set(node.name, k + 1);
1973
+ const strand = strands.get(`${visitName} ${node.name}`)?.[k] ?? canonicalStrand(node);
1974
+ return [stepName(node, strand)];
1975
+ });
1976
+ return sequence.length > 0 ? [
1977
+ {
1978
+ id: index,
1979
+ name: path.name,
1980
+ sequence,
1981
+ sourceTrackID: 0,
1982
+ type: "haplotype"
1983
+ }
1984
+ ] : [];
1985
+ });
1986
+ }
1987
+ function tubeMapNodes(graph) {
1988
+ return graph.nodes.map((n2) => ({
1989
+ name: n2.id,
1990
+ seq: "",
1991
+ sequenceLength: Math.max(1, n2.length)
1992
+ }));
1993
+ }
1994
+ function hasTubeMapPaths(graph) {
1995
+ return (graph.paths?.length ?? 0) > 0;
1996
+ }
1997
+ var STACK_PX = 600;
1998
+ var MIN_TUBE_PX = 3;
1999
+ var MAX_TUBE_PX = 15;
2000
+ function tubeWidth(trackCount) {
2001
+ return Math.max(
2002
+ MIN_TUBE_PX,
2003
+ Math.min(MAX_TUBE_PX, Math.floor(STACK_PX / Math.max(1, trackCount)))
2004
+ );
2005
+ }
2006
+ function runTubeMap(graph) {
2007
+ const paths = graph.paths ?? [];
2008
+ const tracks = tubeMapTracks(graph);
2009
+ const reads = graph.reads ? tubeMapReads(graph, graph.reads, paths.length) : [];
2010
+ const tubeColor = reads.length > 0 ? pathGreyCssColor : pathCssColor;
2011
+ const pathColors = paths.map((_, i) => tubeColor(i, paths.length));
2012
+ const layout = layoutTubeMap(tubeMapNodes(graph), tracks, reads, {
2013
+ nodeWidthOption: "compressed",
2014
+ trackWidth: tubeWidth(tracks.length),
2015
+ trackColor: (track) => track.type === "read" ? readColor(track) : pathColors[track.id]
2016
+ });
2017
+ return layout ? { layout, pathColors } : void 0;
2018
+ }
2019
+ function drawnNodes(layout) {
2020
+ const nodes = [];
2021
+ layout.nodes.forEach((node) => {
2022
+ if (node.order >= 0) {
2023
+ nodes.push(node);
2024
+ }
2025
+ });
2026
+ return nodes;
2027
+ }
2028
+ function centreline(node, yOffset, x0, x1) {
2029
+ const y = node.y + node.contentHeight / 2 - yOffset;
2030
+ return [
2031
+ { x: x0, y },
2032
+ { x: x1, y }
2033
+ ];
2034
+ }
2035
+ function extentOf(layout, yOffset) {
2036
+ return {
2037
+ minY: layout.bounds.minY - yOffset,
2038
+ maxY: layout.bounds.maxY - yOffset
2039
+ };
2040
+ }
2041
+ function tubeMapLayout(graph) {
2042
+ const run = hasTubeMapPaths(graph) ? runTubeMap(graph) : void 0;
2043
+ if (!run) {
2044
+ return void 0;
2045
+ }
2046
+ const { layout, pathColors } = run;
2047
+ const yOffset = layout.bounds.minY;
2048
+ const nodePositions = {};
2049
+ for (const node of drawnNodes(layout)) {
2050
+ nodePositions[node.name] = centreline(
2051
+ node,
2052
+ yOffset,
2053
+ node.x,
2054
+ node.x + node.pixelWidth
2055
+ );
2056
+ }
2057
+ const { minY, maxY } = extentOf(layout, yOffset);
2058
+ return {
2059
+ nodePositions,
2060
+ tubeMap: { layout, yOffset, pathColors },
2061
+ extent: {
2062
+ minX: layout.bounds.minX,
2063
+ maxX: layout.bounds.maxX,
2064
+ minY,
2065
+ maxY
2066
+ }
2067
+ };
2068
+ }
2069
+ function tubeMapColumns(graph, layout) {
2070
+ const nodeById = new Map(graph.nodes.map((n2) => [n2.id, n2]));
2071
+ const byOrder = /* @__PURE__ */ new Map();
2072
+ for (const node of drawnNodes(layout)) {
2073
+ const list = byOrder.get(node.order);
2074
+ if (list) {
2075
+ list.push(node);
2076
+ } else {
2077
+ byOrder.set(node.order, [node]);
2078
+ }
2079
+ }
2080
+ const columns = [];
2081
+ let end = -Infinity;
2082
+ for (const order of [...byOrder.keys()].sort((a, b) => a - b)) {
2083
+ const nodes = byOrder.get(order);
2084
+ let bp0 = Infinity;
2085
+ let bp1 = -Infinity;
2086
+ for (const node of nodes) {
2087
+ const graphNode = nodeById.get(node.name);
2088
+ if (graphNode && isBackbone(graphNode)) {
2089
+ bp0 = Math.min(bp0, graphNode.stable.start);
2090
+ bp1 = Math.max(bp1, graphNode.stable.start + node.sequenceLength);
2091
+ }
2092
+ }
2093
+ if (bp0 === Infinity) {
2094
+ bp0 = bp1 = end;
2095
+ }
2096
+ if (bp0 < end) {
2097
+ bp0 = end;
2098
+ bp1 = Math.max(end, bp1);
2099
+ }
2100
+ end = Math.max(end, bp1);
2101
+ columns.push({
2102
+ order,
2103
+ x0: Math.min(...nodes.map((n2) => n2.x)),
2104
+ x1: Math.max(...nodes.map((n2) => n2.x + n2.pixelWidth)),
2105
+ bp0,
2106
+ bp1
2107
+ });
2108
+ }
2109
+ const first = columns.find((c) => c.bp0 !== -Infinity);
2110
+ for (const column of columns) {
2111
+ if (column.bp0 === -Infinity) {
2112
+ column.bp0 = column.bp1 = first?.bp0 ?? 0;
2113
+ }
2114
+ }
2115
+ return columns;
2116
+ }
2117
+ function pathsReachBackbone(graph) {
2118
+ return graph.nodes.some(isBackbone) && hasTubeMapPaths(graph);
2119
+ }
2120
+ function tubeMapReferenceLayout(graph) {
2121
+ const run = pathsReachBackbone(graph) ? runTubeMap(graph) : void 0;
2122
+ if (!run) {
2123
+ return void 0;
2124
+ }
2125
+ const { layout, pathColors } = run;
2126
+ const columns = tubeMapColumns(graph, layout);
2127
+ const byOrder = new Map(columns.map((c) => [c.order, c]));
2128
+ const yOffset = layout.bounds.minY;
2129
+ const nodePositions = {};
2130
+ for (const node of drawnNodes(layout)) {
2131
+ const column = byOrder.get(node.order);
2132
+ nodePositions[node.name] = centreline(node, yOffset, column.bp0, column.bp1);
2133
+ }
2134
+ return {
2135
+ nodePositions,
2136
+ tubeMap: { layout, yOffset, columns, pathColors },
2137
+ referenceAxis: true,
2138
+ pixelRows: true,
2139
+ extent: extentOf(layout, yOffset)
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/util/geometry.ts
2308
+ function svgPath(points) {
2309
+ const round = (v) => Math.round(v * 100) / 100;
2310
+ return points.map((p, i) => `${i ? "L" : "M"}${round(p.x)},${round(p.y)}`).join("");
2311
+ }
2312
+ function projectLine(x1, y1, x2, y2, distance) {
2313
+ const d = Math.hypot(y2 - y1, x2 - x1);
2314
+ if (d === 0) {
2315
+ return [x2, y2];
2316
+ }
2317
+ const vx = (x2 - x1) / d;
2318
+ const vy = (y2 - y1) / d;
2319
+ return [x2 + distance * vx, y2 + distance * vy];
2320
+ }
2321
+ function curvePointAt(c, t) {
2322
+ const u = 1 - t;
2323
+ const a = u * u * u;
2324
+ const b = 3 * u * u * t;
2325
+ const d = 3 * u * t * t;
2326
+ const e = t * t * t;
2327
+ return {
2328
+ x: a * c.x0 + b * c.cx0 + d * c.cx1 + e * c.x1,
2329
+ y: a * c.y0 + b * c.cy0 + d * c.cy1 + e * c.y1
2330
+ };
2331
+ }
2332
+ function curveMidpoint(curves) {
2333
+ const mid = curves[Math.floor(curves.length / 2)];
2334
+ return mid ? curvePointAt(mid, 0.5) : void 0;
2335
+ }
2336
+ function curveBounds(curves) {
2337
+ let minX = Infinity;
2338
+ let minY = Infinity;
2339
+ let maxX = -Infinity;
2340
+ let maxY = -Infinity;
2341
+ for (const c of curves) {
2342
+ minX = Math.min(minX, c.x0, c.cx0, c.cx1, c.x1);
2343
+ maxX = Math.max(maxX, c.x0, c.cx0, c.cx1, c.x1);
2344
+ minY = Math.min(minY, c.y0, c.cy0, c.cy1, c.y1);
2345
+ maxY = Math.max(maxY, c.y0, c.cy0, c.cy1, c.y1);
2346
+ }
2347
+ return { minX, minY, maxX, maxY };
2348
+ }
2349
+ function subCurve(c, t0, t1) {
2350
+ const p0 = curvePointAt(c, t0);
2351
+ const p1 = curvePointAt(c, t1);
2352
+ const dt = (t1 - t0) / 3;
2353
+ const d0 = curveTangentAt(c, t0);
2354
+ const d1 = curveTangentAt(c, t1);
2355
+ return {
2356
+ x0: p0.x,
2357
+ y0: p0.y,
2358
+ cx0: p0.x + d0.x * dt,
2359
+ cy0: p0.y + d0.y * dt,
2360
+ cx1: p1.x - d1.x * dt,
2361
+ cy1: p1.y - d1.y * dt,
2362
+ x1: p1.x,
2363
+ y1: p1.y
2364
+ };
2365
+ }
2366
+ function curveTangentAt(c, t) {
2367
+ const u = 1 - t;
2368
+ const a = 3 * u * u;
2369
+ const b = 6 * u * t;
2370
+ const d = 3 * t * t;
2371
+ return {
2372
+ x: a * (c.cx0 - c.x0) + b * (c.cx1 - c.cx0) + d * (c.x1 - c.cx1),
2373
+ y: a * (c.cy0 - c.y0) + b * (c.cy1 - c.cy0) + d * (c.y1 - c.cy1)
2374
+ };
2375
+ }
2376
+ function dashCurves(curves, dashWorld) {
2377
+ const out = [];
2378
+ for (const c of curves) {
2379
+ 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);
2380
+ const spans = Math.max(3, Math.round(polyLen / dashWorld) | 1);
2381
+ for (let i = 0; i < spans; i += 2) {
2382
+ out.push([subCurve(c, i / spans, (i + 1) / spans)]);
2383
+ }
2384
+ }
2385
+ return out;
2386
+ }
2387
+ function translateCurves(curves, dx, dy) {
2388
+ const out = new Array(curves.length);
2389
+ for (let i = 0; i < curves.length; i++) {
2390
+ const c = curves[i];
2391
+ out[i] = {
2392
+ x0: c.x0 + dx,
2393
+ y0: c.y0 + dy,
2394
+ cx0: c.cx0 + dx,
2395
+ cy0: c.cy0 + dy,
2396
+ cx1: c.cx1 + dx,
2397
+ cy1: c.cy1 + dy,
2398
+ x1: c.x1 + dx,
2399
+ y1: c.y1 + dy
2400
+ };
2401
+ }
2402
+ return out;
2403
+ }
2404
+ function attachment(segments, side) {
2405
+ const last = segments.length - 1;
2406
+ return side === "end" ? { at: segments[last], inward: segments[last - 1] } : { at: segments[0], inward: segments[1] };
2407
+ }
2408
+ var TANGENT_SPAN_FACTOR = 0.5;
2409
+ var MAX_TANGENT = 80;
2410
+ function spanOf(segments) {
2411
+ let min = Infinity;
2412
+ let max = -Infinity;
2413
+ for (const s of segments) {
2414
+ min = Math.min(min, s.x);
2415
+ max = Math.max(max, s.x);
2416
+ }
2417
+ return { min, max };
2418
+ }
2419
+ function distanceToSpan(x, span) {
2420
+ return Math.max(0, span.min - x, x - span.max);
2421
+ }
2422
+ function facingSide(segments, other, tieBreak) {
2423
+ const startGap = distanceToSpan(segments[0].x, other);
2424
+ const endGap = distanceToSpan(segments[segments.length - 1].x, other);
2425
+ return startGap === endGap ? tieBreak : startGap < endGap ? "start" : "end";
2426
+ }
2427
+ function facingSides(fromSegments, toSegments) {
2428
+ return {
2429
+ from: facingSide(fromSegments, spanOf(toSegments), "end"),
2430
+ to: facingSide(toSegments, spanOf(fromSegments), "start")
2431
+ };
2432
+ }
2433
+ var APEX_FRACTION = 0.75;
2434
+ var BOW_CLEARANCE = 1.15;
2435
+ var ALONG_FRACTION = 0.35;
2436
+ var MAX_ROW_BOW_PX = 3 * ROW_HEIGHT_PX;
2437
+ function bowAround(p1x, p1y, p2x, p2y, toward, maxBow) {
2438
+ const chord = Math.hypot(p2x - p1x, p2y - p1y);
2439
+ const ux = chord === 0 ? 1 : (p2x - p1x) / chord;
2440
+ const uy = chord === 0 ? 0 : (p2y - p1y) / chord;
2441
+ let reach = 0;
2442
+ let minAlong = Infinity;
2443
+ let maxAlong = -Infinity;
2444
+ for (const p of toward) {
2445
+ const dx = p.x - p1x;
2446
+ const dy = p.y - p1y;
2447
+ const perp = dx * -uy + dy * ux;
2448
+ if (Math.abs(perp) > Math.abs(reach)) {
2449
+ reach = perp;
2450
+ }
2451
+ const along = dx * ux + dy * uy;
2452
+ minAlong = Math.min(minAlong, along);
2453
+ maxAlong = Math.max(maxAlong, along);
2454
+ }
2455
+ const span = toward.length ? maxAlong - minAlong : 0;
2456
+ const uncapped = Math.max(
2457
+ Math.abs(reach) * BOW_CLEARANCE / APEX_FRACTION,
2458
+ span * ALONG_FRACTION
2459
+ );
2460
+ const size = maxBow === void 0 ? uncapped : Math.min(uncapped, maxBow);
2461
+ return reach < 0 ? -size : size;
2462
+ }
2463
+ function yToXOf({ scaleX, scaleY }) {
2464
+ return scaleY / scaleX;
2465
+ }
2466
+ var PATH_RIBBON_SPACING = 3;
2467
+ function drawnDirection(a, b, yToX) {
2468
+ const dx = b.x - a.x;
2469
+ const dy = (b.y - a.y) * yToX;
2470
+ const len = Math.hypot(dx, dy);
2471
+ return len === 0 ? void 0 : { x: dx / len, y: dy / len };
2472
+ }
2473
+ function pathRibbonOffsets(fromSegments, toSegments, count, axis, spacing = PATH_RIBBON_SPACING) {
2474
+ const yToX = yToXOf(axis);
2475
+ const dir = drawnDirection(
2476
+ fromSegments[fromSegments.length - 1],
2477
+ toSegments[0],
2478
+ yToX
2479
+ ) ?? drawnDirection(fromSegments[0], fromSegments.at(-1), yToX) ?? drawnDirection(toSegments[0], toSegments.at(-1), yToX) ?? { x: 1, y: 0 };
2480
+ const offsets = [];
2481
+ for (let i = 0; i < count; i++) {
2482
+ const distance = (i - (count - 1) / 2) * spacing;
2483
+ offsets.push({ x: -dir.y * distance, y: dir.x * distance / yToX });
2484
+ }
2485
+ return offsets;
2486
+ }
2487
+ function scaleYOf(points, yToX) {
2488
+ return points.map((p) => ({ x: p.x, y: p.y * yToX }));
2489
+ }
2490
+ function unscaleYOf(curves, yToX) {
2491
+ for (const c of curves) {
2492
+ c.y0 /= yToX;
2493
+ c.cy0 /= yToX;
2494
+ c.cy1 /= yToX;
2495
+ c.y1 /= yToX;
2496
+ }
2497
+ return curves;
2498
+ }
2499
+ var SELF_LOOP_MIN_PX = 12;
2500
+ var SELF_LOOP_MAX_PX = 40;
2501
+ function selfLoopReach(segments, scale) {
2502
+ let length = 0;
2503
+ for (let i = 1; i < segments.length; i++) {
2504
+ length += Math.hypot(
2505
+ segments[i].x - segments[i - 1].x,
2506
+ segments[i].y - segments[i - 1].y
2507
+ );
2508
+ }
2509
+ const nodePx = length * scale;
2510
+ const reachPx = Math.min(
2511
+ SELF_LOOP_MAX_PX,
2512
+ Math.max(SELF_LOOP_MIN_PX, nodePx / 2)
2513
+ );
2514
+ return reachPx / Math.max(scale, Number.EPSILON);
2515
+ }
2516
+ function selfLinkOf(edge) {
2517
+ if (edge.from !== edge.to) {
2518
+ return false;
2519
+ }
2520
+ return edge.fromStrand && edge.toStrand && edge.fromStrand !== edge.toStrand ? edge.fromStrand === "+" ? "end" : "start" : true;
2521
+ }
2522
+ function hairpinCurves(attach, reach, offsetX, offsetY) {
2523
+ const { at, inward } = attach;
2524
+ const dx = inward ? at.x - inward.x : 1;
2525
+ const dy = inward ? at.y - inward.y : 0;
2526
+ const len = Math.hypot(dx, dy) || 1;
2527
+ const ux = dx / len;
2528
+ const uy = dy / len;
2529
+ const px = at.x + offsetX;
2530
+ const py = at.y + offsetY;
2531
+ const nearX = px + ux * reach / 3;
2532
+ const nearY = py + uy * reach / 3;
2533
+ const apexX = px + ux * reach;
2534
+ const apexY = py + uy * reach;
2535
+ const sideX = -uy * reach / 2;
2536
+ const sideY = ux * reach / 2;
2537
+ return [
2538
+ {
2539
+ x0: px,
2540
+ y0: py,
2541
+ cx0: nearX,
2542
+ cy0: nearY,
2543
+ cx1: apexX + sideX,
2544
+ cy1: apexY + sideY,
2545
+ x1: apexX,
2546
+ y1: apexY
2547
+ },
2548
+ {
2549
+ x0: apexX,
2550
+ y0: apexY,
2551
+ cx0: apexX - sideX,
2552
+ cy0: apexY - sideY,
2553
+ cx1: nearX,
2554
+ cy1: nearY,
2555
+ x1: px,
2556
+ y1: py
2557
+ }
2558
+ ];
2559
+ }
2560
+ function computeEdgeCurves(fromSegments, toSegments, selfLink, offsetX, offsetY, axis, bypassed = [], maxBowIn) {
2561
+ const scale = axis.scaleX;
2562
+ const yToX = yToXOf(axis);
2563
+ const maxBow = maxBowIn ?? (axis.pixelRows ? MAX_ROW_BOW_PX : void 0);
2564
+ if (yToX !== 1) {
2565
+ return unscaleYOf(
2566
+ computeEdgeCurves(
2567
+ scaleYOf(fromSegments, yToX),
2568
+ scaleYOf(toSegments, yToX),
2569
+ selfLink,
2570
+ offsetX,
2571
+ offsetY * yToX,
2572
+ { scaleX: scale, scaleY: scale },
2573
+ scaleYOf(bypassed, yToX),
2574
+ // the cap is a y measure and the body works in x units, so it converts
2575
+ // with everything else that crosses this boundary
2576
+ maxBow === void 0 ? void 0 : maxBow * yToX
2577
+ ),
2578
+ yToX
2579
+ );
2580
+ }
2581
+ if (typeof selfLink === "string") {
2582
+ return hairpinCurves(
2583
+ attachment(fromSegments, selfLink),
2584
+ selfLoopReach(fromSegments, scale),
2585
+ offsetX,
2586
+ offsetY
2587
+ );
2588
+ }
2589
+ const sides = selfLink ? { from: "end", to: "start" } : facingSides(fromSegments, toSegments);
2590
+ const fromAttach = attachment(fromSegments, sides.from);
2591
+ const toAttach = attachment(toSegments, sides.to);
2592
+ const fromEnd = fromAttach.at;
2593
+ const toStart = toAttach.at;
2594
+ const p1x = fromEnd.x + offsetX;
2595
+ const p1y = fromEnd.y + offsetY;
2596
+ const p2x = toStart.x + offsetX;
2597
+ const p2y = toStart.y + offsetY;
2598
+ if (selfLink) {
2599
+ let segDirX = 1;
2600
+ let segDirY = 0;
2601
+ if (fromAttach.inward) {
2602
+ const dx = fromEnd.x - fromAttach.inward.x;
2603
+ const dy = fromEnd.y - fromAttach.inward.y;
2604
+ const len = Math.hypot(dx, dy);
2605
+ if (len > 0) {
2606
+ segDirX = dx / len;
2607
+ segDirY = dy / len;
2608
+ }
2609
+ }
2610
+ const ext = selfLoopReach(fromSegments, scale);
2611
+ const perpX = -segDirY;
2612
+ const perpY = segDirX;
2613
+ const midX = (fromEnd.x + toStart.x) / 2 + offsetX + perpX * ext;
2614
+ const midY = (fromEnd.y + toStart.y) / 2 + offsetY + perpY * ext;
2615
+ const cp1x = p1x + segDirX * ext;
2616
+ const cp1y = p1y + segDirY * ext;
2617
+ const cp2x = p2x - segDirX * ext;
2618
+ const cp2y = p2y - segDirY * ext;
2619
+ return [
2620
+ {
2621
+ x0: p1x,
2622
+ y0: p1y,
2623
+ cx0: cp1x,
2624
+ cy0: cp1y,
2625
+ cx1: cp1x + perpX * ext,
2626
+ cy1: cp1y + perpY * ext,
2627
+ x1: midX,
2628
+ y1: midY
2629
+ },
2630
+ {
2631
+ x0: midX,
2632
+ y0: midY,
2633
+ cx0: cp2x + perpX * ext,
2634
+ cy0: cp2y + perpY * ext,
2635
+ cx1: cp2x,
2636
+ cy1: cp2y,
2637
+ x1: p2x,
2638
+ y1: p2y
2639
+ }
2640
+ ];
2641
+ } else {
2642
+ const fromPrev = fromAttach.inward ?? {
2643
+ x: fromEnd.x - (toStart.x - fromEnd.x),
2644
+ y: fromEnd.y - (toStart.y - fromEnd.y)
2645
+ };
2646
+ const toNext = toAttach.inward ?? {
2647
+ x: toStart.x - (fromEnd.x - toStart.x),
2648
+ y: toStart.y - (fromEnd.y - toStart.y)
2649
+ };
2650
+ const dist = Math.hypot(p2x - p1x, p2y - p1y);
2651
+ const tangentProj = (prev, at, towardX, towardY) => {
2652
+ const dx = at.x - prev.x;
2653
+ const dy = at.y - prev.y;
2654
+ const len = Math.hypot(dx, dy);
2655
+ const along = len === 0 ? 0 : (towardX * dx + towardY * dy) / len;
2656
+ return Math.max(
2657
+ 0,
2658
+ Math.min(dist * TANGENT_SPAN_FACTOR, MAX_TANGENT, along * 0.5)
2659
+ );
2660
+ };
2661
+ const towardToX = toStart.x - fromEnd.x;
2662
+ const towardToY = toStart.y - fromEnd.y;
2663
+ const [cx1, cy1] = projectLine(
2664
+ fromPrev.x,
2665
+ fromPrev.y,
2666
+ fromEnd.x,
2667
+ fromEnd.y,
2668
+ tangentProj(fromPrev, fromEnd, towardToX, towardToY)
2669
+ );
2670
+ const [cx2, cy2] = projectLine(
2671
+ toNext.x,
2672
+ toNext.y,
2673
+ toStart.x,
2674
+ toStart.y,
2675
+ tangentProj(toNext, toStart, -towardToX, -towardToY)
2676
+ );
2677
+ const bulge = bypassed.length ? bowAround(p1x, p1y, p2x, p2y, bypassed, maxBow) : 0;
2678
+ const chordLen = dist === 0 ? 1 : dist;
2679
+ const ux = (p2x - p1x) / chordLen;
2680
+ const uy = (p2y - p1y) / chordLen;
2681
+ const bulgeX = -uy * bulge;
2682
+ const bulgeY = ux * bulge;
2683
+ const spreadX = ux * Math.abs(bulge);
2684
+ const spreadY = uy * Math.abs(bulge);
2685
+ return [
2686
+ {
2687
+ x0: p1x,
2688
+ y0: p1y,
2689
+ cx0: cx1 + offsetX + bulgeX - spreadX,
2690
+ cy0: cy1 + offsetY + bulgeY - spreadY,
2691
+ cx1: cx2 + offsetX + bulgeX + spreadX,
2692
+ cy1: cy2 + offsetY + bulgeY + spreadY,
2693
+ x1: p2x,
2694
+ y1: p2y
2695
+ }
2696
+ ];
2697
+ }
2698
+ }
2699
+
2700
+ // src/deletionEdges.ts
2701
+ var MIN_DELETION_BP2 = 1;
2702
+ function bypassedPoints(nodePositions, bypassed) {
2703
+ return bypassed.flatMap((id) => nodePositions[id] ?? []);
2704
+ }
2705
+ function deletionArcCurves(nodePositions, deletion, axis) {
2706
+ const from = nodePositions[deletion.from];
2707
+ const to = nodePositions[deletion.to];
2708
+ return from?.length && to?.length ? computeEdgeCurves(
2709
+ from,
2710
+ to,
2711
+ deletion.from === deletion.to,
2712
+ 0,
2713
+ 0,
2714
+ axis,
2715
+ bypassedPoints(nodePositions, deletion.bypassed)
2716
+ ) : void 0;
2717
+ }
2718
+ function backboneBySequence(graph) {
2719
+ const bySequence = /* @__PURE__ */ new Map();
2720
+ for (const node of graph.nodes) {
2721
+ if (isBackbone(node)) {
2722
+ const list = bySequence.get(node.stable.refName);
2723
+ if (list) {
2724
+ list.push(node);
2725
+ } else {
2726
+ bySequence.set(node.stable.refName, [node]);
2727
+ }
2728
+ }
2729
+ }
2730
+ for (const list of bySequence.values()) {
2731
+ list.sort((a, b) => a.stable.start - b.stable.start);
2732
+ }
2733
+ return bySequence;
2734
+ }
2735
+ function bypassedNodes(nodes, start, end) {
2736
+ const bypassed = [];
2737
+ if (nodes) {
2738
+ for (let i = firstNodeAtOrAfter(nodes, start); i < nodes.length; i++) {
2739
+ const node = nodes[i];
2740
+ if (node.stable.start >= end) {
2741
+ break;
2742
+ }
2743
+ if (node.stable.start + node.length <= end) {
2744
+ bypassed.push(node.id);
2745
+ }
2746
+ }
2747
+ }
2748
+ return bypassed;
2749
+ }
2750
+ function skipsBetween(edge, from, to, forwards) {
2751
+ const { fromStrand, toStrand } = edge;
2752
+ if (fromStrand === void 0 || toStrand === void 0) {
2753
+ return true;
2754
+ }
2755
+ const leavesRight = fromStrand === (from.stable.strand ?? "+");
2756
+ const arrivesLeft = toStrand === (to.stable.strand ?? "+");
2757
+ return forwards ? leavesRight && arrivesLeft : !leavesRight && !arrivesLeft;
2758
+ }
2759
+ function deletionEdges(graph) {
2760
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
2761
+ const backbone = backboneBySequence(graph);
2762
+ const found = [];
2763
+ for (let edgeIndex = 0; edgeIndex < graph.edges.length; edgeIndex++) {
2764
+ const edge = graph.edges[edgeIndex];
2765
+ const from = byId.get(edge.from);
2766
+ const to = byId.get(edge.to);
2767
+ if (from !== void 0 && to !== void 0 && isBackbone(from) && isBackbone(to) && from.stable.refName === to.stable.refName) {
2768
+ const forwards = from.stable.start <= to.stable.start;
2769
+ const [left, right] = forwards ? [from, to] : [to, from];
2770
+ const start = left.stable.start + left.length;
2771
+ const end = right.stable.start;
2772
+ const bp = end - start;
2773
+ if (bp >= MIN_DELETION_BP2 && skipsBetween(edge, from, to, forwards)) {
2774
+ const refName = left.stable.refName;
2775
+ found.push({
2776
+ edgeIndex,
2777
+ from: edge.from,
2778
+ to: edge.to,
2779
+ refName,
2780
+ start,
2781
+ end,
2782
+ bp,
2783
+ bypassed: bypassedNodes(backbone.get(refName), start, end)
2784
+ });
2785
+ }
2786
+ }
2787
+ }
2788
+ return found;
2789
+ }
2790
+
2791
+ // src/renderer/GeometryBuilder.ts
2792
+ import { packAbgr as packAbgr2 } from "@jbrowse/core/util/colorBits";
2793
+
2794
+ // src/renderer/colorBits.ts
2795
+ import {
2796
+ abgrAlpha,
2797
+ abgrBlue,
2798
+ abgrGreen,
2799
+ abgrRed,
2800
+ packAbgr
2801
+ } from "@jbrowse/core/util/colorBits";
2802
+ function fadeAbgr(c, alpha) {
2803
+ return packAbgr(
2804
+ abgrRed(c),
2805
+ abgrGreen(c),
2806
+ abgrBlue(c),
2807
+ Math.round(abgrAlpha(c) * alpha)
2808
+ );
2809
+ }
2810
+ function brightenAbgr(c, factor) {
2811
+ return packAbgr(
2812
+ Math.min(255, Math.round(abgrRed(c) * factor)),
2813
+ Math.min(255, Math.round(abgrGreen(c) * factor)),
2814
+ Math.min(255, Math.round(abgrBlue(c) * factor)),
2815
+ abgrAlpha(c)
2816
+ );
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 n2 = 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[n2 - 1], (y - walkY[n2 - 1]) * yToX) * scale;
2906
+ walkX[n2] = x;
2907
+ walkY[n2] = y;
2908
+ walkPx[n2] = px;
2909
+ n2++;
2910
+ }
2911
+ }
2912
+ return n2;
2913
+ }
2914
+ function walkPointAt(n2, px) {
2915
+ let i = 1;
2916
+ while (i < n2 - 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 n2 = backFromEnd(curves, scale, yToX, nodeHalfWidth + length);
2939
+ const lengthPx = walkPx[n2 - 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(n2, inset);
2947
+ const base = walkPointAt(n2, 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 n2 of graph.nodes) {
3140
+ if (n2.stable !== void 0 && n2.stable.rank > maxRank) {
3141
+ maxRank = n2.stable.rank;
3142
+ }
3143
+ if (n2.depth < minDepth) {
3144
+ minDepth = n2.depth;
3145
+ }
3146
+ if (n2.depth > maxDepth) {
3147
+ maxDepth = n2.depth;
3148
+ }
3149
+ if (n2.length < minLength) {
3150
+ minLength = n2.length;
3151
+ }
3152
+ if (n2.length > maxLength) {
3153
+ maxLength = n2.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
+ 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
+ }
3715
+
3716
+ // src/overlayLabels.ts
3717
+ var LABEL_PX = 11;
3718
+ var LABEL_CHAR_PX = 6.2;
3719
+ var LABEL_PAD = 4;
3720
+ function labelWidth(text) {
3721
+ return text.length * LABEL_CHAR_PX + LABEL_PAD * 2;
3722
+ }
3723
+ function overlaps(a, b) {
3724
+ return a.x0 < b.x1 && a.x1 > b.x0 && a.y0 < b.y1 && a.y1 > b.y0;
3725
+ }
3726
+ function offFrame(box, frame) {
3727
+ return box.x1 <= 0 || box.x0 >= frame.width || box.y1 <= 0 || box.y0 >= frame.height;
3728
+ }
3729
+ function insideFrame(box, frame) {
3730
+ return box.x0 >= 0 && box.x1 <= frame.width && box.y0 >= 0 && box.y1 <= frame.height;
3731
+ }
3732
+ function occupancy(frame, reserved = []) {
3733
+ const placed = [...reserved];
3734
+ return (box) => {
3735
+ if (offFrame(box, frame) || placed.some((p) => overlaps(p, box))) {
3736
+ return false;
3737
+ }
3738
+ placed.push(box);
3739
+ return true;
3740
+ };
3741
+ }
3742
+ function placeLabels(candidates, frame, take) {
3743
+ const out = [];
3744
+ const row = LABEL_PX + LABEL_PAD * 2;
3745
+ for (const c of candidates) {
3746
+ const w = labelWidth(c.text);
3747
+ const direction = c.y > frame.height / 2 ? -1 : 1;
3748
+ for (let tries = 0; tries <= (c.stack ?? 0); tries++) {
3749
+ const y = c.y + direction * tries * row;
3750
+ const box = {
3751
+ x0: c.x - w / 2,
3752
+ x1: c.x + w / 2,
3753
+ y0: y - LABEL_PX - LABEL_PAD,
3754
+ y1: y + LABEL_PAD
3755
+ };
3756
+ if (offFrame(box, frame)) {
3757
+ break;
3758
+ }
3759
+ if (insideFrame(box, frame) && take(box)) {
3760
+ out.push({ ...c, y, w });
3761
+ break;
3762
+ }
3763
+ }
3764
+ }
3765
+ return out;
3766
+ }
3767
+
3768
+ // src/graphLabels.ts
3769
+ var MIN_DELETION_LABEL_PX = 26;
3770
+ var PX_PER_NODE_LABEL = 4e4;
3771
+ var MIN_NODE_LABELS = 8;
3772
+ var LEADER_GAP_PX = 8;
3773
+ var LABEL_CHAR_PX2 = 5.7;
3774
+ var LABEL_PAD_PX = 6;
3775
+ var LABEL_HEIGHT_PX = 13;
3776
+ var LABEL_GAP_PX = 3;
3777
+ var MAX_LABEL_OVERHANG = 2;
3778
+ function labelHalfWidth(text) {
3779
+ return (text.length * LABEL_CHAR_PX2 + LABEL_PAD_PX + LABEL_GAP_PX) / 2;
3780
+ }
3781
+ var LABEL_HALF_HEIGHT = (LABEL_HEIGHT_PX + LABEL_GAP_PX) / 2;
3782
+ var MIN_LABEL_HALF_WIDTH = labelHalfWidth("1 bp");
3783
+ function boxAt(halfW, x, y) {
3784
+ return {
3785
+ x0: x - halfW,
3786
+ x1: x + halfW,
3787
+ y0: y - LABEL_HALF_HEIGHT,
3788
+ y1: y + LABEL_HALF_HEIGHT
3789
+ };
3790
+ }
3791
+ function rowLabelBox(text, screenY) {
3792
+ return {
3793
+ x0: 6,
3794
+ x1: 6 + text.length * 6.2 + 8 + LABEL_GAP_PX,
3795
+ y0: screenY - 8 - LABEL_GAP_PX / 2,
3796
+ y1: screenY + 8 + LABEL_GAP_PX / 2
3797
+ };
3798
+ }
3799
+ function clamp(v, lo, hi) {
3800
+ return hi < lo ? v : Math.min(Math.max(v, lo), hi);
3801
+ }
3802
+ function formatBp(bp) {
3803
+ if (bp >= 1e6) {
3804
+ return `${+(bp / 1e6).toFixed(1)} Mb`;
3805
+ }
3806
+ return bp >= 1e3 ? `${+(bp / 1e3).toFixed(1)} kb` : `${bp} bp`;
3807
+ }
3808
+ function deletionText(bp) {
3809
+ return `${formatBp(bp)} del`;
3810
+ }
3811
+ function arcOutward(curves, apex, scaleX, scaleY) {
3812
+ const first = curves[0];
3813
+ const last = curves[curves.length - 1];
3814
+ const dx = (apex.x - (first.x0 + last.x1) / 2) * scaleX;
3815
+ const dy = (apex.y - (first.y0 + last.y1) / 2) * scaleY;
3816
+ const len = Math.hypot(dx, dy);
3817
+ return len === 0 ? { x: 0, y: -1 } : { x: dx / len, y: dy / len };
3818
+ }
3819
+ function boxSupport(box, dir) {
3820
+ return Math.abs(dir.x) * (box.x1 - box.x0) / 2 + Math.abs(dir.y) * (box.y1 - box.y0) / 2;
3821
+ }
3822
+ function boxRayCrossing(box, dir) {
3823
+ const halfW = (box.x1 - box.x0) / 2;
3824
+ const halfH = (box.y1 - box.y0) / 2;
3825
+ return Math.min(
3826
+ dir.x === 0 ? Infinity : halfW / Math.abs(dir.x),
3827
+ dir.y === 0 ? Infinity : halfH / Math.abs(dir.y)
3828
+ );
3829
+ }
3830
+ function drawnLength(segments, scaleX, scaleY) {
3831
+ let total = 0;
3832
+ for (let i = 1; i < segments.length; i++) {
3833
+ const a = segments[i - 1];
3834
+ const b = segments[i];
3835
+ total += Math.hypot((b.x - a.x) * scaleX, (b.y - a.y) * scaleY);
3836
+ }
3837
+ return total;
3838
+ }
3839
+ function runBounds(nodeIds, nodePositions) {
3840
+ let minX = Infinity;
3841
+ let minY = Infinity;
3842
+ let maxX = -Infinity;
3843
+ let maxY = -Infinity;
3844
+ for (const id of nodeIds) {
3845
+ for (const p of nodePositions[id] ?? []) {
3846
+ minX = Math.min(minX, p.x);
3847
+ maxX = Math.max(maxX, p.x);
3848
+ minY = Math.min(minY, p.y);
3849
+ maxY = Math.max(maxY, p.y);
3850
+ }
3851
+ }
3852
+ return minX === Infinity ? void 0 : { minX, minY, maxX, maxY };
3853
+ }
3854
+ var nodeOrderCache = /* @__PURE__ */ new WeakMap();
3855
+ function labelOrder(nodePositions, nodeLengths) {
3856
+ const cached2 = nodeOrderCache.get(nodePositions);
3857
+ if (cached2?.nodeLengths === nodeLengths) {
3858
+ return cached2.order;
3859
+ }
3860
+ const order = Object.entries(nodePositions).filter(([id, segments]) => segments.length > 0 && nodeLengths.has(id)).sort(([a], [b]) => nodeLengths.get(b) - nodeLengths.get(a));
3861
+ nodeOrderCache.set(nodePositions, { nodeLengths, order });
3862
+ return order;
3863
+ }
3864
+ var arcCache = /* @__PURE__ */ new WeakMap();
3865
+ function arcPlacements(nodePositions, deletions, axis, version) {
3866
+ const { scaleX, scaleY } = axis;
3867
+ const cached2 = arcCache.get(nodePositions);
3868
+ if (cached2?.deletions === deletions && cached2.scaleX === scaleX && cached2.scaleY === scaleY && cached2.version === version) {
3869
+ return cached2.arcs;
3870
+ }
3871
+ const arcs = [];
3872
+ for (const deletion of [...deletions].sort((a, b) => b.bp - a.bp)) {
3873
+ const curves = deletionArcCurves(nodePositions, deletion, axis);
3874
+ const apex = curves ? curveMidpoint(curves) : void 0;
3875
+ if (curves && apex) {
3876
+ const { minX, minY, maxX, maxY } = curveBounds(curves);
3877
+ arcs.push({
3878
+ deletion,
3879
+ curves,
3880
+ apex,
3881
+ // in screen px, so the two axes' units cannot be added by accident
3882
+ extent: Math.max((maxX - minX) * scaleX, (maxY - minY) * scaleY)
3883
+ });
3884
+ }
3885
+ }
3886
+ arcCache.set(nodePositions, { deletions, scaleX, scaleY, version, arcs });
3887
+ return arcs;
3888
+ }
3889
+ function sizeLabelCandidates({
3890
+ nodePositions,
3891
+ nodeLengths,
3892
+ deletions,
3893
+ alleleDeletions,
3894
+ axis,
3895
+ translateX,
3896
+ translateY,
3897
+ width,
3898
+ height,
3899
+ version = 0
3900
+ }) {
3901
+ const { scaleX, scaleY } = axis;
3902
+ const deletionCandidates = [];
3903
+ const nodeCandidates = [];
3904
+ for (const { deletion, curves, apex, extent } of arcPlacements(
3905
+ nodePositions,
3906
+ deletions,
3907
+ axis,
3908
+ version
3909
+ )) {
3910
+ if (extent >= MIN_DELETION_LABEL_PX) {
3911
+ const text = deletionText(deletion.bp);
3912
+ const halfW = labelHalfWidth(text);
3913
+ const box = boxAt(halfW, 0, 0);
3914
+ const arcX = apex.x * scaleX + translateX;
3915
+ const arcY = apex.y * scaleY + translateY;
3916
+ const fits = extent >= halfW * 2 / MAX_LABEL_OVERHANG;
3917
+ const bow = arcOutward(curves, apex, scaleX, scaleY);
3918
+ const offset = boxSupport(box, bow) + LEADER_GAP_PX;
3919
+ const x = fits ? arcX : clamp(arcX + bow.x * offset, halfW, width - halfW);
3920
+ const y = fits ? arcY : clamp(
3921
+ arcY + bow.y * offset,
3922
+ LABEL_HALF_HEIGHT,
3923
+ height - LABEL_HALF_HEIGHT
3924
+ );
3925
+ const away = Math.hypot(x - arcX, y - arcY);
3926
+ let leader;
3927
+ if (away > 0) {
3928
+ const dir = { x: (x - arcX) / away, y: (y - arcY) / away };
3929
+ const crossing = boxRayCrossing(box, dir);
3930
+ leader = {
3931
+ arcX,
3932
+ arcY,
3933
+ labelX: x - dir.x * crossing,
3934
+ labelY: y - dir.y * crossing
3935
+ };
3936
+ }
3937
+ deletionCandidates.push({
3938
+ label: {
3939
+ key: `del:${deletion.edgeIndex}`,
3940
+ text,
3941
+ x,
3942
+ y,
3943
+ kind: "deletion",
3944
+ leader
3945
+ },
3946
+ box: boxAt(halfW, x, y)
3947
+ });
3948
+ }
3949
+ }
3950
+ const labelledByDeletion = /* @__PURE__ */ new Set();
3951
+ const alleleRuns = [...alleleDeletions ?? []].sort((a, b) => b.bp - a.bp);
3952
+ for (const run of alleleRuns) {
3953
+ const drawn = runBounds(run.nodeIds, nodePositions);
3954
+ if (!drawn) {
3955
+ continue;
3956
+ }
3957
+ const shownLeft = Math.max(drawn.minX * scaleX + translateX, 0);
3958
+ const shownRight = Math.min(drawn.maxX * scaleX + translateX, width);
3959
+ const shownTop = Math.max(drawn.minY * scaleY + translateY, 0);
3960
+ const shownBottom = Math.min(drawn.maxY * scaleY + translateY, height);
3961
+ if (shownRight < shownLeft || shownBottom < shownTop) {
3962
+ continue;
3963
+ }
3964
+ const extent = Math.max(shownRight - shownLeft, shownBottom - shownTop);
3965
+ const text = deletionText(run.bp);
3966
+ const halfW = labelHalfWidth(text);
3967
+ if (extent >= halfW * 2 / MAX_LABEL_OVERHANG) {
3968
+ for (const id of run.nodeIds) {
3969
+ labelledByDeletion.add(id);
3970
+ }
3971
+ const x = clamp((shownLeft + shownRight) / 2, halfW, width - halfW);
3972
+ const y = clamp(
3973
+ (shownTop + shownBottom) / 2,
3974
+ LABEL_HALF_HEIGHT,
3975
+ height - LABEL_HALF_HEIGHT
3976
+ );
3977
+ deletionCandidates.push({
3978
+ label: {
3979
+ key: `alleledel:${run.nodeIds.join(",")}`,
3980
+ text,
3981
+ x,
3982
+ y,
3983
+ kind: "deletion"
3984
+ },
3985
+ box: boxAt(halfW, x, y)
3986
+ });
3987
+ }
3988
+ }
3989
+ for (const [id, segments] of labelOrder(nodePositions, nodeLengths)) {
3990
+ if (labelledByDeletion.has(id)) {
3991
+ continue;
3992
+ }
3993
+ const { x, y } = polylineMidpoint(segments);
3994
+ const screenY = y * scaleY + translateY;
3995
+ if (screenY + LABEL_HALF_HEIGHT <= 0 || screenY - LABEL_HALF_HEIGHT >= height) {
3996
+ continue;
3997
+ }
3998
+ const drawn = drawnLength(segments, scaleX, scaleY);
3999
+ if (drawn < MIN_LABEL_HALF_WIDTH * 2 / MAX_LABEL_OVERHANG) {
4000
+ continue;
4001
+ }
4002
+ const text = formatBp(nodeLengths.get(id));
4003
+ const halfW = labelHalfWidth(text);
4004
+ if (drawn >= halfW * 2 / MAX_LABEL_OVERHANG) {
4005
+ const screenX = x * scaleX + translateX;
4006
+ nodeCandidates.push({
4007
+ label: {
4008
+ key: `node:${id}`,
4009
+ text,
4010
+ x: screenX,
4011
+ y: screenY,
4012
+ kind: "node"
4013
+ },
4014
+ box: boxAt(halfW, screenX, screenY)
4015
+ });
4016
+ }
4017
+ }
4018
+ return { deletions: deletionCandidates, nodes: nodeCandidates };
4019
+ }
4020
+ function nodeLabelBudget(width, height) {
4021
+ return Math.max(
4022
+ MIN_NODE_LABELS,
4023
+ Math.floor(width * height / PX_PER_NODE_LABEL)
4024
+ );
4025
+ }
4026
+ function placeSizeLabels(candidates, take, budget = Infinity) {
4027
+ const labels = [];
4028
+ for (const { label, box } of candidates) {
4029
+ if (labels.length >= budget) {
4030
+ break;
4031
+ }
4032
+ if (take(box)) {
4033
+ labels.push(label);
4034
+ }
4035
+ }
4036
+ return labels;
4037
+ }
4038
+
4039
+ // src/walkEncoding.ts
4040
+ import { packAbgr as packAbgr3 } from "@jbrowse/core/util/colorBits";
4041
+ var WALK_FIELDS = [
4042
+ { value: "progress", label: "Progress along the walk" },
4043
+ { value: "walk", label: "One colour for the walk" },
4044
+ { value: "reference", label: "Reference position" }
4045
+ ];
4046
+ var WALK_SCHEMES = [
4047
+ { value: "grey", label: "Grey to black", ends: ["#a3a3a3", "#151515"] },
4048
+ { value: "blue", label: "Cyan to navy", ends: ["#6ee0f0", "#0b2c7a"] },
4049
+ { value: "red", label: "Yellow to red", ends: ["#ffd24a", "#b3121b"] },
4050
+ { value: "purple", label: "Pink to plum", ends: ["#f5a3d8", "#5e1a6e"] },
4051
+ { value: "green", label: "Lime to forest", ends: ["#b6e36c", "#0b5d3b"] },
4052
+ { value: "rainbow", label: "Rainbow" }
4053
+ ];
4054
+ var FAMILIES = ["blue", "red", "purple", "green"];
4055
+ var isField = (v) => WALK_FIELDS.some((f) => f.value === v);
4056
+ var isScheme = (v) => WALK_SCHEMES.some((s) => s.value === v);
4057
+ function resolveEncoding(layer, reference, picked, alone = true) {
4058
+ const { field: askedField, scheme: askedScheme } = layer.color ?? {};
4059
+ const field = isField(askedField) ? askedField : alone ? "progress" : "walk";
4060
+ const family = reference ? "grey" : FAMILIES[picked % FAMILIES.length];
4061
+ const scheme = isScheme(askedScheme) ? askedScheme === "rainbow" && field !== "reference" ? family : askedScheme : family;
4062
+ return { field, scheme };
4063
+ }
4064
+ var BASE_T = 0.5;
4065
+ var toLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
4066
+ var toGamma = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055;
4067
+ function oklab(hex) {
4068
+ const [r, g, b] = [1, 3, 5].map(
4069
+ (i) => toLinear(Number.parseInt(hex.slice(i, i + 2), 16) / 255)
4070
+ );
4071
+ const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
4072
+ const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
4073
+ const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
4074
+ return [
4075
+ 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
4076
+ 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
4077
+ 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
4078
+ ];
4079
+ }
4080
+ function oklabToRgb([L, a, b]) {
4081
+ const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;
4082
+ const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;
4083
+ const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
4084
+ return [
4085
+ 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
4086
+ -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
4087
+ -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
4088
+ ].map((c) => toGamma(Math.max(0, Math.min(1, c))) * 255);
4089
+ }
4090
+ function schemeRgb(scheme, t) {
4091
+ const x = Math.max(0, Math.min(1, t));
4092
+ const family = WALK_SCHEMES.find((s) => s.value === scheme);
4093
+ if (!family || !("ends" in family)) {
4094
+ return hslToRgb(x * REFERENCE_RAMP_MAX_HUE, 0.7, 0.5).map((c) => c * 255);
4095
+ }
4096
+ const from = oklab(family.ends[0]);
4097
+ const to = oklab(family.ends[1]);
4098
+ return oklabToRgb(from.map((v, i) => v + (to[i] - v) * x));
4099
+ }
4100
+ function schemeCss(scheme, t) {
4101
+ const [r, g, b] = schemeRgb(scheme, t).map(Math.round);
4102
+ return `rgb(${r}, ${g}, ${b})`;
4103
+ }
4104
+ function schemeColor(scheme, t) {
4105
+ const [r, g, b] = schemeRgb(scheme, t).map(Math.round);
4106
+ return packAbgr3(r, g, b, 255);
4107
+ }
4108
+ function encodedColor({ field, scheme }, t) {
4109
+ return field === "walk" ? schemeColor(scheme, BASE_T) : t === void 0 ? NO_VALUE_COLOR : schemeColor(scheme, t);
4110
+ }
4111
+ var NO_VALUE_COLOR = REFERENCE_RAMP_ALT_COLOR;
4112
+ function encodingStops({ field, scheme }) {
4113
+ return field === "walk" ? [{ offset: 0, color: schemeCss(scheme, BASE_T) }] : Array.from({ length: 7 }, (_, i) => ({
4114
+ offset: i / 6,
4115
+ color: schemeCss(scheme, i / 6)
4116
+ }));
4117
+ }
4118
+ function encodingSwatchCss(encoding) {
4119
+ const stops = encodingStops(encoding);
4120
+ return stops.length === 1 ? stops[0].color : `linear-gradient(to right, ${stops.map((s) => `${s.color} ${s.offset * 100}%`).join(", ")})`;
4121
+ }
4122
+
4123
+ // src/walkHighlight.ts
4124
+ function referenceOf(graph) {
4125
+ return graph.referencePath ? graph.paths?.find((p) => pathOrigin(p.name).name === graph.referencePath) : void 0;
4126
+ }
4127
+ function walkHighlight(graph, name) {
4128
+ const path = graph.paths?.find((p) => p.name === name);
4129
+ if (!path) {
4130
+ return void 0;
4131
+ }
4132
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
4133
+ const edgeAt = /* @__PURE__ */ new Map();
4134
+ graph.edges.forEach((e, i) => {
4135
+ edgeAt.set(`${e.from}>${e.to}`, i);
4136
+ });
4137
+ const edgeIndexes = /* @__PURE__ */ new Set();
4138
+ for (let i = 1; i < path.nodeIds.length; i++) {
4139
+ const a = path.nodeIds[i - 1];
4140
+ const b = path.nodeIds[i];
4141
+ const index = edgeAt.get(`${a}>${b}`) ?? edgeAt.get(`${b}>${a}`);
4142
+ if (index !== void 0) {
4143
+ edgeIndexes.add(index);
4144
+ }
4145
+ }
4146
+ const lengthOf = (id) => byId.get(id)?.length ?? 0;
4147
+ const bp = path.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0);
4148
+ const progress = /* @__PURE__ */ new Map();
4149
+ let before = 0;
4150
+ for (const id of path.nodeIds) {
4151
+ const length = lengthOf(id);
4152
+ if (!progress.has(id)) {
4153
+ progress.set(id, bp > 0 ? (before + length / 2) / bp : 0);
4154
+ }
4155
+ before += length;
4156
+ }
4157
+ const reference = referenceOf(graph);
4158
+ const originName = pathOrigin(name).name;
4159
+ const origin = graph.anchorPaths?.find((a) => a.name === originName && a.length === bp) ?? graph.anchorPaths?.find((a) => a.name === originName);
4160
+ return {
4161
+ name,
4162
+ reference: reference === path,
4163
+ range: origin ? {
4164
+ contig: path.contig ?? panSNContig(origin.name),
4165
+ start: origin.start,
4166
+ end: origin.start + origin.length
4167
+ } : void 0,
4168
+ nodeIds: new Set(path.nodeIds),
4169
+ edgeIndexes,
4170
+ progress,
4171
+ steps: path.nodeIds.length,
4172
+ bp,
4173
+ referenceBp: reference && reference !== path ? reference.nodeIds.reduce((sum, id) => sum + lengthOf(id), 0) : void 0
4174
+ };
4175
+ }
4176
+ function laneColors(graph, walk, encoding, ramp) {
4177
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
4178
+ const colors = /* @__PURE__ */ new Map();
4179
+ for (const [id, progress] of walk.progress) {
4180
+ const node = byId.get(id);
4181
+ const mid = ramp?.midpoints.get(id);
4182
+ const t = encoding.field === "progress" ? progress : ramp && mid !== void 0 && !(node?.stable && node.stable.rank > 0) ? (mid - ramp.start) / ramp.span : void 0;
4183
+ colors.set(id, encodedColor(encoding, t));
4184
+ }
4185
+ return colors;
4186
+ }
4187
+ function reversedNodes(graph, walk) {
4188
+ const reference = referenceOf(graph);
4189
+ const reversed = /* @__PURE__ */ new Set();
4190
+ let bp = 0;
4191
+ if (!reference || walk.reference) {
4192
+ return { reversed, bp };
4193
+ }
4194
+ const walkPath = pathOrigin(walk.name).name;
4195
+ const referencePath = pathOrigin(reference.name).name;
4196
+ const byId = new Map(graph.nodes.map((n2) => [n2.id, n2]));
4197
+ for (const id of walk.progress.keys()) {
4198
+ const node = byId.get(id);
4199
+ const visits = node ? graph.pathVisits?.get(node.name) : void 0;
4200
+ const own = visits?.find((v) => v.path === walkPath)?.strand;
4201
+ const theirs = visits?.find((v) => v.path === referencePath)?.strand;
4202
+ if (own && theirs && own !== theirs) {
4203
+ reversed.add(id);
4204
+ bp += node.length;
4205
+ }
4206
+ }
4207
+ return { reversed, bp };
4208
+ }
4209
+ function walkLift(graph, layers, ramp) {
4210
+ const seen = /* @__PURE__ */ new Set();
4211
+ const lifted = layers.filter((layer) => !seen.has(layer.walk) && seen.add(layer.walk)).flatMap((layer) => {
4212
+ const walk = walkHighlight(graph, layer.walk);
4213
+ return walk ? [{ walk, layer }] : [];
4214
+ }).sort((a, b) => Number(b.walk.reference) - Number(a.walk.reference));
4215
+ if (lifted.length === 0) {
4216
+ return void 0;
4217
+ }
4218
+ let picked = 0;
4219
+ const walks = lifted.map(({ walk, layer }) => {
4220
+ const encoding = resolveEncoding(
4221
+ layer,
4222
+ walk.reference,
4223
+ walk.reference ? 0 : picked++,
4224
+ lifted.length === 1
4225
+ );
4226
+ const { reversed, bp } = reversedNodes(graph, walk);
4227
+ return {
4228
+ ...walk,
4229
+ encoding,
4230
+ colors: laneColors(graph, walk, encoding, ramp),
4231
+ reversed,
4232
+ reversedBp: bp
4233
+ };
4234
+ });
4235
+ return liftOf(walks, ramp);
4236
+ }
4237
+ var FACET_COLOR = { field: "progress", scheme: "red" };
4238
+ function facetLifts(graph, lift, layers, ramp) {
4239
+ return lift.walks.map((walk) => {
4240
+ const stated = layers.find((l) => l.walk === walk.name)?.color;
4241
+ const encoding = resolveEncoding(
4242
+ { walk: walk.name, color: { ...FACET_COLOR, ...stated } },
4243
+ walk.reference,
4244
+ 0
4245
+ );
4246
+ return liftOf(
4247
+ [
4248
+ {
4249
+ ...walk,
4250
+ encoding,
4251
+ colors: laneColors(graph, walk, encoding, ramp)
4252
+ }
4253
+ ],
4254
+ ramp
4255
+ );
4256
+ });
4257
+ }
4258
+ function liftOf(walks, ramp) {
4259
+ return {
4260
+ walks,
4261
+ referenceDomain: ramp ? { start: ramp.start, end: ramp.start + ramp.span } : void 0,
4262
+ nodeIds: new Set(walks.flatMap((w) => [...w.nodeIds])),
4263
+ edgeIndexes: new Set(walks.flatMap((w) => [...w.edgeIndexes])),
4264
+ names: new Set(walks.map((w) => w.name))
4265
+ };
4266
+ }
4267
+
4268
+ // src/walkKey.ts
4269
+ function rangeText(contig, start, end) {
4270
+ const s = Math.round(start);
4271
+ const e = Math.round(end);
4272
+ return `${contig ? `${contig}:` : ""}${s.toLocaleString()}-${e.toLocaleString()} (${formatBp(e - s)})`;
4273
+ }
4274
+ function walkKey(walk, reference) {
4275
+ const { field } = walk.encoding;
4276
+ const own = walk.range ? rangeText(walk.range.contig, walk.range.start, walk.range.end) : void 0;
4277
+ const scale = field === "progress" ? own : field === "reference" && reference ? rangeText(reference.name, reference.start, reference.end) : void 0;
4278
+ return {
4279
+ delta: walk.referenceBp === void 0 || walk.bp === walk.referenceBp ? "" : ` ${walk.bp > walk.referenceBp ? "+" : "\u2212"}${formatBp(Math.abs(walk.bp - walk.referenceBp))}`,
4280
+ reversed: walk.reversedBp > 0 ? `, ${formatBp(walk.reversedBp)} reversed` : "",
4281
+ shades: field !== "walk",
4282
+ scale,
4283
+ hover: scale === own ? void 0 : own
4284
+ };
4285
+ }
4286
+
4287
+ // src/facetGrid.ts
4288
+ var FACET_GAP_PX = 8;
4289
+ var FACET_TITLE_PX = 34;
4290
+ var FACET_PAD_PX = 12;
4291
+ var MIN_FACET_PANEL_PX = 60;
4292
+ function facetGrid({
4293
+ count,
4294
+ bounds,
4295
+ pixelRows,
4296
+ width,
4297
+ room,
4298
+ columns
4299
+ }) {
4300
+ function tile(across) {
4301
+ const rows = Math.ceil(count / across);
4302
+ const panelWidth = Math.floor(
4303
+ (width - (across - 1) * FACET_GAP_PX) / across
4304
+ );
4305
+ const panelRoom = (room - rows * FACET_TITLE_PX - (rows - 1) * FACET_GAP_PX) / rows;
4306
+ const fitX = (panelWidth - 2 * FACET_PAD_PX) / bounds.w;
4307
+ const fitY = bounds.h > 0 ? (panelRoom - 2 * FACET_PAD_PX) / bounds.h : Infinity;
4308
+ const scale = pixelRows ? fitX : Math.min(fitX, fitY);
4309
+ const drawn = pixelRows ? bounds.h : bounds.h * scale;
4310
+ const height = Math.floor(
4311
+ Math.max(
4312
+ MIN_FACET_PANEL_PX,
4313
+ Math.min(drawn + 2 * FACET_PAD_PX, panelRoom)
4314
+ )
4315
+ );
4316
+ return {
4317
+ columns: across,
4318
+ width: panelWidth,
4319
+ height,
4320
+ scale,
4321
+ total: rows * (height + FACET_TITLE_PX) + (rows - 1) * FACET_GAP_PX
4322
+ };
4323
+ }
4324
+ if (columns !== void 0) {
4325
+ return tile(Math.max(1, Math.min(count, Math.round(columns))));
4326
+ }
4327
+ let best = tile(1);
4328
+ for (let across = 2; across <= count; across++) {
4329
+ const grid = tile(across);
4330
+ if (grid.scale > best.scale) {
4331
+ best = grid;
4332
+ }
4333
+ }
4334
+ return best;
4335
+ }
4336
+ function facetCells(names, by) {
4337
+ const wrap = {
4338
+ columns: void 0,
4339
+ count: names.length,
4340
+ cells: names.map((_, i) => i)
4341
+ };
4342
+ if (by === "walk") {
4343
+ return wrap;
4344
+ }
4345
+ const places = names.map((name) => {
4346
+ const [sample, haplotype, contig] = name.split("#");
4347
+ const n2 = Number(haplotype);
4348
+ return contig !== void 0 && Number.isInteger(n2) && n2 >= 0 ? { sample, column: Math.max(0, n2 - 1) } : void 0;
4349
+ });
4350
+ const rows = [...new Set(places.map((p) => p?.sample))];
4351
+ const columns = Math.max(1, ...places.map((p) => p ? p.column + 1 : 1));
4352
+ const cells = places.map(
4353
+ (p) => p ? rows.indexOf(p.sample) * columns + p.column : -1
4354
+ );
4355
+ return cells.includes(-1) || new Set(cells).size < cells.length ? wrap : { columns, count: rows.length * columns, cells };
4356
+ }
4357
+
4358
+ // src/renderer/svgCanvas.ts
4359
+ var n = (v) => +v.toFixed(1);
4360
+ function paint(css) {
4361
+ const m = /^rgba?\(([^,]+),([^,]+),([^,)]+)(?:,([^)]+))?\)$/.exec(
4362
+ css.replaceAll(" ", "")
4363
+ );
4364
+ if (!m) {
4365
+ return { color: css, opacity: 1 };
4366
+ }
4367
+ const opacity = m[4] === void 0 ? 1 : Number(m[4]);
4368
+ return { color: `rgb(${m[1]},${m[2]},${m[3]})`, opacity };
4369
+ }
4370
+ function paintAttrs(kind, css) {
4371
+ const { color, opacity } = paint(css);
4372
+ return `${kind}="${color}"${opacity < 1 ? ` ${kind}-opacity="${n(opacity)}"` : ""}`;
4373
+ }
4374
+ function svgCanvas(width, height) {
4375
+ const out = [];
4376
+ let d = "";
4377
+ const ctx = {
4378
+ canvas: void 0,
4379
+ fillStyle: "#000",
4380
+ strokeStyle: "#000",
4381
+ lineWidth: 1,
4382
+ lineCap: "butt",
4383
+ lineJoin: "miter",
4384
+ beginPath() {
4385
+ d = "";
4386
+ },
4387
+ moveTo(x, y) {
4388
+ d += `M${n(x)} ${n(y)}`;
4389
+ },
4390
+ lineTo(x, y) {
4391
+ d += `L${n(x)} ${n(y)}`;
4392
+ },
4393
+ bezierCurveTo(cx0, cy0, cx1, cy1, x, y) {
4394
+ d += `C${n(cx0)} ${n(cy0)} ${n(cx1)} ${n(cy1)} ${n(x)} ${n(y)}`;
4395
+ },
4396
+ closePath() {
4397
+ d += "Z";
4398
+ },
4399
+ stroke() {
4400
+ if (d) {
4401
+ out.push(
4402
+ `<path d="${d}" fill="none" ${paintAttrs("stroke", ctx.strokeStyle)} stroke-width="${n(ctx.lineWidth)}" stroke-linecap="${ctx.lineCap}" stroke-linejoin="${ctx.lineJoin}"/>`
4403
+ );
4404
+ }
4405
+ },
4406
+ fill() {
4407
+ if (d) {
4408
+ out.push(`<path d="${d}" ${paintAttrs("fill", ctx.fillStyle)}/>`);
4409
+ }
4410
+ },
4411
+ fillRect(x, y, w, h) {
4412
+ out.push(
4413
+ `<rect x="${n(x)}" y="${n(y)}" width="${n(w)}" height="${n(h)}" ${paintAttrs("fill", ctx.fillStyle)}/>`
4414
+ );
4415
+ }
4416
+ };
4417
+ const canvas = {
4418
+ width,
4419
+ height,
4420
+ style: {},
4421
+ getContext: () => ctx
4422
+ };
4423
+ ctx.canvas = canvas;
4424
+ return {
4425
+ canvas,
4426
+ // the drawing so far, as SVG elements in canvas pixels
4427
+ markup: () => out.join("")
4428
+ };
4429
+ }
4430
+
4431
+ // src/viewport.ts
4432
+ function axisScaleOf(scale, pixelRows) {
4433
+ return { scaleX: scale, scaleY: pixelRows ? 1 : scale, pixelRows };
4434
+ }
4435
+ function viewportOf(t, axis, width, height) {
4436
+ return {
4437
+ minX: -t.translateX / axis.scaleX,
4438
+ minY: -t.translateY / axis.scaleY,
4439
+ maxX: (width - t.translateX) / axis.scaleX,
4440
+ maxY: (height - t.translateY) / axis.scaleY
4441
+ };
4442
+ }
4443
+ function padded(v, panes) {
4444
+ const w = (v.maxX - v.minX) * panes;
4445
+ const h = (v.maxY - v.minY) * panes;
4446
+ return {
4447
+ minX: v.minX - w,
4448
+ minY: v.minY - h,
4449
+ maxX: v.maxX + w,
4450
+ maxY: v.maxY + h
4451
+ };
4452
+ }
4453
+ function contains(outer, inner) {
4454
+ return inner.minX >= outer.minX && inner.maxX <= outer.maxX && inner.minY >= outer.minY && inner.maxY <= outer.maxY;
4455
+ }
4456
+ function zoomAbout(t, factor, centerX, centerY, pixelRows) {
4457
+ const scale = clampZoom(t.scale * factor);
4458
+ const ratio = scale / t.scale;
4459
+ return {
4460
+ scale,
4461
+ translateX: centerX - (centerX - t.translateX) * ratio,
4462
+ translateY: pixelRows ? t.translateY : centerY - (centerY - t.translateY) * ratio
4463
+ };
4464
+ }
4465
+ function screenToLayout(x, y, t, axis) {
4466
+ return {
4467
+ x: (x - t.translateX) / axis.scaleX,
4468
+ y: (y - t.translateY) / axis.scaleY
4469
+ };
4470
+ }
4471
+
4472
+ // src/figure.ts
4473
+ var FONT = 'font-family="Helvetica, Arial, sans-serif" font-size="11"';
4474
+ var BAR_PX = 48;
4475
+ var SWATCH_PX = 18;
4476
+ var KEY_GAP_PX = 16;
4477
+ var FADED = "rgb(160,160,160)";
4478
+ function esc(s) {
4479
+ return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
4480
+ }
4481
+ function keySvg(id, x, y, stops, label, after, under, opacity = 1) {
4482
+ const bar = stops.length > 1 ? BAR_PX : SWATCH_PX;
4483
+ const fill = stops.length > 1 ? `fill="url(#${id})"` : `fill="${stops[0].color}"${opacity < 1 ? ` fill-opacity="${opacity}"` : ""}`;
4484
+ const defs = stops.length > 1 ? `<linearGradient id="${id}">${stops.map((s) => `<stop offset="${s.offset}" stop-color="${s.color}"/>`).join("")}</linearGradient>` : "";
4485
+ const width = Math.max(
4486
+ bar + 5 + (label.length + after.length) * LABEL_CHAR_PX,
4487
+ (under?.length ?? 0) * LABEL_CHAR_PX
4488
+ );
4489
+ return {
4490
+ width,
4491
+ markup: `${defs}<g transform="translate(${x} ${y})" ${FONT}><rect y="5" width="${bar}" height="8" rx="2" ${fill}/><text x="${bar + 5}" y="13"><tspan font-weight="bold">${esc(label)}</tspan>${esc(after)}</text>${under ? `<text y="28">${esc(under)}</text>` : ""}</g>`
4492
+ };
4493
+ }
4494
+ function walkKeySvg(id, x, y, walk, label, reference) {
4495
+ const key = walkKey(walk, reference);
4496
+ return keySvg(
4497
+ id,
4498
+ x,
4499
+ y,
4500
+ encodingStops(walk.encoding),
4501
+ label,
4502
+ key.delta + key.reversed,
4503
+ key.scale
4504
+ );
4505
+ }
4506
+ function figureSvg(graph, layout, o = {}) {
4507
+ const width = o.width ?? 1200;
4508
+ const room = o.height ?? 800;
4509
+ const region = o.region;
4510
+ const bounds = drawingBounds(layout, { region });
4511
+ const pixelRows = layout.pixelRows ?? false;
4512
+ const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
4513
+ const colorScheme = resolveColorScheme(o.colorScheme ?? "auto", graph);
4514
+ const referenceRamp = colorScheme === "reference-position" && !layout.tubeMap ? computeReferenceRamp(graph, region) : void 0;
4515
+ const deletions = layout.tubeMap ? [] : deletionEdges(graph);
4516
+ const layers = o.walks ?? [];
4517
+ const walkRamp = layers.some((l) => l.color?.field === "reference") ? referenceRamp ?? computeReferenceRamp(graph, region) : void 0;
4518
+ const lift = layers.length > 0 && !layout.tubeMap ? walkLift(graph, layers, walkRamp) : void 0;
4519
+ const by = o.facet ?? "none";
4520
+ const panels = by !== "none" && lift && lift.walks.length > 1 ? facetLifts(graph, lift, layers, walkRamp) : void 0;
4521
+ const labels = new Map(
4522
+ (graph.paths?.length ? pathLegend(graph.paths) : []).map((p) => [
4523
+ p.name,
4524
+ p.label
4525
+ ])
4526
+ );
4527
+ const labelOf2 = (name) => labels.get(name) ?? name;
4528
+ const reference = lift?.referenceDomain && {
4529
+ ...lift.referenceDomain,
4530
+ name: region?.refName
4531
+ };
4532
+ function drawing(highlight, w, h) {
4533
+ const fit = fitTransform(
4534
+ bounds,
4535
+ w,
4536
+ h,
4537
+ pixelRows,
4538
+ panels ? {
4539
+ padLeft: FACET_PAD_PX,
4540
+ padTop: FACET_PAD_PX,
4541
+ padRight: FACET_PAD_PX,
4542
+ padBottom: FACET_PAD_PX
4543
+ } : {}
4544
+ );
4545
+ if (!fit) {
4546
+ return "";
4547
+ }
4548
+ const axis = axisScaleOf(fit.scale, pixelRows);
4549
+ const { canvas, markup } = svgCanvas(w, h);
4550
+ const renderer = new Canvas2DRenderer(canvas);
4551
+ renderer.uploadGeometry(
4552
+ buildGeometry({
4553
+ nodePositions: layout.nodePositions,
4554
+ graph,
4555
+ nodeById,
4556
+ colorScheme,
4557
+ contigThickness: o.contigThickness ?? 6,
4558
+ connectorThickness: o.connectorThickness ?? 2,
4559
+ drawPaths: false,
4560
+ nodeWidth: o.nodeWidth ?? "depth",
4561
+ highlight,
4562
+ axis,
4563
+ referenceRamp,
4564
+ deletions: new Map(deletions.map((d) => [d.edgeIndex, d.bypassed])),
4565
+ hiddenEdges: new Set(
4566
+ o.showDeletionEdges ? [] : deletions.map((d) => d.edgeIndex)
4567
+ )
4568
+ })
4569
+ );
4570
+ renderer.updateTransform({
4571
+ scaleX: axis.scaleX,
4572
+ scaleY: axis.scaleY,
4573
+ translateX: fit.translateX,
4574
+ translateY: fit.translateY,
4575
+ dpr: 1
4576
+ });
4577
+ renderer.render([1, 1, 1, 1]);
4578
+ return markup();
4579
+ }
4580
+ const parts = [];
4581
+ let height;
4582
+ if (panels) {
4583
+ const place = facetCells(
4584
+ panels.map((p) => p.walks[0].name),
4585
+ by
4586
+ );
4587
+ const grid = facetGrid({
4588
+ count: place.count,
4589
+ bounds,
4590
+ pixelRows,
4591
+ width,
4592
+ room,
4593
+ columns: place.columns ?? o.columns
4594
+ });
4595
+ panels.forEach((panel, i) => {
4596
+ const cell = place.cells[i];
4597
+ const x = cell % grid.columns * (grid.width + FACET_GAP_PX);
4598
+ const y = Math.floor(cell / grid.columns) * (grid.height + FACET_TITLE_PX + FACET_GAP_PX);
4599
+ const walk = panel.walks[0];
4600
+ parts.push(
4601
+ walkKeySvg(`key${i}`, x + 6, y + 2, walk, labelOf2(walk.name), reference).markup,
4602
+ `<svg x="${x}" y="${y + FACET_TITLE_PX}" width="${grid.width}" height="${grid.height}">${drawing(panel, grid.width, grid.height)}</svg>`
4603
+ );
4604
+ });
4605
+ height = grid.total;
4606
+ } else {
4607
+ let x = 6;
4608
+ const push = (key) => {
4609
+ parts.push(key.markup);
4610
+ x += key.width + KEY_GAP_PX;
4611
+ };
4612
+ if (lift) {
4613
+ lift.walks.forEach((walk, i) => {
4614
+ push(walkKeySvg(`key${i}`, x, 2, walk, labelOf2(walk.name), reference));
4615
+ });
4616
+ push(
4617
+ keySvg(
4618
+ "faded",
4619
+ x,
4620
+ 2,
4621
+ [{ offset: 0, color: FADED }],
4622
+ "",
4623
+ `not on ${lift.walks.length === 1 ? labelOf2(lift.walks[0].name) : "these walks"}`,
4624
+ void 0,
4625
+ 0.3
4626
+ )
4627
+ );
4628
+ } else if (referenceRamp) {
4629
+ push(
4630
+ keySvg(
4631
+ "ramp",
4632
+ x,
4633
+ 2,
4634
+ encodingStops({ field: "reference", scheme: "rainbow" }),
4635
+ "Reference position",
4636
+ "",
4637
+ rangeText(
4638
+ region?.refName,
4639
+ referenceRamp.start,
4640
+ referenceRamp.start + referenceRamp.span
4641
+ )
4642
+ )
4643
+ );
4644
+ }
4645
+ const header = parts.length > 0 ? FACET_TITLE_PX : 0;
4646
+ const usable = width - 2 * FIT_PADDING;
4647
+ const drawn = pixelRows || !(bounds.w > 0) ? bounds.h : bounds.h * usable / bounds.w;
4648
+ const h = Math.max(
4649
+ 160,
4650
+ Math.min(room - header, Math.ceil(drawn + 2 * FIT_PADDING))
4651
+ );
4652
+ parts.push(
4653
+ `<svg y="${header}" width="${width}" height="${h}">${drawing(lift, width, h)}</svg>`
4654
+ );
4655
+ height = header + h;
4656
+ }
4657
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${o.metadata ? `<metadata>${esc(o.metadata)}</metadata>` : ""}<rect width="${width}" height="${height}" fill="#fff"/>${parts.join("")}</svg>
4658
+ `;
4659
+ }
4660
+
4661
+ // src/reference.ts
4662
+ var WELL_KNOWN_SAMPLES = /* @__PURE__ */ new Map([
4663
+ ["hg38", "GRCh38"],
4664
+ ["hg19", "GRCh37"],
4665
+ ["hs1", "CHM13"],
4666
+ ["t2t-chm13", "CHM13"],
4667
+ ["chm13v2", "CHM13"]
4668
+ ]);
4669
+ function wellKnownSample(assemblyName) {
4670
+ return WELL_KNOWN_SAMPLES.get(assemblyName.toLowerCase());
4671
+ }
4672
+ function sampleOf2(refName) {
4673
+ const sample = refName.includes("#") ? panSNSample(refName) : void 0;
4674
+ return sample === void 0 || isGenericSample(sample) ? void 0 : sample;
4675
+ }
4676
+ function haplotypeOf(refName) {
4677
+ return panSNHaplotype(refName) ?? panSNSample(refName);
4678
+ }
4679
+ function graphHaplotypes(graph) {
4680
+ const refNames = new Set((graph.anchorPaths ?? []).map((p) => p.name));
4681
+ for (const node of graph.nodes) {
4682
+ if (node.stable) {
4683
+ refNames.add(node.stable.refName);
4684
+ }
4685
+ }
4686
+ const bySample = /* @__PURE__ */ new Map();
4687
+ for (const refName of refNames) {
4688
+ const sample = sampleOf2(refName);
4689
+ if (sample !== void 0) {
4690
+ const haplotypes = bySample.get(sample) ?? bySample.set(sample, /* @__PURE__ */ new Set()).get(sample);
4691
+ haplotypes.add(haplotypeOf(refName));
4692
+ }
4693
+ }
4694
+ return bySample;
4695
+ }
4696
+ function backboneFrom(contigs, haplotypes) {
4697
+ const samples = new Set(contigs.map((c) => sampleOf2(c.refName)));
4698
+ const [sample] = samples;
4699
+ if (samples.size !== 1 || sample === void 0) {
4700
+ return { contigs, prefixes: [], named: false };
4701
+ }
4702
+ const own = new Set(contigs.map((c) => haplotypeOf(c.refName)));
4703
+ const unique = [...haplotypes.get(sample) ?? []].every((h) => own.has(h));
4704
+ const shared = new Set(contigs.map((c) => panSNHaplotype(c.refName)));
4705
+ const [haplotype] = shared;
4706
+ return {
4707
+ contigs,
4708
+ prefixes: [
4709
+ ...unique ? [sample] : [],
4710
+ ...shared.size === 1 && haplotype ? [haplotype] : []
4711
+ ],
4712
+ named: true
4713
+ };
4714
+ }
4715
+ function graphBackbone(graph) {
4716
+ const spans = /* @__PURE__ */ new Map();
4717
+ for (const node of graph.nodes) {
4718
+ if (isBackbone(node)) {
4719
+ const { refName, start } = node.stable;
4720
+ const end = start + node.length;
4721
+ const span = spans.get(refName);
4722
+ if (span) {
4723
+ span.start = Math.min(span.start, start);
4724
+ span.end = Math.max(span.end, end);
4725
+ } else {
4726
+ spans.set(refName, {
4727
+ refName,
4728
+ contig: panSNContig(refName),
4729
+ start,
4730
+ end
4731
+ });
4732
+ }
4733
+ }
4734
+ }
4735
+ const contigs = [...spans.values()];
4736
+ return contigs.length ? backboneFrom(contigs, graphHaplotypes(graph)) : void 0;
4737
+ }
4738
+ function backboneAssembly(backbone, assemblies) {
4739
+ const names = (a) => [a.name, ...a.aliases ?? []];
4740
+ const naming = (prefix) => (a) => names(a).some((n2) => n2.toLowerCase() === prefix.toLowerCase());
4741
+ const prefixes = [...backbone?.prefixes ?? []].reverse();
4742
+ const sample = backbone?.prefixes.find((p) => !p.includes("#"))?.toLowerCase();
4743
+ for (const prefix of prefixes) {
4744
+ const assembly = assemblies.find(naming(prefix));
4745
+ if (assembly) {
4746
+ return assembly;
4747
+ }
4748
+ }
4749
+ return sample === void 0 ? void 0 : assemblies.find(
4750
+ (a) => names(a).some((n2) => wellKnownSample(n2)?.toLowerCase() === sample)
4751
+ );
4752
+ }
4753
+ function assemblyWalk(graph, assembly) {
4754
+ const haplotypes = graphHaplotypes(graph);
4755
+ return graph.anchorPaths?.find(
4756
+ (walk) => backboneAssembly(
4757
+ backboneFrom(
4758
+ [
4759
+ {
4760
+ refName: walk.name,
4761
+ contig: panSNContig(walk.name),
4762
+ start: walk.start,
4763
+ end: walk.start + walk.length
4764
+ }
4765
+ ],
4766
+ haplotypes
4767
+ ),
4768
+ [assembly]
4769
+ )
4770
+ );
4771
+ }
4772
+ function refNameBinding(refNames) {
4773
+ const exact = new Set(refNames);
4774
+ const byContig = /* @__PURE__ */ new Map();
4775
+ for (const refName of exact) {
4776
+ const contig = panSNContig(refName);
4777
+ byContig.set(contig, byContig.has(contig) ? void 0 : refName);
4778
+ }
4779
+ return (name) => exact.has(name) ? name : byContig.get(name);
4780
+ }
4781
+ function featuresOnBackbone(features, backbone) {
4782
+ const bind = refNameBinding(backbone.contigs.map((c) => c.refName));
4783
+ return features.flatMap((f) => {
4784
+ const refName = bind(f.refName);
4785
+ return refName === void 0 ? [] : [refName === f.refName ? f : { ...f, refName }];
4786
+ });
4787
+ }
4788
+
4789
+ // src/gbzWindow.ts
4790
+ var NoReferenceSampleError = class extends Error {
4791
+ constructor(anchor, referenceSamples) {
4792
+ super(
4793
+ 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`
4794
+ );
4795
+ this.name = "NoReferenceSampleError";
4796
+ }
4797
+ };
4798
+ var NodeLimitError = class extends Error {
4799
+ constructor(limit, windowBp, fitsBp) {
4800
+ super(
4801
+ `this ${windowBp.toLocaleString()} bp window reads more than nodeLimit (${limit.toLocaleString()}) graph nodes; zoom in to about ${fitsBp.toLocaleString()} bp or raise nodeLimit`
4802
+ );
4803
+ this.name = "NodeLimitError";
4804
+ }
4805
+ };
4806
+ function haplotypePrefix(name) {
4807
+ return `${name.sample}#${name.haplotype}`;
4808
+ }
4809
+ function haplotypeWanted(name, wanted) {
4810
+ const prefix = haplotypePrefix(name);
4811
+ return wanted === void 0 || wanted.some((candidate) => panSNMatchesPrefix(prefix, candidate));
4812
+ }
4813
+ async function referenceSamplesOf(db) {
4814
+ return (await db.tag("gbwt_reference_samples") ?? "").split(/\s+/).filter((sample) => sample !== "");
4815
+ }
4816
+ function aliasedSample(anchorSample, referenceSamples) {
4817
+ const lower = anchorSample.toLowerCase();
4818
+ const alias = wellKnownSample(anchorSample)?.toLowerCase();
4819
+ return referenceSamples.find((s) => s === anchorSample) ?? referenceSamples.find((s) => s.toLowerCase() === lower) ?? referenceSamples.find((s) => s.toLowerCase() === alias);
4820
+ }
4821
+ function resolveReferenceSample({
4822
+ configured,
4823
+ anchorPrefix,
4824
+ referenceSamples
4825
+ }) {
4826
+ if (configured !== "") {
4827
+ return configured;
4828
+ }
4829
+ const sample = aliasedSample(panSNSample(anchorPrefix), referenceSamples) ?? (referenceSamples.length === 1 ? referenceSamples[0] : void 0);
4830
+ if (sample === void 0) {
4831
+ throw new NoReferenceSampleError(anchorPrefix, referenceSamples);
4832
+ }
4833
+ return sample;
4834
+ }
4835
+ async function referencePathQuery(db, referenceSample, refName) {
4836
+ const path = (await db.paths()).find(
4837
+ (p) => p.isIndexed && p.name.sample === referenceSample && p.name.contig === refName
4838
+ );
4839
+ return path ? {
4840
+ sample: path.name.sample,
4841
+ contig: refName,
4842
+ haplotype: path.name.haplotype
4843
+ } : void 0;
4844
+ }
4845
+ function nodeLimitError(error, limit, windowBp) {
4846
+ const isLimit = error instanceof Error && (error.name === "SubgraphLimitError" || /^Subgraph size limit of \d+ nodes exceeded/.test(error.message));
4847
+ if (!isLimit) {
4848
+ return void 0;
4849
+ } else {
4850
+ const walked = error.walkedBp;
4851
+ const fits = typeof walked === "number" && walked > 0 ? Math.floor(walked * 0.8) : Math.floor(windowBp / 2);
4852
+ return new NodeLimitError(limit, windowBp, Math.max(fits, 1));
4853
+ }
4854
+ }
4855
+ async function cutWindowGFA(db, query, start, end, { keep, ...opts }) {
4856
+ const subgraph = query ? await db.getSubgraphForRange(query, start, end, {
4857
+ ...opts,
4858
+ haplotypes: "all",
4859
+ ...keep === void 0 ? {} : { keep }
4860
+ }).catch((error) => {
4861
+ throw nodeLimitError(error, opts.limit, end - start) ?? error;
4862
+ }) : void 0;
4863
+ return subgraph ? subgraph.toGFA({ names: "resolved" }) : "";
4864
+ }
4865
+
4866
+ // src/loadBandage.ts
4867
+ var cached;
4868
+ function loadBandage() {
4869
+ cached ??= import("./bandage-layout-3AR6BCUD.js").then((mod) => mod.default()).catch((e) => {
4870
+ cached = void 0;
4871
+ throw e;
4872
+ });
4873
+ return cached;
4874
+ }
4875
+
4876
+ export {
4877
+ isBackbone,
4878
+ firstNodeAtOrAfter,
4879
+ BUBBLE_SPREADS,
4880
+ panSNSample,
4881
+ panSNHaplotype,
4882
+ panSNContig,
4883
+ polylineSlice,
4884
+ loadGraph,
4885
+ engineRequest,
4886
+ engineKey,
4887
+ forceLayout,
4888
+ layoutExtent,
4889
+ FIT_PADDING,
4890
+ clampZoom,
4891
+ fitTransform,
4892
+ drawingBounds,
4893
+ ROW_HEIGHT_PX,
4894
+ referenceOrder,
4895
+ pathColorsLegible,
4896
+ pathLegend,
4897
+ walkRows,
4898
+ walkRowsExtent,
4899
+ FORCE_LAYOUT_LABEL,
4900
+ LAYOUT_MODES,
4901
+ modeUsesLayoutEngine,
4902
+ layoutModeByValue,
4903
+ COLOR_SCHEMES,
4904
+ resolveColorScheme,
4905
+ NODE_WIDTHS,
4906
+ meanDepth,
4907
+ nodeWidthPx,
4908
+ maxNodeWidthPx,
4909
+ nodeInk,
4910
+ fadeAbgr,
4911
+ svgPath,
4912
+ curveBounds,
4913
+ translateCurves,
4914
+ yToXOf,
4915
+ pathRibbonOffsets,
4916
+ deletionEdges,
4917
+ baseEdgeCurves,
4918
+ LIFT_BACKDROP_COLOR,
4919
+ FADED_ALPHA,
4920
+ REFERENCE_RAMP_MAX_HUE,
4921
+ computeReferenceRamp,
4922
+ computeColorSchemeRange,
4923
+ getNodeColor,
4924
+ buildGeometry,
4925
+ Canvas2DRenderer,
4926
+ LABEL_PX,
4927
+ LABEL_CHAR_PX,
4928
+ LABEL_PAD,
4929
+ occupancy,
4930
+ placeLabels,
4931
+ rowLabelBox,
4932
+ formatBp,
4933
+ sizeLabelCandidates,
4934
+ nodeLabelBudget,
4935
+ placeSizeLabels,
4936
+ WALK_FIELDS,
4937
+ WALK_SCHEMES,
4938
+ resolveEncoding,
4939
+ schemeCss,
4940
+ encodingStops,
4941
+ encodingSwatchCss,
4942
+ walkHighlight,
4943
+ walkLift,
4944
+ facetLifts,
4945
+ rangeText,
4946
+ walkKey,
4947
+ FACET_GAP_PX,
4948
+ FACET_TITLE_PX,
4949
+ FACET_PAD_PX,
4950
+ facetGrid,
4951
+ facetCells,
4952
+ svgCanvas,
4953
+ axisScaleOf,
4954
+ viewportOf,
4955
+ padded,
4956
+ contains,
4957
+ zoomAbout,
4958
+ screenToLayout,
4959
+ figureSvg,
4960
+ WELL_KNOWN_SAMPLES,
4961
+ wellKnownSample,
4962
+ graphBackbone,
4963
+ backboneAssembly,
4964
+ assemblyWalk,
4965
+ refNameBinding,
4966
+ featuresOnBackbone,
4967
+ haplotypeWanted,
4968
+ referenceSamplesOf,
4969
+ resolveReferenceSample,
4970
+ referencePathQuery,
4971
+ cutWindowGFA,
4972
+ loadBandage
4973
+ };