archaeopteryx 3.4.0 → 3.5.0

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/forester.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.4.0
23
+ // v 3.5.0
24
24
  // 2026-09-10
25
25
  //
26
26
  // forester.js is a general suite for dealing with phylogenetic trees.
@@ -252,7 +252,9 @@
252
252
  if (!node) {
253
253
  throw ("cannot re-root on null node");
254
254
  }
255
- if (!branchLength) {
255
+ // no position: the middle of the branch. 0 is a position -- the root
256
+ // right at the node, where MAD rooting can put it.
257
+ if (typeof branchLength !== 'number' || isNaN(branchLength)) {
256
258
  branchLength = -1;
257
259
  }
258
260
  if (forester.isString(node)) {
@@ -449,6 +451,510 @@
449
451
  }
450
452
  };
451
453
 
454
+ /** The confidence type of the per-branch values madRoot records. */
455
+ forester.MAD_CONFIDENCE_TYPE = 'MAD';
456
+
457
+ const MAD_EPSILON = 1e-9;
458
+
459
+ /**
460
+ * Roots the tree by Minimal Ancestor Deviation (Tria, Landan & Dagan,
461
+ * Nature Ecology & Evolution 1, 0193, 2017; doi:10.1038/s41559-017-0193),
462
+ * ported from the desktop's PhylogenyMethods.madRoot.
463
+ *
464
+ * Under a (relaxed) clock the ancestor of two tips is equidistant from
465
+ * both; the pair (i,j) with ancestor a deviates by |2*dist(a,i)/dist(i,j) - 1|.
466
+ * For every branch the root position minimizing the summed squared
467
+ * deviation of all tip pairs is found analytically, and the branch and
468
+ * position with the smallest total become the root.
469
+ *
470
+ * Every internal branch gets a confidence of type 'MAD': the root-mean-
471
+ * square deviation were the root placed on that branch -- low is good,
472
+ * and the root's branch carries the smallest. Pendant branches get none,
473
+ * and MAD values from an earlier run are replaced.
474
+ *
475
+ * A no-op for fewer than three tips or a tree without branch lengths.
476
+ * O(n^2) time and O(n) memory: the desktop fills an n x n distance matrix
477
+ * only to sum its columns (1.4 GB at 13,000 tips), so the column sums are
478
+ * added up here as the pairs are met, and a subtree's tips are a range of
479
+ * tip numbers rather than a list.
480
+ *
481
+ * @param phy the tree
482
+ * @returns {boolean} whether the tree was re-rooted
483
+ */
484
+ forester.madRoot = function (phy) {
485
+ let root = forester.getTreeRoot(phy);
486
+ if (!root) {
487
+ return false;
488
+ }
489
+ let t = madTraversal(root);
490
+ let pre = t.pre;
491
+ let kids = t.kids;
492
+ let post = t.post;
493
+ let m = pre.length;
494
+ let tipNo = new Int32Array(m).fill(-1);
495
+ let tipPos = [];
496
+ for (let k = 0; k < m; ++k) {
497
+ if (kids[k].length === 0) {
498
+ tipNo[k] = tipPos.length;
499
+ tipPos.push(k);
500
+ }
501
+ }
502
+ let n = tipPos.length;
503
+ if (n < 3) {
504
+ return false;
505
+ }
506
+ // depth (distance from the current root) of every node
507
+ let depth = new Float64Array(m);
508
+ let maxDepth = 0;
509
+ for (let k = 1; k < m; ++k) {
510
+ depth[k] = depth[t.parentOf[k]] + madLength(pre[k]);
511
+ maxDepth = Math.max(maxDepth, depth[k]);
512
+ }
513
+ if (maxDepth <= 0) {
514
+ return false; // no usable branch lengths
515
+ }
516
+ let tipDepth = new Float64Array(n);
517
+ for (let i = 0; i < n; ++i) {
518
+ tipDepth[i] = depth[tipPos[i]];
519
+ }
520
+ // Pass 1 (post-order): the within-subtree deviation sums, and the
521
+ // column sums sum_i 1/d^2 and sum_i 1/d of every tip. For a pair whose
522
+ // common ancestor sits at depth dm, d = depth[i] + depth[j] - 2*dm and
523
+ // the deviation is 2*(depth[i]-dm)/d - 1.
524
+ let lo = new Int32Array(m); // a subtree's tips are the numbers lo..hi-1
525
+ let hi = new Int32Array(m);
526
+ let down = new Float64Array(m); // sum dev^2 over pairs with their ancestor inside the subtree
527
+ let w0 = new Float64Array(m); // sum_{i!=j in subtree} 1/d^2 (ordered)
528
+ let w1 = new Float64Array(m); // sum_{i!=j in subtree} (2*depth[j]/d^2 - 1/d) (j second)
529
+ let w2 = new Float64Array(m); // sum_{i!=j in subtree} (2*depth[j]/d - 1)^2 (j second)
530
+ let col0 = new Float64Array(n);
531
+ let colInv = new Float64Array(n);
532
+ let partners = new Int32Array(n); // the tips each tip is apart from (d > MAD_EPSILON)
533
+ for (let q = 0; q < m; ++q) {
534
+ let k = post[q];
535
+ let ch = kids[k];
536
+ if (ch.length === 0) {
537
+ lo[k] = tipNo[k];
538
+ hi[k] = tipNo[k] + 1;
539
+ continue;
540
+ }
541
+ let dm = depth[k];
542
+ let dwn = 0, sw0 = 0, sw1 = 0, sw2 = 0;
543
+ for (let c = 0; c < ch.length; ++c) {
544
+ dwn += down[ch[c]];
545
+ sw0 += w0[ch[c]];
546
+ sw1 += w1[ch[c]];
547
+ sw2 += w2[ch[c]];
548
+ }
549
+ // the pairs whose ancestor is this node: one tip from each of two children
550
+ for (let a = 0; a < ch.length; ++a) {
551
+ for (let b = a + 1; b < ch.length; ++b) {
552
+ for (let i = lo[ch[a]]; i < hi[ch[a]]; ++i) {
553
+ let di = tipDepth[i];
554
+ for (let j = lo[ch[b]]; j < hi[ch[b]]; ++j) {
555
+ let dj = tipDepth[j];
556
+ let dij = (di - dm) + (dj - dm);
557
+ if (dij > MAD_EPSILON) {
558
+ let inv = 1.0 / dij;
559
+ let inv2 = inv * inv;
560
+ let dev = (2.0 * (di - dm) * inv) - 1.0;
561
+ dwn += dev * dev;
562
+ sw0 += 2.0 * inv2; // both orderings
563
+ sw1 += (2.0 * (di + dj) * inv2) - (2.0 * inv);
564
+ let gi = (2.0 * di * inv) - 1.0;
565
+ let gj = (2.0 * dj * inv) - 1.0;
566
+ sw2 += (gi * gi) + (gj * gj);
567
+ col0[i] += inv2;
568
+ col0[j] += inv2;
569
+ colInv[i] += inv;
570
+ colInv[j] += inv;
571
+ ++partners[i];
572
+ ++partners[j];
573
+ }
574
+ }
575
+ }
576
+ }
577
+ }
578
+ lo[k] = lo[ch[0]];
579
+ hi[k] = hi[ch[ch.length - 1]];
580
+ down[k] = dwn;
581
+ w0[k] = sw0;
582
+ w1[k] = sw1;
583
+ w2[k] = sw2;
584
+ }
585
+ // Pass 2 (post-order): per-subtree "all-i" sums a0,a1,a2; the cross
586
+ // sums between a subtree and its complement are then b_k = a_k - w_k.
587
+ let a0 = new Float64Array(m);
588
+ let a1 = new Float64Array(m);
589
+ let a2 = new Float64Array(m);
590
+ let b0 = new Float64Array(m);
591
+ let b1 = new Float64Array(m);
592
+ let b2 = new Float64Array(m);
593
+ for (let q = 0; q < m; ++q) {
594
+ let k = post[q];
595
+ let s0 = 0, s1 = 0, s2 = 0;
596
+ if (kids[k].length === 0) {
597
+ let j = tipNo[k];
598
+ s0 = col0[j];
599
+ s1 = (2.0 * tipDepth[j] * col0[j]) - colInv[j];
600
+ // (2*depth/d - 1)^2 expands to 4*depth^2/d^2 - 4*depth/d + 1: the
601
+ // 1 is owed once per pair actually summed. The desktop adds n - 1,
602
+ // which also counts the pairs at distance 0 (identical sequences)
603
+ // that every other sum skips.
604
+ s2 = (4.0 * tipDepth[j] * tipDepth[j] * col0[j]) - (4.0 * tipDepth[j] * colInv[j]) + partners[j];
605
+ } else {
606
+ for (let c = 0; c < kids[k].length; ++c) {
607
+ s0 += a0[kids[k][c]];
608
+ s1 += a1[kids[k][c]];
609
+ s2 += a2[kids[k][c]];
610
+ }
611
+ }
612
+ a0[k] = s0;
613
+ a1[k] = s1;
614
+ a2[k] = s2;
615
+ b0[k] = s0 - w0[k];
616
+ b1[k] = s1 - w1[k];
617
+ b2[k] = s2 - w2[k];
618
+ }
619
+ // Pass 3 (pre-order): up[k] = the deviation sum of the pairs whose
620
+ // ancestor lies outside subtree k, by the rerooting recursion, so
621
+ // every branch's total is O(1).
622
+ let up = new Float64Array(m);
623
+ let atParent = new Float64Array(m); // a branch's cross deviation with the root at its parent
624
+ for (let k = 0; k < m; ++k) {
625
+ let ch = kids[k];
626
+ if (ch.length === 0) {
627
+ continue;
628
+ }
629
+ let sumDown = 0;
630
+ let sumAtParent = 0;
631
+ for (let c = 0; c < ch.length; ++c) {
632
+ let x = ch[c];
633
+ sumDown += down[x];
634
+ atParent[x] = madCross(depth[x], b0[x], b1[x], b2[x], madLength(pre[x]));
635
+ sumAtParent += atParent[x];
636
+ }
637
+ // cross deviation among all the groups meeting here: the child subtrees and the complement
638
+ let own = (k === 0) ? 0.0 : madCross(depth[k], b0[k], b1[k], b2[k], 0.0);
639
+ let crossAmong = (sumAtParent + own) / 2.0;
640
+ for (let c = 0; c < ch.length; ++c) {
641
+ let x = ch[c];
642
+ up[x] = up[k] + (sumDown - down[x]) + (crossAmong - atParent[x]);
643
+ }
644
+ }
645
+ // The branch and position with the smallest total deviation, and every
646
+ // branch's smallest MAD value keyed by the tips on its far side, so
647
+ // the value finds its branch again after the re-root has turned
648
+ // parents into children. The first minimum in post-order wins, as on
649
+ // the desktop.
650
+ let hash = madTipHashes(n);
651
+ let nPairs = (n * (n - 1.0)) / 2.0;
652
+ let madByKey = new Map();
653
+ let bestSsd = Infinity;
654
+ let best = -1;
655
+ let bestX = 0;
656
+ for (let q = 0; q < m; ++q) {
657
+ let c = post[q];
658
+ if (c === 0) {
659
+ continue;
660
+ }
661
+ let length = madLength(pre[c]);
662
+ // the optimal root position, as a distance from c toward its parent, kept on the branch
663
+ let x = (b0[c] > MAD_EPSILON) ? (depth[c] - (b1[c] / (2.0 * b0[c]))) : 0.0;
664
+ if (x < 0) {
665
+ x = 0;
666
+ } else if (x > length) {
667
+ x = length;
668
+ }
669
+ let ssd = madCross(depth[c], b0[c], b1[c], b2[c], x) + down[c] + up[c];
670
+ // a sum of squares; clamp a tiny negative left by cancellation, so sqrt is never NaN
671
+ let mad = Math.sqrt(Math.max(0.0, ssd) / nPairs);
672
+ let key = hash.key(hi[c] - lo[c], hash.xa[hi[c]] ^ hash.xa[lo[c]], hash.xb[hi[c]] ^ hash.xb[lo[c]], lo[c] === 0);
673
+ let prev = madByKey.get(key);
674
+ if (prev === undefined || mad < prev) {
675
+ madByKey.set(key, mad); // the two halves of a bifurcating root share a key
676
+ }
677
+ if (ssd < bestSsd) {
678
+ bestSsd = ssd;
679
+ best = c;
680
+ bestX = x;
681
+ }
682
+ }
683
+ if (best < 0) {
684
+ return false;
685
+ }
686
+ let tipNumber = new Map();
687
+ for (let i = 0; i < n; ++i) {
688
+ tipNumber.set(pre[tipPos[i]], i);
689
+ }
690
+ forester.removeMadConfidences(phy);
691
+ forester.reRoot(phy, pre[best], bestX);
692
+ // annotate the internal branches of the re-rooted tree
693
+ let r = madTraversal(forester.getTreeRoot(phy));
694
+ let count = new Int32Array(r.pre.length);
695
+ let xa = new Int32Array(r.pre.length);
696
+ let xb = new Int32Array(r.pre.length);
697
+ let hasFirst = new Uint8Array(r.pre.length);
698
+ for (let q = 0; q < r.post.length; ++q) {
699
+ let k = r.post[q];
700
+ let ch = r.kids[k];
701
+ if (ch.length === 0) {
702
+ let i = tipNumber.get(r.pre[k]);
703
+ count[k] = 1;
704
+ xa[k] = hash.xa[i + 1] ^ hash.xa[i];
705
+ xb[k] = hash.xb[i + 1] ^ hash.xb[i];
706
+ hasFirst[k] = (i === 0) ? 1 : 0;
707
+ continue;
708
+ }
709
+ for (let c = 0; c < ch.length; ++c) {
710
+ count[k] += count[ch[c]];
711
+ xa[k] ^= xa[ch[c]];
712
+ xb[k] ^= xb[ch[c]];
713
+ hasFirst[k] |= hasFirst[ch[c]];
714
+ }
715
+ if (k !== 0) {
716
+ let mad = madByKey.get(hash.key(count[k], xa[k], xb[k], hasFirst[k] === 1));
717
+ if (mad !== undefined) {
718
+ let kept = r.pre[k].confidences || [];
719
+ r.pre[k].confidences = kept.concat([{value: mad, type: forester.MAD_CONFIDENCE_TYPE}]);
720
+ }
721
+ }
722
+ }
723
+ return true;
724
+ };
725
+
726
+ /**
727
+ * Removes every 'MAD' confidence (from madRoot) and keeps all others: a
728
+ * tree rooted any other way no longer has the rooting those values rate.
729
+ *
730
+ * @param phy the tree
731
+ */
732
+ forester.removeMadConfidences = function (phy) {
733
+ let root = forester.getTreeRoot(phy);
734
+ if (!root) {
735
+ return;
736
+ }
737
+ madTraversal(root).pre.forEach(function (node) {
738
+ if (node.confidences && node.confidences.some(function (c) { return c.type === forester.MAD_CONFIDENCE_TYPE; })) {
739
+ // a new array: re-rooting can leave two branches sharing one
740
+ let kept = node.confidences.filter(function (c) { return c.type !== forester.MAD_CONFIDENCE_TYPE; });
741
+ if (kept.length > 0) {
742
+ node.confidences = kept;
743
+ } else {
744
+ delete node.confidences;
745
+ }
746
+ }
747
+ });
748
+ };
749
+
750
+ /**
751
+ * Whether a node carries data about the node itself, the kind a
752
+ * re-rooting can take the meaning away from: a name, taxonomy, sequence
753
+ * (a domain architecture lives there), events, distribution, date,
754
+ * references, or a property about the node. Not data in this sense: the
755
+ * branch above it -- length, support and MAD values, colour, width, a
756
+ * property applying to the branch -- which re-rooting carries along,
757
+ * visual styling (style: properties), the viewer's own aptx: properties,
758
+ * and an empty taxonomy. The desktop's list is the same.
759
+ *
760
+ * @param node
761
+ * @returns {boolean}
762
+ */
763
+ forester.nodeHasData = function (node) {
764
+ let filled = function (x) {
765
+ return x !== undefined && x !== null && x !== '' && !(Array.isArray(x) && x.length === 0);
766
+ };
767
+ if ((typeof node.name === 'string' && node.name.length > 0)
768
+ || (node.taxonomies && node.taxonomies.some(function (t) {
769
+ return t && Object.keys(t).some(function (key) { return filled(t[key]); });
770
+ }))
771
+ || (node.sequences && node.sequences.length > 0)
772
+ || node.events
773
+ || (node.distributions && node.distributions.length > 0)
774
+ || node.date
775
+ || (node.references && node.references.length > 0)) {
776
+ return true;
777
+ }
778
+ return !!node.properties && node.properties.some(function (p) {
779
+ return p.applies_to !== BRANCH_EVENT_APPLIES_TO
780
+ && !(typeof p.ref === 'string' && (p.ref.indexOf('style:') === 0 || p.ref.indexOf('aptx:') === 0));
781
+ });
782
+ };
783
+
784
+ /**
785
+ * What a re-rooting would do to the data on internal nodes, worked out on
786
+ * a bare copy so the tree itself is untouched: `annotated`, the internal
787
+ * nodes carrying data (nodeHasData), and `changed`, those among them whose
788
+ * clade -- the tips below -- the re-rooting changes. Those lie between
789
+ * the old root and the new one (an old two-child root disappears); every
790
+ * other node keeps its tips. Midpoint and MAD rooting find their root on
791
+ * the copy first. Nothing is copied when no internal node carries data.
792
+ *
793
+ * @param phy the tree
794
+ * @param method 'mad', 'midpoint', or 'node': the root on the branch above `node`
795
+ * @param node the node, for 'node'
796
+ * @returns {{annotated: Array, changed: Array}} nodes of `phy`
797
+ */
798
+ forester.cladesChangedByRerooting = function (phy, method, node) {
799
+ let t = madTraversal(forester.getTreeRoot(phy));
800
+ let annotated = [];
801
+ for (let k = 0; k < t.pre.length; ++k) {
802
+ if (t.kids[k].length > 0 && forester.nodeHasData(t.pre[k])) {
803
+ annotated.push(t.pre[k]);
804
+ }
805
+ }
806
+ if (annotated.length === 0) {
807
+ return {annotated: annotated, changed: []};
808
+ }
809
+ // the copy: shape and branch lengths only, each copy knowing its original
810
+ let copyOf = new Map();
811
+ let tipNumber = new Map();
812
+ for (let k = 0; k < t.pre.length; ++k) {
813
+ let c = {branch_length: t.pre[k].branch_length, original: t.pre[k]};
814
+ if (t.kids[k].length > 0) {
815
+ c.children = [];
816
+ } else {
817
+ tipNumber.set(t.pre[k], tipNumber.size);
818
+ }
819
+ if (k > 0) {
820
+ c.parent = copyOf.get(t.pre[t.parentOf[k]]);
821
+ c.parent.children.push(c);
822
+ }
823
+ copyOf.set(t.pre[k], c);
824
+ }
825
+ let copy = {children: [copyOf.get(t.pre[0])]};
826
+ copy.children[0].parent = copy;
827
+ if (method === 'mad') {
828
+ forester.madRoot(copy);
829
+ } else if (method === 'midpoint') {
830
+ forester.midpointRoot(copy);
831
+ } else {
832
+ forester.reRoot(copy, copyOf.get(node), -1);
833
+ }
834
+ // the tips below every node, before and after, as a count and two hashes
835
+ let hash = madTipHashes(tipNumber.size);
836
+ let cladeKeys = function (traversal, originalOf) {
837
+ let count = new Int32Array(traversal.pre.length);
838
+ let xa = new Int32Array(traversal.pre.length);
839
+ let xb = new Int32Array(traversal.pre.length);
840
+ let keys = new Map();
841
+ for (let q = 0; q < traversal.post.length; ++q) {
842
+ let k = traversal.post[q];
843
+ let ch = traversal.kids[k];
844
+ if (ch.length === 0) {
845
+ let i = tipNumber.get(originalOf(traversal.pre[k]));
846
+ count[k] = 1;
847
+ xa[k] = hash.xa[i + 1] ^ hash.xa[i];
848
+ xb[k] = hash.xb[i + 1] ^ hash.xb[i];
849
+ continue;
850
+ }
851
+ for (let c = 0; c < ch.length; ++c) {
852
+ count[k] += count[ch[c]];
853
+ xa[k] ^= xa[ch[c]];
854
+ xb[k] ^= xb[ch[c]];
855
+ }
856
+ let original = originalOf(traversal.pre[k]);
857
+ if (original) { // not the copy's new root node
858
+ keys.set(original, count[k] + ':' + xa[k] + ':' + xb[k]);
859
+ }
860
+ }
861
+ return keys;
862
+ };
863
+ let before = cladeKeys(t, function (n) { return n; });
864
+ let after = cladeKeys(madTraversal(forester.getTreeRoot(copy)), function (c) { return c.original; });
865
+ return {
866
+ annotated: annotated,
867
+ changed: annotated.filter(function (n) { return after.get(n) !== before.get(n); })
868
+ };
869
+ };
870
+
871
+ function madLength(node) {
872
+ return node.branch_length > 0 ? node.branch_length : 0;
873
+ }
874
+
875
+ // The sum of squared cross-pair deviations between subtree c and its
876
+ // complement, with the root at distance x from c toward its parent. With
877
+ // K = 2*(x - depth[c]) a pair deviates by K/d + (2*depth[j]/d - 1), so the
878
+ // sum is K^2*b0 + 2*K*b1 + b2.
879
+ function madCross(depthC, b0, b1, b2, x) {
880
+ let k = 2.0 * (x - depthC);
881
+ return (k * k * b0) + (2.0 * k * b1) + b2;
882
+ }
883
+
884
+ // Pre-order (the root at 0, parents before children) and post-order,
885
+ // children left to right, as node positions -- without recursion, since
886
+ // a caterpillar tree nests as deep as it has tips.
887
+ function madTraversal(root) {
888
+ let pre = [];
889
+ let parentOf = [];
890
+ let stack = [root];
891
+ let stackParent = [-1];
892
+ while (stack.length > 0) {
893
+ let node = stack.pop();
894
+ let p = stackParent.pop();
895
+ let k = pre.length;
896
+ pre.push(node);
897
+ parentOf.push(p);
898
+ if (node.children) {
899
+ for (let i = node.children.length - 1; i >= 0; --i) {
900
+ stack.push(node.children[i]);
901
+ stackParent.push(k);
902
+ }
903
+ }
904
+ }
905
+ let kids = pre.map(function () {
906
+ return [];
907
+ });
908
+ for (let k = 1; k < pre.length; ++k) {
909
+ kids[parentOf[k]].push(k); // siblings come in left to right
910
+ }
911
+ // the mirrored pre-order, reversed, is the post-order left to right
912
+ let post = [];
913
+ let st = [0];
914
+ while (st.length > 0) {
915
+ let k = st.pop();
916
+ post.push(k);
917
+ for (let c = 0; c < kids[k].length; ++c) {
918
+ st.push(kids[k][c]);
919
+ }
920
+ }
921
+ post.reverse();
922
+ return {pre: pre, parentOf: parentOf, kids: kids, post: post};
923
+ }
924
+
925
+ // A set of tips as a key that does not depend on the rooting: its size
926
+ // and two 32-bit XOR hashes of its tips, taken on the side WITHOUT tip 0
927
+ // so both sides of a branch give the same key. xa/xb are prefix XORs, so
928
+ // the tips lo..hi-1 hash to xa[hi] ^ xa[lo].
929
+ function madTipHashes(n) {
930
+ let xa = new Int32Array(n + 1);
931
+ let xb = new Int32Array(n + 1);
932
+ let s = 0x2545f491;
933
+ let next = function () { // a fixed seed: the same tree always hashes the same
934
+ s = (s + 0x9e3779b9) | 0;
935
+ let z = s;
936
+ z = Math.imul(z ^ (z >>> 16), 0x85ebca6b);
937
+ z = Math.imul(z ^ (z >>> 13), 0xc2b2ae35);
938
+ return z ^ (z >>> 16);
939
+ };
940
+ for (let i = 0; i < n; ++i) {
941
+ xa[i + 1] = xa[i] ^ next();
942
+ xb[i + 1] = xb[i] ^ next();
943
+ }
944
+ return {
945
+ xa: xa,
946
+ xb: xb,
947
+ key: function (size, ha, hb, holdsFirst) {
948
+ if (holdsFirst) {
949
+ size = n - size;
950
+ ha ^= xa[n];
951
+ hb ^= xb[n];
952
+ }
953
+ return size + ':' + ha + ':' + hb;
954
+ }
955
+ };
956
+ }
957
+
452
958
  forester.getFurthestDescendant = function (node) {
453
959
  let children = forester.getAllExternalNodes(node);
454
960
  let farthest = null;
@@ -1691,6 +2197,8 @@
1691
2197
  properties.longestNodeName = 0;
1692
2198
  properties.branchLengths = false;
1693
2199
  properties.confidences = false;
2200
+ // MAD values (madRoot) rate root positions, not clades: never support
2201
+ properties.madValues = false;
1694
2202
  // the largest confidence value seen -- how a caller tells a
1695
2203
  // posterior-probability tree (max <= 1) from a bootstrap tree
1696
2204
  properties.maxConfidence = 0;
@@ -1803,8 +2311,12 @@
1803
2311
  }
1804
2312
  }
1805
2313
  if (n.confidences && n.confidences.length > 0) {
1806
- properties.confidences = true;
1807
2314
  for (let ci = 0; ci < n.confidences.length; ++ci) {
2315
+ if (n.confidences[ci].type === forester.MAD_CONFIDENCE_TYPE) {
2316
+ properties.madValues = true;
2317
+ continue;
2318
+ }
2319
+ properties.confidences = true;
1808
2320
  let cv = n.confidences[ci].value;
1809
2321
  if (typeof cv === 'number' && isFinite(cv) && cv > properties.maxConfidence) {
1810
2322
  properties.maxConfidence = cv;
@@ -3470,11 +3982,17 @@
3470
3982
  nh += ":" + node.branch_length;
3471
3983
  }
3472
3984
  }
3473
- if (writeConfidences && node.confidences && node.confidences.length === 1 && node.confidences[0].value !== undefined && node.confidences[0].value !== null) {
3985
+ // the support slot holds support: a MAD value (madRoot) never goes
3986
+ // there -- it would read as support, and would crowd out the
3987
+ // bootstrap on a branch carrying both. phyloXML keeps it, typed.
3988
+ let support = writeConfidences && node.confidences
3989
+ ? node.confidences.filter(function (c) { return c.type !== forester.MAD_CONFIDENCE_TYPE; })
3990
+ : [];
3991
+ if (support.length === 1 && support[0].value !== undefined && support[0].value !== null) {
3474
3992
  if (decPointsMax && decPointsMax > 0) {
3475
- nh += "[" + forester.roundNumber(node.confidences[0].value, decPointsMax) + "]";
3993
+ nh += "[" + forester.roundNumber(support[0].value, decPointsMax) + "]";
3476
3994
  } else {
3477
- nh += "[" + node.confidences[0].value + "]";
3995
+ nh += "[" + support[0].value + "]";
3478
3996
  }
3479
3997
  }
3480
3998
  if (!last) {
@@ -3978,6 +4496,98 @@
3978
4496
  // becomes "meta:" plus the header with its whitespace as '_' -- the
3979
4497
  // display name prettifies that back to spaces, so "Collection Date"
3980
4498
  // stays "Collection Date" in every menu.
4499
+ // The tips' data as a table, one row per tip in the order given, header
4500
+ // first: what the node menu's "Download Ext. Node Data" writes. The
4501
+ // columns and their names are the desktop's (NodeDataExporter.toNodeDataTsv),
4502
+ // so the two programs write the same table: name (always), the first
4503
+ // taxonomy's scientific name, common name, code, id and rank, the first
4504
+ // sequence's name, gene name, symbol, accession and type, the branch
4505
+ // length, then one column per property ref, sorted, holding its first
4506
+ // value. A column no tip has a value for is left out. When the tip names
4507
+ // cannot key the rows (one blank or repeated), a node_id column comes
4508
+ // first, from idOf(tip, index) or the row number. Tabs and line breaks
4509
+ // inside a value become spaces. A property keeps its ref as the header,
4510
+ // so the table joins back onto a tree with joinMetadataTable.
4511
+ forester.externalNodeDataTable = function (tips, idOf) {
4512
+ tips = tips || [];
4513
+ if (tips.length === 0) {
4514
+ return {columns: [], rows: []};
4515
+ }
4516
+ let clean = function (v) {
4517
+ return (v === undefined || v === null) ? '' : String(v).replace(/[\t\n\r]/g, ' ');
4518
+ };
4519
+ let tax = function (n) {
4520
+ return (n.taxonomies && n.taxonomies[0]) || {};
4521
+ };
4522
+ let seq = function (n) {
4523
+ return (n.sequences && n.sequences[0]) || {};
4524
+ };
4525
+ let cols = [];
4526
+ let add = function (name, extract, force) {
4527
+ let vals = tips.map(function (n, i) {
4528
+ return clean(extract(n, i));
4529
+ });
4530
+ if (force || vals.some(function (v) { return v.length > 0; })) {
4531
+ cols.push({name: name, vals: vals});
4532
+ }
4533
+ };
4534
+ let seen = new Set();
4535
+ let unique = tips.every(function (n) {
4536
+ if (!n.name || seen.has(n.name)) {
4537
+ return false;
4538
+ }
4539
+ seen.add(n.name);
4540
+ return true;
4541
+ });
4542
+ if (!unique) {
4543
+ add('node_id', function (n, i) { return idOf ? idOf(n, i) : i + 1; }, true);
4544
+ }
4545
+ add('name', function (n) { return n.name; }, true);
4546
+ add('taxonomy_scientific_name', function (n) { return tax(n).scientific_name; });
4547
+ add('taxonomy_common_name', function (n) { return tax(n).common_name; });
4548
+ add('taxonomy_code', function (n) { return tax(n).code; });
4549
+ add('taxonomy_id', function (n) { return tax(n).id && tax(n).id.value; });
4550
+ add('taxonomy_rank', function (n) { return tax(n).rank; });
4551
+ add('sequence_name', function (n) { return seq(n).name; });
4552
+ add('gene_name', function (n) { return seq(n).gene_name; });
4553
+ add('sequence_symbol', function (n) { return seq(n).symbol; });
4554
+ add('sequence_accession', function (n) { return seq(n).accession && seq(n).accession.value; });
4555
+ add('sequence_type', function (n) { return seq(n).type; });
4556
+ add('branch_length', function (n) { return (typeof n.branch_length === 'number') ? n.branch_length : ''; });
4557
+ let refs = new Set();
4558
+ tips.forEach(function (n) {
4559
+ (n.properties || []).forEach(function (p) {
4560
+ if (p.ref) {
4561
+ refs.add(p.ref);
4562
+ }
4563
+ });
4564
+ });
4565
+ Array.from(refs).sort().forEach(function (ref) {
4566
+ add(ref, function (n) {
4567
+ let p = (n.properties || []).filter(function (q) { return q.ref === ref; })[0];
4568
+ return p ? p.value : '';
4569
+ });
4570
+ });
4571
+ return {
4572
+ columns: cols.map(function (c) { return c.name; }),
4573
+ rows: tips.map(function (n, i) {
4574
+ return cols.map(function (c) { return c.vals[i]; });
4575
+ })
4576
+ };
4577
+ };
4578
+
4579
+ // externalNodeDataTable as tab-separated text, header line first; empty
4580
+ // for no tips.
4581
+ forester.externalNodeDataTsv = function (tips, idOf) {
4582
+ let t = forester.externalNodeDataTable(tips, idOf);
4583
+ if (t.columns.length === 0) {
4584
+ return '';
4585
+ }
4586
+ return [t.columns].concat(t.rows).map(function (r) {
4587
+ return r.join('\t');
4588
+ }).join('\n') + '\n';
4589
+ };
4590
+
3981
4591
  forester.metadataColumnRef = function (header, index) {
3982
4592
  let h = String(header || '').trim();
3983
4593
  if (h.length === 0) {
@@ -4135,7 +4745,13 @@
4135
4745
  const SEARCH_DOMAIN = textField('Domain', n => searchSeqs(n).reduce((a, s) => a.concat((s.domain_architecture && s.domain_architecture.domains) ? s.domain_architecture.domains.map(d => d.name) : []), []).filter(Boolean));
4136
4746
  const SEARCH_ANNOTATION = textField('Annotation', n => searchSeqs(n).reduce((a, s) => a.concat((s.annotations || []).reduce((b, an) => b.concat([an.desc, an.ref]), [])), []).filter(Boolean), {anyText: true});
4137
4747
  const SEARCH_CROSS_REFERENCE = textField('Cross-Reference', n => searchSeqs(n).reduce((a, s) => a.concat((s.cross_references || []).reduce((b, x) => b.concat([x.value, x.source, x.comment]), [])), []).filter(Boolean), {anyText: true});
4138
- const SEARCH_MOLECULAR_SEQUENCE = textField('Molecular Sequence', n => searchSeqs(n).map(s => s.mol_seq).filter(Boolean), {suggest: false});
4748
+ // The residues: the phyloXML and Nexus readers give mol_seq as {value,
4749
+ // is_aligned}, and a hand-built tree may carry the plain string. Reading
4750
+ // the object itself compared every query against "[object Object]", so
4751
+ // this field never matched a real file (its test built the string form).
4752
+ const SEARCH_MOLECULAR_SEQUENCE = textField('Molecular Sequence', n => searchSeqs(n).map(function (s) {
4753
+ return (s.mol_seq && typeof s.mol_seq === 'object') ? s.mol_seq.value : s.mol_seq;
4754
+ }).filter(Boolean), {suggest: false});
4139
4755
  // in menu order
4140
4756
  const SEARCH_TEXT_FIELDS = [
4141
4757
  SEARCH_NODE_NAME, SEARCH_TAXONOMY_SCIENTIFIC_NAME, SEARCH_TAXONOMY_COMMON_NAME, SEARCH_TAXONOMY_CODE,
@@ -4160,7 +4776,8 @@
4160
4776
  }, {suggest: false});
4161
4777
 
4162
4778
  const SEARCH_BRANCH_LENGTH = numericField('Branch Length', n => (typeof n.branch_length === 'number') ? [n.branch_length] : []);
4163
- const SEARCH_CONFIDENCE = numericField('Confidence', n => n.confidences ? n.confidences.map(c => c.value).filter(v => typeof v === 'number') : []);
4779
+ // support only: a MAD value rates a root position, not the clade
4780
+ const SEARCH_CONFIDENCE = numericField('Confidence', n => n.confidences ? n.confidences.filter(c => c.type !== forester.MAD_CONFIDENCE_TYPE).map(c => c.value).filter(v => typeof v === 'number') : []);
4164
4781
  const SEARCH_CLADE_SIZE = numericField('Clade Size (tips)', n => [n._srchClade], {metrics: true});
4165
4782
  const SEARCH_CHILD_COUNT = numericField('Number of Children', n => [n.children ? n.children.length : 0]);
4166
4783
  const SEARCH_DEPTH = numericField('Depth from Root', n => [n._srchDepth], {metrics: true});
@@ -4299,7 +4916,7 @@
4299
4916
  if (!hasBL && typeof n.branch_length === 'number' && n.branch_length >= 0) hasBL = true;
4300
4917
  if (!hasConf && n.confidences) {
4301
4918
  for (let i = 0; i < n.confidences.length; ++i) {
4302
- if (typeof n.confidences[i].value === 'number') { hasConf = true; break; }
4919
+ if (typeof n.confidences[i].value === 'number' && n.confidences[i].type !== forester.MAD_CONFIDENCE_TYPE) { hasConf = true; break; }
4303
4920
  }
4304
4921
  }
4305
4922
  if (n.properties) {
@@ -4747,6 +5364,32 @@
4747
5364
  'calendar year': 1, 'calendar years': 1
4748
5365
  };
4749
5366
 
5367
+ /**
5368
+ * Whether the tree's branch lengths are time: most of its internal nodes
5369
+ * carry a date, and at least two do -- BEAST node heights, Nextstrain
5370
+ * dates, phyloXML <date>s on ancestors. Tip dates alone do not make one:
5371
+ * a divergence tree can carry collection dates, and re-rooting it (as a
5372
+ * root-to-tip regression does) is a normal step. A time tree is never
5373
+ * re-rooted: a new root would contradict the ancestors' dates. The
5374
+ * desktop's rule is the same (its detectTimeTree, internal-node half).
5375
+ *
5376
+ * @param phy the tree
5377
+ * @returns {boolean}
5378
+ */
5379
+ forester.isTimeTree = function (phy) {
5380
+ let internal = 0;
5381
+ let dated = 0;
5382
+ forester.preOrderTraversalAll(forester.getTreeRoot(phy), function (n) {
5383
+ if (n.children && n.children.length > 0) {
5384
+ ++internal;
5385
+ if (n.date && typeof n.date.value === 'number' && isFinite(n.date.value)) {
5386
+ ++dated;
5387
+ }
5388
+ }
5389
+ });
5390
+ return dated >= 2 && dated * 2 > internal;
5391
+ };
5392
+
4750
5393
  // Everything the viewer needs to decide about and draw a time axis:
4751
5394
  // {type: 'geologic'|'calendar'|null, rootAge, presentDate, dated,
4752
5395
  // hasInternalIntervals, hasExternalIntervals}. rootAge (geologic) and