@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
package/dist/index.cjs
CHANGED
|
@@ -1244,9 +1244,12 @@ function addScaleBar(svg, { scale, basis, defaultX, defaultY, scaleBar, fontSize
|
|
|
1244
1244
|
.text(opts.label ?? String(length));
|
|
1245
1245
|
}
|
|
1246
1246
|
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1247
|
+
// input: a Newick string, or an already-parsed tree (the node shape returned
|
|
1248
|
+
// by readTree()/randomTree()). Passing the same parsed tree object back in
|
|
1249
|
+
// across re-renders (e.g. after mutating it with rotate()) keeps node ids
|
|
1250
|
+
// stable, which onNodeClick below relies on.
|
|
1251
|
+
function drawPhylogeny(input, options = {}) {
|
|
1252
|
+
const {
|
|
1250
1253
|
layout = "rect", // rect/radial/unrooted
|
|
1251
1254
|
width = 800,
|
|
1252
1255
|
height = 800,
|
|
@@ -1257,12 +1260,14 @@ function drawPhylogeny(
|
|
|
1257
1260
|
tipLabels = true,
|
|
1258
1261
|
labelFontSize = 10, // font size (px) for tip labels
|
|
1259
1262
|
tipRadius, // px radius of tip dots; defaults to each layout's original size
|
|
1260
|
-
internalNodeCircles = false, // draw a circle at every internal (non-tip) node
|
|
1261
1263
|
internalNodeRadius = 3, // px radius for internal node circles
|
|
1262
1264
|
nodeLabels = false, // draw text labels at internal nodes (e.g. clade/support labels)
|
|
1263
1265
|
nodeLabelFontSize, // defaults to labelFontSize
|
|
1264
1266
|
scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
|
|
1265
1267
|
alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
|
|
1268
|
+
container, // DOM element or CSS selector — if given, drawPhylogeny mounts the SVG itself instead of just returning it
|
|
1269
|
+
rotateOnClick = false, // click an internal node circle to rotate its children in place and auto-redraw (requires container)
|
|
1270
|
+
onNodeClick, // (node, event) => void — fires when an internal node circle is clicked (requires internalNodeCircles: true)
|
|
1266
1271
|
showTooltips = true,
|
|
1267
1272
|
tooltipFormatter = (d, rtt) =>
|
|
1268
1273
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1271,12 +1276,46 @@ function drawPhylogeny(
|
|
|
1271
1276
|
highlightTips = [], // array of tip labels or ids for static highlight (optional)
|
|
1272
1277
|
highlightStroke = "#e63946",
|
|
1273
1278
|
highlightWidth = 2.5
|
|
1274
|
-
} =
|
|
1275
|
-
|
|
1279
|
+
} = options;
|
|
1280
|
+
// rotateOnClick needs internal node circles to click on; auto-enable them
|
|
1281
|
+
// unless the caller explicitly said otherwise.
|
|
1282
|
+
const internalNodeCircles = options.internalNodeCircles ?? rotateOnClick;
|
|
1276
1283
|
|
|
1277
1284
|
// shared helpers
|
|
1278
1285
|
const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
|
|
1279
1286
|
const nodeLabelSize = nodeLabelFontSize ?? labelFontSize;
|
|
1287
|
+
const parsedTree = (input && typeof input === "object" && Array.isArray(input.children))
|
|
1288
|
+
? input
|
|
1289
|
+
: readTree(input);
|
|
1290
|
+
|
|
1291
|
+
const mountEl = typeof container === "string"
|
|
1292
|
+
? (typeof document !== "undefined" ? document.querySelector(container) : null)
|
|
1293
|
+
: (container ?? null);
|
|
1294
|
+
|
|
1295
|
+
if (rotateOnClick && !mountEl) {
|
|
1296
|
+
throw new Error(
|
|
1297
|
+
"rotateOnClick requires a `container` (a DOM element or CSS selector) so drawPhylogeny can mount the redrawn SVG after each rotation."
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// fires the caller's onNodeClick (if any), then handles the rotate+redraw itself
|
|
1302
|
+
const effectiveOnNodeClick = (onNodeClick || rotateOnClick)
|
|
1303
|
+
? (node, event) => {
|
|
1304
|
+
if (typeof onNodeClick === "function") onNodeClick(node, event);
|
|
1305
|
+
if (rotateOnClick) {
|
|
1306
|
+
rotate(parsedTree, node.thisId);
|
|
1307
|
+
drawPhylogeny(parsedTree, options);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
: undefined;
|
|
1311
|
+
|
|
1312
|
+
function mount(svgNode) {
|
|
1313
|
+
if (mountEl) {
|
|
1314
|
+
mountEl.innerHTML = "";
|
|
1315
|
+
mountEl.appendChild(svgNode);
|
|
1316
|
+
}
|
|
1317
|
+
return svgNode;
|
|
1318
|
+
}
|
|
1280
1319
|
// Works for both radial (uses `r`) and rect (uses `x1`).
|
|
1281
1320
|
// Falls back to summing branchLength up to the root if neither is present.
|
|
1282
1321
|
function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
|
|
@@ -1305,7 +1344,7 @@ function drawPhylogeny(
|
|
|
1305
1344
|
|
|
1306
1345
|
if (layout === "rect") {
|
|
1307
1346
|
// RECTANGULAR LAYOUT
|
|
1308
|
-
const tree_df = rectangleLayout(
|
|
1347
|
+
const tree_df = rectangleLayout(parsedTree);
|
|
1309
1348
|
const horizontal = tree_df.horizontal_lines;
|
|
1310
1349
|
const vertical = tree_df.vertical_lines;
|
|
1311
1350
|
const tips = horizontal.filter((d) => d.isTip);
|
|
@@ -1416,6 +1455,12 @@ function drawPhylogeny(
|
|
|
1416
1455
|
.append("title")
|
|
1417
1456
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1418
1457
|
}
|
|
1458
|
+
|
|
1459
|
+
if (effectiveOnNodeClick) {
|
|
1460
|
+
internalDots
|
|
1461
|
+
.style("cursor", "pointer")
|
|
1462
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1463
|
+
}
|
|
1419
1464
|
}
|
|
1420
1465
|
|
|
1421
1466
|
// internal node labels (optional)
|
|
@@ -1545,13 +1590,12 @@ function drawPhylogeny(
|
|
|
1545
1590
|
});
|
|
1546
1591
|
}
|
|
1547
1592
|
|
|
1548
|
-
return svg.node();
|
|
1593
|
+
return mount(svg.node());
|
|
1549
1594
|
} else if (layout === "radial") {
|
|
1550
1595
|
// RADIAL LAYOUT
|
|
1551
1596
|
if (width !== height) {
|
|
1552
1597
|
throw new Error("width and height must be the same for radial layout");
|
|
1553
1598
|
}
|
|
1554
|
-
const parsedTree = readTree(treeText);
|
|
1555
1599
|
const rad = radialLayout(parsedTree, {
|
|
1556
1600
|
angleStrategy: "fan",
|
|
1557
1601
|
arcsStyle: "fan"
|
|
@@ -1771,6 +1815,12 @@ function drawPhylogeny(
|
|
|
1771
1815
|
.append("title")
|
|
1772
1816
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1773
1817
|
}
|
|
1818
|
+
|
|
1819
|
+
if (effectiveOnNodeClick) {
|
|
1820
|
+
internalDots
|
|
1821
|
+
.style("cursor", "pointer")
|
|
1822
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1823
|
+
}
|
|
1774
1824
|
}
|
|
1775
1825
|
|
|
1776
1826
|
// ===== INTERNAL NODE LABELS (optional) =====
|
|
@@ -1968,10 +2018,9 @@ function drawPhylogeny(
|
|
|
1968
2018
|
});
|
|
1969
2019
|
}
|
|
1970
2020
|
|
|
1971
|
-
return svg.node();
|
|
2021
|
+
return mount(svg.node());
|
|
1972
2022
|
} else if (layout === "unrooted") {
|
|
1973
2023
|
// UNROOTED LAYOUT
|
|
1974
|
-
const parsedTree = readTree(treeText);
|
|
1975
2024
|
const unrootedPhylo = unrooted(parsedTree);
|
|
1976
2025
|
|
|
1977
2026
|
const w = width;
|
|
@@ -2033,6 +2082,13 @@ function drawPhylogeny(
|
|
|
2033
2082
|
.attr("stroke-width", 2)
|
|
2034
2083
|
.attr("fill", (d) => (d.isTip ? "black" : "white"));
|
|
2035
2084
|
|
|
2085
|
+
if (internalNodeCircles && effectiveOnNodeClick) {
|
|
2086
|
+
nodes
|
|
2087
|
+
.filter((d) => !d.isTip)
|
|
2088
|
+
.style("cursor", "pointer")
|
|
2089
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2036
2092
|
if (nodeLabels) {
|
|
2037
2093
|
const labeledInternalNodes = unrootedPhylo.data.filter((d) => !d.isTip && d.thisLabel);
|
|
2038
2094
|
group
|
|
@@ -2195,7 +2251,7 @@ function drawPhylogeny(
|
|
|
2195
2251
|
});
|
|
2196
2252
|
}
|
|
2197
2253
|
|
|
2198
|
-
return svg.node();
|
|
2254
|
+
return mount(svg.node());
|
|
2199
2255
|
} else {
|
|
2200
2256
|
throw new Error(
|
|
2201
2257
|
"Unsupported layout type. Use 'rect', 'radial', or 'unrooted'."
|