@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.js
CHANGED
|
@@ -1223,9 +1223,12 @@ function addScaleBar(svg, { scale, basis, defaultX, defaultY, scaleBar, fontSize
|
|
|
1223
1223
|
.text(opts.label ?? String(length));
|
|
1224
1224
|
}
|
|
1225
1225
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1226
|
+
// input: a Newick string, or an already-parsed tree (the node shape returned
|
|
1227
|
+
// by readTree()/randomTree()). Passing the same parsed tree object back in
|
|
1228
|
+
// across re-renders (e.g. after mutating it with rotate()) keeps node ids
|
|
1229
|
+
// stable, which onNodeClick below relies on.
|
|
1230
|
+
function drawPhylogeny(input, options = {}) {
|
|
1231
|
+
const {
|
|
1229
1232
|
layout = "rect", // rect/radial/unrooted
|
|
1230
1233
|
width = 800,
|
|
1231
1234
|
height = 800,
|
|
@@ -1236,12 +1239,14 @@ function drawPhylogeny(
|
|
|
1236
1239
|
tipLabels = true,
|
|
1237
1240
|
labelFontSize = 10, // font size (px) for tip labels
|
|
1238
1241
|
tipRadius, // px radius of tip dots; defaults to each layout's original size
|
|
1239
|
-
internalNodeCircles = false, // draw a circle at every internal (non-tip) node
|
|
1240
1242
|
internalNodeRadius = 3, // px radius for internal node circles
|
|
1241
1243
|
nodeLabels = false, // draw text labels at internal nodes (e.g. clade/support labels)
|
|
1242
1244
|
nodeLabelFontSize, // defaults to labelFontSize
|
|
1243
1245
|
scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
|
|
1244
1246
|
alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
|
|
1247
|
+
container, // DOM element or CSS selector — if given, drawPhylogeny mounts the SVG itself instead of just returning it
|
|
1248
|
+
rotateOnClick = false, // click an internal node circle to rotate its children in place and auto-redraw (requires container)
|
|
1249
|
+
onNodeClick, // (node, event) => void — fires when an internal node circle is clicked (requires internalNodeCircles: true)
|
|
1245
1250
|
showTooltips = true,
|
|
1246
1251
|
tooltipFormatter = (d, rtt) =>
|
|
1247
1252
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1250,12 +1255,46 @@ function drawPhylogeny(
|
|
|
1250
1255
|
highlightTips = [], // array of tip labels or ids for static highlight (optional)
|
|
1251
1256
|
highlightStroke = "#e63946",
|
|
1252
1257
|
highlightWidth = 2.5
|
|
1253
|
-
} =
|
|
1254
|
-
|
|
1258
|
+
} = options;
|
|
1259
|
+
// rotateOnClick needs internal node circles to click on; auto-enable them
|
|
1260
|
+
// unless the caller explicitly said otherwise.
|
|
1261
|
+
const internalNodeCircles = options.internalNodeCircles ?? rotateOnClick;
|
|
1255
1262
|
|
|
1256
1263
|
// shared helpers
|
|
1257
1264
|
const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
|
|
1258
1265
|
const nodeLabelSize = nodeLabelFontSize ?? labelFontSize;
|
|
1266
|
+
const parsedTree = (input && typeof input === "object" && Array.isArray(input.children))
|
|
1267
|
+
? input
|
|
1268
|
+
: readTree(input);
|
|
1269
|
+
|
|
1270
|
+
const mountEl = typeof container === "string"
|
|
1271
|
+
? (typeof document !== "undefined" ? document.querySelector(container) : null)
|
|
1272
|
+
: (container ?? null);
|
|
1273
|
+
|
|
1274
|
+
if (rotateOnClick && !mountEl) {
|
|
1275
|
+
throw new Error(
|
|
1276
|
+
"rotateOnClick requires a `container` (a DOM element or CSS selector) so drawPhylogeny can mount the redrawn SVG after each rotation."
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// fires the caller's onNodeClick (if any), then handles the rotate+redraw itself
|
|
1281
|
+
const effectiveOnNodeClick = (onNodeClick || rotateOnClick)
|
|
1282
|
+
? (node, event) => {
|
|
1283
|
+
if (typeof onNodeClick === "function") onNodeClick(node, event);
|
|
1284
|
+
if (rotateOnClick) {
|
|
1285
|
+
rotate(parsedTree, node.thisId);
|
|
1286
|
+
drawPhylogeny(parsedTree, options);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
: undefined;
|
|
1290
|
+
|
|
1291
|
+
function mount(svgNode) {
|
|
1292
|
+
if (mountEl) {
|
|
1293
|
+
mountEl.innerHTML = "";
|
|
1294
|
+
mountEl.appendChild(svgNode);
|
|
1295
|
+
}
|
|
1296
|
+
return svgNode;
|
|
1297
|
+
}
|
|
1259
1298
|
// Works for both radial (uses `r`) and rect (uses `x1`).
|
|
1260
1299
|
// Falls back to summing branchLength up to the root if neither is present.
|
|
1261
1300
|
function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
|
|
@@ -1284,7 +1323,7 @@ function drawPhylogeny(
|
|
|
1284
1323
|
|
|
1285
1324
|
if (layout === "rect") {
|
|
1286
1325
|
// RECTANGULAR LAYOUT
|
|
1287
|
-
const tree_df = rectangleLayout(
|
|
1326
|
+
const tree_df = rectangleLayout(parsedTree);
|
|
1288
1327
|
const horizontal = tree_df.horizontal_lines;
|
|
1289
1328
|
const vertical = tree_df.vertical_lines;
|
|
1290
1329
|
const tips = horizontal.filter((d) => d.isTip);
|
|
@@ -1395,6 +1434,12 @@ function drawPhylogeny(
|
|
|
1395
1434
|
.append("title")
|
|
1396
1435
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1397
1436
|
}
|
|
1437
|
+
|
|
1438
|
+
if (effectiveOnNodeClick) {
|
|
1439
|
+
internalDots
|
|
1440
|
+
.style("cursor", "pointer")
|
|
1441
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1442
|
+
}
|
|
1398
1443
|
}
|
|
1399
1444
|
|
|
1400
1445
|
// internal node labels (optional)
|
|
@@ -1524,13 +1569,12 @@ function drawPhylogeny(
|
|
|
1524
1569
|
});
|
|
1525
1570
|
}
|
|
1526
1571
|
|
|
1527
|
-
return svg.node();
|
|
1572
|
+
return mount(svg.node());
|
|
1528
1573
|
} else if (layout === "radial") {
|
|
1529
1574
|
// RADIAL LAYOUT
|
|
1530
1575
|
if (width !== height) {
|
|
1531
1576
|
throw new Error("width and height must be the same for radial layout");
|
|
1532
1577
|
}
|
|
1533
|
-
const parsedTree = readTree(treeText);
|
|
1534
1578
|
const rad = radialLayout(parsedTree, {
|
|
1535
1579
|
angleStrategy: "fan",
|
|
1536
1580
|
arcsStyle: "fan"
|
|
@@ -1750,6 +1794,12 @@ function drawPhylogeny(
|
|
|
1750
1794
|
.append("title")
|
|
1751
1795
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1752
1796
|
}
|
|
1797
|
+
|
|
1798
|
+
if (effectiveOnNodeClick) {
|
|
1799
|
+
internalDots
|
|
1800
|
+
.style("cursor", "pointer")
|
|
1801
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
1802
|
+
}
|
|
1753
1803
|
}
|
|
1754
1804
|
|
|
1755
1805
|
// ===== INTERNAL NODE LABELS (optional) =====
|
|
@@ -1947,10 +1997,9 @@ function drawPhylogeny(
|
|
|
1947
1997
|
});
|
|
1948
1998
|
}
|
|
1949
1999
|
|
|
1950
|
-
return svg.node();
|
|
2000
|
+
return mount(svg.node());
|
|
1951
2001
|
} else if (layout === "unrooted") {
|
|
1952
2002
|
// UNROOTED LAYOUT
|
|
1953
|
-
const parsedTree = readTree(treeText);
|
|
1954
2003
|
const unrootedPhylo = unrooted(parsedTree);
|
|
1955
2004
|
|
|
1956
2005
|
const w = width;
|
|
@@ -2012,6 +2061,13 @@ function drawPhylogeny(
|
|
|
2012
2061
|
.attr("stroke-width", 2)
|
|
2013
2062
|
.attr("fill", (d) => (d.isTip ? "black" : "white"));
|
|
2014
2063
|
|
|
2064
|
+
if (internalNodeCircles && effectiveOnNodeClick) {
|
|
2065
|
+
nodes
|
|
2066
|
+
.filter((d) => !d.isTip)
|
|
2067
|
+
.style("cursor", "pointer")
|
|
2068
|
+
.on("click", (event, d) => effectiveOnNodeClick(d, event));
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2015
2071
|
if (nodeLabels) {
|
|
2016
2072
|
const labeledInternalNodes = unrootedPhylo.data.filter((d) => !d.isTip && d.thisLabel);
|
|
2017
2073
|
group
|
|
@@ -2174,7 +2230,7 @@ function drawPhylogeny(
|
|
|
2174
2230
|
});
|
|
2175
2231
|
}
|
|
2176
2232
|
|
|
2177
|
-
return svg.node();
|
|
2233
|
+
return mount(svg.node());
|
|
2178
2234
|
} else {
|
|
2179
2235
|
throw new Error(
|
|
2180
2236
|
"Unsupported layout type. Use 'rect', 'radial', or 'unrooted'."
|