@euphrasiologist/lwphylo 1.2.18 → 1.2.20

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.cjs CHANGED
@@ -586,43 +586,73 @@ function getChildArcsFan(pd) {
586
586
  const TAU = Math.PI * 2;
587
587
  const norm = (t) => ((t % TAU) + TAU) % TAU;
588
588
 
589
+ const ccwDelta = (a0, a1) => (a1 - a0 + TAU) % TAU;
590
+ const cwDelta = (a0, a1) => (a0 - a1 + TAU) % TAU;
591
+
589
592
  // Circular midpoint that travels CCW from a -> b by half the CCW span
590
593
  function midCCW(a, b) {
591
- const d = (b - a + TAU) % TAU; // CCW delta a->b in [0, 2π)
594
+ const d = (b - a + TAU) % TAU; // CCW delta in [0, 2π)
592
595
  return norm(a + d / 2);
593
596
  }
594
597
 
595
- const byId = new Map(pd.map(d => [d.thisId, d]));
596
- const childrenByParent = new Map(pd.map(d => [d.thisId, d.children || []]));
598
+ const key = (x) => (typeof x === "string" ? +x : x);
599
+
600
+ const byId = new Map(pd.map(d => [key(d.thisId), d]));
601
+ const childrenByParent = new Map(
602
+ pd.map(d => [
603
+ key(d.thisId),
604
+ // normalize children to numeric IDs; drop anything we can't resolve
605
+ (d.children || [])
606
+ .map(ch => (typeof ch === "object" ? ch.thisId : ch))
607
+ .map(key)
608
+ .filter(id => byId.has(id))
609
+ ])
610
+ );
597
611
 
598
612
  const child_arcs = [];
599
613
 
600
- for (const parent of pd) {
601
- const kids = childrenByParent.get(parent.thisId) || [];
614
+ for (const parentRaw of pd) {
615
+ const pid = key(parentRaw.thisId);
616
+ const kids = childrenByParent.get(pid) || [];
602
617
  if (kids.length < 2) continue;
603
618
 
604
619
  // Sort children by angle (normalized) around the circle
605
620
  const A = kids
606
- .map(id => ({ id, a: norm(byId.get(id).angle) }))
621
+ .map(id => {
622
+ const node = byId.get(id);
623
+ return node ? { id, a: norm(node.angle) } : null;
624
+ })
625
+ .filter(Boolean)
607
626
  .sort((u, v) => u.a - v.a);
608
627
 
609
628
  const N = A.length;
629
+ if (N < 2) continue;
630
+
631
+ const parent = byId.get(pid);
632
+ const radius = parent?.r;
633
+ if (!(radius > 0)) continue;
634
+
610
635
  for (let i = 0; i < N; i++) {
611
636
  const prev = A[(i - 1 + N) % N];
612
- const cur = A[i];
637
+ const cur = A[i];
613
638
  const next = A[(i + 1) % N];
614
639
 
615
- // “fan” wedge around the child: midpoint(prev→cur) .. midpoint(cur→next)
640
+ // “fan” wedge around the child: midpoint(prev→cur) .. midpoint(cur→next), CCW
616
641
  const start = midCCW(prev.a, cur.a);
617
- const end = midCCW(cur.a, next.a);
642
+ const end = midCCW(cur.a, next.a);
643
+
644
+ // Choose the **shorter** sweep (0 = CCW, 1 = CW in our math-angle convention)
645
+ const ccw = ccwDelta(start, end);
646
+ const cw = cwDelta(start, end);
647
+ const sweep = ccw <= cw ? 0 : 1;
618
648
 
619
649
  child_arcs.push({
620
- parentId: parent.thisId,
650
+ parentId: pid,
621
651
  childId: cur.id,
622
- radius: parent.r, // IMPORTANT: draw at the PARENT circle
652
+ radius,
623
653
  start,
624
654
  end,
625
- sweep: 1 // CCW (matches describeArcSweep usage)
655
+ sweep
626
656
  });
627
657
  }
628
658
  }
@@ -1598,7 +1628,7 @@ function drawPhylogeny(
1598
1628
  let first = true;
1599
1629
  while (cur && cur.parentId != null) {
1600
1630
  // ----- spoke (parent → child) -----
1601
- const s = spokeByChild.get(cur.thisId);
1631
+ const s = spokeByChild.get(key(cur.thisId));
1602
1632
  if (s) {
1603
1633
  const px = s.x0,
1604
1634
  py = s.y0;
@@ -1622,23 +1652,21 @@ function drawPhylogeny(
1622
1652
  }
1623
1653
 
1624
1654
  // ----- half-arc at parent radius (parent.angle → child.angle) -----
1625
- const a = arcByChild.get(cur.thisId);
1626
- if (a) {
1627
- function pathFromArcRecord(rec) {
1628
- const R = radiusPx(rec.radius);
1629
- return rec.sweep == null
1630
- ? describeArc(centerX, centerY, R, rec.start, rec.end)
1631
- : describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
1632
- }
1633
-
1655
+ const a = arcByChild.get(key(cur.thisId));
1656
+ function pathFromArcRecord(rec) {
1657
+ const R = radiusPx(rec.radius);
1658
+ return rec.sweep == null
1659
+ ? describeArc(centerX, centerY, R, rec.start, rec.end)
1660
+ : describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
1661
+ }
1634
1662
 
1663
+ if (a) {
1635
1664
  arcLayer
1636
1665
  .append("path")
1637
1666
  .attr("d", pathFromArcRecord(a))
1638
1667
  .attr("fill", "none")
1639
1668
  .attr("stroke", stroke)
1640
1669
  .attr("stroke-width", width);
1641
-
1642
1670
  }
1643
1671
 
1644
1672
  first = false;