@euphrasiologist/lwphylo 1.2.5 → 1.2.7

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/index.js CHANGED
@@ -153,6 +153,18 @@ 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
157
+ function describeArcSweep(cx, cy, r, a0, a1, sweep /*0=CCW,1=CW*/) {
158
+ const TAU = Math.PI * 2;
159
+ const norm = (t) => ((t % TAU) + TAU) % TAU;
160
+ const delta = sweep === 0 ? norm(a1 - a0) : norm(a0 - a1); // magnitude on chosen sweep
161
+ if (delta < 1e-9 || r <= 0) return "";
162
+ const largeArcFlag = delta > Math.PI ? 1 : 0;
163
+ const p0 = { x: cx + r * Math.cos(a0), y: cy - r * Math.sin(a0) };
164
+ const p1 = { x: cx + r * Math.cos(a1), y: cy - r * Math.sin(a1) };
165
+ return `M ${p0.x} ${p0.y} A ${r} ${r} 0 ${largeArcFlag} ${sweep} ${p1.x} ${p1.y}`;
166
+ }
167
+
156
168
  /**
157
169
  * Recursive function for pre-order traversal of tree (returns array)
158
170
  */
@@ -426,6 +438,112 @@ function getChildArcs(pd) {
426
438
  return arcs;
427
439
  }
428
440
 
441
+ // fanAngles.js
442
+ const TAU = Math.PI * 2;
443
+ const norm = (t) => ((t % TAU) + TAU) % TAU;
444
+
445
+ // Unwrap angles around a reference so they sit within [ref-π, ref+π]
446
+ function unwrapAround(ref, a) {
447
+ let x = a;
448
+ while (x < ref - Math.PI) x += TAU;
449
+ while (x > ref + Math.PI) x -= TAU;
450
+ return x;
451
+ }
452
+
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
+ function fanAngles(pd, opts = {}) {
464
+ const { openAngleDeg = 0, rotateDeg = 0 } = opts;
465
+ const gap = (openAngleDeg / 360) * TAU;
466
+ const rotate = (rotateDeg / 360) * TAU;
467
+
468
+ // Find root and collect tips in cladewise/DFS order
469
+ let root = null;
470
+ const kids = new Map(pd.map(d => [d.thisId, d.children || []]));
471
+ for (const d of pd) if (d.parentId == null) { root = d.thisId; break; }
472
+
473
+ const tipIds = [];
474
+ (function dfs(id) {
475
+ const c = kids.get(id) || [];
476
+ if (!c.length) { tipIds.push(id); return; }
477
+ for (const ch of c) dfs(ch);
478
+ })(root);
479
+
480
+ const N = Math.max(1, tipIds.length);
481
+ // APE: 0 .. 2π*(1 - 1/N) - gap, length.out=N (no last step overlap)
482
+ const maxA = TAU * (1 - 1 / N) - gap;
483
+ const step = N > 1 ? maxA / (N - 1) : 0;
484
+
485
+ const angle = new Map();
486
+ tipIds.forEach((id, i) => {
487
+ angle.set(id, norm(i * step + rotate));
488
+ });
489
+
490
+ // Internal nodes: arithmetic mean of child angles (unwrapped)
491
+ (function setInternal(id) {
492
+ const c = kids.get(id) || [];
493
+ for (const ch of c) setInternal(ch);
494
+ if (c.length > 0) {
495
+ // unwrap child angles around the first child's angle
496
+ const a0 = angle.get(c[0]);
497
+ const unwrapped = c.map(ch => unwrapAround(a0, angle.get(ch)));
498
+ const mean = unwrapped.reduce((s, v) => s + v, 0) / unwrapped.length;
499
+ angle.set(id, norm(mean));
500
+ }
501
+ })(root);
502
+
503
+ return angle;
504
+ }
505
+
506
+ // getArcsFan.js
507
+
508
+ /**
509
+ * Build arcs like APE's circular.plot:
510
+ * For each internal parent, draw a single arc at radius=parent.r
511
+ * going from first child's angle to last child's angle in child order.
512
+ * If last < first (wrap), we draw CW (decreasing) to stay on the block.
513
+ *
514
+ * pd: array with { thisId, parentId, r, children[], angle }
515
+ * returns: [{ parentId, thisId, radius, start, end, sweep }]
516
+ * where sweep=0 means CCW (start→end increasing),
517
+ * sweep=1 means CW (start→end decreasing across wrap).
518
+ */
519
+ function getArcsFan(pd) {
520
+ const byId = new Map(pd.map(d => [d.thisId, d]));
521
+ const arcs = [];
522
+
523
+ for (const p of pd) {
524
+ const c = p.children || [];
525
+ if (c.length < 2) continue;
526
+ const A = c.map(id => byId.get(id)?.angle).filter(a => a != null);
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;
536
+
537
+ arcs.push({
538
+ parentId: p.parentId,
539
+ thisId: p.thisId,
540
+ radius: p.r,
541
+ start, end, sweep
542
+ });
543
+ }
544
+ return arcs;
545
+ }
546
+
429
547
  /**
430
548
  * Simple wrapper for radial layout:
431
549
  * - data: per-node { angle, r, x, y, ... }
@@ -433,12 +551,29 @@ function getChildArcs(pd) {
433
551
  * - arcs: per-parent arcs spanning all children at parent's radius
434
552
  * - child_arcs: per-child half-arcs (parent.angle → child.angle) at parent's radius
435
553
  */
436
- function radialLayout(node) {
554
+ function radialLayout(node, opts = {}) {
437
555
  const data = {};
438
556
  data.data = radialData(node);
439
557
  data.radii = getRadii(node);
440
558
  data.arcs = getArcs(data.data);
441
559
  data.child_arcs = getChildArcs(data.data);
560
+
561
+ const pd = fortify(node, true);
562
+ const angleMap = fanAngles(pd, {
563
+ openAngleDeg: opts.openAngleDeg ?? 0,
564
+ rotateDeg: opts.rotateDeg ?? 0
565
+ });
566
+
567
+ // stamp angles + x,y back onto pd (r stays your cumulative edge length)
568
+ for (const d of pd) {
569
+ d.angle = angleMap.get(d.thisId) ?? 0;
570
+ d.x = d.r * Math.cos(d.angle);
571
+ d.y = d.r * Math.sin(d.angle);
572
+ }
573
+
574
+ data.data_pd = pd;
575
+ data.arcs_fan = getArcsFan(pd);
576
+
442
577
  return data;
443
578
  }
444
579
 
@@ -1619,5 +1754,5 @@ function drawPhylogeny(
1619
1754
  }
1620
1755
  }
1621
1756
 
1622
- export { describeArc, drawPhylogeny, parentFisheye, phisheye, radialLayout, readTree, rectangleLayout, subTree, unrooted };
1757
+ export { describeArc, describeArcSweep, drawPhylogeny, parentFisheye, phisheye, radialLayout, readTree, rectangleLayout, subTree, unrooted };
1623
1758
  //# sourceMappingURL=index.js.map