@euphrasiologist/lwphylo 1.3.0 → 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 +281 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +279 -10
- 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().
|
|
@@ -1131,6 +1162,67 @@ function subTree (tree, node) {
|
|
|
1131
1162
|
return Object.fromEntries(data.concat(filtered));
|
|
1132
1163
|
}
|
|
1133
1164
|
|
|
1165
|
+
/**
|
|
1166
|
+
* Serialize a parsed tree (the parent/children node shape produced by
|
|
1167
|
+
* readTree() and randomTree()) back into a Newick string.
|
|
1168
|
+
*/
|
|
1169
|
+
|
|
1170
|
+
function toNewick(node) {
|
|
1171
|
+
return `${serialize(node)};`;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function serialize(node) {
|
|
1175
|
+
const label = node.label || '';
|
|
1176
|
+
const branch = node.branchLength == null ? '' : `:${node.branchLength}`;
|
|
1177
|
+
|
|
1178
|
+
if (node.children.length === 0) {
|
|
1179
|
+
return `${label}${branch}`;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const children = node.children.map(serialize).join(',');
|
|
1183
|
+
return `(${children})${label}${branch}`;
|
|
1184
|
+
}
|
|
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
|
+
|
|
1134
1226
|
function drawPhylogeny(
|
|
1135
1227
|
treeText,
|
|
1136
1228
|
{
|
|
@@ -1143,6 +1235,13 @@ function drawPhylogeny(
|
|
|
1143
1235
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
1144
1236
|
tipLabels = true,
|
|
1145
1237
|
labelFontSize = 10, // font size (px) for tip labels
|
|
1238
|
+
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
|
+
internalNodeRadius = 3, // px radius for internal node circles
|
|
1241
|
+
nodeLabels = false, // draw text labels at internal nodes (e.g. clade/support labels)
|
|
1242
|
+
nodeLabelFontSize, // defaults to labelFontSize
|
|
1243
|
+
scaleBar = false, // false | true | number (branch-length units) | { length, x, y, label }
|
|
1244
|
+
alignTipLabels = false, // rect & radial only: align tip labels to a common column/ring, with dashed guide lines back to the true tip position
|
|
1146
1245
|
showTooltips = true,
|
|
1147
1246
|
tooltipFormatter = (d, rtt) =>
|
|
1148
1247
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1156,6 +1255,7 @@ function drawPhylogeny(
|
|
|
1156
1255
|
|
|
1157
1256
|
// shared helpers
|
|
1158
1257
|
const isNumber = (x) => typeof x === "number" && Number.isFinite(x);
|
|
1258
|
+
const nodeLabelSize = nodeLabelFontSize ?? labelFontSize;
|
|
1159
1259
|
// Works for both radial (uses `r`) and rect (uses `x1`).
|
|
1160
1260
|
// Falls back to summing branchLength up to the root if neither is present.
|
|
1161
1261
|
function makeRootToTipGetter(byId, { prefer = "auto" } = {}) {
|
|
@@ -1194,6 +1294,7 @@ function drawPhylogeny(
|
|
|
1194
1294
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1195
1295
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1196
1296
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
1297
|
+
const R_TIP = tipRadius ?? 2;
|
|
1197
1298
|
|
|
1198
1299
|
const maxY = d3.max(horizontal, (d) => d.y1);
|
|
1199
1300
|
const minY = d3.min(horizontal, (d) => d.y1);
|
|
@@ -1251,7 +1352,7 @@ function drawPhylogeny(
|
|
|
1251
1352
|
.join("circle")
|
|
1252
1353
|
.attr("cx", (d) => xScale(d.x1))
|
|
1253
1354
|
.attr("cy", (d) => yScale(d.y1))
|
|
1254
|
-
.attr("r",
|
|
1355
|
+
.attr("r", R_TIP)
|
|
1255
1356
|
.attr("fill", "black");
|
|
1256
1357
|
|
|
1257
1358
|
// tooltips for rect dots
|
|
@@ -1266,13 +1367,72 @@ function drawPhylogeny(
|
|
|
1266
1367
|
.on("mouseenter", function(_event, d) {
|
|
1267
1368
|
hoverLayer.selectAll("*").remove();
|
|
1268
1369
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1269
|
-
d3.select(this).attr("r",
|
|
1370
|
+
d3.select(this).attr("r", R_TIP + 2);
|
|
1270
1371
|
})
|
|
1271
1372
|
.on("mouseleave", function() {
|
|
1272
1373
|
hoverLayer.selectAll("*").remove();
|
|
1273
|
-
d3.select(this).attr("r",
|
|
1374
|
+
d3.select(this).attr("r", R_TIP);
|
|
1274
1375
|
});
|
|
1275
1376
|
|
|
1377
|
+
// internal node circles (optional)
|
|
1378
|
+
if (internalNodeCircles) {
|
|
1379
|
+
const internalNodes = tree_df.data.filter((d) => !d.isTip);
|
|
1380
|
+
const internalDots = group
|
|
1381
|
+
.append("g")
|
|
1382
|
+
.attr("class", "phylo_internal_dots")
|
|
1383
|
+
.selectAll("circle")
|
|
1384
|
+
.data(internalNodes)
|
|
1385
|
+
.join("circle")
|
|
1386
|
+
.attr("cx", (d) => xScale(d.x1))
|
|
1387
|
+
.attr("cy", (d) => yScale(d.y1))
|
|
1388
|
+
.attr("r", internalNodeRadius)
|
|
1389
|
+
.attr("fill", "white")
|
|
1390
|
+
.attr("stroke", "#555")
|
|
1391
|
+
.attr("stroke-width", 1);
|
|
1392
|
+
|
|
1393
|
+
if (showTooltips) {
|
|
1394
|
+
internalDots
|
|
1395
|
+
.append("title")
|
|
1396
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// internal node labels (optional)
|
|
1401
|
+
if (nodeLabels) {
|
|
1402
|
+
const labeledInternalNodes = tree_df.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1403
|
+
svg
|
|
1404
|
+
.append("g")
|
|
1405
|
+
.attr("class", "phylo_node_labels")
|
|
1406
|
+
.selectAll("text")
|
|
1407
|
+
.data(labeledInternalNodes)
|
|
1408
|
+
.join("text")
|
|
1409
|
+
.attr("x", (d) => xScale(d.x1) - 4)
|
|
1410
|
+
.attr("y", (d) => yScale(d.y1) - 4)
|
|
1411
|
+
.attr("text-anchor", "end")
|
|
1412
|
+
.attr("font-size", nodeLabelSize)
|
|
1413
|
+
.text((d) => d.thisLabel);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// column that tip labels align to when alignTipLabels is set
|
|
1417
|
+
const alignX = xScale(maxX);
|
|
1418
|
+
|
|
1419
|
+
// dashed guide lines from each tip's true branch end to the aligned label column
|
|
1420
|
+
if (tipLabels && alignTipLabels) {
|
|
1421
|
+
group
|
|
1422
|
+
.append("g")
|
|
1423
|
+
.attr("class", "phylo_align_guides")
|
|
1424
|
+
.selectAll("line")
|
|
1425
|
+
.data(tips)
|
|
1426
|
+
.join("line")
|
|
1427
|
+
.attr("x1", (d) => xScale(d.x1))
|
|
1428
|
+
.attr("x2", alignX)
|
|
1429
|
+
.attr("y1", (d) => yScale(d.y1))
|
|
1430
|
+
.attr("y2", (d) => yScale(d.y1))
|
|
1431
|
+
.attr("stroke", "#999")
|
|
1432
|
+
.attr("stroke-width", 1)
|
|
1433
|
+
.attr("stroke-dasharray", "2,2");
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1276
1436
|
// labels
|
|
1277
1437
|
if (tipLabels) {
|
|
1278
1438
|
const labels = svg
|
|
@@ -1281,7 +1441,7 @@ function drawPhylogeny(
|
|
|
1281
1441
|
.selectAll("text")
|
|
1282
1442
|
.data(tips)
|
|
1283
1443
|
.join("text")
|
|
1284
|
-
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1444
|
+
.attr("x", (d) => (alignTipLabels ? alignX : xScale(d.x1)) + 4)
|
|
1285
1445
|
.attr("y", (d) => yScale(d.y1))
|
|
1286
1446
|
.attr("dy", "0.32em")
|
|
1287
1447
|
.attr("font-size", labelFontSize)
|
|
@@ -1353,6 +1513,17 @@ function drawPhylogeny(
|
|
|
1353
1513
|
}
|
|
1354
1514
|
}
|
|
1355
1515
|
|
|
1516
|
+
if (scaleBar) {
|
|
1517
|
+
addScaleBar(svg, {
|
|
1518
|
+
scale: xScale,
|
|
1519
|
+
basis: maxX,
|
|
1520
|
+
defaultX: margin.left,
|
|
1521
|
+
defaultY: height - margin.bottom / 2,
|
|
1522
|
+
scaleBar,
|
|
1523
|
+
fontSize: labelFontSize
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1356
1527
|
return svg.node();
|
|
1357
1528
|
} else if (layout === "radial") {
|
|
1358
1529
|
// RADIAL LAYOUT
|
|
@@ -1374,7 +1545,7 @@ function drawPhylogeny(
|
|
|
1374
1545
|
|
|
1375
1546
|
|
|
1376
1547
|
// visuals (0 = let spokes reach the dots)
|
|
1377
|
-
const DOT_R = 3;
|
|
1548
|
+
const DOT_R = tipRadius ?? 3;
|
|
1378
1549
|
const END_CAP = 0;
|
|
1379
1550
|
|
|
1380
1551
|
// ===== SCALES / BOUNDS =====
|
|
@@ -1512,6 +1683,25 @@ function drawPhylogeny(
|
|
|
1512
1683
|
.attr("stroke-width", strokeWidth);
|
|
1513
1684
|
});
|
|
1514
1685
|
|
|
1686
|
+
// ===== ALIGN GUIDES (optional) =====
|
|
1687
|
+
// dashed lines from each tip's true position to the common label ring,
|
|
1688
|
+
// for radialMode "phylo" (true terminals) where tips aren't already co-circular
|
|
1689
|
+
if (tipLabels && alignTipLabels && !isOuter) {
|
|
1690
|
+
group
|
|
1691
|
+
.append("g")
|
|
1692
|
+
.attr("class", "phylo_align_guides")
|
|
1693
|
+
.selectAll("line")
|
|
1694
|
+
.data(tips)
|
|
1695
|
+
.join("line")
|
|
1696
|
+
.attr("x1", (d) => xScaleRadial(d.x))
|
|
1697
|
+
.attr("y1", (d) => yScaleRadial(d.y))
|
|
1698
|
+
.attr("x2", (d) => xScaleRadial(tipMaxR * Math.cos(d.angle)))
|
|
1699
|
+
.attr("y2", (d) => yScaleRadial(tipMaxR * Math.sin(d.angle)))
|
|
1700
|
+
.attr("stroke", "#999")
|
|
1701
|
+
.attr("stroke-width", 1)
|
|
1702
|
+
.attr("stroke-dasharray", "2,2");
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1515
1705
|
// ===== TIP DOTS =====
|
|
1516
1706
|
const tipDots = group
|
|
1517
1707
|
.append("g")
|
|
@@ -1539,6 +1729,45 @@ function drawPhylogeny(
|
|
|
1539
1729
|
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1540
1730
|
}
|
|
1541
1731
|
|
|
1732
|
+
// ===== INTERNAL NODE CIRCLES (optional) =====
|
|
1733
|
+
if (internalNodeCircles) {
|
|
1734
|
+
const internalNodes = rad.data.filter((d) => !d.isTip);
|
|
1735
|
+
const internalDots = group
|
|
1736
|
+
.append("g")
|
|
1737
|
+
.attr("class", "phylo_internal_dots")
|
|
1738
|
+
.selectAll("circle")
|
|
1739
|
+
.data(internalNodes)
|
|
1740
|
+
.join("circle")
|
|
1741
|
+
.attr("cx", (d) => xScaleRadial(d.x))
|
|
1742
|
+
.attr("cy", (d) => yScaleRadial(d.y))
|
|
1743
|
+
.attr("r", internalNodeRadius)
|
|
1744
|
+
.attr("fill", "white")
|
|
1745
|
+
.attr("stroke", "#555")
|
|
1746
|
+
.attr("stroke-width", 1);
|
|
1747
|
+
|
|
1748
|
+
if (showTooltips) {
|
|
1749
|
+
internalDots
|
|
1750
|
+
.append("title")
|
|
1751
|
+
.text((d) => tooltipFormatter(d, rootToTip(d.thisId)));
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// ===== INTERNAL NODE LABELS (optional) =====
|
|
1756
|
+
if (nodeLabels) {
|
|
1757
|
+
const labeledInternalNodes = rad.data.filter((d) => !d.isTip && d.thisLabel);
|
|
1758
|
+
group
|
|
1759
|
+
.append("g")
|
|
1760
|
+
.attr("class", "phylo_node_labels")
|
|
1761
|
+
.selectAll("text")
|
|
1762
|
+
.data(labeledInternalNodes)
|
|
1763
|
+
.join("text")
|
|
1764
|
+
.attr("x", (d) => xScaleRadial(d.x) + 4)
|
|
1765
|
+
.attr("y", (d) => yScaleRadial(d.y) - 4)
|
|
1766
|
+
.attr("font-size", nodeLabelSize)
|
|
1767
|
+
.attr("fill", "black")
|
|
1768
|
+
.text((d) => d.thisLabel);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1542
1771
|
// maps for fast lookup on hover (childId → spoke / arc)
|
|
1543
1772
|
const key = (x) => (typeof x === "string" ? +x : x);
|
|
1544
1773
|
const spokeByChild = new Map(rad.radii.map(s => [key(s.childId ?? s.thisId ?? s.id1), s]));
|
|
@@ -1559,7 +1788,8 @@ function drawPhylogeny(
|
|
|
1559
1788
|
// same tip position rule as dots/spokes:
|
|
1560
1789
|
// - "outer": snap to common ring (tipMaxR)
|
|
1561
1790
|
// - otherwise (e.g. "align"/"phylo"): true tip radius
|
|
1562
|
-
|
|
1791
|
+
// - alignTipLabels forces the common ring regardless of mode
|
|
1792
|
+
const r = (isOuter || alignTipLabels) ? tipMaxR : d.r;
|
|
1563
1793
|
const x = r * Math.cos(d.angle);
|
|
1564
1794
|
const y = r * Math.sin(d.angle);
|
|
1565
1795
|
return `translate(${xScaleRadial(x)},${yScaleRadial(y)})`;
|
|
@@ -1706,6 +1936,17 @@ function drawPhylogeny(
|
|
|
1706
1936
|
});
|
|
1707
1937
|
}
|
|
1708
1938
|
|
|
1939
|
+
if (scaleBar) {
|
|
1940
|
+
addScaleBar(svg, {
|
|
1941
|
+
scale: xScaleRadial,
|
|
1942
|
+
basis: maxRadius,
|
|
1943
|
+
defaultX: 20,
|
|
1944
|
+
defaultY: h - 20,
|
|
1945
|
+
scaleBar,
|
|
1946
|
+
fontSize: labelFontSize
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1709
1950
|
return svg.node();
|
|
1710
1951
|
} else if (layout === "unrooted") {
|
|
1711
1952
|
// UNROOTED LAYOUT
|
|
@@ -1755,6 +1996,8 @@ function drawPhylogeny(
|
|
|
1755
1996
|
.attr("stroke-width", strokeWidth)
|
|
1756
1997
|
.attr("stroke", "#777");
|
|
1757
1998
|
|
|
1999
|
+
const R_TIP = tipRadius ?? 4;
|
|
2000
|
+
|
|
1758
2001
|
const nodes = group
|
|
1759
2002
|
.append("g")
|
|
1760
2003
|
.attr("class", "phylo_points")
|
|
@@ -1762,13 +2005,28 @@ function drawPhylogeny(
|
|
|
1762
2005
|
.data(unrootedPhylo.data)
|
|
1763
2006
|
.join("circle")
|
|
1764
2007
|
.attr("class", "dot")
|
|
1765
|
-
.attr("r", (d) => (d.isTip ?
|
|
2008
|
+
.attr("r", (d) => (d.isTip ? R_TIP : (internalNodeCircles ? internalNodeRadius : 0)))
|
|
1766
2009
|
.attr("cx", (d) => xScaleUnroot(d.x))
|
|
1767
2010
|
.attr("cy", (d) => yScaleUnroot(d.y))
|
|
1768
2011
|
.attr("stroke", "black")
|
|
1769
2012
|
.attr("stroke-width", 2)
|
|
1770
2013
|
.attr("fill", (d) => (d.isTip ? "black" : "white"));
|
|
1771
2014
|
|
|
2015
|
+
if (nodeLabels) {
|
|
2016
|
+
const labeledInternalNodes = unrootedPhylo.data.filter((d) => !d.isTip && d.thisLabel);
|
|
2017
|
+
group
|
|
2018
|
+
.append("g")
|
|
2019
|
+
.attr("class", "phylo_node_labels")
|
|
2020
|
+
.selectAll("text")
|
|
2021
|
+
.data(labeledInternalNodes)
|
|
2022
|
+
.join("text")
|
|
2023
|
+
.attr("x", (d) => xScaleUnroot(d.x) + 4)
|
|
2024
|
+
.attr("y", (d) => yScaleUnroot(d.y) - 4)
|
|
2025
|
+
.attr("font-size", nodeLabelSize)
|
|
2026
|
+
.attr("fill", "black")
|
|
2027
|
+
.text((d) => d.thisLabel);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1772
2030
|
const byId = new Map(unrootedPhylo.data.map((d) => [d.thisId, d]));
|
|
1773
2031
|
const tipById = new Map(
|
|
1774
2032
|
unrootedPhylo.data.filter((d) => d.isTip).map((d) => [d.thisId, d])
|
|
@@ -1858,11 +2116,11 @@ function drawPhylogeny(
|
|
|
1858
2116
|
.filter((d) => d.isTip)
|
|
1859
2117
|
.on("mouseenter", function(_event, d) {
|
|
1860
2118
|
drawUnrootedPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1861
|
-
d3.select(this).attr("r",
|
|
2119
|
+
d3.select(this).attr("r", R_TIP + 2);
|
|
1862
2120
|
})
|
|
1863
2121
|
.on("mouseleave", function() {
|
|
1864
2122
|
hoverLayer.selectAll("*").remove();
|
|
1865
|
-
d3.select(this).attr("r",
|
|
2123
|
+
d3.select(this).attr("r", R_TIP);
|
|
1866
2124
|
});
|
|
1867
2125
|
|
|
1868
2126
|
if (highlightTips && highlightTips.length) {
|
|
@@ -1905,6 +2163,17 @@ function drawPhylogeny(
|
|
|
1905
2163
|
}
|
|
1906
2164
|
}
|
|
1907
2165
|
|
|
2166
|
+
if (scaleBar) {
|
|
2167
|
+
addScaleBar(svg, {
|
|
2168
|
+
scale: xScaleUnroot,
|
|
2169
|
+
basis: maxRadius,
|
|
2170
|
+
defaultX: 20,
|
|
2171
|
+
defaultY: h - 20,
|
|
2172
|
+
scaleBar,
|
|
2173
|
+
fontSize: labelFontSize
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
|
|
1908
2177
|
return svg.node();
|
|
1909
2178
|
} else {
|
|
1910
2179
|
throw new Error(
|
|
@@ -1913,5 +2182,5 @@ function drawPhylogeny(
|
|
|
1913
2182
|
}
|
|
1914
2183
|
}
|
|
1915
2184
|
|
|
1916
|
-
export { describeArc, describeArcSweep, drawPhylogeny, parentFisheye, phisheye, polarToCartesian, radialLayout, randomTree, readTree, rectangleLayout, subTree, unrooted };
|
|
2185
|
+
export { describeArc, describeArcSweep, drawPhylogeny, ladderize, parentFisheye, phisheye, polarToCartesian, radialLayout, randomTree, readTree, rectangleLayout, rotate, subTree, toNewick, unrooted };
|
|
1917
2186
|
//# sourceMappingURL=index.js.map
|