@euphrasiologist/lwphylo 1.4.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 +8 -2
- package/dist/drawPhylogeny.cjs +82 -12
- package/dist/drawPhylogeny.cjs.map +1 -1
- package/dist/drawPhylogeny.esm.js +82 -12
- 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 +68 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +68 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -816,6 +816,20 @@ function unrooted (node) {
|
|
|
816
816
|
return data;
|
|
817
817
|
}
|
|
818
818
|
|
|
819
|
+
/**
|
|
820
|
+
* Reverse the child order at one node, flipping which side of the tree
|
|
821
|
+
* that clade's descendants are drawn on. Defaults to the root. Mutates
|
|
822
|
+
* the tree in place.
|
|
823
|
+
*/
|
|
824
|
+
function rotate(tree, nodeId = tree.id) {
|
|
825
|
+
const target = preorder(tree).find((n) => n.id === nodeId);
|
|
826
|
+
if (!target) {
|
|
827
|
+
throw new Error(`No node with id ${nodeId} found in tree`);
|
|
828
|
+
}
|
|
829
|
+
target.children.reverse();
|
|
830
|
+
return tree;
|
|
831
|
+
}
|
|
832
|
+
|
|
819
833
|
/**
|
|
820
834
|
* Parse a Newick tree string into a doubly-linked list of JS Objects.
|
|
821
835
|
* Assigns labels, branch lengths, and node IDs (tips before internals if input emits them that way).
|
|
@@ -925,9 +939,12 @@ function addScaleBar(svg, { scale, basis, defaultX, defaultY, scaleBar, fontSize
|
|
|
925
939
|
.text(opts.label ?? String(length));
|
|
926
940
|
}
|
|
927
941
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
942
|
+
// input: a Newick string, or an already-parsed tree (the node shape returned
|
|
943
|
+
// by readTree()/randomTree()). Passing the same parsed tree object back in
|
|
944
|
+
// across re-renders (e.g. after mutating it with rotate()) keeps node ids
|
|
945
|
+
// stable, which onNodeClick below relies on.
|
|
946
|
+
function drawPhylogeny(input, options = {}) {
|
|
947
|
+
const {
|
|
931
948
|
layout = "rect", // rect/radial/unrooted
|
|
932
949
|
width = 800,
|
|
933
950
|
height = 800,
|
|
@@ -938,12 +955,14 @@ function drawPhylogeny(
|
|
|
938
955
|
tipLabels = true,
|
|
939
956
|
labelFontSize = 10, // font size (px) for tip labels
|
|
940
957
|
tipRadius, // px radius of tip dots; defaults to each layout's original size
|
|
941
|
-
internalNodeCircles = false, // draw a circle at every internal (non-tip) node
|
|
942
958
|
internalNodeRadius = 3, // px radius for internal node circles
|
|
943
959
|
nodeLabels = false, // draw text labels at internal nodes (e.g. clade/support labels)
|
|
944
960
|
nodeLabelFontSize, // defaults to labelFontSize
|
|
945
961
|
scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
|
|
946
962
|
alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
|
|
963
|
+
container, // DOM element or CSS selector — if given, drawPhylogeny mounts the SVG itself instead of just returning it
|
|
964
|
+
rotateOnClick = false, // click an internal node circle to rotate its children in place and auto-redraw (requires container)
|
|
965
|
+
onNodeClick, // (node, event) => void — fires when an internal node circle is clicked (requires internalNodeCircles: true)
|
|
947
966
|
showTooltips = true,
|
|
948
967
|
tooltipFormatter = (d, rtt) =>
|
|
949
968
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -952,12 +971,46 @@ function drawPhylogeny(
|
|
|
952
971
|
highlightTips = [], // array of tip labels or ids for static highlight (optional)
|
|
953
972
|
highlightStroke = "#e63946",
|
|
954
973
|
highlightWidth = 2.5
|
|
955
|
-
} =
|
|
956
|
-
|
|
974
|
+
} = options;
|
|
975
|
+
// rotateOnClick needs internal node circles to click on; auto-enable them
|
|
976
|
+
// unless the caller explicitly said otherwise.
|
|
977
|
+
const internalNodeCircles = options.internalNodeCircles ?? rotateOnClick;
|
|
957
978
|
|
|
958
979
|
// shared helpers
|
|
959
980
|
const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
|
|
960
981
|
const nodeLabelSize = nodeLabelFontSize ?? labelFontSize;
|
|
982
|
+
const parsedTree = (input && typeof input === "object" && Array.isArray(input.children))
|
|
983
|
+
? input
|
|
984
|
+
: readTree(input);
|
|
985
|
+
|
|
986
|
+
const mountEl = typeof container === "string"
|
|
987
|
+
? (typeof document !== "undefined" ? document.querySelector(container) : null)
|
|
988
|
+
: (container ?? null);
|
|
989
|
+
|
|
990
|
+
if (rotateOnClick && !mountEl) {
|
|
991
|
+
throw new Error(
|
|
992
|
+
"rotateOnClick requires a `container` (a DOM element or CSS selector) so drawPhylogeny can mount the redrawn SVG after each rotation."
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// fires the caller's onNodeClick (if any), then handles the rotate+redraw itself
|
|
997
|
+
const effectiveOnNodeClick = (onNodeClick || rotateOnClick)
|
|
998
|
+
? (node, event) => {
|
|
999
|
+
if (typeof onNodeClick === "function") onNodeClick(node, event);
|
|
1000
|
+
if (rotateOnClick) {
|
|
1001
|
+
rotate(parsedTree, node.thisId);
|
|
1002
|
+
drawPhylogeny(parsedTree, options);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
: undefined;
|
|
1006
|
+
|
|
1007
|
+
function mount(svgNode) {
|
|
1008
|
+
if (mountEl) {
|
|
1009
|
+
mountEl.innerHTML = "";
|
|
1010
|
+
mountEl.appendChild(svgNode);
|
|
1011
|
+
}
|
|
1012
|
+
return svgNode;
|
|
1013
|
+
}
|
|
961
1014
|
// Works for both radial (uses `r`) and rect (uses `x1`).
|
|
962
1015
|
// Falls back to summing branchLength up to the root if neither is present.
|
|
963
1016
|
function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
|
|
@@ -986,7 +1039,7 @@ function drawPhylogeny(
|
|
|
986
1039
|
|
|
987
1040
|
if (layout === "rect") {
|
|
988
1041
|
// RECTANGULAR LAYOUT
|
|
989
|
-
const tree_df = rectangleLayout(
|
|
1042
|
+
const tree_df = rectangleLayout(parsedTree);
|
|
990
1043
|
const horizontal = tree_df.horizontal_lines;
|
|
991
1044
|
const vertical = tree_df.vertical_lines;
|
|
992
1045
|
const tips = horizontal.filter((d) => d.isTip);
|
|
@@ -1097,6 +1150,12 @@ function drawPhylogeny(
|
|
|
1097
1150
|
.append("title")
|
|
1098
1151
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1099
1152
|
}
|
|
1153
|
+
|
|
1154
|
+
if (effectiveOnNodeClick) {
|
|
1155
|
+
internalDots
|
|
1156
|
+
.style("cursor", "pointer")
|
|
1157
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1158
|
+
}
|
|
1100
1159
|
}
|
|
1101
1160
|
|
|
1102
1161
|
// internal node labels (optional)
|
|
@@ -1226,13 +1285,12 @@ function drawPhylogeny(
|
|
|
1226
1285
|
});
|
|
1227
1286
|
}
|
|
1228
1287
|
|
|
1229
|
-
return svg.node();
|
|
1288
|
+
return mount(svg.node());
|
|
1230
1289
|
} else if (layout === "radial") {
|
|
1231
1290
|
// RADIAL LAYOUT
|
|
1232
1291
|
if (width !== height) {
|
|
1233
1292
|
throw new Error("width and height must be the same for radial layout");
|
|
1234
1293
|
}
|
|
1235
|
-
const parsedTree = readTree(treeText);
|
|
1236
1294
|
const rad = radialLayout(parsedTree, {
|
|
1237
1295
|
angleStrategy: "fan",
|
|
1238
1296
|
arcsStyle: "fan"
|
|
@@ -1452,6 +1510,12 @@ function drawPhylogeny(
|
|
|
1452
1510
|
.append("title")
|
|
1453
1511
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1454
1512
|
}
|
|
1513
|
+
|
|
1514
|
+
if (effectiveOnNodeClick) {
|
|
1515
|
+
internalDots
|
|
1516
|
+
.style("cursor", "pointer")
|
|
1517
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1518
|
+
}
|
|
1455
1519
|
}
|
|
1456
1520
|
|
|
1457
1521
|
// ===== INTERNAL NODE LABELS (optional) =====
|
|
@@ -1649,10 +1713,9 @@ function drawPhylogeny(
|
|
|
1649
1713
|
});
|
|
1650
1714
|
}
|
|
1651
1715
|
|
|
1652
|
-
return svg.node();
|
|
1716
|
+
return mount(svg.node());
|
|
1653
1717
|
} else if (layout === "unrooted") {
|
|
1654
1718
|
// UNROOTED LAYOUT
|
|
1655
|
-
const parsedTree = readTree(treeText);
|
|
1656
1719
|
const unrootedPhylo = unrooted(parsedTree);
|
|
1657
1720
|
|
|
1658
1721
|
const w = width;
|
|
@@ -1714,6 +1777,13 @@ function drawPhylogeny(
|
|
|
1714
1777
|
.attr("stroke-width", 2)
|
|
1715
1778
|
.attr("fill", (d) => (d.isTip ? "black" : "white"));
|
|
1716
1779
|
|
|
1780
|
+
if (internalNodeCircles && effectiveOnNodeClick) {
|
|
1781
|
+
nodes
|
|
1782
|
+
.filter((d) => !d.isTip)
|
|
1783
|
+
.style("cursor", "pointer")
|
|
1784
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1717
1787
|
if (nodeLabels) {
|
|
1718
1788
|
const labeledInternalNodes = unrootedPhylo.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1719
1789
|
group
|
|
@@ -1876,7 +1946,7 @@ function drawPhylogeny(
|
|
|
1876
1946
|
});
|
|
1877
1947
|
}
|
|
1878
1948
|
|
|
1879
|
-
return svg.node();
|
|
1949
|
+
return mount(svg.node());
|
|
1880
1950
|
} else {
|
|
1881
1951
|
throw new Error(
|
|
1882
1952
|
"Unsupported layout type. Use 'rect', 'radial', or 'unrooted'."
|