@euphrasiologist/lwphylo 1.2.7 → 1.2.9
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 +130 -98
- package/dist/drawPhylogeny.cjs.map +1 -1
- package/dist/drawPhylogeny.esm.js +130 -98
- 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 +141 -106
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +141 -106
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/drawPhylogeny.cjs
CHANGED
|
@@ -238,6 +238,37 @@ function getRadii(node) {
|
|
|
238
238
|
return segments;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
+
// src/radial/getRadiiFromPd.js
|
|
242
|
+
/**
|
|
243
|
+
* Build per-edge spokes using the *current* pd angles/r.
|
|
244
|
+
* Output: [{ parentId, childId, x0,y0,x1,y1, isTip }]
|
|
245
|
+
*/
|
|
246
|
+
function getRadiiFromPd(pd) {
|
|
247
|
+
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
248
|
+
const root = pd.find(d => d.parentId == null)?.thisId;
|
|
249
|
+
|
|
250
|
+
const segments = [];
|
|
251
|
+
for (const d of pd) {
|
|
252
|
+
if (d.thisId === root) continue;
|
|
253
|
+
const parent = byId.get(d.parentId);
|
|
254
|
+
if (!parent) continue;
|
|
255
|
+
|
|
256
|
+
const theta = d.angle;
|
|
257
|
+
const r0 = parent.r, r1 = d.r;
|
|
258
|
+
|
|
259
|
+
segments.push({
|
|
260
|
+
parentId: parent.thisId,
|
|
261
|
+
childId: d.thisId,
|
|
262
|
+
x0: r0 * Math.cos(theta),
|
|
263
|
+
y0: r0 * Math.sin(theta),
|
|
264
|
+
x1: r1 * Math.cos(theta),
|
|
265
|
+
y1: r1 * Math.sin(theta),
|
|
266
|
+
isTip: !!d.isTip
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return segments;
|
|
270
|
+
}
|
|
271
|
+
|
|
241
272
|
/**
|
|
242
273
|
* Build arc descriptors for each internal parent:
|
|
243
274
|
* - One arc per internal node at radius = parent.r
|
|
@@ -304,41 +335,48 @@ function getArcs(pd) {
|
|
|
304
335
|
}
|
|
305
336
|
|
|
306
337
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
338
|
+
* Build APE-like block arcs per internal parent:
|
|
339
|
+
* radius = parent.r
|
|
340
|
+
* start = first child's angle
|
|
341
|
+
* end = last child's angle
|
|
342
|
+
* sweep = 0 (CCW) if end>=start; 1 (CW) if wrapped across 2π
|
|
312
343
|
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
344
|
+
* @param {Array} pd nodes with {thisId,parentId,children,angle,r}
|
|
345
|
+
* @returns {Array} [{parentId,thisId,radius,start,end,sweep}]
|
|
315
346
|
*/
|
|
316
|
-
function
|
|
347
|
+
function getArcsFan(pd) {
|
|
317
348
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
318
349
|
const arcs = [];
|
|
319
350
|
|
|
320
|
-
for (const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
351
|
+
for (const p of pd) {
|
|
352
|
+
const c = p.children || [];
|
|
353
|
+
if (c.length < 2 || !(p.r > 0)) continue;
|
|
354
|
+
|
|
355
|
+
const first = byId.get(c[0])?.angle;
|
|
356
|
+
const last = byId.get(c[c.length - 1])?.angle;
|
|
357
|
+
if (first == null || last == null) continue;
|
|
358
|
+
|
|
359
|
+
const start = first;
|
|
360
|
+
const end = last;
|
|
361
|
+
const sweep = end >= start ? 0 : 1; // CW if wrapped
|
|
324
362
|
|
|
325
363
|
arcs.push({
|
|
326
|
-
parentId:
|
|
327
|
-
|
|
328
|
-
radius:
|
|
329
|
-
start
|
|
330
|
-
end
|
|
364
|
+
parentId: p.parentId,
|
|
365
|
+
thisId: p.thisId,
|
|
366
|
+
radius: p.r,
|
|
367
|
+
start,
|
|
368
|
+
end,
|
|
369
|
+
sweep
|
|
331
370
|
});
|
|
332
371
|
}
|
|
333
|
-
|
|
334
372
|
return arcs;
|
|
335
373
|
}
|
|
336
374
|
|
|
337
|
-
//
|
|
375
|
+
// APE-like "fan" angles: tips evenly spaced with open-angle gap & rotation,
|
|
376
|
+
// internal nodes = arithmetic mean of unwrapped child angles.
|
|
338
377
|
const TAU = Math.PI * 2;
|
|
339
378
|
const norm = (t) => ((t % TAU) + TAU) % TAU;
|
|
340
379
|
|
|
341
|
-
// Unwrap angles around a reference so they sit within [ref-π, ref+π]
|
|
342
380
|
function unwrapAround(ref, a) {
|
|
343
381
|
let x = a;
|
|
344
382
|
while (x < ref - Math.PI) x += TAU;
|
|
@@ -346,26 +384,17 @@ function unwrapAround(ref, a) {
|
|
|
346
384
|
return x;
|
|
347
385
|
}
|
|
348
386
|
|
|
349
|
-
/**
|
|
350
|
-
* Compute APE "fan" compatible angles:
|
|
351
|
-
* - Tips evenly spaced over [0, span] where span = 2π*(1 - 1/Ntip) - gap
|
|
352
|
-
* - Then + rotate (radians)
|
|
353
|
-
* - Internal nodes = arithmetic mean of child angles (unwrapped)
|
|
354
|
-
*
|
|
355
|
-
* pd: fortified nodes array (has thisId, parentId, children[])
|
|
356
|
-
* opts: { openAngleDeg=0, rotateDeg=0 }
|
|
357
|
-
* returns: Map(nodeId -> angle)
|
|
358
|
-
*/
|
|
359
387
|
function fanAngles(pd, opts = {}) {
|
|
360
388
|
const { openAngleDeg = 0, rotateDeg = 0 } = opts;
|
|
361
389
|
const gap = (openAngleDeg / 360) * TAU;
|
|
362
|
-
const
|
|
390
|
+
const rot = (rotateDeg / 360) * TAU;
|
|
363
391
|
|
|
364
|
-
//
|
|
392
|
+
// root + children index
|
|
365
393
|
let root = null;
|
|
366
|
-
const kids = new Map(pd.map(d => [d.thisId, d.children || []]));
|
|
394
|
+
const kids = new Map(pd.map((d) => [d.thisId, d.children || []]));
|
|
367
395
|
for (const d of pd) if (d.parentId == null) { root = d.thisId; break; }
|
|
368
396
|
|
|
397
|
+
// tip order (DFS left→right like your fortify)
|
|
369
398
|
const tipIds = [];
|
|
370
399
|
(function dfs(id) {
|
|
371
400
|
const c = kids.get(id) || [];
|
|
@@ -374,103 +403,106 @@ function fanAngles(pd, opts = {}) {
|
|
|
374
403
|
})(root);
|
|
375
404
|
|
|
376
405
|
const N = Math.max(1, tipIds.length);
|
|
377
|
-
|
|
378
|
-
const maxA = TAU * (1 - 1 / N) - gap;
|
|
406
|
+
const maxA = TAU * (1 - 1 / N) - gap; // note: no last-step overlap
|
|
379
407
|
const step = N > 1 ? maxA / (N - 1) : 0;
|
|
380
408
|
|
|
381
409
|
const angle = new Map();
|
|
382
|
-
tipIds.forEach((id, i) =>
|
|
383
|
-
angle.set(id, norm(i * step + rotate));
|
|
384
|
-
});
|
|
410
|
+
tipIds.forEach((id, i) => angle.set(id, norm(i * step + rot)));
|
|
385
411
|
|
|
386
|
-
//
|
|
412
|
+
// internal nodes: arithmetic mean of child angles (unwrapped)
|
|
387
413
|
(function setInternal(id) {
|
|
388
414
|
const c = kids.get(id) || [];
|
|
389
415
|
for (const ch of c) setInternal(ch);
|
|
390
|
-
if (c.length
|
|
391
|
-
// unwrap child angles around the first child's angle
|
|
416
|
+
if (c.length) {
|
|
392
417
|
const a0 = angle.get(c[0]);
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
angle.set(id, norm(mean));
|
|
418
|
+
const arr = c.map((ch) => unwrapAround(a0, angle.get(ch)));
|
|
419
|
+
angle.set(id, norm(arr.reduce((s, v) => s + v, 0) / arr.length));
|
|
396
420
|
}
|
|
397
421
|
})(root);
|
|
398
422
|
|
|
399
423
|
return angle;
|
|
400
424
|
}
|
|
401
425
|
|
|
402
|
-
// getArcsFan.js
|
|
403
|
-
|
|
404
426
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
427
|
+
* Per-child "half" arcs for radial trees.
|
|
428
|
+
*
|
|
429
|
+
* For each non-root node (child), emit an arc at the PARENT's radius that
|
|
430
|
+
* spans between the parent's angle and the child's angle. This is the arc
|
|
431
|
+
* segment that meets the child's spoke and is ideal for root→tip highlighting.
|
|
409
432
|
*
|
|
410
|
-
* pd
|
|
411
|
-
*
|
|
412
|
-
* where sweep=0 means CCW (start→end increasing),
|
|
413
|
-
* sweep=1 means CW (start→end decreasing across wrap).
|
|
433
|
+
* Input: pd — the array returned by radialData(node) (each row has .thisId, .parentId, .angle, .r)
|
|
434
|
+
* Output: [{ parentId, childId, radius, start, end }]
|
|
414
435
|
*/
|
|
415
|
-
function
|
|
436
|
+
function getChildArcs(pd) {
|
|
416
437
|
const byId = new Map(pd.map(d => [d.thisId, d]));
|
|
417
438
|
const arcs = [];
|
|
418
439
|
|
|
419
|
-
for (const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (A.length < 2 || !isFinite(p.r) || p.r <= 0) continue;
|
|
424
|
-
|
|
425
|
-
// Children are contiguous in tip order; take first and last
|
|
426
|
-
let start = A[0];
|
|
427
|
-
let end = A[A.length - 1];
|
|
428
|
-
|
|
429
|
-
// Decide direction like APE’s seq(start, end):
|
|
430
|
-
// if end >= start → CCW; else CW across wrap.
|
|
431
|
-
const sweep = end >= start ? 0 : 1;
|
|
440
|
+
for (const child of pd) {
|
|
441
|
+
if (child.parentId == null) continue; // skip root
|
|
442
|
+
const parent = byId.get(child.parentId);
|
|
443
|
+
if (!parent) continue;
|
|
432
444
|
|
|
433
445
|
arcs.push({
|
|
434
|
-
parentId:
|
|
435
|
-
|
|
436
|
-
radius:
|
|
437
|
-
start
|
|
446
|
+
parentId: parent.thisId,
|
|
447
|
+
childId: child.thisId,
|
|
448
|
+
radius: parent.r, // draw on the parent's circle
|
|
449
|
+
start: parent.angle, // start at parent's angle
|
|
450
|
+
end: child.angle // end at child's angle (describeArc will choose the shortest CCW span)
|
|
438
451
|
});
|
|
439
452
|
}
|
|
453
|
+
|
|
440
454
|
return arcs;
|
|
441
455
|
}
|
|
442
456
|
|
|
457
|
+
// src/radial/radialLayout.js
|
|
458
|
+
|
|
443
459
|
/**
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
460
|
+
* radialLayout(node, opts?)
|
|
461
|
+
* opts:
|
|
462
|
+
* - angleStrategy: "cmean" (default, your current) | "fan" (APE-like)
|
|
463
|
+
* - arcsStyle: "shortest" (default) | "fan" (block arcs, supports wrap)
|
|
464
|
+
* - openAngleDeg: number (gap wedge for "fan" angles)
|
|
465
|
+
* - rotateDeg: number (rotation for "fan" angles)
|
|
449
466
|
*/
|
|
450
|
-
function radialLayout(node, opts =
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
d
|
|
466
|
-
|
|
467
|
-
|
|
467
|
+
function radialLayout(node, opts = {}) {
|
|
468
|
+
const {
|
|
469
|
+
// default fan
|
|
470
|
+
angleStrategy = "fan",
|
|
471
|
+
arcsStyle = "fan",
|
|
472
|
+
openAngleDeg = 0,
|
|
473
|
+
rotateDeg = 0
|
|
474
|
+
} = opts;
|
|
475
|
+
|
|
476
|
+
// Start with your current enriched nodes (angle + r from radialData)
|
|
477
|
+
const pd = radialData(node);
|
|
478
|
+
|
|
479
|
+
if (angleStrategy === "fan") {
|
|
480
|
+
// overwrite angles with APE-like fan angles; keep radii r as-is
|
|
481
|
+
const angleMap = fanAngles(pd, { openAngleDeg, rotateDeg });
|
|
482
|
+
for (const d of pd) {
|
|
483
|
+
const a = angleMap.get(d.thisId);
|
|
484
|
+
if (a != null) {
|
|
485
|
+
d.angle = a;
|
|
486
|
+
d.x = d.r * Math.cos(a);
|
|
487
|
+
d.y = d.r * Math.sin(a);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
468
490
|
}
|
|
469
491
|
|
|
470
|
-
|
|
471
|
-
|
|
492
|
+
// Build spokes using the angles currently on pd
|
|
493
|
+
const radii = (angleStrategy === "fan")
|
|
494
|
+
? getRadiiFromPd(pd)
|
|
495
|
+
: getRadii(node);
|
|
472
496
|
|
|
473
|
-
|
|
497
|
+
// Choose arc builder
|
|
498
|
+
const arcs = (arcsStyle === "fan")
|
|
499
|
+
? getArcsFan(pd)
|
|
500
|
+
: getArcs(pd);
|
|
501
|
+
|
|
502
|
+
// per-child arcs for half-arc highlighting if you already use them
|
|
503
|
+
const child_arcs = getChildArcs(pd);
|
|
504
|
+
|
|
505
|
+
return { data: pd, radii, arcs, child_arcs };
|
|
474
506
|
}
|
|
475
507
|
|
|
476
508
|
/**
|