@euphrasiologist/lwphylo 1.2.12 → 1.2.15

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.
@@ -448,6 +448,54 @@ function getChildArcs(pd) {
448
448
  return arcs;
449
449
  }
450
450
 
451
+ function getChildArcsFan(pd) {
452
+ const TAU = Math.PI * 2;
453
+ const norm = (t) => ((t % TAU) + TAU) % TAU;
454
+
455
+ // Circular midpoint that travels CCW from a -> b by half the CCW span
456
+ function midCCW(a, b) {
457
+ const d = (b - a + TAU) % TAU; // CCW delta a->b in [0, 2π)
458
+ return norm(a + d / 2);
459
+ }
460
+
461
+ const byId = new Map(pd.map(d => [d.thisId, d]));
462
+ const childrenByParent = new Map(pd.map(d => [d.thisId, d.children || []]));
463
+
464
+ const child_arcs = [];
465
+
466
+ for (const parent of pd) {
467
+ const kids = childrenByParent.get(parent.thisId) || [];
468
+ if (kids.length < 2) continue;
469
+
470
+ // Sort children by angle (normalized) around the circle
471
+ const A = kids
472
+ .map(id => ({ id, a: norm(byId.get(id).angle) }))
473
+ .sort((u, v) => u.a - v.a);
474
+
475
+ const N = A.length;
476
+ for (let i = 0; i < N; i++) {
477
+ const prev = A[(i - 1 + N) % N];
478
+ const cur = A[i];
479
+ const next = A[(i + 1) % N];
480
+
481
+ // “fan” wedge around the child: midpoint(prev→cur) .. midpoint(cur→next)
482
+ const start = midCCW(prev.a, cur.a);
483
+ const end = midCCW(cur.a, next.a);
484
+
485
+ child_arcs.push({
486
+ parentId: parent.thisId,
487
+ childId: cur.id,
488
+ radius: parent.r, // IMPORTANT: draw at the PARENT circle
489
+ start,
490
+ end,
491
+ sweep: 0 // CCW (matches describeArcSweep usage)
492
+ });
493
+ }
494
+ }
495
+
496
+ return child_arcs;
497
+ }
498
+
451
499
  // src/radial/radialLayout.js
452
500
 
453
501
  /**
@@ -494,7 +542,12 @@ function radialLayout(node, opts = {}) {
494
542
  : getArcs(pd);
495
543
 
496
544
  // per-child arcs for half-arc highlighting if you already use them
497
- const child_arcs = getChildArcs(pd);
545
+ let child_arcs = [];
546
+ if (opts.arcsStyle === "fan") {
547
+ child_arcs = getChildArcsFan(pd);
548
+ } else {
549
+ child_arcs = getChildArcs(pd);
550
+ }
498
551
 
499
552
  return { data: pd, radii, arcs, child_arcs };
500
553
  }
@@ -1094,6 +1147,10 @@ function drawPhylogeny(
1094
1147
 
1095
1148
  return svg.node();
1096
1149
  } else if (layout === "radial") {
1150
+ // RADIAL LAYOUT
1151
+ if (width !== height) {
1152
+ throw new Error("width and height must be the same for radial layout");
1153
+ }
1097
1154
  const parsedTree = readTree(treeText);
1098
1155
  const rad = radialLayout(parsedTree, {
1099
1156
  angleStrategy: "fan",
@@ -1103,6 +1160,10 @@ function drawPhylogeny(
1103
1160
  // ===== MODE =====
1104
1161
  const TIP_MODE = radialMode; // "phylo" (shorten to original tips) or "outer" (project to one circle)
1105
1162
  const isOuter = TIP_MODE === "outer";
1163
+ if (TIP_MODE !== "phylo" && TIP_MODE !== "outer") {
1164
+ throw new Error("radialMode must be either 'phylo' or 'outer'");
1165
+ }
1166
+
1106
1167
 
1107
1168
  // visuals (0 = let spokes reach the dots)
1108
1169
  const DOT_R = 3;
@@ -1195,7 +1256,6 @@ function drawPhylogeny(
1195
1256
  d.sweep
1196
1257
  )
1197
1258
  )
1198
-
1199
1259
  .attr("fill", "none")
1200
1260
  .attr("stroke", "#777")
1201
1261
  .attr("stroke-width", strokeWidth);
@@ -1268,7 +1328,7 @@ function drawPhylogeny(
1268
1328
 
1269
1329
  // maps for fast lookup on hover (childId → spoke / arc)
1270
1330
  const spokeByChild = new Map(rad.radii.map((s) => [childIdOf(s), s]));
1271
- const arcByChild = new Map(rad.child_arcs.map((a) => [a.childId, a]));
1331
+ const arcByChild = new Map((rad.child_arcs ?? []).map((a) => [a.childId, a]));
1272
1332
 
1273
1333
  // ===== LABELS =====
1274
1334
  // Labels — make them follow the tip position used by the current mode
@@ -1374,29 +1434,17 @@ function drawPhylogeny(
1374
1434
  // ----- half-arc at parent radius (parent.angle → child.angle) -----
1375
1435
  const a = arcByChild.get(cur.thisId);
1376
1436
  if (a) {
1437
+ const pathD = (a.sweep == null)
1438
+ ? describeArc(centerX, centerY, radiusPx(a.radius), a.start, a.end)
1439
+ : describeArcSweep(centerX, centerY, radiusPx(a.radius), a.start, a.end, a.sweep);
1440
+
1377
1441
  arcLayer
1378
1442
  .append("path")
1379
- .attr("d", (d) =>
1380
- d.sweep == null
1381
- ? describeArc(
1382
- centerX,
1383
- centerY,
1384
- radiusPx(d.radius),
1385
- d.start,
1386
- d.end
1387
- )
1388
- : describeArcSweep(
1389
- centerX,
1390
- centerY,
1391
- radiusPx(d.radius),
1392
- d.start,
1393
- d.end,
1394
- d.sweep
1395
- )
1396
- )
1443
+ .attr("d", pathD)
1397
1444
  .attr("fill", "none")
1398
1445
  .attr("stroke", stroke)
1399
1446
  .attr("stroke-width", width);
1447
+
1400
1448
  }
1401
1449
 
1402
1450
  first = false;