@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.js
CHANGED
|
@@ -153,16 +153,19 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
153
153
|
return `M ${p0.x} ${p0.y} A ${radius} ${radius} 0 ${largeArcFlag} ${sweepFlag} ${p1.x} ${p1.y}`;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
// describeArcSweep.js
|
|
156
|
+
// src/radial/describeArcSweep.js
|
|
157
|
+
const TAU$1 = Math.PI * 2;
|
|
158
|
+
const norm$1 = (t) => ((t % TAU$1) + TAU$1) % TAU$1;
|
|
159
|
+
|
|
157
160
|
function describeArcSweep(cx, cy, r, a0, a1, sweep /*0=CCW,1=CW*/) {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
const delta = sweep === 0 ? norm(a1 - a0) : norm(a0 - a1); // magnitude on chosen sweep
|
|
161
|
-
if (delta < 1e-9 || r <= 0) return "";
|
|
161
|
+
const delta = sweep === 0 ? norm$1(a1 - a0) : norm$1(a0 - a1);
|
|
162
|
+
if (!(r > 0) || delta < 1e-9) return "";
|
|
162
163
|
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
164
|
+
|
|
165
|
+
const x0 = cx + r * Math.cos(a0), y0 = cy - r * Math.sin(a0);
|
|
166
|
+
const x1 = cx + r * Math.cos(a1), y1 = cy - r * Math.sin(a1);
|
|
167
|
+
|
|
168
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${sweep} ${x1} ${y1}`;
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
/**
|
|
@@ -342,6 +345,37 @@ function getRadii(node) {
|
|
|
342
345
|
return segments;
|
|
343
346
|
}
|
|
344
347
|
|
|
348
|
+
// src/radial/getRadiiFromPd.js
|
|
349
|
+
/**
|
|
350
|
+
* Build per-edge spokes using the *current* pd angles/r.
|
|
351
|
+
* Output: [{ parentId, childId, x0,y0,x1,y1, isTip }]
|
|
352
|
+
*/
|
|
353
|
+
function getRadiiFromPd(pd) {
|
|
354
|
+
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
355
|
+
const root = pd.find(d => d.parentId == null)?.thisId;
|
|
356
|
+
|
|
357
|
+
const segments = [];
|
|
358
|
+
for (const d of pd) {
|
|
359
|
+
if (d.thisId === root) continue;
|
|
360
|
+
const parent = byId.get(d.parentId);
|
|
361
|
+
if (!parent) continue;
|
|
362
|
+
|
|
363
|
+
const theta = d.angle;
|
|
364
|
+
const r0 = parent.r, r1 = d.r;
|
|
365
|
+
|
|
366
|
+
segments.push({
|
|
367
|
+
parentId: parent.thisId,
|
|
368
|
+
childId: d.thisId,
|
|
369
|
+
x0: r0 * Math.cos(theta),
|
|
370
|
+
y0: r0 * Math.sin(theta),
|
|
371
|
+
x1: r1 * Math.cos(theta),
|
|
372
|
+
y1: r1 * Math.sin(theta),
|
|
373
|
+
isTip: !!d.isTip
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return segments;
|
|
377
|
+
}
|
|
378
|
+
|
|
345
379
|
/**
|
|
346
380
|
* Build arc descriptors for each internal parent:
|
|
347
381
|
* - One arc per internal node at radius = parent.r
|
|
@@ -408,41 +442,48 @@ function getArcs(pd) {
|
|
|
408
442
|
}
|
|
409
443
|
|
|
410
444
|
/**
|
|
411
|
-
*
|
|
445
|
+
* Build APE-like block arcs per internal parent:
|
|
446
|
+
* radius = parent.r
|
|
447
|
+
* start = first child's angle
|
|
448
|
+
* end = last child's angle
|
|
449
|
+
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
412
450
|
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* segment that meets the child's spoke and is ideal for root→tip highlighting.
|
|
416
|
-
*
|
|
417
|
-
* Input: pd — the array returned by radialData(node) (each row has .thisId, .parentId, .angle, .r)
|
|
418
|
-
* Output: [{ parentId, childId, radius, start, end }]
|
|
451
|
+
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
452
|
+
* @returns {Array} [{parentId,thisId,radius,start,end,sweep}]
|
|
419
453
|
*/
|
|
420
|
-
function
|
|
454
|
+
function getArcsFan(pd) {
|
|
421
455
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
422
456
|
const arcs = [];
|
|
423
457
|
|
|
424
|
-
for (const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
458
|
+
for (const p of pd) {
|
|
459
|
+
const c = p.children || [];
|
|
460
|
+
if (c.length < 2 || !(p.r > 0)) continue;
|
|
461
|
+
|
|
462
|
+
const first = byId.get(c[0])?.angle;
|
|
463
|
+
const last = byId.get(c[c.length - 1])?.angle;
|
|
464
|
+
if (first == null || last == null) continue;
|
|
465
|
+
|
|
466
|
+
const start = first;
|
|
467
|
+
const end = last;
|
|
468
|
+
const sweep = end >= start ? 0 : 1; // CW if wrapped
|
|
428
469
|
|
|
429
470
|
arcs.push({
|
|
430
|
-
parentId:
|
|
431
|
-
|
|
432
|
-
radius:
|
|
433
|
-
start
|
|
434
|
-
end
|
|
471
|
+
parentId: p.parentId,
|
|
472
|
+
thisId: p.thisId,
|
|
473
|
+
radius: p.r,
|
|
474
|
+
start,
|
|
475
|
+
end,
|
|
476
|
+
sweep
|
|
435
477
|
});
|
|
436
478
|
}
|
|
437
|
-
|
|
438
479
|
return arcs;
|
|
439
480
|
}
|
|
440
481
|
|
|
441
|
-
//
|
|
482
|
+
// APE-like "fan" angles: tips evenly spaced with open-angle gap & rotation,
|
|
483
|
+
// internal nodes = arithmetic mean of unwrapped child angles.
|
|
442
484
|
const TAU = Math.PI * 2;
|
|
443
485
|
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
444
486
|
|
|
445
|
-
// Unwrap angles around a reference so they sit within [ref-π, ref+π]
|
|
446
487
|
function unwrapAround(ref, a) {
|
|
447
488
|
let x = a;
|
|
448
489
|
while (x < ref - Math.PI) x += TAU;
|
|
@@ -450,26 +491,17 @@ function unwrapAround(ref, a) {
|
|
|
450
491
|
return x;
|
|
451
492
|
}
|
|
452
493
|
|
|
453
|
-
/**
|
|
454
|
-
* Compute APE "fan" compatible angles:
|
|
455
|
-
* - Tips evenly spaced over [0, span] where span = 2π*(1 - 1/Ntip) - gap
|
|
456
|
-
* - Then + rotate (radians)
|
|
457
|
-
* - Internal nodes = arithmetic mean of child angles (unwrapped)
|
|
458
|
-
*
|
|
459
|
-
* pd: fortified nodes array (has thisId, parentId, children[])
|
|
460
|
-
* opts: { openAngleDeg=0, rotateDeg=0 }
|
|
461
|
-
* returns: Map(nodeId -> angle)
|
|
462
|
-
*/
|
|
463
494
|
function fanAngles(pd, opts = {}) {
|
|
464
495
|
const { openAngleDeg = 0, rotateDeg = 0 } = opts;
|
|
465
496
|
const gap = (openAngleDeg / 360) * TAU;
|
|
466
|
-
const
|
|
497
|
+
const rot = (rotateDeg / 360) * TAU;
|
|
467
498
|
|
|
468
|
-
//
|
|
499
|
+
// root + children index
|
|
469
500
|
let root = null;
|
|
470
|
-
const kids = new Map(pd.map(d => [d.thisId, d.children || []]));
|
|
501
|
+
const kids = new Map(pd.map((d) => [d.thisId, d.children || []]));
|
|
471
502
|
for (const d of pd) if (d.parentId == null) { root = d.thisId; break; }
|
|
472
503
|
|
|
504
|
+
// tip order (DFS left→right like your fortify)
|
|
473
505
|
const tipIds = [];
|
|
474
506
|
(function dfs(id) {
|
|
475
507
|
const c = kids.get(id) || [];
|
|
@@ -478,103 +510,105 @@ function fanAngles(pd, opts = {}) {
|
|
|
478
510
|
})(root);
|
|
479
511
|
|
|
480
512
|
const N = Math.max(1, tipIds.length);
|
|
481
|
-
|
|
482
|
-
const maxA = TAU * (1 - 1 / N) - gap;
|
|
513
|
+
const maxA = TAU * (1 - 1 / N) - gap; // note: no last-step overlap
|
|
483
514
|
const step = N > 1 ? maxA / (N - 1) : 0;
|
|
484
515
|
|
|
485
516
|
const angle = new Map();
|
|
486
|
-
tipIds.forEach((id, i) =>
|
|
487
|
-
angle.set(id, norm(i * step + rotate));
|
|
488
|
-
});
|
|
517
|
+
tipIds.forEach((id, i) => angle.set(id, norm(i * step + rot)));
|
|
489
518
|
|
|
490
|
-
//
|
|
519
|
+
// internal nodes: arithmetic mean of child angles (unwrapped)
|
|
491
520
|
(function setInternal(id) {
|
|
492
521
|
const c = kids.get(id) || [];
|
|
493
522
|
for (const ch of c) setInternal(ch);
|
|
494
|
-
if (c.length
|
|
495
|
-
// unwrap child angles around the first child's angle
|
|
523
|
+
if (c.length) {
|
|
496
524
|
const a0 = angle.get(c[0]);
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
angle.set(id, norm(mean));
|
|
525
|
+
const arr = c.map((ch) => unwrapAround(a0, angle.get(ch)));
|
|
526
|
+
angle.set(id, norm(arr.reduce((s, v) => s + v, 0) / arr.length));
|
|
500
527
|
}
|
|
501
528
|
})(root);
|
|
502
529
|
|
|
503
530
|
return angle;
|
|
504
531
|
}
|
|
505
532
|
|
|
506
|
-
// getArcsFan.js
|
|
507
|
-
|
|
508
533
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
534
|
+
* Per-child "half" arcs for radial trees.
|
|
535
|
+
*
|
|
536
|
+
* For each non-root node (child), emit an arc at the PARENT's radius that
|
|
537
|
+
* spans between the parent's angle and the child's angle. This is the arc
|
|
538
|
+
* segment that meets the child's spoke and is ideal for root→tip highlighting.
|
|
513
539
|
*
|
|
514
|
-
* pd
|
|
515
|
-
*
|
|
516
|
-
* where sweep=0 means CCW (start→end increasing),
|
|
517
|
-
* sweep=1 means CW (start→end decreasing across wrap).
|
|
540
|
+
* Input: pd — the array returned by radialData(node) (each row has .thisId, .parentId, .angle, .r)
|
|
541
|
+
* Output: [{ parentId, childId, radius, start, end }]
|
|
518
542
|
*/
|
|
519
|
-
function
|
|
543
|
+
function getChildArcs(pd) {
|
|
520
544
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
521
545
|
const arcs = [];
|
|
522
546
|
|
|
523
|
-
for (const
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (A.length < 2 || !isFinite(p.r) || p.r <= 0) continue;
|
|
528
|
-
|
|
529
|
-
// Children are contiguous in tip order; take first and last
|
|
530
|
-
let start = A[0];
|
|
531
|
-
let end = A[A.length - 1];
|
|
532
|
-
|
|
533
|
-
// Decide direction like APE’s seq(start, end):
|
|
534
|
-
// if end >= start → CCW; else CW across wrap.
|
|
535
|
-
const sweep = end >= start ? 0 : 1;
|
|
547
|
+
for (const child of pd) {
|
|
548
|
+
if (child.parentId == null) continue; // skip root
|
|
549
|
+
const parent = byId.get(child.parentId);
|
|
550
|
+
if (!parent) continue;
|
|
536
551
|
|
|
537
552
|
arcs.push({
|
|
538
|
-
parentId:
|
|
539
|
-
|
|
540
|
-
radius:
|
|
541
|
-
start
|
|
553
|
+
parentId: parent.thisId,
|
|
554
|
+
childId: child.thisId,
|
|
555
|
+
radius: parent.r, // draw on the parent's circle
|
|
556
|
+
start: parent.angle, // start at parent's angle
|
|
557
|
+
end: child.angle // end at child's angle (describeArc will choose the shortest CCW span)
|
|
542
558
|
});
|
|
543
559
|
}
|
|
560
|
+
|
|
544
561
|
return arcs;
|
|
545
562
|
}
|
|
546
563
|
|
|
564
|
+
// src/radial/radialLayout.js
|
|
565
|
+
|
|
547
566
|
/**
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
567
|
+
* radialLayout(node, opts?)
|
|
568
|
+
* opts:
|
|
569
|
+
* - angleStrategy: "cmean" (default, your current) | "fan" (APE-like)
|
|
570
|
+
* - arcsStyle: "shortest" (default) | "fan" (block arcs, supports wrap)
|
|
571
|
+
* - openAngleDeg: number (gap wedge for "fan" angles)
|
|
572
|
+
* - rotateDeg: number (rotation for "fan" angles)
|
|
553
573
|
*/
|
|
554
|
-
function radialLayout(node, opts =
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
574
|
+
function radialLayout(node, opts = {}) {
|
|
575
|
+
const {
|
|
576
|
+
angleStrategy = "cmean",
|
|
577
|
+
arcsStyle = "shortest",
|
|
578
|
+
openAngleDeg = 0,
|
|
579
|
+
rotateDeg = 0
|
|
580
|
+
} = opts;
|
|
581
|
+
|
|
582
|
+
// Start with your current enriched nodes (angle + r from radialData)
|
|
583
|
+
const pd = radialData(node);
|
|
584
|
+
|
|
585
|
+
if (angleStrategy === "fan") {
|
|
586
|
+
// overwrite angles with APE-like fan angles; keep radii r as-is
|
|
587
|
+
const angleMap = fanAngles(pd, { openAngleDeg, rotateDeg });
|
|
588
|
+
for (const d of pd) {
|
|
589
|
+
const a = angleMap.get(d.thisId);
|
|
590
|
+
if (a != null) {
|
|
591
|
+
d.angle = a;
|
|
592
|
+
d.x = d.r * Math.cos(a);
|
|
593
|
+
d.y = d.r * Math.sin(a);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
572
596
|
}
|
|
573
597
|
|
|
574
|
-
|
|
575
|
-
|
|
598
|
+
// Build spokes using the angles currently on pd
|
|
599
|
+
const radii = (angleStrategy === "fan")
|
|
600
|
+
? getRadiiFromPd(pd)
|
|
601
|
+
: getRadii(node);
|
|
576
602
|
|
|
577
|
-
|
|
603
|
+
// Choose arc builder
|
|
604
|
+
const arcs = (arcsStyle === "fan")
|
|
605
|
+
? getArcsFan(pd)
|
|
606
|
+
: getArcs(pd);
|
|
607
|
+
|
|
608
|
+
// per-child arcs for half-arc highlighting if you already use them
|
|
609
|
+
const child_arcs = getChildArcs(pd);
|
|
610
|
+
|
|
611
|
+
return { data: pd, radii, arcs, child_arcs };
|
|
578
612
|
}
|
|
579
613
|
|
|
580
614
|
/**
|