@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.
@@ -452,43 +452,73 @@ function getChildArcsFan(pd) {
452
452
  const TAU = Math.PI * 2;
453
453
  const norm = (t) => ((t % TAU) + TAU) % TAU;
454
454
 
455
+ const ccwDelta = (a0, a1) => (a1 - a0 + TAU) % TAU;
456
+ const cwDelta = (a0, a1) => (a0 - a1 + TAU) % TAU;
457
+
455
458
  // Circular midpoint that travels CCW from a -> b by half the CCW span
456
459
  function midCCW(a, b) {
457
- const d = (b - a + TAU) % TAU; // CCW delta a->b in [0, 2π)
460
+ const d = (b - a + TAU) % TAU; // CCW delta in [0, 2π)
458
461
  return norm(a + d / 2);
459
462
  }
460
463
 
461
- const byId = new Map(pd.map(d => [d.thisId, d]));
462
- const childrenByParent = new Map(pd.map(d => [d.thisId, d.children || []]));
464
+ const key = (x) => (typeof x === "string" ? +x : x);
465
+
466
+ const byId = new Map(pd.map(d => [key(d.thisId), d]));
467
+ const childrenByParent = new Map(
468
+ pd.map(d => [
469
+ key(d.thisId),
470
+ // normalize children to numeric IDs; drop anything we can't resolve
471
+ (d.children || [])
472
+ .map(ch => (typeof ch === "object" ? ch.thisId : ch))
473
+ .map(key)
474
+ .filter(id => byId.has(id))
475
+ ])
476
+ );
463
477
 
464
478
  const child_arcs = [];
465
479
 
466
- for (const parent of pd) {
467
- const kids = childrenByParent.get(parent.thisId) || [];
480
+ for (const parentRaw of pd) {
481
+ const pid = key(parentRaw.thisId);
482
+ const kids = childrenByParent.get(pid) || [];
468
483
  if (kids.length < 2) continue;
469
484
 
470
485
  // Sort children by angle (normalized) around the circle
471
486
  const A = kids
472
- .map(id => ({ id, a: norm(byId.get(id).angle) }))
487
+ .map(id => {
488
+ const node = byId.get(id);
489
+ return node ? { id, a: norm(node.angle) } : null;
490
+ })
491
+ .filter(Boolean)
473
492
  .sort((u, v) => u.a - v.a);
474
493
 
475
494
  const N = A.length;
495
+ if (N < 2) continue;
496
+
497
+ const parent = byId.get(pid);
498
+ const radius = parent?.r;
499
+ if (!(radius > 0)) continue;
500
+
476
501
  for (let i = 0; i < N; i++) {
477
502
  const prev = A[(i - 1 + N) % N];
478
- const cur = A[i];
503
+ const cur = A[i];
479
504
  const next = A[(i + 1) % N];
480
505
 
481
- // “fan” wedge around the child: midpoint(prev→cur) .. midpoint(cur→next)
506
+ // “fan” wedge around the child: midpoint(prev→cur) .. midpoint(cur→next), CCW
482
507
  const start = midCCW(prev.a, cur.a);
483
- const end = midCCW(cur.a, next.a);
508
+ const end = midCCW(cur.a, next.a);
509
+
510
+ // Choose the **shorter** sweep (0 = CCW, 1 = CW in our math-angle convention)
511
+ const ccw = ccwDelta(start, end);
512
+ const cw = cwDelta(start, end);
513
+ const sweep = ccw <= cw ? 0 : 1;
484
514
 
485
515
  child_arcs.push({
486
- parentId: parent.thisId,
516
+ parentId: pid,
487
517
  childId: cur.id,
488
- radius: parent.r, // IMPORTANT: draw at the PARENT circle
518
+ radius,
489
519
  start,
490
520
  end,
491
- sweep: 1 // CCW (matches describeArcSweep usage)
521
+ sweep
492
522
  });
493
523
  }
494
524
  }
@@ -1410,7 +1440,7 @@ function drawPhylogeny(
1410
1440
  let first = true;
1411
1441
  while (cur && cur.parentId != null) {
1412
1442
  // ----- spoke (parent → child) -----
1413
- const s = spokeByChild.get(cur.thisId);
1443
+ const s = spokeByChild.get(key(cur.thisId));
1414
1444
  if (s) {
1415
1445
  const px = s.x0,
1416
1446
  py = s.y0;
@@ -1434,23 +1464,21 @@ function drawPhylogeny(
1434
1464
  }
1435
1465
 
1436
1466
  // ----- half-arc at parent radius (parent.angle → child.angle) -----
1437
- const a = arcByChild.get(cur.thisId);
1438
- if (a) {
1439
- function pathFromArcRecord(rec) {
1440
- const R = radiusPx(rec.radius);
1441
- return rec.sweep == null
1442
- ? describeArc(centerX, centerY, R, rec.start, rec.end)
1443
- : describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
1444
- }
1445
-
1467
+ const a = arcByChild.get(key(cur.thisId));
1468
+ function pathFromArcRecord(rec) {
1469
+ const R = radiusPx(rec.radius);
1470
+ return rec.sweep == null
1471
+ ? describeArc(centerX, centerY, R, rec.start, rec.end)
1472
+ : describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
1473
+ }
1446
1474
 
1475
+ if (a) {
1447
1476
  arcLayer
1448
1477
  .append("path")
1449
1478
  .attr("d", pathFromArcRecord(a))
1450
1479
  .attr("fill", "none")
1451
1480
  .attr("stroke", stroke)
1452
1481
  .attr("stroke-width", width);
1453
-
1454
1482
  }
1455
1483
 
1456
1484
  first = false;