archaeopteryx 3.23.0 → 3.25.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 +87 -36
- package/archaeopteryx.d.ts +12 -4
- package/archaeopteryx.js +121 -144
- package/forester.js +131 -22
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ config key live and shows the exact config JSON to copy into your own
|
|
|
25
25
|
`launch()` call.
|
|
26
26
|
|
|
27
27
|
* [Auspice / Nextstrain JSON (Time | Div, Clock plot)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=auspice)
|
|
28
|
-
* [Swine H1 HA1 + alignment (Nexus)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=swh1)
|
|
28
|
+
* [Swine H1 HA1 + alignment (Nexus; Clock plot from the years in the names)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=swh1)
|
|
29
29
|
* [BEAST annotations (Nexus)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=beast)
|
|
30
30
|
* [Flavivirus mature peptides (10 trees)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=flavivirus)
|
|
31
31
|
* [SARS-CoV-2 time tree (calendar)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=sarscov2)
|
|
@@ -64,23 +64,32 @@ Archaeopteryx.js has the following dependencies:
|
|
|
64
64
|
* d3.js (version 7): https://www.npmjs.com/package/d3
|
|
65
65
|
* sax.js (1.6.1): https://www.npmjs.com/package/sax/v/1.6.1
|
|
66
66
|
|
|
67
|
-
For **raster PNG export** (optional — the PNG entry
|
|
68
|
-
|
|
69
|
-
* canvg (4.x): https://www.npmjs.com/package/canvg
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
67
|
+
For **raster PNG export** (optional — the PNG entry, the Copy PNG button and
|
|
68
|
+
the PNG-scale row appear only when `window.Canvg` is present):
|
|
69
|
+
* canvg (4.x): https://www.npmjs.com/package/canvg. **Load it before
|
|
70
|
+
`launch()` is called:** the Download menu looks for it at click time, but
|
|
71
|
+
the Copy PNG button and the PNG-scale row are built with the control
|
|
72
|
+
panel at launch, so a page that provides it later gets a PNG entry and
|
|
73
|
+
nothing else. canvg publishes ES modules only (no classic-script global
|
|
74
|
+
build); this repository ships two ready-made files:
|
|
75
|
+
* `docs/lib/canvg.global.js` — a classic-script build for a plain
|
|
76
|
+
`<script src="canvg.global.js"></script>`, or for one more file in a
|
|
77
|
+
site's concatenated bundle; it defines `window.Canvg` itself. This is
|
|
78
|
+
what the demo pages use. (Generated from `canvg.js` by
|
|
79
|
+
`test/make_canvg_global.js`; a unit test keeps the two in step.)
|
|
80
|
+
* `docs/lib/canvg.js` — the self-contained ES module build (npm's
|
|
81
|
+
`canvg@4.0.3` bundled into one file via `esm.sh/canvg@4?bundle`, with
|
|
82
|
+
its one unnecessary Node-environment shim import removed — see the
|
|
83
|
+
file's own header), for a page that prefers a module bridge of its own:
|
|
84
|
+
```html
|
|
85
|
+
<script type="module">
|
|
86
|
+
import {Canvg} from 'http://path/to/canvg.js';
|
|
87
|
+
window.Canvg = Canvg;
|
|
88
|
+
</script>
|
|
89
|
+
```
|
|
90
|
+
A module script runs after the document is parsed, so a page that calls
|
|
91
|
+
`launch()` from an inline script during parsing must use the classic
|
|
92
|
+
build instead.
|
|
84
93
|
|
|
85
94
|
For **vector PDF export** (optional — the PDF entry appears in the Download
|
|
86
95
|
menu only when both are loaded before archaeopteryx.js):
|
|
@@ -933,11 +942,28 @@ ancestors' points, each at its own date and divergence; the panel opens
|
|
|
933
942
|
without them, since the line is the tips' and a deep root would crowd them
|
|
934
943
|
into a corner.
|
|
935
944
|
|
|
936
|
-
The button is offered on
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
945
|
+
The button is offered on two kinds of tree, whenever the line can be drawn
|
|
946
|
+
(three tips or more, not all on one date; a tree sampled at one moment has
|
|
947
|
+
no slope to estimate):
|
|
948
|
+
|
|
949
|
+
- a tree that has **Time | Div**: every node is dated and has a divergence,
|
|
950
|
+
so the ancestors can be shown too;
|
|
951
|
+
- a tree whose **branch lengths are its divergence and whose tips alone are
|
|
952
|
+
dated** — the tree a root-to-tip plot is run on *before* dating, TempEst's
|
|
953
|
+
case. A tip's divergence is the sum of the branch lengths from the root;
|
|
954
|
+
the plot has tips only (**Internal nodes** is not offered, since no
|
|
955
|
+
ancestor is dated); **Root date, in the tree** says `none`, since the tree
|
|
956
|
+
states no root date, and **Rate** is in the tree's own branch length unit,
|
|
957
|
+
where it states one. A tip's date is what the file states for it (a
|
|
958
|
+
phyloXML `<date>`, a `num_date` annotation) or, failing that, the date in
|
|
959
|
+
its **name**, as TempEst reads it: the rightmost date the label carries
|
|
960
|
+
(`A/swine/Iowa/A02524571/2020|1A.3.3.3` is 2020; a year alone is taken as
|
|
961
|
+
mid-year, 2020.5; a day/month-ambiguous `03/04/2020` is read day first),
|
|
962
|
+
and the panel's note says how many names it read and how many were
|
|
963
|
+
ambiguous. An ancestor is never dated by its name. Every tip must be
|
|
964
|
+
dated, by the file or by its name, and every branch must have a length: a
|
|
965
|
+
tree with a gap is not offered the plot, rather than plotted with tips
|
|
966
|
+
missing.
|
|
941
967
|
|
|
942
968
|
# forester.js
|
|
943
969
|
forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -1039,8 +1065,8 @@ Both entry points take **exactly** the arguments shown — a call with the old
|
|
|
1039
1065
|
trailing arguments (the separate settings bag, `nodeVisualizations`,
|
|
1040
1066
|
`nodeLabels`, `specialVisualizations`, or the positional Newick parse
|
|
1041
1067
|
options) **throws** with a message saying where each one went: everything now
|
|
1042
|
-
lives in the **one config object** (`
|
|
1043
|
-
`
|
|
1068
|
+
lives in the **one config object** (`internalNumericLabels` is a config key;
|
|
1069
|
+
`nodeLabels` is retired, see the deprecated keys below). `config` itself is optional — `archaeopteryx.launch('#phylogram1',
|
|
1044
1070
|
tree)` works.
|
|
1045
1071
|
|
|
1046
1072
|
`container` is a **CSS selector or the DOM element itself** (frameworks hand
|
|
@@ -1534,14 +1560,13 @@ copy-pastable JSON.
|
|
|
1534
1560
|
| `onViewChange` | `null` | `function (state, encoded)`, called once per settled redraw when the view changed: `state` as `getViewState()` returns it, `encoded` its hash-ready string. The demo pages write it into the URL. |
|
|
1535
1561
|
| `searchBinitialValue` | `null` | Prefill search box B. |
|
|
1536
1562
|
| `enableVisualizations` | `true` | Offer the Color / Shape visualizations (which fields they cover is decided from the tree). |
|
|
1537
|
-
| `initialVisualization` | `null` | The visualization to open with, by its Color-menu name (e.g. `'Host'`; case-insensitive). A name the tree cannot honour logs a console warning and falls back to the automatic choice, so a site-wide value is safe on trees without that field. Default: Archaeopteryx.js picks the most informative field itself. |
|
|
1563
|
+
| `initialVisualization` | `null` | The visualization to open with, by its Color-menu name (e.g. `'Host'`; case-insensitive). A name the tree cannot honour logs a console warning and falls back to the automatic choice, so a site-wide value is safe on trees without that field. A name the tree *can* honour is always honoured, but when it is a field the automatic choice would not have opened with -- more than 20 values, or a value on fewer than two thirds of the tips -- a console warning says so and names the field the viewer would have chosen. Default: Archaeopteryx.js picks the most informative field itself. |
|
|
1538
1564
|
| `visualizationsLegendXpos` | `254` | Legend position, x. |
|
|
1539
1565
|
| `visualizationsLegendYpos` | `30` | Legend position, y. |
|
|
1540
1566
|
| `enableDownloads` | `true` | Offer the download buttons. |
|
|
1541
1567
|
| `pngExportScale` | `4` | The PNG resolution the Download section starts on: 2, 4 or 8 (any other value is snapped to the nearest, with a console warning). |
|
|
1542
1568
|
| `nhExportWriteConfidences` | `true` | Write confidences into exported Newick. |
|
|
1543
1569
|
| `internalNumericLabels` | `'auto'` | Newick / Nexus parsing: how a bare numeric internal label (`)100:0.05`) is read. `'auto'` reads them as confidence values only when *every* internal label looks like support; `'confidence'` reads every numeric label as one, whatever its value; `'label'` keeps them as names. Replaces `nhConfidenceValuesAsInternalNames` (still accepted, with a warning; its `true` maps to `'confidence'`). |
|
|
1544
|
-
| `nodeLabels` | `null` | Custom label-field checkboxes: `{key: {label, description, propertyRef, showButton, selected}}` — each adds a panel checkbox labelling nodes with the named property's value. (Was `launch()`'s sixth positional argument.) |
|
|
1545
1570
|
| `enableSubtreeDeletion` | `true` | Offer node / subtree deletion in the node menu. |
|
|
1546
1571
|
| `enableAccessToDatabases` | `true` | Offer the “Access DB” link in the node menu. |
|
|
1547
1572
|
| `enableManualNodeSelection` | `false` | Add the Select/Deselect entries to the node menu. |
|
|
@@ -1564,7 +1589,7 @@ whatever it names.
|
|
|
1564
1589
|
|
|
1565
1590
|
### Accepted, with a warning
|
|
1566
1591
|
|
|
1567
|
-
|
|
1592
|
+
Five keys are neither current nor removed: they are accepted so an existing
|
|
1568
1593
|
embed keeps working, and warn on the console.
|
|
1569
1594
|
|
|
1570
1595
|
| Key | What happens |
|
|
@@ -1573,6 +1598,7 @@ embed keeps working, and warn on the console.
|
|
|
1573
1598
|
| `nhConfidenceValuesInBrackets` | Retired: ignored. It gated whether `[95]` is read as a confidence, but setting it `false` never reinterpreted the bracket — it *discarded* it, so the option's only effect was to throw support values away. A bracket that is not a number is a Newick comment and was ignored either way, and NHX / BEAST blobs go through a different path. Bracketed values are now always read as confidences. |
|
|
1574
1599
|
| `showTimeAxis` | Translated to `showScaleAxis`, value unchanged: the switch now also draws a distance axis on a tree that is not in time. An explicit `showScaleAxis` always wins. |
|
|
1575
1600
|
| `timeAxisGrid` | Translated to `showScaleGrid`, value unchanged. |
|
|
1601
|
+
| `nodeLabels` | Retired in 3.25.0: ignored, with a warning. The custom label checkboxes it declared predate the Metadata checkbox and its "Metadata fields…" chooser, which offer exactly the fields the tree carries; a fixed site-wide list put checkboxes for absent fields on every tree. `labelProperties` sets the starting fields where the automatic choice is not wanted. Removed in a later release. |
|
|
1576
1602
|
|
|
1577
1603
|
### What replaced the rest
|
|
1578
1604
|
|
|
@@ -1717,6 +1743,21 @@ All 118 of them, alphabetically:
|
|
|
1717
1743
|
| `visualizationsLegendXposOrig` | Internal bookkeeping; set visualizationsLegendXpos. |
|
|
1718
1744
|
| `visualizationsLegendYposOrig` | Internal bookkeeping; set visualizationsLegendYpos. |
|
|
1719
1745
|
|
|
1746
|
+
### Removed from `forester` — calling these throws `TypeError`
|
|
1747
|
+
|
|
1748
|
+
3.0.0 also dropped twelve exports of `forester.js` that the viewer no longer
|
|
1749
|
+
used. They are gone, not stubbed, so a 2.x embedding that calls one fails with
|
|
1750
|
+
`TypeError: forester.<name> is not a function` — inside its own parse step, if
|
|
1751
|
+
that is where it called it.
|
|
1752
|
+
|
|
1753
|
+
| Function | Why, and what to do instead |
|
|
1754
|
+
| --- | --- |
|
|
1755
|
+
| `collectPropertyRefs` | Nothing to call. The viewer derives the Color-by candidates (`forester.visualizationCandidates(tree)`, below) and the label-field chooser's list from the tree itself, and substitutes a readable name property (`genome_name`) for identifier-like tip names on its own. A 2.x embedding that built `nodeLabels` from this function's result should delete that block together with the `nodeLabels` key; `labelProperties` sets the starting label fields where the automatic choice is not wanted. |
|
|
1756
|
+
| `collapse`, `unCollapse` | The subtree-collapse feature was removed; they were its only writers. |
|
|
1757
|
+
| `getChildren` | Only meant something while collapse existed; read `node.children`. |
|
|
1758
|
+
| `findByTaxonomyCode`, `findByTaxonomyScientificName` | Superseded by the search (the panel's search fields). |
|
|
1759
|
+
| `calcAverageTreeHeight`, `calcMaxDepth`, `calcBranchLengthSimpleStatistics`, `removeMaxBranchLength`, `isHasNodeData`, `getOneDistinctTaxonomy` | Never used by the viewer; nothing replaces them. |
|
|
1760
|
+
|
|
1720
1761
|
## The automatic visualization system (developer spec)
|
|
1721
1762
|
|
|
1722
1763
|
This section, together with **Value grouping** below, is written to be
|
|
@@ -2599,15 +2640,25 @@ deliberate difference.)
|
|
|
2599
2640
|
### The clock plot
|
|
2600
2641
|
|
|
2601
2642
|
**Model** (`forester.js`, pure). `forester.clockPlotKind(phy)` answers
|
|
2602
|
-
`'divergence'` or `null`: the tree has
|
|
2603
|
-
(`forester.hasTimeAndDivergence`)
|
|
2604
|
-
|
|
2605
|
-
`
|
|
2606
|
-
|
|
2607
|
-
|
|
2643
|
+
`'divergence'`, `'root-to-tip'` or `null`: `'divergence'` where the tree has
|
|
2644
|
+
Time and Div (`forester.hasTimeAndDivergence`); `'root-to-tip'` where every
|
|
2645
|
+
tip is dated (by the file, or by the date in its name:
|
|
2646
|
+
`forester.parseTipLabelDate`), no ancestor is, every branch states a length
|
|
2647
|
+
and no branch states a clock rate (a clock-model tree's lengths are time) — and in
|
|
2648
|
+
both cases three tips or more, not all on one date. It is asked of the
|
|
2649
|
+
**tree**, never of the view. `forester.clockPlotData(phy, view)` returns
|
|
2650
|
+
`{kind, forward, unit, divUnit, fromRates, points, root, fit}`: a point
|
|
2608
2651
|
`{node, date, div, tip}` per node of the view (a tip also carries its
|
|
2609
|
-
`residual
|
|
2610
|
-
|
|
2652
|
+
`residual`; on a root-to-tip tree the tips only, their `div` the sum of the
|
|
2653
|
+
branch lengths from the tree's root), `forward` false where the dates are
|
|
2654
|
+
ages (on a root-to-tip tree: where their unit is geologic), `divUnit` the
|
|
2655
|
+
divergence's unit (`subs/site` on a Time | Div tree, a root-to-tip tree's
|
|
2656
|
+
stated branch length unit, or `null`), `fromRates` true where the divergence
|
|
2657
|
+
is time × clock rate, `calendar` true where the dates are calendar years
|
|
2658
|
+
(their unit says so, or they were read off the tips' names), `fromNames` and
|
|
2659
|
+
`ambiguousNames` how many tips a root-to-tip tree dates by their names and
|
|
2660
|
+
how many of those are day/month-ambiguous (read day first), `root.date`
|
|
2661
|
+
`null` on a root-to-tip tree, and `fit` =
|
|
2611
2662
|
`forester.clockRegression(dates, divergences)` over the tips — ordinary least
|
|
2612
2663
|
squares about the means, `{n, slope, intercept, r2}` — plus `rate` (the slope
|
|
2613
2664
|
in the direction time runs) and `rootDate` (where the line reaches the view
|
package/archaeopteryx.d.ts
CHANGED
|
@@ -29,9 +29,13 @@ export interface Phylogeny extends PhylogenyNode {
|
|
|
29
29
|
|
|
30
30
|
export type Layout = 'rectangular' | 'circular' | 'unrooted';
|
|
31
31
|
|
|
32
|
-
/**
|
|
32
|
+
/** @deprecated Retired in 3.25.0 with `nodeLabels`: accepted with a console
|
|
33
|
+
* warning, no effect; removed in a later release. The Metadata checkbox and
|
|
34
|
+
* its "Metadata fields…" chooser offer the tree's own fields.
|
|
33
35
|
*
|
|
34
|
-
*
|
|
36
|
+
* One custom label-field entry (the config's nodeLabels values).
|
|
37
|
+
*
|
|
38
|
+
* Two modes (3.0.0 to 3.24.0):
|
|
35
39
|
* - a PANEL CHECKBOX needs label, description AND propertyRef set, plus
|
|
36
40
|
* showButton: true -- omit showButton (or any of the three) and no
|
|
37
41
|
* checkbox appears;
|
|
@@ -85,6 +89,10 @@ export interface ArchaeopteryxConfig {
|
|
|
85
89
|
nhConfidenceValuesAsInternalNames?: boolean;
|
|
86
90
|
nhConfidenceValuesInBrackets?: boolean;
|
|
87
91
|
nhExportWriteConfidences?: boolean;
|
|
92
|
+
/** @deprecated Retired in 3.25.0: accepted with a console warning, no
|
|
93
|
+
* effect; removed in a later release. The Metadata checkbox and its
|
|
94
|
+
* "Metadata fields…" chooser offer the tree's own fields; labelProperties
|
|
95
|
+
* sets the starting fields. */
|
|
88
96
|
nodeLabels?: Record<string, NodeLabelSpec> | null;
|
|
89
97
|
/** Called once per settled redraw when the view changed: the state as
|
|
90
98
|
* getViewState() returns it, and its hash-ready string. */
|
|
@@ -176,8 +184,8 @@ export interface ViewState {
|
|
|
176
184
|
shapeBy?: string;
|
|
177
185
|
/** The panel's checked boxes: name, taxonomy, sequence, confidence,
|
|
178
186
|
* madValues, branchLength, external, internal, nodeEvents, branchEvents,
|
|
179
|
-
* supportDots, shortNames, autoHide, visualizations, visualStyles
|
|
180
|
-
* custom:<key>
|
|
187
|
+
* supportDots, shortNames, autoHide, visualizations, visualStyles. (A
|
|
188
|
+
* custom:<key> flag from a 3.24.0 hash is read and ignored.) */
|
|
181
189
|
show?: string[];
|
|
182
190
|
font?: number;
|
|
183
191
|
node?: number;
|
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.25.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.25.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';
|
|
@@ -539,7 +539,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
539
539
|
let _container = null; // the resolved container ELEMENT (never a selector string)
|
|
540
540
|
let _intervalId = 0;
|
|
541
541
|
let _maxLabelLength = 0;
|
|
542
|
-
let _nodeLabels = null;
|
|
543
542
|
let _state = null; // live display state: what the control panel writes to
|
|
544
543
|
let _root = null;
|
|
545
544
|
let _root_const = null;
|
|
@@ -606,6 +605,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
606
605
|
const DOMAIN_SCALE_HEADROOM = 0.9; // drawn length over reserved length
|
|
607
606
|
const DOMAIN_BOX_MIN_H = 6;
|
|
608
607
|
const DOMAIN_BOX_MAX_H = 16;
|
|
608
|
+
const LEGEND_MAX_ROWS = 20; // value rows a legend lists before "n more"; the rest fold
|
|
609
609
|
const DOMAIN_BACKBONE_COLOR = 'rgb(100,100,100)'; // the same in both themes and in exports
|
|
610
610
|
const DOMAIN_SHADOW_COLOR = 'rgb(8,18,21)';
|
|
611
611
|
const DOMAIN_SHADOWS = [[0.4, 0.7, 40], [0.9, 1.5, 28], [1.6, 2.5, 17]]; // dx, dy, alpha of 255, in drawing order
|
|
@@ -1948,7 +1948,32 @@ function (root, d3, forester, phyloXml) {
|
|
|
1948
1948
|
// an explicit request is honoured even for a wide field (21+
|
|
1949
1949
|
// values) the automatic path would only offer, never impose;
|
|
1950
1950
|
// autoColorId too, so it survives subtree views like the
|
|
1951
|
-
// automatic pick does
|
|
1951
|
+
// automatic pick does.
|
|
1952
|
+
//
|
|
1953
|
+
// Honoured, but not in silence. A request for a field the
|
|
1954
|
+
// automatic choice would not open with -- wide, or sparse (on
|
|
1955
|
+
// fewer than 2/3 of the tips; it opens only when nothing denser
|
|
1956
|
+
// exists) -- says so in the console and names the field the
|
|
1957
|
+
// viewer would have chosen. BV-BRC's virus trees carried a
|
|
1958
|
+
// site-wide 'Host' from the influenza days: family trees opened
|
|
1959
|
+
// coloured by 39 to 168 hosts while Genus (4 to 15 values) sat
|
|
1960
|
+
// in the file, and nothing anywhere said the viewer had been
|
|
1961
|
+
// overruled (Christian, 2026-10-08: a console warning when a
|
|
1962
|
+
// sub-optimal visualization is forced is needed). A request for
|
|
1963
|
+
// a clean field that merely ranks below the automatic pick is a
|
|
1964
|
+
// site's taste and warns nothing; nor does a sparse field that
|
|
1965
|
+
// is the automatic pick itself.
|
|
1966
|
+
let auto = forester.openingVisualization(_vis.candidates);
|
|
1967
|
+
if ((requested.wide || requested.sparse) && !(auto && auto.id === requested.id)) {
|
|
1968
|
+
let why = requested.wide
|
|
1969
|
+
? requested.values.length + ' distinct values on this tree, more than the '
|
|
1970
|
+
+ LEGEND_MAX_ROWS + ' the legend lists'
|
|
1971
|
+
: 'a value on ' + requested.coverage + ' of the tree\'s ' + requested.total + ' tips';
|
|
1972
|
+
console.warn(WARNING + ': initialVisualization "' + _settings.initialVisualization
|
|
1973
|
+
+ '" names a field with ' + why + '; left to itself the viewer would '
|
|
1974
|
+
+ (auto ? 'open with "' + auto.label + '" (' + auto.values.length + ' values)'
|
|
1975
|
+
: 'open uncoloured'));
|
|
1976
|
+
}
|
|
1952
1977
|
_vis.autoColorId = requested.id;
|
|
1953
1978
|
_vis.colorId = requested.id;
|
|
1954
1979
|
_state.showVisualizations = true;
|
|
@@ -2144,6 +2169,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
2144
2169
|
}
|
|
2145
2170
|
dialog.addEventListener('close', close);
|
|
2146
2171
|
|
|
2172
|
+
let placed = false; // set by place() once the picker has been put somewhere
|
|
2147
2173
|
render(false);
|
|
2148
2174
|
dialog.show();
|
|
2149
2175
|
place();
|
|
@@ -2151,40 +2177,67 @@ function (root, d3, forester, phyloXml) {
|
|
|
2151
2177
|
// Beside the legend it edits, never on top of it -- a picker that hides
|
|
2152
2178
|
// the swatch it is changing defeats the live preview.
|
|
2153
2179
|
//
|
|
2154
|
-
//
|
|
2155
|
-
//
|
|
2156
|
-
//
|
|
2157
|
-
//
|
|
2158
|
-
//
|
|
2159
|
-
//
|
|
2160
|
-
//
|
|
2180
|
+
// Placed OUTRIGHT the first time, viewport-fixed. A non-modal <dialog>
|
|
2181
|
+
// appended to the body sits at its static position, the end of the
|
|
2182
|
+
// document: on the demo page that is the top of the window (the
|
|
2183
|
+
// container is out of flow), on a page that flows, such as BV-BRC's,
|
|
2184
|
+
// it is below everything on it. Until 2026-10-08 the picker moved
|
|
2185
|
+
// only when that spot overlapped the card, so on BV-BRC it opened
|
|
2186
|
+
// under the site's own bars, off the bottom of the window
|
|
2187
|
+
// (Christian's screenshot). Fixed positioning also keeps a scrolling
|
|
2188
|
+
// page from carrying it away.
|
|
2189
|
+
//
|
|
2190
|
+
// Re-measured rather than remembered afterwards, for two reasons:
|
|
2191
|
+
// every redraw DESTROYS and rebuilds the legend group, so a held
|
|
2192
|
+
// reference goes stale and measures zero; and the card grows when the
|
|
2193
|
+
// [reset colors] chip appears on the first pick, which would leave a
|
|
2194
|
+
// positioned-once picker sitting over the counts. Once placed it only
|
|
2195
|
+
// MOVES when it would otherwise overlap, so the dialog does not chase
|
|
2196
|
+
// the legend around while the user drags.
|
|
2161
2197
|
function place() {
|
|
2162
2198
|
let el = anchorClass ? document.querySelector('g.' + anchorClass) : null;
|
|
2163
2199
|
let box = el ? el.getBoundingClientRect() : null;
|
|
2164
|
-
if (
|
|
2165
|
-
|
|
2200
|
+
if (box && !box.width) {
|
|
2201
|
+
box = null;
|
|
2166
2202
|
}
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2203
|
+
if (placed) {
|
|
2204
|
+
if (!box) {
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
let now = dialog.getBoundingClientRect();
|
|
2208
|
+
let overlaps = now.width > 0
|
|
2209
|
+
&& !(now.left >= box.right || now.right <= box.left
|
|
2210
|
+
|| now.top >= box.bottom || now.bottom <= box.top);
|
|
2211
|
+
if (!overlaps) {
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2173
2214
|
}
|
|
2174
2215
|
let w = dialog.offsetWidth;
|
|
2175
2216
|
let h = dialog.offsetHeight;
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2217
|
+
let left;
|
|
2218
|
+
let top;
|
|
2219
|
+
if (box) {
|
|
2220
|
+
// to the right of the card, or to its left when that would
|
|
2221
|
+
// leave the viewport
|
|
2222
|
+
left = box.right + 10;
|
|
2223
|
+
if (left + w > window.innerWidth - 6) {
|
|
2224
|
+
left = box.left - w - 10;
|
|
2225
|
+
}
|
|
2226
|
+
top = box.top;
|
|
2227
|
+
} else {
|
|
2228
|
+
// no card to sit beside (the view has no swatch for the value):
|
|
2229
|
+
// the tree area's top left corner, inside the window
|
|
2230
|
+
let area = _container ? _container.getBoundingClientRect() : null;
|
|
2231
|
+
left = area ? area.left + 14 : 10;
|
|
2232
|
+
top = area ? Math.max(area.top, 0) + 14 : 10;
|
|
2181
2233
|
}
|
|
2182
2234
|
setStyles(dialog, {
|
|
2183
2235
|
'position': 'fixed',
|
|
2184
2236
|
'margin': '0',
|
|
2185
2237
|
'left': Math.max(6, Math.min(window.innerWidth - w - 6, left)) + 'px',
|
|
2186
|
-
'top': Math.max(6, Math.min(window.innerHeight - h - 6,
|
|
2238
|
+
'top': Math.max(6, Math.min(window.innerHeight - h - 6, top)) + 'px'
|
|
2187
2239
|
});
|
|
2240
|
+
placed = true;
|
|
2188
2241
|
}
|
|
2189
2242
|
setTimeout(function () {
|
|
2190
2243
|
document.addEventListener('pointerdown', onOutside, true);
|
|
@@ -2521,7 +2574,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
2521
2574
|
// is then just its "no value" row
|
|
2522
2575
|
const isRange = kind === 'color' && colorModeOf(vis) === 'range' && vis.values.length > 0;
|
|
2523
2576
|
|
|
2524
|
-
const LEGEND_MAX_ROWS = 20;
|
|
2525
2577
|
let rows = [];
|
|
2526
2578
|
let hidden = 0;
|
|
2527
2579
|
if (!isRange) {
|
|
@@ -4725,25 +4777,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
4725
4777
|
l = append(l, labelPropertiesOf(phynode));
|
|
4726
4778
|
}
|
|
4727
4779
|
|
|
4728
|
-
if (_nodeLabels && phynode.properties) {
|
|
4729
|
-
const props_length = phynode.properties.length;
|
|
4730
|
-
if (props_length > 0) {
|
|
4731
|
-
for (const value of Object.values(_nodeLabels)) {
|
|
4732
|
-
if (value.selected === true && value.propertyRef) {
|
|
4733
|
-
let prop_text = '';
|
|
4734
|
-
for (let pm = 0; pm < props_length; ++pm) {
|
|
4735
|
-
if (phynode.properties[pm].ref === value.propertyRef && phynode.properties[pm].datatype === 'xsd:string' && forester.isNodeScopedProperty(phynode.properties[pm])) {
|
|
4736
|
-
if (prop_text.length > 0) {
|
|
4737
|
-
prop_text += ', '
|
|
4738
|
-
}
|
|
4739
|
-
prop_text += phynode.properties[pm].value;
|
|
4740
|
-
}
|
|
4741
|
-
}
|
|
4742
|
-
l = append(l, prop_text);
|
|
4743
|
-
}
|
|
4744
|
-
}
|
|
4745
|
-
}
|
|
4746
|
-
}
|
|
4747
4780
|
|
|
4748
4781
|
|
|
4749
4782
|
if (_state.showDistributions && phynode.distributions && phynode.distributions.length > 0) {
|
|
@@ -5522,6 +5555,21 @@ function (root, d3, forester, phyloXml) {
|
|
|
5522
5555
|
return v;
|
|
5523
5556
|
},
|
|
5524
5557
|
note: 'the grid follows the scale axis, time or distance'
|
|
5558
|
+
},
|
|
5559
|
+
// Retired in 3.25.0 (Christian, 2026-10-08: "go with the deprecation").
|
|
5560
|
+
// The custom label checkboxes it declared -- a site's fixed list of
|
|
5561
|
+
// property refs, one panel checkbox each, labelling nodes with the
|
|
5562
|
+
// property's value -- predate the Metadata checkbox and its
|
|
5563
|
+
// "Metadata fields…" chooser, which offer exactly the fields the tree
|
|
5564
|
+
// carries and replace identifier-like tip names with a name property
|
|
5565
|
+
// on their own. On BV-BRC the list, written for influenza, put eight
|
|
5566
|
+
// checkboxes on every virus tree, six of them for fields the tree
|
|
5567
|
+
// did not have. The checkbox code went with the key (the label
|
|
5568
|
+
// text, the panel rows, the view state's custom:<key> flags, which
|
|
5569
|
+
// an old hash may still carry and the reader now ignores). Accepted
|
|
5570
|
+
// with this warning for one release cycle, then REMOVED_CONFIG.
|
|
5571
|
+
nodeLabels: {
|
|
5572
|
+
note: 'the Metadata checkbox and its "Metadata fields…" chooser offer the tree\'s own fields; labelProperties sets the starting fields'
|
|
5525
5573
|
}
|
|
5526
5574
|
};
|
|
5527
5575
|
|
|
@@ -5721,7 +5769,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
5721
5769
|
'internalNumericLabels',
|
|
5722
5770
|
'ladderizeTree',
|
|
5723
5771
|
'nhExportWriteConfidences',
|
|
5724
|
-
'nodeLabels',
|
|
5725
5772
|
'onViewChange',
|
|
5726
5773
|
'panelDensity',
|
|
5727
5774
|
'pngExportScale',
|
|
@@ -6321,19 +6368,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
6321
6368
|
} else if (typeof _settings.onViewChange !== 'function') {
|
|
6322
6369
|
throw new Error(ERROR + '"onViewChange" must be a function (state, encoded) or null');
|
|
6323
6370
|
}
|
|
6324
|
-
if (_settings.nodeLabels === undefined) {
|
|
6325
|
-
_settings.nodeLabels = null;
|
|
6326
|
-
} else if (_settings.nodeLabels !== null && typeof _settings.nodeLabels !== 'object') {
|
|
6327
|
-
throw new Error('nodeLabels must be an object (see the README) or null');
|
|
6328
|
-
}
|
|
6329
|
-
if (_settings.nodeLabels) {
|
|
6330
|
-
// a DEEP copy: the viewer writes runtime state into these specs
|
|
6331
|
-
// (the checkbox element id, the live checked state), and the
|
|
6332
|
-
// caller's config object must stay declarative -- an embedder
|
|
6333
|
-
// reusing one nodeLabels constant across launches must not
|
|
6334
|
-
// inherit the previous session's checkbox state
|
|
6335
|
-
_settings.nodeLabels = JSON.parse(JSON.stringify(_settings.nodeLabels));
|
|
6336
|
-
}
|
|
6337
6371
|
if (_settings.pngExportScale === undefined) {
|
|
6338
6372
|
_settings.pngExportScale = 4;
|
|
6339
6373
|
} else if (typeof _settings.pngExportScale !== 'number'
|
|
@@ -6593,7 +6627,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6593
6627
|
+ ' trailing arguments were removed. The separate settings bag merged into the one'
|
|
6594
6628
|
+ ' config object; "nodeVisualizations" and "specialVisualizations" are gone for good'
|
|
6595
6629
|
+ ' (visualizations are derived automatically from the tree itself); "nodeLabels"'
|
|
6596
|
-
+ '
|
|
6630
|
+
+ ' is retired (the Metadata checkbox offers the tree\'s own fields).');
|
|
6597
6631
|
}
|
|
6598
6632
|
if (phylo === undefined || phylo === null) {
|
|
6599
6633
|
throw new Error(ERROR + 'input tree is undefined or null');
|
|
@@ -6684,9 +6718,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
6684
6718
|
.on('zoom', zoom);
|
|
6685
6719
|
_basicTreeProperties = forester.collectBasicTreeProperties(_treeData);
|
|
6686
6720
|
|
|
6687
|
-
// Every launch starts from a clean slate: _vis is rebuilt below
|
|
6688
|
-
//
|
|
6689
|
-
// the same page.
|
|
6721
|
+
// Every launch starts from a clean slate: _vis is rebuilt below, never
|
|
6722
|
+
// inherited from a previous launch in the same page.
|
|
6690
6723
|
_radialRotation = 0;
|
|
6691
6724
|
_radialLabelsHorizontal = false;
|
|
6692
6725
|
_msaColOffset = 0;
|
|
@@ -6704,7 +6737,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
6704
6737
|
// candidate ranking over 13k tips before the card could even paint.
|
|
6705
6738
|
checkLayoutValue(cfg.state ? cfg.state.layout : undefined);
|
|
6706
6739
|
initializeSettings(cfg.settings);
|
|
6707
|
-
_nodeLabels = _settings.nodeLabels || null;
|
|
6708
6740
|
|
|
6709
6741
|
// whatever a previous launch left in this container (or in the
|
|
6710
6742
|
// container before it) goes now, so the old tree does not sit under
|
|
@@ -9896,20 +9928,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
9896
9928
|
return found;
|
|
9897
9929
|
}
|
|
9898
9930
|
|
|
9899
|
-
// The config's custom label checkboxes (nodeLabels) that are offered
|
|
9900
|
-
function customLabelEntries() {
|
|
9901
|
-
let out = [];
|
|
9902
|
-
if (_nodeLabels) {
|
|
9903
|
-
Object.keys(_nodeLabels).forEach(function (key) {
|
|
9904
|
-
let v = _nodeLabels[key];
|
|
9905
|
-
if (v && v.label && v.showButton === true && v.propertyRef && v.description) {
|
|
9906
|
-
out.push({key: key, entry: v});
|
|
9907
|
-
}
|
|
9908
|
-
});
|
|
9909
|
-
}
|
|
9910
|
-
return out;
|
|
9911
|
-
}
|
|
9912
|
-
|
|
9913
9931
|
function searchStateOf(idx) {
|
|
9914
9932
|
let spec = currentSearchSpec(idx);
|
|
9915
9933
|
let value = (spec.value === null || spec.value === undefined) ? '' : String(spec.value);
|
|
@@ -9957,11 +9975,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
9957
9975
|
}).map(function (f) {
|
|
9958
9976
|
return f[0];
|
|
9959
9977
|
});
|
|
9960
|
-
customLabelEntries().forEach(function (c) {
|
|
9961
|
-
if (c.entry.selected === true) {
|
|
9962
|
-
s.show.push('custom:' + c.key);
|
|
9963
|
-
}
|
|
9964
|
-
});
|
|
9965
9978
|
s.font = _state.externalNodeFontSize;
|
|
9966
9979
|
s.node = _state.nodeSizeDefault;
|
|
9967
9980
|
s.branch = _state.branchWidthDefault;
|
|
@@ -10107,9 +10120,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
10107
10120
|
VIEW_SHOW_FLAGS.forEach(function (f) {
|
|
10108
10121
|
_state[f[2]] = on.has(f[0]);
|
|
10109
10122
|
});
|
|
10110
|
-
customLabelEntries().forEach(function (c) {
|
|
10111
|
-
c.entry.selected = on.has('custom:' + c.key);
|
|
10112
|
-
});
|
|
10113
10123
|
}
|
|
10114
10124
|
if (isFinite(s.font)) {
|
|
10115
10125
|
setFontSizes(Number(s.font));
|
|
@@ -10259,11 +10269,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
10259
10269
|
VIEW_SHOW_FLAGS.forEach(function (f) {
|
|
10260
10270
|
setCheckboxValue(f[1], _state[f[2]] === true);
|
|
10261
10271
|
});
|
|
10262
|
-
customLabelEntries().forEach(function (c) {
|
|
10263
|
-
if (c.entry.cb_id) {
|
|
10264
|
-
setCheckboxValue(c.entry.cb_id, c.entry.selected === true);
|
|
10265
|
-
}
|
|
10266
|
-
});
|
|
10267
10272
|
setCheckboxValue(MSA_CB, _state.showMsa === true);
|
|
10268
10273
|
setCheckboxValue(MSA_LOGO_CB, _state.showMsaLogo === true);
|
|
10269
10274
|
setCheckboxValue(HEATMAP_CB, _state.showHeatmap === true);
|
|
@@ -11171,10 +11176,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
11171
11176
|
row.appendChild(input);
|
|
11172
11177
|
row.appendChild(span);
|
|
11173
11178
|
options.appendChild(row);
|
|
11174
|
-
return
|
|
11179
|
+
return row;
|
|
11175
11180
|
};
|
|
11176
11181
|
check('Regression line', 'line', 'to show/hide the least-squares line through the tips');
|
|
11177
|
-
|
|
11182
|
+
// on a root-to-tip tree no ancestor is dated: nothing to show, so the
|
|
11183
|
+
// choice is not offered (layClockPlot hides it by the data's kind)
|
|
11184
|
+
let internalRow = check('Internal nodes', 'internal', 'to show/hide the internal nodes: their dates were inferred,'
|
|
11178
11185
|
+ ' and they take no part in the line');
|
|
11179
11186
|
let deselect = document.createElement('button');
|
|
11180
11187
|
deselect.type = 'button';
|
|
@@ -11197,7 +11204,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11197
11204
|
body.appendChild(note);
|
|
11198
11205
|
|
|
11199
11206
|
_clockPlot = {dialog: shell.dialog, svg: svg, axes: axes, internal: internal, line: line, tips: tips,
|
|
11200
|
-
ring: ring, box: box, stats: stats, note: note, deselect: deselect,
|
|
11207
|
+
ring: ring, box: box, stats: stats, note: note, deselect: deselect, internalRow: internalRow,
|
|
11201
11208
|
data: null, marks: [], seen: null, hover: null, lays: 0, x: null, y: null};
|
|
11202
11209
|
// the shell's close control, and anything else that closes the dialog
|
|
11203
11210
|
let closed = function () {
|
|
@@ -11408,7 +11415,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
11408
11415
|
}
|
|
11409
11416
|
|
|
11410
11417
|
function layClockPlot(cp, data) {
|
|
11411
|
-
let calendar = data.
|
|
11418
|
+
let calendar = data.calendar;
|
|
11419
|
+
let rootToTip = data.kind === 'root-to-tip';
|
|
11420
|
+
cp.internalRow.style.display = rootToTip ? 'none' : '';
|
|
11412
11421
|
let shown = data.points.filter(function (p) {
|
|
11413
11422
|
return p.tip || _clockPlotOptions.internal;
|
|
11414
11423
|
});
|
|
@@ -11464,7 +11473,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11464
11473
|
seg('aptx-clock-frame', left, top, left, bottom);
|
|
11465
11474
|
let unit = data.unit && !calendar ? ' (' + data.unit + ')' : '';
|
|
11466
11475
|
text('aptx-clock-title', (data.forward ? 'Date' : 'Age') + unit, (left + right) / 2, CLOCK_PLOT_H - 6, 'middle');
|
|
11467
|
-
let yt = text('aptx-clock-title', 'Divergence (
|
|
11476
|
+
let yt = text('aptx-clock-title', 'Divergence' + (data.divUnit ? ' (' + data.divUnit + ')' : ''), 0, 0, 'middle');
|
|
11468
11477
|
yt.setAttribute('transform', 'translate(12,' + ((top + bottom) / 2) + ') rotate(-90)');
|
|
11469
11478
|
|
|
11470
11479
|
// --- the points: ancestors under the line, tips over it
|
|
@@ -11506,12 +11515,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
11506
11515
|
let tipCount = data.points.reduce(function (n, p) {
|
|
11507
11516
|
return n + (p.tip ? 1 : 0);
|
|
11508
11517
|
}, 0);
|
|
11509
|
-
let per = '
|
|
11518
|
+
let per = (data.divUnit ? ' ' + data.divUnit : '') + ' per ' + (calendar ? 'year' : (data.unit || 'unit of time'));
|
|
11510
11519
|
if (fit) {
|
|
11511
11520
|
treePropRow(cp.stats, 'Rate', clockNumber(fit.rate) + per);
|
|
11512
11521
|
treePropRow(cp.stats, 'Root date, by the line', fit.rootDate === null
|
|
11513
11522
|
? 'none: divergence does not rise with time' : clockDate(fit.rootDate, calendar));
|
|
11514
|
-
treePropRow(cp.stats, 'Root date, in the tree',
|
|
11523
|
+
treePropRow(cp.stats, 'Root date, in the tree', data.root.date === null
|
|
11524
|
+
? 'none: only the tips are dated' : clockDate(data.root.date, calendar));
|
|
11515
11525
|
treePropRow(cp.stats, 'R²', fit.r2 === null ? 'none: every tip has one divergence' : clockNumber(fit.r2));
|
|
11516
11526
|
} else {
|
|
11517
11527
|
treePropRow(cp.stats, 'Line', 'none: it takes three tips, not all on one date');
|
|
@@ -11520,6 +11530,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
11520
11530
|
cp.note.textContent = (data.fromRates
|
|
11521
11531
|
? 'Divergence here is each branch’s time × its clock rate: the points show the model’s rates, not a measurement of their own. '
|
|
11522
11532
|
: '')
|
|
11533
|
+
+ (rootToTip
|
|
11534
|
+
? 'Divergence here is each tip’s distance from the root along the branch lengths the file states; only the tips are dated. '
|
|
11535
|
+
: '')
|
|
11536
|
+
+ (data.fromNames > 0
|
|
11537
|
+
? (data.fromNames === 1 ? 'One tip is dated by its name' : countNumber(data.fromNames) + ' tips are dated by their names')
|
|
11538
|
+
+ ' (a year alone is taken as mid-year'
|
|
11539
|
+
+ (data.ambiguousNames > 0 ? '; ' + countNumber(data.ambiguousNames) + ' with day and month ambiguous, read day first' : '')
|
|
11540
|
+
+ '). '
|
|
11541
|
+
: '')
|
|
11523
11542
|
+ 'The line is fitted to the tips only. Tips share ancestry, so R² describes the fit and is no test.'
|
|
11524
11543
|
+ ' Click a point to select its node; drag to select the tips in a box.';
|
|
11525
11544
|
// what was laid down, for the browser harness: read here, where it is
|
|
@@ -15366,22 +15385,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
15366
15385
|
}
|
|
15367
15386
|
|
|
15368
15387
|
|
|
15369
|
-
function customCbClicked(cb_id) {
|
|
15370
|
-
if (_nodeLabels) {
|
|
15371
|
-
const cb_value = getCheckboxValue(cb_id);
|
|
15372
|
-
for (const value of Object.values(_nodeLabels)) {
|
|
15373
|
-
if (value.label && value.showButton === true && value.propertyRef && value.description) {
|
|
15374
|
-
if (value.cb_id === cb_id) {
|
|
15375
|
-
value.selected = cb_value;
|
|
15376
|
-
}
|
|
15377
|
-
}
|
|
15378
|
-
}
|
|
15379
|
-
search0();
|
|
15380
|
-
search1();
|
|
15381
|
-
update();
|
|
15382
|
-
}
|
|
15383
|
-
}
|
|
15384
|
-
|
|
15385
15388
|
function taxonomyCbClicked() {
|
|
15386
15389
|
_state.showTaxonomy = getCheckboxValue(TAXONOMY_CB);
|
|
15387
15390
|
if (_state.showTaxonomy) {
|
|
@@ -18225,9 +18228,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
18225
18228
|
// live. Injecting twice is a no-op.
|
|
18226
18229
|
injectPanelStyles();
|
|
18227
18230
|
|
|
18231
|
+
// REMOVED (2026-10-07): the 2.x class "node_mouseover_tooltip" beside
|
|
18232
|
+
// "aptx-tip". A host's old stylesheet outranked .aptx-tip through it
|
|
18233
|
+
// (div.X beats .Y) and restyled the popup; see CHANGELOG, Unreleased.
|
|
18228
18234
|
if (!_node_mouseover_div) {
|
|
18229
18235
|
_node_mouseover_div = d3.select("body").append("div")
|
|
18230
|
-
.attr("class", "
|
|
18236
|
+
.attr("class", "aptx-tip")
|
|
18231
18237
|
.style("opacity", 1e-6);
|
|
18232
18238
|
}
|
|
18233
18239
|
|
|
@@ -18414,20 +18420,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
18414
18420
|
|
|
18415
18421
|
on(SHORTEN_NODE_NAME_CB, 'click', shortenCbClicked);
|
|
18416
18422
|
|
|
18417
|
-
if (_nodeLabels) {
|
|
18418
|
-
for (const [key, value] of Object.entries(_nodeLabels)) {
|
|
18419
|
-
if (value.label && value.showButton === true && value.propertyRef && value.description) {
|
|
18420
|
-
const cb_id = makeIdForCustomCheckboxButton(key);
|
|
18421
|
-
on(cb_id, 'click', function () {
|
|
18422
|
-
customCbClicked(cb_id);
|
|
18423
|
-
});
|
|
18424
|
-
if (value.selected === true) {
|
|
18425
|
-
setCheckboxValue(cb_id, true);
|
|
18426
|
-
}
|
|
18427
|
-
}
|
|
18428
|
-
}
|
|
18429
|
-
}
|
|
18430
|
-
|
|
18431
18423
|
on(LABEL_COLOR_SELECT_MENU, 'change', function () {
|
|
18432
18424
|
let v = this.value;
|
|
18433
18425
|
if (v && v !== DEFAULT && _vis && _vis.byId[v]) {
|
|
@@ -18868,10 +18860,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
18868
18860
|
return '<label class="aptx-seg" title="' + tooltip + '"><input type="radio" name="' + radioGroup + '" id="' + id + '">' + content + '</label>';
|
|
18869
18861
|
}
|
|
18870
18862
|
|
|
18871
|
-
function makeIdForCustomCheckboxButton(key) {
|
|
18872
|
-
return key + '__cb';
|
|
18873
|
-
}
|
|
18874
|
-
|
|
18875
18863
|
function makeDisplayControl() {
|
|
18876
18864
|
let labels = [];
|
|
18877
18865
|
let nodes = [];
|
|
@@ -18893,17 +18881,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
18893
18881
|
labels.push(makeCheckboxItem('Metadata', PROPERTIES_CB, 'to show/hide the nodes\' metadata (phyloXML'
|
|
18894
18882
|
+ ' properties: species, host, …) in their labels; Metadata fields… chooses which, and in what order'));
|
|
18895
18883
|
}
|
|
18896
|
-
if (_nodeLabels) {
|
|
18897
|
-
for (const [key, value] of Object.entries(_nodeLabels)) {
|
|
18898
|
-
if (value.label && value.propertyRef && value.description) {
|
|
18899
|
-
const cb_id = makeIdForCustomCheckboxButton(key);
|
|
18900
|
-
if (value.showButton === true) {
|
|
18901
|
-
labels.push(makeCheckboxItem(value.label, cb_id, value.description));
|
|
18902
|
-
}
|
|
18903
|
-
value.cb_id = cb_id;
|
|
18904
|
-
}
|
|
18905
|
-
}
|
|
18906
|
-
}
|
|
18907
18884
|
if (_basicTreeProperties.confidences) {
|
|
18908
18885
|
labels.push(makeCheckboxItem('Confidence', CONFIDENCE_VALUES_CB, 'to show/hide confidence values'));
|
|
18909
18886
|
}
|
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.25.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -7254,18 +7254,28 @@
|
|
|
7254
7254
|
// a panel linked to the tree, named "Clock plot"; JS only until the
|
|
7255
7255
|
// design is final.
|
|
7256
7256
|
//
|
|
7257
|
-
// THE RULE, designed for two kinds of tree
|
|
7257
|
+
// THE RULE, designed for two kinds of tree (both built; the second on
|
|
7258
|
+
// Christian's word, 2026-10-03):
|
|
7258
7259
|
//
|
|
7259
7260
|
// 1. 'divergence' -- a tree with Time and Div (hasTimeAndDivergence): each
|
|
7260
7261
|
// node states a date and a divergence from the root, recorded on it or
|
|
7261
7262
|
// summed from each branch's time x its clock rate. Every node is a
|
|
7262
7263
|
// point; which of the two layouts is on screen does not matter.
|
|
7263
|
-
// 2. 'root-to-tip'
|
|
7264
|
-
//
|
|
7265
|
-
//
|
|
7266
|
-
//
|
|
7267
|
-
//
|
|
7268
|
-
//
|
|
7264
|
+
// 2. 'root-to-tip' -- a tree whose branch lengths are its divergence and
|
|
7265
|
+
// whose tips are dated, with no date on any ancestor (it is not a time
|
|
7266
|
+
// tree): a tip's divergence is the sum of the stated lengths from the
|
|
7267
|
+
// root. Tips only. This is the tree a clock plot is wanted for most --
|
|
7268
|
+
// is there a signal worth dating? -- and the one whose answer depends
|
|
7269
|
+
// on its root. TempEst's plot. A tip is dated by what the file STATES
|
|
7270
|
+
// for it (a phyloXML <date>, a num_date annotation), or else by the
|
|
7271
|
+
// date its NAME carries, read by the label-date parser both programs
|
|
7272
|
+
// anchor BEAST heights with (Christian, 2026-10-03: names count, as
|
|
7273
|
+
// TempEst reads them; a year alone is mid-year). An ancestor is never
|
|
7274
|
+
// dated by its name. Every tip must be dated and every branch must
|
|
7275
|
+
// state a length: a tip with either missing has no point, and a plot
|
|
7276
|
+
// that drops tips silently answers a different question than it was
|
|
7277
|
+
// asked. A clock-model tree (a rate on every branch) is never this
|
|
7278
|
+
// kind: its lengths are time.
|
|
7269
7279
|
//
|
|
7270
7280
|
// In both, the plot is offered when its LINE can be drawn: three tips or
|
|
7271
7281
|
// more, and not all of them on one date. A tree sampled at one moment has
|
|
@@ -7277,19 +7287,76 @@
|
|
|
7277
7287
|
// an observation; an ancestor's was inferred, usually with a clock, and
|
|
7278
7288
|
// counting it would have the estimate confirm itself. The line is not
|
|
7279
7289
|
// forced through the root, so the date it reaches the root's divergence
|
|
7280
|
-
// at can be held against the date the tree states for the root
|
|
7290
|
+
// at can be held against the date the tree states for the root -- where
|
|
7291
|
+
// the tree states one; a root-to-tip tree does not, and there the line's
|
|
7292
|
+
// date is the only estimate.
|
|
7281
7293
|
const CLOCK_PLOT_MIN_TIPS = 3;
|
|
7282
7294
|
|
|
7295
|
+
// A tip's date on a root-to-tip tree: the one the file states, else the
|
|
7296
|
+
// one its name carries (forester.parseTipLabelDate: the rightmost date in
|
|
7297
|
+
// the label; a year alone is mid-year; an ambiguous numeric date is read
|
|
7298
|
+
// day first). {value, fromName, ambiguous}, or null for no date at all.
|
|
7299
|
+
function rootToTipDate(n) {
|
|
7300
|
+
let d = auspiceNodeDate(n);
|
|
7301
|
+
if (d !== null) {
|
|
7302
|
+
return {value: d, fromName: false, ambiguous: false};
|
|
7303
|
+
}
|
|
7304
|
+
let m = (typeof n.name === 'string' && n.name.length > 0) ? forester.parseTipLabelDate(n.name) : null;
|
|
7305
|
+
return m ? {value: m.decimalYear, fromName: true, ambiguous: m.ambiguous === true} : null;
|
|
7306
|
+
}
|
|
7307
|
+
|
|
7308
|
+
// Kind 2 of THE RULE, before the tips are counted: dated tips (by the
|
|
7309
|
+
// file or by their names), undated ancestors (by the file; a name dates
|
|
7310
|
+
// no ancestor), a length on every branch, no clock rates.
|
|
7311
|
+
function isRootToTipTree(root) {
|
|
7312
|
+
if (!root || root._divergenceFromRates === true || !everyLengthWasStated(root)) {
|
|
7313
|
+
return false;
|
|
7314
|
+
}
|
|
7315
|
+
let ok = true;
|
|
7316
|
+
forester.preOrderTraversalAll(root, function (n) {
|
|
7317
|
+
if (!ok) {
|
|
7318
|
+
return;
|
|
7319
|
+
}
|
|
7320
|
+
ok = (n.children && n.children.length > 0) ? auspiceNodeDate(n) === null : rootToTipDate(n) !== null;
|
|
7321
|
+
});
|
|
7322
|
+
return ok;
|
|
7323
|
+
}
|
|
7324
|
+
|
|
7325
|
+
// Each node's distance from the root along the stated branch lengths:
|
|
7326
|
+
// a root-to-tip tree's divergence.
|
|
7327
|
+
function rootToTipDistances(root) {
|
|
7328
|
+
let dist = new Map();
|
|
7329
|
+
(function walk(n, d) {
|
|
7330
|
+
dist.set(n, d);
|
|
7331
|
+
let children = n.children;
|
|
7332
|
+
if (children) {
|
|
7333
|
+
for (let i = 0; i < children.length; ++i) {
|
|
7334
|
+
let len = children[i].branch_length;
|
|
7335
|
+
walk(children[i], d + ((typeof len === 'number' && isFinite(len)) ? len : 0));
|
|
7336
|
+
}
|
|
7337
|
+
}
|
|
7338
|
+
}(root, 0));
|
|
7339
|
+
return dist;
|
|
7340
|
+
}
|
|
7341
|
+
|
|
7283
7342
|
/**
|
|
7284
7343
|
* Which clock plot the tree has, or null for none (see THE RULE above).
|
|
7285
7344
|
*
|
|
7286
7345
|
* @param phy the tree
|
|
7287
|
-
* @returns {string|null} 'divergence', or null
|
|
7346
|
+
* @returns {string|null} 'divergence', 'root-to-tip', or null
|
|
7288
7347
|
*/
|
|
7289
7348
|
forester.clockPlotKind = function (phy) {
|
|
7290
|
-
if (!phy
|
|
7349
|
+
if (!phy) {
|
|
7291
7350
|
return null;
|
|
7292
7351
|
}
|
|
7352
|
+
let kind = forester.hasTimeAndDivergence(phy) ? 'divergence'
|
|
7353
|
+
: (isRootToTipTree(forester.getTreeRoot(phy)) ? 'root-to-tip' : null);
|
|
7354
|
+
if (kind === null) {
|
|
7355
|
+
return null;
|
|
7356
|
+
}
|
|
7357
|
+
let dateOf = kind === 'root-to-tip' ? function (n) {
|
|
7358
|
+
return rootToTipDate(n).value; // every tip has one: the kind says so
|
|
7359
|
+
} : auspiceNodeDate;
|
|
7293
7360
|
let tips = 0;
|
|
7294
7361
|
let first = null;
|
|
7295
7362
|
let differ = false;
|
|
@@ -7298,14 +7365,14 @@
|
|
|
7298
7365
|
return;
|
|
7299
7366
|
}
|
|
7300
7367
|
++tips;
|
|
7301
|
-
let d =
|
|
7368
|
+
let d = dateOf(n);
|
|
7302
7369
|
if (first === null) {
|
|
7303
7370
|
first = d;
|
|
7304
7371
|
} else if (d !== first) {
|
|
7305
7372
|
differ = true;
|
|
7306
7373
|
}
|
|
7307
7374
|
});
|
|
7308
|
-
return (tips >= CLOCK_PLOT_MIN_TIPS && differ) ?
|
|
7375
|
+
return (tips >= CLOCK_PLOT_MIN_TIPS && differ) ? kind : null;
|
|
7309
7376
|
};
|
|
7310
7377
|
|
|
7311
7378
|
/**
|
|
@@ -7363,14 +7430,25 @@
|
|
|
7363
7430
|
* @param view the tree or clade to plot (the tree, a node, or the
|
|
7364
7431
|
* viewer's subtree root); the whole tree where none is given
|
|
7365
7432
|
* @returns {object|null} null where the tree has no clock plot; else
|
|
7366
|
-
* kind 'divergence'
|
|
7433
|
+
* kind 'divergence' or 'root-to-tip'
|
|
7367
7434
|
* forward whether the dates increase toward the tips (calendar
|
|
7368
7435
|
* dates); false for ages
|
|
7369
7436
|
* unit the dates' unit as the tree states it, or null
|
|
7437
|
+
* divUnit the divergence's unit: 'subs/site' for a tree with Time
|
|
7438
|
+
* and Div, a root-to-tip tree's stated branch length unit,
|
|
7439
|
+
* or null
|
|
7370
7440
|
* fromRates whether the divergence is time x clock rate
|
|
7441
|
+
* calendar whether the dates are calendar years: their unit says so,
|
|
7442
|
+
* or they were read off the tips' names
|
|
7443
|
+
* fromNames how many tips are dated by their names (a root-to-tip
|
|
7444
|
+
* tree; 0 otherwise)
|
|
7445
|
+
* ambiguousNames how many of those names carry a day/month-ambiguous
|
|
7446
|
+
* numeric date, read day first
|
|
7371
7447
|
* points [{node, date, div, tip}] in preorder; the tips'
|
|
7372
|
-
* `residual` is their divergence less the line's
|
|
7373
|
-
*
|
|
7448
|
+
* `residual` is their divergence less the line's. On a
|
|
7449
|
+
* root-to-tip tree the tips only: no ancestor has a date
|
|
7450
|
+
* root {node, date, div}: the top of the view; `date` null on a
|
|
7451
|
+
* root-to-tip tree
|
|
7374
7452
|
* fit forester.clockRegression over the tips, with
|
|
7375
7453
|
* rate the slope in the direction time runs: divergence
|
|
7376
7454
|
* per unit of time (negative where the tips'
|
|
@@ -7391,24 +7469,49 @@
|
|
|
7391
7469
|
if (view) {
|
|
7392
7470
|
top = (!view.parent && view.children && view.children.length === 1) ? view.children[0] : view;
|
|
7393
7471
|
}
|
|
7472
|
+
// a root-to-tip tree's divergence is the path from the TREE's root,
|
|
7473
|
+
// in the view of a clade too: the clade's top then has one as well
|
|
7474
|
+
let rootToTip = kind === 'root-to-tip';
|
|
7475
|
+
let dist = rootToTip ? rootToTipDistances(treeRoot) : null;
|
|
7476
|
+
let divOf = rootToTip ? function (n) {
|
|
7477
|
+
return dist.get(n);
|
|
7478
|
+
} : auspiceNodeDiv;
|
|
7394
7479
|
let points = [];
|
|
7395
7480
|
let xs = [];
|
|
7396
7481
|
let ys = [];
|
|
7482
|
+
let fromNames = 0;
|
|
7483
|
+
let ambiguousNames = 0;
|
|
7397
7484
|
forester.preOrderTraversalAll(top, function (n) {
|
|
7485
|
+
let tip = !(n.children && n.children.length > 0);
|
|
7398
7486
|
let date = auspiceNodeDate(n);
|
|
7399
|
-
|
|
7487
|
+
if (rootToTip && tip && date === null) {
|
|
7488
|
+
let named = rootToTipDate(n); // every tip has one: the kind says so
|
|
7489
|
+
date = named ? named.value : null;
|
|
7490
|
+
if (named && named.fromName) {
|
|
7491
|
+
++fromNames;
|
|
7492
|
+
if (named.ambiguous) {
|
|
7493
|
+
++ambiguousNames;
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7497
|
+
let div = divOf(n);
|
|
7400
7498
|
if (date === null || div === null) {
|
|
7401
|
-
return; //
|
|
7499
|
+
return; // an undated ancestor (root-to-tip), or a guard, not a rule
|
|
7402
7500
|
}
|
|
7403
|
-
let tip = !(n.children && n.children.length > 0);
|
|
7404
7501
|
points.push({node: n, date: date, div: div, tip: tip});
|
|
7405
7502
|
if (tip) {
|
|
7406
7503
|
xs.push(date);
|
|
7407
7504
|
ys.push(div);
|
|
7408
7505
|
}
|
|
7409
7506
|
});
|
|
7410
|
-
let
|
|
7411
|
-
|
|
7507
|
+
let info = forester.timeAxisInfo(treeRoot);
|
|
7508
|
+
// a root-to-tip tree has no dated pair to read the direction off:
|
|
7509
|
+
// its dates are calendar dates unless their unit says ages
|
|
7510
|
+
let forward = rootToTip ? info.type !== 'geologic' : timeIncreasesTowardTips(treeRoot);
|
|
7511
|
+
// calendar years: the dates' unit says so, or they were read off the
|
|
7512
|
+
// tips' names, which carry no other kind of date
|
|
7513
|
+
let calendar = forward && (info.type === 'calendar' || (rootToTip && info.type === null && fromNames > 0));
|
|
7514
|
+
let root = {node: top, date: auspiceNodeDate(top), div: divOf(top)};
|
|
7412
7515
|
let fit = forester.clockRegression(xs, ys);
|
|
7413
7516
|
if (fit) {
|
|
7414
7517
|
fit.rate = forward ? fit.slope : -fit.slope;
|
|
@@ -7420,11 +7523,17 @@
|
|
|
7420
7523
|
}
|
|
7421
7524
|
}
|
|
7422
7525
|
}
|
|
7526
|
+
let lengthUnit = (phy !== treeRoot && typeof phy.branch_length_unit === 'string'
|
|
7527
|
+
&& phy.branch_length_unit.trim().length > 0) ? phy.branch_length_unit.trim() : null;
|
|
7423
7528
|
return {
|
|
7424
7529
|
kind: kind,
|
|
7425
7530
|
forward: forward,
|
|
7426
7531
|
unit: dateUnitOf(treeRoot),
|
|
7427
|
-
|
|
7532
|
+
divUnit: rootToTip ? lengthUnit : DIVERGENCE_LENGTH_UNIT,
|
|
7533
|
+
fromRates: !rootToTip && treeRoot._divergenceFromRates === true && !recordsDivergence(treeRoot),
|
|
7534
|
+
calendar: calendar,
|
|
7535
|
+
fromNames: fromNames,
|
|
7536
|
+
ambiguousNames: ambiguousNames,
|
|
7428
7537
|
points: points,
|
|
7429
7538
|
root: root,
|
|
7430
7539
|
fit: fit
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.25.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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"LICENSE"
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
|
-
"test": "node test/forester_test.js && node test/search_test.js && node test/visualization_test.js && node test/domain_test.js && node test/gene_test.js && node test/clock_test.js && node test/metadata_test.js && node test/heatmap_test.js && node test/clustering_test.js",
|
|
23
|
+
"test": "node test/forester_test.js && node test/search_test.js && node test/visualization_test.js && node test/domain_test.js && node test/gene_test.js && node test/clock_test.js && node test/metadata_test.js && node test/heatmap_test.js && node test/clustering_test.js && node test/canvg_global_test.js",
|
|
24
24
|
"lint": "eslint .",
|
|
25
25
|
"lint:fix": "eslint . --fix",
|
|
26
26
|
"format": "prettier --write .",
|