@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/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,25 +62,27 @@ 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
|
-
|
|
74
|
-
|
|
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
|
+
) {
|
|
76
|
+
if (!(r > 0)) return "";
|
|
75
77
|
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const
|
|
78
|
+
const x0 = cx + r * Math.cos(a0);
|
|
79
|
+
const y0 = cy - r * Math.sin(a0);
|
|
80
|
+
const x1 = cx + r * Math.cos(a1);
|
|
81
|
+
const y1 = cy - r * Math.sin(a1);
|
|
79
82
|
|
|
80
|
-
const
|
|
81
|
-
const x1 = cx + r * Math.cos(a1), y1 = cy - r * Math.sin(a1);
|
|
83
|
+
const svgSweepFlag = (mathSweep === "ccw") ? 0 : 1;
|
|
82
84
|
|
|
83
|
-
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${
|
|
85
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${svgSweepFlag} ${x1} ${y1}`;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
/**
|
|
@@ -357,16 +359,13 @@ function getArcs(pd) {
|
|
|
357
359
|
}
|
|
358
360
|
|
|
359
361
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
* start = first child's angle
|
|
363
|
-
* end = last child's angle
|
|
364
|
-
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
365
|
-
*
|
|
366
|
-
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
367
|
-
* @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).
|
|
368
364
|
*/
|
|
369
365
|
function getArcsFan(pd) {
|
|
366
|
+
const TAU = Math.PI * 2;
|
|
367
|
+
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
368
|
+
|
|
370
369
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
371
370
|
const arcs = [];
|
|
372
371
|
|
|
@@ -378,9 +377,11 @@ function getArcsFan(pd) {
|
|
|
378
377
|
const last = byId.get(c[c.length - 1])?.angle;
|
|
379
378
|
if (first == null || last == null) continue;
|
|
380
379
|
|
|
381
|
-
const start = first;
|
|
382
|
-
const end
|
|
383
|
-
|
|
380
|
+
const start = norm(first);
|
|
381
|
+
const end = norm(last);
|
|
382
|
+
|
|
383
|
+
const deltaCCW = (end - start + TAU) % TAU;
|
|
384
|
+
if (deltaCCW < 1e-9) continue;
|
|
384
385
|
|
|
385
386
|
arcs.push({
|
|
386
387
|
parentId: p.parentId,
|
|
@@ -388,9 +389,11 @@ function getArcsFan(pd) {
|
|
|
388
389
|
radius: p.r,
|
|
389
390
|
start,
|
|
390
391
|
end,
|
|
391
|
-
sweep
|
|
392
|
+
sweep: "ccw", // << math sweep
|
|
393
|
+
largeArc: deltaCCW > Math.PI ? 1 : 0
|
|
392
394
|
});
|
|
393
395
|
}
|
|
396
|
+
|
|
394
397
|
return arcs;
|
|
395
398
|
}
|
|
396
399
|
|
|
@@ -476,80 +479,6 @@ function getChildArcs(pd) {
|
|
|
476
479
|
return arcs;
|
|
477
480
|
}
|
|
478
481
|
|
|
479
|
-
function getChildArcsFan(pd) {
|
|
480
|
-
const TAU = Math.PI * 2;
|
|
481
|
-
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
482
|
-
|
|
483
|
-
// Circular midpoint that travels CCW from a -> b by half the CCW span
|
|
484
|
-
function midCCW(a, b) {
|
|
485
|
-
const d = (b - a + TAU) % TAU; // CCW delta in [0, 2π)
|
|
486
|
-
return norm(a + d / 2);
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const key = (x) => (typeof x === "string" ? +x : x);
|
|
490
|
-
|
|
491
|
-
const byId = new Map(pd.map(d => [key(d.thisId), d]));
|
|
492
|
-
const childrenByParent = new Map(
|
|
493
|
-
pd.map(d => [
|
|
494
|
-
key(d.thisId),
|
|
495
|
-
// normalize children to numeric IDs; drop anything we can't resolve
|
|
496
|
-
(d.children || [])
|
|
497
|
-
.map(ch => (typeof ch === "object" ? ch.thisId : ch))
|
|
498
|
-
.map(key)
|
|
499
|
-
.filter(id => byId.has(id))
|
|
500
|
-
])
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
const child_arcs = [];
|
|
504
|
-
|
|
505
|
-
for (const parentRaw of pd) {
|
|
506
|
-
const pid = key(parentRaw.thisId);
|
|
507
|
-
const kids = childrenByParent.get(pid) || [];
|
|
508
|
-
if (kids.length < 2) continue;
|
|
509
|
-
|
|
510
|
-
// Sort children by angle (normalized) around the circle
|
|
511
|
-
const A = kids
|
|
512
|
-
.map(id => {
|
|
513
|
-
const node = byId.get(id);
|
|
514
|
-
return node ? { id, a: norm(node.angle) } : null;
|
|
515
|
-
})
|
|
516
|
-
.filter(Boolean)
|
|
517
|
-
.sort((u, v) => u.a - v.a);
|
|
518
|
-
|
|
519
|
-
const N = A.length;
|
|
520
|
-
if (N < 2) continue;
|
|
521
|
-
|
|
522
|
-
const parent = byId.get(pid);
|
|
523
|
-
const radius = parent?.r;
|
|
524
|
-
if (!(radius > 0)) continue;
|
|
525
|
-
|
|
526
|
-
for (let i = 0; i < N; i++) {
|
|
527
|
-
const prev = A[(i - 1 + N) % N];
|
|
528
|
-
const cur = A[i];
|
|
529
|
-
const next = A[(i + 1) % N];
|
|
530
|
-
|
|
531
|
-
const start = midCCW(prev.a, cur.a);
|
|
532
|
-
const end = midCCW(cur.a, next.a);
|
|
533
|
-
|
|
534
|
-
// Use SVG-conforming sweep logic
|
|
535
|
-
const delta = (end - start + TAU) % TAU;
|
|
536
|
-
const sweep = delta > Math.PI ? 0 : 1;
|
|
537
|
-
|
|
538
|
-
child_arcs.push({
|
|
539
|
-
parentId: pid,
|
|
540
|
-
childId: cur.id,
|
|
541
|
-
radius,
|
|
542
|
-
start,
|
|
543
|
-
end,
|
|
544
|
-
sweep
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
return child_arcs;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
482
|
/**
|
|
554
483
|
* radialLayout(node, opts?)
|
|
555
484
|
* opts:
|
|
@@ -593,13 +522,8 @@ function radialLayout(node, opts = {}) {
|
|
|
593
522
|
? getArcsFan(pd)
|
|
594
523
|
: getArcs(pd);
|
|
595
524
|
|
|
596
|
-
// per-child arcs for
|
|
597
|
-
|
|
598
|
-
if (arcsStyle === "fan") {
|
|
599
|
-
child_arcs = getChildArcsFan(pd);
|
|
600
|
-
} else {
|
|
601
|
-
child_arcs = getChildArcs(pd);
|
|
602
|
-
}
|
|
525
|
+
// per-child arcs for path highlighting: always parent.angle → child.angle at parent.r
|
|
526
|
+
const child_arcs = getChildArcs(pd);
|
|
603
527
|
|
|
604
528
|
return { data: pd, radii, arcs, child_arcs };
|
|
605
529
|
}
|
|
@@ -927,21 +851,29 @@ function readTree(text) {
|
|
|
927
851
|
text = String(text).replace(/\s+/g, '');
|
|
928
852
|
|
|
929
853
|
const tokens = text.split(/(;|\(|\)|,)/);
|
|
930
|
-
const root = { parent: null, children: [] };
|
|
931
|
-
let curnode = root;
|
|
932
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;
|
|
933
865
|
|
|
934
866
|
for (const token of tokens) {
|
|
935
867
|
if (!token || token === ';') continue;
|
|
936
868
|
|
|
937
869
|
if (token === '(') {
|
|
938
|
-
const child =
|
|
870
|
+
const child = makeNode(curnode);
|
|
939
871
|
curnode.children.push(child);
|
|
940
872
|
curnode = child; // descend
|
|
941
873
|
} else if (token === ',') {
|
|
942
874
|
// back to parent, then create sibling
|
|
943
875
|
curnode = curnode.parent;
|
|
944
|
-
const child =
|
|
876
|
+
const child = makeNode(curnode);
|
|
945
877
|
curnode.children.push(child);
|
|
946
878
|
curnode = child;
|
|
947
879
|
} else if (token === ')') {
|
|
@@ -950,14 +882,15 @@ function readTree(text) {
|
|
|
950
882
|
if (curnode === null) break;
|
|
951
883
|
} else {
|
|
952
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.
|
|
953
888
|
const nodeinfo = token.split(':');
|
|
954
889
|
if (nodeinfo.length === 1) {
|
|
955
890
|
if (token.startsWith(':')) {
|
|
956
|
-
curnode.label = '';
|
|
957
891
|
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
958
892
|
} else {
|
|
959
893
|
curnode.label = nodeinfo[0];
|
|
960
|
-
curnode.branchLength = null;
|
|
961
894
|
}
|
|
962
895
|
} else if (nodeinfo.length === 2) {
|
|
963
896
|
curnode.label = nodeinfo[0];
|
|
@@ -967,13 +900,9 @@ function readTree(text) {
|
|
|
967
900
|
curnode.label = nodeinfo[0] || '';
|
|
968
901
|
curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
|
|
969
902
|
}
|
|
970
|
-
curnode.id = nodeId++; // assign then increment
|
|
971
903
|
}
|
|
972
904
|
}
|
|
973
905
|
|
|
974
|
-
// Ensure root has an id if not assigned during parsing
|
|
975
|
-
if (root.id == null) root.id = nodeId;
|
|
976
|
-
|
|
977
906
|
return root;
|
|
978
907
|
}
|
|
979
908
|
|
|
@@ -988,6 +917,7 @@ function drawPhylogeny(
|
|
|
988
917
|
strokeWidth = 1, // for the phylogeny branches
|
|
989
918
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
990
919
|
tipLabels = true,
|
|
920
|
+
labelFontSize = 10, // font size (px) for tip labels
|
|
991
921
|
showTooltips = true,
|
|
992
922
|
tooltipFormatter = (d, rtt) =>
|
|
993
923
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1035,7 +965,7 @@ function drawPhylogeny(
|
|
|
1035
965
|
const tips = horizontal.filter((d) => d.isTip);
|
|
1036
966
|
|
|
1037
967
|
// indices & root→tip getter
|
|
1038
|
-
const byId = new Map(
|
|
968
|
+
const byId = new Map(tree_df.data.map((d) => [d.thisId, d])); // includes root
|
|
1039
969
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1040
970
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1041
971
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
@@ -1109,6 +1039,7 @@ function drawPhylogeny(
|
|
|
1109
1039
|
// interactive root→tip highlight (rect) on dot hover
|
|
1110
1040
|
tipDots
|
|
1111
1041
|
.on("mouseenter", function(_event, d) {
|
|
1042
|
+
hoverLayer.selectAll("*").remove();
|
|
1112
1043
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1113
1044
|
d3__namespace.select(this).attr("r", 4);
|
|
1114
1045
|
})
|
|
@@ -1128,7 +1059,7 @@ function drawPhylogeny(
|
|
|
1128
1059
|
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1129
1060
|
.attr("y", (d) => yScale(d.y1))
|
|
1130
1061
|
.attr("dy", "0.32em")
|
|
1131
|
-
.attr("font-size",
|
|
1062
|
+
.attr("font-size", labelFontSize)
|
|
1132
1063
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1133
1064
|
|
|
1134
1065
|
if (showTooltips) {
|
|
@@ -1139,6 +1070,7 @@ function drawPhylogeny(
|
|
|
1139
1070
|
|
|
1140
1071
|
labels
|
|
1141
1072
|
.on("mouseenter", function(_event, d) {
|
|
1073
|
+
hoverLayer.selectAll("*").remove();
|
|
1142
1074
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1143
1075
|
d3__namespace.select(this).attr("font-weight", 600);
|
|
1144
1076
|
})
|
|
@@ -1165,7 +1097,6 @@ function drawPhylogeny(
|
|
|
1165
1097
|
|
|
1166
1098
|
// helper to draw root→tip for rect (both vertical+horizontal)
|
|
1167
1099
|
function drawRectPath(tipId, layer, stroke, width) {
|
|
1168
|
-
layer.selectAll("*").remove();
|
|
1169
1100
|
let cur = byId.get(tipId);
|
|
1170
1101
|
while (cur && cur.parentId != null) {
|
|
1171
1102
|
const parent = byId.get(cur.parentId);
|
|
@@ -1222,10 +1153,14 @@ function drawPhylogeny(
|
|
|
1222
1153
|
const END_CAP = 0;
|
|
1223
1154
|
|
|
1224
1155
|
// ===== SCALES / BOUNDS =====
|
|
1225
|
-
const maxRadius = d3__namespace.max(rad.data, (d) => d.r) ?? 0;
|
|
1226
|
-
const scaleRadial = maxRadius + 2 * radialMargin;
|
|
1227
1156
|
const w = width,
|
|
1228
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;
|
|
1229
1164
|
const centerX = w / 2,
|
|
1230
1165
|
centerY = h / 2;
|
|
1231
1166
|
|
|
@@ -1305,7 +1240,8 @@ function drawPhylogeny(
|
|
|
1305
1240
|
radiusPx(d.radius),
|
|
1306
1241
|
d.start,
|
|
1307
1242
|
d.end,
|
|
1308
|
-
d.sweep
|
|
1243
|
+
d.sweep ?? "ccw",
|
|
1244
|
+
d.largeArc ?? 0,
|
|
1309
1245
|
)
|
|
1310
1246
|
)
|
|
1311
1247
|
.attr("fill", "none")
|
|
@@ -1420,7 +1356,7 @@ function drawPhylogeny(
|
|
|
1420
1356
|
.attr("x", xoff)
|
|
1421
1357
|
.attr("alignment-baseline", "middle")
|
|
1422
1358
|
.attr("text-anchor", anchor)
|
|
1423
|
-
.attr("font-size",
|
|
1359
|
+
.attr("font-size", labelFontSize)
|
|
1424
1360
|
.attr("fill", "black")
|
|
1425
1361
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1426
1362
|
});
|
|
@@ -1434,6 +1370,8 @@ function drawPhylogeny(
|
|
|
1434
1370
|
// label hover
|
|
1435
1371
|
labels
|
|
1436
1372
|
.on("mouseenter", function(_event, d) {
|
|
1373
|
+
hoverLines.selectAll("*").remove();
|
|
1374
|
+
hoverArcs.selectAll("*").remove();
|
|
1437
1375
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1438
1376
|
d3__namespace.select(this).select("text").attr("font-weight", 600);
|
|
1439
1377
|
})
|
|
@@ -1453,9 +1391,6 @@ function drawPhylogeny(
|
|
|
1453
1391
|
width = 3
|
|
1454
1392
|
) {
|
|
1455
1393
|
// target may be a tip node *or* a numeric tip id
|
|
1456
|
-
lineLayer.selectAll("*").remove();
|
|
1457
|
-
arcLayer.selectAll("*").remove();
|
|
1458
|
-
|
|
1459
1394
|
let cur = (typeof target === "number" || typeof target === "string")
|
|
1460
1395
|
? byId.get(target)
|
|
1461
1396
|
: target;
|
|
@@ -1494,18 +1429,10 @@ function drawPhylogeny(
|
|
|
1494
1429
|
const R = radiusPx(rec.radius);
|
|
1495
1430
|
return rec.sweep == null
|
|
1496
1431
|
? describeArc(centerX, centerY, R, rec.start, rec.end)
|
|
1497
|
-
: 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);
|
|
1498
1433
|
}
|
|
1499
1434
|
|
|
1500
1435
|
if (a) {
|
|
1501
|
-
console.log("Drawing arc:", {
|
|
1502
|
-
childId: cur.thisId,
|
|
1503
|
-
startDeg: (a.start * 180 / Math.PI).toFixed(2),
|
|
1504
|
-
endDeg: (a.end * 180 / Math.PI).toFixed(2),
|
|
1505
|
-
sweep: a.sweep,
|
|
1506
|
-
radius: a.radius
|
|
1507
|
-
});
|
|
1508
|
-
|
|
1509
1436
|
arcLayer
|
|
1510
1437
|
.append("path")
|
|
1511
1438
|
.attr("d", pathFromArcRecord(a))
|
|
@@ -1522,6 +1449,8 @@ function drawPhylogeny(
|
|
|
1522
1449
|
// tip dot hover
|
|
1523
1450
|
tipDots
|
|
1524
1451
|
.on("mouseenter", function(_event, d) {
|
|
1452
|
+
hoverLines.selectAll("*").remove();
|
|
1453
|
+
hoverArcs.selectAll("*").remove();
|
|
1525
1454
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1526
1455
|
d3__namespace.select(this).attr("r", DOT_R + 2);
|
|
1527
1456
|
})
|
|
@@ -1678,7 +1607,7 @@ function drawPhylogeny(
|
|
|
1678
1607
|
.attr("x", xOffset)
|
|
1679
1608
|
.attr("alignment-baseline", "middle")
|
|
1680
1609
|
.attr("text-anchor", anchor)
|
|
1681
|
-
.attr("font-size",
|
|
1610
|
+
.attr("font-size", labelFontSize)
|
|
1682
1611
|
.attr("fill", "black")
|
|
1683
1612
|
.text(d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1684
1613
|
});
|