@euphrasiologist/lwphylo 1.2.24 → 1.3.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 +5 -6
- package/dist/drawPhylogeny.cjs +66 -132
- package/dist/drawPhylogeny.cjs.map +1 -1
- package/dist/drawPhylogeny.esm.js +66 -132
- 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 +146 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +146 -133
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,16 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight, low level javascript library to plot phylogenies from a Newick file. It uses no dependencies on any other package, but is designed to be given to the D3 library for visualisation.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Website
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Visit https://euphrasiologist.github.io/lwPhylo/ to see examples and live rendering of trees. Can even paste your own in.
|
|
8
8
|
|
|
9
|
-
###
|
|
9
|
+
### Functionality
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
It goes over the three tree layout functions, and hopefully is all quite straightforward.
|
|
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.
|
|
13
12
|
|
|
14
|
-
|
|
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.
|
|
15
14
|
|
|
16
15
|
### Acknowledgements
|
|
17
16
|
|
package/dist/drawPhylogeny.cjs
CHANGED
|
@@ -52,8 +52,8 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
52
52
|
return `M ${p.x} ${p.y}`; // degenerate span → no arc
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
56
|
-
const sweepFlag = 0; // CCW
|
|
55
|
+
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
56
|
+
const sweepFlag = 0; // CCW in our y-flipped coords: math-CCW = decreasing SVG angle = sweepFlag 0
|
|
57
57
|
|
|
58
58
|
const p0 = polarToCartesian(cx, cy, radius, a0);
|
|
59
59
|
const p1 = polarToCartesian(cx, cy, radius, a1);
|
|
@@ -62,15 +62,17 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// src/radial/describeArcSweep.js
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
// IMPORTANT: angles are in "math space" (increasing = CCW).
|
|
66
|
+
// Because we map y as (cy - r*sin(a)), our math angle t maps to SVG angle -t.
|
|
67
|
+
// Increasing t (math CCW) = decreasing SVG angle = sweepFlag 0 (negative direction).
|
|
68
|
+
// math CCW -> svg sweepFlag = 0
|
|
69
|
+
// math CW -> svg sweepFlag = 1
|
|
70
|
+
function describeArcSweep(
|
|
71
|
+
cx, cy, r,
|
|
72
|
+
a0, a1,
|
|
73
|
+
mathSweep = "ccw", // "ccw" | "cw"
|
|
74
|
+
largeArcFlag = 0
|
|
75
|
+
) {
|
|
74
76
|
if (!(r > 0)) return "";
|
|
75
77
|
|
|
76
78
|
const x0 = cx + r * Math.cos(a0);
|
|
@@ -78,7 +80,9 @@ function describeArcSweep(cx, cy, r, a0, a1, sweep = 1, largeArcFlag = 0) {
|
|
|
78
80
|
const x1 = cx + r * Math.cos(a1);
|
|
79
81
|
const y1 = cy - r * Math.sin(a1);
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
const svgSweepFlag = (mathSweep === "ccw") ? 0 : 1;
|
|
84
|
+
|
|
85
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${svgSweepFlag} ${x1} ${y1}`;
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
/**
|
|
@@ -355,16 +359,13 @@ function getArcs(pd) {
|
|
|
355
359
|
}
|
|
356
360
|
|
|
357
361
|
/**
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
* start = first child's angle
|
|
361
|
-
* end = last child's angle
|
|
362
|
-
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
363
|
-
*
|
|
364
|
-
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
365
|
-
* @returns {Array} [{parentId,thisId,radius,start,end,sweep}]
|
|
362
|
+
* APE-like block arcs per internal parent.
|
|
363
|
+
* Draw CCW from first child's angle to last child's angle (wrapping allowed).
|
|
366
364
|
*/
|
|
367
365
|
function getArcsFan(pd) {
|
|
366
|
+
const TAU = Math.PI * 2;
|
|
367
|
+
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
368
|
+
|
|
368
369
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
369
370
|
const arcs = [];
|
|
370
371
|
|
|
@@ -376,9 +377,11 @@ function getArcsFan(pd) {
|
|
|
376
377
|
const last = byId.get(c[c.length - 1])?.angle;
|
|
377
378
|
if (first == null || last == null) continue;
|
|
378
379
|
|
|
379
|
-
const start = first;
|
|
380
|
-
const end
|
|
381
|
-
|
|
380
|
+
const start = norm(first);
|
|
381
|
+
const end = norm(last);
|
|
382
|
+
|
|
383
|
+
const deltaCCW = (end - start + TAU) % TAU;
|
|
384
|
+
if (deltaCCW < 1e-9) continue;
|
|
382
385
|
|
|
383
386
|
arcs.push({
|
|
384
387
|
parentId: p.parentId,
|
|
@@ -386,9 +389,11 @@ function getArcsFan(pd) {
|
|
|
386
389
|
radius: p.r,
|
|
387
390
|
start,
|
|
388
391
|
end,
|
|
389
|
-
sweep
|
|
392
|
+
sweep: "ccw", // << math sweep
|
|
393
|
+
largeArc: deltaCCW > Math.PI ? 1 : 0
|
|
390
394
|
});
|
|
391
395
|
}
|
|
396
|
+
|
|
392
397
|
return arcs;
|
|
393
398
|
}
|
|
394
399
|
|
|
@@ -474,76 +479,6 @@ function getChildArcs(pd) {
|
|
|
474
479
|
return arcs;
|
|
475
480
|
}
|
|
476
481
|
|
|
477
|
-
function getChildArcsFan(pd) {
|
|
478
|
-
const TAU = Math.PI * 2;
|
|
479
|
-
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
480
|
-
|
|
481
|
-
function midCCW(a, b) {
|
|
482
|
-
const d = (b - a + TAU) % TAU;
|
|
483
|
-
return norm(a + d / 2);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const key = (x) => (typeof x === "string" ? +x : x);
|
|
487
|
-
const byId = new Map(pd.map(d => [key(d.thisId), d]));
|
|
488
|
-
const childrenByParent = new Map(
|
|
489
|
-
pd.map(d => [
|
|
490
|
-
key(d.thisId),
|
|
491
|
-
(d.children || [])
|
|
492
|
-
.map(ch => (typeof ch === "object" ? ch.thisId : ch))
|
|
493
|
-
.map(key)
|
|
494
|
-
.filter(id => byId.has(id))
|
|
495
|
-
])
|
|
496
|
-
);
|
|
497
|
-
|
|
498
|
-
const child_arcs = [];
|
|
499
|
-
|
|
500
|
-
for (const parentRaw of pd) {
|
|
501
|
-
const pid = key(parentRaw.thisId);
|
|
502
|
-
const kids = childrenByParent.get(pid) || [];
|
|
503
|
-
if (kids.length < 2) continue;
|
|
504
|
-
|
|
505
|
-
const A = kids
|
|
506
|
-
.map(id => {
|
|
507
|
-
const node = byId.get(id);
|
|
508
|
-
return node ? { id, a: norm(node.angle) } : null;
|
|
509
|
-
})
|
|
510
|
-
.filter(Boolean)
|
|
511
|
-
.sort((u, v) => u.a - v.a);
|
|
512
|
-
|
|
513
|
-
const N = A.length;
|
|
514
|
-
if (N < 2) continue;
|
|
515
|
-
|
|
516
|
-
const parent = byId.get(pid);
|
|
517
|
-
const radius = parent?.r;
|
|
518
|
-
if (!(radius > 0)) continue;
|
|
519
|
-
|
|
520
|
-
for (let i = 0; i < N; i++) {
|
|
521
|
-
const prev = A[(i - 1 + N) % N];
|
|
522
|
-
const cur = A[i];
|
|
523
|
-
const next = A[(i + 1) % N];
|
|
524
|
-
|
|
525
|
-
const start = midCCW(prev.a, cur.a);
|
|
526
|
-
const end = midCCW(cur.a, next.a);
|
|
527
|
-
|
|
528
|
-
const sweep = 1; // always clockwise
|
|
529
|
-
const delta = (end - start + TAU) % TAU;
|
|
530
|
-
const largeArc = delta > Math.PI ? 1 : 0;
|
|
531
|
-
|
|
532
|
-
child_arcs.push({
|
|
533
|
-
parentId: pid,
|
|
534
|
-
childId: cur.id,
|
|
535
|
-
radius,
|
|
536
|
-
start,
|
|
537
|
-
end,
|
|
538
|
-
sweep,
|
|
539
|
-
largeArc
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
return child_arcs;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
482
|
/**
|
|
548
483
|
* radialLayout(node, opts?)
|
|
549
484
|
* opts:
|
|
@@ -587,13 +522,8 @@ function radialLayout(node, opts = {}) {
|
|
|
587
522
|
? getArcsFan(pd)
|
|
588
523
|
: getArcs(pd);
|
|
589
524
|
|
|
590
|
-
// per-child arcs for
|
|
591
|
-
|
|
592
|
-
if (arcsStyle === "fan") {
|
|
593
|
-
child_arcs = getChildArcsFan(pd);
|
|
594
|
-
} else {
|
|
595
|
-
child_arcs = getChildArcs(pd);
|
|
596
|
-
}
|
|
525
|
+
// per-child arcs for path highlighting: always parent.angle → child.angle at parent.r
|
|
526
|
+
const child_arcs = getChildArcs(pd);
|
|
597
527
|
|
|
598
528
|
return { data: pd, radii, arcs, child_arcs };
|
|
599
529
|
}
|
|
@@ -921,21 +851,29 @@ function readTree(text) {
|
|
|
921
851
|
text = String(text).replace(/\s+/g, '');
|
|
922
852
|
|
|
923
853
|
const tokens = text.split(/(;|\(|\)|,)/);
|
|
924
|
-
const root = { parent: null, children: [] };
|
|
925
|
-
let curnode = root;
|
|
926
854
|
let nodeId = 0;
|
|
855
|
+
const makeNode = (parent) => ({
|
|
856
|
+
parent,
|
|
857
|
+
children: [],
|
|
858
|
+
id: nodeId++,
|
|
859
|
+
label: '',
|
|
860
|
+
branchLength: null
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
const root = makeNode(null);
|
|
864
|
+
let curnode = root;
|
|
927
865
|
|
|
928
866
|
for (const token of tokens) {
|
|
929
867
|
if (!token || token === ';') continue;
|
|
930
868
|
|
|
931
869
|
if (token === '(') {
|
|
932
|
-
const child =
|
|
870
|
+
const child = makeNode(curnode);
|
|
933
871
|
curnode.children.push(child);
|
|
934
872
|
curnode = child; // descend
|
|
935
873
|
} else if (token === ',') {
|
|
936
874
|
// back to parent, then create sibling
|
|
937
875
|
curnode = curnode.parent;
|
|
938
|
-
const child =
|
|
876
|
+
const child = makeNode(curnode);
|
|
939
877
|
curnode.children.push(child);
|
|
940
878
|
curnode = child;
|
|
941
879
|
} else if (token === ')') {
|
|
@@ -944,14 +882,15 @@ function readTree(text) {
|
|
|
944
882
|
if (curnode === null) break;
|
|
945
883
|
} else {
|
|
946
884
|
// label/branch-length chunk (e.g., "A:0.01" or "A")
|
|
885
|
+
// Note: nodes are assigned an id at creation (above), so internal
|
|
886
|
+
// (clade) nodes that carry neither a label nor a branch length —
|
|
887
|
+
// e.g. "((A,B),(C,D));" — still get a valid, linkable id here.
|
|
947
888
|
const nodeinfo = token.split(':');
|
|
948
889
|
if (nodeinfo.length === 1) {
|
|
949
890
|
if (token.startsWith(':')) {
|
|
950
|
-
curnode.label = '';
|
|
951
891
|
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
952
892
|
} else {
|
|
953
893
|
curnode.label = nodeinfo[0];
|
|
954
|
-
curnode.branchLength = null;
|
|
955
894
|
}
|
|
956
895
|
} else if (nodeinfo.length === 2) {
|
|
957
896
|
curnode.label = nodeinfo[0];
|
|
@@ -961,13 +900,9 @@ function readTree(text) {
|
|
|
961
900
|
curnode.label = nodeinfo[0] || '';
|
|
962
901
|
curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
|
|
963
902
|
}
|
|
964
|
-
curnode.id = nodeId++; // assign then increment
|
|
965
903
|
}
|
|
966
904
|
}
|
|
967
905
|
|
|
968
|
-
// Ensure root has an id if not assigned during parsing
|
|
969
|
-
if (root.id == null) root.id = nodeId;
|
|
970
|
-
|
|
971
906
|
return root;
|
|
972
907
|
}
|
|
973
908
|
|
|
@@ -982,6 +917,7 @@ function drawPhylogeny(
|
|
|
982
917
|
strokeWidth = 1, // for the phylogeny branches
|
|
983
918
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
984
919
|
tipLabels = true,
|
|
920
|
+
labelFontSize = 10, // font size (px) for tip labels
|
|
985
921
|
showTooltips = true,
|
|
986
922
|
tooltipFormatter = (d, rtt) =>
|
|
987
923
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1029,7 +965,7 @@ function drawPhylogeny(
|
|
|
1029
965
|
const tips = horizontal.filter((d) => d.isTip);
|
|
1030
966
|
|
|
1031
967
|
// indices & root→tip getter
|
|
1032
|
-
const byId = new Map(
|
|
968
|
+
const byId = new Map(tree_df.data.map((d) => [d.thisId, d])); // includes root
|
|
1033
969
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1034
970
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1035
971
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
@@ -1103,6 +1039,7 @@ function drawPhylogeny(
|
|
|
1103
1039
|
// interactive root→tip highlight (rect) on dot hover
|
|
1104
1040
|
tipDots
|
|
1105
1041
|
.on("mouseenter", function(_event, d) {
|
|
1042
|
+
hoverLayer.selectAll("*").remove();
|
|
1106
1043
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1107
1044
|
d3__namespace.select(this).attr("r", 4);
|
|
1108
1045
|
})
|
|
@@ -1122,7 +1059,7 @@ function drawPhylogeny(
|
|
|
1122
1059
|
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1123
1060
|
.attr("y", (d) => yScale(d.y1))
|
|
1124
1061
|
.attr("dy", "0.32em")
|
|
1125
|
-
.attr("font-size",
|
|
1062
|
+
.attr("font-size", labelFontSize)
|
|
1126
1063
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1127
1064
|
|
|
1128
1065
|
if (showTooltips) {
|
|
@@ -1133,6 +1070,7 @@ function drawPhylogeny(
|
|
|
1133
1070
|
|
|
1134
1071
|
labels
|
|
1135
1072
|
.on("mouseenter", function(_event, d) {
|
|
1073
|
+
hoverLayer.selectAll("*").remove();
|
|
1136
1074
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1137
1075
|
d3__namespace.select(this).attr("font-weight", 600);
|
|
1138
1076
|
})
|
|
@@ -1159,7 +1097,6 @@ function drawPhylogeny(
|
|
|
1159
1097
|
|
|
1160
1098
|
// helper to draw root→tip for rect (both vertical+horizontal)
|
|
1161
1099
|
function drawRectPath(tipId, layer, stroke, width) {
|
|
1162
|
-
layer.selectAll("*").remove();
|
|
1163
1100
|
let cur = byId.get(tipId);
|
|
1164
1101
|
while (cur && cur.parentId != null) {
|
|
1165
1102
|
const parent = byId.get(cur.parentId);
|
|
@@ -1216,10 +1153,14 @@ function drawPhylogeny(
|
|
|
1216
1153
|
const END_CAP = 0;
|
|
1217
1154
|
|
|
1218
1155
|
// ===== SCALES / BOUNDS =====
|
|
1219
|
-
const maxRadius = d3__namespace.max(rad.data, (d) => d.r) ?? 0;
|
|
1220
|
-
const scaleRadial = maxRadius + 2 * radialMargin;
|
|
1221
1156
|
const w = width,
|
|
1222
1157
|
h = height;
|
|
1158
|
+
const maxRadius = d3__namespace.max(rad.data, (d) => d.r) ?? 0;
|
|
1159
|
+
// radialMargin is in pixels: tips sit (radialMargin) px from the SVG edge.
|
|
1160
|
+
// Derive the data-space scale so that radiusPx(maxRadius) = w/2 - radialMargin.
|
|
1161
|
+
const scaleRadial = maxRadius > 0
|
|
1162
|
+
? maxRadius * (w / 2) / (w / 2 - radialMargin)
|
|
1163
|
+
: 1;
|
|
1223
1164
|
const centerX = w / 2,
|
|
1224
1165
|
centerY = h / 2;
|
|
1225
1166
|
|
|
@@ -1299,8 +1240,8 @@ function drawPhylogeny(
|
|
|
1299
1240
|
radiusPx(d.radius),
|
|
1300
1241
|
d.start,
|
|
1301
1242
|
d.end,
|
|
1302
|
-
d.sweep,
|
|
1303
|
-
d.largeArc,
|
|
1243
|
+
d.sweep ?? "ccw",
|
|
1244
|
+
d.largeArc ?? 0,
|
|
1304
1245
|
)
|
|
1305
1246
|
)
|
|
1306
1247
|
.attr("fill", "none")
|
|
@@ -1415,7 +1356,7 @@ function drawPhylogeny(
|
|
|
1415
1356
|
.attr("x", xoff)
|
|
1416
1357
|
.attr("alignment-baseline", "middle")
|
|
1417
1358
|
.attr("text-anchor", anchor)
|
|
1418
|
-
.attr("font-size",
|
|
1359
|
+
.attr("font-size", labelFontSize)
|
|
1419
1360
|
.attr("fill", "black")
|
|
1420
1361
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1421
1362
|
});
|
|
@@ -1429,6 +1370,8 @@ function drawPhylogeny(
|
|
|
1429
1370
|
// label hover
|
|
1430
1371
|
labels
|
|
1431
1372
|
.on("mouseenter", function(_event, d) {
|
|
1373
|
+
hoverLines.selectAll("*").remove();
|
|
1374
|
+
hoverArcs.selectAll("*").remove();
|
|
1432
1375
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1433
1376
|
d3__namespace.select(this).select("text").attr("font-weight", 600);
|
|
1434
1377
|
})
|
|
@@ -1448,9 +1391,6 @@ function drawPhylogeny(
|
|
|
1448
1391
|
width = 3
|
|
1449
1392
|
) {
|
|
1450
1393
|
// target may be a tip node *or* a numeric tip id
|
|
1451
|
-
lineLayer.selectAll("*").remove();
|
|
1452
|
-
arcLayer.selectAll("*").remove();
|
|
1453
|
-
|
|
1454
1394
|
let cur = (typeof target === "number" || typeof target === "string")
|
|
1455
1395
|
? byId.get(target)
|
|
1456
1396
|
: target;
|
|
@@ -1489,18 +1429,10 @@ function drawPhylogeny(
|
|
|
1489
1429
|
const R = radiusPx(rec.radius);
|
|
1490
1430
|
return rec.sweep == null
|
|
1491
1431
|
? describeArc(centerX, centerY, R, rec.start, rec.end)
|
|
1492
|
-
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep,
|
|
1432
|
+
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep ?? "ccw", rec.largeArc ?? 0);
|
|
1493
1433
|
}
|
|
1494
1434
|
|
|
1495
1435
|
if (a) {
|
|
1496
|
-
console.log("Drawing arc:", {
|
|
1497
|
-
childId: cur.thisId,
|
|
1498
|
-
startDeg: (a.start * 180 / Math.PI).toFixed(2),
|
|
1499
|
-
endDeg: (a.end * 180 / Math.PI).toFixed(2),
|
|
1500
|
-
sweep: a.sweep,
|
|
1501
|
-
radius: a.radius
|
|
1502
|
-
});
|
|
1503
|
-
|
|
1504
1436
|
arcLayer
|
|
1505
1437
|
.append("path")
|
|
1506
1438
|
.attr("d", pathFromArcRecord(a))
|
|
@@ -1517,6 +1449,8 @@ function drawPhylogeny(
|
|
|
1517
1449
|
// tip dot hover
|
|
1518
1450
|
tipDots
|
|
1519
1451
|
.on("mouseenter", function(_event, d) {
|
|
1452
|
+
hoverLines.selectAll("*").remove();
|
|
1453
|
+
hoverArcs.selectAll("*").remove();
|
|
1520
1454
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1521
1455
|
d3__namespace.select(this).attr("r", DOT_R + 2);
|
|
1522
1456
|
})
|
|
@@ -1673,7 +1607,7 @@ function drawPhylogeny(
|
|
|
1673
1607
|
.attr("x", xOffset)
|
|
1674
1608
|
.attr("alignment-baseline", "middle")
|
|
1675
1609
|
.attr("text-anchor", anchor)
|
|
1676
|
-
.attr("font-size",
|
|
1610
|
+
.attr("font-size", labelFontSize)
|
|
1677
1611
|
.attr("fill", "black")
|
|
1678
1612
|
.text(d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1679
1613
|
});
|