archaeopteryx 3.4.1 → 3.5.1
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/README.md +65 -0
- package/archaeopteryx.d.ts +3 -3
- package/archaeopteryx.js +270 -74
- package/forester.js +587 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ config key live and shows the exact config JSON to copy into your own
|
|
|
27
27
|
* [Auspice / Nextstrain JSON](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=auspice)
|
|
28
28
|
* [Swine H1 HA1 + alignment (Nexus)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=swh1)
|
|
29
29
|
* [BEAST annotations (Nexus)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=beast)
|
|
30
|
+
* [Flavivirus mature peptides (10 trees)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=flavivirus)
|
|
30
31
|
* [SARS-CoV-2 time tree (calendar)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=sarscov2)
|
|
31
32
|
* [Herpesviridae DNA polymerase (201 tips)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=herpes_dnapol)
|
|
32
33
|
* [Caliciviridae (186 strains)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=caliciviridae_500)
|
|
@@ -257,6 +258,70 @@ also resets rotation and label direction. Unrooted
|
|
|
257
258
|
additionally greys out the aligned-phylogram option and Auto-hide Labels
|
|
258
259
|
(there is no common label edge, and no even row spacing to hide against).
|
|
259
260
|
|
|
261
|
+
## Rooting
|
|
262
|
+
|
|
263
|
+
The tool row's re-root button asks how: **MAD re-root (Tria et al., 2017)**
|
|
264
|
+
or **Midpoint re-root** (hover over the MAD entry for the full citation). A
|
|
265
|
+
node's menu offers **Reroot** on the branch above that node. None of them is
|
|
266
|
+
offered for a tree whose phyloXML says `rerootable="false"`, nor for a time
|
|
267
|
+
tree, one whose internal nodes are mostly dated (BEAST node heights,
|
|
268
|
+
Nextstrain dates, phyloXML `<date>`s): a new root would contradict the dates.
|
|
269
|
+
For such a tree the re-root button and the node menu's Reroot are greyed,
|
|
270
|
+
their tooltip saying why, and a shared view's root is ignored.
|
|
271
|
+
|
|
272
|
+
A re-root can take the meaning away from data on internal nodes: a node's
|
|
273
|
+
name, taxonomy, sequence, events, distribution, date, references or node
|
|
274
|
+
properties describe its clade, and a new root changes the clade of every
|
|
275
|
+
node between the old root and the new one. So before re-rooting (from the
|
|
276
|
+
button's menu or the node menu), the change is worked out on a copy of the
|
|
277
|
+
tree, and when it would change the clade of any internal node carrying data,
|
|
278
|
+
a warning says how many (“This tree has data on 15 internal nodes.
|
|
279
|
+
Re-rooting changes the clade of 3 of them, so their data may no longer
|
|
280
|
+
describe them.”) with **Re-root** and **Cancel**. Branch lengths, support and
|
|
281
|
+
MAD values, branch colours and `style:` properties do not count: they belong
|
|
282
|
+
to the branch, or to the look.
|
|
283
|
+
|
|
284
|
+
A tree its file declares unrooted (phyloXML `rooted="false"`, Nexus `[&U]`),
|
|
285
|
+
shown in the unrooted layout, has no root to measure from. There the hover
|
|
286
|
+
card and Display Node Data show an internal node's **Tips around** — the tips
|
|
287
|
+
on each of its sides, smallest first, such as `2 · 3 · 5` — instead of
|
|
288
|
+
distance to parent, depth and tips below; a tip shows its **Branch length**
|
|
289
|
+
and no depth; and the Depth from Root, Distance from Root and Clade Size
|
|
290
|
+
search fields are not offered. The same tree in the rectangular or circular
|
|
291
|
+
layout keeps all of them, since those layouts draw a root.
|
|
292
|
+
|
|
293
|
+
**MAD rooting** (minimal ancestor deviation) roots the tree without assuming
|
|
294
|
+
a clock. The common ancestor of two tips ought to lie halfway between them,
|
|
295
|
+
so every branch and position is scored by how far the tip pairs' ancestors
|
|
296
|
+
fall from that halfway point, and the root goes where that deviation is
|
|
297
|
+
smallest [1]. It has been compared with other rooting methods on prokaryotic
|
|
298
|
+
gene families [2]. It needs branch lengths and at least three tips, and is
|
|
299
|
+
offered only then. Tip pairs closer than 1/100,000 of the tree's diameter
|
|
300
|
+
(identical sequences, or FastTree's 5 × 10⁻⁹ stand-in for a zero branch) are
|
|
301
|
+
left out of the deviation sums: their deviation measures noise, and summing
|
|
302
|
+
it would swamp the precision of all the others. The algorithm is the desktop
|
|
303
|
+
Archaeopteryx's, and gives the same roots; it runs in O(n²) time and O(n) memory (the 13,246-tip H5N1
|
|
304
|
+
demo tree roots in 0.4 s, measured in Node).
|
|
305
|
+
|
|
306
|
+
Every internal branch then carries its **MAD value**: the root-mean-square
|
|
307
|
+
deviation the tree would have with the root on that branch. Lower is better,
|
|
308
|
+
and the root's branch has the smallest. The **MAD Values** checkbox (Display
|
|
309
|
+
Data → Labels, present while the tree carries them) writes them on the
|
|
310
|
+
branches, ahead of any support value, as `MAD/support`: `0.02/95`. They are
|
|
311
|
+
not support, so the Confidence labels, Support Dots and the Confidence search
|
|
312
|
+
field leave them out. Midpoint or manual re-rooting removes them, since they
|
|
313
|
+
describe the MAD rooting only. A shared view remembers a MAD root. A phyloXML
|
|
314
|
+
download keeps them as `<confidence type="MAD">`, as the desktop writes them;
|
|
315
|
+
a Newick or Nexus download never puts one where a support value goes.
|
|
316
|
+
|
|
317
|
+
1. Tria, F.D.K., Landan, G., Dagan, T. (2017). Phylogenetic rooting using
|
|
318
|
+
minimal ancestor deviation. *Nature Ecology & Evolution*, 1, 0193.
|
|
319
|
+
<https://www.nature.com/articles/s41559-017-0193>
|
|
320
|
+
2. Wade, T., Rangel, L.T., Kundu, S., Fournier, G.P., Bansal, M.S. (2020).
|
|
321
|
+
Assessing the accuracy of phylogenetic rooting methods on prokaryotic
|
|
322
|
+
gene families. *PLOS ONE*, 15(5), e0232950.
|
|
323
|
+
<https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0232950>
|
|
324
|
+
|
|
260
325
|
## Metadata tables
|
|
261
326
|
|
|
262
327
|
A tree file rarely carries everything known about its tips. A **metadata
|
package/archaeopteryx.d.ts
CHANGED
|
@@ -114,15 +114,15 @@ export interface ViewState {
|
|
|
114
114
|
display?: 'phylogram' | 'aligned' | 'cladogram';
|
|
115
115
|
/** The ladderize direction applied. */
|
|
116
116
|
order?: 'asc' | 'desc';
|
|
117
|
-
/**
|
|
118
|
-
root?: 'midpoint';
|
|
117
|
+
/** Re-rooted: at the midpoint, or by minimal ancestor deviation. */
|
|
118
|
+
root?: 'midpoint' | 'mad';
|
|
119
119
|
subtree?: number;
|
|
120
120
|
collapsed?: number[];
|
|
121
121
|
/** A visualization id (as the Color-by menu values them), or 'none'. */
|
|
122
122
|
colorBy?: string;
|
|
123
123
|
shapeBy?: string;
|
|
124
124
|
/** The panel's checked boxes: name, taxonomy, sequence, confidence,
|
|
125
|
-
* branchLength, external, internal, nodeEvents, branchEvents,
|
|
125
|
+
* madValues, branchLength, external, internal, nodeEvents, branchEvents,
|
|
126
126
|
* supportDots, shortNames, autoHide, visualizations, visualStyles, and
|
|
127
127
|
* custom:<key> for a nodeLabels checkbox. */
|
|
128
128
|
show?: string[];
|
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.5.1
|
|
24
24
|
// 2026-09-10
|
|
25
25
|
//
|
|
26
26
|
// Archaeopteryx.js is a software tool for the visualization and
|
|
@@ -103,7 +103,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
103
103
|
// IIFE's own function name -- a plain object says what it is.)
|
|
104
104
|
let archaeopteryx = {};
|
|
105
105
|
|
|
106
|
-
const VERSION = '3.
|
|
106
|
+
const VERSION = '3.5.1';
|
|
107
107
|
const WEBSITE = 'https://cmzmasek.github.io/archaeopteryx-js/';
|
|
108
108
|
const DESKTOP_WEBSITE = 'https://cmzmasek.github.io/archaeopteryx/';
|
|
109
109
|
const SOURCE_WEBSITE = 'https://github.com/cmzmasek/archaeopteryx-js';
|
|
@@ -350,6 +350,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
350
350
|
const CLADOGRAM_BUTTON = 'cla_b';
|
|
351
351
|
const CONFIDENCE_VALUES_CB = 'conf_cb';
|
|
352
352
|
const SUPPORT_DOTS_CB = 'suppdots_cb';
|
|
353
|
+
const MAD_VALUES_CB = 'mad_cb';
|
|
353
354
|
const DOWNLOAD_BUTTON = 'dl_b';
|
|
354
355
|
const DYNAHIDE_CB = 'dynahide_cb';
|
|
355
356
|
const MSA_CB = 'msa_cb';
|
|
@@ -371,6 +372,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
371
372
|
const INTERNAL_LABEL_CB = 'intl_cb';
|
|
372
373
|
const LABEL_COLOR_SELECT_MENU = 'lcs_menu';
|
|
373
374
|
const MIDPOINT_ROOT_BUTTON = 'midpointr_b';
|
|
375
|
+
const MAD_CITATION = 'Tria F, Landan G, Dagan T. Phylogenetic rooting using minimal ancestor deviation. Nature Ecology and Evolution. 2017;1:0193';
|
|
374
376
|
const UNCOLLAPSE_ALL_BUTTON = 'uncollapse_all_b';
|
|
375
377
|
const TREE_PREV_BUTTON = 'tree_prev_b';
|
|
376
378
|
const TREE_NEXT_BUTTON = 'tree_next_b';
|
|
@@ -559,6 +561,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
559
561
|
const HPD_BAR_COLOR = 'rgba(70,130,220,0.35)'; // translucent blue, FigTree-like
|
|
560
562
|
const FOSSIL_BAR_COLOR = 'rgba(150,100,55,0.86)'; // opaque-ish sepia
|
|
561
563
|
let _timeInfo = null; // forester.timeAxisInfo, recomputed per render
|
|
564
|
+
let _timeTree = false; // forester.isTimeTree: never re-rooted
|
|
562
565
|
let _clusterH = 0; // the cluster layout's vertical extent, set per render
|
|
563
566
|
let _docListenersBound = false; // page-level key/wheel handlers bind once, not per launch
|
|
564
567
|
let _docListeners = []; // ...and destroy() can take every one of them down again
|
|
@@ -1462,9 +1465,19 @@ function (root, d3, forester, phyloXml) {
|
|
|
1462
1465
|
menu.appendChild(document.createElement('hr'));
|
|
1463
1466
|
return;
|
|
1464
1467
|
}
|
|
1468
|
+
if (item.note) { // a sentence to read before choosing, such as a warning
|
|
1469
|
+
let note = document.createElement('div');
|
|
1470
|
+
note.className = 'aptx-node-menu-note';
|
|
1471
|
+
note.textContent = item.note;
|
|
1472
|
+
menu.appendChild(note);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1465
1475
|
let b = document.createElement('button');
|
|
1466
1476
|
b.type = 'button';
|
|
1467
1477
|
b.textContent = item.label;
|
|
1478
|
+
if (item.title) {
|
|
1479
|
+
b.title = item.title;
|
|
1480
|
+
}
|
|
1468
1481
|
if (item.danger) {
|
|
1469
1482
|
b.className = 'aptx-menu-danger';
|
|
1470
1483
|
}
|
|
@@ -3200,7 +3213,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3200
3213
|
if (_state.showBranchLengthValues) {
|
|
3201
3214
|
nodeChild('text.bllabel').text(makeBranchLengthLabel);
|
|
3202
3215
|
}
|
|
3203
|
-
if (_state.showConfidenceValues) {
|
|
3216
|
+
if (_state.showConfidenceValues || _state.showMadValues) {
|
|
3204
3217
|
nodeChild('text.conflabel').text(makeConfidenceValuesLabel);
|
|
3205
3218
|
}
|
|
3206
3219
|
if (_state.showBranchEvents) {
|
|
@@ -3811,7 +3824,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3811
3824
|
|
|
3812
3825
|
function syncOptionalNodeChildren(node) {
|
|
3813
3826
|
let wantBl = _state.showBranchLengthValues === true;
|
|
3814
|
-
let wantConf = _state.showConfidenceValues === true;
|
|
3827
|
+
let wantConf = _state.showConfidenceValues === true || _state.showMadValues === true;
|
|
3815
3828
|
let wantEvent = _state.showBranchEvents === true;
|
|
3816
3829
|
let wantDot = _state.showSupportDots === true;
|
|
3817
3830
|
|
|
@@ -4080,39 +4093,32 @@ function (root, d3, forester, phyloXml) {
|
|
|
4080
4093
|
}
|
|
4081
4094
|
};
|
|
4082
4095
|
|
|
4096
|
+
// A branch's values as "MAD/support", as the desktop writes them: the MAD
|
|
4097
|
+
// value (from MAD rooting, to two decimals, 0 included -- low is good)
|
|
4098
|
+
// while MAD Values is on, then the support values while Confidence is on,
|
|
4099
|
+
// those only when one reaches the minimum. A MAD value is never support.
|
|
4083
4100
|
let makeConfidenceValuesLabel = function (phynode) {
|
|
4084
|
-
if (phynode.confidences
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
}
|
|
4095
|
-
if (!show) {
|
|
4096
|
-
return;
|
|
4097
|
-
}
|
|
4101
|
+
if (!phynode.confidences || phynode.confidences.length === 0) {
|
|
4102
|
+
return;
|
|
4103
|
+
}
|
|
4104
|
+
let parts = [];
|
|
4105
|
+
let support = [];
|
|
4106
|
+
phynode.confidences.forEach(function (c) {
|
|
4107
|
+
if (c.type !== forester.MAD_CONFIDENCE_TYPE) {
|
|
4108
|
+
support.push(c);
|
|
4109
|
+
} else if (_state.showMadValues && typeof c.value === 'number' && isFinite(c.value)) {
|
|
4110
|
+
parts.push(+c.value.toFixed(2));
|
|
4098
4111
|
}
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
for (let ii = 0; ii < cl; ++ii) {
|
|
4106
|
-
if (c[ii].value) {
|
|
4107
|
-
if (ii > 0) {
|
|
4108
|
-
s += "/";
|
|
4109
|
-
}
|
|
4110
|
-
s += +c[ii].value.toFixed(CONFIDENCE_VALUE_DIGITS_DEFAULT);
|
|
4111
|
-
}
|
|
4112
|
+
});
|
|
4113
|
+
if (_state.showConfidenceValues && support.length > 0
|
|
4114
|
+
&& (!_state.minConfidenceValueToShow || support.some(function (c) { return c.value >= _state.minConfidenceValueToShow; }))) {
|
|
4115
|
+
support.forEach(function (c) {
|
|
4116
|
+
if (c.value) {
|
|
4117
|
+
parts.push(+c.value.toFixed(CONFIDENCE_VALUE_DIGITS_DEFAULT));
|
|
4112
4118
|
}
|
|
4113
|
-
|
|
4114
|
-
}
|
|
4119
|
+
});
|
|
4115
4120
|
}
|
|
4121
|
+
return parts.length > 0 ? parts.join('/') : undefined;
|
|
4116
4122
|
};
|
|
4117
4123
|
|
|
4118
4124
|
let makeBranchEventsLabel = function (phynode) {
|
|
@@ -4173,12 +4179,23 @@ function (root, d3, forester, phyloXml) {
|
|
|
4173
4179
|
return (branchWidth + SUPPORT_DOT_EXTRA_DIAMETER) / 2;
|
|
4174
4180
|
}
|
|
4175
4181
|
|
|
4182
|
+
// A branch's first confidence that is support: a MAD value (MAD rooting)
|
|
4183
|
+
// rates a root position, not the clade.
|
|
4184
|
+
function firstSupportValue(d) {
|
|
4185
|
+
for (let i = 0; i < d.confidences.length; ++i) {
|
|
4186
|
+
if (d.confidences[i].type !== forester.MAD_CONFIDENCE_TYPE) {
|
|
4187
|
+
return d.confidences[i].value;
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
return undefined;
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4176
4193
|
function showSupportDot(d) {
|
|
4177
4194
|
if (!_state.showSupportDots || !d.parent
|
|
4178
4195
|
|| !d.confidences || d.confidences.length === 0) {
|
|
4179
4196
|
return false;
|
|
4180
4197
|
}
|
|
4181
|
-
let v = d
|
|
4198
|
+
let v = firstSupportValue(d);
|
|
4182
4199
|
if (typeof v !== 'number' || !isFinite(v) || v < supportDotThreshold()) {
|
|
4183
4200
|
return false;
|
|
4184
4201
|
}
|
|
@@ -4763,6 +4780,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4763
4780
|
// elements by forester.timeAxisInfo -- again unless the caller set
|
|
4764
4781
|
// showTimeAxis explicitly.
|
|
4765
4782
|
_timeInfo = _treeData ? forester.timeAxisInfo(forester.getTreeRoot(_treeData)) : null;
|
|
4783
|
+
_timeTree = _treeData ? forester.isTimeTree(_treeData) : false;
|
|
4766
4784
|
if (_state.showTimeAxis === undefined) {
|
|
4767
4785
|
_state.showTimeAxis = !!(_timeInfo && _timeInfo.type);
|
|
4768
4786
|
}
|
|
@@ -5877,14 +5895,25 @@ function (root, d3, forester, phyloXml) {
|
|
|
5877
5895
|
update(null, 0);
|
|
5878
5896
|
}});
|
|
5879
5897
|
}
|
|
5880
|
-
if (!_in_subtree && d.parent && d.parent.parent
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5885
|
-
});
|
|
5886
|
-
|
|
5887
|
-
|
|
5898
|
+
if (!_in_subtree && d.parent && d.parent.parent) {
|
|
5899
|
+
let blocked = rerootBlockedReason();
|
|
5900
|
+
if (blocked) {
|
|
5901
|
+
// offered but greyed, saying why, as the desktop does
|
|
5902
|
+
items.push({label: 'Reroot', disabled: true, title: blocked, action: function () {
|
|
5903
|
+
}});
|
|
5904
|
+
} else {
|
|
5905
|
+
items.push({label: 'Reroot', action: function () {
|
|
5906
|
+
confirmRerooting(tree, 'node', d, event, function () {
|
|
5907
|
+
rerootKeepingCollapse(function () {
|
|
5908
|
+
forester.removeMadConfidences(tree); // they rate the MAD rooting only
|
|
5909
|
+
forester.reRoot(tree, d, -1);
|
|
5910
|
+
});
|
|
5911
|
+
_basicTreeProperties = forester.collectBasicTreeProperties(_root);
|
|
5912
|
+
syncMadValuesCheckbox();
|
|
5913
|
+
zoomToFit();
|
|
5914
|
+
});
|
|
5915
|
+
}});
|
|
5916
|
+
}
|
|
5888
5917
|
}
|
|
5889
5918
|
if (_settings.enableManualNodeSelection) {
|
|
5890
5919
|
items.push({label: 'Select/Deselect Node', action: function () { selectDeselectNode(d); }});
|
|
@@ -7106,6 +7135,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7106
7135
|
['taxonomy', TAXONOMY_CB, 'showTaxonomy'],
|
|
7107
7136
|
['sequence', SEQUENCE_CB, 'showSequence'],
|
|
7108
7137
|
['confidence', CONFIDENCE_VALUES_CB, 'showConfidenceValues'],
|
|
7138
|
+
['madValues', MAD_VALUES_CB, 'showMadValues'],
|
|
7109
7139
|
['branchLength', BRANCH_LENGTH_VALUES_CB, 'showBranchLengthValues'],
|
|
7110
7140
|
['external', EXTERNAL_LABEL_CB, 'showExternalLabels'],
|
|
7111
7141
|
['internal', INTERNAL_LABEL_CB, 'showInternalLabels'],
|
|
@@ -7271,12 +7301,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
7271
7301
|
launchInto(_container, _trees, s.tree, cfg);
|
|
7272
7302
|
return;
|
|
7273
7303
|
}
|
|
7274
|
-
if (s.root === 'midpoint' && _viewOps.root !==
|
|
7275
|
-
&& (
|
|
7276
|
-
|
|
7277
|
-
forester.midpointRoot(_root_const);
|
|
7278
|
-
});
|
|
7279
|
-
_viewOps.root = 'midpoint';
|
|
7304
|
+
if ((s.root === 'midpoint' || s.root === 'mad') && _viewOps.root !== s.root
|
|
7305
|
+
&& rerootingAllowed()) {
|
|
7306
|
+
rootTreeBy(s.root);
|
|
7280
7307
|
}
|
|
7281
7308
|
if (s.order === 'asc' || s.order === 'desc') {
|
|
7282
7309
|
ladderizeSubtree(_root_const, s.order === 'asc', false);
|
|
@@ -7309,6 +7336,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
7309
7336
|
if (s.layout === 'rectangular' || s.layout === 'circular' || s.layout === 'unrooted') {
|
|
7310
7337
|
_state.circularDisplay = s.layout === 'circular';
|
|
7311
7338
|
_state.unrootedDisplay = s.layout === 'unrooted';
|
|
7339
|
+
if (_treeData && _treeData.rooted === false && byId(SEARCH_FIELD_SELECT_0)) {
|
|
7340
|
+
populateSearchMenus(); // before the view's searches pick their fields
|
|
7341
|
+
}
|
|
7312
7342
|
}
|
|
7313
7343
|
if (s.display === 'phylogram' || s.display === 'aligned' || s.display === 'cladogram') {
|
|
7314
7344
|
let measured = _basicTreeProperties.branchLengths === true;
|
|
@@ -8801,11 +8831,86 @@ function (root, d3, forester, phyloXml) {
|
|
|
8801
8831
|
}
|
|
8802
8832
|
}
|
|
8803
8833
|
|
|
8804
|
-
//
|
|
8805
|
-
//
|
|
8806
|
-
//
|
|
8834
|
+
// Why the tree cannot be re-rooted, or null when it can. Every way of
|
|
8835
|
+
// re-rooting asks -- the re-root button and its menu, the node menu's
|
|
8836
|
+
// Reroot, a shared view's root: never a tree its file marks
|
|
8837
|
+
// rerootable="false", never a time tree. The reason is the greyed
|
|
8838
|
+
// controls' tooltip, as on the desktop.
|
|
8839
|
+
function rerootBlockedReason() {
|
|
8840
|
+
if (!_treeData) {
|
|
8841
|
+
return 'no tree';
|
|
8842
|
+
}
|
|
8843
|
+
if (_treeData.rerootable === false) {
|
|
8844
|
+
return 'This tree is marked as not re-rootable (rerootable="false").';
|
|
8845
|
+
}
|
|
8846
|
+
if (_timeTree) {
|
|
8847
|
+
return 'Time trees can\'t be re-rooted: their branch lengths are times measured from this root.';
|
|
8848
|
+
}
|
|
8849
|
+
return null;
|
|
8850
|
+
}
|
|
8851
|
+
|
|
8852
|
+
function rerootingAllowed() {
|
|
8853
|
+
return rerootBlockedReason() === null;
|
|
8854
|
+
}
|
|
8855
|
+
|
|
8856
|
+
// MAD rooting needs branch lengths, three tips, and a tree that may be
|
|
8857
|
+
// re-rooted at all.
|
|
8858
|
+
function madRootingPossible() {
|
|
8859
|
+
return rerootingAllowed()
|
|
8860
|
+
&& !!_basicTreeProperties && !!_basicTreeProperties.branchLengths
|
|
8861
|
+
&& _basicTreeProperties.externalNodesCount >= 3;
|
|
8862
|
+
}
|
|
8863
|
+
|
|
8864
|
+
// Roots the whole tree by 'mad' or 'midpoint', from the re-root menu or a
|
|
8865
|
+
// shared view. MAD values rate the MAD rooting only, so any other rooting
|
|
8866
|
+
// removes them.
|
|
8867
|
+
function rootTreeBy(method) {
|
|
8868
|
+
let rooted = true;
|
|
8869
|
+
rerootKeepingCollapse(function () {
|
|
8870
|
+
if (method === 'mad') {
|
|
8871
|
+
rooted = forester.madRoot(_root_const);
|
|
8872
|
+
} else {
|
|
8873
|
+
forester.removeMadConfidences(_root_const);
|
|
8874
|
+
forester.midpointRoot(_root_const);
|
|
8875
|
+
}
|
|
8876
|
+
});
|
|
8877
|
+
if (rooted) {
|
|
8878
|
+
_viewOps.root = method; // what a shared view replays
|
|
8879
|
+
}
|
|
8880
|
+
_basicTreeProperties = forester.collectBasicTreeProperties(_root_const);
|
|
8881
|
+
syncMadValuesCheckbox();
|
|
8882
|
+
}
|
|
8883
|
+
|
|
8884
|
+
// Before a re-root from the panel or the node menu (a shared view never
|
|
8885
|
+
// asks: whoever shared it chose): when it would change the clade of
|
|
8886
|
+
// internal nodes carrying data, say so where the choice was made, and
|
|
8887
|
+
// re-root only on "Re-root". Otherwise re-root at once.
|
|
8888
|
+
function confirmRerooting(phy, method, node, anchor, proceed) {
|
|
8889
|
+
let effect = forester.cladesChangedByRerooting(phy, method, node);
|
|
8890
|
+
let k = effect.changed.length;
|
|
8891
|
+
if (k === 0) {
|
|
8892
|
+
proceed();
|
|
8893
|
+
return;
|
|
8894
|
+
}
|
|
8895
|
+
let n = effect.annotated.length;
|
|
8896
|
+
let text = (n === 1)
|
|
8897
|
+
? 'This tree has data on 1 internal node. Re-rooting changes its clade, so its data may no longer describe it.'
|
|
8898
|
+
: 'This tree has data on ' + n + ' internal nodes. Re-rooting changes the clade of ' + k + ' of them, so '
|
|
8899
|
+
+ (k === 1 ? 'its data may no longer describe it.' : 'their data may no longer describe them.');
|
|
8900
|
+
showNodeMenu([
|
|
8901
|
+
{note: text},
|
|
8902
|
+
{label: 'Re-root', action: proceed},
|
|
8903
|
+
{label: 'Cancel', action: function () {
|
|
8904
|
+
}}
|
|
8905
|
+
], anchor, 'Re-root tree');
|
|
8906
|
+
}
|
|
8907
|
+
|
|
8908
|
+
// Re-rooting rearranges the whole tree and its button is easy to hit by
|
|
8909
|
+
// accident, so it asks first -- through the same little popup the node
|
|
8910
|
+
// menu uses (click anywhere else or press Esc to cancel) -- and the popup
|
|
8911
|
+
// is where the method is picked.
|
|
8807
8912
|
function midpointRootButtonPressed(event) {
|
|
8808
|
-
if (!_in_subtree && _root && (
|
|
8913
|
+
if (!_in_subtree && _root && rerootingAllowed()) {
|
|
8809
8914
|
let ev = event;
|
|
8810
8915
|
if (!ev || ev.pageX === undefined || (ev.pageX === 0 && ev.pageY === 0)) {
|
|
8811
8916
|
// keyboard/synthetic invocation: anchor the popup at the button
|
|
@@ -8815,21 +8920,30 @@ function (root, d3, forester, phyloXml) {
|
|
|
8815
8920
|
ev = {pageX: window.scrollX + r.right + 4, pageY: window.scrollY + r.top};
|
|
8816
8921
|
}
|
|
8817
8922
|
}
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8923
|
+
let items = [];
|
|
8924
|
+
if (madRootingPossible()) {
|
|
8925
|
+
items.push({
|
|
8926
|
+
label: 'MAD re-root (Tria et al., 2017)', title: MAD_CITATION, action: function () {
|
|
8927
|
+
confirmRerooting(_root_const, 'mad', null, ev, function () {
|
|
8928
|
+
rootTreeBy('mad');
|
|
8929
|
+
zoomToFit();
|
|
8823
8930
|
});
|
|
8824
|
-
_viewOps.root = 'midpoint'; // what a shared view replays
|
|
8825
|
-
zoomToFit();
|
|
8826
|
-
}
|
|
8827
|
-
},
|
|
8828
|
-
{
|
|
8829
|
-
label: 'Cancel', action: function () {
|
|
8830
8931
|
}
|
|
8932
|
+
});
|
|
8933
|
+
}
|
|
8934
|
+
items.push({
|
|
8935
|
+
label: 'Midpoint re-root', action: function () {
|
|
8936
|
+
confirmRerooting(_root_const, 'midpoint', null, ev, function () {
|
|
8937
|
+
rootTreeBy('midpoint');
|
|
8938
|
+
zoomToFit();
|
|
8939
|
+
});
|
|
8940
|
+
}
|
|
8941
|
+
});
|
|
8942
|
+
items.push({
|
|
8943
|
+
label: 'Cancel', action: function () {
|
|
8831
8944
|
}
|
|
8832
|
-
|
|
8945
|
+
});
|
|
8946
|
+
showNodeMenu(items, ev, 're-root the tree?');
|
|
8833
8947
|
}
|
|
8834
8948
|
}
|
|
8835
8949
|
|
|
@@ -9034,7 +9148,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
9034
9148
|
// then set up each box's mode menu. Called when a tree is (re)loaded.
|
|
9035
9149
|
function populateSearchMenus() {
|
|
9036
9150
|
let before = _searchFields;
|
|
9037
|
-
|
|
9151
|
+
// an unrooted tree in the unrooted layout has no root to measure from
|
|
9152
|
+
let rootMeasured = [forester.searchFields.depth, forester.searchFields.distanceFromRoot, forester.searchFields.cladeSize];
|
|
9153
|
+
_searchFields = forester.availableSearchFields(_root).filter(function (f) {
|
|
9154
|
+
return !unrootedContext() || rootMeasured.indexOf(f) < 0;
|
|
9155
|
+
});
|
|
9038
9156
|
[SEARCH_FIELD_SELECT_0, SEARCH_FIELD_SELECT_1].forEach(function (selId) {
|
|
9039
9157
|
let sel = byId(selId);
|
|
9040
9158
|
if (!sel) return;
|
|
@@ -9369,6 +9487,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
9369
9487
|
function layoutButtonClicked() {
|
|
9370
9488
|
_state.circularDisplay = getCheckboxValue(LAYOUT_CIRC_BUTTON);
|
|
9371
9489
|
_state.unrootedDisplay = getCheckboxValue(LAYOUT_UNROOTED_BUTTON);
|
|
9490
|
+
if (_treeData && _treeData.rooted === false) {
|
|
9491
|
+
populateSearchMenus(); // the root-measured search fields go and come with the unrooted layout
|
|
9492
|
+
search0();
|
|
9493
|
+
search1();
|
|
9494
|
+
}
|
|
9372
9495
|
if (radialDisplay() && _state.showDomainArchitectures && _basicTreeProperties.domainArchitectures) {
|
|
9373
9496
|
// the domain boxes ride the tips' spokes, so the labels must too
|
|
9374
9497
|
_radialLabelsHorizontal = false;
|
|
@@ -9529,6 +9652,31 @@ function (root, d3, forester, phyloXml) {
|
|
|
9529
9652
|
scheduleUpdate();
|
|
9530
9653
|
}
|
|
9531
9654
|
|
|
9655
|
+
function madValuesCbClicked() {
|
|
9656
|
+
_state.showMadValues = getCheckboxValue(MAD_VALUES_CB);
|
|
9657
|
+
scheduleUpdate();
|
|
9658
|
+
}
|
|
9659
|
+
|
|
9660
|
+
// The MAD Values checkbox is built for every tree MAD rooting could root
|
|
9661
|
+
// and shows while the tree carries MAD values: after MAD rooting, or from
|
|
9662
|
+
// a file saved after it. Any other rooting removes the values, and the
|
|
9663
|
+
// checkbox goes with them. Inside a subtree the whole tree's state stands.
|
|
9664
|
+
function syncMadValuesCheckbox() {
|
|
9665
|
+
let cb = byId(MAD_VALUES_CB);
|
|
9666
|
+
if (!cb || _in_subtree) {
|
|
9667
|
+
return;
|
|
9668
|
+
}
|
|
9669
|
+
let present = !!(_basicTreeProperties && _basicTreeProperties.madValues);
|
|
9670
|
+
if (!present) {
|
|
9671
|
+
_state.showMadValues = false;
|
|
9672
|
+
}
|
|
9673
|
+
setCheckboxValue(MAD_VALUES_CB, _state.showMadValues === true);
|
|
9674
|
+
let item = cb.closest('label');
|
|
9675
|
+
if (item) {
|
|
9676
|
+
item.style.display = present ? '' : 'none';
|
|
9677
|
+
}
|
|
9678
|
+
}
|
|
9679
|
+
|
|
9532
9680
|
function branchLengthsCbClicked() {
|
|
9533
9681
|
_state.showBranchLengthValues = getCheckboxValue(BRANCH_LENGTH_VALUES_CB);
|
|
9534
9682
|
scheduleUpdate();
|
|
@@ -10249,6 +10397,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10249
10397
|
+ '.aptx-node-menu button.aptx-menu-danger:hover, .aptx-node-menu button.aptx-menu-danger:focus-visible {'
|
|
10250
10398
|
+ ' background:#e5484d; color:#fff; }'
|
|
10251
10399
|
+ '.aptx-node-menu hr { border:0; border-top:1px solid var(--p-line); margin:3px 4px; }'
|
|
10400
|
+
+ '.aptx-node-menu-note { padding:4px 9px 7px; color:var(--p-ink); white-space:normal; }'
|
|
10252
10401
|
// The search suggestions: the node menu's chrome under the value box.
|
|
10253
10402
|
+ '.aptx-suggest { position:absolute; z-index:1000; max-width:320px; padding:4px; box-sizing:border-box;'
|
|
10254
10403
|
+ ' border:1px solid var(--p-line-strong); border-radius:10px; background:var(--p-bg); color:var(--p-ink);'
|
|
@@ -10731,6 +10880,32 @@ function (root, d3, forester, phyloXml) {
|
|
|
10731
10880
|
// Content arrives as "Label: value" lines separated by <br>. Setting the
|
|
10732
10881
|
// label part apart makes a wall of such lines scannable. Lines without a
|
|
10733
10882
|
// label (a heading like "Taxonomy", or a FASTA sequence) are left alone.
|
|
10883
|
+
// The unrooted layout of a tree its file declares unrooted (phyloXML
|
|
10884
|
+
// rooted="false", Nexus [&U]; a plain Newick tree declares nothing). The
|
|
10885
|
+
// unrooted layout of a rooted tree still has its root.
|
|
10886
|
+
function unrootedContext() {
|
|
10887
|
+
return _state.unrootedDisplay === true && !!_treeData && _treeData.rooted === false;
|
|
10888
|
+
}
|
|
10889
|
+
|
|
10890
|
+
// The tips on each side of an internal node of an unrooted tree, one
|
|
10891
|
+
// count per neighbour, smallest first: what "tips below" becomes when
|
|
10892
|
+
// there is no below.
|
|
10893
|
+
function tipsAround(d) {
|
|
10894
|
+
let sides = d.children.map(function (c) {
|
|
10895
|
+
return forester.calcSumOfAllExternalDescendants(c);
|
|
10896
|
+
});
|
|
10897
|
+
let below = sides.reduce(function (a, b) {
|
|
10898
|
+
return a + b;
|
|
10899
|
+
}, 0);
|
|
10900
|
+
let all = forester.calcSumOfAllExternalDescendants(forester.getTreeRoot(_treeData));
|
|
10901
|
+
if (all > below) {
|
|
10902
|
+
sides.push(all - below);
|
|
10903
|
+
}
|
|
10904
|
+
return sides.sort(function (a, b) {
|
|
10905
|
+
return a - b;
|
|
10906
|
+
});
|
|
10907
|
+
}
|
|
10908
|
+
|
|
10734
10909
|
// The node's data as the hover tooltip and the "Display Node Data" dialog
|
|
10735
10910
|
// both show it -- ONE builder, because they used to be two near-copies
|
|
10736
10911
|
// and carried the same two bugs twice (a bare "Date: " line whenever a
|
|
@@ -10744,13 +10919,19 @@ function (root, d3, forester, phyloXml) {
|
|
|
10744
10919
|
// the section above it. markUpDataLabels() renders the result: a line
|
|
10745
10920
|
// shaped "Key: value" is a row, anything else a heading, and a leading
|
|
10746
10921
|
// "- " marks a row as a section's sub-entry.
|
|
10922
|
+
//
|
|
10923
|
+
// A tree its file declares unrooted, shown in the unrooted layout, has no
|
|
10924
|
+
// root to measure from (unrootedContext): an internal node shows no
|
|
10925
|
+
// distance to parent, depth or tips below but the tips on each of its
|
|
10926
|
+
// sides, and a tip's branch is just its branch length -- as on the desktop.
|
|
10747
10927
|
function nodeDataText(d) {
|
|
10748
10928
|
let text = '';
|
|
10929
|
+
let unrooted = unrootedContext();
|
|
10749
10930
|
if (d.name) {
|
|
10750
10931
|
text += 'Name: ' + d.name + '<br>';
|
|
10751
10932
|
}
|
|
10752
|
-
if (d.branch_length) {
|
|
10753
|
-
text += 'Distance to parent: ' + d.branch_length + '<br>';
|
|
10933
|
+
if (d.branch_length && !(unrooted && d.children)) {
|
|
10934
|
+
text += (unrooted ? 'Branch length: ' : 'Distance to parent: ') + d.branch_length + '<br>';
|
|
10754
10935
|
}
|
|
10755
10936
|
let date = dateText(d.date);
|
|
10756
10937
|
if (date) {
|
|
@@ -10764,9 +10945,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
10764
10945
|
}
|
|
10765
10946
|
}
|
|
10766
10947
|
}
|
|
10767
|
-
|
|
10768
|
-
|
|
10769
|
-
|
|
10948
|
+
if (!unrooted) {
|
|
10949
|
+
text += 'Depth: ' + forester.calcDepth(d) + '<br>';
|
|
10950
|
+
if (d.children) {
|
|
10951
|
+
text += 'Tips below: ' + forester.calcSumOfAllExternalDescendants(d) + '<br>';
|
|
10952
|
+
}
|
|
10953
|
+
} else if (d.children) {
|
|
10954
|
+
text += 'Tips around: ' + tipsAround(d).join(' · ') + '<br>';
|
|
10770
10955
|
}
|
|
10771
10956
|
if (d.confidences) {
|
|
10772
10957
|
for (let i = 0; i < d.confidences.length; ++i) {
|
|
@@ -11359,6 +11544,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11359
11544
|
|
|
11360
11545
|
on(CONFIDENCE_VALUES_CB, 'click', confidenceValuesCbClicked);
|
|
11361
11546
|
on(SUPPORT_DOTS_CB, 'click', supportDotsCbClicked);
|
|
11547
|
+
on(MAD_VALUES_CB, 'click', madValuesCbClicked);
|
|
11362
11548
|
on(SEARCH_B_TOGGLE, 'click', revealSearchB);
|
|
11363
11549
|
on(SEARCH_NAV_PREV, 'click', function () {
|
|
11364
11550
|
stepToFoundNode(-1);
|
|
@@ -11867,6 +12053,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
11867
12053
|
if (_basicTreeProperties.confidences) {
|
|
11868
12054
|
labels.push(makeCheckboxItem('Confidence', CONFIDENCE_VALUES_CB, 'to show/hide confidence values'));
|
|
11869
12055
|
}
|
|
12056
|
+
if (_basicTreeProperties.madValues || madRootingPossible()) {
|
|
12057
|
+
labels.push(makeCheckboxItem('MAD Values', MAD_VALUES_CB, 'to show/hide MAD values: the ancestor deviation were the root on that branch (lower is better, the root branch has the smallest)'));
|
|
12058
|
+
}
|
|
11870
12059
|
if (_basicTreeProperties.branchLengths) {
|
|
11871
12060
|
labels.push(makeCheckboxItem('Branch Length', BRANCH_LENGTH_VALUES_CB, 'to show/hide branch length values'));
|
|
11872
12061
|
}
|
|
@@ -11955,7 +12144,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11955
12144
|
h = h.concat(makeGlyphButton('whole_tree', RETURN_TO_SUPERTREE_BUTTON, 'return all the way to the complete tree (if in a sub-tree)'));
|
|
11956
12145
|
h = h.concat(makeGlyphButton('up_one_level', RETURN_TO_SUPERTREE_BUTTON_BY_ONE, 'move up by one level towards the complete tree (if in a sub-tree)'));
|
|
11957
12146
|
h = h.concat(makeGlyphButton('uncollapse_all', UNCOLLAPSE_ALL_BUTTON, 'uncollapse all'));
|
|
11958
|
-
h = h.concat(makeGlyphButton('midpoint', MIDPOINT_ROOT_BUTTON, '
|
|
12147
|
+
h = h.concat(makeGlyphButton('midpoint', MIDPOINT_ROOT_BUTTON, 're-root the tree: MAD or midpoint'));
|
|
11959
12148
|
h = h.concat('</div>');
|
|
11960
12149
|
h = h.concat('</fieldset>');
|
|
11961
12150
|
return h;
|
|
@@ -12190,6 +12379,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
12190
12379
|
setCheckboxValue(SEQUENCE_CB, _state.showSequence)
|
|
12191
12380
|
setCheckboxValue(CONFIDENCE_VALUES_CB, _state.showConfidenceValues);
|
|
12192
12381
|
setCheckboxValue(SUPPORT_DOTS_CB, _state.showSupportDots);
|
|
12382
|
+
syncMadValuesCheckbox();
|
|
12193
12383
|
setCheckboxValue(BRANCH_LENGTH_VALUES_CB, _state.showBranchLengthValues);
|
|
12194
12384
|
setCheckboxValue(NODE_EVENTS_CB, _state.showNodeEvents);
|
|
12195
12385
|
setCheckboxValue(BRANCH_EVENTS_CB, _state.showBranchEvents);
|
|
@@ -12308,10 +12498,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
12308
12498
|
disableButton(byId(RETURN_TO_SUPERTREE_BUTTON));
|
|
12309
12499
|
}
|
|
12310
12500
|
|
|
12311
|
-
|
|
12312
|
-
|
|
12501
|
+
let rerootButton = byId(MIDPOINT_ROOT_BUTTON);
|
|
12502
|
+
let blocked = rerootBlockedReason();
|
|
12503
|
+
if (!_in_subtree && !blocked) {
|
|
12504
|
+
enableButton(rerootButton);
|
|
12313
12505
|
} else {
|
|
12314
|
-
disableButton(
|
|
12506
|
+
disableButton(rerootButton);
|
|
12507
|
+
}
|
|
12508
|
+
if (rerootButton) {
|
|
12509
|
+
rerootButton.title = blocked
|
|
12510
|
+
|| (_in_subtree ? 'return to the whole tree to re-root it' : 're-root the tree: MAD or midpoint');
|
|
12315
12511
|
}
|
|
12316
12512
|
let b;
|
|
12317
12513
|
if (_foundNodes0 && !_searchBox0Empty) {
|
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.5.1
|
|
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
|
-
|
|
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,544 @@
|
|
|
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
|
+
// Tip pairs closer than this share of the tree's diameter are left out of
|
|
459
|
+
// every sum, like pairs at distance 0. Their 1/d^2 terms (tips 1e-8 apart,
|
|
460
|
+
// as FastTree's 5e-9 "zero" branches make them, reach 1e16) cancel
|
|
461
|
+
// catastrophically in the subtractions below and swamp every other pair:
|
|
462
|
+
// MAD then answered differently from every rooting, and re-rooting moved
|
|
463
|
+
// the root back and forth. The diameter does not depend on the rooting, so
|
|
464
|
+
// neither does the cut. Joint with the desktop (Christian, 2026-09-14).
|
|
465
|
+
const MAD_NEAR_FRACTION = 1e-5;
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Roots the tree by Minimal Ancestor Deviation (Tria, Landan & Dagan,
|
|
469
|
+
* Nature Ecology & Evolution 1, 0193, 2017; doi:10.1038/s41559-017-0193),
|
|
470
|
+
* ported from the desktop's PhylogenyMethods.madRoot.
|
|
471
|
+
*
|
|
472
|
+
* Under a (relaxed) clock the ancestor of two tips is equidistant from
|
|
473
|
+
* both; the pair (i,j) with ancestor a deviates by |2*dist(a,i)/dist(i,j) - 1|.
|
|
474
|
+
* For every branch the root position minimizing the summed squared
|
|
475
|
+
* deviation of all tip pairs is found analytically, and the branch and
|
|
476
|
+
* position with the smallest total become the root.
|
|
477
|
+
*
|
|
478
|
+
* Every internal branch gets a confidence of type 'MAD': the root-mean-
|
|
479
|
+
* square deviation were the root placed on that branch -- low is good,
|
|
480
|
+
* and the root's branch carries the smallest. Pendant branches get none,
|
|
481
|
+
* and MAD values from an earlier run are replaced.
|
|
482
|
+
*
|
|
483
|
+
* Tip pairs closer than 1e-5 of the tree's diameter (identical sequences,
|
|
484
|
+
* FastTree's 5e-9 "zero" branches) are left out of the deviation sums:
|
|
485
|
+
* their deviation measures noise, and their terms would swamp the
|
|
486
|
+
* precision of all the others.
|
|
487
|
+
*
|
|
488
|
+
* A no-op for fewer than three tips or a tree without branch lengths.
|
|
489
|
+
* O(n^2) time and O(n) memory: the desktop fills an n x n distance matrix
|
|
490
|
+
* only to sum its columns (1.4 GB at 13,000 tips), so the column sums are
|
|
491
|
+
* added up here as the pairs are met, and a subtree's tips are a range of
|
|
492
|
+
* tip numbers rather than a list.
|
|
493
|
+
*
|
|
494
|
+
* @param phy the tree
|
|
495
|
+
* @returns {boolean} whether the tree was re-rooted
|
|
496
|
+
*/
|
|
497
|
+
forester.madRoot = function (phy) {
|
|
498
|
+
let root = forester.getTreeRoot(phy);
|
|
499
|
+
if (!root) {
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
let t = madTraversal(root);
|
|
503
|
+
let pre = t.pre;
|
|
504
|
+
let kids = t.kids;
|
|
505
|
+
let post = t.post;
|
|
506
|
+
let m = pre.length;
|
|
507
|
+
let tipNo = new Int32Array(m).fill(-1);
|
|
508
|
+
let tipPos = [];
|
|
509
|
+
for (let k = 0; k < m; ++k) {
|
|
510
|
+
if (kids[k].length === 0) {
|
|
511
|
+
tipNo[k] = tipPos.length;
|
|
512
|
+
tipPos.push(k);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
let n = tipPos.length;
|
|
516
|
+
if (n < 3) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
// depth (distance from the current root) of every node
|
|
520
|
+
let depth = new Float64Array(m);
|
|
521
|
+
let maxDepth = 0;
|
|
522
|
+
for (let k = 1; k < m; ++k) {
|
|
523
|
+
depth[k] = depth[t.parentOf[k]] + madLength(pre[k]);
|
|
524
|
+
maxDepth = Math.max(maxDepth, depth[k]);
|
|
525
|
+
}
|
|
526
|
+
if (maxDepth <= 0) {
|
|
527
|
+
return false; // no usable branch lengths
|
|
528
|
+
}
|
|
529
|
+
// the diameter, the longest tip-to-tip path: at every node, its two
|
|
530
|
+
// longest paths down through different children
|
|
531
|
+
let longestDown = new Float64Array(m);
|
|
532
|
+
let diameter = 0;
|
|
533
|
+
for (let q = 0; q < m; ++q) {
|
|
534
|
+
let k = post[q];
|
|
535
|
+
let first = 0;
|
|
536
|
+
let second = 0;
|
|
537
|
+
for (let c = 0; c < kids[k].length; ++c) {
|
|
538
|
+
let path = longestDown[kids[k][c]] + madLength(pre[kids[k][c]]);
|
|
539
|
+
if (path > first) {
|
|
540
|
+
second = first;
|
|
541
|
+
first = path;
|
|
542
|
+
} else if (path > second) {
|
|
543
|
+
second = path;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
longestDown[k] = first;
|
|
547
|
+
diameter = Math.max(diameter, first + second);
|
|
548
|
+
}
|
|
549
|
+
let nearEps = Math.max(MAD_EPSILON, MAD_NEAR_FRACTION * diameter);
|
|
550
|
+
let tipDepth = new Float64Array(n);
|
|
551
|
+
for (let i = 0; i < n; ++i) {
|
|
552
|
+
tipDepth[i] = depth[tipPos[i]];
|
|
553
|
+
}
|
|
554
|
+
// Pass 1 (post-order): the within-subtree deviation sums, and the
|
|
555
|
+
// column sums sum_i 1/d^2 and sum_i 1/d of every tip. For a pair whose
|
|
556
|
+
// common ancestor sits at depth dm, d = depth[i] + depth[j] - 2*dm and
|
|
557
|
+
// the deviation is 2*(depth[i]-dm)/d - 1.
|
|
558
|
+
let lo = new Int32Array(m); // a subtree's tips are the numbers lo..hi-1
|
|
559
|
+
let hi = new Int32Array(m);
|
|
560
|
+
let down = new Float64Array(m); // sum dev^2 over pairs with their ancestor inside the subtree
|
|
561
|
+
let w0 = new Float64Array(m); // sum_{i!=j in subtree} 1/d^2 (ordered)
|
|
562
|
+
let w1 = new Float64Array(m); // sum_{i!=j in subtree} (2*depth[j]/d^2 - 1/d) (j second)
|
|
563
|
+
let w2 = new Float64Array(m); // sum_{i!=j in subtree} (2*depth[j]/d - 1)^2 (j second)
|
|
564
|
+
let col0 = new Float64Array(n);
|
|
565
|
+
let colInv = new Float64Array(n);
|
|
566
|
+
let partners = new Int32Array(n); // the tips each tip is counted against (d > nearEps)
|
|
567
|
+
for (let q = 0; q < m; ++q) {
|
|
568
|
+
let k = post[q];
|
|
569
|
+
let ch = kids[k];
|
|
570
|
+
if (ch.length === 0) {
|
|
571
|
+
lo[k] = tipNo[k];
|
|
572
|
+
hi[k] = tipNo[k] + 1;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
let dm = depth[k];
|
|
576
|
+
let dwn = 0, sw0 = 0, sw1 = 0, sw2 = 0;
|
|
577
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
578
|
+
dwn += down[ch[c]];
|
|
579
|
+
sw0 += w0[ch[c]];
|
|
580
|
+
sw1 += w1[ch[c]];
|
|
581
|
+
sw2 += w2[ch[c]];
|
|
582
|
+
}
|
|
583
|
+
// the pairs whose ancestor is this node: one tip from each of two children
|
|
584
|
+
for (let a = 0; a < ch.length; ++a) {
|
|
585
|
+
for (let b = a + 1; b < ch.length; ++b) {
|
|
586
|
+
for (let i = lo[ch[a]]; i < hi[ch[a]]; ++i) {
|
|
587
|
+
let di = tipDepth[i];
|
|
588
|
+
for (let j = lo[ch[b]]; j < hi[ch[b]]; ++j) {
|
|
589
|
+
let dj = tipDepth[j];
|
|
590
|
+
let dij = (di - dm) + (dj - dm);
|
|
591
|
+
if (dij > nearEps) {
|
|
592
|
+
let inv = 1.0 / dij;
|
|
593
|
+
let inv2 = inv * inv;
|
|
594
|
+
let dev = (2.0 * (di - dm) * inv) - 1.0;
|
|
595
|
+
dwn += dev * dev;
|
|
596
|
+
sw0 += 2.0 * inv2; // both orderings
|
|
597
|
+
sw1 += (2.0 * (di + dj) * inv2) - (2.0 * inv);
|
|
598
|
+
let gi = (2.0 * di * inv) - 1.0;
|
|
599
|
+
let gj = (2.0 * dj * inv) - 1.0;
|
|
600
|
+
sw2 += (gi * gi) + (gj * gj);
|
|
601
|
+
col0[i] += inv2;
|
|
602
|
+
col0[j] += inv2;
|
|
603
|
+
colInv[i] += inv;
|
|
604
|
+
colInv[j] += inv;
|
|
605
|
+
++partners[i];
|
|
606
|
+
++partners[j];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
lo[k] = lo[ch[0]];
|
|
613
|
+
hi[k] = hi[ch[ch.length - 1]];
|
|
614
|
+
down[k] = dwn;
|
|
615
|
+
w0[k] = sw0;
|
|
616
|
+
w1[k] = sw1;
|
|
617
|
+
w2[k] = sw2;
|
|
618
|
+
}
|
|
619
|
+
// Pass 2 (post-order): per-subtree "all-i" sums a0,a1,a2; the cross
|
|
620
|
+
// sums between a subtree and its complement are then b_k = a_k - w_k.
|
|
621
|
+
let a0 = new Float64Array(m);
|
|
622
|
+
let a1 = new Float64Array(m);
|
|
623
|
+
let a2 = new Float64Array(m);
|
|
624
|
+
let b0 = new Float64Array(m);
|
|
625
|
+
let b1 = new Float64Array(m);
|
|
626
|
+
let b2 = new Float64Array(m);
|
|
627
|
+
for (let q = 0; q < m; ++q) {
|
|
628
|
+
let k = post[q];
|
|
629
|
+
let s0 = 0, s1 = 0, s2 = 0;
|
|
630
|
+
if (kids[k].length === 0) {
|
|
631
|
+
let j = tipNo[k];
|
|
632
|
+
s0 = col0[j];
|
|
633
|
+
s1 = (2.0 * tipDepth[j] * col0[j]) - colInv[j];
|
|
634
|
+
// (2*depth/d - 1)^2 expands to 4*depth^2/d^2 - 4*depth/d + 1: the
|
|
635
|
+
// 1 is owed once per pair actually summed. The desktop adds n - 1,
|
|
636
|
+
// which also counts the pairs at distance 0 (identical sequences)
|
|
637
|
+
// that every other sum skips.
|
|
638
|
+
s2 = (4.0 * tipDepth[j] * tipDepth[j] * col0[j]) - (4.0 * tipDepth[j] * colInv[j]) + partners[j];
|
|
639
|
+
} else {
|
|
640
|
+
for (let c = 0; c < kids[k].length; ++c) {
|
|
641
|
+
s0 += a0[kids[k][c]];
|
|
642
|
+
s1 += a1[kids[k][c]];
|
|
643
|
+
s2 += a2[kids[k][c]];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
a0[k] = s0;
|
|
647
|
+
a1[k] = s1;
|
|
648
|
+
a2[k] = s2;
|
|
649
|
+
b0[k] = s0 - w0[k];
|
|
650
|
+
b1[k] = s1 - w1[k];
|
|
651
|
+
b2[k] = s2 - w2[k];
|
|
652
|
+
}
|
|
653
|
+
// Pass 3 (pre-order): up[k] = the deviation sum of the pairs whose
|
|
654
|
+
// ancestor lies outside subtree k, by the rerooting recursion, so
|
|
655
|
+
// every branch's total is O(1).
|
|
656
|
+
let up = new Float64Array(m);
|
|
657
|
+
let atParent = new Float64Array(m); // a branch's cross deviation with the root at its parent
|
|
658
|
+
for (let k = 0; k < m; ++k) {
|
|
659
|
+
let ch = kids[k];
|
|
660
|
+
if (ch.length === 0) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
let sumDown = 0;
|
|
664
|
+
let sumAtParent = 0;
|
|
665
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
666
|
+
let x = ch[c];
|
|
667
|
+
sumDown += down[x];
|
|
668
|
+
atParent[x] = madCross(depth[x], b0[x], b1[x], b2[x], madLength(pre[x]));
|
|
669
|
+
sumAtParent += atParent[x];
|
|
670
|
+
}
|
|
671
|
+
// cross deviation among all the groups meeting here: the child subtrees and the complement
|
|
672
|
+
let own = (k === 0) ? 0.0 : madCross(depth[k], b0[k], b1[k], b2[k], 0.0);
|
|
673
|
+
let crossAmong = (sumAtParent + own) / 2.0;
|
|
674
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
675
|
+
let x = ch[c];
|
|
676
|
+
up[x] = up[k] + (sumDown - down[x]) + (crossAmong - atParent[x]);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
// The branch and position with the smallest total deviation, and every
|
|
680
|
+
// branch's smallest MAD value keyed by the tips on its far side, so
|
|
681
|
+
// the value finds its branch again after the re-root has turned
|
|
682
|
+
// parents into children. The first minimum in post-order wins, as on
|
|
683
|
+
// the desktop.
|
|
684
|
+
let hash = madTipHashes(n);
|
|
685
|
+
let nPairs = (n * (n - 1.0)) / 2.0;
|
|
686
|
+
let madByKey = new Map();
|
|
687
|
+
let bestSsd = Infinity;
|
|
688
|
+
let best = -1;
|
|
689
|
+
let bestX = 0;
|
|
690
|
+
for (let q = 0; q < m; ++q) {
|
|
691
|
+
let c = post[q];
|
|
692
|
+
if (c === 0) {
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
let length = madLength(pre[c]);
|
|
696
|
+
// the optimal root position, as a distance from c toward its parent, kept on the branch
|
|
697
|
+
let x = (b0[c] > MAD_EPSILON) ? (depth[c] - (b1[c] / (2.0 * b0[c]))) : 0.0;
|
|
698
|
+
if (x < 0) {
|
|
699
|
+
x = 0;
|
|
700
|
+
} else if (x > length) {
|
|
701
|
+
x = length;
|
|
702
|
+
}
|
|
703
|
+
let ssd = madCross(depth[c], b0[c], b1[c], b2[c], x) + down[c] + up[c];
|
|
704
|
+
// a sum of squares; clamp a tiny negative left by cancellation, so sqrt is never NaN
|
|
705
|
+
let mad = Math.sqrt(Math.max(0.0, ssd) / nPairs);
|
|
706
|
+
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);
|
|
707
|
+
let prev = madByKey.get(key);
|
|
708
|
+
if (prev === undefined || mad < prev) {
|
|
709
|
+
madByKey.set(key, mad); // the two halves of a bifurcating root share a key
|
|
710
|
+
}
|
|
711
|
+
if (ssd < bestSsd) {
|
|
712
|
+
bestSsd = ssd;
|
|
713
|
+
best = c;
|
|
714
|
+
bestX = x;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
if (best < 0) {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
let tipNumber = new Map();
|
|
721
|
+
for (let i = 0; i < n; ++i) {
|
|
722
|
+
tipNumber.set(pre[tipPos[i]], i);
|
|
723
|
+
}
|
|
724
|
+
forester.removeMadConfidences(phy);
|
|
725
|
+
forester.reRoot(phy, pre[best], bestX);
|
|
726
|
+
// annotate the internal branches of the re-rooted tree
|
|
727
|
+
let r = madTraversal(forester.getTreeRoot(phy));
|
|
728
|
+
let count = new Int32Array(r.pre.length);
|
|
729
|
+
let xa = new Int32Array(r.pre.length);
|
|
730
|
+
let xb = new Int32Array(r.pre.length);
|
|
731
|
+
let hasFirst = new Uint8Array(r.pre.length);
|
|
732
|
+
for (let q = 0; q < r.post.length; ++q) {
|
|
733
|
+
let k = r.post[q];
|
|
734
|
+
let ch = r.kids[k];
|
|
735
|
+
if (ch.length === 0) {
|
|
736
|
+
let i = tipNumber.get(r.pre[k]);
|
|
737
|
+
count[k] = 1;
|
|
738
|
+
xa[k] = hash.xa[i + 1] ^ hash.xa[i];
|
|
739
|
+
xb[k] = hash.xb[i + 1] ^ hash.xb[i];
|
|
740
|
+
hasFirst[k] = (i === 0) ? 1 : 0;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
744
|
+
count[k] += count[ch[c]];
|
|
745
|
+
xa[k] ^= xa[ch[c]];
|
|
746
|
+
xb[k] ^= xb[ch[c]];
|
|
747
|
+
hasFirst[k] |= hasFirst[ch[c]];
|
|
748
|
+
}
|
|
749
|
+
if (k !== 0) {
|
|
750
|
+
let mad = madByKey.get(hash.key(count[k], xa[k], xb[k], hasFirst[k] === 1));
|
|
751
|
+
if (mad !== undefined) {
|
|
752
|
+
let kept = r.pre[k].confidences || [];
|
|
753
|
+
r.pre[k].confidences = kept.concat([{value: mad, type: forester.MAD_CONFIDENCE_TYPE}]);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return true;
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Removes every 'MAD' confidence (from madRoot) and keeps all others: a
|
|
762
|
+
* tree rooted any other way no longer has the rooting those values rate.
|
|
763
|
+
*
|
|
764
|
+
* @param phy the tree
|
|
765
|
+
*/
|
|
766
|
+
forester.removeMadConfidences = function (phy) {
|
|
767
|
+
let root = forester.getTreeRoot(phy);
|
|
768
|
+
if (!root) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
madTraversal(root).pre.forEach(function (node) {
|
|
772
|
+
if (node.confidences && node.confidences.some(function (c) { return c.type === forester.MAD_CONFIDENCE_TYPE; })) {
|
|
773
|
+
// a new array: re-rooting can leave two branches sharing one
|
|
774
|
+
let kept = node.confidences.filter(function (c) { return c.type !== forester.MAD_CONFIDENCE_TYPE; });
|
|
775
|
+
if (kept.length > 0) {
|
|
776
|
+
node.confidences = kept;
|
|
777
|
+
} else {
|
|
778
|
+
delete node.confidences;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Whether a node carries data about the node itself, the kind a
|
|
786
|
+
* re-rooting can take the meaning away from: a name, taxonomy, sequence
|
|
787
|
+
* (a domain architecture lives there), events, distribution, date,
|
|
788
|
+
* references, or a property about the node. Not data in this sense: the
|
|
789
|
+
* branch above it -- length, support and MAD values, colour, width, a
|
|
790
|
+
* property applying to the branch -- which re-rooting carries along,
|
|
791
|
+
* visual styling (style: properties), the viewer's own aptx: properties,
|
|
792
|
+
* and an empty taxonomy. The desktop's list is the same.
|
|
793
|
+
*
|
|
794
|
+
* @param node
|
|
795
|
+
* @returns {boolean}
|
|
796
|
+
*/
|
|
797
|
+
forester.nodeHasData = function (node) {
|
|
798
|
+
let filled = function (x) {
|
|
799
|
+
return x !== undefined && x !== null && x !== '' && !(Array.isArray(x) && x.length === 0);
|
|
800
|
+
};
|
|
801
|
+
if ((typeof node.name === 'string' && node.name.length > 0)
|
|
802
|
+
|| (node.taxonomies && node.taxonomies.some(function (t) {
|
|
803
|
+
return t && Object.keys(t).some(function (key) { return filled(t[key]); });
|
|
804
|
+
}))
|
|
805
|
+
|| (node.sequences && node.sequences.length > 0)
|
|
806
|
+
|| node.events
|
|
807
|
+
|| (node.distributions && node.distributions.length > 0)
|
|
808
|
+
|| node.date
|
|
809
|
+
|| (node.references && node.references.length > 0)) {
|
|
810
|
+
return true;
|
|
811
|
+
}
|
|
812
|
+
return !!node.properties && node.properties.some(function (p) {
|
|
813
|
+
return p.applies_to !== BRANCH_EVENT_APPLIES_TO
|
|
814
|
+
&& !(typeof p.ref === 'string' && (p.ref.indexOf('style:') === 0 || p.ref.indexOf('aptx:') === 0));
|
|
815
|
+
});
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* What a re-rooting would do to the data on internal nodes, worked out on
|
|
820
|
+
* a bare copy so the tree itself is untouched: `annotated`, the internal
|
|
821
|
+
* nodes carrying data (nodeHasData), and `changed`, those among them whose
|
|
822
|
+
* clade -- the tips below -- the re-rooting changes. Those lie between
|
|
823
|
+
* the old root and the new one (an old two-child root disappears); every
|
|
824
|
+
* other node keeps its tips. Midpoint and MAD rooting find their root on
|
|
825
|
+
* the copy first. Nothing is copied when no internal node carries data.
|
|
826
|
+
*
|
|
827
|
+
* @param phy the tree
|
|
828
|
+
* @param method 'mad', 'midpoint', or 'node': the root on the branch above `node`
|
|
829
|
+
* @param node the node, for 'node'
|
|
830
|
+
* @returns {{annotated: Array, changed: Array}} nodes of `phy`
|
|
831
|
+
*/
|
|
832
|
+
forester.cladesChangedByRerooting = function (phy, method, node) {
|
|
833
|
+
let t = madTraversal(forester.getTreeRoot(phy));
|
|
834
|
+
let annotated = [];
|
|
835
|
+
for (let k = 0; k < t.pre.length; ++k) {
|
|
836
|
+
if (t.kids[k].length > 0 && forester.nodeHasData(t.pre[k])) {
|
|
837
|
+
annotated.push(t.pre[k]);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
if (annotated.length === 0) {
|
|
841
|
+
return {annotated: annotated, changed: []};
|
|
842
|
+
}
|
|
843
|
+
// the copy: shape and branch lengths only, each copy knowing its original
|
|
844
|
+
let copyOf = new Map();
|
|
845
|
+
let tipNumber = new Map();
|
|
846
|
+
for (let k = 0; k < t.pre.length; ++k) {
|
|
847
|
+
let c = {branch_length: t.pre[k].branch_length, original: t.pre[k]};
|
|
848
|
+
if (t.kids[k].length > 0) {
|
|
849
|
+
c.children = [];
|
|
850
|
+
} else {
|
|
851
|
+
tipNumber.set(t.pre[k], tipNumber.size);
|
|
852
|
+
}
|
|
853
|
+
if (k > 0) {
|
|
854
|
+
c.parent = copyOf.get(t.pre[t.parentOf[k]]);
|
|
855
|
+
c.parent.children.push(c);
|
|
856
|
+
}
|
|
857
|
+
copyOf.set(t.pre[k], c);
|
|
858
|
+
}
|
|
859
|
+
let copy = {children: [copyOf.get(t.pre[0])]};
|
|
860
|
+
copy.children[0].parent = copy;
|
|
861
|
+
if (method === 'mad') {
|
|
862
|
+
forester.madRoot(copy);
|
|
863
|
+
} else if (method === 'midpoint') {
|
|
864
|
+
forester.midpointRoot(copy);
|
|
865
|
+
} else {
|
|
866
|
+
forester.reRoot(copy, copyOf.get(node), -1);
|
|
867
|
+
}
|
|
868
|
+
// the tips below every node, before and after, as a count and two hashes
|
|
869
|
+
let hash = madTipHashes(tipNumber.size);
|
|
870
|
+
let cladeKeys = function (traversal, originalOf) {
|
|
871
|
+
let count = new Int32Array(traversal.pre.length);
|
|
872
|
+
let xa = new Int32Array(traversal.pre.length);
|
|
873
|
+
let xb = new Int32Array(traversal.pre.length);
|
|
874
|
+
let keys = new Map();
|
|
875
|
+
for (let q = 0; q < traversal.post.length; ++q) {
|
|
876
|
+
let k = traversal.post[q];
|
|
877
|
+
let ch = traversal.kids[k];
|
|
878
|
+
if (ch.length === 0) {
|
|
879
|
+
let i = tipNumber.get(originalOf(traversal.pre[k]));
|
|
880
|
+
count[k] = 1;
|
|
881
|
+
xa[k] = hash.xa[i + 1] ^ hash.xa[i];
|
|
882
|
+
xb[k] = hash.xb[i + 1] ^ hash.xb[i];
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
for (let c = 0; c < ch.length; ++c) {
|
|
886
|
+
count[k] += count[ch[c]];
|
|
887
|
+
xa[k] ^= xa[ch[c]];
|
|
888
|
+
xb[k] ^= xb[ch[c]];
|
|
889
|
+
}
|
|
890
|
+
let original = originalOf(traversal.pre[k]);
|
|
891
|
+
if (original) { // not the copy's new root node
|
|
892
|
+
keys.set(original, count[k] + ':' + xa[k] + ':' + xb[k]);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
return keys;
|
|
896
|
+
};
|
|
897
|
+
let before = cladeKeys(t, function (n) { return n; });
|
|
898
|
+
let after = cladeKeys(madTraversal(forester.getTreeRoot(copy)), function (c) { return c.original; });
|
|
899
|
+
return {
|
|
900
|
+
annotated: annotated,
|
|
901
|
+
changed: annotated.filter(function (n) { return after.get(n) !== before.get(n); })
|
|
902
|
+
};
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
function madLength(node) {
|
|
906
|
+
return node.branch_length > 0 ? node.branch_length : 0;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// The sum of squared cross-pair deviations between subtree c and its
|
|
910
|
+
// complement, with the root at distance x from c toward its parent. With
|
|
911
|
+
// K = 2*(x - depth[c]) a pair deviates by K/d + (2*depth[j]/d - 1), so the
|
|
912
|
+
// sum is K^2*b0 + 2*K*b1 + b2.
|
|
913
|
+
function madCross(depthC, b0, b1, b2, x) {
|
|
914
|
+
let k = 2.0 * (x - depthC);
|
|
915
|
+
return (k * k * b0) + (2.0 * k * b1) + b2;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Pre-order (the root at 0, parents before children) and post-order,
|
|
919
|
+
// children left to right, as node positions -- without recursion, since
|
|
920
|
+
// a caterpillar tree nests as deep as it has tips.
|
|
921
|
+
function madTraversal(root) {
|
|
922
|
+
let pre = [];
|
|
923
|
+
let parentOf = [];
|
|
924
|
+
let stack = [root];
|
|
925
|
+
let stackParent = [-1];
|
|
926
|
+
while (stack.length > 0) {
|
|
927
|
+
let node = stack.pop();
|
|
928
|
+
let p = stackParent.pop();
|
|
929
|
+
let k = pre.length;
|
|
930
|
+
pre.push(node);
|
|
931
|
+
parentOf.push(p);
|
|
932
|
+
if (node.children) {
|
|
933
|
+
for (let i = node.children.length - 1; i >= 0; --i) {
|
|
934
|
+
stack.push(node.children[i]);
|
|
935
|
+
stackParent.push(k);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
let kids = pre.map(function () {
|
|
940
|
+
return [];
|
|
941
|
+
});
|
|
942
|
+
for (let k = 1; k < pre.length; ++k) {
|
|
943
|
+
kids[parentOf[k]].push(k); // siblings come in left to right
|
|
944
|
+
}
|
|
945
|
+
// the mirrored pre-order, reversed, is the post-order left to right
|
|
946
|
+
let post = [];
|
|
947
|
+
let st = [0];
|
|
948
|
+
while (st.length > 0) {
|
|
949
|
+
let k = st.pop();
|
|
950
|
+
post.push(k);
|
|
951
|
+
for (let c = 0; c < kids[k].length; ++c) {
|
|
952
|
+
st.push(kids[k][c]);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
post.reverse();
|
|
956
|
+
return {pre: pre, parentOf: parentOf, kids: kids, post: post};
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// A set of tips as a key that does not depend on the rooting: its size
|
|
960
|
+
// and two 32-bit XOR hashes of its tips, taken on the side WITHOUT tip 0
|
|
961
|
+
// so both sides of a branch give the same key. xa/xb are prefix XORs, so
|
|
962
|
+
// the tips lo..hi-1 hash to xa[hi] ^ xa[lo].
|
|
963
|
+
function madTipHashes(n) {
|
|
964
|
+
let xa = new Int32Array(n + 1);
|
|
965
|
+
let xb = new Int32Array(n + 1);
|
|
966
|
+
let s = 0x2545f491;
|
|
967
|
+
let next = function () { // a fixed seed: the same tree always hashes the same
|
|
968
|
+
s = (s + 0x9e3779b9) | 0;
|
|
969
|
+
let z = s;
|
|
970
|
+
z = Math.imul(z ^ (z >>> 16), 0x85ebca6b);
|
|
971
|
+
z = Math.imul(z ^ (z >>> 13), 0xc2b2ae35);
|
|
972
|
+
return z ^ (z >>> 16);
|
|
973
|
+
};
|
|
974
|
+
for (let i = 0; i < n; ++i) {
|
|
975
|
+
xa[i + 1] = xa[i] ^ next();
|
|
976
|
+
xb[i + 1] = xb[i] ^ next();
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
xa: xa,
|
|
980
|
+
xb: xb,
|
|
981
|
+
key: function (size, ha, hb, holdsFirst) {
|
|
982
|
+
if (holdsFirst) {
|
|
983
|
+
size = n - size;
|
|
984
|
+
ha ^= xa[n];
|
|
985
|
+
hb ^= xb[n];
|
|
986
|
+
}
|
|
987
|
+
return size + ':' + ha + ':' + hb;
|
|
988
|
+
}
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
|
|
452
992
|
forester.getFurthestDescendant = function (node) {
|
|
453
993
|
let children = forester.getAllExternalNodes(node);
|
|
454
994
|
let farthest = null;
|
|
@@ -1691,6 +2231,8 @@
|
|
|
1691
2231
|
properties.longestNodeName = 0;
|
|
1692
2232
|
properties.branchLengths = false;
|
|
1693
2233
|
properties.confidences = false;
|
|
2234
|
+
// MAD values (madRoot) rate root positions, not clades: never support
|
|
2235
|
+
properties.madValues = false;
|
|
1694
2236
|
// the largest confidence value seen -- how a caller tells a
|
|
1695
2237
|
// posterior-probability tree (max <= 1) from a bootstrap tree
|
|
1696
2238
|
properties.maxConfidence = 0;
|
|
@@ -1803,8 +2345,12 @@
|
|
|
1803
2345
|
}
|
|
1804
2346
|
}
|
|
1805
2347
|
if (n.confidences && n.confidences.length > 0) {
|
|
1806
|
-
properties.confidences = true;
|
|
1807
2348
|
for (let ci = 0; ci < n.confidences.length; ++ci) {
|
|
2349
|
+
if (n.confidences[ci].type === forester.MAD_CONFIDENCE_TYPE) {
|
|
2350
|
+
properties.madValues = true;
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
properties.confidences = true;
|
|
1808
2354
|
let cv = n.confidences[ci].value;
|
|
1809
2355
|
if (typeof cv === 'number' && isFinite(cv) && cv > properties.maxConfidence) {
|
|
1810
2356
|
properties.maxConfidence = cv;
|
|
@@ -3470,11 +4016,17 @@
|
|
|
3470
4016
|
nh += ":" + node.branch_length;
|
|
3471
4017
|
}
|
|
3472
4018
|
}
|
|
3473
|
-
|
|
4019
|
+
// the support slot holds support: a MAD value (madRoot) never goes
|
|
4020
|
+
// there -- it would read as support, and would crowd out the
|
|
4021
|
+
// bootstrap on a branch carrying both. phyloXML keeps it, typed.
|
|
4022
|
+
let support = writeConfidences && node.confidences
|
|
4023
|
+
? node.confidences.filter(function (c) { return c.type !== forester.MAD_CONFIDENCE_TYPE; })
|
|
4024
|
+
: [];
|
|
4025
|
+
if (support.length === 1 && support[0].value !== undefined && support[0].value !== null) {
|
|
3474
4026
|
if (decPointsMax && decPointsMax > 0) {
|
|
3475
|
-
nh += "[" + forester.roundNumber(
|
|
4027
|
+
nh += "[" + forester.roundNumber(support[0].value, decPointsMax) + "]";
|
|
3476
4028
|
} else {
|
|
3477
|
-
nh += "[" +
|
|
4029
|
+
nh += "[" + support[0].value + "]";
|
|
3478
4030
|
}
|
|
3479
4031
|
}
|
|
3480
4032
|
if (!last) {
|
|
@@ -4258,7 +4810,8 @@
|
|
|
4258
4810
|
}, {suggest: false});
|
|
4259
4811
|
|
|
4260
4812
|
const SEARCH_BRANCH_LENGTH = numericField('Branch Length', n => (typeof n.branch_length === 'number') ? [n.branch_length] : []);
|
|
4261
|
-
|
|
4813
|
+
// support only: a MAD value rates a root position, not the clade
|
|
4814
|
+
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') : []);
|
|
4262
4815
|
const SEARCH_CLADE_SIZE = numericField('Clade Size (tips)', n => [n._srchClade], {metrics: true});
|
|
4263
4816
|
const SEARCH_CHILD_COUNT = numericField('Number of Children', n => [n.children ? n.children.length : 0]);
|
|
4264
4817
|
const SEARCH_DEPTH = numericField('Depth from Root', n => [n._srchDepth], {metrics: true});
|
|
@@ -4397,7 +4950,7 @@
|
|
|
4397
4950
|
if (!hasBL && typeof n.branch_length === 'number' && n.branch_length >= 0) hasBL = true;
|
|
4398
4951
|
if (!hasConf && n.confidences) {
|
|
4399
4952
|
for (let i = 0; i < n.confidences.length; ++i) {
|
|
4400
|
-
if (typeof n.confidences[i].value === 'number') { hasConf = true; break; }
|
|
4953
|
+
if (typeof n.confidences[i].value === 'number' && n.confidences[i].type !== forester.MAD_CONFIDENCE_TYPE) { hasConf = true; break; }
|
|
4401
4954
|
}
|
|
4402
4955
|
}
|
|
4403
4956
|
if (n.properties) {
|
|
@@ -4845,6 +5398,32 @@
|
|
|
4845
5398
|
'calendar year': 1, 'calendar years': 1
|
|
4846
5399
|
};
|
|
4847
5400
|
|
|
5401
|
+
/**
|
|
5402
|
+
* Whether the tree's branch lengths are time: most of its internal nodes
|
|
5403
|
+
* carry a date, and at least two do -- BEAST node heights, Nextstrain
|
|
5404
|
+
* dates, phyloXML <date>s on ancestors. Tip dates alone do not make one:
|
|
5405
|
+
* a divergence tree can carry collection dates, and re-rooting it (as a
|
|
5406
|
+
* root-to-tip regression does) is a normal step. A time tree is never
|
|
5407
|
+
* re-rooted: a new root would contradict the ancestors' dates. The
|
|
5408
|
+
* desktop's rule is the same (its detectTimeTree, internal-node half).
|
|
5409
|
+
*
|
|
5410
|
+
* @param phy the tree
|
|
5411
|
+
* @returns {boolean}
|
|
5412
|
+
*/
|
|
5413
|
+
forester.isTimeTree = function (phy) {
|
|
5414
|
+
let internal = 0;
|
|
5415
|
+
let dated = 0;
|
|
5416
|
+
forester.preOrderTraversalAll(forester.getTreeRoot(phy), function (n) {
|
|
5417
|
+
if (n.children && n.children.length > 0) {
|
|
5418
|
+
++internal;
|
|
5419
|
+
if (n.date && typeof n.date.value === 'number' && isFinite(n.date.value)) {
|
|
5420
|
+
++dated;
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
});
|
|
5424
|
+
return dated >= 2 && dated * 2 > internal;
|
|
5425
|
+
};
|
|
5426
|
+
|
|
4848
5427
|
// Everything the viewer needs to decide about and draw a time axis:
|
|
4849
5428
|
// {type: 'geologic'|'calendar'|null, rootAge, presentDate, dated,
|
|
4850
5429
|
// hasInternalIntervals, hasExternalIntervals}. rootAge (geologic) and
|
package/package.json
CHANGED