archaeopteryx 3.13.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 +32 -1
- package/archaeopteryx.js +296 -5
- package/forester.js +225 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1059,7 +1059,38 @@ never at all on a tall screen. Growing the window back leaves the folds where
|
|
|
1059
1059
|
they are: what is open is your choice, and only running out of room overrules
|
|
1060
1060
|
it. Anything that puts something into a folded section opens it, so jumping to
|
|
1061
1061
|
the search box (⌘F / Ctrl+F) or a tool reporting its result as search hits
|
|
1062
|
-
unfolds Search.
|
|
1062
|
+
unfolds Search. The **ⓘ** button in the panel header (or ⌘I / Ctrl+I) opens **Tree
|
|
1063
|
+
properties**, modelled on the desktop Archaeopteryx's *View › Tree
|
|
1064
|
+
Properties* and read-only:
|
|
1065
|
+
|
|
1066
|
+
* **Tree** — name, description, identifier, type, branch-length unit,
|
|
1067
|
+
rootedness and rerootability (each shown only where the file actually says
|
|
1068
|
+
so), and "tree 2 of 3" when the file held several.
|
|
1069
|
+
* **Structure** — tips, internal nodes, total nodes, branches, whether the
|
|
1070
|
+
tree is fully binary or how many polytomies it has and the widest of them,
|
|
1071
|
+
depth from root to deepest tip, the longest tip label, and how many internal
|
|
1072
|
+
nodes are named.
|
|
1073
|
+
* **Branch lengths** — how many branches carry one, then **minimum, median,
|
|
1074
|
+
maximum and mean**, the sum as total tree length, the height from root to
|
|
1075
|
+
farthest tip, any zero-length or negative branches, and whether the tree is
|
|
1076
|
+
ultrametric.
|
|
1077
|
+
* **Support** — the same n / minimum / median / maximum / mean, **one section
|
|
1078
|
+
per kind**. A bootstrap and a posterior on the same branch are not on one
|
|
1079
|
+
scale, so they are never pooled; MAD values are not support and are counted
|
|
1080
|
+
with the annotations instead.
|
|
1081
|
+
* **The tree carries** — taxonomies (and how many are distinct), taxonomy
|
|
1082
|
+
identifiers, sequences, molecular sequences with the alignment width, domain
|
|
1083
|
+
architectures, dates, distributions, events, branch colours, and **every
|
|
1084
|
+
phyloXML property `ref` with the number of nodes carrying it** — each as
|
|
1085
|
+
"n of m tips" or "n of m nodes".
|
|
1086
|
+
* **Time** — geologic ages or calendar years, dated nodes, the oldest node or
|
|
1087
|
+
most recent tip, and whether there are confidence intervals.
|
|
1088
|
+
|
|
1089
|
+
It describes what is on screen, so inside a subtree it describes the subtree
|
|
1090
|
+
and says so, and it is recomputed each time it opens rather than cached (24 ms
|
|
1091
|
+
on the 13,246-tip demo). There is no histogram.
|
|
1092
|
+
|
|
1093
|
+
A host with little room to give can
|
|
1063
1094
|
also start the whole panel tighter and narrower with
|
|
1064
1095
|
[`panelDensity: 'compact'`](#configuration), or collapsed to its header bar
|
|
1065
1096
|
with `collapseControlPanel`.
|
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';
|
|
@@ -9575,6 +9576,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9575
9576
|
{key: 'g', label: 'G', shift: true, what: 'Previous search hit', run: function () { stepToFoundNode(-1); }, typing: true},
|
|
9576
9577
|
{key: ',', label: '<', aliases: ['<'], shift: true, what: 'Previous tree of the file', run: function () { stepTree(-1); }},
|
|
9577
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},
|
|
9578
9580
|
{key: '/', label: '/', aliases: ['?'], what: 'This list', run: showShortcutsDialog, typing: true}
|
|
9579
9581
|
];
|
|
9580
9582
|
|
|
@@ -11738,7 +11740,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11738
11740
|
if (matches.length > SUGGEST_MAX_ROWS) {
|
|
11739
11741
|
let more = document.createElement('div');
|
|
11740
11742
|
more.className = 'aptx-suggest-more';
|
|
11741
|
-
more.textContent = (matches.length - SUGGEST_MAX_ROWS) + ' more
|
|
11743
|
+
more.textContent = (matches.length - SUGGEST_MAX_ROWS) + ' more, keep typing';
|
|
11742
11744
|
box.appendChild(more);
|
|
11743
11745
|
}
|
|
11744
11746
|
document.body.appendChild(box);
|
|
@@ -12165,7 +12167,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
12165
12167
|
}
|
|
12166
12168
|
item.classList.toggle('aptx-check-active', !!active);
|
|
12167
12169
|
item.title = active
|
|
12168
|
-
? item.dataset.baseTitle + '
|
|
12170
|
+
? item.dataset.baseTitle + ', hiding now: 1 in ' + _dynahide_factor + ' labels shown'
|
|
12169
12171
|
: item.dataset.baseTitle;
|
|
12170
12172
|
}
|
|
12171
12173
|
|
|
@@ -12795,6 +12797,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
12795
12797
|
}
|
|
12796
12798
|
|
|
12797
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
|
+
|
|
12798
12809
|
function glyphSun() {
|
|
12799
12810
|
let s = glyphDot(50, 50, 20);
|
|
12800
12811
|
for (let i = 0; i < 8; ++i) {
|
|
@@ -12862,6 +12873,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
12862
12873
|
case 'uncollapse_all': sw = 8.5; cap = 'round'; body = glyphUncollapseAll(); break;
|
|
12863
12874
|
case 'tree_prev': sw = 9; cap = 'round'; body = glyphChevron(false); break;
|
|
12864
12875
|
case 'tree_next': sw = 9; cap = 'round'; body = glyphChevron(true); break;
|
|
12876
|
+
case 'info': sw = 7; cap = 'round'; body = glyphInfo(); break;
|
|
12865
12877
|
case 'sun': body = glyphSun(); break;
|
|
12866
12878
|
case 'moon': body = glyphMoon(); break;
|
|
12867
12879
|
default: throw new Error('unknown control-panel glyph: ' + kind);
|
|
@@ -13000,7 +13012,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
13000
13012
|
+ '@media (prefers-reduced-motion:reduce){ .aptx-busy-dot { animation:none;'
|
|
13001
13013
|
+ ' border-color:var(--p-accent); } }'
|
|
13002
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.
|
|
13003
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);'
|
|
13004
13023
|
+ ' background:var(--p-bg); color:var(--p-ink); max-width:92vw;'
|
|
13005
13024
|
+ ' box-shadow:0 24px 48px -16px rgba(23,34,46,0.45),0 4px 12px -4px rgba(23,34,46,0.22);'
|
|
13006
13025
|
+ ' font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;'
|
|
@@ -13045,6 +13064,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
13045
13064
|
+ '.aptx-dialog-key { flex:0 0 42%; color:var(--p-muted); }'
|
|
13046
13065
|
+ '.aptx-dialog-val { flex:1 1 auto; min-width:0; overflow-wrap:anywhere; }'
|
|
13047
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%; }'
|
|
13048
13070
|
+ '.aptx-shortcuts .aptx-dialog-key { flex-basis:38%; display:flex; align-items:center; }'
|
|
13049
13071
|
+ '.aptx-shortcuts .aptx-dialog-val { align-self:center; }'
|
|
13050
13072
|
+ '.aptx-keys { display:inline-flex; gap:3px; flex-wrap:wrap; }'
|
|
@@ -13199,7 +13221,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13199
13221
|
+ '.aptx-panel .aptx-seg:has(> input:checked) { background:var(--p-accent); color:#fff; }'
|
|
13200
13222
|
+ '.aptx-panel .aptx-seg:has(> input:disabled) { opacity:0.4; cursor:default; }'
|
|
13201
13223
|
+ '.aptx-panel .aptx-actions { margin-left:auto; display:flex; align-items:center; gap:5px; }'
|
|
13202
|
-
+ '.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; }'
|
|
13203
13225
|
+ '.aptx-panel .aptx-theme-btn:hover { background:var(--p-accent-weak); color:var(--p-accent-ink); border-color:var(--p-accent); }'
|
|
13204
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; }'
|
|
13205
13227
|
+ '.aptx-panel input[type=range]::-webkit-slider-runnable-track { height:4px; border-radius:999px; background:var(--p-line-strong); }'
|
|
@@ -13354,6 +13376,24 @@ function (root, d3, forester, phyloXml) {
|
|
|
13354
13376
|
let actions = document.createElement('div');
|
|
13355
13377
|
actions.className = 'aptx-actions';
|
|
13356
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
|
+
|
|
13357
13397
|
let progName = header.querySelector('.' + PROGNAMELINK);
|
|
13358
13398
|
if (progName) {
|
|
13359
13399
|
// Clicking the title used to navigate away to the project site;
|
|
@@ -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.
|
package/forester.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
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -3814,6 +3814,230 @@
|
|
|
3814
3814
|
};
|
|
3815
3815
|
|
|
3816
3816
|
|
|
3817
|
+
// Everything the Tree Properties dialog reports, gathered in ONE pass:
|
|
3818
|
+
// the desktop's TreeFacts.scan, minus its histogram (Christian, 2026-09-23:
|
|
3819
|
+
// "we don't need a histogram ... but a more detailed stats would be
|
|
3820
|
+
// useful, min, median, max, n, also type ... also need to include the
|
|
3821
|
+
// properties").
|
|
3822
|
+
//
|
|
3823
|
+
// Side-effect free and DOM-free, so it can be tested in Node -- the whole
|
|
3824
|
+
// reason it lives here rather than in the dialog that renders it, which is
|
|
3825
|
+
// the same split the desktop makes between TreeFacts and
|
|
3826
|
+
// TreePropertiesForm.
|
|
3827
|
+
//
|
|
3828
|
+
// Support values are grouped BY TYPE, because a tree can carry a bootstrap
|
|
3829
|
+
// and a posterior on the same branch and their ranges are not comparable
|
|
3830
|
+
// (0-100 against 0-1); averaging them together would produce a number that
|
|
3831
|
+
// describes neither. MAD values are excluded for the same reason they are
|
|
3832
|
+
// excluded from the Newick support slot: they rate a root position, not a
|
|
3833
|
+
// clade.
|
|
3834
|
+
forester.treeStatistics = function (phyOrNode) {
|
|
3835
|
+
// Callers hand this either a phylogeny wrapper or the root node, and
|
|
3836
|
+
// the difference is one extra internal node, one extra branch and one
|
|
3837
|
+
// more of depth -- small enough to read as plausible. getTreeRoot
|
|
3838
|
+
// normalizes both, as collectBasicTreeProperties does.
|
|
3839
|
+
let root = phyOrNode ? forester.getTreeRoot(phyOrNode) : null;
|
|
3840
|
+
let stats = {
|
|
3841
|
+
tips: 0, internal: 0, nodes: 0, branches: 0,
|
|
3842
|
+
polytomies: 0, maxChildren: 0,
|
|
3843
|
+
depth: 0, height: 0, heightKnown: false,
|
|
3844
|
+
collapsed: 0,
|
|
3845
|
+
branchLengths: null,
|
|
3846
|
+
support: [],
|
|
3847
|
+
ultrametric: false,
|
|
3848
|
+
tipsWithTaxonomy: 0, distinctTaxonomies: 0, tipsWithTaxonomyId: 0,
|
|
3849
|
+
tipsWithSequence: 0, tipsWithMolSeq: 0, tipsWithDomains: 0,
|
|
3850
|
+
nodesWithDate: 0, tipsWithDistribution: 0, namedInternal: 0,
|
|
3851
|
+
duplications: 0, speciations: 0, nodesWithEvents: 0,
|
|
3852
|
+
propertyRefs: []
|
|
3853
|
+
};
|
|
3854
|
+
if (!root) {
|
|
3855
|
+
return stats;
|
|
3856
|
+
}
|
|
3857
|
+
let lengths = [];
|
|
3858
|
+
let zeroLengths = 0;
|
|
3859
|
+
let negativeLengths = 0;
|
|
3860
|
+
let byType = Object.create(null);
|
|
3861
|
+
let taxa = Object.create(null);
|
|
3862
|
+
let refs = Object.create(null);
|
|
3863
|
+
let refOrder = [];
|
|
3864
|
+
let tipDepths = [];
|
|
3865
|
+
let maxDepth = 0;
|
|
3866
|
+
|
|
3867
|
+
(function walk(n, depth, distance) {
|
|
3868
|
+
++stats.nodes;
|
|
3869
|
+
let kids = n.children || [];
|
|
3870
|
+
if (depth > maxDepth) {
|
|
3871
|
+
maxDepth = depth;
|
|
3872
|
+
}
|
|
3873
|
+
if (n !== root) {
|
|
3874
|
+
++stats.branches;
|
|
3875
|
+
let b = n.branch_length;
|
|
3876
|
+
if (typeof b === 'number' && isFinite(b)) {
|
|
3877
|
+
lengths.push(b);
|
|
3878
|
+
if (b === 0) {
|
|
3879
|
+
++zeroLengths;
|
|
3880
|
+
} else if (b < 0) {
|
|
3881
|
+
++negativeLengths;
|
|
3882
|
+
}
|
|
3883
|
+
distance += b;
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
// support, by type, MAD left out
|
|
3887
|
+
if (n.confidences) {
|
|
3888
|
+
for (let i = 0; i < n.confidences.length; ++i) {
|
|
3889
|
+
let c = n.confidences[i];
|
|
3890
|
+
if (c.type === forester.MAD_CONFIDENCE_TYPE) {
|
|
3891
|
+
continue;
|
|
3892
|
+
}
|
|
3893
|
+
if (typeof c.value !== 'number' || !isFinite(c.value)) {
|
|
3894
|
+
continue;
|
|
3895
|
+
}
|
|
3896
|
+
let key = c.type || '';
|
|
3897
|
+
if (!byType[key]) {
|
|
3898
|
+
byType[key] = [];
|
|
3899
|
+
stats.support.push({type: key, values: byType[key]});
|
|
3900
|
+
}
|
|
3901
|
+
byType[key].push(c.value);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
if (n.date && typeof n.date.value === 'number' && isFinite(n.date.value)) {
|
|
3905
|
+
++stats.nodesWithDate;
|
|
3906
|
+
}
|
|
3907
|
+
if (n.properties) {
|
|
3908
|
+
let seen = Object.create(null);
|
|
3909
|
+
for (let i = 0; i < n.properties.length; ++i) {
|
|
3910
|
+
let ref = n.properties[i] && n.properties[i].ref;
|
|
3911
|
+
if (!ref || seen[ref]) {
|
|
3912
|
+
continue; // a node carrying a ref twice counts once
|
|
3913
|
+
}
|
|
3914
|
+
seen[ref] = true;
|
|
3915
|
+
if (refs[ref] === undefined) {
|
|
3916
|
+
refs[ref] = 0;
|
|
3917
|
+
refOrder.push(ref);
|
|
3918
|
+
}
|
|
3919
|
+
++refs[ref];
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
if (n.events) {
|
|
3923
|
+
++stats.nodesWithEvents;
|
|
3924
|
+
if (n.events.duplications > 0) {
|
|
3925
|
+
stats.duplications += n.events.duplications;
|
|
3926
|
+
}
|
|
3927
|
+
if (n.events.speciations > 0) {
|
|
3928
|
+
stats.speciations += n.events.speciations;
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
if (kids.length > 0) {
|
|
3932
|
+
++stats.internal;
|
|
3933
|
+
if (kids.length > stats.maxChildren) {
|
|
3934
|
+
stats.maxChildren = kids.length;
|
|
3935
|
+
}
|
|
3936
|
+
if (kids.length > 2) {
|
|
3937
|
+
++stats.polytomies;
|
|
3938
|
+
}
|
|
3939
|
+
if (n !== root && n.name) {
|
|
3940
|
+
++stats.namedInternal;
|
|
3941
|
+
}
|
|
3942
|
+
for (let i = 0; i < kids.length; ++i) {
|
|
3943
|
+
walk(kids[i], depth + 1, distance);
|
|
3944
|
+
}
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
++stats.tips;
|
|
3948
|
+
tipDepths.push(distance);
|
|
3949
|
+
if (n.taxonomies && n.taxonomies.length > 0) {
|
|
3950
|
+
++stats.tipsWithTaxonomy;
|
|
3951
|
+
let t = n.taxonomies[0];
|
|
3952
|
+
let label = t.scientific_name || t.code || t.common_name || '';
|
|
3953
|
+
if (label) {
|
|
3954
|
+
taxa[label] = true;
|
|
3955
|
+
}
|
|
3956
|
+
if (t.id && t.id.value) {
|
|
3957
|
+
++stats.tipsWithTaxonomyId;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
if (n.sequences && n.sequences.length > 0) {
|
|
3961
|
+
++stats.tipsWithSequence;
|
|
3962
|
+
let q = n.sequences[0];
|
|
3963
|
+
if (q.mol_seq && q.mol_seq.value) {
|
|
3964
|
+
++stats.tipsWithMolSeq;
|
|
3965
|
+
}
|
|
3966
|
+
if (forester.domainArchitectureOf(n)) {
|
|
3967
|
+
++stats.tipsWithDomains;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
if (n.distributions && n.distributions.length > 0) {
|
|
3971
|
+
++stats.tipsWithDistribution;
|
|
3972
|
+
}
|
|
3973
|
+
})(root, 0, 0);
|
|
3974
|
+
|
|
3975
|
+
stats.depth = maxDepth;
|
|
3976
|
+
stats.distinctTaxonomies = Object.keys(taxa).length;
|
|
3977
|
+
stats.propertyRefs = refOrder.map(function (r) {
|
|
3978
|
+
return {ref: r, nodes: refs[r]};
|
|
3979
|
+
});
|
|
3980
|
+
stats.branchLengths = forester.describeValues(lengths);
|
|
3981
|
+
if (stats.branchLengths) {
|
|
3982
|
+
stats.branchLengths.zero = zeroLengths;
|
|
3983
|
+
stats.branchLengths.negative = negativeLengths;
|
|
3984
|
+
}
|
|
3985
|
+
stats.support = stats.support.map(function (g) {
|
|
3986
|
+
let d = forester.describeValues(g.values);
|
|
3987
|
+
d.type = g.type;
|
|
3988
|
+
return d;
|
|
3989
|
+
}).filter(function (d) {
|
|
3990
|
+
return d.n > 0;
|
|
3991
|
+
});
|
|
3992
|
+
if (tipDepths.length > 0) {
|
|
3993
|
+
// running, not Math.min.apply: the spread of 150k tip depths
|
|
3994
|
+
// overflows the call stack, as timeAxisInfo found before this
|
|
3995
|
+
let lo = Infinity;
|
|
3996
|
+
let hi = -Infinity;
|
|
3997
|
+
for (let i = 0; i < tipDepths.length; ++i) {
|
|
3998
|
+
if (tipDepths[i] < lo) {
|
|
3999
|
+
lo = tipDepths[i];
|
|
4000
|
+
}
|
|
4001
|
+
if (tipDepths[i] > hi) {
|
|
4002
|
+
hi = tipDepths[i];
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
stats.height = hi;
|
|
4006
|
+
stats.heightKnown = lengths.length > 0;
|
|
4007
|
+
// ultrametric: every tip the same distance from the root, to a
|
|
4008
|
+
// tolerance scaled to the tree, since branch lengths are floats
|
|
4009
|
+
stats.ultrametric = stats.heightKnown && (hi - lo) <= (Math.max(hi, 1e-12) * 1e-6);
|
|
4010
|
+
}
|
|
4011
|
+
return stats;
|
|
4012
|
+
};
|
|
4013
|
+
|
|
4014
|
+
// n, minimum, median, maximum, mean and sum of a list of numbers, or null
|
|
4015
|
+
// when there are none. The median is the middle value of the sorted list,
|
|
4016
|
+
// or the mean of the two middle ones -- taken on a COPY, because sorting
|
|
4017
|
+
// the caller's array would reorder data it still owns.
|
|
4018
|
+
forester.describeValues = function (values) {
|
|
4019
|
+
if (!values || values.length === 0) {
|
|
4020
|
+
return null;
|
|
4021
|
+
}
|
|
4022
|
+
let v = values.slice().sort(function (a, b) {
|
|
4023
|
+
return a - b;
|
|
4024
|
+
});
|
|
4025
|
+
let n = v.length;
|
|
4026
|
+
let sum = 0;
|
|
4027
|
+
for (let i = 0; i < n; ++i) {
|
|
4028
|
+
sum += v[i];
|
|
4029
|
+
}
|
|
4030
|
+
let mid = Math.floor(n / 2);
|
|
4031
|
+
return {
|
|
4032
|
+
n: n,
|
|
4033
|
+
min: v[0],
|
|
4034
|
+
max: v[n - 1],
|
|
4035
|
+
median: (n % 2 === 1) ? v[mid] : ((v[mid - 1] + v[mid]) / 2),
|
|
4036
|
+
mean: sum / n,
|
|
4037
|
+
sum: sum
|
|
4038
|
+
};
|
|
4039
|
+
};
|
|
4040
|
+
|
|
3817
4041
|
forester.calcMaxBranchLength = function (node) {
|
|
3818
4042
|
let max = 0;
|
|
3819
4043
|
forester.preOrderTraversalAll(node, function (n) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"description": "Archaeopteryx.js is a software tool for the visualization and analysis of highly annotated phylogenetic trees.",
|
|
5
5
|
"main": "archaeopteryx.js",
|
|
6
6
|
"types": "archaeopteryx.d.ts",
|