@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
|
@@ -31,8 +31,8 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
31
31
|
return `M ${p.x} ${p.y}`; // degenerate span → no arc
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
35
|
-
const sweepFlag = 0; // CCW
|
|
34
|
+
const largeArcFlag = delta > Math.PI ? 1 : 0;
|
|
35
|
+
const sweepFlag = 0; // CCW in our y-flipped coords: math-CCW = decreasing SVG angle = sweepFlag 0
|
|
36
36
|
|
|
37
37
|
const p0 = polarToCartesian(cx, cy, radius, a0);
|
|
38
38
|
const p1 = polarToCartesian(cx, cy, radius, a1);
|
|
@@ -41,25 +41,27 @@ function describeArc(cx, cy, radius, startAngle, endAngle) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// src/radial/describeArcSweep.js
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
44
|
+
// IMPORTANT: angles are in "math space" (increasing = CCW).
|
|
45
|
+
// Because we map y as (cy - r*sin(a)), our math angle t maps to SVG angle -t.
|
|
46
|
+
// Increasing t (math CCW) = decreasing SVG angle = sweepFlag 0 (negative direction).
|
|
47
|
+
// math CCW -> svg sweepFlag = 0
|
|
48
|
+
// math CW -> svg sweepFlag = 1
|
|
49
|
+
function describeArcSweep(
|
|
50
|
+
cx, cy, r,
|
|
51
|
+
a0, a1,
|
|
52
|
+
mathSweep = "ccw", // "ccw" | "cw"
|
|
53
|
+
largeArcFlag = 0
|
|
54
|
+
) {
|
|
55
|
+
if (!(r > 0)) return "";
|
|
54
56
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
57
|
+
const x0 = cx + r * Math.cos(a0);
|
|
58
|
+
const y0 = cy - r * Math.sin(a0);
|
|
59
|
+
const x1 = cx + r * Math.cos(a1);
|
|
60
|
+
const y1 = cy - r * Math.sin(a1);
|
|
58
61
|
|
|
59
|
-
const
|
|
60
|
-
const x1 = cx + r * Math.cos(a1), y1 = cy - r * Math.sin(a1);
|
|
62
|
+
const svgSweepFlag = (mathSweep === "ccw") ? 0 : 1;
|
|
61
63
|
|
|
62
|
-
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${
|
|
64
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${svgSweepFlag} ${x1} ${y1}`;
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -336,16 +338,13 @@ function getArcs(pd) {
|
|
|
336
338
|
}
|
|
337
339
|
|
|
338
340
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
* start = first child's angle
|
|
342
|
-
* end = last child's angle
|
|
343
|
-
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
344
|
-
*
|
|
345
|
-
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
346
|
-
* @returns {Array} [{parentId,thisId,radius,start,end,sweep}]
|
|
341
|
+
* APE-like block arcs per internal parent.
|
|
342
|
+
* Draw CCW from first child's angle to last child's angle (wrapping allowed).
|
|
347
343
|
*/
|
|
348
344
|
function getArcsFan(pd) {
|
|
345
|
+
const TAU = Math.PI * 2;
|
|
346
|
+
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
347
|
+
|
|
349
348
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
350
349
|
const arcs = [];
|
|
351
350
|
|
|
@@ -357,9 +356,11 @@ function getArcsFan(pd) {
|
|
|
357
356
|
const last = byId.get(c[c.length - 1])?.angle;
|
|
358
357
|
if (first == null || last == null) continue;
|
|
359
358
|
|
|
360
|
-
const start = first;
|
|
361
|
-
const end
|
|
362
|
-
|
|
359
|
+
const start = norm(first);
|
|
360
|
+
const end = norm(last);
|
|
361
|
+
|
|
362
|
+
const deltaCCW = (end - start + TAU) % TAU;
|
|
363
|
+
if (deltaCCW < 1e-9) continue;
|
|
363
364
|
|
|
364
365
|
arcs.push({
|
|
365
366
|
parentId: p.parentId,
|
|
@@ -367,9 +368,11 @@ function getArcsFan(pd) {
|
|
|
367
368
|
radius: p.r,
|
|
368
369
|
start,
|
|
369
370
|
end,
|
|
370
|
-
sweep
|
|
371
|
+
sweep: "ccw", // << math sweep
|
|
372
|
+
largeArc: deltaCCW > Math.PI ? 1 : 0
|
|
371
373
|
});
|
|
372
374
|
}
|
|
375
|
+
|
|
373
376
|
return arcs;
|
|
374
377
|
}
|
|
375
378
|
|
|
@@ -455,80 +458,6 @@ function getChildArcs(pd) {
|
|
|
455
458
|
return arcs;
|
|
456
459
|
}
|
|
457
460
|
|
|
458
|
-
function getChildArcsFan(pd) {
|
|
459
|
-
const TAU = Math.PI * 2;
|
|
460
|
-
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
461
|
-
|
|
462
|
-
// Circular midpoint that travels CCW from a -> b by half the CCW span
|
|
463
|
-
function midCCW(a, b) {
|
|
464
|
-
const d = (b - a + TAU) % TAU; // CCW delta in [0, 2π)
|
|
465
|
-
return norm(a + d / 2);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
const key = (x) => (typeof x === "string" ? +x : x);
|
|
469
|
-
|
|
470
|
-
const byId = new Map(pd.map(d => [key(d.thisId), d]));
|
|
471
|
-
const childrenByParent = new Map(
|
|
472
|
-
pd.map(d => [
|
|
473
|
-
key(d.thisId),
|
|
474
|
-
// normalize children to numeric IDs; drop anything we can't resolve
|
|
475
|
-
(d.children || [])
|
|
476
|
-
.map(ch => (typeof ch === "object" ? ch.thisId : ch))
|
|
477
|
-
.map(key)
|
|
478
|
-
.filter(id => byId.has(id))
|
|
479
|
-
])
|
|
480
|
-
);
|
|
481
|
-
|
|
482
|
-
const child_arcs = [];
|
|
483
|
-
|
|
484
|
-
for (const parentRaw of pd) {
|
|
485
|
-
const pid = key(parentRaw.thisId);
|
|
486
|
-
const kids = childrenByParent.get(pid) || [];
|
|
487
|
-
if (kids.length < 2) continue;
|
|
488
|
-
|
|
489
|
-
// Sort children by angle (normalized) around the circle
|
|
490
|
-
const A = kids
|
|
491
|
-
.map(id => {
|
|
492
|
-
const node = byId.get(id);
|
|
493
|
-
return node ? { id, a: norm(node.angle) } : null;
|
|
494
|
-
})
|
|
495
|
-
.filter(Boolean)
|
|
496
|
-
.sort((u, v) => u.a - v.a);
|
|
497
|
-
|
|
498
|
-
const N = A.length;
|
|
499
|
-
if (N < 2) continue;
|
|
500
|
-
|
|
501
|
-
const parent = byId.get(pid);
|
|
502
|
-
const radius = parent?.r;
|
|
503
|
-
if (!(radius > 0)) continue;
|
|
504
|
-
|
|
505
|
-
for (let i = 0; i < N; i++) {
|
|
506
|
-
const prev = A[(i - 1 + N) % N];
|
|
507
|
-
const cur = A[i];
|
|
508
|
-
const next = A[(i + 1) % N];
|
|
509
|
-
|
|
510
|
-
const start = midCCW(prev.a, cur.a);
|
|
511
|
-
const end = midCCW(cur.a, next.a);
|
|
512
|
-
|
|
513
|
-
// Use SVG-conforming sweep logic
|
|
514
|
-
const delta = (end - start + TAU) % TAU;
|
|
515
|
-
const sweep = delta > Math.PI ? 0 : 1;
|
|
516
|
-
|
|
517
|
-
child_arcs.push({
|
|
518
|
-
parentId: pid,
|
|
519
|
-
childId: cur.id,
|
|
520
|
-
radius,
|
|
521
|
-
start,
|
|
522
|
-
end,
|
|
523
|
-
sweep
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return child_arcs;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
461
|
/**
|
|
533
462
|
* radialLayout(node, opts?)
|
|
534
463
|
* opts:
|
|
@@ -572,13 +501,8 @@ function radialLayout(node, opts = {}) {
|
|
|
572
501
|
? getArcsFan(pd)
|
|
573
502
|
: getArcs(pd);
|
|
574
503
|
|
|
575
|
-
// per-child arcs for
|
|
576
|
-
|
|
577
|
-
if (arcsStyle === "fan") {
|
|
578
|
-
child_arcs = getChildArcsFan(pd);
|
|
579
|
-
} else {
|
|
580
|
-
child_arcs = getChildArcs(pd);
|
|
581
|
-
}
|
|
504
|
+
// per-child arcs for path highlighting: always parent.angle → child.angle at parent.r
|
|
505
|
+
const child_arcs = getChildArcs(pd);
|
|
582
506
|
|
|
583
507
|
return { data: pd, radii, arcs, child_arcs };
|
|
584
508
|
}
|
|
@@ -906,21 +830,29 @@ function readTree(text) {
|
|
|
906
830
|
text = String(text).replace(/\s+/g, '');
|
|
907
831
|
|
|
908
832
|
const tokens = text.split(/(;|\(|\)|,)/);
|
|
909
|
-
const root = { parent: null, children: [] };
|
|
910
|
-
let curnode = root;
|
|
911
833
|
let nodeId = 0;
|
|
834
|
+
const makeNode = (parent) => ({
|
|
835
|
+
parent,
|
|
836
|
+
children: [],
|
|
837
|
+
id: nodeId++,
|
|
838
|
+
label: '',
|
|
839
|
+
branchLength: null
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
const root = makeNode(null);
|
|
843
|
+
let curnode = root;
|
|
912
844
|
|
|
913
845
|
for (const token of tokens) {
|
|
914
846
|
if (!token || token === ';') continue;
|
|
915
847
|
|
|
916
848
|
if (token === '(') {
|
|
917
|
-
const child =
|
|
849
|
+
const child = makeNode(curnode);
|
|
918
850
|
curnode.children.push(child);
|
|
919
851
|
curnode = child; // descend
|
|
920
852
|
} else if (token === ',') {
|
|
921
853
|
// back to parent, then create sibling
|
|
922
854
|
curnode = curnode.parent;
|
|
923
|
-
const child =
|
|
855
|
+
const child = makeNode(curnode);
|
|
924
856
|
curnode.children.push(child);
|
|
925
857
|
curnode = child;
|
|
926
858
|
} else if (token === ')') {
|
|
@@ -929,14 +861,15 @@ function readTree(text) {
|
|
|
929
861
|
if (curnode === null) break;
|
|
930
862
|
} else {
|
|
931
863
|
// label/branch-length chunk (e.g., "A:0.01" or "A")
|
|
864
|
+
// Note: nodes are assigned an id at creation (above), so internal
|
|
865
|
+
// (clade) nodes that carry neither a label nor a branch length —
|
|
866
|
+
// e.g. "((A,B),(C,D));" — still get a valid, linkable id here.
|
|
932
867
|
const nodeinfo = token.split(':');
|
|
933
868
|
if (nodeinfo.length === 1) {
|
|
934
869
|
if (token.startsWith(':')) {
|
|
935
|
-
curnode.label = '';
|
|
936
870
|
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
937
871
|
} else {
|
|
938
872
|
curnode.label = nodeinfo[0];
|
|
939
|
-
curnode.branchLength = null;
|
|
940
873
|
}
|
|
941
874
|
} else if (nodeinfo.length === 2) {
|
|
942
875
|
curnode.label = nodeinfo[0];
|
|
@@ -946,13 +879,9 @@ function readTree(text) {
|
|
|
946
879
|
curnode.label = nodeinfo[0] || '';
|
|
947
880
|
curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
|
|
948
881
|
}
|
|
949
|
-
curnode.id = nodeId++; // assign then increment
|
|
950
882
|
}
|
|
951
883
|
}
|
|
952
884
|
|
|
953
|
-
// Ensure root has an id if not assigned during parsing
|
|
954
|
-
if (root.id == null) root.id = nodeId;
|
|
955
|
-
|
|
956
885
|
return root;
|
|
957
886
|
}
|
|
958
887
|
|
|
@@ -967,6 +896,7 @@ function drawPhylogeny(
|
|
|
967
896
|
strokeWidth = 1, // for the phylogeny branches
|
|
968
897
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
969
898
|
tipLabels = true,
|
|
899
|
+
labelFontSize = 10, // font size (px) for tip labels
|
|
970
900
|
showTooltips = true,
|
|
971
901
|
tooltipFormatter = (d, rtt) =>
|
|
972
902
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1014,7 +944,7 @@ function drawPhylogeny(
|
|
|
1014
944
|
const tips = horizontal.filter((d) => d.isTip);
|
|
1015
945
|
|
|
1016
946
|
// indices & root→tip getter
|
|
1017
|
-
const byId = new Map(
|
|
947
|
+
const byId = new Map(tree_df.data.map((d) => [d.thisId, d])); // includes root
|
|
1018
948
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1019
949
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1020
950
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
@@ -1088,6 +1018,7 @@ function drawPhylogeny(
|
|
|
1088
1018
|
// interactive root→tip highlight (rect) on dot hover
|
|
1089
1019
|
tipDots
|
|
1090
1020
|
.on("mouseenter", function(_event, d) {
|
|
1021
|
+
hoverLayer.selectAll("*").remove();
|
|
1091
1022
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1092
1023
|
d3.select(this).attr("r", 4);
|
|
1093
1024
|
})
|
|
@@ -1107,7 +1038,7 @@ function drawPhylogeny(
|
|
|
1107
1038
|
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1108
1039
|
.attr("y", (d) => yScale(d.y1))
|
|
1109
1040
|
.attr("dy", "0.32em")
|
|
1110
|
-
.attr("font-size",
|
|
1041
|
+
.attr("font-size", labelFontSize)
|
|
1111
1042
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1112
1043
|
|
|
1113
1044
|
if (showTooltips) {
|
|
@@ -1118,6 +1049,7 @@ function drawPhylogeny(
|
|
|
1118
1049
|
|
|
1119
1050
|
labels
|
|
1120
1051
|
.on("mouseenter", function(_event, d) {
|
|
1052
|
+
hoverLayer.selectAll("*").remove();
|
|
1121
1053
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1122
1054
|
d3.select(this).attr("font-weight", 600);
|
|
1123
1055
|
})
|
|
@@ -1144,7 +1076,6 @@ function drawPhylogeny(
|
|
|
1144
1076
|
|
|
1145
1077
|
// helper to draw root→tip for rect (both vertical+horizontal)
|
|
1146
1078
|
function drawRectPath(tipId, layer, stroke, width) {
|
|
1147
|
-
layer.selectAll("*").remove();
|
|
1148
1079
|
let cur = byId.get(tipId);
|
|
1149
1080
|
while (cur && cur.parentId != null) {
|
|
1150
1081
|
const parent = byId.get(cur.parentId);
|
|
@@ -1201,10 +1132,14 @@ function drawPhylogeny(
|
|
|
1201
1132
|
const END_CAP = 0;
|
|
1202
1133
|
|
|
1203
1134
|
// ===== SCALES / BOUNDS =====
|
|
1204
|
-
const maxRadius = d3.max(rad.data, (d) => d.r) ?? 0;
|
|
1205
|
-
const scaleRadial = maxRadius + 2 * radialMargin;
|
|
1206
1135
|
const w = width,
|
|
1207
1136
|
h = height;
|
|
1137
|
+
const maxRadius = d3.max(rad.data, (d) => d.r) ?? 0;
|
|
1138
|
+
// radialMargin is in pixels: tips sit (radialMargin) px from the SVG edge.
|
|
1139
|
+
// Derive the data-space scale so that radiusPx(maxRadius) = w/2 - radialMargin.
|
|
1140
|
+
const scaleRadial = maxRadius > 0
|
|
1141
|
+
? maxRadius * (w / 2) / (w / 2 - radialMargin)
|
|
1142
|
+
: 1;
|
|
1208
1143
|
const centerX = w / 2,
|
|
1209
1144
|
centerY = h / 2;
|
|
1210
1145
|
|
|
@@ -1284,7 +1219,8 @@ function drawPhylogeny(
|
|
|
1284
1219
|
radiusPx(d.radius),
|
|
1285
1220
|
d.start,
|
|
1286
1221
|
d.end,
|
|
1287
|
-
d.sweep
|
|
1222
|
+
d.sweep ?? "ccw",
|
|
1223
|
+
d.largeArc ?? 0,
|
|
1288
1224
|
)
|
|
1289
1225
|
)
|
|
1290
1226
|
.attr("fill", "none")
|
|
@@ -1399,7 +1335,7 @@ function drawPhylogeny(
|
|
|
1399
1335
|
.attr("x", xoff)
|
|
1400
1336
|
.attr("alignment-baseline", "middle")
|
|
1401
1337
|
.attr("text-anchor", anchor)
|
|
1402
|
-
.attr("font-size",
|
|
1338
|
+
.attr("font-size", labelFontSize)
|
|
1403
1339
|
.attr("fill", "black")
|
|
1404
1340
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1405
1341
|
});
|
|
@@ -1413,6 +1349,8 @@ function drawPhylogeny(
|
|
|
1413
1349
|
// label hover
|
|
1414
1350
|
labels
|
|
1415
1351
|
.on("mouseenter", function(_event, d) {
|
|
1352
|
+
hoverLines.selectAll("*").remove();
|
|
1353
|
+
hoverArcs.selectAll("*").remove();
|
|
1416
1354
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1417
1355
|
d3.select(this).select("text").attr("font-weight", 600);
|
|
1418
1356
|
})
|
|
@@ -1432,9 +1370,6 @@ function drawPhylogeny(
|
|
|
1432
1370
|
width = 3
|
|
1433
1371
|
) {
|
|
1434
1372
|
// target may be a tip node *or* a numeric tip id
|
|
1435
|
-
lineLayer.selectAll("*").remove();
|
|
1436
|
-
arcLayer.selectAll("*").remove();
|
|
1437
|
-
|
|
1438
1373
|
let cur = (typeof target === "number" || typeof target === "string")
|
|
1439
1374
|
? byId.get(target)
|
|
1440
1375
|
: target;
|
|
@@ -1473,18 +1408,10 @@ function drawPhylogeny(
|
|
|
1473
1408
|
const R = radiusPx(rec.radius);
|
|
1474
1409
|
return rec.sweep == null
|
|
1475
1410
|
? describeArc(centerX, centerY, R, rec.start, rec.end)
|
|
1476
|
-
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep);
|
|
1411
|
+
: describeArcSweep(centerX, centerY, R, rec.start, rec.end, rec.sweep ?? "ccw", rec.largeArc ?? 0);
|
|
1477
1412
|
}
|
|
1478
1413
|
|
|
1479
1414
|
if (a) {
|
|
1480
|
-
console.log("Drawing arc:", {
|
|
1481
|
-
childId: cur.thisId,
|
|
1482
|
-
startDeg: (a.start * 180 / Math.PI).toFixed(2),
|
|
1483
|
-
endDeg: (a.end * 180 / Math.PI).toFixed(2),
|
|
1484
|
-
sweep: a.sweep,
|
|
1485
|
-
radius: a.radius
|
|
1486
|
-
});
|
|
1487
|
-
|
|
1488
1415
|
arcLayer
|
|
1489
1416
|
.append("path")
|
|
1490
1417
|
.attr("d", pathFromArcRecord(a))
|
|
@@ -1501,6 +1428,8 @@ function drawPhylogeny(
|
|
|
1501
1428
|
// tip dot hover
|
|
1502
1429
|
tipDots
|
|
1503
1430
|
.on("mouseenter", function(_event, d) {
|
|
1431
|
+
hoverLines.selectAll("*").remove();
|
|
1432
|
+
hoverArcs.selectAll("*").remove();
|
|
1504
1433
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1505
1434
|
d3.select(this).attr("r", DOT_R + 2);
|
|
1506
1435
|
})
|
|
@@ -1657,7 +1586,7 @@ function drawPhylogeny(
|
|
|
1657
1586
|
.attr("x", xOffset)
|
|
1658
1587
|
.attr("alignment-baseline", "middle")
|
|
1659
1588
|
.attr("text-anchor", anchor)
|
|
1660
|
-
.attr("font-size",
|
|
1589
|
+
.attr("font-size", labelFontSize)
|
|
1661
1590
|
.attr("fill", "black")
|
|
1662
1591
|
.text(d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1663
1592
|
});
|