@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.js
CHANGED
|
@@ -929,6 +929,23 @@ function unrooted (node) {
|
|
|
929
929
|
return data;
|
|
930
930
|
}
|
|
931
931
|
|
|
932
|
+
/**
|
|
933
|
+
* Reorder every node's children by descendant tip count, so the tree
|
|
934
|
+
* "ladders" from smallest to largest clade at each branching point
|
|
935
|
+
* (or the reverse, with ascending: false). Mutates the tree in place.
|
|
936
|
+
*/
|
|
937
|
+
function ladderize(tree, { ascending = true } = {}) {
|
|
938
|
+
const sign = ascending ? 1 : -1;
|
|
939
|
+
|
|
940
|
+
(function sort(node) {
|
|
941
|
+
if (node.children.length === 0) return;
|
|
942
|
+
node.children.forEach(sort);
|
|
943
|
+
node.children.sort((a, b) => sign * (numTips(a) - numTips(b)));
|
|
944
|
+
})(tree);
|
|
945
|
+
|
|
946
|
+
return tree;
|
|
947
|
+
}
|
|
948
|
+
|
|
932
949
|
function makeIndexById(rows, key = "thisId") {
|
|
933
950
|
return new Map(rows.map(d => [d[key], d]));
|
|
934
951
|
}
|
|
@@ -938,6 +955,20 @@ function parentFisheye(d, data) {
|
|
|
938
955
|
return parent ? { px: parent.fisheye.x, py: parent.fisheye.y } : null;
|
|
939
956
|
}
|
|
940
957
|
|
|
958
|
+
/**
|
|
959
|
+
* Reverse the child order at one node, flipping which side of the tree
|
|
960
|
+
* that clade's descendants are drawn on. Defaults to the root. Mutates
|
|
961
|
+
* the tree in place.
|
|
962
|
+
*/
|
|
963
|
+
function rotate(tree, nodeId = tree.id) {
|
|
964
|
+
const target = preorder(tree).find((n) => n.id === nodeId);
|
|
965
|
+
if (!target) {
|
|
966
|
+
throw new Error(`No node with id ${nodeId} found in tree`);
|
|
967
|
+
}
|
|
968
|
+
target.children.reverse();
|
|
969
|
+
return tree;
|
|
970
|
+
}
|
|
971
|
+
|
|
941
972
|
/**
|
|
942
973
|
* Generate a random bifurcating tree with `nTips` tips, in the same
|
|
943
974
|
* parent/children node shape produced by readTree().
|
|
@@ -1152,8 +1183,52 @@ function serialize(node) {
|
|
|
1152
1183
|
return `(${children})${label}${branch}`;
|
|
1153
1184
|
}
|
|
1154
1185
|
|
|
1186
|
+
// Round a raw data-space length to a "nice" 1/2/5-of-a-power-of-ten value,
|
|
1187
|
+
// so an auto-sized scale bar doesn't show an ugly number like "0.347".
|
|
1188
|
+
function niceScaleLength(target) {
|
|
1189
|
+
if (!(target > 0)) return 1;
|
|
1190
|
+
const exp = Math.floor(Math.log10(target));
|
|
1191
|
+
const base = Math.pow(10, exp);
|
|
1192
|
+
const residual = target / base;
|
|
1193
|
+
const niceResidual = residual < 1.5 ? 1 : residual < 3.5 ? 2 : residual < 7.5 ? 5 : 10;
|
|
1194
|
+
return niceResidual * base;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// scaleBar: true | number (explicit data-units length) | { length, x, y, label }
|
|
1198
|
+
function addScaleBar(svg, { scale, basis, defaultX, defaultY, scaleBar, fontSize }) {
|
|
1199
|
+
const opts = (scaleBar === true || typeof scaleBar === "number") ? {} : scaleBar;
|
|
1200
|
+
const length = typeof scaleBar === "number" ? scaleBar : (opts.length ?? niceScaleLength(basis / 5));
|
|
1201
|
+
const x = opts.x ?? defaultX;
|
|
1202
|
+
const y = opts.y ?? defaultY;
|
|
1203
|
+
const barPx = scale(length) - scale(0);
|
|
1204
|
+
|
|
1205
|
+
const g = svg.append("g").attr("class", "phylo_scale_bar");
|
|
1206
|
+
g.append("line")
|
|
1207
|
+
.attr("x1", x).attr("x2", x + barPx)
|
|
1208
|
+
.attr("y1", y).attr("y2", y)
|
|
1209
|
+
.attr("stroke", "#000")
|
|
1210
|
+
.attr("stroke-width", 1);
|
|
1211
|
+
[x, x + barPx].forEach((tx) => {
|
|
1212
|
+
g.append("line")
|
|
1213
|
+
.attr("x1", tx).attr("x2", tx)
|
|
1214
|
+
.attr("y1", y - 4).attr("y2", y + 4)
|
|
1215
|
+
.attr("stroke", "#000")
|
|
1216
|
+
.attr("stroke-width", 1);
|
|
1217
|
+
});
|
|
1218
|
+
g.append("text")
|
|
1219
|
+
.attr("x", x + barPx / 2)
|
|
1220
|
+
.attr("y", y - 6)
|
|
1221
|
+
.attr("text-anchor", "middle")
|
|
1222
|
+
.attr("font-size", fontSize)
|
|
1223
|
+
.text(opts.label ?? String(length));
|
|
1224
|
+
}
|
|
1225
|
+
|
|
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.
|
|
1155
1230
|
function drawPhylogeny(
|
|
1156
|
-
|
|
1231
|
+
input,
|
|
1157
1232
|
{
|
|
1158
1233
|
layout = "rect", // rect/radial/unrooted
|
|
1159
1234
|
width = 800,
|
|
@@ -1164,6 +1239,14 @@ function drawPhylogeny(
|
|
|
1164
1239
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
1165
1240
|
tipLabels = true,
|
|
1166
1241
|
labelFontSize = 10, // font size (px) for tip labels
|
|
1242
|
+
tipRadius, // px radius of tip dots; defaults to each layout's original size
|
|
1243
|
+
internalNodeCircles = false, // draw a circle at every internal (non-tip) node
|
|
1244
|
+
internalNodeRadius = 3, // px radius for internal node circles
|
|
1245
|
+
nodeLabels = false, // draw text labels at internal nodes (e.g. clade/support labels)
|
|
1246
|
+
nodeLabelFontSize, // defaults to labelFontSize
|
|
1247
|
+
scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
|
|
1248
|
+
alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
|
|
1249
|
+
onNodeClick, // (node, event) => void — fires when an internal node circle is clicked (requires internalNodeCircles: true)
|
|
1167
1250
|
showTooltips = true,
|
|
1168
1251
|
tooltipFormatter = (d, rtt) =>
|
|
1169
1252
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1177,6 +1260,10 @@ function drawPhylogeny(
|
|
|
1177
1260
|
|
|
1178
1261
|
// shared helpers
|
|
1179
1262
|
const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
|
|
1263
|
+
const nodeLabelSize = nodeLabelFontSize ?? labelFontSize;
|
|
1264
|
+
const parsedTree = (input && typeof input === "object" && Array.isArray(input.children))
|
|
1265
|
+
? input
|
|
1266
|
+
: readTree(input);
|
|
1180
1267
|
// Works for both radial (uses `r`) and rect (uses `x1`).
|
|
1181
1268
|
// Falls back to summing branchLength up to the root if neither is present.
|
|
1182
1269
|
function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
|
|
@@ -1205,7 +1292,7 @@ function drawPhylogeny(
|
|
|
1205
1292
|
|
|
1206
1293
|
if (layout === "rect") {
|
|
1207
1294
|
// RECTANGULAR LAYOUT
|
|
1208
|
-
const tree_df = rectangleLayout(
|
|
1295
|
+
const tree_df = rectangleLayout(parsedTree);
|
|
1209
1296
|
const horizontal = tree_df.horizontal_lines;
|
|
1210
1297
|
const vertical = tree_df.vertical_lines;
|
|
1211
1298
|
const tips = horizontal.filter((d) => d.isTip);
|
|
@@ -1215,6 +1302,7 @@ function drawPhylogeny(
|
|
|
1215
1302
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1216
1303
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1217
1304
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
1305
|
+
const R_TIP = tipRadius ?? 2;
|
|
1218
1306
|
|
|
1219
1307
|
const maxY = d3.max(horizontal, (d) => d.y1);
|
|
1220
1308
|
const minY = d3.min(horizontal, (d) => d.y1);
|
|
@@ -1272,7 +1360,7 @@ function drawPhylogeny(
|
|
|
1272
1360
|
.join("circle")
|
|
1273
1361
|
.attr("cx", (d) => xScale(d.x1))
|
|
1274
1362
|
.attr("cy", (d) => yScale(d.y1))
|
|
1275
|
-
.attr("r",
|
|
1363
|
+
.attr("r", R_TIP)
|
|
1276
1364
|
.attr("fill", "black");
|
|
1277
1365
|
|
|
1278
1366
|
// tooltips for rect dots
|
|
@@ -1287,13 +1375,78 @@ function drawPhylogeny(
|
|
|
1287
1375
|
.on("mouseenter", function(_event, d) {
|
|
1288
1376
|
hoverLayer.selectAll("*").remove();
|
|
1289
1377
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1290
|
-
d3.select(this).attr("r",
|
|
1378
|
+
d3.select(this).attr("r", R_TIP + 2);
|
|
1291
1379
|
})
|
|
1292
1380
|
.on("mouseleave", function() {
|
|
1293
1381
|
hoverLayer.selectAll("*").remove();
|
|
1294
|
-
d3.select(this).attr("r",
|
|
1382
|
+
d3.select(this).attr("r", R_TIP);
|
|
1295
1383
|
});
|
|
1296
1384
|
|
|
1385
|
+
// internal node circles (optional)
|
|
1386
|
+
if (internalNodeCircles) {
|
|
1387
|
+
const internalNodes = tree_df.data.filter((d) => !d.isTip);
|
|
1388
|
+
const internalDots = group
|
|
1389
|
+
.append("g")
|
|
1390
|
+
.attr("class", "phylo_internal_dots")
|
|
1391
|
+
.selectAll("circle")
|
|
1392
|
+
.data(internalNodes)
|
|
1393
|
+
.join("circle")
|
|
1394
|
+
.attr("cx", (d) => xScale(d.x1))
|
|
1395
|
+
.attr("cy", (d) => yScale(d.y1))
|
|
1396
|
+
.attr("r", internalNodeRadius)
|
|
1397
|
+
.attr("fill", "white")
|
|
1398
|
+
.attr("stroke", "#555")
|
|
1399
|
+
.attr("stroke-width", 1);
|
|
1400
|
+
|
|
1401
|
+
if (showTooltips) {
|
|
1402
|
+
internalDots
|
|
1403
|
+
.append("title")
|
|
1404
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if (onNodeClick) {
|
|
1408
|
+
internalDots
|
|
1409
|
+
.style("cursor", "pointer")
|
|
1410
|
+
.on("click", (event, d) => onNodeClick(d, event));
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// internal node labels (optional)
|
|
1415
|
+
if (nodeLabels) {
|
|
1416
|
+
const labeledInternalNodes = tree_df.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1417
|
+
svg
|
|
1418
|
+
.append("g")
|
|
1419
|
+
.attr("class", "phylo_node_labels")
|
|
1420
|
+
.selectAll("text")
|
|
1421
|
+
.data(labeledInternalNodes)
|
|
1422
|
+
.join("text")
|
|
1423
|
+
.attr("x", (d) => xScale(d.x1) - 4)
|
|
1424
|
+
.attr("y", (d) => yScale(d.y1) - 4)
|
|
1425
|
+
.attr("text-anchor", "end")
|
|
1426
|
+
.attr("font-size", nodeLabelSize)
|
|
1427
|
+
.text((d) => d.thisLabel);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// column that tip labels align to when alignTipLabels is set
|
|
1431
|
+
const alignX = xScale(maxX);
|
|
1432
|
+
|
|
1433
|
+
// dashed guide lines from each tip's true branch end to the aligned label column
|
|
1434
|
+
if (tipLabels && alignTipLabels) {
|
|
1435
|
+
group
|
|
1436
|
+
.append("g")
|
|
1437
|
+
.attr("class", "phylo_align_guides")
|
|
1438
|
+
.selectAll("line")
|
|
1439
|
+
.data(tips)
|
|
1440
|
+
.join("line")
|
|
1441
|
+
.attr("x1", (d) => xScale(d.x1))
|
|
1442
|
+
.attr("x2", alignX)
|
|
1443
|
+
.attr("y1", (d) => yScale(d.y1))
|
|
1444
|
+
.attr("y2", (d) => yScale(d.y1))
|
|
1445
|
+
.attr("stroke", "#999")
|
|
1446
|
+
.attr("stroke-width", 1)
|
|
1447
|
+
.attr("stroke-dasharray", "2,2");
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1297
1450
|
// labels
|
|
1298
1451
|
if (tipLabels) {
|
|
1299
1452
|
const labels = svg
|
|
@@ -1302,7 +1455,7 @@ function drawPhylogeny(
|
|
|
1302
1455
|
.selectAll("text")
|
|
1303
1456
|
.data(tips)
|
|
1304
1457
|
.join("text")
|
|
1305
|
-
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1458
|
+
.attr("x", (d) => (alignTipLabels ? alignX : xScale(d.x1)) + 4)
|
|
1306
1459
|
.attr("y", (d) => yScale(d.y1))
|
|
1307
1460
|
.attr("dy", "0.32em")
|
|
1308
1461
|
.attr("font-size", labelFontSize)
|
|
@@ -1374,13 +1527,23 @@ function drawPhylogeny(
|
|
|
1374
1527
|
}
|
|
1375
1528
|
}
|
|
1376
1529
|
|
|
1530
|
+
if (scaleBar) {
|
|
1531
|
+
addScaleBar(svg, {
|
|
1532
|
+
scale: xScale,
|
|
1533
|
+
basis: maxX,
|
|
1534
|
+
defaultX: margin.left,
|
|
1535
|
+
defaultY: height - margin.bottom / 2,
|
|
1536
|
+
scaleBar,
|
|
1537
|
+
fontSize: labelFontSize
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1377
1541
|
return svg.node();
|
|
1378
1542
|
} else if (layout === "radial") {
|
|
1379
1543
|
// RADIAL LAYOUT
|
|
1380
1544
|
if (width !== height) {
|
|
1381
1545
|
throw new Error("width and height must be the same for radial layout");
|
|
1382
1546
|
}
|
|
1383
|
-
const parsedTree = readTree(treeText);
|
|
1384
1547
|
const rad = radialLayout(parsedTree, {
|
|
1385
1548
|
angleStrategy: "fan",
|
|
1386
1549
|
arcsStyle: "fan"
|
|
@@ -1395,7 +1558,7 @@ function drawPhylogeny(
|
|
|
1395
1558
|
|
|
1396
1559
|
|
|
1397
1560
|
// visuals (0 = let spokes reach the dots)
|
|
1398
|
-
const DOT_R = 3;
|
|
1561
|
+
const DOT_R = tipRadius ?? 3;
|
|
1399
1562
|
const END_CAP = 0;
|
|
1400
1563
|
|
|
1401
1564
|
// ===== SCALES / BOUNDS =====
|
|
@@ -1533,6 +1696,25 @@ function drawPhylogeny(
|
|
|
1533
1696
|
.attr("stroke-width", strokeWidth);
|
|
1534
1697
|
});
|
|
1535
1698
|
|
|
1699
|
+
// ===== ALIGN GUIDES (optional) =====
|
|
1700
|
+
// dashed lines from each tip's true position to the common label ring,
|
|
1701
|
+
// for radialMode "phylo" (true terminals) where tips aren't already co-circular
|
|
1702
|
+
if (tipLabels && alignTipLabels && !isOuter) {
|
|
1703
|
+
group
|
|
1704
|
+
.append("g")
|
|
1705
|
+
.attr("class", "phylo_align_guides")
|
|
1706
|
+
.selectAll("line")
|
|
1707
|
+
.data(tips)
|
|
1708
|
+
.join("line")
|
|
1709
|
+
.attr("x1", (d) => xScaleRadial(d.x))
|
|
1710
|
+
.attr("y1", (d) => yScaleRadial(d.y))
|
|
1711
|
+
.attr("x2", (d) => xScaleRadial(tipMaxR * Math.cos(d.angle)))
|
|
1712
|
+
.attr("y2", (d) => yScaleRadial(tipMaxR * Math.sin(d.angle)))
|
|
1713
|
+
.attr("stroke", "#999")
|
|
1714
|
+
.attr("stroke-width", 1)
|
|
1715
|
+
.attr("stroke-dasharray", "2,2");
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1536
1718
|
// ===== TIP DOTS =====
|
|
1537
1719
|
const tipDots = group
|
|
1538
1720
|
.append("g")
|
|
@@ -1560,6 +1742,51 @@ function drawPhylogeny(
|
|
|
1560
1742
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1561
1743
|
}
|
|
1562
1744
|
|
|
1745
|
+
// ===== INTERNAL NODE CIRCLES (optional) =====
|
|
1746
|
+
if (internalNodeCircles) {
|
|
1747
|
+
const internalNodes = rad.data.filter((d) => !d.isTip);
|
|
1748
|
+
const internalDots = group
|
|
1749
|
+
.append("g")
|
|
1750
|
+
.attr("class", "phylo_internal_dots")
|
|
1751
|
+
.selectAll("circle")
|
|
1752
|
+
.data(internalNodes)
|
|
1753
|
+
.join("circle")
|
|
1754
|
+
.attr("cx", (d) => xScaleRadial(d.x))
|
|
1755
|
+
.attr("cy", (d) => yScaleRadial(d.y))
|
|
1756
|
+
.attr("r", internalNodeRadius)
|
|
1757
|
+
.attr("fill", "white")
|
|
1758
|
+
.attr("stroke", "#555")
|
|
1759
|
+
.attr("stroke-width", 1);
|
|
1760
|
+
|
|
1761
|
+
if (showTooltips) {
|
|
1762
|
+
internalDots
|
|
1763
|
+
.append("title")
|
|
1764
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (onNodeClick) {
|
|
1768
|
+
internalDots
|
|
1769
|
+
.style("cursor", "pointer")
|
|
1770
|
+
.on("click", (event, d) => onNodeClick(d, event));
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// ===== INTERNAL NODE LABELS (optional) =====
|
|
1775
|
+
if (nodeLabels) {
|
|
1776
|
+
const labeledInternalNodes = rad.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1777
|
+
group
|
|
1778
|
+
.append("g")
|
|
1779
|
+
.attr("class", "phylo_node_labels")
|
|
1780
|
+
.selectAll("text")
|
|
1781
|
+
.data(labeledInternalNodes)
|
|
1782
|
+
.join("text")
|
|
1783
|
+
.attr("x", (d) => xScaleRadial(d.x) + 4)
|
|
1784
|
+
.attr("y", (d) => yScaleRadial(d.y) - 4)
|
|
1785
|
+
.attr("font-size", nodeLabelSize)
|
|
1786
|
+
.attr("fill", "black")
|
|
1787
|
+
.text((d) => d.thisLabel);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1563
1790
|
// maps for fast lookup on hover (childId → spoke / arc)
|
|
1564
1791
|
const key = (x) => (typeof x === "string" ? +x : x);
|
|
1565
1792
|
const spokeByChild = new Map(rad.radii.map(s => [key(s.childId ?? s.thisId ?? s.id1), s]));
|
|
@@ -1580,7 +1807,8 @@ function drawPhylogeny(
|
|
|
1580
1807
|
// same tip position rule as dots/spokes:
|
|
1581
1808
|
// - "outer": snap to common ring (tipMaxR)
|
|
1582
1809
|
// - otherwise (e.g. "align"/"phylo"): true tip radius
|
|
1583
|
-
|
|
1810
|
+
// - alignTipLabels forces the common ring regardless of mode
|
|
1811
|
+
const r = (isOuter || alignTipLabels) ? tipMaxR : d.r;
|
|
1584
1812
|
const x = r * Math.cos(d.angle);
|
|
1585
1813
|
const y = r * Math.sin(d.angle);
|
|
1586
1814
|
return `translate(${xScaleRadial(x)},${yScaleRadial(y)})`;
|
|
@@ -1727,10 +1955,20 @@ function drawPhylogeny(
|
|
|
1727
1955
|
});
|
|
1728
1956
|
}
|
|
1729
1957
|
|
|
1958
|
+
if (scaleBar) {
|
|
1959
|
+
addScaleBar(svg, {
|
|
1960
|
+
scale: xScaleRadial,
|
|
1961
|
+
basis: maxRadius,
|
|
1962
|
+
defaultX: 20,
|
|
1963
|
+
defaultY: h - 20,
|
|
1964
|
+
scaleBar,
|
|
1965
|
+
fontSize: labelFontSize
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1730
1969
|
return svg.node();
|
|
1731
1970
|
} else if (layout === "unrooted") {
|
|
1732
1971
|
// UNROOTED LAYOUT
|
|
1733
|
-
const parsedTree = readTree(treeText);
|
|
1734
1972
|
const unrootedPhylo = unrooted(parsedTree);
|
|
1735
1973
|
|
|
1736
1974
|
const w = width;
|
|
@@ -1776,6 +2014,8 @@ function drawPhylogeny(
|
|
|
1776
2014
|
.attr("stroke-width", strokeWidth)
|
|
1777
2015
|
.attr("stroke", "#777");
|
|
1778
2016
|
|
|
2017
|
+
const R_TIP = tipRadius ?? 4;
|
|
2018
|
+
|
|
1779
2019
|
const nodes = group
|
|
1780
2020
|
.append("g")
|
|
1781
2021
|
.attr("class", "phylo_points")
|
|
@@ -1783,13 +2023,35 @@ function drawPhylogeny(
|
|
|
1783
2023
|
.data(unrootedPhylo.data)
|
|
1784
2024
|
.join("circle")
|
|
1785
2025
|
.attr("class", "dot")
|
|
1786
|
-
.attr("r", (d) => (d.isTip ?
|
|
2026
|
+
.attr("r", (d) => (d.isTip ? R_TIP : (internalNodeCircles ? internalNodeRadius : 0)))
|
|
1787
2027
|
.attr("cx", (d) => xScaleUnroot(d.x))
|
|
1788
2028
|
.attr("cy", (d) => yScaleUnroot(d.y))
|
|
1789
2029
|
.attr("stroke", "black")
|
|
1790
2030
|
.attr("stroke-width", 2)
|
|
1791
2031
|
.attr("fill", (d) => (d.isTip ? "black" : "white"));
|
|
1792
2032
|
|
|
2033
|
+
if (internalNodeCircles && onNodeClick) {
|
|
2034
|
+
nodes
|
|
2035
|
+
.filter((d) => !d.isTip)
|
|
2036
|
+
.style("cursor", "pointer")
|
|
2037
|
+
.on("click", (event, d) => onNodeClick(d, event));
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
if (nodeLabels) {
|
|
2041
|
+
const labeledInternalNodes = unrootedPhylo.data.filter((d) => !d.isTip && d.thisLabel);
|
|
2042
|
+
group
|
|
2043
|
+
.append("g")
|
|
2044
|
+
.attr("class", "phylo_node_labels")
|
|
2045
|
+
.selectAll("text")
|
|
2046
|
+
.data(labeledInternalNodes)
|
|
2047
|
+
.join("text")
|
|
2048
|
+
.attr("x", (d) => xScaleUnroot(d.x) + 4)
|
|
2049
|
+
.attr("y", (d) => yScaleUnroot(d.y) - 4)
|
|
2050
|
+
.attr("font-size", nodeLabelSize)
|
|
2051
|
+
.attr("fill", "black")
|
|
2052
|
+
.text((d) => d.thisLabel);
|
|
2053
|
+
}
|
|
2054
|
+
|
|
1793
2055
|
const byId = new Map(unrootedPhylo.data.map((d) => [d.thisId, d]));
|
|
1794
2056
|
const tipById = new Map(
|
|
1795
2057
|
unrootedPhylo.data.filter((d) => d.isTip).map((d) => [d.thisId, d])
|
|
@@ -1879,11 +2141,11 @@ function drawPhylogeny(
|
|
|
1879
2141
|
.filter((d) => d.isTip)
|
|
1880
2142
|
.on("mouseenter", function(_event, d) {
|
|
1881
2143
|
drawUnrootedPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1882
|
-
d3.select(this).attr("r",
|
|
2144
|
+
d3.select(this).attr("r", R_TIP + 2);
|
|
1883
2145
|
})
|
|
1884
2146
|
.on("mouseleave", function() {
|
|
1885
2147
|
hoverLayer.selectAll("*").remove();
|
|
1886
|
-
d3.select(this).attr("r",
|
|
2148
|
+
d3.select(this).attr("r", R_TIP);
|
|
1887
2149
|
});
|
|
1888
2150
|
|
|
1889
2151
|
if (highlightTips && highlightTips.length) {
|
|
@@ -1926,6 +2188,17 @@ function drawPhylogeny(
|
|
|
1926
2188
|
}
|
|
1927
2189
|
}
|
|
1928
2190
|
|
|
2191
|
+
if (scaleBar) {
|
|
2192
|
+
addScaleBar(svg, {
|
|
2193
|
+
scale: xScaleUnroot,
|
|
2194
|
+
basis: maxRadius,
|
|
2195
|
+
defaultX: 20,
|
|
2196
|
+
defaultY: h - 20,
|
|
2197
|
+
scaleBar,
|
|
2198
|
+
fontSize: labelFontSize
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
|
|
1929
2202
|
return svg.node();
|
|
1930
2203
|
} else {
|
|
1931
2204
|
throw new Error(
|
|
@@ -1934,5 +2207,5 @@ function drawPhylogeny(
|
|
|
1934
2207
|
}
|
|
1935
2208
|
}
|
|
1936
2209
|
|
|
1937
|
-
export { describeArc, describeArcSweep, drawPhylogeny, parentFisheye, phisheye, polarToCartesian, radialLayout, randomTree, readTree, rectangleLayout, subTree, toNewick, unrooted };
|
|
2210
|
+
export { describeArc, describeArcSweep, drawPhylogeny, ladderize, parentFisheye, phisheye, polarToCartesian, radialLayout, randomTree, readTree, rectangleLayout, rotate, subTree, toNewick, unrooted };
|
|
1938
2211
|
//# sourceMappingURL=index.js.map
|