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/README.md +91 -9
- package/archaeopteryx.d.ts +3 -3
- package/archaeopteryx.js +501 -264
- package/forester.js +652 -9
- package/package.json +1 -1
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.5.0
|
|
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.0';
|
|
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
|
|
@@ -1147,12 +1150,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
1147
1150
|
// on the desktop: dots in the search colours, so hits outside the
|
|
1148
1151
|
// current viewport can be spotted and steered to. Search results
|
|
1149
1152
|
// re-run update(), which ends here, so the dots track every search.
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1153
|
+
// Displayed nodes only: a hit inside a collapsed clade has no drawn
|
|
1154
|
+
// position, so the clade gets the dot, in its wedge's mark colour.
|
|
1152
1155
|
let hits = [];
|
|
1153
1156
|
if (_root) {
|
|
1154
1157
|
forEachDisplayed(function (n) {
|
|
1155
|
-
let c = getFoundColor(n);
|
|
1158
|
+
let c = getFoundColor(n) || (isCollapsed(n) ? collapsedMarkColor(n) : null);
|
|
1156
1159
|
if (c) {
|
|
1157
1160
|
hits.push({node: n, color: c});
|
|
1158
1161
|
}
|
|
@@ -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
|
}
|
|
@@ -1997,6 +2010,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1997
2010
|
}
|
|
1998
2011
|
});
|
|
1999
2012
|
_vis.colorNext[vis.id] = next;
|
|
2013
|
+
vis.categoryOnScreen = new Set(vis.values); // what the scale may be asked about (visualizationColorFor)
|
|
2000
2014
|
vis.categoryScale = d3.scaleOrdinal()
|
|
2001
2015
|
.domain(vis.values)
|
|
2002
2016
|
.range(vis.values.map(function (v) {
|
|
@@ -2758,13 +2772,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
2758
2772
|
}
|
|
2759
2773
|
}
|
|
2760
2774
|
|
|
2761
|
-
// The dim gate: only while a hit is actually
|
|
2775
|
+
// The dim gate: only while a hit is actually SHOWN, so the tree never
|
|
2762
2776
|
// washes out with nothing emphasised (a 0-hit search, or every hit
|
|
2763
|
-
//
|
|
2777
|
+
// outside the displayed subtree). A collapsed clade holding a hit
|
|
2778
|
+
// counts as shown: it stays bright, outlined and counting its hits,
|
|
2779
|
+
// so the rest fading points at it (Christian, 2026-09-14: "less
|
|
2780
|
+
// confusing to users" than no dimming at all).
|
|
2764
2781
|
_dimNonMatches = false;
|
|
2765
2782
|
if ((_foundNodes0 && _foundNodes0.size > 0) || (_foundNodes1 && _foundNodes1.size > 0)) {
|
|
2766
2783
|
for (let i = 0, len = nodes.length; i !== len; ++i) {
|
|
2767
|
-
if (isNodeFound(nodes[i])) {
|
|
2784
|
+
if (isNodeFound(nodes[i]) || (isCollapsed(nodes[i]) && collapsedFoundCounts(nodes[i]).found > 0)) {
|
|
2768
2785
|
_dimNonMatches = true;
|
|
2769
2786
|
break;
|
|
2770
2787
|
}
|
|
@@ -2782,10 +2799,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
2782
2799
|
placeRootStub();
|
|
2783
2800
|
|
|
2784
2801
|
if (_state.circularDisplay) {
|
|
2802
|
+
// the outermost drawn point sets the scale: a node, or the far
|
|
2803
|
+
// edge of a collapsed clade's wedge -- which reaches the clade's
|
|
2804
|
+
// farthest tip, hidden from nodes but not from the drawing, and
|
|
2805
|
+
// would otherwise poke through the ring the labels stand on
|
|
2785
2806
|
let maxY = 0;
|
|
2786
2807
|
for (let i = 0; i < nodes.length; ++i) {
|
|
2787
|
-
|
|
2788
|
-
|
|
2808
|
+
let y = isCollapsed(nodes[i]) ? Math.max(nodes[i].y, collapsedReach(nodes[i])[1]) : nodes[i].y;
|
|
2809
|
+
if (y > maxY) {
|
|
2810
|
+
maxY = y;
|
|
2789
2811
|
}
|
|
2790
2812
|
}
|
|
2791
2813
|
_radial = {
|
|
@@ -3153,9 +3175,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
3153
3175
|
// Dim Non-Matches: one opacity on the node GROUP dims its labels, dot,
|
|
3154
3176
|
// shape and branch-data numbers together; the branch lines are separate
|
|
3155
3177
|
// path.link elements and keep their full colour, as on the desktop.
|
|
3156
|
-
// Hits and selected nodes (getFoundColor) are never dimmed
|
|
3178
|
+
// Hits and selected nodes (getFoundColor) are never dimmed -- and
|
|
3179
|
+
// neither is a collapsed clade holding one: its wedge and label live in
|
|
3180
|
+
// the clade node's group, and the clade node itself is never a hit.
|
|
3157
3181
|
node.style('opacity', function (d) {
|
|
3158
|
-
|
|
3182
|
+
if (!_dimNonMatches || getFoundColor(d) || (isCollapsed(d) && collapsedHoldsHighlight(d))) {
|
|
3183
|
+
return null;
|
|
3184
|
+
}
|
|
3185
|
+
return DIM_NON_MATCH_OPACITY;
|
|
3159
3186
|
});
|
|
3160
3187
|
|
|
3161
3188
|
let nodeUpdate = animateOrSet(node, transitionDuration)
|
|
@@ -3186,7 +3213,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3186
3213
|
if (_state.showBranchLengthValues) {
|
|
3187
3214
|
nodeChild('text.bllabel').text(makeBranchLengthLabel);
|
|
3188
3215
|
}
|
|
3189
|
-
if (_state.showConfidenceValues) {
|
|
3216
|
+
if (_state.showConfidenceValues || _state.showMadValues) {
|
|
3190
3217
|
nodeChild('text.conflabel').text(makeConfidenceValuesLabel);
|
|
3191
3218
|
}
|
|
3192
3219
|
if (_state.showBranchEvents) {
|
|
@@ -3303,43 +3330,75 @@ function (root, d3, forester, phyloXml) {
|
|
|
3303
3330
|
// _svgGroup, so that name put them in the way of selectAll('path.link')
|
|
3304
3331
|
// -- the main link data-join, and the overview's miniature.
|
|
3305
3332
|
_svgGroup.selectAll('g.aptx-align-ext').remove();
|
|
3306
|
-
|
|
3307
|
-
|
|
3333
|
+
let alignedRect = !radialDisplay() && _state.phylogram && _state.alignPhylogram;
|
|
3334
|
+
let tipGuides = alignedRect && _state.showExternalLabels
|
|
3335
|
+
&& (_state.showNodeName || _state.showTaxonomy || _state.showSequence);
|
|
3336
|
+
// a collapsed clade's label always shows, so its guide does too: from
|
|
3337
|
+
// past the wedge's farthest tip to the label column
|
|
3338
|
+
let collapsedGuides = alignedRect ? nodes.filter(function (d) {
|
|
3339
|
+
return isCollapsed(d) && !(_state.dynahide && d.hide);
|
|
3340
|
+
}) : [];
|
|
3341
|
+
if (tipGuides || collapsedGuides.length > 0) {
|
|
3308
3342
|
let ext = _svgGroup.insert('g', 'g').attr('class', 'aptx-align-ext');
|
|
3309
|
-
|
|
3310
|
-
.
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
.
|
|
3317
|
-
|
|
3343
|
+
let guideStyle = function (sel) {
|
|
3344
|
+
return sel.attr('fill', 'none')
|
|
3345
|
+
.attr('stroke-width', 1)
|
|
3346
|
+
.attr('stroke', _state.branchColorDefault)
|
|
3347
|
+
.style('stroke-opacity', 0.25);
|
|
3348
|
+
};
|
|
3349
|
+
if (tipGuides) {
|
|
3350
|
+
guideStyle(ext.selectAll('path.aptx-tip-guide')
|
|
3351
|
+
.data(links.filter(function (d) {
|
|
3352
|
+
return (!d.target.children && !(_state.dynahide && d.target.hide));
|
|
3353
|
+
}))
|
|
3354
|
+
.enter().append('path').attr('class', 'aptx-tip-guide'))
|
|
3355
|
+
.attr('d', function (d) {
|
|
3356
|
+
return connection(d.target);
|
|
3357
|
+
});
|
|
3358
|
+
}
|
|
3359
|
+
guideStyle(ext.selectAll('path.aptx-collapsed-guide')
|
|
3360
|
+
.data(collapsedGuides)
|
|
3361
|
+
.enter().append('path').attr('class', 'aptx-collapsed-guide'))
|
|
3318
3362
|
.attr('d', function (d) {
|
|
3319
|
-
|
|
3363
|
+
let x1 = collapsedReach(d)[1] + 5;
|
|
3364
|
+
return (_w - x1) > 5 ? 'M' + x1 + ',' + d.x + 'L' + _w + ',' + d.x : null;
|
|
3320
3365
|
});
|
|
3321
3366
|
}
|
|
3322
3367
|
|
|
3323
3368
|
// circular: a thin dashed connector from each external node out to the
|
|
3324
3369
|
// common label ring (so labels line up like iTOL's aligned display).
|
|
3325
3370
|
_svgGroup.selectAll('g.aptx-radial-conn').remove();
|
|
3326
|
-
if (_state.circularDisplay
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
.
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
.
|
|
3371
|
+
if (_state.circularDisplay) {
|
|
3372
|
+
// tips while their labels show; a collapsed clade always (its label
|
|
3373
|
+
// always shows), from its wedge's farthest reach out to the ring
|
|
3374
|
+
let connected = nodes.filter(function (d) {
|
|
3375
|
+
if (_state.dynahide && d.hide) {
|
|
3376
|
+
return false;
|
|
3377
|
+
}
|
|
3378
|
+
if (isCollapsed(d)) {
|
|
3379
|
+
return _radial.maxRad - radialRadius(collapsedReach(d)[1]) > 2;
|
|
3380
|
+
}
|
|
3381
|
+
return !d.children && _state.showExternalLabels;
|
|
3382
|
+
});
|
|
3383
|
+
let innerY = function (d) {
|
|
3384
|
+
return isCollapsed(d) ? collapsedReach(d)[1] : d.y;
|
|
3385
|
+
};
|
|
3386
|
+
if (connected.length > 0) {
|
|
3387
|
+
let conn = _svgGroup.insert('g', 'g').attr('class', 'aptx-radial-conn');
|
|
3388
|
+
conn.selectAll('line')
|
|
3389
|
+
.data(connected)
|
|
3390
|
+
.enter().append('line')
|
|
3391
|
+
.attr('stroke', _state.branchColorDefault)
|
|
3392
|
+
// width/opacity match the rectangular aligned extensions; the
|
|
3393
|
+
// dash alone marks these as connectors (0.5px at 0.3 vanished)
|
|
3394
|
+
.attr('stroke-width', 1)
|
|
3395
|
+
.style('stroke-opacity', 0.4)
|
|
3396
|
+
.style('stroke-dasharray', '2,3')
|
|
3397
|
+
.attr('x1', function (d) { return radialXY(d.x, innerY(d))[0]; })
|
|
3398
|
+
.attr('y1', function (d) { return radialXY(d.x, innerY(d))[1]; })
|
|
3399
|
+
.attr('x2', function (d) { return polarXY(radialAngle(d.x), _radial.maxRad)[0]; })
|
|
3400
|
+
.attr('y2', function (d) { return polarXY(radialAngle(d.x), _radial.maxRad)[1]; });
|
|
3401
|
+
}
|
|
3343
3402
|
}
|
|
3344
3403
|
|
|
3345
3404
|
for (let i = 0, len = nodes.length; i !== len; ++i) {
|
|
@@ -3553,6 +3612,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
3553
3612
|
if (colorModeOf(vis) === 'range') {
|
|
3554
3613
|
return vis.rangeScale ? vis.rangeScale(Number(value)) : null;
|
|
3555
3614
|
}
|
|
3615
|
+
// A value only tips hidden in a collapsed clade carry is not on
|
|
3616
|
+
// screen, so not in the scale's domain -- and a d3 ordinal scale asked
|
|
3617
|
+
// about an unknown value extends its domain and hands out an on-screen
|
|
3618
|
+
// value's colour. Such a value keeps its remembered colour instead
|
|
3619
|
+
// (assigned at launch over the whole tree, or the user's override):
|
|
3620
|
+
// the clade's wedge keeps the colour its tips wore. Christian,
|
|
3621
|
+
// 2026-09-14; reported by the desktop session, which follows.
|
|
3622
|
+
if (vis.categoryOnScreen && !vis.categoryOnScreen.has(value)) {
|
|
3623
|
+
let mem = _vis.colorMemory[vis.id];
|
|
3624
|
+
return (mem && mem[visMemoryKey(vis, value)]) || null;
|
|
3625
|
+
}
|
|
3556
3626
|
return vis.categoryScale(value);
|
|
3557
3627
|
}
|
|
3558
3628
|
|
|
@@ -3593,11 +3663,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
3593
3663
|
if (_root && ((_foundNodes0 && _foundNodes0.size > 0) || (_foundNodes1 && _foundNodes1.size > 0))) {
|
|
3594
3664
|
// natural (top-to-bottom) drawing order -- forester's preorder
|
|
3595
3665
|
// walks children in reverse, which made the NEXT arrow step upward
|
|
3666
|
+
// a collapsed clade holding hits is ONE stop, on its node: the hits
|
|
3667
|
+
// inside have no position of their own to step to
|
|
3596
3668
|
let walk = function (n) {
|
|
3597
|
-
if (isNodeFound(n)) {
|
|
3669
|
+
if (isNodeFound(n) || (isCollapsed(n) && collapsedFoundCounts(n).found > 0)) {
|
|
3598
3670
|
hits.push(n);
|
|
3599
3671
|
}
|
|
3600
|
-
if (n.children && !isCollapsed(n)) {
|
|
3672
|
+
if (n.children && !isCollapsed(n)) {
|
|
3601
3673
|
for (let i = 0; i < n.children.length; ++i) {
|
|
3602
3674
|
walk(n.children[i]);
|
|
3603
3675
|
}
|
|
@@ -3752,7 +3824,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3752
3824
|
|
|
3753
3825
|
function syncOptionalNodeChildren(node) {
|
|
3754
3826
|
let wantBl = _state.showBranchLengthValues === true;
|
|
3755
|
-
let wantConf = _state.showConfidenceValues === true;
|
|
3827
|
+
let wantConf = _state.showConfidenceValues === true || _state.showMadValues === true;
|
|
3756
3828
|
let wantEvent = _state.showBranchEvents === true;
|
|
3757
3829
|
let wantDot = _state.showSupportDots === true;
|
|
3758
3830
|
|
|
@@ -4021,39 +4093,32 @@ function (root, d3, forester, phyloXml) {
|
|
|
4021
4093
|
}
|
|
4022
4094
|
};
|
|
4023
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.
|
|
4024
4100
|
let makeConfidenceValuesLabel = function (phynode) {
|
|
4025
|
-
if (phynode.confidences
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
}
|
|
4036
|
-
if (!show) {
|
|
4037
|
-
return;
|
|
4038
|
-
}
|
|
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));
|
|
4039
4111
|
}
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
for (let ii = 0; ii < cl; ++ii) {
|
|
4047
|
-
if (c[ii].value) {
|
|
4048
|
-
if (ii > 0) {
|
|
4049
|
-
s += "/";
|
|
4050
|
-
}
|
|
4051
|
-
s += +c[ii].value.toFixed(CONFIDENCE_VALUE_DIGITS_DEFAULT);
|
|
4052
|
-
}
|
|
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));
|
|
4053
4118
|
}
|
|
4054
|
-
|
|
4055
|
-
}
|
|
4119
|
+
});
|
|
4056
4120
|
}
|
|
4121
|
+
return parts.length > 0 ? parts.join('/') : undefined;
|
|
4057
4122
|
};
|
|
4058
4123
|
|
|
4059
4124
|
let makeBranchEventsLabel = function (phynode) {
|
|
@@ -4114,12 +4179,23 @@ function (root, d3, forester, phyloXml) {
|
|
|
4114
4179
|
return (branchWidth + SUPPORT_DOT_EXTRA_DIAMETER) / 2;
|
|
4115
4180
|
}
|
|
4116
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
|
+
|
|
4117
4193
|
function showSupportDot(d) {
|
|
4118
4194
|
if (!_state.showSupportDots || !d.parent
|
|
4119
4195
|
|| !d.confidences || d.confidences.length === 0) {
|
|
4120
4196
|
return false;
|
|
4121
4197
|
}
|
|
4122
|
-
let v = d
|
|
4198
|
+
let v = firstSupportValue(d);
|
|
4123
4199
|
if (typeof v !== 'number' || !isFinite(v) || v < supportDotThreshold()) {
|
|
4124
4200
|
return false;
|
|
4125
4201
|
}
|
|
@@ -4704,6 +4780,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4704
4780
|
// elements by forester.timeAxisInfo -- again unless the caller set
|
|
4705
4781
|
// showTimeAxis explicitly.
|
|
4706
4782
|
_timeInfo = _treeData ? forester.timeAxisInfo(forester.getTreeRoot(_treeData)) : null;
|
|
4783
|
+
_timeTree = _treeData ? forester.isTimeTree(_treeData) : false;
|
|
4707
4784
|
if (_state.showTimeAxis === undefined) {
|
|
4708
4785
|
_state.showTimeAxis = !!(_timeInfo && _timeInfo.type);
|
|
4709
4786
|
}
|
|
@@ -5412,6 +5489,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
5412
5489
|
if (index === _treeIndex) {
|
|
5413
5490
|
return makeViewerHandle();
|
|
5414
5491
|
}
|
|
5492
|
+
// collapsing is part of the view, and a switched-to tree opens fresh:
|
|
5493
|
+
// nothing collapsed, now or when you come back (Christian, 2026-09-14)
|
|
5494
|
+
clearCollapsedFlags(_trees[_treeIndex]);
|
|
5495
|
+
clearCollapsedFlags(_trees[index]);
|
|
5415
5496
|
return launchInto(_container, _trees, index, _launchConfig);
|
|
5416
5497
|
}
|
|
5417
5498
|
|
|
@@ -5571,134 +5652,21 @@ function (root, d3, forester, phyloXml) {
|
|
|
5571
5652
|
update();
|
|
5572
5653
|
}
|
|
5573
5654
|
|
|
5655
|
+
// The tips under the node as a tab-separated table with a header,
|
|
5656
|
+
// the desktop's columns (forester.externalNodeDataTable), top to
|
|
5657
|
+
// bottom as drawn; it reads back through a metadata-table join.
|
|
5574
5658
|
function downloadExternalNodeDataAll(node) {
|
|
5575
|
-
|
|
5576
|
-
let addSep = function (t) {
|
|
5577
|
-
if (t.length > 0) {
|
|
5578
|
-
t += '\t';
|
|
5579
|
-
}
|
|
5580
|
-
return t;
|
|
5581
|
-
};
|
|
5582
|
-
|
|
5583
|
-
let addSepSame = function (t) {
|
|
5584
|
-
if (t.length > 0) {
|
|
5585
|
-
t += ', ';
|
|
5586
|
-
}
|
|
5587
|
-
return t;
|
|
5588
|
-
};
|
|
5589
|
-
let text_all = '';
|
|
5590
|
-
|
|
5591
5659
|
const ext_nodes = forester.getAllExternalNodes(node).reverse();
|
|
5592
|
-
|
|
5593
5660
|
let filename;
|
|
5594
5661
|
if (ext_nodes.length === 1 && ext_nodes[0].name) {
|
|
5595
|
-
filename = 'External_Node_Data_for_Node_' + ext_nodes[0].name.replace(/\W/g, '_') + '.
|
|
5662
|
+
filename = 'External_Node_Data_for_Node_' + ext_nodes[0].name.replace(/\W/g, '_') + '.tsv';
|
|
5596
5663
|
} else {
|
|
5597
|
-
filename = 'External_Node_Data_for_' + ext_nodes.length + '_Nodes.
|
|
5598
|
-
}
|
|
5599
|
-
|
|
5600
|
-
for (let j = 0, l = ext_nodes.length; j < l; ++j) {
|
|
5601
|
-
let text = '';
|
|
5602
|
-
let n = ext_nodes[j];
|
|
5603
|
-
if (n.name) {
|
|
5604
|
-
text += n.name
|
|
5605
|
-
}
|
|
5606
|
-
|
|
5607
|
-
if (n.properties && (n.properties.length > 0)) {
|
|
5608
|
-
const sorted_properties = n.properties.concat().sort();
|
|
5609
|
-
const l = sorted_properties.length;
|
|
5610
|
-
let properties_text = '';
|
|
5611
|
-
let prev_property_ref = null;
|
|
5612
|
-
for (let pl = 0; pl < l; ++pl) {
|
|
5613
|
-
if (forester.isNodeScopedProperty(sorted_properties[pl])) {
|
|
5614
|
-
if (sorted_properties[pl].ref === prev_property_ref) {
|
|
5615
|
-
properties_text = addSepSame(properties_text);
|
|
5616
|
-
} else {
|
|
5617
|
-
prev_property_ref = sorted_properties[pl].ref;
|
|
5618
|
-
properties_text = addSep(properties_text);
|
|
5619
|
-
}
|
|
5620
|
-
properties_text += sorted_properties[pl].value;
|
|
5621
|
-
}
|
|
5622
|
-
}
|
|
5623
|
-
if (properties_text.length > 0) {
|
|
5624
|
-
text = addSep(text);
|
|
5625
|
-
text += properties_text;
|
|
5626
|
-
}
|
|
5627
|
-
}
|
|
5628
|
-
|
|
5629
|
-
if (n.taxonomies) {
|
|
5630
|
-
let tax_text = '';
|
|
5631
|
-
for (let i = 0; i < n.taxonomies.length; ++i) {
|
|
5632
|
-
let t = n.taxonomies[i];
|
|
5633
|
-
if (t.id) {
|
|
5634
|
-
if (t.id.provider) {
|
|
5635
|
-
tax_text = addSep(tax_text);
|
|
5636
|
-
tax_text += '[' + t.id.provider + ']:' + t.id.value;
|
|
5637
|
-
} else {
|
|
5638
|
-
tax_text = addSep(tax_text);
|
|
5639
|
-
tax_text += t.id.value;
|
|
5640
|
-
}
|
|
5641
|
-
}
|
|
5642
|
-
if (t.code) {
|
|
5643
|
-
tax_text = addSep(tax_text);
|
|
5644
|
-
tax_text += t.code;
|
|
5645
|
-
}
|
|
5646
|
-
if (t.scientific_name) {
|
|
5647
|
-
tax_text = addSep(tax_text);
|
|
5648
|
-
tax_text += t.scientific_name;
|
|
5649
|
-
}
|
|
5650
|
-
if (t.common_name) {
|
|
5651
|
-
tax_text = addSep(tax_text);
|
|
5652
|
-
tax_text += t.common_name;
|
|
5653
|
-
}
|
|
5654
|
-
if (t.rank) {
|
|
5655
|
-
tax_text = addSep(tax_text);
|
|
5656
|
-
tax_text += t.rank;
|
|
5657
|
-
}
|
|
5658
|
-
}
|
|
5659
|
-
text = addSep(text);
|
|
5660
|
-
text += tax_text;
|
|
5661
|
-
}
|
|
5662
|
-
if (n.sequences) {
|
|
5663
|
-
let seq_text = '';
|
|
5664
|
-
for (let i = 0; i < n.sequences.length; ++i) {
|
|
5665
|
-
let s = n.sequences[i];
|
|
5666
|
-
if (s.accession) {
|
|
5667
|
-
if (s.accession.source) {
|
|
5668
|
-
seq_text = addSep(seq_text);
|
|
5669
|
-
seq_text += '[' + s.accession.source + ']:' + s.accession.value;
|
|
5670
|
-
} else {
|
|
5671
|
-
seq_text = addSep(seq_text);
|
|
5672
|
-
seq_text += s.accession.value;
|
|
5673
|
-
}
|
|
5674
|
-
}
|
|
5675
|
-
if (s.symbol) {
|
|
5676
|
-
seq_text = addSep(seq_text);
|
|
5677
|
-
seq_text += s.symbol;
|
|
5678
|
-
}
|
|
5679
|
-
if (s.name) {
|
|
5680
|
-
seq_text = addSep(seq_text);
|
|
5681
|
-
seq_text += s.name;
|
|
5682
|
-
}
|
|
5683
|
-
if (s.gene_name) {
|
|
5684
|
-
seq_text = addSep(seq_text);
|
|
5685
|
-
seq_text += s.gene_name;
|
|
5686
|
-
}
|
|
5687
|
-
if (s.location) {
|
|
5688
|
-
seq_text = addSep(seq_text);
|
|
5689
|
-
seq_text += s.location;
|
|
5690
|
-
}
|
|
5691
|
-
}
|
|
5692
|
-
text = addSep(text);
|
|
5693
|
-
text += seq_text;
|
|
5694
|
-
}
|
|
5695
|
-
if (text.length > 0) {
|
|
5696
|
-
text_all += text + '\n';
|
|
5697
|
-
}
|
|
5664
|
+
filename = 'External_Node_Data_for_' + ext_nodes.length + '_Nodes.tsv';
|
|
5698
5665
|
}
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5666
|
+
const tsv = forester.externalNodeDataTsv(ext_nodes, function (n, i) {
|
|
5667
|
+
return (n.viewId !== undefined) ? n.viewId : i + 1;
|
|
5668
|
+
});
|
|
5669
|
+
saveAs(new Blob([tsv], {type: 'text/tab-separated-values'}), filename);
|
|
5702
5670
|
update();
|
|
5703
5671
|
}
|
|
5704
5672
|
|
|
@@ -5927,9 +5895,25 @@ function (root, d3, forester, phyloXml) {
|
|
|
5927
5895
|
update(null, 0);
|
|
5928
5896
|
}});
|
|
5929
5897
|
}
|
|
5930
|
-
if (!_in_subtree && d.parent && d.parent.parent
|
|
5931
|
-
|
|
5932
|
-
|
|
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
|
+
}
|
|
5933
5917
|
}
|
|
5934
5918
|
if (_settings.enableManualNodeSelection) {
|
|
5935
5919
|
items.push({label: 'Select/Deselect Node', action: function () { selectDeselectNode(d); }});
|
|
@@ -6848,6 +6832,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6848
6832
|
}
|
|
6849
6833
|
d.collapsed = !d.collapsed;
|
|
6850
6834
|
calcMaxExtLabel();
|
|
6835
|
+
refreshVisualizations(false); // the legends describe the tips on screen
|
|
6851
6836
|
scheduleUpdate(null, 0);
|
|
6852
6837
|
}
|
|
6853
6838
|
|
|
@@ -6856,6 +6841,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6856
6841
|
n.collapsed = false;
|
|
6857
6842
|
});
|
|
6858
6843
|
calcMaxExtLabel();
|
|
6844
|
+
refreshVisualizations(false);
|
|
6859
6845
|
scheduleUpdate(null, 0);
|
|
6860
6846
|
}
|
|
6861
6847
|
|
|
@@ -6871,6 +6857,47 @@ function (root, d3, forester, phyloXml) {
|
|
|
6871
6857
|
});
|
|
6872
6858
|
}
|
|
6873
6859
|
|
|
6860
|
+
// Runs a re-rooting without turning a collapsed clade inside out. A clade
|
|
6861
|
+
// on the path to the new root has its parent turned into a child, so its
|
|
6862
|
+
// flag would go on hiding a different set of tips -- with a midpoint
|
|
6863
|
+
// inside a collapsed clade, the whole rest of the tree. Such a clade
|
|
6864
|
+
// opens; every other clade keeps its tips and stays collapsed.
|
|
6865
|
+
function rerootKeepingCollapse(reroot) {
|
|
6866
|
+
let flagged = [];
|
|
6867
|
+
forester.preOrderTraversalAll(_root_const || _root, function (n) {
|
|
6868
|
+
if (n.collapsed === true && n.children) {
|
|
6869
|
+
flagged.push({node: n, tips: forester.getAllExternalNodes(n)});
|
|
6870
|
+
}
|
|
6871
|
+
});
|
|
6872
|
+
reroot();
|
|
6873
|
+
let opened = false;
|
|
6874
|
+
flagged.forEach(function (f) {
|
|
6875
|
+
let now = f.node.children ? forester.getAllExternalNodes(f.node) : [];
|
|
6876
|
+
let kept = new Set(f.tips);
|
|
6877
|
+
if (now.length !== f.tips.length || !now.every(function (t) { return kept.has(t); })) {
|
|
6878
|
+
f.node.collapsed = false;
|
|
6879
|
+
opened = true;
|
|
6880
|
+
}
|
|
6881
|
+
});
|
|
6882
|
+
if (opened) {
|
|
6883
|
+
calcMaxExtLabel();
|
|
6884
|
+
refreshVisualizations(false);
|
|
6885
|
+
}
|
|
6886
|
+
}
|
|
6887
|
+
|
|
6888
|
+
// What a collapsed clade is marked with for the tips it hides: the colour
|
|
6889
|
+
// of its first search hit, else the selection colour when a tip is
|
|
6890
|
+
// selected, else null. The wedge, the overview dot and the dimming all
|
|
6891
|
+
// read it, so they always agree.
|
|
6892
|
+
function collapsedMarkColor(d) {
|
|
6893
|
+
let tips = forester.getAllExternalNodes(d);
|
|
6894
|
+
let hit = tips.filter(isNodeFound)[0];
|
|
6895
|
+
if (hit) {
|
|
6896
|
+
return getFoundColor(hit);
|
|
6897
|
+
}
|
|
6898
|
+
return tips.some(function (t) { return _selectedNodes.has(t); }) ? _state.selectedColorDefault : null;
|
|
6899
|
+
}
|
|
6900
|
+
|
|
6874
6901
|
// How tall a collapsed clade draws, in rows: 1 for a pair, growing with
|
|
6875
6902
|
// the logarithm of the tip count, capped so a huge clade does not eat
|
|
6876
6903
|
// the screen. The cluster layout's separation turns it into space.
|
|
@@ -6912,6 +6939,24 @@ function (root, d3, forester, phyloXml) {
|
|
|
6912
6939
|
return prefix.length >= 2 ? prefix : '';
|
|
6913
6940
|
}
|
|
6914
6941
|
|
|
6942
|
+
// Whether any tip hidden in a collapsed clade is a hit or selected -- the
|
|
6943
|
+
// clade then stays undimmed, as that tip would be if it were drawn.
|
|
6944
|
+
function collapsedHoldsHighlight(d) {
|
|
6945
|
+
return collapsedMarkColor(d) !== null;
|
|
6946
|
+
}
|
|
6947
|
+
|
|
6948
|
+
// Whether every tip a collapsed clade hides is marked: all of them search
|
|
6949
|
+
// hits when the clade holds any hit, else all of them selected. Such a
|
|
6950
|
+
// wedge is filled in the mark colour and its label set in it, bold.
|
|
6951
|
+
function collapsedFullyMarked(d) {
|
|
6952
|
+
let hits = collapsedFoundCounts(d);
|
|
6953
|
+
if (hits.found > 0) {
|
|
6954
|
+
return hits.found === hits.total;
|
|
6955
|
+
}
|
|
6956
|
+
let tips = forester.getAllExternalNodes(d);
|
|
6957
|
+
return tips.length > 0 && tips.every(function (t) { return _selectedNodes.has(t); });
|
|
6958
|
+
}
|
|
6959
|
+
|
|
6915
6960
|
function collapsedFoundCounts(d) {
|
|
6916
6961
|
let found = 0;
|
|
6917
6962
|
let total = 0;
|
|
@@ -6959,26 +7004,28 @@ function (root, d3, forester, phyloXml) {
|
|
|
6959
7004
|
return best || _state.branchColorDefault;
|
|
6960
7005
|
}
|
|
6961
7006
|
|
|
6962
|
-
// The wedge's
|
|
6963
|
-
//
|
|
6964
|
-
//
|
|
6965
|
-
//
|
|
6966
|
-
//
|
|
6967
|
-
|
|
7007
|
+
// The wedge's reach: the clade's nearest and farthest tips, in the
|
|
7008
|
+
// layout's x. One edge runs to the nearest tip and the other to the
|
|
7009
|
+
// farthest, so the shape itself shows how uneven the clade's branch
|
|
7010
|
+
// lengths are (as iTOL draws it). Christian chose this over the
|
|
7011
|
+
// desktop's symmetric triangle on an average base (2026-09-14). A
|
|
7012
|
+
// phylogram measures the tips; a cladogram, where every leaf sits on
|
|
7013
|
+
// the tip column, gives the wedge one depth step.
|
|
7014
|
+
function collapsedReach(d) {
|
|
6968
7015
|
if (_state.phylogram && _yScale) {
|
|
6969
|
-
let
|
|
6970
|
-
let
|
|
7016
|
+
let min = Infinity;
|
|
7017
|
+
let max = -Infinity;
|
|
6971
7018
|
forester.preOrderTraversal(d, function (t) {
|
|
6972
7019
|
if (!t.children && typeof t.distToRoot === 'number') {
|
|
6973
|
-
|
|
6974
|
-
|
|
7020
|
+
if (t.distToRoot < min) { min = t.distToRoot; }
|
|
7021
|
+
if (t.distToRoot > max) { max = t.distToRoot; }
|
|
6975
7022
|
}
|
|
6976
7023
|
});
|
|
6977
|
-
if (
|
|
6978
|
-
return Math.max(d.y + 2, _yScale(
|
|
7024
|
+
if (isFinite(min)) {
|
|
7025
|
+
return [Math.max(d.y + 2, _yScale(min)), Math.max(d.y + 2, _yScale(max))];
|
|
6979
7026
|
}
|
|
6980
7027
|
}
|
|
6981
|
-
return d.y + _cladogramUnit;
|
|
7028
|
+
return [d.y + _cladogramUnit, d.y + _cladogramUnit];
|
|
6982
7029
|
}
|
|
6983
7030
|
|
|
6984
7031
|
let _cladogramUnit = 0; // one depth step of the last cladogram layout, px
|
|
@@ -7001,52 +7048,71 @@ function (root, d3, forester, phyloXml) {
|
|
|
7001
7048
|
let fs = _state.externalNodeFontSize;
|
|
7002
7049
|
wedge.each(function (d) {
|
|
7003
7050
|
let color = collapsedColor(d);
|
|
7004
|
-
let
|
|
7005
|
-
let
|
|
7051
|
+
let mark = collapsedMarkColor(d);
|
|
7052
|
+
let full = collapsedFullyMarked(d);
|
|
7006
7053
|
let h = Math.max(6, collapsedRows(d) * _rowUnit * 0.82);
|
|
7007
|
-
let
|
|
7054
|
+
let reach = collapsedReach(d);
|
|
7008
7055
|
let path;
|
|
7009
7056
|
if (radialDisplay()) {
|
|
7010
7057
|
// in the rotated frame: +x along the spoke from the node (the
|
|
7011
|
-
// group's origin, already at the node's radius)
|
|
7012
|
-
//
|
|
7013
|
-
//
|
|
7014
|
-
let
|
|
7015
|
-
|
|
7058
|
+
// group's origin, already at the node's radius) -- distances,
|
|
7059
|
+
// never absolute radii, or the wedge lands a whole radius
|
|
7060
|
+
// further out than its label
|
|
7061
|
+
let r0 = radialRadius(d.y);
|
|
7062
|
+
let dr1 = radialRadius(reach[0]) - r0;
|
|
7063
|
+
let dr2 = radialRadius(reach[1]) - r0;
|
|
7064
|
+
path = 'M0,0 L' + dr1 + ',' + (-h / 2) + ' L' + dr2 + ',' + (h / 2) + ' Z';
|
|
7016
7065
|
} else {
|
|
7017
|
-
let
|
|
7018
|
-
|
|
7066
|
+
let dx1 = reach[0] - d.y;
|
|
7067
|
+
let dx2 = reach[1] - d.y;
|
|
7068
|
+
path = 'M0,0 L' + dx1 + ',' + (-h / 2) + ' L' + dx2 + ',' + (h / 2) + ' Z';
|
|
7019
7069
|
}
|
|
7020
7070
|
d3.select(this)
|
|
7021
7071
|
.attr('d', path)
|
|
7022
7072
|
.attr('transform', radialDisplay() ? 'rotate(' + labelAngleDeg(d) + ')' : null)
|
|
7023
|
-
.style('fill',
|
|
7024
|
-
.style('fill-opacity',
|
|
7025
|
-
.style('stroke',
|
|
7026
|
-
.style('stroke-width',
|
|
7073
|
+
.style('fill', full ? mark : color)
|
|
7074
|
+
.style('fill-opacity', full ? 0.45 : 0.22)
|
|
7075
|
+
.style('stroke', mark || color)
|
|
7076
|
+
.style('stroke-width', mark ? 1.5 : 1)
|
|
7027
7077
|
.style('stroke-opacity', 0.9)
|
|
7028
7078
|
.style('stroke-linejoin', 'round');
|
|
7029
7079
|
});
|
|
7030
7080
|
label.each(function (d) {
|
|
7031
|
-
let
|
|
7032
|
-
let
|
|
7033
|
-
let
|
|
7034
|
-
let ink = allFound ? getFoundColor(forester.getAllExternalNodes(d).filter(isNodeFound)[0]) : _state.labelColorDefault;
|
|
7081
|
+
let far = collapsedReach(d)[1]; // the label clears the farthest tip
|
|
7082
|
+
let allFound = collapsedFullyMarked(d);
|
|
7083
|
+
let ink = allFound ? collapsedMarkColor(d) : _state.labelColorDefault;
|
|
7035
7084
|
let t = d3.select(this)
|
|
7036
7085
|
.text(collapsedLabel(d))
|
|
7037
7086
|
.style('font', (allFound ? '600 ' : '') + fs + 'px ' + FONT_DEFAULTS)
|
|
7038
7087
|
.style('fill', ink)
|
|
7039
7088
|
.style('pointer-events', 'none');
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7089
|
+
// placed exactly as a tip's label is (update's text.extlabel): on
|
|
7090
|
+
// the common ring in circular, rotated along the spoke or upright;
|
|
7091
|
+
// on the label column in the aligned phylogram; else just past the
|
|
7092
|
+
// wedge, where a tip's label sits past its tip
|
|
7093
|
+
let gap = _state.nodeLabelGap;
|
|
7094
|
+
let flip = radialDisplay() && labelFlip(d);
|
|
7095
|
+
if (_state.circularDisplay && _radial) {
|
|
7096
|
+
t.attr('text-anchor', flip ? 'end' : 'start')
|
|
7097
|
+
.attr('x', flip ? -gap : gap).attr('dy', '0.32em');
|
|
7098
|
+
if (_radialLabelsHorizontal) {
|
|
7099
|
+
let p = radialXY(d.x, d.y);
|
|
7100
|
+
let q = polarXY(radialAngle(d.x), _radial.maxRad);
|
|
7101
|
+
t.attr('transform', 'translate(' + (q[0] - p[0]) + ',' + (q[1] - p[1]) + ')');
|
|
7102
|
+
} else {
|
|
7103
|
+
let off = _radial.maxRad - radialRadius(d.y);
|
|
7104
|
+
t.attr('transform', 'rotate(' + labelAngleDeg(d) + ') translate(' + off + ',0)' + (flip ? ' rotate(180)' : ''));
|
|
7105
|
+
}
|
|
7106
|
+
} else if (radialDisplay()) {
|
|
7107
|
+
let r = radialRadius(far) - radialRadius(d.y) + gap;
|
|
7043
7108
|
t.attr('transform', 'rotate(' + labelAngleDeg(d) + ') translate(' + r + ',0)' + (flip ? ' rotate(180)' : ''))
|
|
7044
7109
|
.attr('text-anchor', flip ? 'end' : 'start')
|
|
7045
7110
|
.attr('x', 0).attr('dy', '0.32em');
|
|
7046
7111
|
} else {
|
|
7112
|
+
let column = (_state.phylogram && _state.alignPhylogram) ? _w : far;
|
|
7047
7113
|
t.attr('transform', null)
|
|
7048
7114
|
.attr('text-anchor', 'start')
|
|
7049
|
-
.attr('x',
|
|
7115
|
+
.attr('x', column - d.y + gap)
|
|
7050
7116
|
.attr('dy', (0.3 * fs) + 'px');
|
|
7051
7117
|
}
|
|
7052
7118
|
});
|
|
@@ -7069,6 +7135,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7069
7135
|
['taxonomy', TAXONOMY_CB, 'showTaxonomy'],
|
|
7070
7136
|
['sequence', SEQUENCE_CB, 'showSequence'],
|
|
7071
7137
|
['confidence', CONFIDENCE_VALUES_CB, 'showConfidenceValues'],
|
|
7138
|
+
['madValues', MAD_VALUES_CB, 'showMadValues'],
|
|
7072
7139
|
['branchLength', BRANCH_LENGTH_VALUES_CB, 'showBranchLengthValues'],
|
|
7073
7140
|
['external', EXTERNAL_LABEL_CB, 'showExternalLabels'],
|
|
7074
7141
|
['internal', INTERNAL_LABEL_CB, 'showInternalLabels'],
|
|
@@ -7234,10 +7301,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
7234
7301
|
launchInto(_container, _trees, s.tree, cfg);
|
|
7235
7302
|
return;
|
|
7236
7303
|
}
|
|
7237
|
-
if (s.root === 'midpoint' && _viewOps.root !==
|
|
7238
|
-
&& (
|
|
7239
|
-
|
|
7240
|
-
_viewOps.root = 'midpoint';
|
|
7304
|
+
if ((s.root === 'midpoint' || s.root === 'mad') && _viewOps.root !== s.root
|
|
7305
|
+
&& rerootingAllowed()) {
|
|
7306
|
+
rootTreeBy(s.root);
|
|
7241
7307
|
}
|
|
7242
7308
|
if (s.order === 'asc' || s.order === 'desc') {
|
|
7243
7309
|
ladderizeSubtree(_root_const, s.order === 'asc', false);
|
|
@@ -7270,6 +7336,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
7270
7336
|
if (s.layout === 'rectangular' || s.layout === 'circular' || s.layout === 'unrooted') {
|
|
7271
7337
|
_state.circularDisplay = s.layout === 'circular';
|
|
7272
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
|
+
}
|
|
7273
7342
|
}
|
|
7274
7343
|
if (s.display === 'phylogram' || s.display === 'aligned' || s.display === 'cladogram') {
|
|
7275
7344
|
let measured = _basicTreeProperties.branchLengths === true;
|
|
@@ -8762,11 +8831,86 @@ function (root, d3, forester, phyloXml) {
|
|
|
8762
8831
|
}
|
|
8763
8832
|
}
|
|
8764
8833
|
|
|
8765
|
-
//
|
|
8766
|
-
//
|
|
8767
|
-
//
|
|
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.
|
|
8768
8912
|
function midpointRootButtonPressed(event) {
|
|
8769
|
-
if (!_in_subtree && _root && (
|
|
8913
|
+
if (!_in_subtree && _root && rerootingAllowed()) {
|
|
8770
8914
|
let ev = event;
|
|
8771
8915
|
if (!ev || ev.pageX === undefined || (ev.pageX === 0 && ev.pageY === 0)) {
|
|
8772
8916
|
// keyboard/synthetic invocation: anchor the popup at the button
|
|
@@ -8776,19 +8920,30 @@ function (root, d3, forester, phyloXml) {
|
|
|
8776
8920
|
ev = {pageX: window.scrollX + r.right + 4, pageY: window.scrollY + r.top};
|
|
8777
8921
|
}
|
|
8778
8922
|
}
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
{
|
|
8788
|
-
label: 'Cancel', action: function () {
|
|
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();
|
|
8930
|
+
});
|
|
8789
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
|
+
});
|
|
8790
8940
|
}
|
|
8791
|
-
|
|
8941
|
+
});
|
|
8942
|
+
items.push({
|
|
8943
|
+
label: 'Cancel', action: function () {
|
|
8944
|
+
}
|
|
8945
|
+
});
|
|
8946
|
+
showNodeMenu(items, ev, 're-root the tree?');
|
|
8792
8947
|
}
|
|
8793
8948
|
}
|
|
8794
8949
|
|
|
@@ -8993,7 +9148,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
8993
9148
|
// then set up each box's mode menu. Called when a tree is (re)loaded.
|
|
8994
9149
|
function populateSearchMenus() {
|
|
8995
9150
|
let before = _searchFields;
|
|
8996
|
-
|
|
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
|
+
});
|
|
8997
9156
|
[SEARCH_FIELD_SELECT_0, SEARCH_FIELD_SELECT_1].forEach(function (selId) {
|
|
8998
9157
|
let sel = byId(selId);
|
|
8999
9158
|
if (!sel) return;
|
|
@@ -9328,6 +9487,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
9328
9487
|
function layoutButtonClicked() {
|
|
9329
9488
|
_state.circularDisplay = getCheckboxValue(LAYOUT_CIRC_BUTTON);
|
|
9330
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
|
+
}
|
|
9331
9495
|
if (radialDisplay() && _state.showDomainArchitectures && _basicTreeProperties.domainArchitectures) {
|
|
9332
9496
|
// the domain boxes ride the tips' spokes, so the labels must too
|
|
9333
9497
|
_radialLabelsHorizontal = false;
|
|
@@ -9488,6 +9652,31 @@ function (root, d3, forester, phyloXml) {
|
|
|
9488
9652
|
scheduleUpdate();
|
|
9489
9653
|
}
|
|
9490
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
|
+
|
|
9491
9680
|
function branchLengthsCbClicked() {
|
|
9492
9681
|
_state.showBranchLengthValues = getCheckboxValue(BRANCH_LENGTH_VALUES_CB);
|
|
9493
9682
|
scheduleUpdate();
|
|
@@ -10208,6 +10397,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10208
10397
|
+ '.aptx-node-menu button.aptx-menu-danger:hover, .aptx-node-menu button.aptx-menu-danger:focus-visible {'
|
|
10209
10398
|
+ ' background:#e5484d; color:#fff; }'
|
|
10210
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; }'
|
|
10211
10401
|
// The search suggestions: the node menu's chrome under the value box.
|
|
10212
10402
|
+ '.aptx-suggest { position:absolute; z-index:1000; max-width:320px; padding:4px; box-sizing:border-box;'
|
|
10213
10403
|
+ ' border:1px solid var(--p-line-strong); border-radius:10px; background:var(--p-bg); color:var(--p-ink);'
|
|
@@ -10690,6 +10880,32 @@ function (root, d3, forester, phyloXml) {
|
|
|
10690
10880
|
// Content arrives as "Label: value" lines separated by <br>. Setting the
|
|
10691
10881
|
// label part apart makes a wall of such lines scannable. Lines without a
|
|
10692
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
|
+
|
|
10693
10909
|
// The node's data as the hover tooltip and the "Display Node Data" dialog
|
|
10694
10910
|
// both show it -- ONE builder, because they used to be two near-copies
|
|
10695
10911
|
// and carried the same two bugs twice (a bare "Date: " line whenever a
|
|
@@ -10703,13 +10919,19 @@ function (root, d3, forester, phyloXml) {
|
|
|
10703
10919
|
// the section above it. markUpDataLabels() renders the result: a line
|
|
10704
10920
|
// shaped "Key: value" is a row, anything else a heading, and a leading
|
|
10705
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.
|
|
10706
10927
|
function nodeDataText(d) {
|
|
10707
10928
|
let text = '';
|
|
10929
|
+
let unrooted = unrootedContext();
|
|
10708
10930
|
if (d.name) {
|
|
10709
10931
|
text += 'Name: ' + d.name + '<br>';
|
|
10710
10932
|
}
|
|
10711
|
-
if (d.branch_length) {
|
|
10712
|
-
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>';
|
|
10713
10935
|
}
|
|
10714
10936
|
let date = dateText(d.date);
|
|
10715
10937
|
if (date) {
|
|
@@ -10723,9 +10945,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
10723
10945
|
}
|
|
10724
10946
|
}
|
|
10725
10947
|
}
|
|
10726
|
-
|
|
10727
|
-
|
|
10728
|
-
|
|
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>';
|
|
10729
10955
|
}
|
|
10730
10956
|
if (d.confidences) {
|
|
10731
10957
|
for (let i = 0; i < d.confidences.length; ++i) {
|
|
@@ -11318,6 +11544,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11318
11544
|
|
|
11319
11545
|
on(CONFIDENCE_VALUES_CB, 'click', confidenceValuesCbClicked);
|
|
11320
11546
|
on(SUPPORT_DOTS_CB, 'click', supportDotsCbClicked);
|
|
11547
|
+
on(MAD_VALUES_CB, 'click', madValuesCbClicked);
|
|
11321
11548
|
on(SEARCH_B_TOGGLE, 'click', revealSearchB);
|
|
11322
11549
|
on(SEARCH_NAV_PREV, 'click', function () {
|
|
11323
11550
|
stepToFoundNode(-1);
|
|
@@ -11826,6 +12053,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
11826
12053
|
if (_basicTreeProperties.confidences) {
|
|
11827
12054
|
labels.push(makeCheckboxItem('Confidence', CONFIDENCE_VALUES_CB, 'to show/hide confidence values'));
|
|
11828
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
|
+
}
|
|
11829
12059
|
if (_basicTreeProperties.branchLengths) {
|
|
11830
12060
|
labels.push(makeCheckboxItem('Branch Length', BRANCH_LENGTH_VALUES_CB, 'to show/hide branch length values'));
|
|
11831
12061
|
}
|
|
@@ -11914,7 +12144,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11914
12144
|
h = h.concat(makeGlyphButton('whole_tree', RETURN_TO_SUPERTREE_BUTTON, 'return all the way to the complete tree (if in a sub-tree)'));
|
|
11915
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)'));
|
|
11916
12146
|
h = h.concat(makeGlyphButton('uncollapse_all', UNCOLLAPSE_ALL_BUTTON, 'uncollapse all'));
|
|
11917
|
-
h = h.concat(makeGlyphButton('midpoint', MIDPOINT_ROOT_BUTTON, '
|
|
12147
|
+
h = h.concat(makeGlyphButton('midpoint', MIDPOINT_ROOT_BUTTON, 're-root the tree: MAD or midpoint'));
|
|
11918
12148
|
h = h.concat('</div>');
|
|
11919
12149
|
h = h.concat('</fieldset>');
|
|
11920
12150
|
return h;
|
|
@@ -12149,6 +12379,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
12149
12379
|
setCheckboxValue(SEQUENCE_CB, _state.showSequence)
|
|
12150
12380
|
setCheckboxValue(CONFIDENCE_VALUES_CB, _state.showConfidenceValues);
|
|
12151
12381
|
setCheckboxValue(SUPPORT_DOTS_CB, _state.showSupportDots);
|
|
12382
|
+
syncMadValuesCheckbox();
|
|
12152
12383
|
setCheckboxValue(BRANCH_LENGTH_VALUES_CB, _state.showBranchLengthValues);
|
|
12153
12384
|
setCheckboxValue(NODE_EVENTS_CB, _state.showNodeEvents);
|
|
12154
12385
|
setCheckboxValue(BRANCH_EVENTS_CB, _state.showBranchEvents);
|
|
@@ -12267,10 +12498,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
12267
12498
|
disableButton(byId(RETURN_TO_SUPERTREE_BUTTON));
|
|
12268
12499
|
}
|
|
12269
12500
|
|
|
12270
|
-
|
|
12271
|
-
|
|
12501
|
+
let rerootButton = byId(MIDPOINT_ROOT_BUTTON);
|
|
12502
|
+
let blocked = rerootBlockedReason();
|
|
12503
|
+
if (!_in_subtree && !blocked) {
|
|
12504
|
+
enableButton(rerootButton);
|
|
12272
12505
|
} else {
|
|
12273
|
-
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');
|
|
12274
12511
|
}
|
|
12275
12512
|
let b;
|
|
12276
12513
|
if (_foundNodes0 && !_searchBox0Empty) {
|