@euphrasiologist/lwphylo 1.3.1 → 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.
- package/README.md +11 -1
- package/dist/drawPhylogeny.cjs +255 -13
- package/dist/drawPhylogeny.cjs.map +1 -1
- package/dist/drawPhylogeny.esm.js +255 -13
- 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 +288 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +287 -14
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -950,6 +950,23 @@ function unrooted (node) {
|
|
|
950
950
|
return data;
|
|
951
951
|
}
|
|
952
952
|
|
|
953
|
+
/**
|
|
954
|
+
* Reorder every node's children by descendant tip count, so the tree
|
|
955
|
+
* "ladders" from smallest to largest clade at each branching point
|
|
956
|
+
* (or the reverse, with ascending: false). Mutates the tree in place.
|
|
957
|
+
*/
|
|
958
|
+
function ladderize(tree, { ascending = true } = {}) {
|
|
959
|
+
const sign = ascending ? 1 : -1;
|
|
960
|
+
|
|
961
|
+
(function sort(node) {
|
|
962
|
+
if (node.children.length === 0) return;
|
|
963
|
+
node.children.forEach(sort);
|
|
964
|
+
node.children.sort((a, b) => sign * (numTips(a) - numTips(b)));
|
|
965
|
+
})(tree);
|
|
966
|
+
|
|
967
|
+
return tree;
|
|
968
|
+
}
|
|
969
|
+
|
|
953
970
|
function makeIndexById(rows, key = "thisId") {
|
|
954
971
|
return new Map(rows.map(d => [d[key], d]));
|
|
955
972
|
}
|
|
@@ -959,6 +976,20 @@ function parentFisheye(d, data) {
|
|
|
959
976
|
return parent ? { px: parent.fisheye.x, py: parent.fisheye.y } : null;
|
|
960
977
|
}
|
|
961
978
|
|
|
979
|
+
/**
|
|
980
|
+
* Reverse the child order at one node, flipping which side of the tree
|
|
981
|
+
* that clade's descendants are drawn on. Defaults to the root. Mutates
|
|
982
|
+
* the tree in place.
|
|
983
|
+
*/
|
|
984
|
+
function rotate(tree, nodeId = tree.id) {
|
|
985
|
+
const target = preorder(tree).find((n) => n.id === nodeId);
|
|
986
|
+
if (!target) {
|
|
987
|
+
throw new Error(`No node with id ${nodeId} found in tree`);
|
|
988
|
+
}
|
|
989
|
+
target.children.reverse();
|
|
990
|
+
return tree;
|
|
991
|
+
}
|
|
992
|
+
|
|
962
993
|
/**
|
|
963
994
|
* Generate a random bifurcating tree with `nTips` tips, in the same
|
|
964
995
|
* parent/children node shape produced by readTree().
|
|
@@ -1173,8 +1204,52 @@ function serialize(node) {
|
|
|
1173
1204
|
return `(${children})${label}${branch}`;
|
|
1174
1205
|
}
|
|
1175
1206
|
|
|
1207
|
+
// Round a raw data-space length to a "nice" 1/2/5-of-a-power-of-ten value,
|
|
1208
|
+
// so an auto-sized scale bar doesn't show an ugly number like "0.347".
|
|
1209
|
+
function niceScaleLength(target) {
|
|
1210
|
+
if (!(target > 0)) return 1;
|
|
1211
|
+
const exp = Math.floor(Math.log10(target));
|
|
1212
|
+
const base = Math.pow(10, exp);
|
|
1213
|
+
const residual = target / base;
|
|
1214
|
+
const niceResidual = residual < 1.5 ? 1 : residual < 3.5 ? 2 : residual < 7.5 ? 5 : 10;
|
|
1215
|
+
return niceResidual * base;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
// scaleBar: true | number (explicit data-units length) | { length, x, y, label }
|
|
1219
|
+
function addScaleBar(svg, { scale, basis, defaultX, defaultY, scaleBar, fontSize }) {
|
|
1220
|
+
const opts = (scaleBar === true || typeof scaleBar === "number") ? {} : scaleBar;
|
|
1221
|
+
const length = typeof scaleBar === "number" ? scaleBar : (opts.length ?? niceScaleLength(basis / 5));
|
|
1222
|
+
const x = opts.x ?? defaultX;
|
|
1223
|
+
const y = opts.y ?? defaultY;
|
|
1224
|
+
const barPx = scale(length) - scale(0);
|
|
1225
|
+
|
|
1226
|
+
const g = svg.append("g").attr("class", "phylo_scale_bar");
|
|
1227
|
+
g.append("line")
|
|
1228
|
+
.attr("x1", x).attr("x2", x + barPx)
|
|
1229
|
+
.attr("y1", y).attr("y2", y)
|
|
1230
|
+
.attr("stroke", "#000")
|
|
1231
|
+
.attr("stroke-width", 1);
|
|
1232
|
+
[x, x + barPx].forEach((tx) => {
|
|
1233
|
+
g.append("line")
|
|
1234
|
+
.attr("x1", tx).attr("x2", tx)
|
|
1235
|
+
.attr("y1", y - 4).attr("y2", y + 4)
|
|
1236
|
+
.attr("stroke", "#000")
|
|
1237
|
+
.attr("stroke-width", 1);
|
|
1238
|
+
});
|
|
1239
|
+
g.append("text")
|
|
1240
|
+
.attr("x", x + barPx / 2)
|
|
1241
|
+
.attr("y", y - 6)
|
|
1242
|
+
.attr("text-anchor", "middle")
|
|
1243
|
+
.attr("font-size", fontSize)
|
|
1244
|
+
.text(opts.label ?? String(length));
|
|
1245
|
+
}
|
|
1246
|
+
|
|
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.
|
|
1176
1251
|
function drawPhylogeny(
|
|
1177
|
-
|
|
1252
|
+
input,
|
|
1178
1253
|
{
|
|
1179
1254
|
layout = "rect", // rect/radial/unrooted
|
|
1180
1255
|
width = 800,
|
|
@@ -1185,6 +1260,14 @@ function drawPhylogeny(
|
|
|
1185
1260
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
1186
1261
|
tipLabels = true,
|
|
1187
1262
|
labelFontSize = 10, // font size (px) for tip labels
|
|
1263
|
+
tipRadius, // px radius of tip dots; defaults to each layout's original size
|
|
1264
|
+
internalNodeCircles = false, // draw a circle at every internal (non-tip) node
|
|
1265
|
+
internalNodeRadius = 3, // px radius for internal node circles
|
|
1266
|
+
nodeLabels = false, // draw text labels at internal nodes (e.g. clade/support labels)
|
|
1267
|
+
nodeLabelFontSize, // defaults to labelFontSize
|
|
1268
|
+
scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
|
|
1269
|
+
alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
|
|
1270
|
+
onNodeClick, // (node, event) => void — fires when an internal node circle is clicked (requires internalNodeCircles: true)
|
|
1188
1271
|
showTooltips = true,
|
|
1189
1272
|
tooltipFormatter = (d, rtt) =>
|
|
1190
1273
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1198,6 +1281,10 @@ function drawPhylogeny(
|
|
|
1198
1281
|
|
|
1199
1282
|
// shared helpers
|
|
1200
1283
|
const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
|
|
1284
|
+
const nodeLabelSize = nodeLabelFontSize ?? labelFontSize;
|
|
1285
|
+
const parsedTree = (input && typeof input === "object" && Array.isArray(input.children))
|
|
1286
|
+
? input
|
|
1287
|
+
: readTree(input);
|
|
1201
1288
|
// Works for both radial (uses `r`) and rect (uses `x1`).
|
|
1202
1289
|
// Falls back to summing branchLength up to the root if neither is present.
|
|
1203
1290
|
function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
|
|
@@ -1226,7 +1313,7 @@ function drawPhylogeny(
|
|
|
1226
1313
|
|
|
1227
1314
|
if (layout === "rect") {
|
|
1228
1315
|
// RECTANGULAR LAYOUT
|
|
1229
|
-
const tree_df = rectangleLayout(
|
|
1316
|
+
const tree_df = rectangleLayout(parsedTree);
|
|
1230
1317
|
const horizontal = tree_df.horizontal_lines;
|
|
1231
1318
|
const vertical = tree_df.vertical_lines;
|
|
1232
1319
|
const tips = horizontal.filter((d) => d.isTip);
|
|
@@ -1236,6 +1323,7 @@ function drawPhylogeny(
|
|
|
1236
1323
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1237
1324
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1238
1325
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
1326
|
+
const R_TIP = tipRadius ?? 2;
|
|
1239
1327
|
|
|
1240
1328
|
const maxY = d3__namespace.max(horizontal, (d) => d.y1);
|
|
1241
1329
|
const minY = d3__namespace.min(horizontal, (d) => d.y1);
|
|
@@ -1293,7 +1381,7 @@ function drawPhylogeny(
|
|
|
1293
1381
|
.join("circle")
|
|
1294
1382
|
.attr("cx", (d) => xScale(d.x1))
|
|
1295
1383
|
.attr("cy", (d) => yScale(d.y1))
|
|
1296
|
-
.attr("r",
|
|
1384
|
+
.attr("r", R_TIP)
|
|
1297
1385
|
.attr("fill", "black");
|
|
1298
1386
|
|
|
1299
1387
|
// tooltips for rect dots
|
|
@@ -1308,13 +1396,78 @@ function drawPhylogeny(
|
|
|
1308
1396
|
.on("mouseenter", function(_event, d) {
|
|
1309
1397
|
hoverLayer.selectAll("*").remove();
|
|
1310
1398
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1311
|
-
d3__namespace.select(this).attr("r",
|
|
1399
|
+
d3__namespace.select(this).attr("r", R_TIP + 2);
|
|
1312
1400
|
})
|
|
1313
1401
|
.on("mouseleave", function() {
|
|
1314
1402
|
hoverLayer.selectAll("*").remove();
|
|
1315
|
-
d3__namespace.select(this).attr("r",
|
|
1403
|
+
d3__namespace.select(this).attr("r", R_TIP);
|
|
1316
1404
|
});
|
|
1317
1405
|
|
|
1406
|
+
// internal node circles (optional)
|
|
1407
|
+
if (internalNodeCircles) {
|
|
1408
|
+
const internalNodes = tree_df.data.filter((d) => !d.isTip);
|
|
1409
|
+
const internalDots = group
|
|
1410
|
+
.append("g")
|
|
1411
|
+
.attr("class", "phylo_internal_dots")
|
|
1412
|
+
.selectAll("circle")
|
|
1413
|
+
.data(internalNodes)
|
|
1414
|
+
.join("circle")
|
|
1415
|
+
.attr("cx", (d) => xScale(d.x1))
|
|
1416
|
+
.attr("cy", (d) => yScale(d.y1))
|
|
1417
|
+
.attr("r", internalNodeRadius)
|
|
1418
|
+
.attr("fill", "white")
|
|
1419
|
+
.attr("stroke", "#555")
|
|
1420
|
+
.attr("stroke-width", 1);
|
|
1421
|
+
|
|
1422
|
+
if (showTooltips) {
|
|
1423
|
+
internalDots
|
|
1424
|
+
.append("title")
|
|
1425
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (onNodeClick) {
|
|
1429
|
+
internalDots
|
|
1430
|
+
.style("cursor", "pointer")
|
|
1431
|
+
.on("click", (event, d) => onNodeClick(d, event));
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// internal node labels (optional)
|
|
1436
|
+
if (nodeLabels) {
|
|
1437
|
+
const labeledInternalNodes = tree_df.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1438
|
+
svg
|
|
1439
|
+
.append("g")
|
|
1440
|
+
.attr("class", "phylo_node_labels")
|
|
1441
|
+
.selectAll("text")
|
|
1442
|
+
.data(labeledInternalNodes)
|
|
1443
|
+
.join("text")
|
|
1444
|
+
.attr("x", (d) => xScale(d.x1) - 4)
|
|
1445
|
+
.attr("y", (d) => yScale(d.y1) - 4)
|
|
1446
|
+
.attr("text-anchor", "end")
|
|
1447
|
+
.attr("font-size", nodeLabelSize)
|
|
1448
|
+
.text((d) => d.thisLabel);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// column that tip labels align to when alignTipLabels is set
|
|
1452
|
+
const alignX = xScale(maxX);
|
|
1453
|
+
|
|
1454
|
+
// dashed guide lines from each tip's true branch end to the aligned label column
|
|
1455
|
+
if (tipLabels && alignTipLabels) {
|
|
1456
|
+
group
|
|
1457
|
+
.append("g")
|
|
1458
|
+
.attr("class", "phylo_align_guides")
|
|
1459
|
+
.selectAll("line")
|
|
1460
|
+
.data(tips)
|
|
1461
|
+
.join("line")
|
|
1462
|
+
.attr("x1", (d) => xScale(d.x1))
|
|
1463
|
+
.attr("x2", alignX)
|
|
1464
|
+
.attr("y1", (d) => yScale(d.y1))
|
|
1465
|
+
.attr("y2", (d) => yScale(d.y1))
|
|
1466
|
+
.attr("stroke", "#999")
|
|
1467
|
+
.attr("stroke-width", 1)
|
|
1468
|
+
.attr("stroke-dasharray", "2,2");
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1318
1471
|
// labels
|
|
1319
1472
|
if (tipLabels) {
|
|
1320
1473
|
const labels = svg
|
|
@@ -1323,7 +1476,7 @@ function drawPhylogeny(
|
|
|
1323
1476
|
.selectAll("text")
|
|
1324
1477
|
.data(tips)
|
|
1325
1478
|
.join("text")
|
|
1326
|
-
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1479
|
+
.attr("x", (d) => (alignTipLabels ? alignX : xScale(d.x1)) + 4)
|
|
1327
1480
|
.attr("y", (d) => yScale(d.y1))
|
|
1328
1481
|
.attr("dy", "0.32em")
|
|
1329
1482
|
.attr("font-size", labelFontSize)
|
|
@@ -1395,13 +1548,23 @@ function drawPhylogeny(
|
|
|
1395
1548
|
}
|
|
1396
1549
|
}
|
|
1397
1550
|
|
|
1551
|
+
if (scaleBar) {
|
|
1552
|
+
addScaleBar(svg, {
|
|
1553
|
+
scale: xScale,
|
|
1554
|
+
basis: maxX,
|
|
1555
|
+
defaultX: margin.left,
|
|
1556
|
+
defaultY: height - margin.bottom / 2,
|
|
1557
|
+
scaleBar,
|
|
1558
|
+
fontSize: labelFontSize
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1398
1562
|
return svg.node();
|
|
1399
1563
|
} else if (layout === "radial") {
|
|
1400
1564
|
// RADIAL LAYOUT
|
|
1401
1565
|
if (width !== height) {
|
|
1402
1566
|
throw new Error("width and height must be the same for radial layout");
|
|
1403
1567
|
}
|
|
1404
|
-
const parsedTree = readTree(treeText);
|
|
1405
1568
|
const rad = radialLayout(parsedTree, {
|
|
1406
1569
|
angleStrategy: "fan",
|
|
1407
1570
|
arcsStyle: "fan"
|
|
@@ -1416,7 +1579,7 @@ function drawPhylogeny(
|
|
|
1416
1579
|
|
|
1417
1580
|
|
|
1418
1581
|
// visuals (0 = let spokes reach the dots)
|
|
1419
|
-
const DOT_R = 3;
|
|
1582
|
+
const DOT_R = tipRadius ?? 3;
|
|
1420
1583
|
const END_CAP = 0;
|
|
1421
1584
|
|
|
1422
1585
|
// ===== SCALES / BOUNDS =====
|
|
@@ -1554,6 +1717,25 @@ function drawPhylogeny(
|
|
|
1554
1717
|
.attr("stroke-width", strokeWidth);
|
|
1555
1718
|
});
|
|
1556
1719
|
|
|
1720
|
+
// ===== ALIGN GUIDES (optional) =====
|
|
1721
|
+
// dashed lines from each tip's true position to the common label ring,
|
|
1722
|
+
// for radialMode "phylo" (true terminals) where tips aren't already co-circular
|
|
1723
|
+
if (tipLabels && alignTipLabels && !isOuter) {
|
|
1724
|
+
group
|
|
1725
|
+
.append("g")
|
|
1726
|
+
.attr("class", "phylo_align_guides")
|
|
1727
|
+
.selectAll("line")
|
|
1728
|
+
.data(tips)
|
|
1729
|
+
.join("line")
|
|
1730
|
+
.attr("x1", (d) => xScaleRadial(d.x))
|
|
1731
|
+
.attr("y1", (d) => yScaleRadial(d.y))
|
|
1732
|
+
.attr("x2", (d) => xScaleRadial(tipMaxR * Math.cos(d.angle)))
|
|
1733
|
+
.attr("y2", (d) => yScaleRadial(tipMaxR * Math.sin(d.angle)))
|
|
1734
|
+
.attr("stroke", "#999")
|
|
1735
|
+
.attr("stroke-width", 1)
|
|
1736
|
+
.attr("stroke-dasharray", "2,2");
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1557
1739
|
// ===== TIP DOTS =====
|
|
1558
1740
|
const tipDots = group
|
|
1559
1741
|
.append("g")
|
|
@@ -1581,6 +1763,51 @@ function drawPhylogeny(
|
|
|
1581
1763
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1582
1764
|
}
|
|
1583
1765
|
|
|
1766
|
+
// ===== INTERNAL NODE CIRCLES (optional) =====
|
|
1767
|
+
if (internalNodeCircles) {
|
|
1768
|
+
const internalNodes = rad.data.filter((d) => !d.isTip);
|
|
1769
|
+
const internalDots = group
|
|
1770
|
+
.append("g")
|
|
1771
|
+
.attr("class", "phylo_internal_dots")
|
|
1772
|
+
.selectAll("circle")
|
|
1773
|
+
.data(internalNodes)
|
|
1774
|
+
.join("circle")
|
|
1775
|
+
.attr("cx", (d) => xScaleRadial(d.x))
|
|
1776
|
+
.attr("cy", (d) => yScaleRadial(d.y))
|
|
1777
|
+
.attr("r", internalNodeRadius)
|
|
1778
|
+
.attr("fill", "white")
|
|
1779
|
+
.attr("stroke", "#555")
|
|
1780
|
+
.attr("stroke-width", 1);
|
|
1781
|
+
|
|
1782
|
+
if (showTooltips) {
|
|
1783
|
+
internalDots
|
|
1784
|
+
.append("title")
|
|
1785
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
if (onNodeClick) {
|
|
1789
|
+
internalDots
|
|
1790
|
+
.style("cursor", "pointer")
|
|
1791
|
+
.on("click", (event, d) => onNodeClick(d, event));
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
// ===== INTERNAL NODE LABELS (optional) =====
|
|
1796
|
+
if (nodeLabels) {
|
|
1797
|
+
const labeledInternalNodes = rad.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1798
|
+
group
|
|
1799
|
+
.append("g")
|
|
1800
|
+
.attr("class", "phylo_node_labels")
|
|
1801
|
+
.selectAll("text")
|
|
1802
|
+
.data(labeledInternalNodes)
|
|
1803
|
+
.join("text")
|
|
1804
|
+
.attr("x", (d) => xScaleRadial(d.x) + 4)
|
|
1805
|
+
.attr("y", (d) => yScaleRadial(d.y) - 4)
|
|
1806
|
+
.attr("font-size", nodeLabelSize)
|
|
1807
|
+
.attr("fill", "black")
|
|
1808
|
+
.text((d) => d.thisLabel);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1584
1811
|
// maps for fast lookup on hover (childId → spoke / arc)
|
|
1585
1812
|
const key = (x) => (typeof x === "string" ? +x : x);
|
|
1586
1813
|
const spokeByChild = new Map(rad.radii.map(s => [key(s.childId ?? s.thisId ?? s.id1), s]));
|
|
@@ -1601,7 +1828,8 @@ function drawPhylogeny(
|
|
|
1601
1828
|
// same tip position rule as dots/spokes:
|
|
1602
1829
|
// - "outer": snap to common ring (tipMaxR)
|
|
1603
1830
|
// - otherwise (e.g. "align"/"phylo"): true tip radius
|
|
1604
|
-
|
|
1831
|
+
// - alignTipLabels forces the common ring regardless of mode
|
|
1832
|
+
const r = (isOuter || alignTipLabels) ? tipMaxR : d.r;
|
|
1605
1833
|
const x = r * Math.cos(d.angle);
|
|
1606
1834
|
const y = r * Math.sin(d.angle);
|
|
1607
1835
|
return `translate(${xScaleRadial(x)},${yScaleRadial(y)})`;
|
|
@@ -1748,10 +1976,20 @@ function drawPhylogeny(
|
|
|
1748
1976
|
});
|
|
1749
1977
|
}
|
|
1750
1978
|
|
|
1979
|
+
if (scaleBar) {
|
|
1980
|
+
addScaleBar(svg, {
|
|
1981
|
+
scale: xScaleRadial,
|
|
1982
|
+
basis: maxRadius,
|
|
1983
|
+
defaultX: 20,
|
|
1984
|
+
defaultY: h - 20,
|
|
1985
|
+
scaleBar,
|
|
1986
|
+
fontSize: labelFontSize
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1751
1990
|
return svg.node();
|
|
1752
1991
|
} else if (layout === "unrooted") {
|
|
1753
1992
|
// UNROOTED LAYOUT
|
|
1754
|
-
const parsedTree = readTree(treeText);
|
|
1755
1993
|
const unrootedPhylo = unrooted(parsedTree);
|
|
1756
1994
|
|
|
1757
1995
|
const w = width;
|
|
@@ -1797,6 +2035,8 @@ function drawPhylogeny(
|
|
|
1797
2035
|
.attr("stroke-width", strokeWidth)
|
|
1798
2036
|
.attr("stroke", "#777");
|
|
1799
2037
|
|
|
2038
|
+
const R_TIP = tipRadius ?? 4;
|
|
2039
|
+
|
|
1800
2040
|
const nodes = group
|
|
1801
2041
|
.append("g")
|
|
1802
2042
|
.attr("class", "phylo_points")
|
|
@@ -1804,13 +2044,35 @@ function drawPhylogeny(
|
|
|
1804
2044
|
.data(unrootedPhylo.data)
|
|
1805
2045
|
.join("circle")
|
|
1806
2046
|
.attr("class", "dot")
|
|
1807
|
-
.attr("r", (d) => (d.isTip ?
|
|
2047
|
+
.attr("r", (d) => (d.isTip ? R_TIP : (internalNodeCircles ? internalNodeRadius : 0)))
|
|
1808
2048
|
.attr("cx", (d) => xScaleUnroot(d.x))
|
|
1809
2049
|
.attr("cy", (d) => yScaleUnroot(d.y))
|
|
1810
2050
|
.attr("stroke", "black")
|
|
1811
2051
|
.attr("stroke-width", 2)
|
|
1812
2052
|
.attr("fill", (d) => (d.isTip ? "black" : "white"));
|
|
1813
2053
|
|
|
2054
|
+
if (internalNodeCircles && onNodeClick) {
|
|
2055
|
+
nodes
|
|
2056
|
+
.filter((d) => !d.isTip)
|
|
2057
|
+
.style("cursor", "pointer")
|
|
2058
|
+
.on("click", (event, d) => onNodeClick(d, event));
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
if (nodeLabels) {
|
|
2062
|
+
const labeledInternalNodes = unrootedPhylo.data.filter((d) => !d.isTip && d.thisLabel);
|
|
2063
|
+
group
|
|
2064
|
+
.append("g")
|
|
2065
|
+
.attr("class", "phylo_node_labels")
|
|
2066
|
+
.selectAll("text")
|
|
2067
|
+
.data(labeledInternalNodes)
|
|
2068
|
+
.join("text")
|
|
2069
|
+
.attr("x", (d) => xScaleUnroot(d.x) + 4)
|
|
2070
|
+
.attr("y", (d) => yScaleUnroot(d.y) - 4)
|
|
2071
|
+
.attr("font-size", nodeLabelSize)
|
|
2072
|
+
.attr("fill", "black")
|
|
2073
|
+
.text((d) => d.thisLabel);
|
|
2074
|
+
}
|
|
2075
|
+
|
|
1814
2076
|
const byId = new Map(unrootedPhylo.data.map((d) => [d.thisId, d]));
|
|
1815
2077
|
const tipById = new Map(
|
|
1816
2078
|
unrootedPhylo.data.filter((d) => d.isTip).map((d) => [d.thisId, d])
|
|
@@ -1900,11 +2162,11 @@ function drawPhylogeny(
|
|
|
1900
2162
|
.filter((d) => d.isTip)
|
|
1901
2163
|
.on("mouseenter", function(_event, d) {
|
|
1902
2164
|
drawUnrootedPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1903
|
-
d3__namespace.select(this).attr("r",
|
|
2165
|
+
d3__namespace.select(this).attr("r", R_TIP + 2);
|
|
1904
2166
|
})
|
|
1905
2167
|
.on("mouseleave", function() {
|
|
1906
2168
|
hoverLayer.selectAll("*").remove();
|
|
1907
|
-
d3__namespace.select(this).attr("r",
|
|
2169
|
+
d3__namespace.select(this).attr("r", R_TIP);
|
|
1908
2170
|
});
|
|
1909
2171
|
|
|
1910
2172
|
if (highlightTips && highlightTips.length) {
|
|
@@ -1947,6 +2209,17 @@ function drawPhylogeny(
|
|
|
1947
2209
|
}
|
|
1948
2210
|
}
|
|
1949
2211
|
|
|
2212
|
+
if (scaleBar) {
|
|
2213
|
+
addScaleBar(svg, {
|
|
2214
|
+
scale: xScaleUnroot,
|
|
2215
|
+
basis: maxRadius,
|
|
2216
|
+
defaultX: 20,
|
|
2217
|
+
defaultY: h - 20,
|
|
2218
|
+
scaleBar,
|
|
2219
|
+
fontSize: labelFontSize
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
|
|
1950
2223
|
return svg.node();
|
|
1951
2224
|
} else {
|
|
1952
2225
|
throw new Error(
|
|
@@ -1958,6 +2231,7 @@ function drawPhylogeny(
|
|
|
1958
2231
|
exports.describeArc = describeArc;
|
|
1959
2232
|
exports.describeArcSweep = describeArcSweep;
|
|
1960
2233
|
exports.drawPhylogeny = drawPhylogeny;
|
|
2234
|
+
exports.ladderize = ladderize;
|
|
1961
2235
|
exports.parentFisheye = parentFisheye;
|
|
1962
2236
|
exports.phisheye = phisheye;
|
|
1963
2237
|
exports.polarToCartesian = polarToCartesian;
|
|
@@ -1965,6 +2239,7 @@ exports.radialLayout = radialLayout;
|
|
|
1965
2239
|
exports.randomTree = randomTree;
|
|
1966
2240
|
exports.readTree = readTree;
|
|
1967
2241
|
exports.rectangleLayout = rectangleLayout;
|
|
2242
|
+
exports.rotate = rotate;
|
|
1968
2243
|
exports.subTree = subTree;
|
|
1969
2244
|
exports.toNewick = toNewick;
|
|
1970
2245
|
exports.unrooted = unrooted;
|