@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.js
CHANGED
|
@@ -153,6 +153,21 @@ 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
|
+
// src/radial/describeArcSweep.js
|
|
157
|
+
const TAU$1 = Math.PI * 2;
|
|
158
|
+
const norm$1 = (t) => ((t % TAU$1) + TAU$1) % TAU$1;
|
|
159
|
+
|
|
160
|
+
function describeArcSweep(cx, cy, r, a0, a1, sweep /*0=CCW,1=CW*/) {
|
|
161
|
+
const delta = sweep === 0 ? norm$1(a1 - a0) : norm$1(a0 - a1);
|
|
162
|
+
if (!(r > 0) || delta < 1e-9) return "";
|
|
163
|
+
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
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}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
156
171
|
/**
|
|
157
172
|
* Recursive function for pre-order traversal of tree (returns array)
|
|
158
173
|
*/
|
|
@@ -330,6 +345,37 @@ function getRadii(node) {
|
|
|
330
345
|
return segments;
|
|
331
346
|
}
|
|
332
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
|
+
|
|
333
379
|
/**
|
|
334
380
|
* Build arc descriptors for each internal parent:
|
|
335
381
|
* - One arc per internal node at radius = parent.r
|
|
@@ -396,41 +442,48 @@ function getArcs(pd) {
|
|
|
396
442
|
}
|
|
397
443
|
|
|
398
444
|
/**
|
|
399
|
-
*
|
|
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π
|
|
400
450
|
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
403
|
-
* segment that meets the child's spoke and is ideal for root→tip highlighting.
|
|
404
|
-
*
|
|
405
|
-
* Input: pd — the array returned by radialData(node) (each row has .thisId, .parentId, .angle, .r)
|
|
406
|
-
* 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}]
|
|
407
453
|
*/
|
|
408
|
-
function
|
|
454
|
+
function getArcsFan(pd) {
|
|
409
455
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
410
456
|
const arcs = [];
|
|
411
457
|
|
|
412
|
-
for (const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
416
469
|
|
|
417
470
|
arcs.push({
|
|
418
|
-
parentId:
|
|
419
|
-
|
|
420
|
-
radius:
|
|
421
|
-
start
|
|
422
|
-
end
|
|
471
|
+
parentId: p.parentId,
|
|
472
|
+
thisId: p.thisId,
|
|
473
|
+
radius: p.r,
|
|
474
|
+
start,
|
|
475
|
+
end,
|
|
476
|
+
sweep
|
|
423
477
|
});
|
|
424
478
|
}
|
|
425
|
-
|
|
426
479
|
return arcs;
|
|
427
480
|
}
|
|
428
481
|
|
|
429
|
-
//
|
|
482
|
+
// APE-like "fan" angles: tips evenly spaced with open-angle gap & rotation,
|
|
483
|
+
// internal nodes = arithmetic mean of unwrapped child angles.
|
|
430
484
|
const TAU = Math.PI * 2;
|
|
431
485
|
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
432
486
|
|
|
433
|
-
// Unwrap angles around a reference so they sit within [ref-π, ref+π]
|
|
434
487
|
function unwrapAround(ref, a) {
|
|
435
488
|
let x = a;
|
|
436
489
|
while (x < ref - Math.PI) x += TAU;
|
|
@@ -438,26 +491,17 @@ function unwrapAround(ref, a) {
|
|
|
438
491
|
return x;
|
|
439
492
|
}
|
|
440
493
|
|
|
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
494
|
function fanAngles(pd, opts = {}) {
|
|
452
495
|
const { openAngleDeg = 0, rotateDeg = 0 } = opts;
|
|
453
496
|
const gap = (openAngleDeg / 360) * TAU;
|
|
454
|
-
const
|
|
497
|
+
const rot = (rotateDeg / 360) * TAU;
|
|
455
498
|
|
|
456
|
-
//
|
|
499
|
+
// root + children index
|
|
457
500
|
let root = null;
|
|
458
|
-
const kids = new Map(pd.map(d => [d.thisId, d.children || []]));
|
|
501
|
+
const kids = new Map(pd.map((d) => [d.thisId, d.children || []]));
|
|
459
502
|
for (const d of pd) if (d.parentId == null) { root = d.thisId; break; }
|
|
460
503
|
|
|
504
|
+
// tip order (DFS left→right like your fortify)
|
|
461
505
|
const tipIds = [];
|
|
462
506
|
(function dfs(id) {
|
|
463
507
|
const c = kids.get(id) || [];
|
|
@@ -466,103 +510,105 @@ function fanAngles(pd, opts = {}) {
|
|
|
466
510
|
})(root);
|
|
467
511
|
|
|
468
512
|
const N = Math.max(1, tipIds.length);
|
|
469
|
-
|
|
470
|
-
const maxA = TAU * (1 - 1 / N) - gap;
|
|
513
|
+
const maxA = TAU * (1 - 1 / N) - gap; // note: no last-step overlap
|
|
471
514
|
const step = N > 1 ? maxA / (N - 1) : 0;
|
|
472
515
|
|
|
473
516
|
const angle = new Map();
|
|
474
|
-
tipIds.forEach((id, i) =>
|
|
475
|
-
angle.set(id, norm(i * step + rotate));
|
|
476
|
-
});
|
|
517
|
+
tipIds.forEach((id, i) => angle.set(id, norm(i * step + rot)));
|
|
477
518
|
|
|
478
|
-
//
|
|
519
|
+
// internal nodes: arithmetic mean of child angles (unwrapped)
|
|
479
520
|
(function setInternal(id) {
|
|
480
521
|
const c = kids.get(id) || [];
|
|
481
522
|
for (const ch of c) setInternal(ch);
|
|
482
|
-
if (c.length
|
|
483
|
-
// unwrap child angles around the first child's angle
|
|
523
|
+
if (c.length) {
|
|
484
524
|
const a0 = angle.get(c[0]);
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
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));
|
|
488
527
|
}
|
|
489
528
|
})(root);
|
|
490
529
|
|
|
491
530
|
return angle;
|
|
492
531
|
}
|
|
493
532
|
|
|
494
|
-
// getArcsFan.js
|
|
495
|
-
|
|
496
533
|
/**
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
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.
|
|
501
539
|
*
|
|
502
|
-
* pd
|
|
503
|
-
*
|
|
504
|
-
* where sweep=0 means CCW (start→end increasing),
|
|
505
|
-
* 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 }]
|
|
506
542
|
*/
|
|
507
|
-
function
|
|
543
|
+
function getChildArcs(pd) {
|
|
508
544
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
509
545
|
const arcs = [];
|
|
510
546
|
|
|
511
|
-
for (const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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;
|
|
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;
|
|
524
551
|
|
|
525
552
|
arcs.push({
|
|
526
|
-
parentId:
|
|
527
|
-
|
|
528
|
-
radius:
|
|
529
|
-
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)
|
|
530
558
|
});
|
|
531
559
|
}
|
|
560
|
+
|
|
532
561
|
return arcs;
|
|
533
562
|
}
|
|
534
563
|
|
|
564
|
+
// src/radial/radialLayout.js
|
|
565
|
+
|
|
535
566
|
/**
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
*
|
|
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)
|
|
541
573
|
*/
|
|
542
|
-
function radialLayout(node, opts =
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
+
}
|
|
560
596
|
}
|
|
561
597
|
|
|
562
|
-
|
|
563
|
-
|
|
598
|
+
// Build spokes using the angles currently on pd
|
|
599
|
+
const radii = (angleStrategy === "fan")
|
|
600
|
+
? getRadiiFromPd(pd)
|
|
601
|
+
: getRadii(node);
|
|
564
602
|
|
|
565
|
-
|
|
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 };
|
|
566
612
|
}
|
|
567
613
|
|
|
568
614
|
/**
|
|
@@ -1742,5 +1788,5 @@ function drawPhylogeny(
|
|
|
1742
1788
|
}
|
|
1743
1789
|
}
|
|
1744
1790
|
|
|
1745
|
-
export { describeArc, drawPhylogeny, parentFisheye, phisheye, radialLayout, readTree, rectangleLayout, subTree, unrooted };
|
|
1791
|
+
export { describeArc, describeArcSweep, drawPhylogeny, parentFisheye, phisheye, radialLayout, readTree, rectangleLayout, subTree, unrooted };
|
|
1746
1792
|
//# sourceMappingURL=index.js.map
|