@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
|
@@ -885,8 +885,52 @@ function readTree(text) {
|
|
|
885
885
|
return root;
|
|
886
886
|
}
|
|
887
887
|
|
|
888
|
+
// Round a raw data-space length to a "nice" 1/2/5-of-a-power-of-ten value,
|
|
889
|
+
// so an auto-sized scale bar doesn't show an ugly number like "0.347".
|
|
890
|
+
function niceScaleLength(target) {
|
|
891
|
+
if (!(target > 0)) return 1;
|
|
892
|
+
const exp = Math.floor(Math.log10(target));
|
|
893
|
+
const base = Math.pow(10, exp);
|
|
894
|
+
const residual = target / base;
|
|
895
|
+
const niceResidual = residual < 1.5 ? 1 : residual < 3.5 ? 2 : residual < 7.5 ? 5 : 10;
|
|
896
|
+
return niceResidual * base;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// scaleBar: true | number (explicit data-units length) | { length, x, y, label }
|
|
900
|
+
function addScaleBar(svg, { scale, basis, defaultX, defaultY, scaleBar, fontSize }) {
|
|
901
|
+
const opts = (scaleBar === true || typeof scaleBar === "number") ? {} : scaleBar;
|
|
902
|
+
const length = typeof scaleBar === "number" ? scaleBar : (opts.length ?? niceScaleLength(basis / 5));
|
|
903
|
+
const x = opts.x ?? defaultX;
|
|
904
|
+
const y = opts.y ?? defaultY;
|
|
905
|
+
const barPx = scale(length) - scale(0);
|
|
906
|
+
|
|
907
|
+
const g = svg.append("g").attr("class", "phylo_scale_bar");
|
|
908
|
+
g.append("line")
|
|
909
|
+
.attr("x1", x).attr("x2", x + barPx)
|
|
910
|
+
.attr("y1", y).attr("y2", y)
|
|
911
|
+
.attr("stroke", "#000")
|
|
912
|
+
.attr("stroke-width", 1);
|
|
913
|
+
[x, x + barPx].forEach((tx) => {
|
|
914
|
+
g.append("line")
|
|
915
|
+
.attr("x1", tx).attr("x2", tx)
|
|
916
|
+
.attr("y1", y - 4).attr("y2", y + 4)
|
|
917
|
+
.attr("stroke", "#000")
|
|
918
|
+
.attr("stroke-width", 1);
|
|
919
|
+
});
|
|
920
|
+
g.append("text")
|
|
921
|
+
.attr("x", x + barPx / 2)
|
|
922
|
+
.attr("y", y - 6)
|
|
923
|
+
.attr("text-anchor", "middle")
|
|
924
|
+
.attr("font-size", fontSize)
|
|
925
|
+
.text(opts.label ?? String(length));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// input: a Newick string, or an already-parsed tree (the node shape returned
|
|
929
|
+
// by readTree()/randomTree()). Passing the same parsed tree object back in
|
|
930
|
+
// across re-renders (e.g. after mutating it with rotate()) keeps node ids
|
|
931
|
+
// stable, which onNodeClick below relies on.
|
|
888
932
|
function drawPhylogeny(
|
|
889
|
-
|
|
933
|
+
input,
|
|
890
934
|
{
|
|
891
935
|
layout = "rect", // rect/radial/unrooted
|
|
892
936
|
width = 800,
|
|
@@ -897,6 +941,14 @@ function drawPhylogeny(
|
|
|
897
941
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
898
942
|
tipLabels = true,
|
|
899
943
|
labelFontSize = 10, // font size (px) for tip labels
|
|
944
|
+
tipRadius, // px radius of tip dots; defaults to each layout's original size
|
|
945
|
+
internalNodeCircles = false, // draw a circle at every internal (non-tip) node
|
|
946
|
+
internalNodeRadius = 3, // px radius for internal node circles
|
|
947
|
+
nodeLabels = false, // draw text labels at internal nodes (e.g. clade/support labels)
|
|
948
|
+
nodeLabelFontSize, // defaults to labelFontSize
|
|
949
|
+
scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
|
|
950
|
+
alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
|
|
951
|
+
onNodeClick, // (node, event) => void — fires when an internal node circle is clicked (requires internalNodeCircles: true)
|
|
900
952
|
showTooltips = true,
|
|
901
953
|
tooltipFormatter = (d, rtt) =>
|
|
902
954
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -910,6 +962,10 @@ function drawPhylogeny(
|
|
|
910
962
|
|
|
911
963
|
// shared helpers
|
|
912
964
|
const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
|
|
965
|
+
const nodeLabelSize = nodeLabelFontSize ?? labelFontSize;
|
|
966
|
+
const parsedTree = (input && typeof input === "object" && Array.isArray(input.children))
|
|
967
|
+
? input
|
|
968
|
+
: readTree(input);
|
|
913
969
|
// Works for both radial (uses `r`) and rect (uses `x1`).
|
|
914
970
|
// Falls back to summing branchLength up to the root if neither is present.
|
|
915
971
|
function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
|
|
@@ -938,7 +994,7 @@ function drawPhylogeny(
|
|
|
938
994
|
|
|
939
995
|
if (layout === "rect") {
|
|
940
996
|
// RECTANGULAR LAYOUT
|
|
941
|
-
const tree_df = rectangleLayout(
|
|
997
|
+
const tree_df = rectangleLayout(parsedTree);
|
|
942
998
|
const horizontal = tree_df.horizontal_lines;
|
|
943
999
|
const vertical = tree_df.vertical_lines;
|
|
944
1000
|
const tips = horizontal.filter((d) => d.isTip);
|
|
@@ -948,6 +1004,7 @@ function drawPhylogeny(
|
|
|
948
1004
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
949
1005
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
950
1006
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
1007
|
+
const R_TIP = tipRadius ?? 2;
|
|
951
1008
|
|
|
952
1009
|
const maxY = d3.max(horizontal, (d) => d.y1);
|
|
953
1010
|
const minY = d3.min(horizontal, (d) => d.y1);
|
|
@@ -1005,7 +1062,7 @@ function drawPhylogeny(
|
|
|
1005
1062
|
.join("circle")
|
|
1006
1063
|
.attr("cx", (d) => xScale(d.x1))
|
|
1007
1064
|
.attr("cy", (d) => yScale(d.y1))
|
|
1008
|
-
.attr("r",
|
|
1065
|
+
.attr("r", R_TIP)
|
|
1009
1066
|
.attr("fill", "black");
|
|
1010
1067
|
|
|
1011
1068
|
// tooltips for rect dots
|
|
@@ -1020,13 +1077,78 @@ function drawPhylogeny(
|
|
|
1020
1077
|
.on("mouseenter", function(_event, d) {
|
|
1021
1078
|
hoverLayer.selectAll("*").remove();
|
|
1022
1079
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1023
|
-
d3.select(this).attr("r",
|
|
1080
|
+
d3.select(this).attr("r", R_TIP + 2);
|
|
1024
1081
|
})
|
|
1025
1082
|
.on("mouseleave", function() {
|
|
1026
1083
|
hoverLayer.selectAll("*").remove();
|
|
1027
|
-
d3.select(this).attr("r",
|
|
1084
|
+
d3.select(this).attr("r", R_TIP);
|
|
1028
1085
|
});
|
|
1029
1086
|
|
|
1087
|
+
// internal node circles (optional)
|
|
1088
|
+
if (internalNodeCircles) {
|
|
1089
|
+
const internalNodes = tree_df.data.filter((d) => !d.isTip);
|
|
1090
|
+
const internalDots = group
|
|
1091
|
+
.append("g")
|
|
1092
|
+
.attr("class", "phylo_internal_dots")
|
|
1093
|
+
.selectAll("circle")
|
|
1094
|
+
.data(internalNodes)
|
|
1095
|
+
.join("circle")
|
|
1096
|
+
.attr("cx", (d) => xScale(d.x1))
|
|
1097
|
+
.attr("cy", (d) => yScale(d.y1))
|
|
1098
|
+
.attr("r", internalNodeRadius)
|
|
1099
|
+
.attr("fill", "white")
|
|
1100
|
+
.attr("stroke", "#555")
|
|
1101
|
+
.attr("stroke-width", 1);
|
|
1102
|
+
|
|
1103
|
+
if (showTooltips) {
|
|
1104
|
+
internalDots
|
|
1105
|
+
.append("title")
|
|
1106
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (onNodeClick) {
|
|
1110
|
+
internalDots
|
|
1111
|
+
.style("cursor", "pointer")
|
|
1112
|
+
.on("click", (event, d) => onNodeClick(d, event));
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// internal node labels (optional)
|
|
1117
|
+
if (nodeLabels) {
|
|
1118
|
+
const labeledInternalNodes = tree_df.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1119
|
+
svg
|
|
1120
|
+
.append("g")
|
|
1121
|
+
.attr("class", "phylo_node_labels")
|
|
1122
|
+
.selectAll("text")
|
|
1123
|
+
.data(labeledInternalNodes)
|
|
1124
|
+
.join("text")
|
|
1125
|
+
.attr("x", (d) => xScale(d.x1) - 4)
|
|
1126
|
+
.attr("y", (d) => yScale(d.y1) - 4)
|
|
1127
|
+
.attr("text-anchor", "end")
|
|
1128
|
+
.attr("font-size", nodeLabelSize)
|
|
1129
|
+
.text((d) => d.thisLabel);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// column that tip labels align to when alignTipLabels is set
|
|
1133
|
+
const alignX = xScale(maxX);
|
|
1134
|
+
|
|
1135
|
+
// dashed guide lines from each tip's true branch end to the aligned label column
|
|
1136
|
+
if (tipLabels && alignTipLabels) {
|
|
1137
|
+
group
|
|
1138
|
+
.append("g")
|
|
1139
|
+
.attr("class", "phylo_align_guides")
|
|
1140
|
+
.selectAll("line")
|
|
1141
|
+
.data(tips)
|
|
1142
|
+
.join("line")
|
|
1143
|
+
.attr("x1", (d) => xScale(d.x1))
|
|
1144
|
+
.attr("x2", alignX)
|
|
1145
|
+
.attr("y1", (d) => yScale(d.y1))
|
|
1146
|
+
.attr("y2", (d) => yScale(d.y1))
|
|
1147
|
+
.attr("stroke", "#999")
|
|
1148
|
+
.attr("stroke-width", 1)
|
|
1149
|
+
.attr("stroke-dasharray", "2,2");
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1030
1152
|
// labels
|
|
1031
1153
|
if (tipLabels) {
|
|
1032
1154
|
const labels = svg
|
|
@@ -1035,7 +1157,7 @@ function drawPhylogeny(
|
|
|
1035
1157
|
.selectAll("text")
|
|
1036
1158
|
.data(tips)
|
|
1037
1159
|
.join("text")
|
|
1038
|
-
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1160
|
+
.attr("x", (d) => (alignTipLabels ? alignX : xScale(d.x1)) + 4)
|
|
1039
1161
|
.attr("y", (d) => yScale(d.y1))
|
|
1040
1162
|
.attr("dy", "0.32em")
|
|
1041
1163
|
.attr("font-size", labelFontSize)
|
|
@@ -1107,13 +1229,23 @@ function drawPhylogeny(
|
|
|
1107
1229
|
}
|
|
1108
1230
|
}
|
|
1109
1231
|
|
|
1232
|
+
if (scaleBar) {
|
|
1233
|
+
addScaleBar(svg, {
|
|
1234
|
+
scale: xScale,
|
|
1235
|
+
basis: maxX,
|
|
1236
|
+
defaultX: margin.left,
|
|
1237
|
+
defaultY: height - margin.bottom / 2,
|
|
1238
|
+
scaleBar,
|
|
1239
|
+
fontSize: labelFontSize
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1110
1243
|
return svg.node();
|
|
1111
1244
|
} else if (layout === "radial") {
|
|
1112
1245
|
// RADIAL LAYOUT
|
|
1113
1246
|
if (width !== height) {
|
|
1114
1247
|
throw new Error("width and height must be the same for radial layout");
|
|
1115
1248
|
}
|
|
1116
|
-
const parsedTree = readTree(treeText);
|
|
1117
1249
|
const rad = radialLayout(parsedTree, {
|
|
1118
1250
|
angleStrategy: "fan",
|
|
1119
1251
|
arcsStyle: "fan"
|
|
@@ -1128,7 +1260,7 @@ function drawPhylogeny(
|
|
|
1128
1260
|
|
|
1129
1261
|
|
|
1130
1262
|
// visuals (0 = let spokes reach the dots)
|
|
1131
|
-
const DOT_R = 3;
|
|
1263
|
+
const DOT_R = tipRadius ?? 3;
|
|
1132
1264
|
const END_CAP = 0;
|
|
1133
1265
|
|
|
1134
1266
|
// ===== SCALES / BOUNDS =====
|
|
@@ -1266,6 +1398,25 @@ function drawPhylogeny(
|
|
|
1266
1398
|
.attr("stroke-width", strokeWidth);
|
|
1267
1399
|
});
|
|
1268
1400
|
|
|
1401
|
+
// ===== ALIGN GUIDES (optional) =====
|
|
1402
|
+
// dashed lines from each tip's true position to the common label ring,
|
|
1403
|
+
// for radialMode "phylo" (true terminals) where tips aren't already co-circular
|
|
1404
|
+
if (tipLabels && alignTipLabels && !isOuter) {
|
|
1405
|
+
group
|
|
1406
|
+
.append("g")
|
|
1407
|
+
.attr("class", "phylo_align_guides")
|
|
1408
|
+
.selectAll("line")
|
|
1409
|
+
.data(tips)
|
|
1410
|
+
.join("line")
|
|
1411
|
+
.attr("x1", (d) => xScaleRadial(d.x))
|
|
1412
|
+
.attr("y1", (d) => yScaleRadial(d.y))
|
|
1413
|
+
.attr("x2", (d) => xScaleRadial(tipMaxR * Math.cos(d.angle)))
|
|
1414
|
+
.attr("y2", (d) => yScaleRadial(tipMaxR * Math.sin(d.angle)))
|
|
1415
|
+
.attr("stroke", "#999")
|
|
1416
|
+
.attr("stroke-width", 1)
|
|
1417
|
+
.attr("stroke-dasharray", "2,2");
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1269
1420
|
// ===== TIP DOTS =====
|
|
1270
1421
|
const tipDots = group
|
|
1271
1422
|
.append("g")
|
|
@@ -1293,6 +1444,51 @@ function drawPhylogeny(
|
|
|
1293
1444
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1294
1445
|
}
|
|
1295
1446
|
|
|
1447
|
+
// ===== INTERNAL NODE CIRCLES (optional) =====
|
|
1448
|
+
if (internalNodeCircles) {
|
|
1449
|
+
const internalNodes = rad.data.filter((d) => !d.isTip);
|
|
1450
|
+
const internalDots = group
|
|
1451
|
+
.append("g")
|
|
1452
|
+
.attr("class", "phylo_internal_dots")
|
|
1453
|
+
.selectAll("circle")
|
|
1454
|
+
.data(internalNodes)
|
|
1455
|
+
.join("circle")
|
|
1456
|
+
.attr("cx", (d) => xScaleRadial(d.x))
|
|
1457
|
+
.attr("cy", (d) => yScaleRadial(d.y))
|
|
1458
|
+
.attr("r", internalNodeRadius)
|
|
1459
|
+
.attr("fill", "white")
|
|
1460
|
+
.attr("stroke", "#555")
|
|
1461
|
+
.attr("stroke-width", 1);
|
|
1462
|
+
|
|
1463
|
+
if (showTooltips) {
|
|
1464
|
+
internalDots
|
|
1465
|
+
.append("title")
|
|
1466
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
if (onNodeClick) {
|
|
1470
|
+
internalDots
|
|
1471
|
+
.style("cursor", "pointer")
|
|
1472
|
+
.on("click", (event, d) => onNodeClick(d, event));
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// ===== INTERNAL NODE LABELS (optional) =====
|
|
1477
|
+
if (nodeLabels) {
|
|
1478
|
+
const labeledInternalNodes = rad.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1479
|
+
group
|
|
1480
|
+
.append("g")
|
|
1481
|
+
.attr("class", "phylo_node_labels")
|
|
1482
|
+
.selectAll("text")
|
|
1483
|
+
.data(labeledInternalNodes)
|
|
1484
|
+
.join("text")
|
|
1485
|
+
.attr("x", (d) => xScaleRadial(d.x) + 4)
|
|
1486
|
+
.attr("y", (d) => yScaleRadial(d.y) - 4)
|
|
1487
|
+
.attr("font-size", nodeLabelSize)
|
|
1488
|
+
.attr("fill", "black")
|
|
1489
|
+
.text((d) => d.thisLabel);
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1296
1492
|
// maps for fast lookup on hover (childId → spoke / arc)
|
|
1297
1493
|
const key = (x) => (typeof x === "string" ? +x : x);
|
|
1298
1494
|
const spokeByChild = new Map(rad.radii.map(s => [key(s.childId ?? s.thisId ?? s.id1), s]));
|
|
@@ -1313,7 +1509,8 @@ function drawPhylogeny(
|
|
|
1313
1509
|
// same tip position rule as dots/spokes:
|
|
1314
1510
|
// - "outer": snap to common ring (tipMaxR)
|
|
1315
1511
|
// - otherwise (e.g. "align"/"phylo"): true tip radius
|
|
1316
|
-
|
|
1512
|
+
// - alignTipLabels forces the common ring regardless of mode
|
|
1513
|
+
const r = (isOuter || alignTipLabels) ? tipMaxR : d.r;
|
|
1317
1514
|
const x = r * Math.cos(d.angle);
|
|
1318
1515
|
const y = r * Math.sin(d.angle);
|
|
1319
1516
|
return `translate(${xScaleRadial(x)},${yScaleRadial(y)})`;
|
|
@@ -1460,10 +1657,20 @@ function drawPhylogeny(
|
|
|
1460
1657
|
});
|
|
1461
1658
|
}
|
|
1462
1659
|
|
|
1660
|
+
if (scaleBar) {
|
|
1661
|
+
addScaleBar(svg, {
|
|
1662
|
+
scale: xScaleRadial,
|
|
1663
|
+
basis: maxRadius,
|
|
1664
|
+
defaultX: 20,
|
|
1665
|
+
defaultY: h - 20,
|
|
1666
|
+
scaleBar,
|
|
1667
|
+
fontSize: labelFontSize
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1463
1671
|
return svg.node();
|
|
1464
1672
|
} else if (layout === "unrooted") {
|
|
1465
1673
|
// UNROOTED LAYOUT
|
|
1466
|
-
const parsedTree = readTree(treeText);
|
|
1467
1674
|
const unrootedPhylo = unrooted(parsedTree);
|
|
1468
1675
|
|
|
1469
1676
|
const w = width;
|
|
@@ -1509,6 +1716,8 @@ function drawPhylogeny(
|
|
|
1509
1716
|
.attr("stroke-width", strokeWidth)
|
|
1510
1717
|
.attr("stroke", "#777");
|
|
1511
1718
|
|
|
1719
|
+
const R_TIP = tipRadius ?? 4;
|
|
1720
|
+
|
|
1512
1721
|
const nodes = group
|
|
1513
1722
|
.append("g")
|
|
1514
1723
|
.attr("class", "phylo_points")
|
|
@@ -1516,13 +1725,35 @@ function drawPhylogeny(
|
|
|
1516
1725
|
.data(unrootedPhylo.data)
|
|
1517
1726
|
.join("circle")
|
|
1518
1727
|
.attr("class", "dot")
|
|
1519
|
-
.attr("r", (d) => (d.isTip ?
|
|
1728
|
+
.attr("r", (d) => (d.isTip ? R_TIP : (internalNodeCircles ? internalNodeRadius : 0)))
|
|
1520
1729
|
.attr("cx", (d) => xScaleUnroot(d.x))
|
|
1521
1730
|
.attr("cy", (d) => yScaleUnroot(d.y))
|
|
1522
1731
|
.attr("stroke", "black")
|
|
1523
1732
|
.attr("stroke-width", 2)
|
|
1524
1733
|
.attr("fill", (d) => (d.isTip ? "black" : "white"));
|
|
1525
1734
|
|
|
1735
|
+
if (internalNodeCircles && onNodeClick) {
|
|
1736
|
+
nodes
|
|
1737
|
+
.filter((d) => !d.isTip)
|
|
1738
|
+
.style("cursor", "pointer")
|
|
1739
|
+
.on("click", (event, d) => onNodeClick(d, event));
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (nodeLabels) {
|
|
1743
|
+
const labeledInternalNodes = unrootedPhylo.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1744
|
+
group
|
|
1745
|
+
.append("g")
|
|
1746
|
+
.attr("class", "phylo_node_labels")
|
|
1747
|
+
.selectAll("text")
|
|
1748
|
+
.data(labeledInternalNodes)
|
|
1749
|
+
.join("text")
|
|
1750
|
+
.attr("x", (d) => xScaleUnroot(d.x) + 4)
|
|
1751
|
+
.attr("y", (d) => yScaleUnroot(d.y) - 4)
|
|
1752
|
+
.attr("font-size", nodeLabelSize)
|
|
1753
|
+
.attr("fill", "black")
|
|
1754
|
+
.text((d) => d.thisLabel);
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1526
1757
|
const byId = new Map(unrootedPhylo.data.map((d) => [d.thisId, d]));
|
|
1527
1758
|
const tipById = new Map(
|
|
1528
1759
|
unrootedPhylo.data.filter((d) => d.isTip).map((d) => [d.thisId, d])
|
|
@@ -1612,11 +1843,11 @@ function drawPhylogeny(
|
|
|
1612
1843
|
.filter((d) => d.isTip)
|
|
1613
1844
|
.on("mouseenter", function(_event, d) {
|
|
1614
1845
|
drawUnrootedPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1615
|
-
d3.select(this).attr("r",
|
|
1846
|
+
d3.select(this).attr("r", R_TIP + 2);
|
|
1616
1847
|
})
|
|
1617
1848
|
.on("mouseleave", function() {
|
|
1618
1849
|
hoverLayer.selectAll("*").remove();
|
|
1619
|
-
d3.select(this).attr("r",
|
|
1850
|
+
d3.select(this).attr("r", R_TIP);
|
|
1620
1851
|
});
|
|
1621
1852
|
|
|
1622
1853
|
if (highlightTips && highlightTips.length) {
|
|
@@ -1659,6 +1890,17 @@ function drawPhylogeny(
|
|
|
1659
1890
|
}
|
|
1660
1891
|
}
|
|
1661
1892
|
|
|
1893
|
+
if (scaleBar) {
|
|
1894
|
+
addScaleBar(svg, {
|
|
1895
|
+
scale: xScaleUnroot,
|
|
1896
|
+
basis: maxRadius,
|
|
1897
|
+
defaultX: 20,
|
|
1898
|
+
defaultY: h - 20,
|
|
1899
|
+
scaleBar,
|
|
1900
|
+
fontSize: labelFontSize
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1662
1904
|
return svg.node();
|
|
1663
1905
|
} else {
|
|
1664
1906
|
throw new Error(
|