@euphrasiologist/lwphylo 1.4.0 → 1.5.0

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.
@@ -925,8 +925,12 @@ function addScaleBar(svg, { scale, basis, defaultX, defaultY, scaleBar, fontSize
925
925
  .text(opts.label ?? String(length));
926
926
  }
927
927
 
928
+ // input: a Newick string, or an already-parsed tree (the node shape returned
929
+ // by readTree()/randomTree()). Passing the same parsed tree object back in
930
+ // across re-renders (e.g. after mutating it with rotate()) keeps node ids
931
+ // stable, which onNodeClick below relies on.
928
932
  function drawPhylogeny(
929
- treeText,
933
+ input,
930
934
  {
931
935
  layout = "rect", // rect/radial/unrooted
932
936
  width = 800,
@@ -944,6 +948,7 @@ function drawPhylogeny(
944
948
  nodeLabelFontSize, // defaults to labelFontSize
945
949
  scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
946
950
  alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
951
+ onNodeClick, // (node, event) => void — fires when an internal node circle is clicked (requires internalNodeCircles: true)
947
952
  showTooltips = true,
948
953
  tooltipFormatter = (d, rtt) =>
949
954
  `${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
@@ -958,6 +963,9 @@ function drawPhylogeny(
958
963
  // shared helpers
959
964
  const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
960
965
  const nodeLabelSize = nodeLabelFontSize ?? labelFontSize;
966
+ const parsedTree = (input && typeof input === "object" && Array.isArray(input.children))
967
+ ? input
968
+ : readTree(input);
961
969
  // Works for both radial (uses `r`) and rect (uses `x1`).
962
970
  // Falls back to summing branchLength up to the root if neither is present.
963
971
  function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
@@ -986,7 +994,7 @@ function drawPhylogeny(
986
994
 
987
995
  if (layout === "rect") {
988
996
  // RECTANGULAR LAYOUT
989
- const tree_df = rectangleLayout(readTree(treeText));
997
+ const tree_df = rectangleLayout(parsedTree);
990
998
  const horizontal = tree_df.horizontal_lines;
991
999
  const vertical = tree_df.vertical_lines;
992
1000
  const tips = horizontal.filter((d) => d.isTip);
@@ -1097,6 +1105,12 @@ function drawPhylogeny(
1097
1105
  .append("title")
1098
1106
  .text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
1099
1107
  }
1108
+
1109
+ if (onNodeClick) {
1110
+ internalDots
1111
+ .style("cursor", "pointer")
1112
+ .on("click", (event, d) => onNodeClick(d, event));
1113
+ }
1100
1114
  }
1101
1115
 
1102
1116
  // internal node labels (optional)
@@ -1232,7 +1246,6 @@ function drawPhylogeny(
1232
1246
  if (width !== height) {
1233
1247
  throw new Error("width and height must be the same for radial layout");
1234
1248
  }
1235
- const parsedTree = readTree(treeText);
1236
1249
  const rad = radialLayout(parsedTree, {
1237
1250
  angleStrategy: "fan",
1238
1251
  arcsStyle: "fan"
@@ -1452,6 +1465,12 @@ function drawPhylogeny(
1452
1465
  .append("title")
1453
1466
  .text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
1454
1467
  }
1468
+
1469
+ if (onNodeClick) {
1470
+ internalDots
1471
+ .style("cursor", "pointer")
1472
+ .on("click", (event, d) => onNodeClick(d, event));
1473
+ }
1455
1474
  }
1456
1475
 
1457
1476
  // ===== INTERNAL NODE LABELS (optional) =====
@@ -1652,7 +1671,6 @@ function drawPhylogeny(
1652
1671
  return svg.node();
1653
1672
  } else if (layout === "unrooted") {
1654
1673
  // UNROOTED LAYOUT
1655
- const parsedTree = readTree(treeText);
1656
1674
  const unrootedPhylo = unrooted(parsedTree);
1657
1675
 
1658
1676
  const w = width;
@@ -1714,6 +1732,13 @@ function drawPhylogeny(
1714
1732
  .attr("stroke-width", 2)
1715
1733
  .attr("fill", (d) => (d.isTip ? "black" : "white"));
1716
1734
 
1735
+ if (internalNodeCircles && onNodeClick) {
1736
+ nodes
1737
+ .filter((d) => !d.isTip)
1738
+ .style("cursor", "pointer")
1739
+ .on("click", (event, d) => onNodeClick(d, event));
1740
+ }
1741
+
1717
1742
  if (nodeLabels) {
1718
1743
  const labeledInternalNodes = unrootedPhylo.data.filter((d) => !d.isTip && d.thisLabel);
1719
1744
  group