@euphrasiologist/lwphylo 1.2.7 → 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 +140 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +140 -106
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -174,16 +174,19 @@ 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
|
-
// describeArcSweep.js
|
|
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
|
+
|
|
178
181
|
function describeArcSweep(cx, cy, r, a0, a1, sweep /*0=CCW,1=CW*/) {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
const delta = sweep === 0 ? norm(a1 - a0) : norm(a0 - a1); // magnitude on chosen sweep
|
|
182
|
-
if (delta < 1e-9 || r <= 0) return "";
|
|
182
|
+
const delta = sweep === 0 ? norm$1(a1 - a0) : norm$1(a0 - a1);
|
|
183
|
+
if (!(r > 0) || delta < 1e-9) return "";
|
|
183
184
|
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
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}`;
|
|
187
190
|
}
|
|
188
191
|
|
|
189
192
|
/**
|
|
@@ -363,6 +366,37 @@ function getRadii(node) {
|
|
|
363
366
|
return segments;
|
|
364
367
|
}
|
|
365
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
|
+
|
|
366
400
|
/**
|
|
367
401
|
* Build arc descriptors for each internal parent:
|
|
368
402
|
* - One arc per internal node at radius = parent.r
|
|
@@ -429,41 +463,48 @@ function getArcs(pd) {
|
|
|
429
463
|
}
|
|
430
464
|
|
|
431
465
|
/**
|
|
432
|
-
*
|
|
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π
|
|
433
471
|
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* segment that meets the child's spoke and is ideal for root→tip highlighting.
|
|
437
|
-
*
|
|
438
|
-
* Input: pd — the array returned by radialData(node) (each row has .thisId, .parentId, .angle, .r)
|
|
439
|
-
* 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}]
|
|
440
474
|
*/
|
|
441
|
-
function
|
|
475
|
+
function getArcsFan(pd) {
|
|
442
476
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
443
477
|
const arcs = [];
|
|
444
478
|
|
|
445
|
-
for (const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
|
449
490
|
|
|
450
491
|
arcs.push({
|
|
451
|
-
parentId:
|
|
452
|
-
|
|
453
|
-
radius:
|
|
454
|
-
start
|
|
455
|
-
end
|
|
492
|
+
parentId: p.parentId,
|
|
493
|
+
thisId: p.thisId,
|
|
494
|
+
radius: p.r,
|
|
495
|
+
start,
|
|
496
|
+
end,
|
|
497
|
+
sweep
|
|
456
498
|
});
|
|
457
499
|
}
|
|
458
|
-
|
|
459
500
|
return arcs;
|
|
460
501
|
}
|
|
461
502
|
|
|
462
|
-
//
|
|
503
|
+
// APE-like "fan" angles: tips evenly spaced with open-angle gap & rotation,
|
|
504
|
+
// internal nodes = arithmetic mean of unwrapped child angles.
|
|
463
505
|
const TAU = Math.PI * 2;
|
|
464
506
|
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
465
507
|
|
|
466
|
-
// Unwrap angles around a reference so they sit within [ref-π, ref+π]
|
|
467
508
|
function unwrapAround(ref, a) {
|
|
468
509
|
let x = a;
|
|
469
510
|
while (x < ref - Math.PI) x += TAU;
|
|
@@ -471,26 +512,17 @@ function unwrapAround(ref, a) {
|
|
|
471
512
|
return x;
|
|
472
513
|
}
|
|
473
514
|
|
|
474
|
-
/**
|
|
475
|
-
* Compute APE "fan" compatible angles:
|
|
476
|
-
* - Tips evenly spaced over [0, span] where span = 2π*(1 - 1/Ntip) - gap
|
|
477
|
-
* - Then + rotate (radians)
|
|
478
|
-
* - Internal nodes = arithmetic mean of child angles (unwrapped)
|
|
479
|
-
*
|
|
480
|
-
* pd: fortified nodes array (has thisId, parentId, children[])
|
|
481
|
-
* opts: { openAngleDeg=0, rotateDeg=0 }
|
|
482
|
-
* returns: Map(nodeId -> angle)
|
|
483
|
-
*/
|
|
484
515
|
function fanAngles(pd, opts = {}) {
|
|
485
516
|
const { openAngleDeg = 0, rotateDeg = 0 } = opts;
|
|
486
517
|
const gap = (openAngleDeg / 360) * TAU;
|
|
487
|
-
const
|
|
518
|
+
const rot = (rotateDeg / 360) * TAU;
|
|
488
519
|
|
|
489
|
-
//
|
|
520
|
+
// root + children index
|
|
490
521
|
let root = null;
|
|
491
|
-
const kids = new Map(pd.map(d => [d.thisId, d.children || []]));
|
|
522
|
+
const kids = new Map(pd.map((d) => [d.thisId, d.children || []]));
|
|
492
523
|
for (const d of pd) if (d.parentId == null) { root = d.thisId; break; }
|
|
493
524
|
|
|
525
|
+
// tip order (DFS left→right like your fortify)
|
|
494
526
|
const tipIds = [];
|
|
495
527
|
(function dfs(id) {
|
|
496
528
|
const c = kids.get(id) || [];
|
|
@@ -499,103 +531,105 @@ function fanAngles(pd, opts = {}) {
|
|
|
499
531
|
})(root);
|
|
500
532
|
|
|
501
533
|
const N = Math.max(1, tipIds.length);
|
|
502
|
-
|
|
503
|
-
const maxA = TAU * (1 - 1 / N) - gap;
|
|
534
|
+
const maxA = TAU * (1 - 1 / N) - gap; // note: no last-step overlap
|
|
504
535
|
const step = N > 1 ? maxA / (N - 1) : 0;
|
|
505
536
|
|
|
506
537
|
const angle = new Map();
|
|
507
|
-
tipIds.forEach((id, i) =>
|
|
508
|
-
angle.set(id, norm(i * step + rotate));
|
|
509
|
-
});
|
|
538
|
+
tipIds.forEach((id, i) => angle.set(id, norm(i * step + rot)));
|
|
510
539
|
|
|
511
|
-
//
|
|
540
|
+
// internal nodes: arithmetic mean of child angles (unwrapped)
|
|
512
541
|
(function setInternal(id) {
|
|
513
542
|
const c = kids.get(id) || [];
|
|
514
543
|
for (const ch of c) setInternal(ch);
|
|
515
|
-
if (c.length
|
|
516
|
-
// unwrap child angles around the first child's angle
|
|
544
|
+
if (c.length) {
|
|
517
545
|
const a0 = angle.get(c[0]);
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
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));
|
|
521
548
|
}
|
|
522
549
|
})(root);
|
|
523
550
|
|
|
524
551
|
return angle;
|
|
525
552
|
}
|
|
526
553
|
|
|
527
|
-
// getArcsFan.js
|
|
528
|
-
|
|
529
554
|
/**
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
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.
|
|
534
560
|
*
|
|
535
|
-
* pd
|
|
536
|
-
*
|
|
537
|
-
* where sweep=0 means CCW (start→end increasing),
|
|
538
|
-
* 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 }]
|
|
539
563
|
*/
|
|
540
|
-
function
|
|
564
|
+
function getChildArcs(pd) {
|
|
541
565
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
542
566
|
const arcs = [];
|
|
543
567
|
|
|
544
|
-
for (const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (A.length < 2 || !isFinite(p.r) || p.r <= 0) continue;
|
|
549
|
-
|
|
550
|
-
// Children are contiguous in tip order; take first and last
|
|
551
|
-
let start = A[0];
|
|
552
|
-
let end = A[A.length - 1];
|
|
553
|
-
|
|
554
|
-
// Decide direction like APE’s seq(start, end):
|
|
555
|
-
// if end >= start → CCW; else CW across wrap.
|
|
556
|
-
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;
|
|
557
572
|
|
|
558
573
|
arcs.push({
|
|
559
|
-
parentId:
|
|
560
|
-
|
|
561
|
-
radius:
|
|
562
|
-
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)
|
|
563
579
|
});
|
|
564
580
|
}
|
|
581
|
+
|
|
565
582
|
return arcs;
|
|
566
583
|
}
|
|
567
584
|
|
|
585
|
+
// src/radial/radialLayout.js
|
|
586
|
+
|
|
568
587
|
/**
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
573
|
-
*
|
|
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)
|
|
574
594
|
*/
|
|
575
|
-
function radialLayout(node, opts =
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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
|
+
}
|
|
593
617
|
}
|
|
594
618
|
|
|
595
|
-
|
|
596
|
-
|
|
619
|
+
// Build spokes using the angles currently on pd
|
|
620
|
+
const radii = (angleStrategy === "fan")
|
|
621
|
+
? getRadiiFromPd(pd)
|
|
622
|
+
: getRadii(node);
|
|
597
623
|
|
|
598
|
-
|
|
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 };
|
|
599
633
|
}
|
|
600
634
|
|
|
601
635
|
/**
|