@euphrasiologist/lwphylo 1.3.1 → 1.4.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 +10 -1
- package/dist/drawPhylogeny.cjs +226 -9
- package/dist/drawPhylogeny.cjs.map +1 -1
- package/dist/drawPhylogeny.esm.js +226 -9
- 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 +259 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +258 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,16 @@ Visit https://euphrasiologist.github.io/lwPhylo/ to see examples and live render
|
|
|
10
10
|
|
|
11
11
|
Newick trees can be parsed using the `readTree()` function. This object can then be wrapped in three main functions; `rectangleLayout()` to produce a "regular" phylogenetic tree, `radialLayout()` to produce a circular phylogeny, and `unrooted()` to produce an unrooted tree via the equal angle layout algorithm.
|
|
12
12
|
|
|
13
|
-
Need a tree to experiment with? `randomTree(nTips, { maxBranchLength, labelPrefix, seed })` generates a random bifurcating tree in the same node shape as `readTree()`, ready to pass straight into any of the layout functions.
|
|
13
|
+
Need a tree to experiment with? `randomTree(nTips, { maxBranchLength, labelPrefix, seed })` generates a random bifurcating tree in the same node shape as `readTree()`, ready to pass straight into any of the layout functions. `toNewick(tree)` serializes one of these parsed tree objects back to a Newick string, so it can be handed to `drawPhylogeny()` (which expects Newick text): `drawPhylogeny(toNewick(randomTree(20)))`.
|
|
14
|
+
|
|
15
|
+
`ladderize(tree, { ascending })` and `rotate(tree, nodeId)` change tip order by reordering a node's children in place — ladderize sorts every clade by descendant tip count (smallest first by default), rotate flips the child order at one node (the root, if no id is given).
|
|
16
|
+
|
|
17
|
+
`drawPhylogeny(newick, options)` accepts, in addition to `layout`/`width`/`height`/`tipLabels`/`labelFontSize`/`highlightTips`:
|
|
18
|
+
- `tipRadius` — px radius of tip circles.
|
|
19
|
+
- `internalNodeCircles` (bool) + `internalNodeRadius` — draw a circle at every internal node.
|
|
20
|
+
- `nodeLabels` (bool) + `nodeLabelFontSize` — draw text labels (e.g. clade/support values) at internal nodes that have one.
|
|
21
|
+
- `scaleBar` — `true` for an auto-sized branch-length scale bar, a number for an explicit length in branch-length units, or `{ length, x, y, label }` for full control.
|
|
22
|
+
- `alignTipLabels` (bool, rect & radial layouts) — align tip labels to a common column/ring, with dashed guide lines back to each tip's true position.
|
|
14
23
|
|
|
15
24
|
### Acknowledgements
|
|
16
25
|
|
package/dist/drawPhylogeny.cjs
CHANGED
|
@@ -906,6 +906,46 @@ function readTree(text) {
|
|
|
906
906
|
return root;
|
|
907
907
|
}
|
|
908
908
|
|
|
909
|
+
// Round a raw data-space length to a "nice" 1/2/5-of-a-power-of-ten value,
|
|
910
|
+
// so an auto-sized scale bar doesn't show an ugly number like "0.347".
|
|
911
|
+
function niceScaleLength(target) {
|
|
912
|
+
if (!(target > 0)) return 1;
|
|
913
|
+
const exp = Math.floor(Math.log10(target));
|
|
914
|
+
const base = Math.pow(10, exp);
|
|
915
|
+
const residual = target / base;
|
|
916
|
+
const niceResidual = residual < 1.5 ? 1 : residual < 3.5 ? 2 : residual < 7.5 ? 5 : 10;
|
|
917
|
+
return niceResidual * base;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// scaleBar: true | number (explicit data-units length) | { length, x, y, label }
|
|
921
|
+
function addScaleBar(svg, { scale, basis, defaultX, defaultY, scaleBar, fontSize }) {
|
|
922
|
+
const opts = (scaleBar === true || typeof scaleBar === "number") ? {} : scaleBar;
|
|
923
|
+
const length = typeof scaleBar === "number" ? scaleBar : (opts.length ?? niceScaleLength(basis / 5));
|
|
924
|
+
const x = opts.x ?? defaultX;
|
|
925
|
+
const y = opts.y ?? defaultY;
|
|
926
|
+
const barPx = scale(length) - scale(0);
|
|
927
|
+
|
|
928
|
+
const g = svg.append("g").attr("class", "phylo_scale_bar");
|
|
929
|
+
g.append("line")
|
|
930
|
+
.attr("x1", x).attr("x2", x + barPx)
|
|
931
|
+
.attr("y1", y).attr("y2", y)
|
|
932
|
+
.attr("stroke", "#000")
|
|
933
|
+
.attr("stroke-width", 1);
|
|
934
|
+
[x, x + barPx].forEach((tx) => {
|
|
935
|
+
g.append("line")
|
|
936
|
+
.attr("x1", tx).attr("x2", tx)
|
|
937
|
+
.attr("y1", y - 4).attr("y2", y + 4)
|
|
938
|
+
.attr("stroke", "#000")
|
|
939
|
+
.attr("stroke-width", 1);
|
|
940
|
+
});
|
|
941
|
+
g.append("text")
|
|
942
|
+
.attr("x", x + barPx / 2)
|
|
943
|
+
.attr("y", y - 6)
|
|
944
|
+
.attr("text-anchor", "middle")
|
|
945
|
+
.attr("font-size", fontSize)
|
|
946
|
+
.text(opts.label ?? String(length));
|
|
947
|
+
}
|
|
948
|
+
|
|
909
949
|
function drawPhylogeny(
|
|
910
950
|
treeText,
|
|
911
951
|
{
|
|
@@ -918,6 +958,13 @@ function drawPhylogeny(
|
|
|
918
958
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
919
959
|
tipLabels = true,
|
|
920
960
|
labelFontSize = 10, // font size (px) for tip labels
|
|
961
|
+
tipRadius, // px radius of tip dots; defaults to each layout's original size
|
|
962
|
+
internalNodeCircles = false, // draw a circle at every internal (non-tip) node
|
|
963
|
+
internalNodeRadius = 3, // px radius for internal node circles
|
|
964
|
+
nodeLabels = false, // draw text labels at internal nodes (e.g. clade/support labels)
|
|
965
|
+
nodeLabelFontSize, // defaults to labelFontSize
|
|
966
|
+
scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
|
|
967
|
+
alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
|
|
921
968
|
showTooltips = true,
|
|
922
969
|
tooltipFormatter = (d, rtt) =>
|
|
923
970
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -931,6 +978,7 @@ function drawPhylogeny(
|
|
|
931
978
|
|
|
932
979
|
// shared helpers
|
|
933
980
|
const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
|
|
981
|
+
const nodeLabelSize = nodeLabelFontSize ?? labelFontSize;
|
|
934
982
|
// Works for both radial (uses `r`) and rect (uses `x1`).
|
|
935
983
|
// Falls back to summing branchLength up to the root if neither is present.
|
|
936
984
|
function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
|
|
@@ -969,6 +1017,7 @@ function drawPhylogeny(
|
|
|
969
1017
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
970
1018
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
971
1019
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
1020
|
+
const R_TIP = tipRadius ?? 2;
|
|
972
1021
|
|
|
973
1022
|
const maxY = d3__namespace.max(horizontal, (d) => d.y1);
|
|
974
1023
|
const minY = d3__namespace.min(horizontal, (d) => d.y1);
|
|
@@ -1026,7 +1075,7 @@ function drawPhylogeny(
|
|
|
1026
1075
|
.join("circle")
|
|
1027
1076
|
.attr("cx", (d) => xScale(d.x1))
|
|
1028
1077
|
.attr("cy", (d) => yScale(d.y1))
|
|
1029
|
-
.attr("r",
|
|
1078
|
+
.attr("r", R_TIP)
|
|
1030
1079
|
.attr("fill", "black");
|
|
1031
1080
|
|
|
1032
1081
|
// tooltips for rect dots
|
|
@@ -1041,13 +1090,72 @@ function drawPhylogeny(
|
|
|
1041
1090
|
.on("mouseenter", function(_event, d) {
|
|
1042
1091
|
hoverLayer.selectAll("*").remove();
|
|
1043
1092
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1044
|
-
d3__namespace.select(this).attr("r",
|
|
1093
|
+
d3__namespace.select(this).attr("r", R_TIP + 2);
|
|
1045
1094
|
})
|
|
1046
1095
|
.on("mouseleave", function() {
|
|
1047
1096
|
hoverLayer.selectAll("*").remove();
|
|
1048
|
-
d3__namespace.select(this).attr("r",
|
|
1097
|
+
d3__namespace.select(this).attr("r", R_TIP);
|
|
1049
1098
|
});
|
|
1050
1099
|
|
|
1100
|
+
// internal node circles (optional)
|
|
1101
|
+
if (internalNodeCircles) {
|
|
1102
|
+
const internalNodes = tree_df.data.filter((d) => !d.isTip);
|
|
1103
|
+
const internalDots = group
|
|
1104
|
+
.append("g")
|
|
1105
|
+
.attr("class", "phylo_internal_dots")
|
|
1106
|
+
.selectAll("circle")
|
|
1107
|
+
.data(internalNodes)
|
|
1108
|
+
.join("circle")
|
|
1109
|
+
.attr("cx", (d) => xScale(d.x1))
|
|
1110
|
+
.attr("cy", (d) => yScale(d.y1))
|
|
1111
|
+
.attr("r", internalNodeRadius)
|
|
1112
|
+
.attr("fill", "white")
|
|
1113
|
+
.attr("stroke", "#555")
|
|
1114
|
+
.attr("stroke-width", 1);
|
|
1115
|
+
|
|
1116
|
+
if (showTooltips) {
|
|
1117
|
+
internalDots
|
|
1118
|
+
.append("title")
|
|
1119
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// internal node labels (optional)
|
|
1124
|
+
if (nodeLabels) {
|
|
1125
|
+
const labeledInternalNodes = tree_df.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1126
|
+
svg
|
|
1127
|
+
.append("g")
|
|
1128
|
+
.attr("class", "phylo_node_labels")
|
|
1129
|
+
.selectAll("text")
|
|
1130
|
+
.data(labeledInternalNodes)
|
|
1131
|
+
.join("text")
|
|
1132
|
+
.attr("x", (d) => xScale(d.x1) - 4)
|
|
1133
|
+
.attr("y", (d) => yScale(d.y1) - 4)
|
|
1134
|
+
.attr("text-anchor", "end")
|
|
1135
|
+
.attr("font-size", nodeLabelSize)
|
|
1136
|
+
.text((d) => d.thisLabel);
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// column that tip labels align to when alignTipLabels is set
|
|
1140
|
+
const alignX = xScale(maxX);
|
|
1141
|
+
|
|
1142
|
+
// dashed guide lines from each tip's true branch end to the aligned label column
|
|
1143
|
+
if (tipLabels && alignTipLabels) {
|
|
1144
|
+
group
|
|
1145
|
+
.append("g")
|
|
1146
|
+
.attr("class", "phylo_align_guides")
|
|
1147
|
+
.selectAll("line")
|
|
1148
|
+
.data(tips)
|
|
1149
|
+
.join("line")
|
|
1150
|
+
.attr("x1", (d) => xScale(d.x1))
|
|
1151
|
+
.attr("x2", alignX)
|
|
1152
|
+
.attr("y1", (d) => yScale(d.y1))
|
|
1153
|
+
.attr("y2", (d) => yScale(d.y1))
|
|
1154
|
+
.attr("stroke", "#999")
|
|
1155
|
+
.attr("stroke-width", 1)
|
|
1156
|
+
.attr("stroke-dasharray", "2,2");
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1051
1159
|
// labels
|
|
1052
1160
|
if (tipLabels) {
|
|
1053
1161
|
const labels = svg
|
|
@@ -1056,7 +1164,7 @@ function drawPhylogeny(
|
|
|
1056
1164
|
.selectAll("text")
|
|
1057
1165
|
.data(tips)
|
|
1058
1166
|
.join("text")
|
|
1059
|
-
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1167
|
+
.attr("x", (d) => (alignTipLabels ? alignX : xScale(d.x1)) + 4)
|
|
1060
1168
|
.attr("y", (d) => yScale(d.y1))
|
|
1061
1169
|
.attr("dy", "0.32em")
|
|
1062
1170
|
.attr("font-size", labelFontSize)
|
|
@@ -1128,6 +1236,17 @@ function drawPhylogeny(
|
|
|
1128
1236
|
}
|
|
1129
1237
|
}
|
|
1130
1238
|
|
|
1239
|
+
if (scaleBar) {
|
|
1240
|
+
addScaleBar(svg, {
|
|
1241
|
+
scale: xScale,
|
|
1242
|
+
basis: maxX,
|
|
1243
|
+
defaultX: margin.left,
|
|
1244
|
+
defaultY: height - margin.bottom / 2,
|
|
1245
|
+
scaleBar,
|
|
1246
|
+
fontSize: labelFontSize
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1131
1250
|
return svg.node();
|
|
1132
1251
|
} else if (layout === "radial") {
|
|
1133
1252
|
// RADIAL LAYOUT
|
|
@@ -1149,7 +1268,7 @@ function drawPhylogeny(
|
|
|
1149
1268
|
|
|
1150
1269
|
|
|
1151
1270
|
// visuals (0 = let spokes reach the dots)
|
|
1152
|
-
const DOT_R = 3;
|
|
1271
|
+
const DOT_R = tipRadius ?? 3;
|
|
1153
1272
|
const END_CAP = 0;
|
|
1154
1273
|
|
|
1155
1274
|
// ===== SCALES / BOUNDS =====
|
|
@@ -1287,6 +1406,25 @@ function drawPhylogeny(
|
|
|
1287
1406
|
.attr("stroke-width", strokeWidth);
|
|
1288
1407
|
});
|
|
1289
1408
|
|
|
1409
|
+
// ===== ALIGN GUIDES (optional) =====
|
|
1410
|
+
// dashed lines from each tip's true position to the common label ring,
|
|
1411
|
+
// for radialMode "phylo" (true terminals) where tips aren't already co-circular
|
|
1412
|
+
if (tipLabels && alignTipLabels && !isOuter) {
|
|
1413
|
+
group
|
|
1414
|
+
.append("g")
|
|
1415
|
+
.attr("class", "phylo_align_guides")
|
|
1416
|
+
.selectAll("line")
|
|
1417
|
+
.data(tips)
|
|
1418
|
+
.join("line")
|
|
1419
|
+
.attr("x1", (d) => xScaleRadial(d.x))
|
|
1420
|
+
.attr("y1", (d) => yScaleRadial(d.y))
|
|
1421
|
+
.attr("x2", (d) => xScaleRadial(tipMaxR * Math.cos(d.angle)))
|
|
1422
|
+
.attr("y2", (d) => yScaleRadial(tipMaxR * Math.sin(d.angle)))
|
|
1423
|
+
.attr("stroke", "#999")
|
|
1424
|
+
.attr("stroke-width", 1)
|
|
1425
|
+
.attr("stroke-dasharray", "2,2");
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1290
1428
|
// ===== TIP DOTS =====
|
|
1291
1429
|
const tipDots = group
|
|
1292
1430
|
.append("g")
|
|
@@ -1314,6 +1452,45 @@ function drawPhylogeny(
|
|
|
1314
1452
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1315
1453
|
}
|
|
1316
1454
|
|
|
1455
|
+
// ===== INTERNAL NODE CIRCLES (optional) =====
|
|
1456
|
+
if (internalNodeCircles) {
|
|
1457
|
+
const internalNodes = rad.data.filter((d) => !d.isTip);
|
|
1458
|
+
const internalDots = group
|
|
1459
|
+
.append("g")
|
|
1460
|
+
.attr("class", "phylo_internal_dots")
|
|
1461
|
+
.selectAll("circle")
|
|
1462
|
+
.data(internalNodes)
|
|
1463
|
+
.join("circle")
|
|
1464
|
+
.attr("cx", (d) => xScaleRadial(d.x))
|
|
1465
|
+
.attr("cy", (d) => yScaleRadial(d.y))
|
|
1466
|
+
.attr("r", internalNodeRadius)
|
|
1467
|
+
.attr("fill", "white")
|
|
1468
|
+
.attr("stroke", "#555")
|
|
1469
|
+
.attr("stroke-width", 1);
|
|
1470
|
+
|
|
1471
|
+
if (showTooltips) {
|
|
1472
|
+
internalDots
|
|
1473
|
+
.append("title")
|
|
1474
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// ===== INTERNAL NODE LABELS (optional) =====
|
|
1479
|
+
if (nodeLabels) {
|
|
1480
|
+
const labeledInternalNodes = rad.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1481
|
+
group
|
|
1482
|
+
.append("g")
|
|
1483
|
+
.attr("class", "phylo_node_labels")
|
|
1484
|
+
.selectAll("text")
|
|
1485
|
+
.data(labeledInternalNodes)
|
|
1486
|
+
.join("text")
|
|
1487
|
+
.attr("x", (d) => xScaleRadial(d.x) + 4)
|
|
1488
|
+
.attr("y", (d) => yScaleRadial(d.y) - 4)
|
|
1489
|
+
.attr("font-size", nodeLabelSize)
|
|
1490
|
+
.attr("fill", "black")
|
|
1491
|
+
.text((d) => d.thisLabel);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1317
1494
|
// maps for fast lookup on hover (childId → spoke / arc)
|
|
1318
1495
|
const key = (x) => (typeof x === "string" ? +x : x);
|
|
1319
1496
|
const spokeByChild = new Map(rad.radii.map(s => [key(s.childId ?? s.thisId ?? s.id1), s]));
|
|
@@ -1334,7 +1511,8 @@ function drawPhylogeny(
|
|
|
1334
1511
|
// same tip position rule as dots/spokes:
|
|
1335
1512
|
// - "outer": snap to common ring (tipMaxR)
|
|
1336
1513
|
// - otherwise (e.g. "align"/"phylo"): true tip radius
|
|
1337
|
-
|
|
1514
|
+
// - alignTipLabels forces the common ring regardless of mode
|
|
1515
|
+
const r = (isOuter || alignTipLabels) ? tipMaxR : d.r;
|
|
1338
1516
|
const x = r * Math.cos(d.angle);
|
|
1339
1517
|
const y = r * Math.sin(d.angle);
|
|
1340
1518
|
return `translate(${xScaleRadial(x)},${yScaleRadial(y)})`;
|
|
@@ -1481,6 +1659,17 @@ function drawPhylogeny(
|
|
|
1481
1659
|
});
|
|
1482
1660
|
}
|
|
1483
1661
|
|
|
1662
|
+
if (scaleBar) {
|
|
1663
|
+
addScaleBar(svg, {
|
|
1664
|
+
scale: xScaleRadial,
|
|
1665
|
+
basis: maxRadius,
|
|
1666
|
+
defaultX: 20,
|
|
1667
|
+
defaultY: h - 20,
|
|
1668
|
+
scaleBar,
|
|
1669
|
+
fontSize: labelFontSize
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1484
1673
|
return svg.node();
|
|
1485
1674
|
} else if (layout === "unrooted") {
|
|
1486
1675
|
// UNROOTED LAYOUT
|
|
@@ -1530,6 +1719,8 @@ function drawPhylogeny(
|
|
|
1530
1719
|
.attr("stroke-width", strokeWidth)
|
|
1531
1720
|
.attr("stroke", "#777");
|
|
1532
1721
|
|
|
1722
|
+
const R_TIP = tipRadius ?? 4;
|
|
1723
|
+
|
|
1533
1724
|
const nodes = group
|
|
1534
1725
|
.append("g")
|
|
1535
1726
|
.attr("class", "phylo_points")
|
|
@@ -1537,13 +1728,28 @@ function drawPhylogeny(
|
|
|
1537
1728
|
.data(unrootedPhylo.data)
|
|
1538
1729
|
.join("circle")
|
|
1539
1730
|
.attr("class", "dot")
|
|
1540
|
-
.attr("r", (d) => (d.isTip ?
|
|
1731
|
+
.attr("r", (d) => (d.isTip ? R_TIP : (internalNodeCircles ? internalNodeRadius : 0)))
|
|
1541
1732
|
.attr("cx", (d) => xScaleUnroot(d.x))
|
|
1542
1733
|
.attr("cy", (d) => yScaleUnroot(d.y))
|
|
1543
1734
|
.attr("stroke", "black")
|
|
1544
1735
|
.attr("stroke-width", 2)
|
|
1545
1736
|
.attr("fill", (d) => (d.isTip ? "black" : "white"));
|
|
1546
1737
|
|
|
1738
|
+
if (nodeLabels) {
|
|
1739
|
+
const labeledInternalNodes = unrootedPhylo.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1740
|
+
group
|
|
1741
|
+
.append("g")
|
|
1742
|
+
.attr("class", "phylo_node_labels")
|
|
1743
|
+
.selectAll("text")
|
|
1744
|
+
.data(labeledInternalNodes)
|
|
1745
|
+
.join("text")
|
|
1746
|
+
.attr("x", (d) => xScaleUnroot(d.x) + 4)
|
|
1747
|
+
.attr("y", (d) => yScaleUnroot(d.y) - 4)
|
|
1748
|
+
.attr("font-size", nodeLabelSize)
|
|
1749
|
+
.attr("fill", "black")
|
|
1750
|
+
.text((d) => d.thisLabel);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1547
1753
|
const byId = new Map(unrootedPhylo.data.map((d) => [d.thisId, d]));
|
|
1548
1754
|
const tipById = new Map(
|
|
1549
1755
|
unrootedPhylo.data.filter((d) => d.isTip).map((d) => [d.thisId, d])
|
|
@@ -1633,11 +1839,11 @@ function drawPhylogeny(
|
|
|
1633
1839
|
.filter((d) => d.isTip)
|
|
1634
1840
|
.on("mouseenter", function(_event, d) {
|
|
1635
1841
|
drawUnrootedPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1636
|
-
d3__namespace.select(this).attr("r",
|
|
1842
|
+
d3__namespace.select(this).attr("r", R_TIP + 2);
|
|
1637
1843
|
})
|
|
1638
1844
|
.on("mouseleave", function() {
|
|
1639
1845
|
hoverLayer.selectAll("*").remove();
|
|
1640
|
-
d3__namespace.select(this).attr("r",
|
|
1846
|
+
d3__namespace.select(this).attr("r", R_TIP);
|
|
1641
1847
|
});
|
|
1642
1848
|
|
|
1643
1849
|
if (highlightTips && highlightTips.length) {
|
|
@@ -1680,6 +1886,17 @@ function drawPhylogeny(
|
|
|
1680
1886
|
}
|
|
1681
1887
|
}
|
|
1682
1888
|
|
|
1889
|
+
if (scaleBar) {
|
|
1890
|
+
addScaleBar(svg, {
|
|
1891
|
+
scale: xScaleUnroot,
|
|
1892
|
+
basis: maxRadius,
|
|
1893
|
+
defaultX: 20,
|
|
1894
|
+
defaultY: h - 20,
|
|
1895
|
+
scaleBar,
|
|
1896
|
+
fontSize: labelFontSize
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1683
1900
|
return svg.node();
|
|
1684
1901
|
} else {
|
|
1685
1902
|
throw new Error(
|