@euphrasiologist/lwphylo 1.5.0 → 1.5.1
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/README.md +6 -1
- package/dist/drawPhylogeny.cjs +60 -15
- package/dist/drawPhylogeny.cjs.map +1 -1
- package/dist/drawPhylogeny.esm.js +60 -15
- package/dist/drawPhylogeny.esm.js.map +1 -1
- package/dist/drawPhylogeny.umd.js +1 -1
- package/dist/drawPhylogeny.umd.js.map +1 -1
- package/dist/index.cjs +46 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +46 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,7 +20,12 @@ Need a tree to experiment with? `randomTree(nTips, { maxBranchLength, labelPrefi
|
|
|
20
20
|
- `nodeLabels` (bool) + `nodeLabelFontSize` — draw text labels (e.g. clade/support values) at internal nodes that have one.
|
|
21
21
|
- `scaleBar` — `true` for an auto-sized branch-length scale bar, a number for an explicit length in branch-length units, or `{ length, x, y, label }` for full control.
|
|
22
22
|
- `alignTipLabels` (bool, rect & radial layouts) — align tip labels to a common column/ring, with dashed guide lines back to each tip's true position.
|
|
23
|
-
- `onNodeClick(node, event)` — fires when an internal node circle is clicked (requires `internalNodeCircles: true`).
|
|
23
|
+
- `onNodeClick(node, event)` — fires when an internal node circle is clicked (requires `internalNodeCircles: true`).
|
|
24
|
+
- `container` (DOM element or CSS selector) + `rotateOnClick` (bool) — the batteries-included version of click-to-rotate: with both set, `drawPhylogeny` mounts the SVG into `container` itself and, on every internal-node click, rotates that clade and redraws back into the same container — no manual render loop needed:
|
|
25
|
+
```js
|
|
26
|
+
drawPhylogeny(newickString, { layout: "rect", container: "#tree", rotateOnClick: true });
|
|
27
|
+
```
|
|
28
|
+
`rotateOnClick` auto-enables `internalNodeCircles` unless you set it explicitly. A supplied `onNodeClick` still fires too, before the rotate. Without a `container`, `rotateOnClick` throws — there'd be nowhere to put the redrawn SVG. For finer control (e.g. wrapping the SVG in your own zoom/pan `<g>`, as the demo site does), skip `container`/`rotateOnClick` and drive it yourself with `onNodeClick` + `rotate()` as shown in the demo's own source.
|
|
24
29
|
|
|
25
30
|
### Acknowledgements
|
|
26
31
|
|
package/dist/drawPhylogeny.cjs
CHANGED
|
@@ -837,6 +837,20 @@ function unrooted (node) {
|
|
|
837
837
|
return data;
|
|
838
838
|
}
|
|
839
839
|
|
|
840
|
+
/**
|
|
841
|
+
* Reverse the child order at one node, flipping which side of the tree
|
|
842
|
+
* that clade's descendants are drawn on. Defaults to the root. Mutates
|
|
843
|
+
* the tree in place.
|
|
844
|
+
*/
|
|
845
|
+
function rotate(tree, nodeId = tree.id) {
|
|
846
|
+
const target = preorder(tree).find((n) => n.id === nodeId);
|
|
847
|
+
if (!target) {
|
|
848
|
+
throw new Error(`No node with id ${nodeId} found in tree`);
|
|
849
|
+
}
|
|
850
|
+
target.children.reverse();
|
|
851
|
+
return tree;
|
|
852
|
+
}
|
|
853
|
+
|
|
840
854
|
/**
|
|
841
855
|
* Parse a Newick tree string into a doubly-linked list of JS Objects.
|
|
842
856
|
* Assigns labels, branch lengths, and node IDs (tips before internals if input emits them that way).
|
|
@@ -950,9 +964,8 @@ function addScaleBar(svg, { scale, basis, defaultX, defaultY, scaleBar, fontSize
|
|
|
950
964
|
// by readTree()/randomTree()). Passing the same parsed tree object back in
|
|
951
965
|
// across re-renders (e.g. after mutating it with rotate()) keeps node ids
|
|
952
966
|
// stable, which onNodeClick below relies on.
|
|
953
|
-
function drawPhylogeny(
|
|
954
|
-
|
|
955
|
-
{
|
|
967
|
+
function drawPhylogeny(input, options = {}) {
|
|
968
|
+
const {
|
|
956
969
|
layout = "rect", // rect/radial/unrooted
|
|
957
970
|
width = 800,
|
|
958
971
|
height = 800,
|
|
@@ -963,12 +976,13 @@ function drawPhylogeny(
|
|
|
963
976
|
tipLabels = true,
|
|
964
977
|
labelFontSize = 10, // font size (px) for tip labels
|
|
965
978
|
tipRadius, // px radius of tip dots; defaults to each layout's original size
|
|
966
|
-
internalNodeCircles = false, // draw a circle at every internal (non-tip) node
|
|
967
979
|
internalNodeRadius = 3, // px radius for internal node circles
|
|
968
980
|
nodeLabels = false, // draw text labels at internal nodes (e.g. clade/support labels)
|
|
969
981
|
nodeLabelFontSize, // defaults to labelFontSize
|
|
970
982
|
scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
|
|
971
983
|
alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
|
|
984
|
+
container, // DOM element or CSS selector — if given, drawPhylogeny mounts the SVG itself instead of just returning it
|
|
985
|
+
rotateOnClick = false, // click an internal node circle to rotate its children in place and auto-redraw (requires container)
|
|
972
986
|
onNodeClick, // (node, event) => void — fires when an internal node circle is clicked (requires internalNodeCircles: true)
|
|
973
987
|
showTooltips = true,
|
|
974
988
|
tooltipFormatter = (d, rtt) =>
|
|
@@ -978,8 +992,10 @@ function drawPhylogeny(
|
|
|
978
992
|
highlightTips = [], // array of tip labels or ids for static highlight (optional)
|
|
979
993
|
highlightStroke = "#e63946",
|
|
980
994
|
highlightWidth = 2.5
|
|
981
|
-
} =
|
|
982
|
-
|
|
995
|
+
} = options;
|
|
996
|
+
// rotateOnClick needs internal node circles to click on; auto-enable them
|
|
997
|
+
// unless the caller explicitly said otherwise.
|
|
998
|
+
const internalNodeCircles = options.internalNodeCircles ?? rotateOnClick;
|
|
983
999
|
|
|
984
1000
|
// shared helpers
|
|
985
1001
|
const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
|
|
@@ -987,6 +1003,35 @@ function drawPhylogeny(
|
|
|
987
1003
|
const parsedTree = (input && typeof input === "object" && Array.isArray(input.children))
|
|
988
1004
|
? input
|
|
989
1005
|
: readTree(input);
|
|
1006
|
+
|
|
1007
|
+
const mountEl = typeof container === "string"
|
|
1008
|
+
? (typeof document !== "undefined" ? document.querySelector(container) : null)
|
|
1009
|
+
: (container ?? null);
|
|
1010
|
+
|
|
1011
|
+
if (rotateOnClick && !mountEl) {
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
"rotateOnClick requires a `container` (a DOM element or CSS selector) so drawPhylogeny can mount the redrawn SVG after each rotation."
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// fires the caller's onNodeClick (if any), then handles the rotate+redraw itself
|
|
1018
|
+
const effectiveOnNodeClick = (onNodeClick || rotateOnClick)
|
|
1019
|
+
? (node, event) => {
|
|
1020
|
+
if (typeof onNodeClick === "function") onNodeClick(node, event);
|
|
1021
|
+
if (rotateOnClick) {
|
|
1022
|
+
rotate(parsedTree, node.thisId);
|
|
1023
|
+
drawPhylogeny(parsedTree, options);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
: undefined;
|
|
1027
|
+
|
|
1028
|
+
function mount(svgNode) {
|
|
1029
|
+
if (mountEl) {
|
|
1030
|
+
mountEl.innerHTML = "";
|
|
1031
|
+
mountEl.appendChild(svgNode);
|
|
1032
|
+
}
|
|
1033
|
+
return svgNode;
|
|
1034
|
+
}
|
|
990
1035
|
// Works for both radial (uses `r`) and rect (uses `x1`).
|
|
991
1036
|
// Falls back to summing branchLength up to the root if neither is present.
|
|
992
1037
|
function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
|
|
@@ -1127,10 +1172,10 @@ function drawPhylogeny(
|
|
|
1127
1172
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1128
1173
|
}
|
|
1129
1174
|
|
|
1130
|
-
if (
|
|
1175
|
+
if (effectiveOnNodeClick) {
|
|
1131
1176
|
internalDots
|
|
1132
1177
|
.style("cursor", "pointer")
|
|
1133
|
-
.on("click", (event, d) =>
|
|
1178
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1134
1179
|
}
|
|
1135
1180
|
}
|
|
1136
1181
|
|
|
@@ -1261,7 +1306,7 @@ function drawPhylogeny(
|
|
|
1261
1306
|
});
|
|
1262
1307
|
}
|
|
1263
1308
|
|
|
1264
|
-
return svg.node();
|
|
1309
|
+
return mount(svg.node());
|
|
1265
1310
|
} else if (layout === "radial") {
|
|
1266
1311
|
// RADIAL LAYOUT
|
|
1267
1312
|
if (width !== height) {
|
|
@@ -1487,10 +1532,10 @@ function drawPhylogeny(
|
|
|
1487
1532
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1488
1533
|
}
|
|
1489
1534
|
|
|
1490
|
-
if (
|
|
1535
|
+
if (effectiveOnNodeClick) {
|
|
1491
1536
|
internalDots
|
|
1492
1537
|
.style("cursor", "pointer")
|
|
1493
|
-
.on("click", (event, d) =>
|
|
1538
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1494
1539
|
}
|
|
1495
1540
|
}
|
|
1496
1541
|
|
|
@@ -1689,7 +1734,7 @@ function drawPhylogeny(
|
|
|
1689
1734
|
});
|
|
1690
1735
|
}
|
|
1691
1736
|
|
|
1692
|
-
return svg.node();
|
|
1737
|
+
return mount(svg.node());
|
|
1693
1738
|
} else if (layout === "unrooted") {
|
|
1694
1739
|
// UNROOTED LAYOUT
|
|
1695
1740
|
const unrootedPhylo = unrooted(parsedTree);
|
|
@@ -1753,11 +1798,11 @@ function drawPhylogeny(
|
|
|
1753
1798
|
.attr("stroke-width", 2)
|
|
1754
1799
|
.attr("fill", (d) => (d.isTip ? "black" : "white"));
|
|
1755
1800
|
|
|
1756
|
-
if (internalNodeCircles &&
|
|
1801
|
+
if (internalNodeCircles && effectiveOnNodeClick) {
|
|
1757
1802
|
nodes
|
|
1758
1803
|
.filter((d) => !d.isTip)
|
|
1759
1804
|
.style("cursor", "pointer")
|
|
1760
|
-
.on("click", (event, d) =>
|
|
1805
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1761
1806
|
}
|
|
1762
1807
|
|
|
1763
1808
|
if (nodeLabels) {
|
|
@@ -1922,7 +1967,7 @@ function drawPhylogeny(
|
|
|
1922
1967
|
});
|
|
1923
1968
|
}
|
|
1924
1969
|
|
|
1925
|
-
return svg.node();
|
|
1970
|
+
return mount(svg.node());
|
|
1926
1971
|
} else {
|
|
1927
1972
|
throw new Error(
|
|
1928
1973
|
"Unsupported layout type. Use 'rect', 'radial', or 'unrooted'."
|