archaeopteryx 3.12.0 → 3.14.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 +72 -7
- package/archaeopteryx.js +343 -54
- package/forester.js +593 -82
- package/package.json +3 -3
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.14.0
|
|
24
24
|
// 2026-09-17
|
|
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.14.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';
|
|
@@ -414,6 +414,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
414
414
|
const BRANCH_SCALE_TIME_BUTTON = 'branch_scale_time_b';
|
|
415
415
|
const BRANCH_SCALE_DIV_BUTTON = 'branch_scale_div_b';
|
|
416
416
|
const ABOUT_DIALOG = 'aptx_about';
|
|
417
|
+
const TREE_PROPERTIES_DIALOG = 'aptx_tree_props';
|
|
417
418
|
const PROG_NAME = 'progname';
|
|
418
419
|
const PROGNAMELINK = 'prognamelink';
|
|
419
420
|
const TREE_DESC = 'tree_desc';
|
|
@@ -423,8 +424,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
423
424
|
const RETURN_TO_SUPERTREE_BUTTON_BY_ONE = 'ret1_b';
|
|
424
425
|
const SEARCH_FIELD_0 = 'sf0';
|
|
425
426
|
const SEARCH_FIELD_1 = 'sf1';
|
|
426
|
-
const SEARCH_B_WRAP = 'search_b_wrap';
|
|
427
|
-
const SEARCH_B_TOGGLE = 'search_b_tgl';
|
|
428
427
|
const SEARCH_NAV_ROW = 'searchnavrow';
|
|
429
428
|
const SEARCH_NAV_PREV = 'searchnavprev';
|
|
430
429
|
const SEARCH_NAV_NEXT = 'searchnavnext';
|
|
@@ -4088,8 +4087,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
4088
4087
|
};
|
|
4089
4088
|
|
|
4090
4089
|
|
|
4090
|
+
// A branch length of ZERO is a length and gets a label. Testing the
|
|
4091
|
+
// number for truthiness read 0 as "no branch length", so the one branch
|
|
4092
|
+
// whose length is worth pointing out -- two identical sequences, a
|
|
4093
|
+
// multifurcation written out as a run of zero-length branches -- was the
|
|
4094
|
+
// one drawn bare. Same mistake the phyloXML writer was making until
|
|
4095
|
+
// phyloxml 1.1.1; the desktop cannot make it at all, because it marks an
|
|
4096
|
+
// absent length with a sentinel (-1024) rather than with zero.
|
|
4091
4097
|
let makeBranchLengthLabel = function (phynode) {
|
|
4092
|
-
if (phynode.branch_length) {
|
|
4098
|
+
if (phynode.branch_length !== undefined && phynode.branch_length !== null) {
|
|
4093
4099
|
if (_state.phylogram && _state.minBranchLengthValueToShow && phynode.branch_length < _state.minBranchLengthValueToShow) {
|
|
4094
4100
|
return;
|
|
4095
4101
|
}
|
|
@@ -4117,7 +4123,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
4117
4123
|
if (_state.showConfidenceValues && support.length > 0
|
|
4118
4124
|
&& (!_state.minConfidenceValueToShow || support.some(function (c) { return c.value >= _state.minConfidenceValueToShow; }))) {
|
|
4119
4125
|
support.forEach(function (c) {
|
|
4120
|
-
|
|
4126
|
+
// a support of 0 is a support value, and a telling one: the
|
|
4127
|
+
// MAD branch above already tested it this way
|
|
4128
|
+
if (typeof c.value === 'number' && isFinite(c.value)) {
|
|
4121
4129
|
parts.push(+c.value.toFixed(CONFIDENCE_VALUE_DIGITS_DEFAULT));
|
|
4122
4130
|
}
|
|
4123
4131
|
});
|
|
@@ -7358,8 +7366,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
7358
7366
|
msaScrollTo(Infinity);
|
|
7359
7367
|
});
|
|
7360
7368
|
|
|
7361
|
-
// "column [ 1234 ]
|
|
7362
|
-
// the window and takes a column to jump to; the rest reads back
|
|
7369
|
+
// "column [ 1234 ] 1357 of 30,000": the box is the left edge of
|
|
7370
|
+
// the window and takes a column to jump to; the rest reads back.
|
|
7371
|
+
// No dash between them -- the box's own border already separates
|
|
7372
|
+
// the two numbers, and a dash sitting against it read as a minus
|
|
7373
|
+
// sign on the number after it.
|
|
7363
7374
|
let read = document.createElement('span');
|
|
7364
7375
|
read.className = 'aptx-msa-nav-read';
|
|
7365
7376
|
let lbl = document.createElement('span');
|
|
@@ -7409,7 +7420,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7409
7420
|
_msaNav._jump.value = String(offset + 1);
|
|
7410
7421
|
}
|
|
7411
7422
|
_msaNav._jump.max = String(total);
|
|
7412
|
-
_msaNav._tail.textContent =
|
|
7423
|
+
_msaNav._tail.textContent = (offset + visible).toLocaleString()
|
|
7413
7424
|
+ ' of ' + total.toLocaleString();
|
|
7414
7425
|
let atStart = offset <= 0;
|
|
7415
7426
|
let atEnd = offset >= total - visible;
|
|
@@ -9291,9 +9302,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
9291
9302
|
if (want.value) {
|
|
9292
9303
|
openPanelSection('Search'); // a view carrying a search shows it
|
|
9293
9304
|
}
|
|
9294
|
-
if (idx === 1 && want.value) {
|
|
9295
|
-
showSearchB();
|
|
9296
|
-
}
|
|
9297
9305
|
}
|
|
9298
9306
|
|
|
9299
9307
|
// The panel's controls from the state, after applyViewState
|
|
@@ -9568,6 +9576,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9568
9576
|
{key: 'g', label: 'G', shift: true, what: 'Previous search hit', run: function () { stepToFoundNode(-1); }, typing: true},
|
|
9569
9577
|
{key: ',', label: '<', aliases: ['<'], shift: true, what: 'Previous tree of the file', run: function () { stepTree(-1); }},
|
|
9570
9578
|
{key: '.', label: '>', aliases: ['>'], shift: true, what: 'Next tree of the file', run: function () { stepTree(1); }},
|
|
9579
|
+
{key: 'i', label: 'I', what: 'Tree properties and statistics', run: showTreePropertiesDialog},
|
|
9571
9580
|
{key: '/', label: '/', aliases: ['?'], what: 'This list', run: showShortcutsDialog, typing: true}
|
|
9572
9581
|
];
|
|
9573
9582
|
|
|
@@ -10429,30 +10438,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
10429
10438
|
});
|
|
10430
10439
|
}
|
|
10431
10440
|
|
|
10432
|
-
// Search B starts hidden to keep the panel compact; one click (or a
|
|
10433
|
-
// configured initial value) reveals it, and it stays revealed.
|
|
10434
|
-
// Search B is folded away until wanted: the '+ Search B' link (which
|
|
10435
|
-
// focuses it) or a view that carries a second search.
|
|
10436
|
-
function showSearchB() {
|
|
10437
|
-
openPanelSection('Search');
|
|
10438
|
-
let wrap = byId(SEARCH_B_WRAP);
|
|
10439
|
-
if (wrap) {
|
|
10440
|
-
wrap.style.display = '';
|
|
10441
|
-
}
|
|
10442
|
-
let tgl = byId(SEARCH_B_TOGGLE);
|
|
10443
|
-
if (tgl) {
|
|
10444
|
-
tgl.style.display = 'none';
|
|
10445
|
-
}
|
|
10446
|
-
}
|
|
10447
|
-
|
|
10448
|
-
function revealSearchB() {
|
|
10449
|
-
showSearchB();
|
|
10450
|
-
let f = byId(SEARCH_FIELD_1);
|
|
10451
|
-
if (f) {
|
|
10452
|
-
f.focus();
|
|
10453
|
-
}
|
|
10454
|
-
}
|
|
10455
|
-
|
|
10456
10441
|
// ===================== Time axis =====================
|
|
10457
10442
|
// The desktop's time overlays, drawn beneath a rectangular PHYLOGRAM of
|
|
10458
10443
|
// a dated tree: the two-band ICS geologic axis with a "Ma before
|
|
@@ -11441,8 +11426,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
11441
11426
|
}
|
|
11442
11427
|
if (_state.searchBinitialValue) {
|
|
11443
11428
|
setValue(SEARCH_FIELD_1, _state.searchBinitialValue);
|
|
11444
|
-
revealSearchB();
|
|
11445
|
-
|
|
11446
11429
|
} else {
|
|
11447
11430
|
setValue(SEARCH_FIELD_1, '');
|
|
11448
11431
|
}
|
|
@@ -11757,7 +11740,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11757
11740
|
if (matches.length > SUGGEST_MAX_ROWS) {
|
|
11758
11741
|
let more = document.createElement('div');
|
|
11759
11742
|
more.className = 'aptx-suggest-more';
|
|
11760
|
-
more.textContent = (matches.length - SUGGEST_MAX_ROWS) + ' more
|
|
11743
|
+
more.textContent = (matches.length - SUGGEST_MAX_ROWS) + ' more, keep typing';
|
|
11761
11744
|
box.appendChild(more);
|
|
11762
11745
|
}
|
|
11763
11746
|
document.body.appendChild(box);
|
|
@@ -12184,7 +12167,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
12184
12167
|
}
|
|
12185
12168
|
item.classList.toggle('aptx-check-active', !!active);
|
|
12186
12169
|
item.title = active
|
|
12187
|
-
? item.dataset.baseTitle + '
|
|
12170
|
+
? item.dataset.baseTitle + ', hiding now: 1 in ' + _dynahide_factor + ' labels shown'
|
|
12188
12171
|
: item.dataset.baseTitle;
|
|
12189
12172
|
}
|
|
12190
12173
|
|
|
@@ -12539,7 +12522,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
12539
12522
|
let open = panelSections(panel).filter(function (s) {
|
|
12540
12523
|
return !s.fieldset.classList.contains('aptx-collapsed') && s.name !== justOpened;
|
|
12541
12524
|
});
|
|
12542
|
-
|
|
12525
|
+
// Never the one just opened -- that is the one the user asked
|
|
12526
|
+
// for -- and never the last one standing. Without the second
|
|
12527
|
+
// clause a container dragged short enough empties the panel down
|
|
12528
|
+
// to a stack of legends, taking away the section being worked in.
|
|
12529
|
+
// (justOpened is itself open and excluded from this list, so when
|
|
12530
|
+
// it is set the last one standing is already protected above.)
|
|
12531
|
+
if (open.length === 0 || (!justOpened && open.length === 1)) {
|
|
12543
12532
|
return;
|
|
12544
12533
|
}
|
|
12545
12534
|
open.sort(function (a, b) {
|
|
@@ -12808,6 +12797,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
12808
12797
|
}
|
|
12809
12798
|
|
|
12810
12799
|
// The theme toggle, showing the theme it will switch TO.
|
|
12800
|
+
// A circled "i": the tree-properties button. The stem is a stroke and the
|
|
12801
|
+
// tittle a filled dot, so both keep their shape at the 20px the header
|
|
12802
|
+
// button draws them at.
|
|
12803
|
+
function glyphInfo() {
|
|
12804
|
+
return '<circle cx="50" cy="50" r="40"/>'
|
|
12805
|
+
+ glyphLine(50, 45, 50, 71)
|
|
12806
|
+
+ glyphDot(50, 29, 5.5);
|
|
12807
|
+
}
|
|
12808
|
+
|
|
12811
12809
|
function glyphSun() {
|
|
12812
12810
|
let s = glyphDot(50, 50, 20);
|
|
12813
12811
|
for (let i = 0; i < 8; ++i) {
|
|
@@ -12875,6 +12873,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
12875
12873
|
case 'uncollapse_all': sw = 8.5; cap = 'round'; body = glyphUncollapseAll(); break;
|
|
12876
12874
|
case 'tree_prev': sw = 9; cap = 'round'; body = glyphChevron(false); break;
|
|
12877
12875
|
case 'tree_next': sw = 9; cap = 'round'; body = glyphChevron(true); break;
|
|
12876
|
+
case 'info': sw = 7; cap = 'round'; body = glyphInfo(); break;
|
|
12878
12877
|
case 'sun': body = glyphSun(); break;
|
|
12879
12878
|
case 'moon': body = glyphMoon(); break;
|
|
12880
12879
|
default: throw new Error('unknown control-panel glyph: ' + kind);
|
|
@@ -13013,7 +13012,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
13013
13012
|
+ '@media (prefers-reduced-motion:reduce){ .aptx-busy-dot { animation:none;'
|
|
13014
13013
|
+ ' border-color:var(--p-accent); } }'
|
|
13015
13014
|
+ '.aptx-busy-sub { color:var(--p-muted); }'
|
|
13015
|
+
// The same frosted glass as the node menu and the suggestion list:
|
|
13016
|
+
// --p-bg is deliberately translucent (0.86) so panels float over
|
|
13017
|
+
// the tree, but without the blur a dialog puts dense text straight
|
|
13018
|
+
// on top of whatever is behind it -- and over an alignment or a
|
|
13019
|
+
// heat map that is a rainbow. The menus had the blur from the
|
|
13020
|
+
// start; the dialogs were simply missed.
|
|
13016
13021
|
+ '.aptx-dialog { padding:0; border:1px solid var(--p-line-strong); border-radius:12px;'
|
|
13022
|
+
+ ' -webkit-backdrop-filter:blur(8px); backdrop-filter:blur(8px);'
|
|
13017
13023
|
+ ' background:var(--p-bg); color:var(--p-ink); max-width:92vw;'
|
|
13018
13024
|
+ ' box-shadow:0 24px 48px -16px rgba(23,34,46,0.45),0 4px 12px -4px rgba(23,34,46,0.22);'
|
|
13019
13025
|
+ ' font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;'
|
|
@@ -13058,6 +13064,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
13058
13064
|
+ '.aptx-dialog-key { flex:0 0 42%; color:var(--p-muted); }'
|
|
13059
13065
|
+ '.aptx-dialog-val { flex:1 1 auto; min-width:0; overflow-wrap:anywhere; }'
|
|
13060
13066
|
// the shortcuts cheat sheet: key chips in the key column
|
|
13067
|
+
+ '.aptx-props .aptx-props-head { font-size:10px; letter-spacing:.08em; text-transform:uppercase; color:var(--p-muted); margin:12px 0 3px; }'
|
|
13068
|
+
+ '.aptx-props > div:first-child { margin-top:0; }'
|
|
13069
|
+
+ '.aptx-props .aptx-dialog-key { flex-basis:46%; }'
|
|
13061
13070
|
+ '.aptx-shortcuts .aptx-dialog-key { flex-basis:38%; display:flex; align-items:center; }'
|
|
13062
13071
|
+ '.aptx-shortcuts .aptx-dialog-val { align-self:center; }'
|
|
13063
13072
|
+ '.aptx-keys { display:inline-flex; gap:3px; flex-wrap:wrap; }'
|
|
@@ -13212,7 +13221,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13212
13221
|
+ '.aptx-panel .aptx-seg:has(> input:checked) { background:var(--p-accent); color:#fff; }'
|
|
13213
13222
|
+ '.aptx-panel .aptx-seg:has(> input:disabled) { opacity:0.4; cursor:default; }'
|
|
13214
13223
|
+ '.aptx-panel .aptx-actions { margin-left:auto; display:flex; align-items:center; gap:5px; }'
|
|
13215
|
-
+ '.aptx-panel .aptx-theme-btn { flex:none; width:20px; height:20px; display:grid; place-items:center; padding:0; border:1px solid var(--p-line-strong); border-radius:6px; background:var(--p-surface2); color:var(--p-muted); cursor:pointer; font-size:12px; line-height:1; }'
|
|
13224
|
+
+ '.aptx-panel .aptx-theme-btn, .aptx-panel .aptx-info-btn { flex:none; width:20px; height:20px; display:grid; place-items:center; padding:0; border:1px solid var(--p-line-strong); border-radius:6px; background:var(--p-surface2); color:var(--p-muted); cursor:pointer; font-size:12px; line-height:1; }'
|
|
13216
13225
|
+ '.aptx-panel .aptx-theme-btn:hover { background:var(--p-accent-weak); color:var(--p-accent-ink); border-color:var(--p-accent); }'
|
|
13217
13226
|
+ '.aptx-panel input[type=range] { -webkit-appearance:none; appearance:none; display:block; width:100%; height:15px; margin:2px 0 9px; padding:0; background:transparent; cursor:pointer; }'
|
|
13218
13227
|
+ '.aptx-panel input[type=range]::-webkit-slider-runnable-track { height:4px; border-radius:999px; background:var(--p-line-strong); }'
|
|
@@ -13246,8 +13255,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
13246
13255
|
+ '.aptx-panel .aptx-slider-row { display:flex; align-items:center; gap:7px; margin:3px 0; }'
|
|
13247
13256
|
+ '.aptx-panel .aptx-slider-row label { flex:0 0 42px; font-size:10px; color:var(--p-muted); }'
|
|
13248
13257
|
+ '.aptx-panel .aptx-slider-row input[type=range] { flex:1 1 auto; min-width:0; margin:0; }'
|
|
13249
|
-
+ '.aptx-panel .aptx-linkbtn { background:none; border:0; padding:1px 0 2px; margin:0; font:inherit; font-size:10px; color:var(--p-accent); cursor:pointer; display:block; }'
|
|
13250
|
-
+ '.aptx-panel .aptx-linkbtn:hover { text-decoration:underline; }'
|
|
13251
13258
|
// one row, its buttons sharing it evenly, its edges on the zoom rows'
|
|
13252
13259
|
+ '.aptx-panel .aptx-toolrow { margin-top:5px; display:flex; gap:3px; }'
|
|
13253
13260
|
+ '.aptx-panel .aptx-toolrow .aptx-gbtn { flex:1 1 0; min-width:0; padding:0; margin:2px 0; }'
|
|
@@ -13369,6 +13376,24 @@ function (root, d3, forester, phyloXml) {
|
|
|
13369
13376
|
let actions = document.createElement('div');
|
|
13370
13377
|
actions.className = 'aptx-actions';
|
|
13371
13378
|
|
|
13379
|
+
let infoBtn = document.createElement('button');
|
|
13380
|
+
infoBtn.type = 'button';
|
|
13381
|
+
// NOT aptx-theme-btn: applyPanelTheme repaints the innerHTML of
|
|
13382
|
+
// every element carrying that class with the sun/moon, so
|
|
13383
|
+
// borrowing it for the chrome replaced this glyph the instant a
|
|
13384
|
+
// theme was applied. The two share their styling through the CSS
|
|
13385
|
+
// selector instead, which is all the class was wanted for.
|
|
13386
|
+
infoBtn.className = 'aptx-info-btn';
|
|
13387
|
+
infoBtn.title = 'Tree properties and statistics ('
|
|
13388
|
+
+ (IS_MAC ? '\u2318I' : 'Ctrl+I') + ')';
|
|
13389
|
+
infoBtn.setAttribute('aria-label', 'Tree properties and statistics');
|
|
13390
|
+
infoBtn.innerHTML = makeGlyph('info');
|
|
13391
|
+
infoBtn.addEventListener('click', function (e) {
|
|
13392
|
+
e.stopPropagation();
|
|
13393
|
+
showTreePropertiesDialog();
|
|
13394
|
+
});
|
|
13395
|
+
actions.appendChild(infoBtn);
|
|
13396
|
+
|
|
13372
13397
|
let progName = header.querySelector('.' + PROGNAMELINK);
|
|
13373
13398
|
if (progName) {
|
|
13374
13399
|
// Clicking the title used to navigate away to the project site;
|
|
@@ -13455,6 +13480,20 @@ function (root, d3, forester, phyloXml) {
|
|
|
13455
13480
|
// spare, since the user opened none of them just now.
|
|
13456
13481
|
fitPanelSections(panel, null);
|
|
13457
13482
|
|
|
13483
|
+
// ... and again whenever the room changes. The panel is held to its
|
|
13484
|
+
// container's height, so a window dragged shorter (or a host resizing
|
|
13485
|
+
// the div around us) can push an arrangement that fitted a moment ago
|
|
13486
|
+
// into a scrollbar. Watching the CONTAINER, not the panel: folding a
|
|
13487
|
+
// section changes the panel's height and would re-enter the observer.
|
|
13488
|
+
// One-way on purpose -- growing the window back does NOT reopen what
|
|
13489
|
+
// was folded. What is open is the user's choice; only running out of
|
|
13490
|
+
// room may overrule it, and then only far enough to fit.
|
|
13491
|
+
if (typeof ResizeObserver !== 'undefined' && panel.parentElement) {
|
|
13492
|
+
new ResizeObserver(function () {
|
|
13493
|
+
fitPanelSections(panel, null);
|
|
13494
|
+
}).observe(panel.parentElement);
|
|
13495
|
+
}
|
|
13496
|
+
|
|
13458
13497
|
// Apply the current light/dark choice to this (and every) panel.
|
|
13459
13498
|
applyPanelTheme();
|
|
13460
13499
|
}
|
|
@@ -13599,7 +13638,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
13599
13638
|
if (d.name) {
|
|
13600
13639
|
text += 'Name: ' + d.name + '<br>';
|
|
13601
13640
|
}
|
|
13602
|
-
if (d.branch_length
|
|
13641
|
+
if (d.branch_length !== undefined && d.branch_length !== null
|
|
13642
|
+
&& !(unrooted && d.children)) {
|
|
13603
13643
|
text += (unrooted ? 'Branch length: ' : 'Distance to parent: ') + d.branch_length + '<br>';
|
|
13604
13644
|
}
|
|
13605
13645
|
let date = dateText(d.date);
|
|
@@ -13927,6 +13967,257 @@ function (root, d3, forester, phyloXml) {
|
|
|
13927
13967
|
shell.dialog.showModal();
|
|
13928
13968
|
}
|
|
13929
13969
|
|
|
13970
|
+
// ===================== Tree properties =====================
|
|
13971
|
+
// What the tree IS and what it carries: the desktop's View > Tree
|
|
13972
|
+
// Properties, read-only (Christian, 2026-09-23 -- the editable half waits
|
|
13973
|
+
// for demand). Nothing else in the viewer answers "what is actually in
|
|
13974
|
+
// this file", which is the first question a tree raises.
|
|
13975
|
+
//
|
|
13976
|
+
// Computed FRESH every time it opens rather than cached: the whole of
|
|
13977
|
+
// collectBasicTreeProperties costs 7ms on the 13,246-tip H5N1 demo, and a
|
|
13978
|
+
// cache would go stale the moment the user re-roots or enters a subtree.
|
|
13979
|
+
// It describes what is ON SCREEN, so in a subtree it describes the
|
|
13980
|
+
// subtree, and says so.
|
|
13981
|
+
//
|
|
13982
|
+
// Every value goes in through textContent. Tree names, descriptions and
|
|
13983
|
+
// identifiers are file data, and file data never becomes markup.
|
|
13984
|
+
|
|
13985
|
+
function treePropRow(parent, label, value) {
|
|
13986
|
+
if (value === null || value === undefined || value === '') {
|
|
13987
|
+
return;
|
|
13988
|
+
}
|
|
13989
|
+
let line = document.createElement('div');
|
|
13990
|
+
line.className = 'aptx-dialog-line';
|
|
13991
|
+
let k = document.createElement('span');
|
|
13992
|
+
k.className = 'aptx-dialog-key';
|
|
13993
|
+
k.textContent = label;
|
|
13994
|
+
let v = document.createElement('span');
|
|
13995
|
+
v.className = 'aptx-dialog-val';
|
|
13996
|
+
v.textContent = String(value);
|
|
13997
|
+
line.appendChild(k);
|
|
13998
|
+
line.appendChild(v);
|
|
13999
|
+
parent.appendChild(line);
|
|
14000
|
+
}
|
|
14001
|
+
|
|
14002
|
+
function treePropSection(body, title) {
|
|
14003
|
+
let h = document.createElement('div');
|
|
14004
|
+
h.className = 'aptx-props-head';
|
|
14005
|
+
h.textContent = title;
|
|
14006
|
+
body.appendChild(h);
|
|
14007
|
+
let box = document.createElement('div');
|
|
14008
|
+
body.appendChild(box);
|
|
14009
|
+
return box;
|
|
14010
|
+
}
|
|
14011
|
+
|
|
14012
|
+
function countNumber(n) {
|
|
14013
|
+
return (typeof n === 'number' && isFinite(n)) ? n.toLocaleString() : null;
|
|
14014
|
+
}
|
|
14015
|
+
|
|
14016
|
+
// A number for a fact row: thousands separators for counts, and enough
|
|
14017
|
+
// significant digits for a branch length, which can be 5e-9.
|
|
14018
|
+
function statNumber(v, digits) {
|
|
14019
|
+
if (typeof v !== 'number' || !isFinite(v)) {
|
|
14020
|
+
return null;
|
|
14021
|
+
}
|
|
14022
|
+
if (v === 0 || (Math.abs(v) >= 0.001 && Math.abs(v) < 1e7)) {
|
|
14023
|
+
return String(forester.roundNumber(v, digits === undefined ? 6 : digits));
|
|
14024
|
+
}
|
|
14025
|
+
return v.toExponential(3);
|
|
14026
|
+
}
|
|
14027
|
+
|
|
14028
|
+
// "1,234 of 5,678 tips", the desktop's coverage phrasing.
|
|
14029
|
+
function statOf(part, whole, noun) {
|
|
14030
|
+
if (!(whole > 0)) {
|
|
14031
|
+
return null;
|
|
14032
|
+
}
|
|
14033
|
+
return countNumber(part) + ' of ' + countNumber(whole) + (noun ? (' ' + noun) : '');
|
|
14034
|
+
}
|
|
14035
|
+
|
|
14036
|
+
// n / minimum / median / maximum / mean, the block Christian asked for in
|
|
14037
|
+
// place of a histogram. One shape, used for branch lengths and for each
|
|
14038
|
+
// kind of support, so the two read the same way.
|
|
14039
|
+
function statSpread(box, d, digits) {
|
|
14040
|
+
treePropRow(box, 'Minimum', statNumber(d.min, digits));
|
|
14041
|
+
treePropRow(box, 'Median', statNumber(d.median, digits));
|
|
14042
|
+
treePropRow(box, 'Maximum', statNumber(d.max, digits));
|
|
14043
|
+
treePropRow(box, 'Mean', statNumber(d.mean, digits));
|
|
14044
|
+
}
|
|
14045
|
+
|
|
14046
|
+
function showTreePropertiesDialog() {
|
|
14047
|
+
if (!_root) {
|
|
14048
|
+
return;
|
|
14049
|
+
}
|
|
14050
|
+
let shell = makeDialogShell(TREE_PROPERTIES_DIALOG, 'Tree properties', 420);
|
|
14051
|
+
shell.body.classList.add('aptx-props');
|
|
14052
|
+
// A description is free text and can be a paragraph, and the coverage
|
|
14053
|
+
// section grows with the annotations, so the body scrolls rather than
|
|
14054
|
+
// running off the screen.
|
|
14055
|
+
shell.body.style.maxHeight = '66vh';
|
|
14056
|
+
shell.body.style.overflowY = 'auto';
|
|
14057
|
+
let st = forester.treeStatistics(_root);
|
|
14058
|
+
let bp = forester.collectBasicTreeProperties(_root);
|
|
14059
|
+
let time = forester.timeAxisInfo(_root);
|
|
14060
|
+
let phy = _treeData || {};
|
|
14061
|
+
|
|
14062
|
+
// --- what the tree says it is ---------------------------------
|
|
14063
|
+
let box = treePropSection(shell.body, 'Tree');
|
|
14064
|
+
treePropRow(box, 'Name', phy.name);
|
|
14065
|
+
treePropRow(box, 'Description', phy.description);
|
|
14066
|
+
if (phy.id && phy.id.value) {
|
|
14067
|
+
treePropRow(box, 'Identifier', phy.id.provider
|
|
14068
|
+
? (phy.id.value + ' (' + phy.id.provider + ')') : phy.id.value);
|
|
14069
|
+
}
|
|
14070
|
+
treePropRow(box, 'Type', phy.type);
|
|
14071
|
+
treePropRow(box, 'Branch-length unit', phy.branch_length_unit);
|
|
14072
|
+
// Only when the FILE said so. phyloXML has a rooted attribute and a
|
|
14073
|
+
// Nexus tree statement has [&R]/[&U]; plain Newick says nothing, and a
|
|
14074
|
+
// properties dialog that answered "yes" there would be reporting our
|
|
14075
|
+
// writing default as though the file had stated it.
|
|
14076
|
+
if (typeof phy.rooted === 'boolean') {
|
|
14077
|
+
treePropRow(box, 'Rooted', phy.rooted ? 'yes' : 'no');
|
|
14078
|
+
}
|
|
14079
|
+
if (typeof phy.rerootable === 'boolean') {
|
|
14080
|
+
treePropRow(box, 'Rerootable', phy.rerootable ? 'yes' : 'no');
|
|
14081
|
+
}
|
|
14082
|
+
if (_trees && _trees.length > 1) {
|
|
14083
|
+
treePropRow(box, 'In this file', 'tree ' + (_treeIndex + 1) + ' of ' + _trees.length);
|
|
14084
|
+
}
|
|
14085
|
+
if (_in_subtree) {
|
|
14086
|
+
treePropRow(box, 'Showing', 'a subtree, not the whole tree');
|
|
14087
|
+
}
|
|
14088
|
+
if (!box.firstChild) {
|
|
14089
|
+
treePropRow(box, 'Name', '(the tree carries no name or metadata)');
|
|
14090
|
+
}
|
|
14091
|
+
|
|
14092
|
+
// --- shape ----------------------------------------------------
|
|
14093
|
+
box = treePropSection(shell.body, 'Structure');
|
|
14094
|
+
treePropRow(box, 'Tips', countNumber(st.tips));
|
|
14095
|
+
treePropRow(box, 'Internal nodes', countNumber(st.internal));
|
|
14096
|
+
treePropRow(box, 'Nodes, total', countNumber(st.nodes));
|
|
14097
|
+
treePropRow(box, 'Branches', countNumber(st.branches));
|
|
14098
|
+
treePropRow(box, 'Branching', st.polytomies === 0 ? 'fully binary'
|
|
14099
|
+
: (countNumber(st.polytomies) + (st.polytomies === 1 ? ' polytomy' : ' polytomies')
|
|
14100
|
+
+ ', widest ' + countNumber(st.maxChildren) + ' children'));
|
|
14101
|
+
treePropRow(box, 'Depth', countNumber(st.depth) + ' (root to deepest tip)');
|
|
14102
|
+
// ours, not the desktop's, and worth keeping: the label width is what
|
|
14103
|
+
// decides how much room the tree needs, and whether internal nodes are
|
|
14104
|
+
// named decides whether their labels are worth turning on
|
|
14105
|
+
treePropRow(box, 'Longest tip label', bp.longestNodeName > 0
|
|
14106
|
+
? (countNumber(bp.longestNodeName)
|
|
14107
|
+
+ (bp.longestNodeName === 1 ? ' character' : ' characters')) : null);
|
|
14108
|
+
treePropRow(box, 'Internal nodes named', st.internal > 0
|
|
14109
|
+
? (st.namedInternal > 0 ? statOf(st.namedInternal, st.internal) : 'none') : null);
|
|
14110
|
+
|
|
14111
|
+
// --- branch lengths -------------------------------------------
|
|
14112
|
+
box = treePropSection(shell.body, 'Branch lengths');
|
|
14113
|
+
if (st.branchLengths) {
|
|
14114
|
+
treePropRow(box, 'Branches with a length',
|
|
14115
|
+
statOf(st.branchLengths.n, st.branches));
|
|
14116
|
+
statSpread(box, st.branchLengths);
|
|
14117
|
+
treePropRow(box, 'Sum', statNumber(st.branchLengths.sum) + ' (total tree length)');
|
|
14118
|
+
treePropRow(box, 'Height', statNumber(st.height) + ' (root to farthest tip)');
|
|
14119
|
+
if (st.branchLengths.zero > 0) {
|
|
14120
|
+
treePropRow(box, 'Zero-length', countNumber(st.branchLengths.zero));
|
|
14121
|
+
}
|
|
14122
|
+
if (st.branchLengths.negative > 0) {
|
|
14123
|
+
treePropRow(box, 'Negative', countNumber(st.branchLengths.negative)
|
|
14124
|
+
+ ' (not drawn to scale)');
|
|
14125
|
+
}
|
|
14126
|
+
treePropRow(box, 'Ultrametric', st.ultrametric
|
|
14127
|
+
? 'yes (every tip the same distance from the root)' : 'no');
|
|
14128
|
+
} else {
|
|
14129
|
+
treePropRow(box, 'Branches with a length', 'none, the tree has no lengths');
|
|
14130
|
+
}
|
|
14131
|
+
|
|
14132
|
+
// --- support, one section per kind ----------------------------
|
|
14133
|
+
// A bootstrap and a posterior on the same branch are not on one scale,
|
|
14134
|
+
// so they are never pooled: each type gets its own n and spread.
|
|
14135
|
+
st.support.forEach(function (d) {
|
|
14136
|
+
// 'unknown' is what the Newick and Nexus readers put on a bare
|
|
14137
|
+
// [95]: those formats have one support slot and nowhere to name
|
|
14138
|
+
// what it is, so it is an absence of type rather than a type
|
|
14139
|
+
// called unknown, and it should not head a section as one.
|
|
14140
|
+
let untyped = !d.type || d.type === 'unknown';
|
|
14141
|
+
let title = (st.support.length === 1 && untyped)
|
|
14142
|
+
? 'Support values' : ('Support: ' + (untyped ? 'untyped' : d.type));
|
|
14143
|
+
let sbox = treePropSection(shell.body, title);
|
|
14144
|
+
treePropRow(sbox, 'Branches with support', statOf(d.n, st.branches));
|
|
14145
|
+
statSpread(sbox, d);
|
|
14146
|
+
});
|
|
14147
|
+
|
|
14148
|
+
// --- what came with it ----------------------------------------
|
|
14149
|
+
box = treePropSection(shell.body, 'The tree carries');
|
|
14150
|
+
let carried = 0;
|
|
14151
|
+
function carries(label, present, detail) {
|
|
14152
|
+
if (!present) {
|
|
14153
|
+
return;
|
|
14154
|
+
}
|
|
14155
|
+
++carried;
|
|
14156
|
+
treePropRow(box, label, detail || 'yes');
|
|
14157
|
+
}
|
|
14158
|
+
carries('Taxonomies', st.tipsWithTaxonomy > 0, statOf(st.tipsWithTaxonomy, st.tips, 'tips')
|
|
14159
|
+
+ (st.distinctTaxonomies > 0 ? (', ' + countNumber(st.distinctTaxonomies) + ' distinct') : ''));
|
|
14160
|
+
carries('Taxonomy identifiers', st.tipsWithTaxonomyId > 0,
|
|
14161
|
+
statOf(st.tipsWithTaxonomyId, st.tips, 'tips'));
|
|
14162
|
+
carries('Sequences', st.tipsWithSequence > 0, statOf(st.tipsWithSequence, st.tips, 'tips'));
|
|
14163
|
+
// alignedMolSeqs starts TRUE and is only ever set false, so it means
|
|
14164
|
+
// "no UNALIGNED sequence was seen" -- a tree carrying no sequences at
|
|
14165
|
+
// all reports true. Read alone it told the flu and ammonite demos they
|
|
14166
|
+
// had an alignment. Every other reader in this file pairs it with a
|
|
14167
|
+
// length for the same reason.
|
|
14168
|
+
carries('Molecular sequences', st.tipsWithMolSeq > 0,
|
|
14169
|
+
statOf(st.tipsWithMolSeq, st.tips, 'tips')
|
|
14170
|
+
+ (bp.alignedMolSeqs && bp.maxMolSeqLength > 0
|
|
14171
|
+
? (', aligned, ' + countNumber(bp.maxMolSeqLength) + ' columns') : ''));
|
|
14172
|
+
carries('Domain architectures', st.tipsWithDomains > 0,
|
|
14173
|
+
statOf(st.tipsWithDomains, st.tips, 'tips')
|
|
14174
|
+
+ (bp.maxDomainArchitectureLength > 0
|
|
14175
|
+
? (', up to ' + countNumber(bp.maxDomainArchitectureLength) + ' residues') : ''));
|
|
14176
|
+
carries('Dates', st.nodesWithDate > 0, statOf(st.nodesWithDate, st.nodes, 'nodes'));
|
|
14177
|
+
carries('Distributions', st.tipsWithDistribution > 0,
|
|
14178
|
+
statOf(st.tipsWithDistribution, st.tips, 'tips'));
|
|
14179
|
+
carries('MAD values', bp.madValues);
|
|
14180
|
+
carries('Branch colours', bp.branchColors);
|
|
14181
|
+
if (st.nodesWithEvents > 0) {
|
|
14182
|
+
let parts = [];
|
|
14183
|
+
if (st.duplications > 0) {
|
|
14184
|
+
parts.push(countNumber(st.duplications)
|
|
14185
|
+
+ (st.duplications === 1 ? ' duplication' : ' duplications'));
|
|
14186
|
+
}
|
|
14187
|
+
if (st.speciations > 0) {
|
|
14188
|
+
parts.push(countNumber(st.speciations)
|
|
14189
|
+
+ (st.speciations === 1 ? ' speciation' : ' speciations'));
|
|
14190
|
+
}
|
|
14191
|
+
parts.push(statOf(st.nodesWithEvents, st.nodes, 'nodes'));
|
|
14192
|
+
carries('Events', true, parts.join(', '));
|
|
14193
|
+
}
|
|
14194
|
+
st.propertyRefs.forEach(function (r) {
|
|
14195
|
+
carries('Property ' + r.ref, true, statOf(r.nodes, st.nodes, 'nodes'));
|
|
14196
|
+
});
|
|
14197
|
+
if (carried === 0) {
|
|
14198
|
+
treePropRow(box, 'Annotations', 'none, names and topology only');
|
|
14199
|
+
}
|
|
14200
|
+
|
|
14201
|
+
// --- time -----------------------------------------------------
|
|
14202
|
+
if (time && (time.type || time.dated)) {
|
|
14203
|
+
box = treePropSection(shell.body, 'Time');
|
|
14204
|
+
treePropRow(box, 'Dates', time.type === 'geologic'
|
|
14205
|
+
? 'geologic ages' : (time.type === 'calendar' ? 'calendar years' : 'yes'));
|
|
14206
|
+
treePropRow(box, 'Dated nodes', statOf(st.nodesWithDate, st.nodes, 'nodes')
|
|
14207
|
+
+ (time.dated ? ' (a dated tree)' : ''));
|
|
14208
|
+
if (time.type === 'geologic' && time.rootAge > 0) {
|
|
14209
|
+
treePropRow(box, 'Oldest node', statNumber(time.rootAge, 4) + ' Ma');
|
|
14210
|
+
}
|
|
14211
|
+
if (time.type === 'calendar' && time.presentDate > 0) {
|
|
14212
|
+
treePropRow(box, 'Most recent tip', statNumber(time.presentDate, 4));
|
|
14213
|
+
}
|
|
14214
|
+
treePropRow(box, 'Confidence intervals',
|
|
14215
|
+
(time.hasInternalIntervals || time.hasExternalIntervals) ? 'yes' : null);
|
|
14216
|
+
}
|
|
14217
|
+
|
|
14218
|
+
shell.dialog.showModal();
|
|
14219
|
+
}
|
|
14220
|
+
|
|
13930
14221
|
// The About box behind the panel's title. Everything here is fixed text and
|
|
13931
14222
|
// fixed URLs -- no tree data reaches it -- so the links are built as real
|
|
13932
14223
|
// elements rather than markup.
|
|
@@ -14241,7 +14532,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
14241
14532
|
on(CONFIDENCE_VALUES_CB, 'click', confidenceValuesCbClicked);
|
|
14242
14533
|
on(SUPPORT_DOTS_CB, 'click', supportDotsCbClicked);
|
|
14243
14534
|
on(MAD_VALUES_CB, 'click', madValuesCbClicked);
|
|
14244
|
-
on(SEARCH_B_TOGGLE, 'click', revealSearchB);
|
|
14245
14535
|
on(SEARCH_NAV_PREV, 'click', function () {
|
|
14246
14536
|
stepToFoundNode(-1);
|
|
14247
14537
|
});
|
|
@@ -14985,14 +15275,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
14985
15275
|
h = h.concat('<fieldset>');
|
|
14986
15276
|
h = h.concat('<legend>Search</legend>');
|
|
14987
15277
|
h = h.concat(makeSearchBox('Search A', 0));
|
|
14988
|
-
// Search B
|
|
14989
|
-
//
|
|
14990
|
-
//
|
|
14991
|
-
|
|
14992
|
-
|
|
14993
|
-
|
|
15278
|
+
// Search B used to sit behind a '+ Search B' link, to keep 45px out
|
|
15279
|
+
// of the panel. That link was a second, weaker disclosure mechanism
|
|
15280
|
+
// nested inside the section fold: it was forgotten on every reload
|
|
15281
|
+
// (the fold is remembered), it was one-way -- nothing put B back --
|
|
15282
|
+
// and it defeated the fit rule, since revealing B grew a section
|
|
15283
|
+
// that had just been fitted without it, leaving the panel to
|
|
15284
|
+
// scroll. The fold IS the disclosure. B is simply here.
|
|
14994
15285
|
h = h.concat(makeSearchBox('Search B', 1));
|
|
14995
|
-
h = h.concat('</div>');
|
|
14996
15286
|
h = h.concat('<div class="aptx-searchnav" id="' + SEARCH_NAV_ROW + '" style="display:none">');
|
|
14997
15287
|
h = h.concat('<button type="button" class="aptx-gbtn" id="' + SEARCH_NAV_PREV
|
|
14998
15288
|
+ '" title="center the previous search hit">◀</button>');
|
|
@@ -15207,7 +15497,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
15207
15497
|
}
|
|
15208
15498
|
if (_state.searchBinitialValue) {
|
|
15209
15499
|
setValue(SEARCH_FIELD_1, _state.searchBinitialValue);
|
|
15210
|
-
revealSearchB();
|
|
15211
15500
|
}
|
|
15212
15501
|
}
|
|
15213
15502
|
|