@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/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,25 +154,27 @@ 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
|
-
|
|
166
|
-
|
|
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
|
+
) {
|
|
168
|
+
if (!(r > 0)) return "";
|
|
167
169
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
const
|
|
170
|
+
const x0 = cx + r * Math.cos(a0);
|
|
171
|
+
const y0 = cy - r * Math.sin(a0);
|
|
172
|
+
const x1 = cx + r * Math.cos(a1);
|
|
173
|
+
const y1 = cy - r * Math.sin(a1);
|
|
171
174
|
|
|
172
|
-
const
|
|
173
|
-
const x1 = cx + r * Math.cos(a1), y1 = cy - r * Math.sin(a1);
|
|
175
|
+
const svgSweepFlag = (mathSweep === "ccw") ? 0 : 1;
|
|
174
176
|
|
|
175
|
-
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${
|
|
177
|
+
return `M ${x0} ${y0} A ${r} ${r} 0 ${largeArcFlag} ${svgSweepFlag} ${x1} ${y1}`;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
180
|
/**
|
|
@@ -449,16 +451,13 @@ function getArcs(pd) {
|
|
|
449
451
|
}
|
|
450
452
|
|
|
451
453
|
/**
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
* start = first child's angle
|
|
455
|
-
* end = last child's angle
|
|
456
|
-
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
457
|
-
*
|
|
458
|
-
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
459
|
-
* @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).
|
|
460
456
|
*/
|
|
461
457
|
function getArcsFan(pd) {
|
|
458
|
+
const TAU = Math.PI * 2;
|
|
459
|
+
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
460
|
+
|
|
462
461
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
463
462
|
const arcs = [];
|
|
464
463
|
|
|
@@ -470,9 +469,11 @@ function getArcsFan(pd) {
|
|
|
470
469
|
const last = byId.get(c[c.length - 1])?.angle;
|
|
471
470
|
if (first == null || last == null) continue;
|
|
472
471
|
|
|
473
|
-
const start = first;
|
|
474
|
-
const end
|
|
475
|
-
|
|
472
|
+
const start = norm(first);
|
|
473
|
+
const end = norm(last);
|
|
474
|
+
|
|
475
|
+
const deltaCCW = (end - start + TAU) % TAU;
|
|
476
|
+
if (deltaCCW < 1e-9) continue;
|
|
476
477
|
|
|
477
478
|
arcs.push({
|
|
478
479
|
parentId: p.parentId,
|
|
@@ -480,9 +481,11 @@ function getArcsFan(pd) {
|
|
|
480
481
|
radius: p.r,
|
|
481
482
|
start,
|
|
482
483
|
end,
|
|
483
|
-
sweep
|
|
484
|
+
sweep: "ccw", // << math sweep
|
|
485
|
+
largeArc: deltaCCW > Math.PI ? 1 : 0
|
|
484
486
|
});
|
|
485
487
|
}
|
|
488
|
+
|
|
486
489
|
return arcs;
|
|
487
490
|
}
|
|
488
491
|
|
|
@@ -568,80 +571,6 @@ function getChildArcs(pd) {
|
|
|
568
571
|
return arcs;
|
|
569
572
|
}
|
|
570
573
|
|
|
571
|
-
function getChildArcsFan(pd) {
|
|
572
|
-
const TAU = Math.PI * 2;
|
|
573
|
-
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
574
|
-
|
|
575
|
-
// Circular midpoint that travels CCW from a -> b by half the CCW span
|
|
576
|
-
function midCCW(a, b) {
|
|
577
|
-
const d = (b - a + TAU) % TAU; // CCW delta in [0, 2π)
|
|
578
|
-
return norm(a + d / 2);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const key = (x) => (typeof x === "string" ? +x : x);
|
|
582
|
-
|
|
583
|
-
const byId = new Map(pd.map(d => [key(d.thisId), d]));
|
|
584
|
-
const childrenByParent = new Map(
|
|
585
|
-
pd.map(d => [
|
|
586
|
-
key(d.thisId),
|
|
587
|
-
// normalize children to numeric IDs; drop anything we can't resolve
|
|
588
|
-
(d.children || [])
|
|
589
|
-
.map(ch => (typeof ch === "object" ? ch.thisId : ch))
|
|
590
|
-
.map(key)
|
|
591
|
-
.filter(id => byId.has(id))
|
|
592
|
-
])
|
|
593
|
-
);
|
|
594
|
-
|
|
595
|
-
const child_arcs = [];
|
|
596
|
-
|
|
597
|
-
for (const parentRaw of pd) {
|
|
598
|
-
const pid = key(parentRaw.thisId);
|
|
599
|
-
const kids = childrenByParent.get(pid) || [];
|
|
600
|
-
if (kids.length < 2) continue;
|
|
601
|
-
|
|
602
|
-
// Sort children by angle (normalized) around the circle
|
|
603
|
-
const A = kids
|
|
604
|
-
.map(id => {
|
|
605
|
-
const node = byId.get(id);
|
|
606
|
-
return node ? { id, a: norm(node.angle) } : null;
|
|
607
|
-
})
|
|
608
|
-
.filter(Boolean)
|
|
609
|
-
.sort((u, v) => u.a - v.a);
|
|
610
|
-
|
|
611
|
-
const N = A.length;
|
|
612
|
-
if (N < 2) continue;
|
|
613
|
-
|
|
614
|
-
const parent = byId.get(pid);
|
|
615
|
-
const radius = parent?.r;
|
|
616
|
-
if (!(radius > 0)) continue;
|
|
617
|
-
|
|
618
|
-
for (let i = 0; i < N; i++) {
|
|
619
|
-
const prev = A[(i - 1 + N) % N];
|
|
620
|
-
const cur = A[i];
|
|
621
|
-
const next = A[(i + 1) % N];
|
|
622
|
-
|
|
623
|
-
const start = midCCW(prev.a, cur.a);
|
|
624
|
-
const end = midCCW(cur.a, next.a);
|
|
625
|
-
|
|
626
|
-
// Use SVG-conforming sweep logic
|
|
627
|
-
const delta = (end - start + TAU) % TAU;
|
|
628
|
-
const sweep = delta > Math.PI ? 0 : 1;
|
|
629
|
-
|
|
630
|
-
child_arcs.push({
|
|
631
|
-
parentId: pid,
|
|
632
|
-
childId: cur.id,
|
|
633
|
-
radius,
|
|
634
|
-
start,
|
|
635
|
-
end,
|
|
636
|
-
sweep
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
return child_arcs;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
574
|
/**
|
|
646
575
|
* radialLayout(node, opts?)
|
|
647
576
|
* opts:
|
|
@@ -685,13 +614,8 @@ function radialLayout(node, opts = {}) {
|
|
|
685
614
|
? getArcsFan(pd)
|
|
686
615
|
: getArcs(pd);
|
|
687
616
|
|
|
688
|
-
// per-child arcs for
|
|
689
|
-
|
|
690
|
-
if (arcsStyle === "fan") {
|
|
691
|
-
child_arcs = getChildArcsFan(pd);
|
|
692
|
-
} else {
|
|
693
|
-
child_arcs = getChildArcs(pd);
|
|
694
|
-
}
|
|
617
|
+
// per-child arcs for path highlighting: always parent.angle → child.angle at parent.r
|
|
618
|
+
const child_arcs = getChildArcs(pd);
|
|
695
619
|
|
|
696
620
|
return { data: pd, radii, arcs, child_arcs };
|
|
697
621
|
}
|
|
@@ -1028,21 +952,29 @@ function readTree(text) {
|
|
|
1028
952
|
text = String(text).replace(/\s+/g, '');
|
|
1029
953
|
|
|
1030
954
|
const tokens = text.split(/(;|\(|\)|,)/);
|
|
1031
|
-
const root = { parent: null, children: [] };
|
|
1032
|
-
let curnode = root;
|
|
1033
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;
|
|
1034
966
|
|
|
1035
967
|
for (const token of tokens) {
|
|
1036
968
|
if (!token || token === ';') continue;
|
|
1037
969
|
|
|
1038
970
|
if (token === '(') {
|
|
1039
|
-
const child =
|
|
971
|
+
const child = makeNode(curnode);
|
|
1040
972
|
curnode.children.push(child);
|
|
1041
973
|
curnode = child; // descend
|
|
1042
974
|
} else if (token === ',') {
|
|
1043
975
|
// back to parent, then create sibling
|
|
1044
976
|
curnode = curnode.parent;
|
|
1045
|
-
const child =
|
|
977
|
+
const child = makeNode(curnode);
|
|
1046
978
|
curnode.children.push(child);
|
|
1047
979
|
curnode = child;
|
|
1048
980
|
} else if (token === ')') {
|
|
@@ -1051,14 +983,15 @@ function readTree(text) {
|
|
|
1051
983
|
if (curnode === null) break;
|
|
1052
984
|
} else {
|
|
1053
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.
|
|
1054
989
|
const nodeinfo = token.split(':');
|
|
1055
990
|
if (nodeinfo.length === 1) {
|
|
1056
991
|
if (token.startsWith(':')) {
|
|
1057
|
-
curnode.label = '';
|
|
1058
992
|
curnode.branchLength = parseFloat(nodeinfo[0]);
|
|
1059
993
|
} else {
|
|
1060
994
|
curnode.label = nodeinfo[0];
|
|
1061
|
-
curnode.branchLength = null;
|
|
1062
995
|
}
|
|
1063
996
|
} else if (nodeinfo.length === 2) {
|
|
1064
997
|
curnode.label = nodeinfo[0];
|
|
@@ -1068,13 +1001,9 @@ function readTree(text) {
|
|
|
1068
1001
|
curnode.label = nodeinfo[0] || '';
|
|
1069
1002
|
curnode.branchLength = parseFloat(nodeinfo[nodeinfo.length - 1]);
|
|
1070
1003
|
}
|
|
1071
|
-
curnode.id = nodeId++; // assign then increment
|
|
1072
1004
|
}
|
|
1073
1005
|
}
|
|
1074
1006
|
|
|
1075
|
-
// Ensure root has an id if not assigned during parsing
|
|
1076
|
-
if (root.id == null) root.id = nodeId;
|
|
1077
|
-
|
|
1078
1007
|
return root;
|
|
1079
1008
|
}
|
|
1080
1009
|
|
|
@@ -1134,6 +1063,7 @@ function drawPhylogeny(
|
|
|
1134
1063
|
strokeWidth = 1, // for the phylogeny branches
|
|
1135
1064
|
radialMode = "outer", // "outer" (co-circular tips) or "phylo" (true terminals)
|
|
1136
1065
|
tipLabels = true,
|
|
1066
|
+
labelFontSize = 10, // font size (px) for tip labels
|
|
1137
1067
|
showTooltips = true,
|
|
1138
1068
|
tooltipFormatter = (d, rtt) =>
|
|
1139
1069
|
`${d.thisLabel ?? "(unnamed)"}\nroot→tip: ${(+rtt).toFixed(4)}`,
|
|
@@ -1181,7 +1111,7 @@ function drawPhylogeny(
|
|
|
1181
1111
|
const tips = horizontal.filter((d) => d.isTip);
|
|
1182
1112
|
|
|
1183
1113
|
// indices & root→tip getter
|
|
1184
|
-
const byId = new Map(
|
|
1114
|
+
const byId = new Map(tree_df.data.map((d) => [d.thisId, d])); // includes root
|
|
1185
1115
|
const tipById = new Map(tips.map((d) => [d.thisId, d]));
|
|
1186
1116
|
const tipByLabel = new Map(tips.map((d) => [d.thisLabel, d]));
|
|
1187
1117
|
const rootToTip = makeRootToTipGetter(byId, { prefer: "x1" });
|
|
@@ -1255,6 +1185,7 @@ function drawPhylogeny(
|
|
|
1255
1185
|
// interactive root→tip highlight (rect) on dot hover
|
|
1256
1186
|
tipDots
|
|
1257
1187
|
.on("mouseenter", function(_event, d) {
|
|
1188
|
+
hoverLayer.selectAll("*").remove();
|
|
1258
1189
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1259
1190
|
d3.select(this).attr("r", 4);
|
|
1260
1191
|
})
|
|
@@ -1274,7 +1205,7 @@ function drawPhylogeny(
|
|
|
1274
1205
|
.attr("x", (d) => xScale(d.x1) + 4)
|
|
1275
1206
|
.attr("y", (d) => yScale(d.y1))
|
|
1276
1207
|
.attr("dy", "0.32em")
|
|
1277
|
-
.attr("font-size",
|
|
1208
|
+
.attr("font-size", labelFontSize)
|
|
1278
1209
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1279
1210
|
|
|
1280
1211
|
if (showTooltips) {
|
|
@@ -1285,6 +1216,7 @@ function drawPhylogeny(
|
|
|
1285
1216
|
|
|
1286
1217
|
labels
|
|
1287
1218
|
.on("mouseenter", function(_event, d) {
|
|
1219
|
+
hoverLayer.selectAll("*").remove();
|
|
1288
1220
|
drawRectPath(d.thisId, hoverLayer, hoverStroke, hoverWidth);
|
|
1289
1221
|
d3.select(this).attr("font-weight", 600);
|
|
1290
1222
|
})
|
|
@@ -1311,7 +1243,6 @@ function drawPhylogeny(
|
|
|
1311
1243
|
|
|
1312
1244
|
// helper to draw root→tip for rect (both vertical+horizontal)
|
|
1313
1245
|
function drawRectPath(tipId, layer, stroke, width) {
|
|
1314
|
-
layer.selectAll("*").remove();
|
|
1315
1246
|
let cur = byId.get(tipId);
|
|
1316
1247
|
while (cur && cur.parentId != null) {
|
|
1317
1248
|
const parent = byId.get(cur.parentId);
|
|
@@ -1368,10 +1299,14 @@ function drawPhylogeny(
|
|
|
1368
1299
|
const END_CAP = 0;
|
|
1369
1300
|
|
|
1370
1301
|
// ===== SCALES / BOUNDS =====
|
|
1371
|
-
const maxRadius = d3.max(rad.data, (d) => d.r) ?? 0;
|
|
1372
|
-
const scaleRadial = maxRadius + 2 * radialMargin;
|
|
1373
1302
|
const w = width,
|
|
1374
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;
|
|
1375
1310
|
const centerX = w / 2,
|
|
1376
1311
|
centerY = h / 2;
|
|
1377
1312
|
|
|
@@ -1451,7 +1386,8 @@ function drawPhylogeny(
|
|
|
1451
1386
|
radiusPx(d.radius),
|
|
1452
1387
|
d.start,
|
|
1453
1388
|
d.end,
|
|
1454
|
-
d.sweep
|
|
1389
|
+
d.sweep ?? "ccw",
|
|
1390
|
+
d.largeArc ?? 0,
|
|
1455
1391
|
)
|
|
1456
1392
|
)
|
|
1457
1393
|
.attr("fill", "none")
|
|
@@ -1566,7 +1502,7 @@ function drawPhylogeny(
|
|
|
1566
1502
|
.attr("x", xoff)
|
|
1567
1503
|
.attr("alignment-baseline", "middle")
|
|
1568
1504
|
.attr("text-anchor", anchor)
|
|
1569
|
-
.attr("font-size",
|
|
1505
|
+
.attr("font-size", labelFontSize)
|
|
1570
1506
|
.attr("fill", "black")
|
|
1571
1507
|
.text((d) => d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1572
1508
|
});
|
|
@@ -1580,6 +1516,8 @@ function drawPhylogeny(
|
|
|
1580
1516
|
// label hover
|
|
1581
1517
|
labels
|
|
1582
1518
|
.on("mouseenter", function(_event, d) {
|
|
1519
|
+
hoverLines.selectAll("*").remove();
|
|
1520
|
+
hoverArcs.selectAll("*").remove();
|
|
1583
1521
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1584
1522
|
d3.select(this).select("text").attr("font-weight", 600);
|
|
1585
1523
|
})
|
|
@@ -1599,9 +1537,6 @@ function drawPhylogeny(
|
|
|
1599
1537
|
width = 3
|
|
1600
1538
|
) {
|
|
1601
1539
|
// target may be a tip node *or* a numeric tip id
|
|
1602
|
-
lineLayer.selectAll("*").remove();
|
|
1603
|
-
arcLayer.selectAll("*").remove();
|
|
1604
|
-
|
|
1605
1540
|
let cur = (typeof target === "number" || typeof target === "string")
|
|
1606
1541
|
? byId.get(target)
|
|
1607
1542
|
: target;
|
|
@@ -1640,18 +1575,10 @@ function drawPhylogeny(
|
|
|
1640
1575
|
const R = radiusPx(rec.radius);
|
|
1641
1576
|
return rec.sweep == null
|
|
1642
1577
|
? describeArc(centerX, centerY, R, rec.start, rec.end)
|
|
1643
|
-
: 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);
|
|
1644
1579
|
}
|
|
1645
1580
|
|
|
1646
1581
|
if (a) {
|
|
1647
|
-
console.log("Drawing arc:", {
|
|
1648
|
-
childId: cur.thisId,
|
|
1649
|
-
startDeg: (a.start * 180 / Math.PI).toFixed(2),
|
|
1650
|
-
endDeg: (a.end * 180 / Math.PI).toFixed(2),
|
|
1651
|
-
sweep: a.sweep,
|
|
1652
|
-
radius: a.radius
|
|
1653
|
-
});
|
|
1654
|
-
|
|
1655
1582
|
arcLayer
|
|
1656
1583
|
.append("path")
|
|
1657
1584
|
.attr("d", pathFromArcRecord(a))
|
|
@@ -1668,6 +1595,8 @@ function drawPhylogeny(
|
|
|
1668
1595
|
// tip dot hover
|
|
1669
1596
|
tipDots
|
|
1670
1597
|
.on("mouseenter", function(_event, d) {
|
|
1598
|
+
hoverLines.selectAll("*").remove();
|
|
1599
|
+
hoverArcs.selectAll("*").remove();
|
|
1671
1600
|
drawRadialPath(d, hoverLines, hoverArcs, hoverStroke, hoverWidth);
|
|
1672
1601
|
d3.select(this).attr("r", DOT_R + 2);
|
|
1673
1602
|
})
|
|
@@ -1824,7 +1753,7 @@ function drawPhylogeny(
|
|
|
1824
1753
|
.attr("x", xOffset)
|
|
1825
1754
|
.attr("alignment-baseline", "middle")
|
|
1826
1755
|
.attr("text-anchor", anchor)
|
|
1827
|
-
.attr("font-size",
|
|
1756
|
+
.attr("font-size", labelFontSize)
|
|
1828
1757
|
.attr("fill", "black")
|
|
1829
1758
|
.text(d.thisLabel?.replace(/_/g, " ") ?? "");
|
|
1830
1759
|
});
|