@euphrasiologist/lwphylo 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/drawPhylogeny.cjs +125 -2
- package/dist/drawPhylogeny.cjs.map +1 -1
- package/dist/drawPhylogeny.esm.js +125 -2
- package/dist/drawPhylogeny.esm.js.map +1 -1
- package/dist/drawPhylogeny.umd.js +1 -1
- package/dist/drawPhylogeny.umd.js.map +1 -1
- package/dist/index.cjs +125 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +125 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -426,6 +426,112 @@ function getChildArcs(pd) {
|
|
|
426
426
|
return arcs;
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
+
// fanAngles.js
|
|
430
|
+
const TAU = Math.PI * 2;
|
|
431
|
+
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
432
|
+
|
|
433
|
+
// Unwrap angles around a reference so they sit within [ref-π, ref+π]
|
|
434
|
+
function unwrapAround(ref, a) {
|
|
435
|
+
let x = a;
|
|
436
|
+
while (x < ref - Math.PI) x += TAU;
|
|
437
|
+
while (x > ref + Math.PI) x -= TAU;
|
|
438
|
+
return x;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Compute APE "fan" compatible angles:
|
|
443
|
+
* - Tips evenly spaced over [0, span] where span = 2π*(1 - 1/Ntip) - gap
|
|
444
|
+
* - Then + rotate (radians)
|
|
445
|
+
* - Internal nodes = arithmetic mean of child angles (unwrapped)
|
|
446
|
+
*
|
|
447
|
+
* pd: fortified nodes array (has thisId, parentId, children[])
|
|
448
|
+
* opts: { openAngleDeg=0, rotateDeg=0 }
|
|
449
|
+
* returns: Map(nodeId -> angle)
|
|
450
|
+
*/
|
|
451
|
+
function fanAngles(pd, opts = {}) {
|
|
452
|
+
const { openAngleDeg = 0, rotateDeg = 0 } = opts;
|
|
453
|
+
const gap = (openAngleDeg / 360) * TAU;
|
|
454
|
+
const rotate = (rotateDeg / 360) * TAU;
|
|
455
|
+
|
|
456
|
+
// Find root and collect tips in cladewise/DFS order
|
|
457
|
+
let root = null;
|
|
458
|
+
const kids = new Map(pd.map(d => [d.thisId, d.children || []]));
|
|
459
|
+
for (const d of pd) if (d.parentId == null) { root = d.thisId; break; }
|
|
460
|
+
|
|
461
|
+
const tipIds = [];
|
|
462
|
+
(function dfs(id) {
|
|
463
|
+
const c = kids.get(id) || [];
|
|
464
|
+
if (!c.length) { tipIds.push(id); return; }
|
|
465
|
+
for (const ch of c) dfs(ch);
|
|
466
|
+
})(root);
|
|
467
|
+
|
|
468
|
+
const N = Math.max(1, tipIds.length);
|
|
469
|
+
// APE: 0 .. 2π*(1 - 1/N) - gap, length.out=N (no last step overlap)
|
|
470
|
+
const maxA = TAU * (1 - 1 / N) - gap;
|
|
471
|
+
const step = N > 1 ? maxA / (N - 1) : 0;
|
|
472
|
+
|
|
473
|
+
const angle = new Map();
|
|
474
|
+
tipIds.forEach((id, i) => {
|
|
475
|
+
angle.set(id, norm(i * step + rotate));
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Internal nodes: arithmetic mean of child angles (unwrapped)
|
|
479
|
+
(function setInternal(id) {
|
|
480
|
+
const c = kids.get(id) || [];
|
|
481
|
+
for (const ch of c) setInternal(ch);
|
|
482
|
+
if (c.length > 0) {
|
|
483
|
+
// unwrap child angles around the first child's angle
|
|
484
|
+
const a0 = angle.get(c[0]);
|
|
485
|
+
const unwrapped = c.map(ch => unwrapAround(a0, angle.get(ch)));
|
|
486
|
+
const mean = unwrapped.reduce((s, v) => s + v, 0) / unwrapped.length;
|
|
487
|
+
angle.set(id, norm(mean));
|
|
488
|
+
}
|
|
489
|
+
})(root);
|
|
490
|
+
|
|
491
|
+
return angle;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// getArcsFan.js
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Build arcs like APE's circular.plot:
|
|
498
|
+
* For each internal parent, draw a single arc at radius=parent.r
|
|
499
|
+
* going from first child's angle to last child's angle in child order.
|
|
500
|
+
* If last < first (wrap), we draw CW (decreasing) to stay on the block.
|
|
501
|
+
*
|
|
502
|
+
* pd: array with { thisId, parentId, r, children[], angle }
|
|
503
|
+
* returns: [{ parentId, thisId, radius, start, end, sweep }]
|
|
504
|
+
* where sweep=0 means CCW (start→end increasing),
|
|
505
|
+
* sweep=1 means CW (start→end decreasing across wrap).
|
|
506
|
+
*/
|
|
507
|
+
function getArcsFan(pd) {
|
|
508
|
+
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
509
|
+
const arcs = [];
|
|
510
|
+
|
|
511
|
+
for (const p of pd) {
|
|
512
|
+
const c = p.children || [];
|
|
513
|
+
if (c.length < 2) continue;
|
|
514
|
+
const A = c.map(id => byId.get(id)?.angle).filter(a => a != null);
|
|
515
|
+
if (A.length < 2 || !isFinite(p.r) || p.r <= 0) continue;
|
|
516
|
+
|
|
517
|
+
// Children are contiguous in tip order; take first and last
|
|
518
|
+
let start = A[0];
|
|
519
|
+
let end = A[A.length - 1];
|
|
520
|
+
|
|
521
|
+
// Decide direction like APE’s seq(start, end):
|
|
522
|
+
// if end >= start → CCW; else CW across wrap.
|
|
523
|
+
const sweep = end >= start ? 0 : 1;
|
|
524
|
+
|
|
525
|
+
arcs.push({
|
|
526
|
+
parentId: p.parentId,
|
|
527
|
+
thisId: p.thisId,
|
|
528
|
+
radius: p.r,
|
|
529
|
+
start, end, sweep
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return arcs;
|
|
533
|
+
}
|
|
534
|
+
|
|
429
535
|
/**
|
|
430
536
|
* Simple wrapper for radial layout:
|
|
431
537
|
* - data: per-node { angle, r, x, y, ... }
|
|
@@ -433,12 +539,29 @@ function getChildArcs(pd) {
|
|
|
433
539
|
* - arcs: per-parent arcs spanning all children at parent's radius
|
|
434
540
|
* - child_arcs: per-child half-arcs (parent.angle → child.angle) at parent's radius
|
|
435
541
|
*/
|
|
436
|
-
function radialLayout(node) {
|
|
542
|
+
function radialLayout(node, opts = {}) {
|
|
437
543
|
const data = {};
|
|
438
544
|
data.data = radialData(node);
|
|
439
545
|
data.radii = getRadii(node);
|
|
440
546
|
data.arcs = getArcs(data.data);
|
|
441
547
|
data.child_arcs = getChildArcs(data.data);
|
|
548
|
+
|
|
549
|
+
const pd = fortify(node, true);
|
|
550
|
+
const angleMap = fanAngles(pd, {
|
|
551
|
+
openAngleDeg: opts.openAngleDeg ?? 0,
|
|
552
|
+
rotateDeg: opts.rotateDeg ?? 0
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// stamp angles + x,y back onto pd (r stays your cumulative edge length)
|
|
556
|
+
for (const d of pd) {
|
|
557
|
+
d.angle = angleMap.get(d.thisId) ?? 0;
|
|
558
|
+
d.x = d.r * Math.cos(d.angle);
|
|
559
|
+
d.y = d.r * Math.sin(d.angle);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
data.data_pd = pd;
|
|
563
|
+
data.arcs_fan = getArcsFan(pd);
|
|
564
|
+
|
|
442
565
|
return data;
|
|
443
566
|
}
|
|
444
567
|
|
|
@@ -1482,7 +1605,7 @@ function drawPhylogeny(
|
|
|
1482
1605
|
const tipByLabel = new Map(
|
|
1483
1606
|
unrootedPhylo.data.filter((d) => d.isTip).map((d) => [d.thisLabel, d])
|
|
1484
1607
|
);
|
|
1485
|
-
const rootToTip = makeRootToTipGetter(byId
|
|
1608
|
+
const rootToTip = makeRootToTipGetter(byId);
|
|
1486
1609
|
|
|
1487
1610
|
if (showTooltips) {
|
|
1488
1611
|
nodes
|