@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.4 Copyright 2021 Max Brown
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
- // 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;
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
- 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;
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
- for (let i = 0; i < pd.length; i++) {
427
- if (pd[i].thisId !== root) {
428
- data.push({
429
- 'start': reflectAngle(Math.max(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
430
- 'end': reflectAngle(Math.min(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
- });
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
- // 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);
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 (node) {
472
- // phylodata layout...
473
- var pd = fortify(node);
474
- // set y to null... not needed here.
475
- pd.map(d => d.y = null);
476
- // where y corresponds to a tip, return tip number
477
- // make y0 and y1 equal.
478
- var tipID = 1;
479
- for (let i = 0; i < pd.length; i++) {
480
- if (pd[i].isTip === true) {
481
- pd[i].y0 = tipID;
482
- pd[i].y1 = tipID;
483
- tipID += 1;
484
- }
485
- }
486
-
487
- // probably incredibly inefficient for large trees.
488
- // gets the y values of two child branches by looping through the whole tree...
489
- function yVals(child_1, child_2) {
490
- for (var i = 0; i < pd.length; i++) {
491
- if (pd[i].thisId === child_1) {
492
- var y1 = pd[i].y0;
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
- // find root
511
- var root = pd.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
512
-
513
- // sort the data temporarily in decreasing parentId
514
- pd.sort((a, b) => b.thisId - a.thisId);
515
-
516
- // get the branchlength of the parentID
517
- function getParentBranchLength(current_parentId) {
518
- for (var i = 0; i < pd.length; i++) {
519
- if (pd[i].thisId === current_parentId) {
520
- var branchLength = pd[i].x1;
521
- }
522
- }
523
- return branchLength;
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
- var data = getHorizontal(node);
560
-
561
- // for the current iteration of the loop find the matching parentId
562
- // then take the difference
563
- function findPairs(current_node) {
564
- for (var i = 0; i < data.length; i++) {
565
- if (data[i].parentId === current_node.parentId) {
566
- var height = Math.abs(data[i].y0 - current_node.y0);
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
- var verticals = [];
573
- // find root
574
- var root = data.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
575
-
576
- for (var i = 0; i < data.length; i++) {
577
- if (data[i].thisId !== root) {
578
-
579
- verticals.push({
580
- 'thisId': data[i].thisId,
581
- 'x0': data[i].x0,
582
- 'x1': data[i].x0, // x values remain constant
583
- 'y0': data[i].y0,
584
- 'y1': data[i].y0 + findPairs(data[i]),
585
- 'heights': findPairs(data[i])
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.4 Copyright 2021 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,r=200,e=2,a=[0,0],i={};function l(e){var l=i.xscale(e.x)-a[0],h=i.yscale(e.y)-a[1],s=Math.sqrt(l*l+h*h);if(!s||s>=r)return{x:i.xscale(e.x),y:i.yscale(e.y),z:s>=r?1:10};var d=n*(1-Math.exp(-s*t))/s*.75+.25;return{x:a[0]+l*d,y:a[1]+h*d,z:Math.min(d,10)}}function h(){return n=(n=Math.exp(e))/(n-1)*r,t=e/r,l}return l.radius=function(n){return arguments.length?(r=+n,h()):r},l.distortion=function(n){return arguments.length?(e=+n,h()):e},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 r(n,t,r,e){return{x:n+r*Math.cos(e),y:t+r*Math.sin(e)}}function e(n,t=!0){var r=[];for(const t of function n(t,r=[]){r.push(t);for(var e=0;e<t.children.length;e++)r=n(t.children[e],r);return r}(n))null===t.parent?r.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}):r.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&&(r=r.sort((function(n,t){return n.thisId-t.thisId}))),r}function a(n){var t=0;for(const r of function(n){for(var t,r=[n],e=[];r.length>0;){t=r.pop(),e.push(t);for(const n of t.children)r.push(n)}return e}(n))0==r.children.length&&t++;return t}function i(n,t){let r=0,e=0;if(void 0===t)for(let t of n)null!=t&&(t=+t)>=t&&(++r,e+=t);else{let a=-1;for(let i of n)null!=(i=t(i,++a,n))&&(i=+i)>=i&&(++r,e+=i)}if(r)return e/r}function l(n){var t=e(n),r=a(n),l=1;for(let n=0;n<t.length;n++)1==t[n].isTip&&(t[n].angle=l/r*2*Math.PI,l+=1);function h(n,r){for(var e=0;e<t.length;e++){if(t[e].thisId===n)var a=t[e].angle;if(t[e].thisId===r)var l=t[e].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 d(n){for(var r=0;r<t.length;r++)if(t[r].thisId===n)var e=t[r].r;return e}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 o=d(t[n].parentId);t[n].r=o+t[n].branchLength,t[n].x=(o+t[n].branchLength)*Math.cos(t[n].angle),t[n].y=(o+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 r=Math.cos(n),e=Math.sin(n);return(a=Math.atan2(..."X"===t?[e,-r]:[-e,r])).toString().includes("3.1415")&&(a=Math.abs(a)),a;var a}function s(n){var t=e(n);t.map(n=>n.y=null);var r=1;for(let n=0;n<t.length;n++)!0===t[n].isTip&&(t[n].y0=r,t[n].y1=r,r+=1);function a(n,r){for(var e=0;e<t.length;e++){if(t[e].thisId===n)var a=t[e].y0;if(t[e].thisId===r)var l=t[e].y0}return i([a,l])}for(let n=0;n<t.length;n++)!1===t[n].isTip&&(t[n].y0=a(t[n].children[0],t[n].children[1]),t[n].y1=a(t[n].children[0],t[n].children[1]));var l=t.map(n=>null===n.parentId?n.thisId:null).filter(n=>null!=n)[0];function h(n){for(var r=0;r<t.length;r++)if(t[r].thisId===n)var e=t[r].x1;return e}t.sort((n,t)=>t.thisId-n.thisId);for(let n=0;n<t.length;n++)if(t[n].parentId===l)t[n].x0=0,t[n].x1=t[n].branchLength;else{var s=h(t[n].parentId);t[n].x0=s,t[n].x1=s+t[n].branchLength}return t.sort((n,t)=>n.thisId-t.thisId),t.map(({y:n,x:t,angle:r,...e})=>e)}n.describeArc=function(n,t,e,a,i){var l=r(n,t,e,a),h=r(n,t,e,i);return["M",l.x,l.y,"A",e,e,0,0,0,h.x,h.y].join(" ")},n.parentFisheye=function(n,t){for(let r=0;r<t.length;r++)if(n.parentId===t[r].thisId)return{px:t[r].fisheye.x,py:t[r].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 r(n){for(var r=0;r<t.length;r++)if(t[r].thisId===n.parentId)var e=t[r].r;return e}for(var e=[],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&&e.push({thisId:t[i].thisId,thisLabel:t[i].thisLabel,x0:t[i].x,x1:r(t[i])*Math.cos(t[i].angle),y0:t[i].y,y1:r(t[i])*Math.sin(t[i].angle),isTip:t[i].isTip});return e}(n),t.arcs=function(n){var t=[],r=n.map(n=>null===n.parentId?n.thisId:null).filter(n=>null!=n)[0];function e(t){for(var r=0;r<n.length;r++)if(n[r].parentId===t)var e=n[r].angle;return e}function a(t){for(var r=0;r<n.length;r++)if(n[r].thisId===t)var e=n[r].r;return e}for(let i=0;i<n.length;i++)n[i].thisId!==r&&t.push({start:h(Math.max(n[i].angle,e(n[i].parentId)),"Y"),end:h(Math.min(n[i].angle,e(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(/(;|\(|\)|,)/),r={parent:null,children:[]},e=r,a=0;for(const n of t)if(""!=n&&";"!=n)if("("==n){let n={parent:e,children:[]};e.children.push(n),e=n}else if(","==n){let n={parent:e=e.parent,children:[]};e.children.push(n),e=n}else if(")"==n){if(null===(e=e.parent))break}else{var i=n.split(":");1==i.length?n.startsWith(":")?(e.label="",e.branchLength=parseFloat(i[0])):(e.label=i[0],e.branchLength=null):2==i.length?(e.label=i[0],e.branchLength=parseFloat(i[1])):console.warn(n,"I don't know what to do with two colons!"),e.id=a++}return e.id=a,r},n.rectangleLayout=function(n){var t={};return t.data=s(n),t.vertical_lines=function(n){var t=s(n);function r(n){for(var r=0;r<t.length;r++)if(t[r].parentId===n.parentId)var e=Math.abs(t[r].y0-n.y0);return e}for(var e=[],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&&e.push({thisId:t[i].thisId,x0:t[i].x0,x1:t[i].x0,y0:t[i].y0,y1:t[i].y0+r(t[i]),heights:r(t[i])});return e}(n),t},n.subTree=function(n,t){let r={};n.data.forEach(n=>{r[n.thisId]={...n}});let e={};const a=function(n){r[n]&&(e[n]=r[n],r[n].children&&r[n].children.forEach(n=>{a(n)}))};a(t);const i=[["data",Object.values(e)]],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={},r=e(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 r,e,i=t.start,l=0;l<t.children.length;l++)(r=t.children[l]).ntips=a(r),e=(t.end-t.start)*r.ntips/t.ntips,r.start=i,r.end=r.start+e,r.angle=r.start+(r.end-r.start)/2,i=r.end,r.x=t.x+r.branchLength*Math.sin(r.angle*Math.PI),r.y=t.y+r.branchLength*Math.cos(r.angle*Math.PI),n(r);return t}(n));return t.data=r,t.edges=function(n,t=!1){var r,e=[];n.sort((function(n,t){return n.thisId-t.thisId}));for(const h of n)if(null!==h.parentId&&null!=(r=n[h.parentId]))if(t){var a={x1:h.x,y1:h.y,id1:h.thisId,x2:r.x,y2:h.y,id2:void 0};e.push(a);var i={x1:r.x,y1:h.y,id1:void 0,x2:r.x,y2:r.y,id2:h.parentId};e.push(i)}else{var l={x1:h.x,y1:h.y,id1:h.thisId,x2:r.x,y2:r.y,id2:h.parentId};e.push(l)}return e}(r),t},Object.defineProperty(n,"__esModule",{value:!0})}));
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.4",
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 && /Users/mb39/homebrew/bin/yarn test"
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
+ }
@@ -6,56 +6,56 @@ import reflectAngle from "./reflectAngle.js"
6
6
  * arc from
7
7
  */
8
8
 
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;
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
- 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;
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
- for (let i = 0; i < pd.length; i++) {
40
- if (pd[i].thisId !== root) {
41
- data.push({
42
- 'start': reflectAngle(Math.max(pd[i].angle, sisterAngle(pd[i].parentId)), "Y"),
43
- 'end': reflectAngle(Math.min(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
- })
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
- // 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);
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
- return data.filter(d => d.start !== d.end & d.radius !== 0);
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 (node) {
10
- // phylodata layout...
11
- var pd = fortify(node);
12
- // set y to null... not needed here.
13
- pd.map(d => d.y = null);
14
- // where y corresponds to a tip, return tip number
15
- // make y0 and y1 equal.
16
- var tipID = 1;
17
- for (let i = 0; i < pd.length; i++) {
18
- if (pd[i].isTip === true) {
19
- pd[i].y0 = tipID;
20
- pd[i].y1 = tipID;
21
- tipID += 1;
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
- // probably incredibly inefficient for large trees.
26
- // gets the y values of two child branches by looping through the whole tree...
27
- function yVals(child_1, child_2) {
28
- for (var i = 0; i < pd.length; i++) {
29
- if (pd[i].thisId === child_1) {
30
- var y1 = pd[i].y0;
31
- }
32
- if (pd[i].thisId === child_2) {
33
- var y2 = pd[i].y0;
34
- }
35
- }
36
- return mean(([y1, y2]))
37
- }
38
-
39
- // if the node is not a tip...
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
- /* eslint-enable no-unused-vars */
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
- var data = getHorizontal(node);
9
-
10
- // for the current iteration of the loop find the matching parentId
11
- // then take the difference
12
- function findPairs(current_node) {
13
- for (var i = 0; i < data.length; i++) {
14
- if (data[i].parentId === current_node.parentId) {
15
- var height = Math.abs(data[i].y0 - current_node.y0);
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
- var verticals = [];
22
- // find root
23
- var root = data.map(d => d.parentId === null ? d.thisId : null).filter(d => d != null)[0];
24
-
25
- for (var i = 0; i < data.length; i++) {
26
- if (data[i].thisId !== root) {
27
-
28
- verticals.push({
29
- 'thisId': data[i].thisId,
30
- 'x0': data[i].x0,
31
- 'x1': data[i].x0, // x values remain constant
32
- 'y0': data[i].y0,
33
- 'y1': data[i].y0 + findPairs(data[i]),
34
- 'heights': findPairs(data[i])
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
+ }