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/archaeopteryx.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.12.0
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.12.0';
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
- if (c.value) {
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 ] - 1357 of 30,000": the box is the left edge of
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 = '\u2013 ' + (offset + visible).toLocaleString()
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 — keep typing';
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 + ' — hiding now: 1 in ' + _dynahide_factor + ' labels shown'
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
- if (open.length === 0) {
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 && !(unrooted && d.children)) {
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 starts hidden, one click away: the second box (and the
14989
- // Combine control it brings) is rarely needed, and the panel is
14990
- // long enough without it
14991
- h = h.concat('<button type="button" class="aptx-linkbtn" id="' + SEARCH_B_TOGGLE
14992
- + '" title="add a second search box, combinable with the first">+ Search B</button>');
14993
- h = h.concat('<div id="' + SEARCH_B_WRAP + '" style="display:none">');
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">&#9664;</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