@euphrasiologist/lwphylo 1.2.6 → 1.2.8
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 +129 -98
- package/dist/drawPhylogeny.cjs.map +1 -1
- package/dist/drawPhylogeny.esm.js +129 -98
- 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 +145 -98
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +145 -99
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -174,6 +174,21 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
174
174
|
return `M ${p0.x} ${p0.y} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${p1.x} ${p1.y}`;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// src/radial/describeArcSweep.js
|
|
178
|
+
const TAU$1 = Math.PI * 2;
|
|
179
|
+
const norm$1 = (t) => ((t % TAU$1) + TAU$1) % TAU$1;
|
|
180
|
+
|
|
181
|
+
function describeArcSweep(cx, cy, r, a0, a1, sweep /*0=CCW,1=CW*/) {
|
|
182
|
+
const delta = sweep === 0 ? norm$1(a1 - a0) : norm$1(a0 - a1);
|
|
183
|
+
if (!(r > 0) || delta < 1e-9) return "";
|
|
184
|
+
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
185
|
+
|
|
186
|
+
const x0 = cx + r * Math.cos(a0), y0 = cy - r * Math.sin(a0);
|
|
187
|
+
const x1 = cx + r * Math.cos(a1), y1 = cy - r * Math.sin(a1);
|
|
188
|
+
|
|
189
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${sweep} ${x1} ${y1}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
177
192
|
/**
|
|
178
193
|
* Recursive function for pre-order traversal of tree (returns array)
|
|
179
194
|
*/
|
|
@@ -351,6 +366,37 @@ function getRadii(node) {
|
|
|
351
366
|
return segments;
|
|
352
367
|
}
|
|
353
368
|
|
|
369
|
+
// src/radial/getRadiiFromPd.js
|
|
370
|
+
/**
|
|
371
|
+
* Build per-edge spokes using the *current* pd angles/r.
|
|
372
|
+
* Output: [{ parentId, childId, x0,y0,x1,y1, isTip }]
|
|
373
|
+
*/
|
|
374
|
+
function getRadiiFromPd(pd) {
|
|
375
|
+
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
376
|
+
const root = pd.find(d => d.parentId == null)?.thisId;
|
|
377
|
+
|
|
378
|
+
const segments = [];
|
|
379
|
+
for (const d of pd) {
|
|
380
|
+
if (d.thisId === root) continue;
|
|
381
|
+
const parent = byId.get(d.parentId);
|
|
382
|
+
if (!parent) continue;
|
|
383
|
+
|
|
384
|
+
const theta = d.angle;
|
|
385
|
+
const r0 = parent.r, r1 = d.r;
|
|
386
|
+
|
|
387
|
+
segments.push({
|
|
388
|
+
parentId: parent.thisId,
|
|
389
|
+
childId: d.thisId,
|
|
390
|
+
x0: r0 * Math.cos(theta),
|
|
391
|
+
y0: r0 * Math.sin(theta),
|
|
392
|
+
x1: r1 * Math.cos(theta),
|
|
393
|
+
y1: r1 * Math.sin(theta),
|
|
394
|
+
isTip: !!d.isTip
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return segments;
|
|
398
|
+
}
|
|
399
|
+
|
|
354
400
|
/**
|
|
355
401
|
* Build arc descriptors for each internal parent:
|
|
356
402
|
* - One arc per internal node at radius = parent.r
|
|
@@ -417,41 +463,48 @@ function getArcs(pd) {
|
|
|
417
463
|
}
|
|
418
464
|
|
|
419
465
|
/**
|
|
420
|
-
*
|
|
466
|
+
* Build APE-like block arcs per internal parent:
|
|
467
|
+
* radius = parent.r
|
|
468
|
+
* start = first child's angle
|
|
469
|
+
* end = last child's angle
|
|
470
|
+
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
421
471
|
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* segment that meets the child's spoke and is ideal for root→tip highlighting.
|
|
425
|
-
*
|
|
426
|
-
* Input: pd — the array returned by radialData(node) (each row has .thisId, .parentId, .angle, .r)
|
|
427
|
-
* Output: [{ parentId, childId, radius, start, end }]
|
|
472
|
+
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
473
|
+
* @returns {Array} [{parentId,thisId,radius,start,end,sweep}]
|
|
428
474
|
*/
|
|
429
|
-
function
|
|
475
|
+
function getArcsFan(pd) {
|
|
430
476
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
431
477
|
const arcs = [];
|
|
432
478
|
|
|
433
|
-
for (const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
479
|
+
for (const p of pd) {
|
|
480
|
+
const c = p.children || [];
|
|
481
|
+
if (c.length < 2 || !(p.r > 0)) continue;
|
|
482
|
+
|
|
483
|
+
const first = byId.get(c[0])?.angle;
|
|
484
|
+
const last = byId.get(c[c.length - 1])?.angle;
|
|
485
|
+
if (first == null || last == null) continue;
|
|
486
|
+
|
|
487
|
+
const start = first;
|
|
488
|
+
const end = last;
|
|
489
|
+
const sweep = end >= start ? 0 : 1; // CW if wrapped
|
|
437
490
|
|
|
438
491
|
arcs.push({
|
|
439
|
-
parentId:
|
|
440
|
-
|
|
441
|
-
radius:
|
|
442
|
-
start
|
|
443
|
-
end
|
|
492
|
+
parentId: p.parentId,
|
|
493
|
+
thisId: p.thisId,
|
|
494
|
+
radius: p.r,
|
|
495
|
+
start,
|
|
496
|
+
end,
|
|
497
|
+
sweep
|
|
444
498
|
});
|
|
445
499
|
}
|
|
446
|
-
|
|
447
500
|
return arcs;
|
|
448
501
|
}
|
|
449
502
|
|
|
450
|
-
//
|
|
503
|
+
// APE-like "fan" angles: tips evenly spaced with open-angle gap & rotation,
|
|
504
|
+
// internal nodes = arithmetic mean of unwrapped child angles.
|
|
451
505
|
const TAU = Math.PI * 2;
|
|
452
506
|
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
453
507
|
|
|
454
|
-
// Unwrap angles around a reference so they sit within [ref-π, ref+π]
|
|
455
508
|
function unwrapAround(ref, a) {
|
|
456
509
|
let x = a;
|
|
457
510
|
while (x < ref - Math.PI) x += TAU;
|
|
@@ -459,26 +512,17 @@ function unwrapAround(ref, a) {
|
|
|
459
512
|
return x;
|
|
460
513
|
}
|
|
461
514
|
|
|
462
|
-
/**
|
|
463
|
-
* Compute APE "fan" compatible angles:
|
|
464
|
-
* - Tips evenly spaced over [0, span] where span = 2π*(1 - 1/Ntip) - gap
|
|
465
|
-
* - Then + rotate (radians)
|
|
466
|
-
* - Internal nodes = arithmetic mean of child angles (unwrapped)
|
|
467
|
-
*
|
|
468
|
-
* pd: fortified nodes array (has thisId, parentId, children[])
|
|
469
|
-
* opts: { openAngleDeg=0, rotateDeg=0 }
|
|
470
|
-
* returns: Map(nodeId -> angle)
|
|
471
|
-
*/
|
|
472
515
|
function fanAngles(pd, opts = {}) {
|
|
473
516
|
const { openAngleDeg = 0, rotateDeg = 0 } = opts;
|
|
474
517
|
const gap = (openAngleDeg / 360) * TAU;
|
|
475
|
-
const
|
|
518
|
+
const rot = (rotateDeg / 360) * TAU;
|
|
476
519
|
|
|
477
|
-
//
|
|
520
|
+
// root + children index
|
|
478
521
|
let root = null;
|
|
479
|
-
const kids = new Map(pd.map(d => [d.thisId, d.children || []]));
|
|
522
|
+
const kids = new Map(pd.map((d) => [d.thisId, d.children || []]));
|
|
480
523
|
for (const d of pd) if (d.parentId == null) { root = d.thisId; break; }
|
|
481
524
|
|
|
525
|
+
// tip order (DFS left→right like your fortify)
|
|
482
526
|
const tipIds = [];
|
|
483
527
|
(function dfs(id) {
|
|
484
528
|
const c = kids.get(id) || [];
|
|
@@ -487,103 +531,105 @@ function fanAngles(pd, opts = {}) {
|
|
|
487
531
|
})(root);
|
|
488
532
|
|
|
489
533
|
const N = Math.max(1, tipIds.length);
|
|
490
|
-
|
|
491
|
-
const maxA = TAU * (1 - 1 / N) - gap;
|
|
534
|
+
const maxA = TAU * (1 - 1 / N) - gap; // note: no last-step overlap
|
|
492
535
|
const step = N > 1 ? maxA / (N - 1) : 0;
|
|
493
536
|
|
|
494
537
|
const angle = new Map();
|
|
495
|
-
tipIds.forEach((id, i) =>
|
|
496
|
-
angle.set(id, norm(i * step + rotate));
|
|
497
|
-
});
|
|
538
|
+
tipIds.forEach((id, i) => angle.set(id, norm(i * step + rot)));
|
|
498
539
|
|
|
499
|
-
//
|
|
540
|
+
// internal nodes: arithmetic mean of child angles (unwrapped)
|
|
500
541
|
(function setInternal(id) {
|
|
501
542
|
const c = kids.get(id) || [];
|
|
502
543
|
for (const ch of c) setInternal(ch);
|
|
503
|
-
if (c.length
|
|
504
|
-
// unwrap child angles around the first child's angle
|
|
544
|
+
if (c.length) {
|
|
505
545
|
const a0 = angle.get(c[0]);
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
angle.set(id, norm(mean));
|
|
546
|
+
const arr = c.map((ch) => unwrapAround(a0, angle.get(ch)));
|
|
547
|
+
angle.set(id, norm(arr.reduce((s, v) => s + v, 0) / arr.length));
|
|
509
548
|
}
|
|
510
549
|
})(root);
|
|
511
550
|
|
|
512
551
|
return angle;
|
|
513
552
|
}
|
|
514
553
|
|
|
515
|
-
// getArcsFan.js
|
|
516
|
-
|
|
517
554
|
/**
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
*
|
|
521
|
-
*
|
|
555
|
+
* Per-child "half" arcs for radial trees.
|
|
556
|
+
*
|
|
557
|
+
* For each non-root node (child), emit an arc at the PARENT's radius that
|
|
558
|
+
* spans between the parent's angle and the child's angle. This is the arc
|
|
559
|
+
* segment that meets the child's spoke and is ideal for root→tip highlighting.
|
|
522
560
|
*
|
|
523
|
-
* pd
|
|
524
|
-
*
|
|
525
|
-
* where sweep=0 means CCW (start→end increasing),
|
|
526
|
-
* sweep=1 means CW (start→end decreasing across wrap).
|
|
561
|
+
* Input: pd — the array returned by radialData(node) (each row has .thisId, .parentId, .angle, .r)
|
|
562
|
+
* Output: [{ parentId, childId, radius, start, end }]
|
|
527
563
|
*/
|
|
528
|
-
function
|
|
564
|
+
function getChildArcs(pd) {
|
|
529
565
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
530
566
|
const arcs = [];
|
|
531
567
|
|
|
532
|
-
for (const
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (A.length < 2 || !isFinite(p.r) || p.r <= 0) continue;
|
|
537
|
-
|
|
538
|
-
// Children are contiguous in tip order; take first and last
|
|
539
|
-
let start = A[0];
|
|
540
|
-
let end = A[A.length - 1];
|
|
541
|
-
|
|
542
|
-
// Decide direction like APE’s seq(start, end):
|
|
543
|
-
// if end >= start → CCW; else CW across wrap.
|
|
544
|
-
const sweep = end >= start ? 0 : 1;
|
|
568
|
+
for (const child of pd) {
|
|
569
|
+
if (child.parentId == null) continue; // skip root
|
|
570
|
+
const parent = byId.get(child.parentId);
|
|
571
|
+
if (!parent) continue;
|
|
545
572
|
|
|
546
573
|
arcs.push({
|
|
547
|
-
parentId:
|
|
548
|
-
|
|
549
|
-
radius:
|
|
550
|
-
start
|
|
574
|
+
parentId: parent.thisId,
|
|
575
|
+
childId: child.thisId,
|
|
576
|
+
radius: parent.r, // draw on the parent's circle
|
|
577
|
+
start: parent.angle, // start at parent's angle
|
|
578
|
+
end: child.angle // end at child's angle (describeArc will choose the shortest CCW span)
|
|
551
579
|
});
|
|
552
580
|
}
|
|
581
|
+
|
|
553
582
|
return arcs;
|
|
554
583
|
}
|
|
555
584
|
|
|
585
|
+
// src/radial/radialLayout.js
|
|
586
|
+
|
|
556
587
|
/**
|
|
557
|
-
*
|
|
558
|
-
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
588
|
+
* radialLayout(node, opts?)
|
|
589
|
+
* opts:
|
|
590
|
+
* - angleStrategy: "cmean" (default, your current) | "fan" (APE-like)
|
|
591
|
+
* - arcsStyle: "shortest" (default) | "fan" (block arcs, supports wrap)
|
|
592
|
+
* - openAngleDeg: number (gap wedge for "fan" angles)
|
|
593
|
+
* - rotateDeg: number (rotation for "fan" angles)
|
|
562
594
|
*/
|
|
563
|
-
function radialLayout(node, opts =
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
595
|
+
function radialLayout(node, opts = {}) {
|
|
596
|
+
const {
|
|
597
|
+
angleStrategy = "cmean",
|
|
598
|
+
arcsStyle = "shortest",
|
|
599
|
+
openAngleDeg = 0,
|
|
600
|
+
rotateDeg = 0
|
|
601
|
+
} = opts;
|
|
602
|
+
|
|
603
|
+
// Start with your current enriched nodes (angle + r from radialData)
|
|
604
|
+
const pd = radialData(node);
|
|
605
|
+
|
|
606
|
+
if (angleStrategy === "fan") {
|
|
607
|
+
// overwrite angles with APE-like fan angles; keep radii r as-is
|
|
608
|
+
const angleMap = fanAngles(pd, { openAngleDeg, rotateDeg });
|
|
609
|
+
for (const d of pd) {
|
|
610
|
+
const a = angleMap.get(d.thisId);
|
|
611
|
+
if (a != null) {
|
|
612
|
+
d.angle = a;
|
|
613
|
+
d.x = d.r * Math.cos(a);
|
|
614
|
+
d.y = d.r * Math.sin(a);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
581
617
|
}
|
|
582
618
|
|
|
583
|
-
|
|
584
|
-
|
|
619
|
+
// Build spokes using the angles currently on pd
|
|
620
|
+
const radii = (angleStrategy === "fan")
|
|
621
|
+
? getRadiiFromPd(pd)
|
|
622
|
+
: getRadii(node);
|
|
585
623
|
|
|
586
|
-
|
|
624
|
+
// Choose arc builder
|
|
625
|
+
const arcs = (arcsStyle === "fan")
|
|
626
|
+
? getArcsFan(pd)
|
|
627
|
+
: getArcs(pd);
|
|
628
|
+
|
|
629
|
+
// per-child arcs for half-arc highlighting if you already use them
|
|
630
|
+
const child_arcs = getChildArcs(pd);
|
|
631
|
+
|
|
632
|
+
return { data: pd, radii, arcs, child_arcs };
|
|
587
633
|
}
|
|
588
634
|
|
|
589
635
|
/**
|
|
@@ -1764,6 +1810,7 @@ function drawPhylogeny(
|
|
|
1764
1810
|
}
|
|
1765
1811
|
|
|
1766
1812
|
exports.describeArc = describeArc;
|
|
1813
|
+
exports.describeArcSweep = describeArcSweep;
|
|
1767
1814
|
exports.drawPhylogeny = drawPhylogeny;
|
|
1768
1815
|
exports.parentFisheye = parentFisheye;
|
|
1769
1816
|
exports.phisheye = phisheye;
|