@euphrasiologist/lwphylo 1.1.4 → 1.1.7
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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// https://github.com/Euphrasiologist/lwPhylo#readme v1.1.
|
|
1
|
+
// https://github.com/Euphrasiologist/lwPhylo#readme v1.1.7 Copyright 2025 Max Brown
|
|
2
2
|
(function (global, factory) {
|
|
3
3
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
4
4
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
@@ -393,60 +393,60 @@ function reflectAngle (rad, dir) {
|
|
|
393
393
|
* arc from
|
|
394
394
|
*/
|
|
395
395
|
|
|
396
|
-
function getArcs(pd) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
396
|
+
function getArcs (pd) {
|
|
397
|
+
// must return start of arc and end
|
|
398
|
+
// these are pairs of edges that have the same parent
|
|
399
|
+
// start is the min(angle) of the children and end is the max(angle)
|
|
400
|
+
// radius is the radius of the parent
|
|
401
|
+
// origin is 0, 0
|
|
402
|
+
|
|
403
|
+
var data = [];
|
|
404
|
+
var root = pd.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
// get the branchlength of the parentID
|
|
408
|
+
function sisterAngle(current_parentId) {
|
|
409
|
+
for (var i = 0; i < pd.length; i++) {
|
|
410
|
+
if (pd[i].parentId === current_parentId) {
|
|
411
|
+
var sister_angle = pd[i].angle;
|
|
413
412
|
}
|
|
414
|
-
return sister_angle;
|
|
415
413
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
414
|
+
return sister_angle;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function parentRadius(current_parentId) {
|
|
418
|
+
for (var i = 0; i < pd.length; i++) {
|
|
419
|
+
if (pd[i].thisId === current_parentId) {
|
|
420
|
+
var parent_r = pd[i].r;
|
|
422
421
|
}
|
|
423
|
-
return parent_r;
|
|
424
422
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
423
|
+
return parent_r;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
for (let i = 0; i < pd.length; i++) {
|
|
427
|
+
if (pd[i].thisId !== root) {
|
|
428
|
+
data.push({
|
|
429
|
+
'start': reflectAngle(Math.min(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
|
|
430
|
+
'end': reflectAngle(Math.max(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
|
|
431
|
+
'radius': parentRadius(pd[i].parentId),
|
|
432
|
+
'thisId': pd[i].thisId,
|
|
433
|
+
'parentId': pd[i].parentId
|
|
434
|
+
|
|
435
|
+
});
|
|
437
436
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
437
|
+
}
|
|
438
|
+
// TODO: understand why this works (and it may not in every case)... test!
|
|
439
|
+
for (let i = 0; i < data.length; i++) {
|
|
440
|
+
if (Math.sign(data[i].start) !== Math.sign(data[i].end)) {
|
|
441
|
+
data[i].end = Math.abs(data[i].end);
|
|
442
|
+
data[i].start = -Math.abs(data[i].start);
|
|
444
443
|
}
|
|
445
|
-
|
|
446
|
-
return data.filter(d => d.start !== d.end & d.radius !== 0);
|
|
447
|
-
|
|
448
444
|
}
|
|
449
445
|
|
|
446
|
+
return data.filter(d => d.start !== d.end & d.radius !== 0);
|
|
447
|
+
|
|
448
|
+
}
|
|
449
|
+
|
|
450
450
|
/**
|
|
451
451
|
* Simple wrapper function for getting the data,
|
|
452
452
|
* radii, and arcs.
|
|
@@ -468,124 +468,79 @@ function radialLayout (node) {
|
|
|
468
468
|
* Attempted copy of .layout.rect() function here https://github.com/ArtPoon/ggfree/blob/master/R/tree.R
|
|
469
469
|
*/
|
|
470
470
|
|
|
471
|
-
function getHorizontal
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
//
|
|
475
|
-
pd.map(d => d.
|
|
476
|
-
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (pd[i].thisId === child_2) {
|
|
495
|
-
var y2 = pd[i].y0;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
return mean(([y1, y2]))
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// if the node is not a tip...
|
|
502
|
-
for (let i = 0; i < pd.length; i++) {
|
|
503
|
-
if (pd[i].isTip === false) {
|
|
504
|
-
// then y0 === y1 and is the mean of the parental nodes
|
|
505
|
-
pd[i].y0 = yVals(pd[i].children[0], pd[i].children[1]);
|
|
506
|
-
pd[i].y1 = yVals(pd[i].children[0], pd[i].children[1]);
|
|
471
|
+
function getHorizontal(node) {
|
|
472
|
+
const pd = fortify(node);
|
|
473
|
+
|
|
474
|
+
// Fast lookup from id -> pd index
|
|
475
|
+
const idIndex = new Map(pd.map((d, i) => [d.thisId, i]));
|
|
476
|
+
|
|
477
|
+
// 1) Leaf order from the INPUT TREE (respects your child order / ladderize)
|
|
478
|
+
const leafIds = [];
|
|
479
|
+
(function dfs(n) {
|
|
480
|
+
if (!n.children || n.children.length === 0) { leafIds.push(n.id); return; }
|
|
481
|
+
n.children.forEach(dfs);
|
|
482
|
+
})(node);
|
|
483
|
+
|
|
484
|
+
// Map each leaf id to a vertical slot (1..N)
|
|
485
|
+
const tipSlot = new Map(leafIds.map((id, i) => [id, i + 1]));
|
|
486
|
+
|
|
487
|
+
// 2) Set Y for tips directly from that order; compute Y for internal nodes via postorder
|
|
488
|
+
(function setY(n) {
|
|
489
|
+
const i = idIndex.get(n.id);
|
|
490
|
+
if (!n.children || n.children.length === 0) {
|
|
491
|
+
const y = tipSlot.get(n.id);
|
|
492
|
+
pd[i].y0 = y; pd[i].y1 = y;
|
|
493
|
+
return y;
|
|
507
494
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
// last loop...
|
|
527
|
-
// now get the x0 and x1 coordinates.
|
|
528
|
-
for (let i = 0; i < pd.length; i++) {
|
|
529
|
-
// special cases where parent is the root.
|
|
530
|
-
if (pd[i].parentId === root) {
|
|
531
|
-
// x0 = 0 and x1 is branch length
|
|
532
|
-
pd[i].x0 = 0;
|
|
533
|
-
pd[i].x1 = pd[i].branchLength;
|
|
534
|
-
} else {
|
|
535
|
-
// the x0 is that of the parent
|
|
536
|
-
var parent_branchLength = getParentBranchLength(pd[i].parentId);
|
|
537
|
-
pd[i].x0 = parent_branchLength;
|
|
538
|
-
// the x1 is the sum of parent and current branchlength
|
|
539
|
-
pd[i].x1 = parent_branchLength + pd[i].branchLength;
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// return the original sorted data
|
|
544
|
-
pd.sort((a, b) => a.thisId - b.thisId);
|
|
545
|
-
|
|
546
|
-
// remove root?
|
|
547
|
-
|
|
548
|
-
// finally get rid of unwanted y, x and angle properties
|
|
549
|
-
/* eslint-disable no-unused-vars */
|
|
495
|
+
const ys = n.children.map(setY);
|
|
496
|
+
const y = mean(ys);
|
|
497
|
+
pd[i].y0 = y; pd[i].y1 = y;
|
|
498
|
+
return y;
|
|
499
|
+
})(node);
|
|
500
|
+
|
|
501
|
+
// 3) Set X by accumulating branch lengths down the tree (no sorting-by-id needed)
|
|
502
|
+
(function setX(n, xParent) {
|
|
503
|
+
const i = idIndex.get(n.id);
|
|
504
|
+
const bl = pd[i].branchLength ?? 0;
|
|
505
|
+
const x0 = xParent ?? 0;
|
|
506
|
+
const x1 = x0 + bl;
|
|
507
|
+
pd[i].x0 = x0; pd[i].x1 = x1;
|
|
508
|
+
if (n.children && n.children.length) n.children.forEach(c => setX(c, x1));
|
|
509
|
+
})(node, 0);
|
|
510
|
+
|
|
511
|
+
// Clean up and return
|
|
550
512
|
return pd.map(({ y, x, angle, ...item }) => item);
|
|
551
|
-
/* eslint-enable no-unused-vars */
|
|
552
513
|
}
|
|
553
514
|
|
|
554
|
-
/**
|
|
555
|
-
* Get the vertical lines to draw.
|
|
556
|
-
*/
|
|
557
|
-
|
|
558
515
|
function getVertical (node) {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
//
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return height;
|
|
516
|
+
const data = getHorizontal(node);
|
|
517
|
+
|
|
518
|
+
// Group rows by parentId (children that share a parent)
|
|
519
|
+
const byParent = new Map();
|
|
520
|
+
for (const row of data) {
|
|
521
|
+
if (row.parentId == null) continue;
|
|
522
|
+
const a = byParent.get(row.parentId);
|
|
523
|
+
if (a) a.push(row); else byParent.set(row.parentId, [row]);
|
|
570
524
|
}
|
|
571
525
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
526
|
+
const verticals = [];
|
|
527
|
+
for (const [parentId, kids] of byParent.entries()) {
|
|
528
|
+
if (!kids.length) continue;
|
|
529
|
+
// Works for binary and multifurcations:
|
|
530
|
+
const yvals = kids.map(d => d.y0);
|
|
531
|
+
const y0 = Math.min(...yvals);
|
|
532
|
+
const y1 = Math.max(...yvals);
|
|
533
|
+
// All children share the same junction x (their x0)
|
|
534
|
+
const x = kids[0].x0;
|
|
535
|
+
|
|
536
|
+
verticals.push({
|
|
537
|
+
parentId,
|
|
538
|
+
x0: x,
|
|
539
|
+
x1: x,
|
|
540
|
+
y0,
|
|
541
|
+
y1,
|
|
542
|
+
heights: y1 - y0
|
|
543
|
+
});
|
|
589
544
|
}
|
|
590
545
|
|
|
591
546
|
return verticals;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// https://github.com/Euphrasiologist/lwPhylo#readme v1.1.
|
|
2
|
-
!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((n=n||self).lwphylo=n.lwphylo||{})}(this,(function(n){"use strict";const t={circular:()=>{var n,t,
|
|
1
|
+
// https://github.com/Euphrasiologist/lwPhylo#readme v1.1.7 Copyright 2025 Max Brown
|
|
2
|
+
!function(n,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((n=n||self).lwphylo=n.lwphylo||{})}(this,(function(n){"use strict";const t={circular:()=>{var n,t,e=200,r=2,a=[0,0],i={};function l(r){var l=i.xscale(r.x)-a[0],h=i.yscale(r.y)-a[1],s=Math.sqrt(l*l+h*h);if(!s||s>=e)return{x:i.xscale(r.x),y:i.yscale(r.y),z:s>=e?1:10};var o=n*(1-Math.exp(-s*t))/s*.75+.25;return{x:a[0]+l*o,y:a[1]+h*o,z:Math.min(o,10)}}function h(){return n=(n=Math.exp(r))/(n-1)*e,t=r/e,l}return l.radius=function(n){return arguments.length?(e=+n,h()):e},l.distortion=function(n){return arguments.length?(r=+n,h()):r},l.focus=function(n){return arguments.length?(a=n,l):a},l.scales=function(n,t){return arguments.length?(i={xscale:n,yscale:t},l):i},h()}};function e(n,t,e,r){return{x:n+e*Math.cos(r),y:t+e*Math.sin(r)}}function r(n,t=!0){var e=[];for(const t of function n(t,e=[]){e.push(t);for(var r=0;r<t.children.length;r++)e=n(t.children[r],e);return e}(n))null===t.parent?e.push({parentId:null,parentLabel:null,thisId:t.id,thisLabel:t.label,children:t.children.map(n=>n.id),branchLength:0,isTip:!1,x:t.x,y:t.y,angle:t.angle}):e.push({parentId:t.parent.id,parentLabel:t.parent.label,thisId:t.id,thisLabel:t.label,children:t.children.map(n=>n.id),branchLength:t.branchLength,isTip:0==t.children.length,x:t.x,y:t.y,angle:t.angle});return t&&(e=e.sort((function(n,t){return n.thisId-t.thisId}))),e}function a(n){var t=0;for(const e of function(n){for(var t,e=[n],r=[];e.length>0;){t=e.pop(),r.push(t);for(const n of t.children)e.push(n)}return r}(n))0==e.children.length&&t++;return t}function i(n,t){let e=0,r=0;if(void 0===t)for(let t of n)null!=t&&(t=+t)>=t&&(++e,r+=t);else{let a=-1;for(let i of n)null!=(i=t(i,++a,n))&&(i=+i)>=i&&(++e,r+=i)}if(e)return r/e}function l(n){var t=r(n),e=a(n),l=1;for(let n=0;n<t.length;n++)1==t[n].isTip&&(t[n].angle=l/e*2*Math.PI,l+=1);function h(n,e){for(var r=0;r<t.length;r++){if(t[r].thisId===n)var a=t[r].angle;if(t[r].thisId===e)var l=t[r].angle}return i([a,l])}for(let n=0;n<t.length;n++)!1===t[n].isTip&&(t[n].angle=h(t[n].children[0],t[n].children[1]));var s=t.map(n=>null===n.parentId?n.thisId:null).filter(n=>null!=n)[0];function o(n){for(var e=0;e<t.length;e++)if(t[e].thisId===n)var r=t[e].r;return r}t.sort((n,t)=>t.thisId-n.thisId);for(let n=0;n<t.length;n++)if(t[n].parentId===s)!0===t[n].isTip?(t[n].r=t[n].branchLength,t[n].x=t[n].branchLength,t[n].y=0):(t[n].r=t[n].branchLength,t[n].x=t[n].branchLength*Math.cos(t[n].angle),t[n].y=t[n].branchLength*Math.sin(t[n].angle));else{var c=o(t[n].parentId);t[n].r=c+t[n].branchLength,t[n].x=(c+t[n].branchLength)*Math.cos(t[n].angle),t[n].y=(c+t[n].branchLength)*Math.sin(t[n].angle)}return t[0].r=0,t[0].x=0,t[0].y=0,t}function h(n,t){const e=Math.cos(n),r=Math.sin(n);return(a=Math.atan2(..."X"===t?[r,-e]:[-r,e])).toString().includes("3.1415")&&(a=Math.abs(a)),a;var a}function s(n){const t=r(n),e=new Map(t.map((n,t)=>[n.thisId,t])),a=[];!function n(t){t.children&&0!==t.children.length?t.children.forEach(n):a.push(t.id)}(n);const l=new Map(a.map((n,t)=>[n,t+1]));return function n(r){const a=e.get(r.id);if(!r.children||0===r.children.length){const n=l.get(r.id);return t[a].y0=n,t[a].y1=n,n}const h=i(r.children.map(n));return t[a].y0=h,t[a].y1=h,h}(n),function n(r,a){const i=e.get(r.id),l=a??0,h=l+(t[i].branchLength??0);t[i].x0=l,t[i].x1=h,r.children&&r.children.length&&r.children.forEach(t=>n(t,h))}(n,0),t.map(({y:n,x:t,angle:e,...r})=>r)}n.describeArc=function(n,t,r,a,i){var l=e(n,t,r,a),h=e(n,t,r,i);return["M",l.x,l.y,"A",r,r,0,0,0,h.x,h.y].join(" ")},n.parentFisheye=function(n,t){for(let e=0;e<t.length;e++)if(n.parentId===t[e].thisId)return{px:t[e].fisheye.x,py:t[e].fisheye.y}},n.phisheye=t,n.radialLayout=function(n){var t={};return t.data=l(n),t.radii=function(n){var t=l(n);function e(n){for(var e=0;e<t.length;e++)if(t[e].thisId===n.parentId)var r=t[e].r;return r}for(var r=[],a=t.map(n=>null===n.parentId?n.thisId:null).filter(n=>null!=n)[0],i=0;i<t.length;i++)t[i].thisId!==a&&r.push({thisId:t[i].thisId,thisLabel:t[i].thisLabel,x0:t[i].x,x1:e(t[i])*Math.cos(t[i].angle),y0:t[i].y,y1:e(t[i])*Math.sin(t[i].angle),isTip:t[i].isTip});return r}(n),t.arcs=function(n){var t=[],e=n.map(n=>null===n.parentId?n.thisId:null).filter(n=>null!=n)[0];function r(t){for(var e=0;e<n.length;e++)if(n[e].parentId===t)var r=n[e].angle;return r}function a(t){for(var e=0;e<n.length;e++)if(n[e].thisId===t)var r=n[e].r;return r}for(let i=0;i<n.length;i++)n[i].thisId!==e&&t.push({start:h(Math.min(n[i].angle,r(n[i].parentId)),"Y"),end:h(Math.max(n[i].angle,r(n[i].parentId)),"Y"),radius:a(n[i].parentId),thisId:n[i].thisId,parentId:n[i].parentId});for(let n=0;n<t.length;n++)Math.sign(t[n].start)!==Math.sign(t[n].end)&&(t[n].end=Math.abs(t[n].end),t[n].start=-Math.abs(t[n].start));return t.filter(n=>n.start!==n.end&0!==n.radius)}(t.data),t},n.readTree=function(n){var t=(n=n.replace(/ \t/g,"")).split(/(;|\(|\)|,)/),e={parent:null,children:[]},r=e,a=0;for(const n of t)if(""!=n&&";"!=n)if("("==n){let n={parent:r,children:[]};r.children.push(n),r=n}else if(","==n){let n={parent:r=r.parent,children:[]};r.children.push(n),r=n}else if(")"==n){if(null===(r=r.parent))break}else{var i=n.split(":");1==i.length?n.startsWith(":")?(r.label="",r.branchLength=parseFloat(i[0])):(r.label=i[0],r.branchLength=null):2==i.length?(r.label=i[0],r.branchLength=parseFloat(i[1])):console.warn(n,"I don't know what to do with two colons!"),r.id=a++}return r.id=a,e},n.rectangleLayout=function(n){var t={};return t.data=s(n),t.vertical_lines=function(n){const t=s(n),e=new Map;for(const n of t){if(null==n.parentId)continue;const t=e.get(n.parentId);t?t.push(n):e.set(n.parentId,[n])}const r=[];for(const[n,t]of e.entries()){if(!t.length)continue;const e=t.map(n=>n.y0),a=Math.min(...e),i=Math.max(...e),l=t[0].x0;r.push({parentId:n,x0:l,x1:l,y0:a,y1:i,heights:i-a})}return r}(n),t},n.subTree=function(n,t){let e={};n.data.forEach(n=>{e[n.thisId]={...n}});let r={};const a=function(n){e[n]&&(r[n]=e[n],e[n].children&&e[n].children.forEach(n=>{a(n)}))};a(t);const i=[["data",Object.values(r)]],l=i[0][1].map(n=>n.thisId);var h=[];for(const t in n)"data"!==t&&h.push([t,n[t]]);var s=h.map(n=>[n[0],n[1].filter(n=>l.includes(n.thisId))]);return Object.fromEntries(i.concat(s))},n.unrooted=function(n){var t={},e=r(function n(t){null===t.parent&&(t.start=0,t.end=2,t.angle=0,t.ntips=a(t),t.x=0,t.y=0);for(var e,r,i=t.start,l=0;l<t.children.length;l++)(e=t.children[l]).ntips=a(e),r=(t.end-t.start)*e.ntips/t.ntips,e.start=i,e.end=e.start+r,e.angle=e.start+(e.end-e.start)/2,i=e.end,e.x=t.x+e.branchLength*Math.sin(e.angle*Math.PI),e.y=t.y+e.branchLength*Math.cos(e.angle*Math.PI),n(e);return t}(n));return t.data=e,t.edges=function(n,t=!1){var e,r=[];n.sort((function(n,t){return n.thisId-t.thisId}));for(const h of n)if(null!==h.parentId&&null!=(e=n[h.parentId]))if(t){var a={x1:h.x,y1:h.y,id1:h.thisId,x2:e.x,y2:h.y,id2:void 0};r.push(a);var i={x1:e.x,y1:h.y,id1:void 0,x2:e.x,y2:e.y,id2:h.parentId};r.push(i)}else{var l={x1:h.x,y1:h.y,id1:h.thisId,x2:e.x,y2:e.y,id2:h.parentId};r.push(l)}return r}(e),t},Object.defineProperty(n,"__esModule",{value:!0})}));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@euphrasiologist/lwphylo",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "A lightweight, low level javascript library to plot phylogenies from Newick files.",
|
|
5
5
|
"main": "dist/@euphrasiologist/lwphylo.js",
|
|
6
6
|
"unpkg": "dist/@euphrasiologist/lwphylo.min.js",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"pretest": "rollup -c",
|
|
18
18
|
"test": "eslint src",
|
|
19
|
-
"prepublishOnly": "rm -rf dist &&
|
|
19
|
+
"prepublishOnly": "rm -rf dist && npm run test"
|
|
20
20
|
},
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
@@ -38,4 +38,4 @@
|
|
|
38
38
|
"rollup": "1",
|
|
39
39
|
"rollup-plugin-terser": "5"
|
|
40
40
|
}
|
|
41
|
-
}
|
|
41
|
+
}
|
package/src/radial/getArcs.js
CHANGED
|
@@ -6,56 +6,56 @@ import reflectAngle from "./reflectAngle.js"
|
|
|
6
6
|
* arc from
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export default function(pd) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
9
|
+
export default function (pd) {
|
|
10
|
+
// must return start of arc and end
|
|
11
|
+
// these are pairs of edges that have the same parent
|
|
12
|
+
// start is the min(angle) of the children and end is the max(angle)
|
|
13
|
+
// radius is the radius of the parent
|
|
14
|
+
// origin is 0, 0
|
|
15
|
+
|
|
16
|
+
var data = [];
|
|
17
|
+
var root = pd.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
// get the branchlength of the parentID
|
|
21
|
+
function sisterAngle(current_parentId) {
|
|
22
|
+
for (var i = 0; i < pd.length; i++) {
|
|
23
|
+
if (pd[i].parentId === current_parentId) {
|
|
24
|
+
var sister_angle = pd[i].angle;
|
|
26
25
|
}
|
|
27
|
-
return sister_angle;
|
|
28
26
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
return sister_angle;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function parentRadius(current_parentId) {
|
|
31
|
+
for (var i = 0; i < pd.length; i++) {
|
|
32
|
+
if (pd[i].thisId === current_parentId) {
|
|
33
|
+
var parent_r = pd[i].r;
|
|
35
34
|
}
|
|
36
|
-
return parent_r;
|
|
37
35
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
return parent_r;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < pd.length; i++) {
|
|
40
|
+
if (pd[i].thisId !== root) {
|
|
41
|
+
data.push({
|
|
42
|
+
'start': reflectAngle(Math.min(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
|
|
43
|
+
'end': reflectAngle(Math.max(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
|
|
44
|
+
'radius': parentRadius(pd[i].parentId),
|
|
45
|
+
'thisId': pd[i].thisId,
|
|
46
|
+
'parentId': pd[i].parentId
|
|
47
|
+
|
|
48
|
+
})
|
|
50
49
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
}
|
|
51
|
+
// TODO: understand why this works (and it may not in every case)... test!
|
|
52
|
+
for (let i = 0; i < data.length; i++) {
|
|
53
|
+
if (Math.sign(data[i].start) !== Math.sign(data[i].end)) {
|
|
54
|
+
data[i].end = Math.abs(data[i].end);
|
|
55
|
+
data[i].start = -Math.abs(data[i].start);
|
|
57
56
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return data.filter(d => d.start !== d.end & d.radius !== 0);
|
|
60
|
+
|
|
61
|
+
}
|
|
@@ -6,85 +6,46 @@ import fortify from "../utils/fortify.js"
|
|
|
6
6
|
* Attempted copy of .layout.rect() function here https://github.com/ArtPoon/ggfree/blob/master/R/tree.R
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export default function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
pd.map(d => d.
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
9
|
+
export default function(node) {
|
|
10
|
+
const pd = fortify(node);
|
|
11
|
+
|
|
12
|
+
// Fast lookup from id -> pd index
|
|
13
|
+
const idIndex = new Map(pd.map((d, i) => [d.thisId, i]));
|
|
14
|
+
|
|
15
|
+
// 1) Leaf order from the INPUT TREE (respects your child order / ladderize)
|
|
16
|
+
const leafIds = [];
|
|
17
|
+
(function dfs(n) {
|
|
18
|
+
if (!n.children || n.children.length === 0) { leafIds.push(n.id); return; }
|
|
19
|
+
n.children.forEach(dfs);
|
|
20
|
+
})(node);
|
|
21
|
+
|
|
22
|
+
// Map each leaf id to a vertical slot (1..N)
|
|
23
|
+
const tipSlot = new Map(leafIds.map((id, i) => [id, i + 1]));
|
|
24
|
+
|
|
25
|
+
// 2) Set Y for tips directly from that order; compute Y for internal nodes via postorder
|
|
26
|
+
(function setY(n) {
|
|
27
|
+
const i = idIndex.get(n.id);
|
|
28
|
+
if (!n.children || n.children.length === 0) {
|
|
29
|
+
const y = tipSlot.get(n.id);
|
|
30
|
+
pd[i].y0 = y; pd[i].y1 = y;
|
|
31
|
+
return y;
|
|
22
32
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
for (let i = 0; i < pd.length; i++) {
|
|
41
|
-
if (pd[i].isTip === false) {
|
|
42
|
-
// then y0 === y1 and is the mean of the parental nodes
|
|
43
|
-
pd[i].y0 = yVals(pd[i].children[0], pd[i].children[1]);
|
|
44
|
-
pd[i].y1 = yVals(pd[i].children[0], pd[i].children[1]);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// find root
|
|
49
|
-
var root = pd.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
|
|
50
|
-
|
|
51
|
-
// sort the data temporarily in decreasing parentId
|
|
52
|
-
pd.sort((a, b) => b.thisId - a.thisId);
|
|
53
|
-
|
|
54
|
-
// get the branchlength of the parentID
|
|
55
|
-
function getParentBranchLength(current_parentId) {
|
|
56
|
-
for (var i = 0; i < pd.length; i++) {
|
|
57
|
-
if (pd[i].thisId === current_parentId) {
|
|
58
|
-
var branchLength = pd[i].x1;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return branchLength;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// last loop...
|
|
65
|
-
// now get the x0 and x1 coordinates.
|
|
66
|
-
for (let i = 0; i < pd.length; i++) {
|
|
67
|
-
// special cases where parent is the root.
|
|
68
|
-
if (pd[i].parentId === root) {
|
|
69
|
-
// x0 = 0 and x1 is branch length
|
|
70
|
-
pd[i].x0 = 0;
|
|
71
|
-
pd[i].x1 = pd[i].branchLength;
|
|
72
|
-
} else {
|
|
73
|
-
// the x0 is that of the parent
|
|
74
|
-
var parent_branchLength = getParentBranchLength(pd[i].parentId);
|
|
75
|
-
pd[i].x0 = parent_branchLength;
|
|
76
|
-
// the x1 is the sum of parent and current branchlength
|
|
77
|
-
pd[i].x1 = parent_branchLength + pd[i].branchLength;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// return the original sorted data
|
|
82
|
-
pd.sort((a, b) => a.thisId - b.thisId);
|
|
83
|
-
|
|
84
|
-
// remove root?
|
|
85
|
-
|
|
86
|
-
// finally get rid of unwanted y, x and angle properties
|
|
87
|
-
/* eslint-disable no-unused-vars */
|
|
33
|
+
const ys = n.children.map(setY);
|
|
34
|
+
const y = mean(ys);
|
|
35
|
+
pd[i].y0 = y; pd[i].y1 = y;
|
|
36
|
+
return y;
|
|
37
|
+
})(node);
|
|
38
|
+
|
|
39
|
+
// 3) Set X by accumulating branch lengths down the tree (no sorting-by-id needed)
|
|
40
|
+
(function setX(n, xParent) {
|
|
41
|
+
const i = idIndex.get(n.id);
|
|
42
|
+
const bl = pd[i].branchLength ?? 0;
|
|
43
|
+
const x0 = xParent ?? 0;
|
|
44
|
+
const x1 = x0 + bl;
|
|
45
|
+
pd[i].x0 = x0; pd[i].x1 = x1;
|
|
46
|
+
if (n.children && n.children.length) n.children.forEach(c => setX(c, x1));
|
|
47
|
+
})(node, 0);
|
|
48
|
+
|
|
49
|
+
// Clean up and return
|
|
88
50
|
return pd.map(({ y, x, angle, ...item }) => item);
|
|
89
|
-
|
|
90
|
-
}
|
|
51
|
+
}
|
|
@@ -1,41 +1,35 @@
|
|
|
1
1
|
import getHorizontal from "./getHorizontal.js"
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Get the vertical lines to draw.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
3
|
export default function (node) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return height;
|
|
4
|
+
const data = getHorizontal(node);
|
|
5
|
+
|
|
6
|
+
// Group rows by parentId (children that share a parent)
|
|
7
|
+
const byParent = new Map();
|
|
8
|
+
for (const row of data) {
|
|
9
|
+
if (row.parentId == null) continue;
|
|
10
|
+
const a = byParent.get(row.parentId);
|
|
11
|
+
if (a) a.push(row); else byParent.set(row.parentId, [row]);
|
|
19
12
|
}
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
14
|
+
const verticals = [];
|
|
15
|
+
for (const [parentId, kids] of byParent.entries()) {
|
|
16
|
+
if (!kids.length) continue;
|
|
17
|
+
// Works for binary and multifurcations:
|
|
18
|
+
const yvals = kids.map(d => d.y0);
|
|
19
|
+
const y0 = Math.min(...yvals);
|
|
20
|
+
const y1 = Math.max(...yvals);
|
|
21
|
+
// All children share the same junction x (their x0)
|
|
22
|
+
const x = kids[0].x0;
|
|
23
|
+
|
|
24
|
+
verticals.push({
|
|
25
|
+
parentId,
|
|
26
|
+
x0: x,
|
|
27
|
+
x1: x,
|
|
28
|
+
y0,
|
|
29
|
+
y1,
|
|
30
|
+
heights: y1 - y0
|
|
31
|
+
});
|
|
38
32
|
}
|
|
39
33
|
|
|
40
34
|
return verticals;
|
|
41
|
-
}
|
|
35
|
+
}
|