@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.js CHANGED
@@ -565,43 +565,73 @@ function getChildArcsFan(pd) {
565
565
  const TAU = Math.PI * 2;
566
566
  const norm = (t) => ((t % TAU) + TAU) % TAU;
567
567
 
568
+ const ccwDelta = (a0, a1) => (a1 - a0 + TAU) % TAU;
569
+ const cwDelta = (a0, a1) => (a0 - a1 + TAU) % TAU;
570
+
568
571
  // Circular midpoint that travels CCW from a -> b by half the CCW span
569
572
  function midCCW(a, b) {
570
- const d = (b - a + TAU) % TAU; // CCW delta a->b in [0, 2π)
573
+ const d = (b - a + TAU) % TAU; // CCW delta in [0, 2π)
571
574
  return norm(a + d / 2);
572
575
  }
573
576
 
574
- const byId = new Map(pd.map(d => [d.thisId, d]));
575
- const childrenByParent = new Map(pd.map(d => [d.thisId, d.children || []]));
577
+ const key = (x) => (typeof x === "string" ? +x : x);
578
+
579
+ const byId = new Map(pd.map(d => [key(d.thisId), d]));
580
+ const childrenByParent = new Map(
581
+ pd.map(d => [
582
+ key(d.thisId),
583
+ // normalize children to numeric IDs; drop anything we can't resolve
584
+ (d.children || [])
585
+ .map(ch => (typeof ch === "object" ? ch.thisId : ch))
586
+ .map(key)
587
+ .filter(id => byId.has(id))
588
+ ])
589
+ );
576
590
 
577
591
  const child_arcs = [];
578
592
 
579
- for (const parent of pd) {
580
- const kids = childrenByParent.get(parent.thisId) || [];
593
+ for (const parentRaw of pd) {
594
+ const pid = key(parentRaw.thisId);
595
+ const kids = childrenByParent.get(pid) || [];
581
596
  if (kids.length < 2) continue;
582
597
 
583
598
  // Sort children by angle (normalized) around the circle
584
599
  const A = kids
585
- .map(id => ({ id, a: norm(byId.get(id).angle) }))
600
+ .map(id => {
601
+ const node = byId.get(id);
602
+ return node ? { id, a: norm(node.angle) } : null;
603
+ })
604
+ .filter(Boolean)
586
605
  .sort((u, v) => u.a - v.a);
587
606
 
588
607
  const N = A.length;
608
+ if (N < 2) continue;
609
+
610
+ const parent = byId.get(pid);
611
+ const radius = parent?.r;
612
+ if (!(radius > 0)) continue;
613
+
589
614
  for (let i = 0; i < N; i++) {
590
615
  const prev = A[(i - 1 + N) % N];
591
- const cur = A[i];
616
+ const cur = A[i];
592
617
  const next = A[(i + 1) % N];
593
618
 
594
- // “fan” wedge around the child: midpoint(prev→cur) .. midpoint(cur→next)
619
+ // “fan” wedge around the child: midpoint(prev→cur) .. midpoint(cur→next), CCW
595
620
  const start = midCCW(prev.a, cur.a);
596
- const end = midCCW(cur.a, next.a);
621
+ const end = midCCW(cur.a, next.a);
622
+
623
+ // Choose the **shorter** sweep (0 = CCW, 1 = CW in our math-angle convention)
624
+ const ccw = ccwDelta(start, end);
625
+ const cw = cwDelta(start, end);
626
+ const sweep = ccw <= cw ? 0 : 1;
597
627
 
598
628
  child_arcs.push({
599
- parentId: parent.thisId,
629
+ parentId: pid,
600
630
  childId: cur.id,
601
- radius: parent.r, // IMPORTANT: draw at the PARENT circle
631
+ radius,
602
632
  start,
603
633
  end,
604
- sweep: 1 // CCW (matches describeArcSweep usage)
634
+ sweep
605
635
  });
606
636
  }
607
637
  }
@@ -1577,7 +1607,7 @@ function drawPhylogeny(
1577
1607
  let first = true;
1578
1608
  while (cur && cur.parentId != null) {
1579
1609
  // ----- spoke (parent → child) -----
1580
- const s = spokeByChild.get(cur.thisId);
1610
+ const s = spokeByChild.get(key(cur.thisId));
1581
1611
  if (s) {
1582
1612
  const px = s.x0,
1583
1613
  py = s.y0;
@@ -1601,23 +1631,21 @@ function drawPhylogeny(
1601
1631
  }
1602
1632
 
1603
1633
  // ----- half-arc at parent radius (parent.angle → child.angle) -----
1604
- const a = arcByChild.get(cur.thisId);
1605
- if (a) {
1606
- function pathFromArcRecord(rec) {
1607
- const R = radiusPx(rec.radius);
1608
- return rec.sweep == null
1609
- ? describeArc(centerX, centerY, R, rec.start, rec.end)
1610
- : describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
1611
- }
1612
-
1634
+ const a = arcByChild.get(key(cur.thisId));
1635
+ function pathFromArcRecord(rec) {
1636
+ const R = radiusPx(rec.radius);
1637
+ return rec.sweep == null
1638
+ ? describeArc(centerX, centerY, R, rec.start, rec.end)
1639
+ : describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
1640
+ }
1613
1641
 
1642
+ if (a) {
1614
1643
  arcLayer
1615
1644
  .append("path")
1616
1645
  .attr("d", pathFromArcRecord(a))
1617
1646
  .attr("fill", "none")
1618
1647
  .attr("stroke", stroke)
1619
1648
  .attr("stroke-width", width);
1620
-
1621
1649
  }
1622
1650
 
1623
1651
  first = false;