@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.
@@ -473,43 +473,73 @@ function getChildArcsFan(pd) {
473
473
  const TAU = Math.PI * 2;
474
474
  const norm = (t) => ((t % TAU) + TAU) % TAU;
475
475
 
476
+ const ccwDelta = (a0, a1) => (a1 - a0 + TAU) % TAU;
477
+ const cwDelta = (a0, a1) => (a0 - a1 + TAU) % TAU;
478
+
476
479
  // Circular midpoint that travels CCW from a -> b by half the CCW span
477
480
  function midCCW(a, b) {
478
- const d = (b - a + TAU) % TAU; // CCW delta a->b in [0, 2π)
481
+ const d = (b - a + TAU) % TAU; // CCW delta in [0, 2π)
479
482
  return norm(a + d / 2);
480
483
  }
481
484
 
482
- const byId = new Map(pd.map(d => [d.thisId, d]));
483
- const childrenByParent = new Map(pd.map(d => [d.thisId, d.children || []]));
485
+ const key = (x) => (typeof x === "string" ? +x : x);
486
+
487
+ const byId = new Map(pd.map(d => [key(d.thisId), d]));
488
+ const childrenByParent = new Map(
489
+ pd.map(d => [
490
+ key(d.thisId),
491
+ // normalize children to numeric IDs; drop anything we can't resolve
492
+ (d.children || [])
493
+ .map(ch => (typeof ch === "object" ? ch.thisId : ch))
494
+ .map(key)
495
+ .filter(id => byId.has(id))
496
+ ])
497
+ );
484
498
 
485
499
  const child_arcs = [];
486
500
 
487
- for (const parent of pd) {
488
- const kids = childrenByParent.get(parent.thisId) || [];
501
+ for (const parentRaw of pd) {
502
+ const pid = key(parentRaw.thisId);
503
+ const kids = childrenByParent.get(pid) || [];
489
504
  if (kids.length < 2) continue;
490
505
 
491
506
  // Sort children by angle (normalized) around the circle
492
507
  const A = kids
493
- .map(id => ({ id, a: norm(byId.get(id).angle) }))
508
+ .map(id => {
509
+ const node = byId.get(id);
510
+ return node ? { id, a: norm(node.angle) } : null;
511
+ })
512
+ .filter(Boolean)
494
513
  .sort((u, v) => u.a - v.a);
495
514
 
496
515
  const N = A.length;
516
+ if (N < 2) continue;
517
+
518
+ const parent = byId.get(pid);
519
+ const radius = parent?.r;
520
+ if (!(radius > 0)) continue;
521
+
497
522
  for (let i = 0; i < N; i++) {
498
523
  const prev = A[(i - 1 + N) % N];
499
- const cur = A[i];
524
+ const cur = A[i];
500
525
  const next = A[(i + 1) % N];
501
526
 
502
- // “fan” wedge around the child: midpoint(prev→cur) .. midpoint(cur→next)
527
+ // “fan” wedge around the child: midpoint(prev→cur) .. midpoint(cur→next), CCW
503
528
  const start = midCCW(prev.a, cur.a);
504
- const end = midCCW(cur.a, next.a);
529
+ const end = midCCW(cur.a, next.a);
530
+
531
+ // Choose the **shorter** sweep (0 = CCW, 1 = CW in our math-angle convention)
532
+ const ccw = ccwDelta(start, end);
533
+ const cw = cwDelta(start, end);
534
+ const sweep = ccw <= cw ? 0 : 1;
505
535
 
506
536
  child_arcs.push({
507
- parentId: parent.thisId,
537
+ parentId: pid,
508
538
  childId: cur.id,
509
- radius: parent.r, // IMPORTANT: draw at the PARENT circle
539
+ radius,
510
540
  start,
511
541
  end,
512
- sweep: 1 // CCW (matches describeArcSweep usage)
542
+ sweep
513
543
  });
514
544
  }
515
545
  }
@@ -1431,7 +1461,7 @@ function drawPhylogeny(
1431
1461
  let first = true;
1432
1462
  while (cur && cur.parentId != null) {
1433
1463
  // ----- spoke (parent → child) -----
1434
- const s = spokeByChild.get(cur.thisId);
1464
+ const s = spokeByChild.get(key(cur.thisId));
1435
1465
  if (s) {
1436
1466
  const px = s.x0,
1437
1467
  py = s.y0;
@@ -1455,23 +1485,21 @@ function drawPhylogeny(
1455
1485
  }
1456
1486
 
1457
1487
  // ----- half-arc at parent radius (parent.angle → child.angle) -----
1458
- const a = arcByChild.get(cur.thisId);
1459
- if (a) {
1460
- function pathFromArcRecord(rec) {
1461
- const R = radiusPx(rec.radius);
1462
- return rec.sweep == null
1463
- ? describeArc(centerX, centerY, R, rec.start, rec.end)
1464
- : describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
1465
- }
1466
-
1488
+ const a = arcByChild.get(key(cur.thisId));
1489
+ function pathFromArcRecord(rec) {
1490
+ const R = radiusPx(rec.radius);
1491
+ return rec.sweep == null
1492
+ ? describeArc(centerX, centerY, R, rec.start, rec.end)
1493
+ : describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
1494
+ }
1467
1495
 
1496
+ if (a) {
1468
1497
  arcLayer
1469
1498
  .append("path")
1470
1499
  .attr("d", pathFromArcRecord(a))
1471
1500
  .attr("fill", "none")
1472
1501
  .attr("stroke", stroke)
1473
1502
  .attr("stroke-width", width);
1474
-
1475
1503
  }
1476
1504
 
1477
1505
  first = false;