@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/dist/index.cjs
CHANGED
|
@@ -165,8 +165,8 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
165
165
|
return `M ${p.x} ${p.y}`; // degenerate span → no arc
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
169
|
-
const sweepFlag = 0; // CCW
|
|
168
|
+
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
169
|
+
const sweepFlag = 0; // CCW in our y-flipped coords: math-CCW = decreasing SVG angle = sweepFlag 0
|
|
170
170
|
|
|
171
171
|
const p0 = polarToCartesian(cx, cy, radius, a0);
|
|
172
172
|
const p1 = polarToCartesian(cx, cy, radius, a1);
|
|
@@ -175,15 +175,17 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// src/radial/describeArcSweep.js
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
178
|
+
// IMPORTANT: angles are in "math space" (increasing = CCW).
|
|
179
|
+
// Because we map y as (cy - r*sin(a)), our math angle t maps to SVG angle -t.
|
|
180
|
+
// Increasing t (math CCW) = decreasing SVG angle = sweepFlag 0 (negative direction).
|
|
181
|
+
// math CCW -> svg sweepFlag = 0
|
|
182
|
+
// math CW -> svg sweepFlag = 1
|
|
183
|
+
function describeArcSweep(
|
|
184
|
+
cx, cy, r,
|
|
185
|
+
a0, a1,
|
|
186
|
+
mathSweep = "ccw", // "ccw" | "cw"
|
|
187
|
+
largeArcFlag = 0
|
|
188
|
+
) {
|
|
187
189
|
if (!(r > 0)) return "";
|
|
188
190
|
|
|
189
191
|
const x0 = cx + r * Math.cos(a0);
|
|
@@ -191,7 +193,9 @@ function describeArcSweep(cx, cy, r, a0, a1, sweep = 1, largeArcFlag = 0) {
|
|
|
191
193
|
const x1 = cx + r * Math.cos(a1);
|
|
192
194
|
const y1 = cy - r * Math.sin(a1);
|
|
193
195
|
|
|
194
|
-
|
|
196
|
+
const svgSweepFlag = (mathSweep === "ccw") ? 0 : 1;
|
|
197
|
+
|
|
198
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${svgSweepFlag} ${x1} ${y1}`;
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
/**
|
|
@@ -468,16 +472,13 @@ function getArcs(pd) {
|
|
|
468
472
|
}
|
|
469
473
|
|
|
470
474
|
/**
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
* start = first child's angle
|
|
474
|
-
* end = last child's angle
|
|
475
|
-
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
476
|
-
*
|
|
477
|
-
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
478
|
-
* @returns {Array} [{parentId,thisId,radius,start,end,sweep}]
|
|
475
|
+
* APE-like block arcs per internal parent.
|
|
476
|
+
* Draw CCW from first child's angle to last child's angle (wrapping allowed).
|
|
479
477
|
*/
|
|
480
478
|
function getArcsFan(pd) {
|
|
479
|
+
const TAU = Math.PI * 2;
|
|
480
|
+
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
481
|
+
|
|
481
482
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
482
483
|
const arcs = [];
|
|
483
484
|
|
|
@@ -489,9 +490,11 @@ function getArcsFan(pd) {
|
|
|
489
490
|
const last = byId.get(c[c.length - 1])?.angle;
|
|
490
491
|
if (first == null || last == null) continue;
|
|
491
492
|
|
|
492
|
-
const start = first;
|
|
493
|
-
const end
|
|
494
|
-
|
|
493
|
+
const start = norm(first);
|
|
494
|
+
const end = norm(last);
|
|
495
|
+
|
|
496
|
+
const deltaCCW = (end - start + TAU) % TAU;
|
|
497
|
+
if (deltaCCW < 1e-9) continue;
|
|
495
498
|
|
|
496
499
|
arcs.push({
|
|
497
500
|
parentId: p.parentId,
|
|
@@ -499,9 +502,11 @@ function getArcsFan(pd) {
|
|
|
499
502
|
radius: p.r,
|
|
500
503
|
start,
|
|
501
504
|
end,
|
|
502
|
-
sweep
|
|
505
|
+
sweep: "ccw", // << math sweep
|
|
506
|
+
largeArc: deltaCCW > Math.PI ? 1 : 0
|
|
503
507
|
});
|
|
504
508
|
}
|
|
509
|
+
|
|
505
510
|
return arcs;
|
|
506
511
|
}
|
|
507
512
|
|
|
@@ -587,76 +592,6 @@ function getChildArcs(pd) {
|
|
|
587
592
|
return arcs;
|
|
588
593
|
}
|
|
589
594
|
|
|
590
|
-
function getChildArcsFan(pd) {
|
|
591
|
-
const TAU = Math.PI * 2;
|
|
592
|
-
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
593
|
-
|
|
594
|
-
function midCCW(a, b) {
|
|
595
|
-
const d = (b - a + TAU) % TAU;
|
|
596
|
-
return norm(a + d / 2);
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const key = (x) => (typeof x === "string" ? +x : x);
|
|
600
|
-
const byId = new Map(pd.map(d => [key(d.thisId), d]));
|
|
601
|
-
const childrenByParent = new Map(
|
|
602
|
-
pd.map(d => [
|
|
603
|
-
key(d.thisId),
|
|
604
|
-
(d.children || [])
|
|
605
|
-
.map(ch => (typeof ch === "object" ? ch.thisId : ch))
|
|
606
|
-
.map(key)
|
|
607
|
-
.filter(id => byId.has(id))
|
|
608
|
-
])
|
|
609
|
-
);
|
|
610
|
-
|
|
611
|
-
const child_arcs = [];
|
|
612
|
-
|
|
613
|
-
for (const parentRaw of pd) {
|
|
614
|
-
const pid = key(parentRaw.thisId);
|
|
615
|
-
const kids = childrenByParent.get(pid) || [];
|
|
616
|
-
if (kids.length < 2) continue;
|
|
617
|
-
|
|
618
|
-
const A = kids
|
|
619
|
-
.map(id => {
|
|
620
|
-
const node = byId.get(id);
|
|
621
|
-
return node ? { id, a: norm(node.angle) } : null;
|
|
622
|
-
})
|
|
623
|
-
.filter(Boolean)
|
|
624
|
-
.sort((u, v) => u.a - v.a);
|
|
625
|
-
|
|
626
|
-
const N = A.length;
|
|
627
|
-
if (N < 2) continue;
|
|
628
|
-
|
|
629
|
-
const parent = byId.get(pid);
|
|
630
|
-
const radius = parent?.r;
|
|
631
|
-
if (!(radius > 0)) continue;
|
|
632
|
-
|
|
633
|
-
for (let i = 0; i < N; i++) {
|
|
634
|
-
const prev = A[(i - 1 + N) % N];
|
|
635
|
-
const cur = A[i];
|
|
636
|
-
const next = A[(i + 1) % N];
|
|
637
|
-
|
|
638
|
-
const start = midCCW(prev.a, cur.a);
|
|
639
|
-
const end = midCCW(cur.a, next.a);
|
|
640
|
-
|
|
641
|
-
const sweep = 1; // always clockwise
|
|
642
|
-
const delta = (end - start + TAU) % TAU;
|
|
643
|
-
const largeArc = delta > Math.PI ? 1 : 0;
|
|
644
|
-
|
|
645
|
-
child_arcs.push({
|
|
646
|
-
parentId: pid,
|
|
647
|
-
childId: cur.id,
|
|
648
|
-
radius,
|
|
649
|
-
start,
|
|
650
|
-
end,
|
|
651
|
-
sweep,
|
|
652
|
-
largeArc
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
return child_arcs;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
595
|
/**
|
|
661
596
|
* radialLayout(node, opts?)
|
|
662
597
|
* opts:
|
|
@@ -700,13 +635,8 @@ function radialLayout(node, opts = {}) {
|
|
|
700
635
|
? getArcsFan(pd)
|
|
701
636
|
: getArcs(pd);
|
|
702
637
|
|
|
703
|
-
// per-child arcs for
|
|
704
|
-
|
|
705
|
-
if (arcsStyle === "fan") {
|
|
706
|
-
child_arcs = getChildArcsFan(pd);
|
|
707
|
-
} else {
|
|
708
|
-
child_arcs = getChildArcs(pd);
|
|
709
|
-
}
|
|
638
|
+
// per-child arcs for path highlighting: always parent.angle → child.angle at parent.r
|
|
639
|
+
const child_arcs = getChildArcs(pd);
|
|
710
640
|
|
|
711
641
|
return { data: pd, radii, arcs, child_arcs };
|
|
712
642
|
}
|
|
@@ -1029,6 +959,85 @@ function parentFisheye(d, data) {
|
|
|
1029
959
|
return parent ? { px: parent.fisheye.x, py: parent.fisheye.y } : null;
|
|
1030
960
|
}
|
|
1031
961
|
|
|
962
|
+
/**
|
|
963
|
+
* Generate a random bifurcating tree with `nTips` tips, in the same
|
|
964
|
+
* parent/children node shape produced by readTree().
|
|
965
|
+
*
|
|
966
|
+
* Topology is grown by repeatedly picking a random extant lineage to split
|
|
967
|
+
* (a Yule/coalescent-style process), so internal branching order is random
|
|
968
|
+
* rather than a fixed balanced/caterpillar shape. Branch lengths are drawn
|
|
969
|
+
* uniformly from [0, maxBranchLength).
|
|
970
|
+
*/
|
|
971
|
+
|
|
972
|
+
function randomTree(nTips = 10, {
|
|
973
|
+
maxBranchLength = 1,
|
|
974
|
+
labelPrefix = 't',
|
|
975
|
+
seed = null
|
|
976
|
+
} = {}) {
|
|
977
|
+
if (!Number.isInteger(nTips) || nTips < 1) {
|
|
978
|
+
throw new Error("nTips must be a positive integer");
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const random = seed == null ? Math.random : mulberry32(seed);
|
|
982
|
+
|
|
983
|
+
let nodeId = 0;
|
|
984
|
+
const makeNode = (parent) => ({
|
|
985
|
+
parent,
|
|
986
|
+
children: [],
|
|
987
|
+
id: nodeId++,
|
|
988
|
+
label: '',
|
|
989
|
+
branchLength: null
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
const root = makeNode(null);
|
|
993
|
+
|
|
994
|
+
if (nTips === 1) {
|
|
995
|
+
root.label = `${labelPrefix}1`;
|
|
996
|
+
return root;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// start with two lineages hanging off the root
|
|
1000
|
+
let lineages = [makeNode(root), makeNode(root)];
|
|
1001
|
+
root.children.push(...lineages);
|
|
1002
|
+
|
|
1003
|
+
// repeatedly split a random lineage until we have nTips of them
|
|
1004
|
+
while (lineages.length < nTips) {
|
|
1005
|
+
const i = Math.floor(random() * lineages.length);
|
|
1006
|
+
const parent = lineages[i];
|
|
1007
|
+
const left = makeNode(parent);
|
|
1008
|
+
const right = makeNode(parent);
|
|
1009
|
+
parent.children.push(left, right);
|
|
1010
|
+
lineages.splice(i, 1, left, right);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// assign branch lengths to every non-root node, and tip labels in
|
|
1014
|
+
// left-to-right order
|
|
1015
|
+
let tipIndex = 0;
|
|
1016
|
+
const assign = (node) => {
|
|
1017
|
+
for (const child of node.children) {
|
|
1018
|
+
child.branchLength = random() * maxBranchLength;
|
|
1019
|
+
assign(child);
|
|
1020
|
+
}
|
|
1021
|
+
if (node.children.length === 0) {
|
|
1022
|
+
node.label = `${labelPrefix}${++tipIndex}`;
|
|
1023
|
+
}
|
|
1024
|
+
};
|
|
1025
|
+
assign(root);
|
|
1026
|
+
|
|
1027
|
+
return root;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// small deterministic PRNG so `seed` gives reproducible trees
|
|
1031
|
+
function mulberry32(seed) {
|
|
1032
|
+
let a = seed >>> 0;
|
|
1033
|
+
return function () {
|
|
1034
|
+
a |= 0; a = (a + 0x6D2B79F5) | 0;
|
|
1035
|
+
let t = Math.imul(a ^ (a >>> 15), 1 | a);
|
|
1036
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
1037
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1032
1041
|
/**
|
|
1033
1042
|
* Parse a Newick tree string into a doubly-linked list of JS Objects.
|
|
1034
1043
|
* Assigns labels, branch lengths, and node IDs (tips before internals if input emits them that way).
|
|
@@ -1043,21 +1052,29 @@ function readTree(text) {
|
|
|
1043
1052
|
text = String(text).replace(/\s+/g, '');
|
|
1044
1053
|
|
|
1045
1054
|
const tokens = text.split(/(;|\(|\)|,)/);
|
|
1046
|
-
const root = { parent: null, children: [] };
|
|
1047
|
-
let curnode = root;
|
|
1048
1055
|
let nodeId = 0;
|
|
1056
|
+
const makeNode = (parent) => ({
|
|
1057
|
+
parent,
|
|
1058
|
+
children: [],
|
|
1059
|
+
id: nodeId++,
|
|
1060
|
+
label: '',
|
|
1061
|
+
branchLength: null
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
const root = makeNode(null);
|
|
1065
|
+
let curnode = root;
|
|
1049
1066
|
|
|
1050
1067
|
for (const token of tokens) {
|
|
1051
1068
|
if (!token || token === ';') continue;
|
|
1052
1069
|
|
|
1053
1070
|
if (token === '(') {
|
|
1054
|
-
const child =
|
|
1071
|
+
const child = makeNode(curnode);
|
|
1055
1072
|
curnode.children.push(child);
|
|
1056
1073
|
curnode = child; // descend
|
|
1057
1074
|
} else if (token === ',') {
|
|
1058
1075
|
// back to parent, then create sibling
|
|
1059
1076
|
curnode = curnode.parent;
|
|
1060
|
-
const child =
|
|
1077
|
+
const child = makeNode(curnode);
|
|
1061
1078
|
curnode.children.push(child);
|
|
1062
1079
|
curnode = child;
|
|
1063
1080
|
} else if (token === ')') {
|
|
@@ -1066,14 +1083,15 @@ function readTree(text) {
|
|
|
1066
1083
|
if (curnode === null) break;
|
|
1067
1084
|
} else {
|
|
1068
1085
|
// label/branch-length chunk (e.g., "A:0.01" or "A")
|
|
1086
|
+
// Note: nodes are assigned an id at creation (above), so internal
|
|
1087
|
+
// (clade) nodes that carry neither a label nor a branch length —
|
|
1088
|
+
// e.g. "((A,B),(C,D));" — still get a valid, linkable id here.
|
|
1069
1089
|
const nodeinfo = token.split(':');
|
|
1070
1090
|
if (nodeinfo.length === 1) {
|
|
1071
1091
|
if (token.startsWith(':')) {
|
|
1072
|
-
curnode.label = '';
|
|
1073
1092
|
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
1074
1093
|
} else {
|
|
1075
1094
|
curnode.label = nodeinfo[0];
|
|
1076
|
-
curnode.branchLength = null;
|
|
1077
1095
|
}
|
|
1078
1096
|
} else if (nodeinfo.length === 2) {
|
|
1079
1097
|
curnode.label = nodeinfo[0];
|
|
@@ -1083,13 +1101,9 @@ function readTree(text) {
|
|
|
1083
1101
|
curnode.label = nodeinfo[0] || '';
|
|
1084
1102
|
curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
|
|
1085
1103
|
}
|
|
1086
|
-
curnode.id = nodeId++; // assign then increment
|
|
1087
1104
|
}
|
|
1088
1105
|
}
|
|
1089
1106
|
|
|
1090
|
-
// Ensure root has an id if not assigned during parsing
|
|
1091
|
-
if (root.id == null) root.id = nodeId;
|
|
1092
|
-
|
|
1093
1107
|
return root;
|
|
1094
1108
|
}
|
|
1095
1109
|
|
|
@@ -1149,6 +1163,7 @@ function drawPhylogeny(
|
|
|
1149
1163
|
strokeWidth = 1, // for the phylogeny branches
|
|
1150
1164
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
1151
1165
|
tipLabels = true,
|
|
1166
|
+
labelFontSize = 10, // font size (px) for tip labels
|
|
1152
1167
|
showTooltips = true,
|
|
1153
1168
|
tooltipFormatter = (d, rtt) =>
|
|
1154
1169
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1196,7 +1211,7 @@ function drawPhylogeny(
|
|
|
1196
1211
|
const tips = horizontal.filter((d) => d.isTip);
|
|
1197
1212
|
|
|
1198
1213
|
// indices & root→tip getter
|
|
1199
|
-
const byId = new Map(
|
|
1214
|
+
const byId = new Map(tree_df.data.map((d) => [d.thisId, d])); // includes root
|
|
1200
1215
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1201
1216
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1202
1217
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
@@ -1270,6 +1285,7 @@ function drawPhylogeny(
|
|
|
1270
1285
|
// interactive root→tip highlight (rect) on dot hover
|
|
1271
1286
|
tipDots
|
|
1272
1287
|
.on("mouseenter", function(_event, d) {
|
|
1288
|
+
hoverLayer.selectAll("*").remove();
|
|
1273
1289
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1274
1290
|
d3__namespace.select(this).attr("r", 4);
|
|
1275
1291
|
})
|
|
@@ -1289,7 +1305,7 @@ function drawPhylogeny(
|
|
|
1289
1305
|
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1290
1306
|
.attr("y", (d) => yScale(d.y1))
|
|
1291
1307
|
.attr("dy", "0.32em")
|
|
1292
|
-
.attr("font-size",
|
|
1308
|
+
.attr("font-size", labelFontSize)
|
|
1293
1309
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1294
1310
|
|
|
1295
1311
|
if (showTooltips) {
|
|
@@ -1300,6 +1316,7 @@ function drawPhylogeny(
|
|
|
1300
1316
|
|
|
1301
1317
|
labels
|
|
1302
1318
|
.on("mouseenter", function(_event, d) {
|
|
1319
|
+
hoverLayer.selectAll("*").remove();
|
|
1303
1320
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1304
1321
|
d3__namespace.select(this).attr("font-weight", 600);
|
|
1305
1322
|
})
|
|
@@ -1326,7 +1343,6 @@ function drawPhylogeny(
|
|
|
1326
1343
|
|
|
1327
1344
|
// helper to draw root→tip for rect (both vertical+horizontal)
|
|
1328
1345
|
function drawRectPath(tipId, layer, stroke, width) {
|
|
1329
|
-
layer.selectAll("*").remove();
|
|
1330
1346
|
let cur = byId.get(tipId);
|
|
1331
1347
|
while (cur && cur.parentId != null) {
|
|
1332
1348
|
const parent = byId.get(cur.parentId);
|
|
@@ -1383,10 +1399,14 @@ function drawPhylogeny(
|
|
|
1383
1399
|
const END_CAP = 0;
|
|
1384
1400
|
|
|
1385
1401
|
// ===== SCALES / BOUNDS =====
|
|
1386
|
-
const maxRadius = d3__namespace.max(rad.data, (d) => d.r) ?? 0;
|
|
1387
|
-
const scaleRadial = maxRadius + 2 * radialMargin;
|
|
1388
1402
|
const w = width,
|
|
1389
1403
|
h = height;
|
|
1404
|
+
const maxRadius = d3__namespace.max(rad.data, (d) => d.r) ?? 0;
|
|
1405
|
+
// radialMargin is in pixels: tips sit (radialMargin) px from the SVG edge.
|
|
1406
|
+
// Derive the data-space scale so that radiusPx(maxRadius) = w/2 - radialMargin.
|
|
1407
|
+
const scaleRadial = maxRadius > 0
|
|
1408
|
+
? maxRadius * (w / 2) / (w / 2 - radialMargin)
|
|
1409
|
+
: 1;
|
|
1390
1410
|
const centerX = w / 2,
|
|
1391
1411
|
centerY = h / 2;
|
|
1392
1412
|
|
|
@@ -1466,8 +1486,8 @@ function drawPhylogeny(
|
|
|
1466
1486
|
radiusPx(d.radius),
|
|
1467
1487
|
d.start,
|
|
1468
1488
|
d.end,
|
|
1469
|
-
d.sweep,
|
|
1470
|
-
d.largeArc,
|
|
1489
|
+
d.sweep ?? "ccw",
|
|
1490
|
+
d.largeArc ?? 0,
|
|
1471
1491
|
)
|
|
1472
1492
|
)
|
|
1473
1493
|
.attr("fill", "none")
|
|
@@ -1582,7 +1602,7 @@ function drawPhylogeny(
|
|
|
1582
1602
|
.attr("x", xoff)
|
|
1583
1603
|
.attr("alignment-baseline", "middle")
|
|
1584
1604
|
.attr("text-anchor", anchor)
|
|
1585
|
-
.attr("font-size",
|
|
1605
|
+
.attr("font-size", labelFontSize)
|
|
1586
1606
|
.attr("fill", "black")
|
|
1587
1607
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1588
1608
|
});
|
|
@@ -1596,6 +1616,8 @@ function drawPhylogeny(
|
|
|
1596
1616
|
// label hover
|
|
1597
1617
|
labels
|
|
1598
1618
|
.on("mouseenter", function(_event, d) {
|
|
1619
|
+
hoverLines.selectAll("*").remove();
|
|
1620
|
+
hoverArcs.selectAll("*").remove();
|
|
1599
1621
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1600
1622
|
d3__namespace.select(this).select("text").attr("font-weight", 600);
|
|
1601
1623
|
})
|
|
@@ -1615,9 +1637,6 @@ function drawPhylogeny(
|
|
|
1615
1637
|
width = 3
|
|
1616
1638
|
) {
|
|
1617
1639
|
// target may be a tip node *or* a numeric tip id
|
|
1618
|
-
lineLayer.selectAll("*").remove();
|
|
1619
|
-
arcLayer.selectAll("*").remove();
|
|
1620
|
-
|
|
1621
1640
|
let cur = (typeof target === "number" || typeof target === "string")
|
|
1622
1641
|
? byId.get(target)
|
|
1623
1642
|
: target;
|
|
@@ -1656,18 +1675,10 @@ function drawPhylogeny(
|
|
|
1656
1675
|
const R = radiusPx(rec.radius);
|
|
1657
1676
|
return rec.sweep == null
|
|
1658
1677
|
? describeArc(centerX, centerY, R, rec.start, rec.end)
|
|
1659
|
-
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep,
|
|
1678
|
+
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep ?? "ccw", rec.largeArc ?? 0);
|
|
1660
1679
|
}
|
|
1661
1680
|
|
|
1662
1681
|
if (a) {
|
|
1663
|
-
console.log("Drawing arc:", {
|
|
1664
|
-
childId: cur.thisId,
|
|
1665
|
-
startDeg: (a.start * 180 / Math.PI).toFixed(2),
|
|
1666
|
-
endDeg: (a.end * 180 / Math.PI).toFixed(2),
|
|
1667
|
-
sweep: a.sweep,
|
|
1668
|
-
radius: a.radius
|
|
1669
|
-
});
|
|
1670
|
-
|
|
1671
1682
|
arcLayer
|
|
1672
1683
|
.append("path")
|
|
1673
1684
|
.attr("d", pathFromArcRecord(a))
|
|
@@ -1684,6 +1695,8 @@ function drawPhylogeny(
|
|
|
1684
1695
|
// tip dot hover
|
|
1685
1696
|
tipDots
|
|
1686
1697
|
.on("mouseenter", function(_event, d) {
|
|
1698
|
+
hoverLines.selectAll("*").remove();
|
|
1699
|
+
hoverArcs.selectAll("*").remove();
|
|
1687
1700
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1688
1701
|
d3__namespace.select(this).attr("r", DOT_R + 2);
|
|
1689
1702
|
})
|
|
@@ -1840,7 +1853,7 @@ function drawPhylogeny(
|
|
|
1840
1853
|
.attr("x", xOffset)
|
|
1841
1854
|
.attr("alignment-baseline", "middle")
|
|
1842
1855
|
.attr("text-anchor", anchor)
|
|
1843
|
-
.attr("font-size",
|
|
1856
|
+
.attr("font-size", labelFontSize)
|
|
1844
1857
|
.attr("fill", "black")
|
|
1845
1858
|
.text(d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1846
1859
|
});
|
|
@@ -1928,6 +1941,7 @@ exports.parentFisheye = parentFisheye;
|
|
|
1928
1941
|
exports.phisheye = phisheye;
|
|
1929
1942
|
exports.polarToCartesian = polarToCartesian;
|
|
1930
1943
|
exports.radialLayout = radialLayout;
|
|
1944
|
+
exports.randomTree = randomTree;
|
|
1931
1945
|
exports.readTree = readTree;
|
|
1932
1946
|
exports.rectangleLayout = rectangleLayout;
|
|
1933
1947
|
exports.subTree = subTree;
|