@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.js
CHANGED
|
@@ -144,8 +144,8 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
144
144
|
return `M ${p.x} ${p.y}`; // degenerate span → no arc
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
148
|
-
const sweepFlag = 0; // CCW
|
|
147
|
+
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
148
|
+
const sweepFlag = 0; // CCW in our y-flipped coords: math-CCW = decreasing SVG angle = sweepFlag 0
|
|
149
149
|
|
|
150
150
|
const p0 = polarToCartesian(cx, cy, radius, a0);
|
|
151
151
|
const p1 = polarToCartesian(cx, cy, radius, a1);
|
|
@@ -154,15 +154,17 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
// src/radial/describeArcSweep.js
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
// IMPORTANT: angles are in "math space" (increasing = CCW).
|
|
158
|
+
// Because we map y as (cy - r*sin(a)), our math angle t maps to SVG angle -t.
|
|
159
|
+
// Increasing t (math CCW) = decreasing SVG angle = sweepFlag 0 (negative direction).
|
|
160
|
+
// math CCW -> svg sweepFlag = 0
|
|
161
|
+
// math CW -> svg sweepFlag = 1
|
|
162
|
+
function describeArcSweep(
|
|
163
|
+
cx, cy, r,
|
|
164
|
+
a0, a1,
|
|
165
|
+
mathSweep = "ccw", // "ccw" | "cw"
|
|
166
|
+
largeArcFlag = 0
|
|
167
|
+
) {
|
|
166
168
|
if (!(r > 0)) return "";
|
|
167
169
|
|
|
168
170
|
const x0 = cx + r * Math.cos(a0);
|
|
@@ -170,7 +172,9 @@ function describeArcSweep(cx, cy, r, a0, a1, sweep = 1, largeArcFlag = 0) {
|
|
|
170
172
|
const x1 = cx + r * Math.cos(a1);
|
|
171
173
|
const y1 = cy - r * Math.sin(a1);
|
|
172
174
|
|
|
173
|
-
|
|
175
|
+
const svgSweepFlag = (mathSweep === "ccw") ? 0 : 1;
|
|
176
|
+
|
|
177
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${svgSweepFlag} ${x1} ${y1}`;
|
|
174
178
|
}
|
|
175
179
|
|
|
176
180
|
/**
|
|
@@ -447,16 +451,13 @@ function getArcs(pd) {
|
|
|
447
451
|
}
|
|
448
452
|
|
|
449
453
|
/**
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
* start = first child's angle
|
|
453
|
-
* end = last child's angle
|
|
454
|
-
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
455
|
-
*
|
|
456
|
-
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
457
|
-
* @returns {Array} [{parentId,thisId,radius,start,end,sweep}]
|
|
454
|
+
* APE-like block arcs per internal parent.
|
|
455
|
+
* Draw CCW from first child's angle to last child's angle (wrapping allowed).
|
|
458
456
|
*/
|
|
459
457
|
function getArcsFan(pd) {
|
|
458
|
+
const TAU = Math.PI * 2;
|
|
459
|
+
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
460
|
+
|
|
460
461
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
461
462
|
const arcs = [];
|
|
462
463
|
|
|
@@ -468,9 +469,11 @@ function getArcsFan(pd) {
|
|
|
468
469
|
const last = byId.get(c[c.length - 1])?.angle;
|
|
469
470
|
if (first == null || last == null) continue;
|
|
470
471
|
|
|
471
|
-
const start = first;
|
|
472
|
-
const end
|
|
473
|
-
|
|
472
|
+
const start = norm(first);
|
|
473
|
+
const end = norm(last);
|
|
474
|
+
|
|
475
|
+
const deltaCCW = (end - start + TAU) % TAU;
|
|
476
|
+
if (deltaCCW < 1e-9) continue;
|
|
474
477
|
|
|
475
478
|
arcs.push({
|
|
476
479
|
parentId: p.parentId,
|
|
@@ -478,9 +481,11 @@ function getArcsFan(pd) {
|
|
|
478
481
|
radius: p.r,
|
|
479
482
|
start,
|
|
480
483
|
end,
|
|
481
|
-
sweep
|
|
484
|
+
sweep: "ccw", // << math sweep
|
|
485
|
+
largeArc: deltaCCW > Math.PI ? 1 : 0
|
|
482
486
|
});
|
|
483
487
|
}
|
|
488
|
+
|
|
484
489
|
return arcs;
|
|
485
490
|
}
|
|
486
491
|
|
|
@@ -566,76 +571,6 @@ function getChildArcs(pd) {
|
|
|
566
571
|
return arcs;
|
|
567
572
|
}
|
|
568
573
|
|
|
569
|
-
function getChildArcsFan(pd) {
|
|
570
|
-
const TAU = Math.PI * 2;
|
|
571
|
-
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
572
|
-
|
|
573
|
-
function midCCW(a, b) {
|
|
574
|
-
const d = (b - a + TAU) % TAU;
|
|
575
|
-
return norm(a + d / 2);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const key = (x) => (typeof x === "string" ? +x : x);
|
|
579
|
-
const byId = new Map(pd.map(d => [key(d.thisId), d]));
|
|
580
|
-
const childrenByParent = new Map(
|
|
581
|
-
pd.map(d => [
|
|
582
|
-
key(d.thisId),
|
|
583
|
-
(d.children || [])
|
|
584
|
-
.map(ch => (typeof ch === "object" ? ch.thisId : ch))
|
|
585
|
-
.map(key)
|
|
586
|
-
.filter(id => byId.has(id))
|
|
587
|
-
])
|
|
588
|
-
);
|
|
589
|
-
|
|
590
|
-
const child_arcs = [];
|
|
591
|
-
|
|
592
|
-
for (const parentRaw of pd) {
|
|
593
|
-
const pid = key(parentRaw.thisId);
|
|
594
|
-
const kids = childrenByParent.get(pid) || [];
|
|
595
|
-
if (kids.length < 2) continue;
|
|
596
|
-
|
|
597
|
-
const A = kids
|
|
598
|
-
.map(id => {
|
|
599
|
-
const node = byId.get(id);
|
|
600
|
-
return node ? { id, a: norm(node.angle) } : null;
|
|
601
|
-
})
|
|
602
|
-
.filter(Boolean)
|
|
603
|
-
.sort((u, v) => u.a - v.a);
|
|
604
|
-
|
|
605
|
-
const N = A.length;
|
|
606
|
-
if (N < 2) continue;
|
|
607
|
-
|
|
608
|
-
const parent = byId.get(pid);
|
|
609
|
-
const radius = parent?.r;
|
|
610
|
-
if (!(radius > 0)) continue;
|
|
611
|
-
|
|
612
|
-
for (let i = 0; i < N; i++) {
|
|
613
|
-
const prev = A[(i - 1 + N) % N];
|
|
614
|
-
const cur = A[i];
|
|
615
|
-
const next = A[(i + 1) % N];
|
|
616
|
-
|
|
617
|
-
const start = midCCW(prev.a, cur.a);
|
|
618
|
-
const end = midCCW(cur.a, next.a);
|
|
619
|
-
|
|
620
|
-
const sweep = 1; // always clockwise
|
|
621
|
-
const delta = (end - start + TAU) % TAU;
|
|
622
|
-
const largeArc = delta > Math.PI ? 1 : 0;
|
|
623
|
-
|
|
624
|
-
child_arcs.push({
|
|
625
|
-
parentId: pid,
|
|
626
|
-
childId: cur.id,
|
|
627
|
-
radius,
|
|
628
|
-
start,
|
|
629
|
-
end,
|
|
630
|
-
sweep,
|
|
631
|
-
largeArc
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
return child_arcs;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
574
|
/**
|
|
640
575
|
* radialLayout(node, opts?)
|
|
641
576
|
* opts:
|
|
@@ -679,13 +614,8 @@ function radialLayout(node, opts = {}) {
|
|
|
679
614
|
? getArcsFan(pd)
|
|
680
615
|
: getArcs(pd);
|
|
681
616
|
|
|
682
|
-
// per-child arcs for
|
|
683
|
-
|
|
684
|
-
if (arcsStyle === "fan") {
|
|
685
|
-
child_arcs = getChildArcsFan(pd);
|
|
686
|
-
} else {
|
|
687
|
-
child_arcs = getChildArcs(pd);
|
|
688
|
-
}
|
|
617
|
+
// per-child arcs for path highlighting: always parent.angle → child.angle at parent.r
|
|
618
|
+
const child_arcs = getChildArcs(pd);
|
|
689
619
|
|
|
690
620
|
return { data: pd, radii, arcs, child_arcs };
|
|
691
621
|
}
|
|
@@ -1022,21 +952,29 @@ function readTree(text) {
|
|
|
1022
952
|
text = String(text).replace(/\s+/g, '');
|
|
1023
953
|
|
|
1024
954
|
const tokens = text.split(/(;|\(|\)|,)/);
|
|
1025
|
-
const root = { parent: null, children: [] };
|
|
1026
|
-
let curnode = root;
|
|
1027
955
|
let nodeId = 0;
|
|
956
|
+
const makeNode = (parent) => ({
|
|
957
|
+
parent,
|
|
958
|
+
children: [],
|
|
959
|
+
id: nodeId++,
|
|
960
|
+
label: '',
|
|
961
|
+
branchLength: null
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
const root = makeNode(null);
|
|
965
|
+
let curnode = root;
|
|
1028
966
|
|
|
1029
967
|
for (const token of tokens) {
|
|
1030
968
|
if (!token || token === ';') continue;
|
|
1031
969
|
|
|
1032
970
|
if (token === '(') {
|
|
1033
|
-
const child =
|
|
971
|
+
const child = makeNode(curnode);
|
|
1034
972
|
curnode.children.push(child);
|
|
1035
973
|
curnode = child; // descend
|
|
1036
974
|
} else if (token === ',') {
|
|
1037
975
|
// back to parent, then create sibling
|
|
1038
976
|
curnode = curnode.parent;
|
|
1039
|
-
const child =
|
|
977
|
+
const child = makeNode(curnode);
|
|
1040
978
|
curnode.children.push(child);
|
|
1041
979
|
curnode = child;
|
|
1042
980
|
} else if (token === ')') {
|
|
@@ -1045,14 +983,15 @@ function readTree(text) {
|
|
|
1045
983
|
if (curnode === null) break;
|
|
1046
984
|
} else {
|
|
1047
985
|
// label/branch-length chunk (e.g., "A:0.01" or "A")
|
|
986
|
+
// Note: nodes are assigned an id at creation (above), so internal
|
|
987
|
+
// (clade) nodes that carry neither a label nor a branch length —
|
|
988
|
+
// e.g. "((A,B),(C,D));" — still get a valid, linkable id here.
|
|
1048
989
|
const nodeinfo = token.split(':');
|
|
1049
990
|
if (nodeinfo.length === 1) {
|
|
1050
991
|
if (token.startsWith(':')) {
|
|
1051
|
-
curnode.label = '';
|
|
1052
992
|
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
1053
993
|
} else {
|
|
1054
994
|
curnode.label = nodeinfo[0];
|
|
1055
|
-
curnode.branchLength = null;
|
|
1056
995
|
}
|
|
1057
996
|
} else if (nodeinfo.length === 2) {
|
|
1058
997
|
curnode.label = nodeinfo[0];
|
|
@@ -1062,13 +1001,9 @@ function readTree(text) {
|
|
|
1062
1001
|
curnode.label = nodeinfo[0] || '';
|
|
1063
1002
|
curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
|
|
1064
1003
|
}
|
|
1065
|
-
curnode.id = nodeId++; // assign then increment
|
|
1066
1004
|
}
|
|
1067
1005
|
}
|
|
1068
1006
|
|
|
1069
|
-
// Ensure root has an id if not assigned during parsing
|
|
1070
|
-
if (root.id == null) root.id = nodeId;
|
|
1071
|
-
|
|
1072
1007
|
return root;
|
|
1073
1008
|
}
|
|
1074
1009
|
|
|
@@ -1128,6 +1063,7 @@ function drawPhylogeny(
|
|
|
1128
1063
|
strokeWidth = 1, // for the phylogeny branches
|
|
1129
1064
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
1130
1065
|
tipLabels = true,
|
|
1066
|
+
labelFontSize = 10, // font size (px) for tip labels
|
|
1131
1067
|
showTooltips = true,
|
|
1132
1068
|
tooltipFormatter = (d, rtt) =>
|
|
1133
1069
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1175,7 +1111,7 @@ function drawPhylogeny(
|
|
|
1175
1111
|
const tips = horizontal.filter((d) => d.isTip);
|
|
1176
1112
|
|
|
1177
1113
|
// indices & root→tip getter
|
|
1178
|
-
const byId = new Map(
|
|
1114
|
+
const byId = new Map(tree_df.data.map((d) => [d.thisId, d])); // includes root
|
|
1179
1115
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1180
1116
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1181
1117
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
@@ -1249,6 +1185,7 @@ function drawPhylogeny(
|
|
|
1249
1185
|
// interactive root→tip highlight (rect) on dot hover
|
|
1250
1186
|
tipDots
|
|
1251
1187
|
.on("mouseenter", function(_event, d) {
|
|
1188
|
+
hoverLayer.selectAll("*").remove();
|
|
1252
1189
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1253
1190
|
d3.select(this).attr("r", 4);
|
|
1254
1191
|
})
|
|
@@ -1268,7 +1205,7 @@ function drawPhylogeny(
|
|
|
1268
1205
|
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1269
1206
|
.attr("y", (d) => yScale(d.y1))
|
|
1270
1207
|
.attr("dy", "0.32em")
|
|
1271
|
-
.attr("font-size",
|
|
1208
|
+
.attr("font-size", labelFontSize)
|
|
1272
1209
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1273
1210
|
|
|
1274
1211
|
if (showTooltips) {
|
|
@@ -1279,6 +1216,7 @@ function drawPhylogeny(
|
|
|
1279
1216
|
|
|
1280
1217
|
labels
|
|
1281
1218
|
.on("mouseenter", function(_event, d) {
|
|
1219
|
+
hoverLayer.selectAll("*").remove();
|
|
1282
1220
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1283
1221
|
d3.select(this).attr("font-weight", 600);
|
|
1284
1222
|
})
|
|
@@ -1305,7 +1243,6 @@ function drawPhylogeny(
|
|
|
1305
1243
|
|
|
1306
1244
|
// helper to draw root→tip for rect (both vertical+horizontal)
|
|
1307
1245
|
function drawRectPath(tipId, layer, stroke, width) {
|
|
1308
|
-
layer.selectAll("*").remove();
|
|
1309
1246
|
let cur = byId.get(tipId);
|
|
1310
1247
|
while (cur && cur.parentId != null) {
|
|
1311
1248
|
const parent = byId.get(cur.parentId);
|
|
@@ -1362,10 +1299,14 @@ function drawPhylogeny(
|
|
|
1362
1299
|
const END_CAP = 0;
|
|
1363
1300
|
|
|
1364
1301
|
// ===== SCALES / BOUNDS =====
|
|
1365
|
-
const maxRadius = d3.max(rad.data, (d) => d.r) ?? 0;
|
|
1366
|
-
const scaleRadial = maxRadius + 2 * radialMargin;
|
|
1367
1302
|
const w = width,
|
|
1368
1303
|
h = height;
|
|
1304
|
+
const maxRadius = d3.max(rad.data, (d) => d.r) ?? 0;
|
|
1305
|
+
// radialMargin is in pixels: tips sit (radialMargin) px from the SVG edge.
|
|
1306
|
+
// Derive the data-space scale so that radiusPx(maxRadius) = w/2 - radialMargin.
|
|
1307
|
+
const scaleRadial = maxRadius > 0
|
|
1308
|
+
? maxRadius * (w / 2) / (w / 2 - radialMargin)
|
|
1309
|
+
: 1;
|
|
1369
1310
|
const centerX = w / 2,
|
|
1370
1311
|
centerY = h / 2;
|
|
1371
1312
|
|
|
@@ -1445,8 +1386,8 @@ function drawPhylogeny(
|
|
|
1445
1386
|
radiusPx(d.radius),
|
|
1446
1387
|
d.start,
|
|
1447
1388
|
d.end,
|
|
1448
|
-
d.sweep,
|
|
1449
|
-
d.largeArc,
|
|
1389
|
+
d.sweep ?? "ccw",
|
|
1390
|
+
d.largeArc ?? 0,
|
|
1450
1391
|
)
|
|
1451
1392
|
)
|
|
1452
1393
|
.attr("fill", "none")
|
|
@@ -1561,7 +1502,7 @@ function drawPhylogeny(
|
|
|
1561
1502
|
.attr("x", xoff)
|
|
1562
1503
|
.attr("alignment-baseline", "middle")
|
|
1563
1504
|
.attr("text-anchor", anchor)
|
|
1564
|
-
.attr("font-size",
|
|
1505
|
+
.attr("font-size", labelFontSize)
|
|
1565
1506
|
.attr("fill", "black")
|
|
1566
1507
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1567
1508
|
});
|
|
@@ -1575,6 +1516,8 @@ function drawPhylogeny(
|
|
|
1575
1516
|
// label hover
|
|
1576
1517
|
labels
|
|
1577
1518
|
.on("mouseenter", function(_event, d) {
|
|
1519
|
+
hoverLines.selectAll("*").remove();
|
|
1520
|
+
hoverArcs.selectAll("*").remove();
|
|
1578
1521
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1579
1522
|
d3.select(this).select("text").attr("font-weight", 600);
|
|
1580
1523
|
})
|
|
@@ -1594,9 +1537,6 @@ function drawPhylogeny(
|
|
|
1594
1537
|
width = 3
|
|
1595
1538
|
) {
|
|
1596
1539
|
// target may be a tip node *or* a numeric tip id
|
|
1597
|
-
lineLayer.selectAll("*").remove();
|
|
1598
|
-
arcLayer.selectAll("*").remove();
|
|
1599
|
-
|
|
1600
1540
|
let cur = (typeof target === "number" || typeof target === "string")
|
|
1601
1541
|
? byId.get(target)
|
|
1602
1542
|
: target;
|
|
@@ -1635,18 +1575,10 @@ function drawPhylogeny(
|
|
|
1635
1575
|
const R = radiusPx(rec.radius);
|
|
1636
1576
|
return rec.sweep == null
|
|
1637
1577
|
? describeArc(centerX, centerY, R, rec.start, rec.end)
|
|
1638
|
-
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep,
|
|
1578
|
+
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep ?? "ccw", rec.largeArc ?? 0);
|
|
1639
1579
|
}
|
|
1640
1580
|
|
|
1641
1581
|
if (a) {
|
|
1642
|
-
console.log("Drawing arc:", {
|
|
1643
|
-
childId: cur.thisId,
|
|
1644
|
-
startDeg: (a.start * 180 / Math.PI).toFixed(2),
|
|
1645
|
-
endDeg: (a.end * 180 / Math.PI).toFixed(2),
|
|
1646
|
-
sweep: a.sweep,
|
|
1647
|
-
radius: a.radius
|
|
1648
|
-
});
|
|
1649
|
-
|
|
1650
1582
|
arcLayer
|
|
1651
1583
|
.append("path")
|
|
1652
1584
|
.attr("d", pathFromArcRecord(a))
|
|
@@ -1663,6 +1595,8 @@ function drawPhylogeny(
|
|
|
1663
1595
|
// tip dot hover
|
|
1664
1596
|
tipDots
|
|
1665
1597
|
.on("mouseenter", function(_event, d) {
|
|
1598
|
+
hoverLines.selectAll("*").remove();
|
|
1599
|
+
hoverArcs.selectAll("*").remove();
|
|
1666
1600
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1667
1601
|
d3.select(this).attr("r", DOT_R + 2);
|
|
1668
1602
|
})
|
|
@@ -1819,7 +1753,7 @@ function drawPhylogeny(
|
|
|
1819
1753
|
.attr("x", xOffset)
|
|
1820
1754
|
.attr("alignment-baseline", "middle")
|
|
1821
1755
|
.attr("text-anchor", anchor)
|
|
1822
|
-
.attr("font-size",
|
|
1756
|
+
.attr("font-size", labelFontSize)
|
|
1823
1757
|
.attr("fill", "black")
|
|
1824
1758
|
.text(d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1825
1759
|
});
|