@euphrasiologist/lwphylo 1.2.23 → 1.2.26
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/dist/drawPhylogeny.cjs +70 -141
- package/dist/drawPhylogeny.cjs.map +1 -1
- package/dist/drawPhylogeny.esm.js +70 -141
- 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 +70 -141
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +70 -141
- 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,25 +175,27 @@ 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
|
-
|
|
187
|
-
|
|
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
|
+
) {
|
|
189
|
+
if (!(r > 0)) return "";
|
|
188
190
|
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const
|
|
191
|
+
const x0 = cx + r * Math.cos(a0);
|
|
192
|
+
const y0 = cy - r * Math.sin(a0);
|
|
193
|
+
const x1 = cx + r * Math.cos(a1);
|
|
194
|
+
const y1 = cy - r * Math.sin(a1);
|
|
192
195
|
|
|
193
|
-
const
|
|
194
|
-
const x1 = cx + r * Math.cos(a1), y1 = cy - r * Math.sin(a1);
|
|
196
|
+
const svgSweepFlag = (mathSweep === "ccw") ? 0 : 1;
|
|
195
197
|
|
|
196
|
-
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${
|
|
198
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${svgSweepFlag} ${x1} ${y1}`;
|
|
197
199
|
}
|
|
198
200
|
|
|
199
201
|
/**
|
|
@@ -470,16 +472,13 @@ function getArcs(pd) {
|
|
|
470
472
|
}
|
|
471
473
|
|
|
472
474
|
/**
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
* start = first child's angle
|
|
476
|
-
* end = last child's angle
|
|
477
|
-
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
478
|
-
*
|
|
479
|
-
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
480
|
-
* @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).
|
|
481
477
|
*/
|
|
482
478
|
function getArcsFan(pd) {
|
|
479
|
+
const TAU = Math.PI * 2;
|
|
480
|
+
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
481
|
+
|
|
483
482
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
484
483
|
const arcs = [];
|
|
485
484
|
|
|
@@ -491,9 +490,11 @@ function getArcsFan(pd) {
|
|
|
491
490
|
const last = byId.get(c[c.length - 1])?.angle;
|
|
492
491
|
if (first == null || last == null) continue;
|
|
493
492
|
|
|
494
|
-
const start = first;
|
|
495
|
-
const end
|
|
496
|
-
|
|
493
|
+
const start = norm(first);
|
|
494
|
+
const end = norm(last);
|
|
495
|
+
|
|
496
|
+
const deltaCCW = (end - start + TAU) % TAU;
|
|
497
|
+
if (deltaCCW < 1e-9) continue;
|
|
497
498
|
|
|
498
499
|
arcs.push({
|
|
499
500
|
parentId: p.parentId,
|
|
@@ -501,9 +502,11 @@ function getArcsFan(pd) {
|
|
|
501
502
|
radius: p.r,
|
|
502
503
|
start,
|
|
503
504
|
end,
|
|
504
|
-
sweep
|
|
505
|
+
sweep: "ccw", // << math sweep
|
|
506
|
+
largeArc: deltaCCW > Math.PI ? 1 : 0
|
|
505
507
|
});
|
|
506
508
|
}
|
|
509
|
+
|
|
507
510
|
return arcs;
|
|
508
511
|
}
|
|
509
512
|
|
|
@@ -589,80 +592,6 @@ function getChildArcs(pd) {
|
|
|
589
592
|
return arcs;
|
|
590
593
|
}
|
|
591
594
|
|
|
592
|
-
function getChildArcsFan(pd) {
|
|
593
|
-
const TAU = Math.PI * 2;
|
|
594
|
-
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
595
|
-
|
|
596
|
-
// Circular midpoint that travels CCW from a -> b by half the CCW span
|
|
597
|
-
function midCCW(a, b) {
|
|
598
|
-
const d = (b - a + TAU) % TAU; // CCW delta in [0, 2π)
|
|
599
|
-
return norm(a + d / 2);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const key = (x) => (typeof x === "string" ? +x : x);
|
|
603
|
-
|
|
604
|
-
const byId = new Map(pd.map(d => [key(d.thisId), d]));
|
|
605
|
-
const childrenByParent = new Map(
|
|
606
|
-
pd.map(d => [
|
|
607
|
-
key(d.thisId),
|
|
608
|
-
// normalize children to numeric IDs; drop anything we can't resolve
|
|
609
|
-
(d.children || [])
|
|
610
|
-
.map(ch => (typeof ch === "object" ? ch.thisId : ch))
|
|
611
|
-
.map(key)
|
|
612
|
-
.filter(id => byId.has(id))
|
|
613
|
-
])
|
|
614
|
-
);
|
|
615
|
-
|
|
616
|
-
const child_arcs = [];
|
|
617
|
-
|
|
618
|
-
for (const parentRaw of pd) {
|
|
619
|
-
const pid = key(parentRaw.thisId);
|
|
620
|
-
const kids = childrenByParent.get(pid) || [];
|
|
621
|
-
if (kids.length < 2) continue;
|
|
622
|
-
|
|
623
|
-
// Sort children by angle (normalized) around the circle
|
|
624
|
-
const A = kids
|
|
625
|
-
.map(id => {
|
|
626
|
-
const node = byId.get(id);
|
|
627
|
-
return node ? { id, a: norm(node.angle) } : null;
|
|
628
|
-
})
|
|
629
|
-
.filter(Boolean)
|
|
630
|
-
.sort((u, v) => u.a - v.a);
|
|
631
|
-
|
|
632
|
-
const N = A.length;
|
|
633
|
-
if (N < 2) continue;
|
|
634
|
-
|
|
635
|
-
const parent = byId.get(pid);
|
|
636
|
-
const radius = parent?.r;
|
|
637
|
-
if (!(radius > 0)) continue;
|
|
638
|
-
|
|
639
|
-
for (let i = 0; i < N; i++) {
|
|
640
|
-
const prev = A[(i - 1 + N) % N];
|
|
641
|
-
const cur = A[i];
|
|
642
|
-
const next = A[(i + 1) % N];
|
|
643
|
-
|
|
644
|
-
const start = midCCW(prev.a, cur.a);
|
|
645
|
-
const end = midCCW(cur.a, next.a);
|
|
646
|
-
|
|
647
|
-
// Use SVG-conforming sweep logic
|
|
648
|
-
const delta = (end - start + TAU) % TAU;
|
|
649
|
-
const sweep = delta > Math.PI ? 0 : 1;
|
|
650
|
-
|
|
651
|
-
child_arcs.push({
|
|
652
|
-
parentId: pid,
|
|
653
|
-
childId: cur.id,
|
|
654
|
-
radius,
|
|
655
|
-
start,
|
|
656
|
-
end,
|
|
657
|
-
sweep
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
return child_arcs;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
595
|
/**
|
|
667
596
|
* radialLayout(node, opts?)
|
|
668
597
|
* opts:
|
|
@@ -706,13 +635,8 @@ function radialLayout(node, opts = {}) {
|
|
|
706
635
|
? getArcsFan(pd)
|
|
707
636
|
: getArcs(pd);
|
|
708
637
|
|
|
709
|
-
// per-child arcs for
|
|
710
|
-
|
|
711
|
-
if (arcsStyle === "fan") {
|
|
712
|
-
child_arcs = getChildArcsFan(pd);
|
|
713
|
-
} else {
|
|
714
|
-
child_arcs = getChildArcs(pd);
|
|
715
|
-
}
|
|
638
|
+
// per-child arcs for path highlighting: always parent.angle → child.angle at parent.r
|
|
639
|
+
const child_arcs = getChildArcs(pd);
|
|
716
640
|
|
|
717
641
|
return { data: pd, radii, arcs, child_arcs };
|
|
718
642
|
}
|
|
@@ -1049,21 +973,29 @@ function readTree(text) {
|
|
|
1049
973
|
text = String(text).replace(/\s+/g, '');
|
|
1050
974
|
|
|
1051
975
|
const tokens = text.split(/(;|\(|\)|,)/);
|
|
1052
|
-
const root = { parent: null, children: [] };
|
|
1053
|
-
let curnode = root;
|
|
1054
976
|
let nodeId = 0;
|
|
977
|
+
const makeNode = (parent) => ({
|
|
978
|
+
parent,
|
|
979
|
+
children: [],
|
|
980
|
+
id: nodeId++,
|
|
981
|
+
label: '',
|
|
982
|
+
branchLength: null
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
const root = makeNode(null);
|
|
986
|
+
let curnode = root;
|
|
1055
987
|
|
|
1056
988
|
for (const token of tokens) {
|
|
1057
989
|
if (!token || token === ';') continue;
|
|
1058
990
|
|
|
1059
991
|
if (token === '(') {
|
|
1060
|
-
const child =
|
|
992
|
+
const child = makeNode(curnode);
|
|
1061
993
|
curnode.children.push(child);
|
|
1062
994
|
curnode = child; // descend
|
|
1063
995
|
} else if (token === ',') {
|
|
1064
996
|
// back to parent, then create sibling
|
|
1065
997
|
curnode = curnode.parent;
|
|
1066
|
-
const child =
|
|
998
|
+
const child = makeNode(curnode);
|
|
1067
999
|
curnode.children.push(child);
|
|
1068
1000
|
curnode = child;
|
|
1069
1001
|
} else if (token === ')') {
|
|
@@ -1072,14 +1004,15 @@ function readTree(text) {
|
|
|
1072
1004
|
if (curnode === null) break;
|
|
1073
1005
|
} else {
|
|
1074
1006
|
// label/branch-length chunk (e.g., "A:0.01" or "A")
|
|
1007
|
+
// Note: nodes are assigned an id at creation (above), so internal
|
|
1008
|
+
// (clade) nodes that carry neither a label nor a branch length —
|
|
1009
|
+
// e.g. "((A,B),(C,D));" — still get a valid, linkable id here.
|
|
1075
1010
|
const nodeinfo = token.split(':');
|
|
1076
1011
|
if (nodeinfo.length === 1) {
|
|
1077
1012
|
if (token.startsWith(':')) {
|
|
1078
|
-
curnode.label = '';
|
|
1079
1013
|
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
1080
1014
|
} else {
|
|
1081
1015
|
curnode.label = nodeinfo[0];
|
|
1082
|
-
curnode.branchLength = null;
|
|
1083
1016
|
}
|
|
1084
1017
|
} else if (nodeinfo.length === 2) {
|
|
1085
1018
|
curnode.label = nodeinfo[0];
|
|
@@ -1089,13 +1022,9 @@ function readTree(text) {
|
|
|
1089
1022
|
curnode.label = nodeinfo[0] || '';
|
|
1090
1023
|
curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
|
|
1091
1024
|
}
|
|
1092
|
-
curnode.id = nodeId++; // assign then increment
|
|
1093
1025
|
}
|
|
1094
1026
|
}
|
|
1095
1027
|
|
|
1096
|
-
// Ensure root has an id if not assigned during parsing
|
|
1097
|
-
if (root.id == null) root.id = nodeId;
|
|
1098
|
-
|
|
1099
1028
|
return root;
|
|
1100
1029
|
}
|
|
1101
1030
|
|
|
@@ -1155,6 +1084,7 @@ function drawPhylogeny(
|
|
|
1155
1084
|
strokeWidth = 1, // for the phylogeny branches
|
|
1156
1085
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
1157
1086
|
tipLabels = true,
|
|
1087
|
+
labelFontSize = 10, // font size (px) for tip labels
|
|
1158
1088
|
showTooltips = true,
|
|
1159
1089
|
tooltipFormatter = (d, rtt) =>
|
|
1160
1090
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1202,7 +1132,7 @@ function drawPhylogeny(
|
|
|
1202
1132
|
const tips = horizontal.filter((d) => d.isTip);
|
|
1203
1133
|
|
|
1204
1134
|
// indices & root→tip getter
|
|
1205
|
-
const byId = new Map(
|
|
1135
|
+
const byId = new Map(tree_df.data.map((d) => [d.thisId, d])); // includes root
|
|
1206
1136
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1207
1137
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1208
1138
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
@@ -1276,6 +1206,7 @@ function drawPhylogeny(
|
|
|
1276
1206
|
// interactive root→tip highlight (rect) on dot hover
|
|
1277
1207
|
tipDots
|
|
1278
1208
|
.on("mouseenter", function(_event, d) {
|
|
1209
|
+
hoverLayer.selectAll("*").remove();
|
|
1279
1210
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1280
1211
|
d3__namespace.select(this).attr("r", 4);
|
|
1281
1212
|
})
|
|
@@ -1295,7 +1226,7 @@ function drawPhylogeny(
|
|
|
1295
1226
|
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1296
1227
|
.attr("y", (d) => yScale(d.y1))
|
|
1297
1228
|
.attr("dy", "0.32em")
|
|
1298
|
-
.attr("font-size",
|
|
1229
|
+
.attr("font-size", labelFontSize)
|
|
1299
1230
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1300
1231
|
|
|
1301
1232
|
if (showTooltips) {
|
|
@@ -1306,6 +1237,7 @@ function drawPhylogeny(
|
|
|
1306
1237
|
|
|
1307
1238
|
labels
|
|
1308
1239
|
.on("mouseenter", function(_event, d) {
|
|
1240
|
+
hoverLayer.selectAll("*").remove();
|
|
1309
1241
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1310
1242
|
d3__namespace.select(this).attr("font-weight", 600);
|
|
1311
1243
|
})
|
|
@@ -1332,7 +1264,6 @@ function drawPhylogeny(
|
|
|
1332
1264
|
|
|
1333
1265
|
// helper to draw root→tip for rect (both vertical+horizontal)
|
|
1334
1266
|
function drawRectPath(tipId, layer, stroke, width) {
|
|
1335
|
-
layer.selectAll("*").remove();
|
|
1336
1267
|
let cur = byId.get(tipId);
|
|
1337
1268
|
while (cur && cur.parentId != null) {
|
|
1338
1269
|
const parent = byId.get(cur.parentId);
|
|
@@ -1389,10 +1320,14 @@ function drawPhylogeny(
|
|
|
1389
1320
|
const END_CAP = 0;
|
|
1390
1321
|
|
|
1391
1322
|
// ===== SCALES / BOUNDS =====
|
|
1392
|
-
const maxRadius = d3__namespace.max(rad.data, (d) => d.r) ?? 0;
|
|
1393
|
-
const scaleRadial = maxRadius + 2 * radialMargin;
|
|
1394
1323
|
const w = width,
|
|
1395
1324
|
h = height;
|
|
1325
|
+
const maxRadius = d3__namespace.max(rad.data, (d) => d.r) ?? 0;
|
|
1326
|
+
// radialMargin is in pixels: tips sit (radialMargin) px from the SVG edge.
|
|
1327
|
+
// Derive the data-space scale so that radiusPx(maxRadius) = w/2 - radialMargin.
|
|
1328
|
+
const scaleRadial = maxRadius > 0
|
|
1329
|
+
? maxRadius * (w / 2) / (w / 2 - radialMargin)
|
|
1330
|
+
: 1;
|
|
1396
1331
|
const centerX = w / 2,
|
|
1397
1332
|
centerY = h / 2;
|
|
1398
1333
|
|
|
@@ -1472,7 +1407,8 @@ function drawPhylogeny(
|
|
|
1472
1407
|
radiusPx(d.radius),
|
|
1473
1408
|
d.start,
|
|
1474
1409
|
d.end,
|
|
1475
|
-
d.sweep
|
|
1410
|
+
d.sweep ?? "ccw",
|
|
1411
|
+
d.largeArc ?? 0,
|
|
1476
1412
|
)
|
|
1477
1413
|
)
|
|
1478
1414
|
.attr("fill", "none")
|
|
@@ -1587,7 +1523,7 @@ function drawPhylogeny(
|
|
|
1587
1523
|
.attr("x", xoff)
|
|
1588
1524
|
.attr("alignment-baseline", "middle")
|
|
1589
1525
|
.attr("text-anchor", anchor)
|
|
1590
|
-
.attr("font-size",
|
|
1526
|
+
.attr("font-size", labelFontSize)
|
|
1591
1527
|
.attr("fill", "black")
|
|
1592
1528
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1593
1529
|
});
|
|
@@ -1601,6 +1537,8 @@ function drawPhylogeny(
|
|
|
1601
1537
|
// label hover
|
|
1602
1538
|
labels
|
|
1603
1539
|
.on("mouseenter", function(_event, d) {
|
|
1540
|
+
hoverLines.selectAll("*").remove();
|
|
1541
|
+
hoverArcs.selectAll("*").remove();
|
|
1604
1542
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1605
1543
|
d3__namespace.select(this).select("text").attr("font-weight", 600);
|
|
1606
1544
|
})
|
|
@@ -1620,9 +1558,6 @@ function drawPhylogeny(
|
|
|
1620
1558
|
width = 3
|
|
1621
1559
|
) {
|
|
1622
1560
|
// target may be a tip node *or* a numeric tip id
|
|
1623
|
-
lineLayer.selectAll("*").remove();
|
|
1624
|
-
arcLayer.selectAll("*").remove();
|
|
1625
|
-
|
|
1626
1561
|
let cur = (typeof target === "number" || typeof target === "string")
|
|
1627
1562
|
? byId.get(target)
|
|
1628
1563
|
: target;
|
|
@@ -1661,18 +1596,10 @@ function drawPhylogeny(
|
|
|
1661
1596
|
const R = radiusPx(rec.radius);
|
|
1662
1597
|
return rec.sweep == null
|
|
1663
1598
|
? describeArc(centerX, centerY, R, rec.start, rec.end)
|
|
1664
|
-
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
|
|
1599
|
+
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep ?? "ccw", rec.largeArc ?? 0);
|
|
1665
1600
|
}
|
|
1666
1601
|
|
|
1667
1602
|
if (a) {
|
|
1668
|
-
console.log("Drawing arc:", {
|
|
1669
|
-
childId: cur.thisId,
|
|
1670
|
-
startDeg: (a.start * 180 / Math.PI).toFixed(2),
|
|
1671
|
-
endDeg: (a.end * 180 / Math.PI).toFixed(2),
|
|
1672
|
-
sweep: a.sweep,
|
|
1673
|
-
radius: a.radius
|
|
1674
|
-
});
|
|
1675
|
-
|
|
1676
1603
|
arcLayer
|
|
1677
1604
|
.append("path")
|
|
1678
1605
|
.attr("d", pathFromArcRecord(a))
|
|
@@ -1689,6 +1616,8 @@ function drawPhylogeny(
|
|
|
1689
1616
|
// tip dot hover
|
|
1690
1617
|
tipDots
|
|
1691
1618
|
.on("mouseenter", function(_event, d) {
|
|
1619
|
+
hoverLines.selectAll("*").remove();
|
|
1620
|
+
hoverArcs.selectAll("*").remove();
|
|
1692
1621
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1693
1622
|
d3__namespace.select(this).attr("r", DOT_R + 2);
|
|
1694
1623
|
})
|
|
@@ -1845,7 +1774,7 @@ function drawPhylogeny(
|
|
|
1845
1774
|
.attr("x", xOffset)
|
|
1846
1775
|
.attr("alignment-baseline", "middle")
|
|
1847
1776
|
.attr("text-anchor", anchor)
|
|
1848
|
-
.attr("font-size",
|
|
1777
|
+
.attr("font-size", labelFontSize)
|
|
1849
1778
|
.attr("fill", "black")
|
|
1850
1779
|
.text(d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1851
1780
|
});
|