@euphrasiologist/lwphylo 1.2.24 → 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 +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 +66 -132
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +66 -132
- 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
|
}
|
|
@@ -1043,21 +973,29 @@ function readTree(text) {
|
|
|
1043
973
|
text = String(text).replace(/\s+/g, '');
|
|
1044
974
|
|
|
1045
975
|
const tokens = text.split(/(;|\(|\)|,)/);
|
|
1046
|
-
const root = { parent: null, children: [] };
|
|
1047
|
-
let curnode = root;
|
|
1048
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;
|
|
1049
987
|
|
|
1050
988
|
for (const token of tokens) {
|
|
1051
989
|
if (!token || token === ';') continue;
|
|
1052
990
|
|
|
1053
991
|
if (token === '(') {
|
|
1054
|
-
const child =
|
|
992
|
+
const child = makeNode(curnode);
|
|
1055
993
|
curnode.children.push(child);
|
|
1056
994
|
curnode = child; // descend
|
|
1057
995
|
} else if (token === ',') {
|
|
1058
996
|
// back to parent, then create sibling
|
|
1059
997
|
curnode = curnode.parent;
|
|
1060
|
-
const child =
|
|
998
|
+
const child = makeNode(curnode);
|
|
1061
999
|
curnode.children.push(child);
|
|
1062
1000
|
curnode = child;
|
|
1063
1001
|
} else if (token === ')') {
|
|
@@ -1066,14 +1004,15 @@ function readTree(text) {
|
|
|
1066
1004
|
if (curnode === null) break;
|
|
1067
1005
|
} else {
|
|
1068
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.
|
|
1069
1010
|
const nodeinfo = token.split(':');
|
|
1070
1011
|
if (nodeinfo.length === 1) {
|
|
1071
1012
|
if (token.startsWith(':')) {
|
|
1072
|
-
curnode.label = '';
|
|
1073
1013
|
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
1074
1014
|
} else {
|
|
1075
1015
|
curnode.label = nodeinfo[0];
|
|
1076
|
-
curnode.branchLength = null;
|
|
1077
1016
|
}
|
|
1078
1017
|
} else if (nodeinfo.length === 2) {
|
|
1079
1018
|
curnode.label = nodeinfo[0];
|
|
@@ -1083,13 +1022,9 @@ function readTree(text) {
|
|
|
1083
1022
|
curnode.label = nodeinfo[0] || '';
|
|
1084
1023
|
curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
|
|
1085
1024
|
}
|
|
1086
|
-
curnode.id = nodeId++; // assign then increment
|
|
1087
1025
|
}
|
|
1088
1026
|
}
|
|
1089
1027
|
|
|
1090
|
-
// Ensure root has an id if not assigned during parsing
|
|
1091
|
-
if (root.id == null) root.id = nodeId;
|
|
1092
|
-
|
|
1093
1028
|
return root;
|
|
1094
1029
|
}
|
|
1095
1030
|
|
|
@@ -1149,6 +1084,7 @@ function drawPhylogeny(
|
|
|
1149
1084
|
strokeWidth = 1, // for the phylogeny branches
|
|
1150
1085
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
1151
1086
|
tipLabels = true,
|
|
1087
|
+
labelFontSize = 10, // font size (px) for tip labels
|
|
1152
1088
|
showTooltips = true,
|
|
1153
1089
|
tooltipFormatter = (d, rtt) =>
|
|
1154
1090
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1196,7 +1132,7 @@ function drawPhylogeny(
|
|
|
1196
1132
|
const tips = horizontal.filter((d) => d.isTip);
|
|
1197
1133
|
|
|
1198
1134
|
// indices & root→tip getter
|
|
1199
|
-
const byId = new Map(
|
|
1135
|
+
const byId = new Map(tree_df.data.map((d) => [d.thisId, d])); // includes root
|
|
1200
1136
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1201
1137
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1202
1138
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
@@ -1270,6 +1206,7 @@ function drawPhylogeny(
|
|
|
1270
1206
|
// interactive root→tip highlight (rect) on dot hover
|
|
1271
1207
|
tipDots
|
|
1272
1208
|
.on("mouseenter", function(_event, d) {
|
|
1209
|
+
hoverLayer.selectAll("*").remove();
|
|
1273
1210
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1274
1211
|
d3__namespace.select(this).attr("r", 4);
|
|
1275
1212
|
})
|
|
@@ -1289,7 +1226,7 @@ function drawPhylogeny(
|
|
|
1289
1226
|
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1290
1227
|
.attr("y", (d) => yScale(d.y1))
|
|
1291
1228
|
.attr("dy", "0.32em")
|
|
1292
|
-
.attr("font-size",
|
|
1229
|
+
.attr("font-size", labelFontSize)
|
|
1293
1230
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1294
1231
|
|
|
1295
1232
|
if (showTooltips) {
|
|
@@ -1300,6 +1237,7 @@ function drawPhylogeny(
|
|
|
1300
1237
|
|
|
1301
1238
|
labels
|
|
1302
1239
|
.on("mouseenter", function(_event, d) {
|
|
1240
|
+
hoverLayer.selectAll("*").remove();
|
|
1303
1241
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1304
1242
|
d3__namespace.select(this).attr("font-weight", 600);
|
|
1305
1243
|
})
|
|
@@ -1326,7 +1264,6 @@ function drawPhylogeny(
|
|
|
1326
1264
|
|
|
1327
1265
|
// helper to draw root→tip for rect (both vertical+horizontal)
|
|
1328
1266
|
function drawRectPath(tipId, layer, stroke, width) {
|
|
1329
|
-
layer.selectAll("*").remove();
|
|
1330
1267
|
let cur = byId.get(tipId);
|
|
1331
1268
|
while (cur && cur.parentId != null) {
|
|
1332
1269
|
const parent = byId.get(cur.parentId);
|
|
@@ -1383,10 +1320,14 @@ function drawPhylogeny(
|
|
|
1383
1320
|
const END_CAP = 0;
|
|
1384
1321
|
|
|
1385
1322
|
// ===== SCALES / BOUNDS =====
|
|
1386
|
-
const maxRadius = d3__namespace.max(rad.data, (d) => d.r) ?? 0;
|
|
1387
|
-
const scaleRadial = maxRadius + 2 * radialMargin;
|
|
1388
1323
|
const w = width,
|
|
1389
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;
|
|
1390
1331
|
const centerX = w / 2,
|
|
1391
1332
|
centerY = h / 2;
|
|
1392
1333
|
|
|
@@ -1466,8 +1407,8 @@ function drawPhylogeny(
|
|
|
1466
1407
|
radiusPx(d.radius),
|
|
1467
1408
|
d.start,
|
|
1468
1409
|
d.end,
|
|
1469
|
-
d.sweep,
|
|
1470
|
-
d.largeArc,
|
|
1410
|
+
d.sweep ?? "ccw",
|
|
1411
|
+
d.largeArc ?? 0,
|
|
1471
1412
|
)
|
|
1472
1413
|
)
|
|
1473
1414
|
.attr("fill", "none")
|
|
@@ -1582,7 +1523,7 @@ function drawPhylogeny(
|
|
|
1582
1523
|
.attr("x", xoff)
|
|
1583
1524
|
.attr("alignment-baseline", "middle")
|
|
1584
1525
|
.attr("text-anchor", anchor)
|
|
1585
|
-
.attr("font-size",
|
|
1526
|
+
.attr("font-size", labelFontSize)
|
|
1586
1527
|
.attr("fill", "black")
|
|
1587
1528
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1588
1529
|
});
|
|
@@ -1596,6 +1537,8 @@ function drawPhylogeny(
|
|
|
1596
1537
|
// label hover
|
|
1597
1538
|
labels
|
|
1598
1539
|
.on("mouseenter", function(_event, d) {
|
|
1540
|
+
hoverLines.selectAll("*").remove();
|
|
1541
|
+
hoverArcs.selectAll("*").remove();
|
|
1599
1542
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1600
1543
|
d3__namespace.select(this).select("text").attr("font-weight", 600);
|
|
1601
1544
|
})
|
|
@@ -1615,9 +1558,6 @@ function drawPhylogeny(
|
|
|
1615
1558
|
width = 3
|
|
1616
1559
|
) {
|
|
1617
1560
|
// target may be a tip node *or* a numeric tip id
|
|
1618
|
-
lineLayer.selectAll("*").remove();
|
|
1619
|
-
arcLayer.selectAll("*").remove();
|
|
1620
|
-
|
|
1621
1561
|
let cur = (typeof target === "number" || typeof target === "string")
|
|
1622
1562
|
? byId.get(target)
|
|
1623
1563
|
: target;
|
|
@@ -1656,18 +1596,10 @@ function drawPhylogeny(
|
|
|
1656
1596
|
const R = radiusPx(rec.radius);
|
|
1657
1597
|
return rec.sweep == null
|
|
1658
1598
|
? describeArc(centerX, centerY, R, rec.start, rec.end)
|
|
1659
|
-
: 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);
|
|
1660
1600
|
}
|
|
1661
1601
|
|
|
1662
1602
|
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
1603
|
arcLayer
|
|
1672
1604
|
.append("path")
|
|
1673
1605
|
.attr("d", pathFromArcRecord(a))
|
|
@@ -1684,6 +1616,8 @@ function drawPhylogeny(
|
|
|
1684
1616
|
// tip dot hover
|
|
1685
1617
|
tipDots
|
|
1686
1618
|
.on("mouseenter", function(_event, d) {
|
|
1619
|
+
hoverLines.selectAll("*").remove();
|
|
1620
|
+
hoverArcs.selectAll("*").remove();
|
|
1687
1621
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1688
1622
|
d3__namespace.select(this).attr("r", DOT_R + 2);
|
|
1689
1623
|
})
|
|
@@ -1840,7 +1774,7 @@ function drawPhylogeny(
|
|
|
1840
1774
|
.attr("x", xOffset)
|
|
1841
1775
|
.attr("alignment-baseline", "middle")
|
|
1842
1776
|
.attr("text-anchor", anchor)
|
|
1843
|
-
.attr("font-size",
|
|
1777
|
+
.attr("font-size", labelFontSize)
|
|
1844
1778
|
.attr("fill", "black")
|
|
1845
1779
|
.text(d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1846
1780
|
});
|