@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
|
@@ -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,15 +41,17 @@ 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
|
-
|
|
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
|
+
) {
|
|
53
55
|
if (!(r > 0)) return "";
|
|
54
56
|
|
|
55
57
|
const x0 = cx + r * Math.cos(a0);
|
|
@@ -57,7 +59,9 @@ function describeArcSweep(cx, cy, r, a0, a1, sweep = 1, largeArcFlag = 0) {
|
|
|
57
59
|
const x1 = cx + r * Math.cos(a1);
|
|
58
60
|
const y1 = cy - r * Math.sin(a1);
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
const svgSweepFlag = (mathSweep === "ccw") ? 0 : 1;
|
|
63
|
+
|
|
64
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${svgSweepFlag} ${x1} ${y1}`;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
/**
|
|
@@ -334,16 +338,13 @@ function getArcs(pd) {
|
|
|
334
338
|
}
|
|
335
339
|
|
|
336
340
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
* start = first child's angle
|
|
340
|
-
* end = last child's angle
|
|
341
|
-
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
342
|
-
*
|
|
343
|
-
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
344
|
-
* @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).
|
|
345
343
|
*/
|
|
346
344
|
function getArcsFan(pd) {
|
|
345
|
+
const TAU = Math.PI * 2;
|
|
346
|
+
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
347
|
+
|
|
347
348
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
348
349
|
const arcs = [];
|
|
349
350
|
|
|
@@ -355,9 +356,11 @@ function getArcsFan(pd) {
|
|
|
355
356
|
const last = byId.get(c[c.length - 1])?.angle;
|
|
356
357
|
if (first == null || last == null) continue;
|
|
357
358
|
|
|
358
|
-
const start = first;
|
|
359
|
-
const end
|
|
360
|
-
|
|
359
|
+
const start = norm(first);
|
|
360
|
+
const end = norm(last);
|
|
361
|
+
|
|
362
|
+
const deltaCCW = (end - start + TAU) % TAU;
|
|
363
|
+
if (deltaCCW < 1e-9) continue;
|
|
361
364
|
|
|
362
365
|
arcs.push({
|
|
363
366
|
parentId: p.parentId,
|
|
@@ -365,9 +368,11 @@ function getArcsFan(pd) {
|
|
|
365
368
|
radius: p.r,
|
|
366
369
|
start,
|
|
367
370
|
end,
|
|
368
|
-
sweep
|
|
371
|
+
sweep: "ccw", // << math sweep
|
|
372
|
+
largeArc: deltaCCW > Math.PI ? 1 : 0
|
|
369
373
|
});
|
|
370
374
|
}
|
|
375
|
+
|
|
371
376
|
return arcs;
|
|
372
377
|
}
|
|
373
378
|
|
|
@@ -453,76 +458,6 @@ function getChildArcs(pd) {
|
|
|
453
458
|
return arcs;
|
|
454
459
|
}
|
|
455
460
|
|
|
456
|
-
function getChildArcsFan(pd) {
|
|
457
|
-
const TAU = Math.PI * 2;
|
|
458
|
-
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
459
|
-
|
|
460
|
-
function midCCW(a, b) {
|
|
461
|
-
const d = (b - a + TAU) % TAU;
|
|
462
|
-
return norm(a + d / 2);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const key = (x) => (typeof x === "string" ? +x : x);
|
|
466
|
-
const byId = new Map(pd.map(d => [key(d.thisId), d]));
|
|
467
|
-
const childrenByParent = new Map(
|
|
468
|
-
pd.map(d => [
|
|
469
|
-
key(d.thisId),
|
|
470
|
-
(d.children || [])
|
|
471
|
-
.map(ch => (typeof ch === "object" ? ch.thisId : ch))
|
|
472
|
-
.map(key)
|
|
473
|
-
.filter(id => byId.has(id))
|
|
474
|
-
])
|
|
475
|
-
);
|
|
476
|
-
|
|
477
|
-
const child_arcs = [];
|
|
478
|
-
|
|
479
|
-
for (const parentRaw of pd) {
|
|
480
|
-
const pid = key(parentRaw.thisId);
|
|
481
|
-
const kids = childrenByParent.get(pid) || [];
|
|
482
|
-
if (kids.length < 2) continue;
|
|
483
|
-
|
|
484
|
-
const A = kids
|
|
485
|
-
.map(id => {
|
|
486
|
-
const node = byId.get(id);
|
|
487
|
-
return node ? { id, a: norm(node.angle) } : null;
|
|
488
|
-
})
|
|
489
|
-
.filter(Boolean)
|
|
490
|
-
.sort((u, v) => u.a - v.a);
|
|
491
|
-
|
|
492
|
-
const N = A.length;
|
|
493
|
-
if (N < 2) continue;
|
|
494
|
-
|
|
495
|
-
const parent = byId.get(pid);
|
|
496
|
-
const radius = parent?.r;
|
|
497
|
-
if (!(radius > 0)) continue;
|
|
498
|
-
|
|
499
|
-
for (let i = 0; i < N; i++) {
|
|
500
|
-
const prev = A[(i - 1 + N) % N];
|
|
501
|
-
const cur = A[i];
|
|
502
|
-
const next = A[(i + 1) % N];
|
|
503
|
-
|
|
504
|
-
const start = midCCW(prev.a, cur.a);
|
|
505
|
-
const end = midCCW(cur.a, next.a);
|
|
506
|
-
|
|
507
|
-
const sweep = 1; // always clockwise
|
|
508
|
-
const delta = (end - start + TAU) % TAU;
|
|
509
|
-
const largeArc = delta > Math.PI ? 1 : 0;
|
|
510
|
-
|
|
511
|
-
child_arcs.push({
|
|
512
|
-
parentId: pid,
|
|
513
|
-
childId: cur.id,
|
|
514
|
-
radius,
|
|
515
|
-
start,
|
|
516
|
-
end,
|
|
517
|
-
sweep,
|
|
518
|
-
largeArc
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
return child_arcs;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
461
|
/**
|
|
527
462
|
* radialLayout(node, opts?)
|
|
528
463
|
* opts:
|
|
@@ -566,13 +501,8 @@ function radialLayout(node, opts = {}) {
|
|
|
566
501
|
? getArcsFan(pd)
|
|
567
502
|
: getArcs(pd);
|
|
568
503
|
|
|
569
|
-
// per-child arcs for
|
|
570
|
-
|
|
571
|
-
if (arcsStyle === "fan") {
|
|
572
|
-
child_arcs = getChildArcsFan(pd);
|
|
573
|
-
} else {
|
|
574
|
-
child_arcs = getChildArcs(pd);
|
|
575
|
-
}
|
|
504
|
+
// per-child arcs for path highlighting: always parent.angle → child.angle at parent.r
|
|
505
|
+
const child_arcs = getChildArcs(pd);
|
|
576
506
|
|
|
577
507
|
return { data: pd, radii, arcs, child_arcs };
|
|
578
508
|
}
|
|
@@ -900,21 +830,29 @@ function readTree(text) {
|
|
|
900
830
|
text = String(text).replace(/\s+/g, '');
|
|
901
831
|
|
|
902
832
|
const tokens = text.split(/(;|\(|\)|,)/);
|
|
903
|
-
const root = { parent: null, children: [] };
|
|
904
|
-
let curnode = root;
|
|
905
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;
|
|
906
844
|
|
|
907
845
|
for (const token of tokens) {
|
|
908
846
|
if (!token || token === ';') continue;
|
|
909
847
|
|
|
910
848
|
if (token === '(') {
|
|
911
|
-
const child =
|
|
849
|
+
const child = makeNode(curnode);
|
|
912
850
|
curnode.children.push(child);
|
|
913
851
|
curnode = child; // descend
|
|
914
852
|
} else if (token === ',') {
|
|
915
853
|
// back to parent, then create sibling
|
|
916
854
|
curnode = curnode.parent;
|
|
917
|
-
const child =
|
|
855
|
+
const child = makeNode(curnode);
|
|
918
856
|
curnode.children.push(child);
|
|
919
857
|
curnode = child;
|
|
920
858
|
} else if (token === ')') {
|
|
@@ -923,14 +861,15 @@ function readTree(text) {
|
|
|
923
861
|
if (curnode === null) break;
|
|
924
862
|
} else {
|
|
925
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.
|
|
926
867
|
const nodeinfo = token.split(':');
|
|
927
868
|
if (nodeinfo.length === 1) {
|
|
928
869
|
if (token.startsWith(':')) {
|
|
929
|
-
curnode.label = '';
|
|
930
870
|
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
931
871
|
} else {
|
|
932
872
|
curnode.label = nodeinfo[0];
|
|
933
|
-
curnode.branchLength = null;
|
|
934
873
|
}
|
|
935
874
|
} else if (nodeinfo.length === 2) {
|
|
936
875
|
curnode.label = nodeinfo[0];
|
|
@@ -940,13 +879,9 @@ function readTree(text) {
|
|
|
940
879
|
curnode.label = nodeinfo[0] || '';
|
|
941
880
|
curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
|
|
942
881
|
}
|
|
943
|
-
curnode.id = nodeId++; // assign then increment
|
|
944
882
|
}
|
|
945
883
|
}
|
|
946
884
|
|
|
947
|
-
// Ensure root has an id if not assigned during parsing
|
|
948
|
-
if (root.id == null) root.id = nodeId;
|
|
949
|
-
|
|
950
885
|
return root;
|
|
951
886
|
}
|
|
952
887
|
|
|
@@ -961,6 +896,7 @@ function drawPhylogeny(
|
|
|
961
896
|
strokeWidth = 1, // for the phylogeny branches
|
|
962
897
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
963
898
|
tipLabels = true,
|
|
899
|
+
labelFontSize = 10, // font size (px) for tip labels
|
|
964
900
|
showTooltips = true,
|
|
965
901
|
tooltipFormatter = (d, rtt) =>
|
|
966
902
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1008,7 +944,7 @@ function drawPhylogeny(
|
|
|
1008
944
|
const tips = horizontal.filter((d) => d.isTip);
|
|
1009
945
|
|
|
1010
946
|
// indices & root→tip getter
|
|
1011
|
-
const byId = new Map(
|
|
947
|
+
const byId = new Map(tree_df.data.map((d) => [d.thisId, d])); // includes root
|
|
1012
948
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1013
949
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1014
950
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
@@ -1082,6 +1018,7 @@ function drawPhylogeny(
|
|
|
1082
1018
|
// interactive root→tip highlight (rect) on dot hover
|
|
1083
1019
|
tipDots
|
|
1084
1020
|
.on("mouseenter", function(_event, d) {
|
|
1021
|
+
hoverLayer.selectAll("*").remove();
|
|
1085
1022
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1086
1023
|
d3.select(this).attr("r", 4);
|
|
1087
1024
|
})
|
|
@@ -1101,7 +1038,7 @@ function drawPhylogeny(
|
|
|
1101
1038
|
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1102
1039
|
.attr("y", (d) => yScale(d.y1))
|
|
1103
1040
|
.attr("dy", "0.32em")
|
|
1104
|
-
.attr("font-size",
|
|
1041
|
+
.attr("font-size", labelFontSize)
|
|
1105
1042
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1106
1043
|
|
|
1107
1044
|
if (showTooltips) {
|
|
@@ -1112,6 +1049,7 @@ function drawPhylogeny(
|
|
|
1112
1049
|
|
|
1113
1050
|
labels
|
|
1114
1051
|
.on("mouseenter", function(_event, d) {
|
|
1052
|
+
hoverLayer.selectAll("*").remove();
|
|
1115
1053
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1116
1054
|
d3.select(this).attr("font-weight", 600);
|
|
1117
1055
|
})
|
|
@@ -1138,7 +1076,6 @@ function drawPhylogeny(
|
|
|
1138
1076
|
|
|
1139
1077
|
// helper to draw root→tip for rect (both vertical+horizontal)
|
|
1140
1078
|
function drawRectPath(tipId, layer, stroke, width) {
|
|
1141
|
-
layer.selectAll("*").remove();
|
|
1142
1079
|
let cur = byId.get(tipId);
|
|
1143
1080
|
while (cur && cur.parentId != null) {
|
|
1144
1081
|
const parent = byId.get(cur.parentId);
|
|
@@ -1195,10 +1132,14 @@ function drawPhylogeny(
|
|
|
1195
1132
|
const END_CAP = 0;
|
|
1196
1133
|
|
|
1197
1134
|
// ===== SCALES / BOUNDS =====
|
|
1198
|
-
const maxRadius = d3.max(rad.data, (d) => d.r) ?? 0;
|
|
1199
|
-
const scaleRadial = maxRadius + 2 * radialMargin;
|
|
1200
1135
|
const w = width,
|
|
1201
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;
|
|
1202
1143
|
const centerX = w / 2,
|
|
1203
1144
|
centerY = h / 2;
|
|
1204
1145
|
|
|
@@ -1278,8 +1219,8 @@ function drawPhylogeny(
|
|
|
1278
1219
|
radiusPx(d.radius),
|
|
1279
1220
|
d.start,
|
|
1280
1221
|
d.end,
|
|
1281
|
-
d.sweep,
|
|
1282
|
-
d.largeArc,
|
|
1222
|
+
d.sweep ?? "ccw",
|
|
1223
|
+
d.largeArc ?? 0,
|
|
1283
1224
|
)
|
|
1284
1225
|
)
|
|
1285
1226
|
.attr("fill", "none")
|
|
@@ -1394,7 +1335,7 @@ function drawPhylogeny(
|
|
|
1394
1335
|
.attr("x", xoff)
|
|
1395
1336
|
.attr("alignment-baseline", "middle")
|
|
1396
1337
|
.attr("text-anchor", anchor)
|
|
1397
|
-
.attr("font-size",
|
|
1338
|
+
.attr("font-size", labelFontSize)
|
|
1398
1339
|
.attr("fill", "black")
|
|
1399
1340
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1400
1341
|
});
|
|
@@ -1408,6 +1349,8 @@ function drawPhylogeny(
|
|
|
1408
1349
|
// label hover
|
|
1409
1350
|
labels
|
|
1410
1351
|
.on("mouseenter", function(_event, d) {
|
|
1352
|
+
hoverLines.selectAll("*").remove();
|
|
1353
|
+
hoverArcs.selectAll("*").remove();
|
|
1411
1354
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1412
1355
|
d3.select(this).select("text").attr("font-weight", 600);
|
|
1413
1356
|
})
|
|
@@ -1427,9 +1370,6 @@ function drawPhylogeny(
|
|
|
1427
1370
|
width = 3
|
|
1428
1371
|
) {
|
|
1429
1372
|
// target may be a tip node *or* a numeric tip id
|
|
1430
|
-
lineLayer.selectAll("*").remove();
|
|
1431
|
-
arcLayer.selectAll("*").remove();
|
|
1432
|
-
|
|
1433
1373
|
let cur = (typeof target === "number" || typeof target === "string")
|
|
1434
1374
|
? byId.get(target)
|
|
1435
1375
|
: target;
|
|
@@ -1468,18 +1408,10 @@ function drawPhylogeny(
|
|
|
1468
1408
|
const R = radiusPx(rec.radius);
|
|
1469
1409
|
return rec.sweep == null
|
|
1470
1410
|
? describeArc(centerX, centerY, R, rec.start, rec.end)
|
|
1471
|
-
: 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);
|
|
1472
1412
|
}
|
|
1473
1413
|
|
|
1474
1414
|
if (a) {
|
|
1475
|
-
console.log("Drawing arc:", {
|
|
1476
|
-
childId: cur.thisId,
|
|
1477
|
-
startDeg: (a.start * 180 / Math.PI).toFixed(2),
|
|
1478
|
-
endDeg: (a.end * 180 / Math.PI).toFixed(2),
|
|
1479
|
-
sweep: a.sweep,
|
|
1480
|
-
radius: a.radius
|
|
1481
|
-
});
|
|
1482
|
-
|
|
1483
1415
|
arcLayer
|
|
1484
1416
|
.append("path")
|
|
1485
1417
|
.attr("d", pathFromArcRecord(a))
|
|
@@ -1496,6 +1428,8 @@ function drawPhylogeny(
|
|
|
1496
1428
|
// tip dot hover
|
|
1497
1429
|
tipDots
|
|
1498
1430
|
.on("mouseenter", function(_event, d) {
|
|
1431
|
+
hoverLines.selectAll("*").remove();
|
|
1432
|
+
hoverArcs.selectAll("*").remove();
|
|
1499
1433
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1500
1434
|
d3.select(this).attr("r", DOT_R + 2);
|
|
1501
1435
|
})
|
|
@@ -1652,7 +1586,7 @@ function drawPhylogeny(
|
|
|
1652
1586
|
.attr("x", xOffset)
|
|
1653
1587
|
.attr("alignment-baseline", "middle")
|
|
1654
1588
|
.attr("text-anchor", anchor)
|
|
1655
|
-
.attr("font-size",
|
|
1589
|
+
.attr("font-size", labelFontSize)
|
|
1656
1590
|
.attr("fill", "black")
|
|
1657
1591
|
.text(d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1658
1592
|
});
|