archaeopteryx 3.19.0 → 3.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -10
- package/archaeopteryx.d.ts +6 -0
- package/archaeopteryx.js +425 -75
- package/forester.js +191 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -166,7 +166,7 @@ using `viewer.destroy()` to switch trees in place.
|
|
|
166
166
|
## Using the visualizations
|
|
167
167
|
|
|
168
168
|
A tree opens **already coloured** by its most informative field — the viewer
|
|
169
|
-
inspects what the tree carries (taxonomy, sequence fields,
|
|
169
|
+
inspects what the tree carries (taxonomy, sequence fields, metadata)
|
|
170
170
|
and decides by itself what is worth showing. There is nothing to configure.
|
|
171
171
|
|
|
172
172
|
* The **Color** and **Shape** menus (top of the control panel) list every
|
|
@@ -661,16 +661,42 @@ number wherever it appears — that is what makes a block of related columns
|
|
|
661
661
|
readable as a block, and it is the point of a heat map rather than a row of
|
|
662
662
|
independent stripes. The scale spans the whole tree, so entering a subtree
|
|
663
663
|
narrows the rows and leaves the colours where they were. The columns that
|
|
664
|
-
appear are
|
|
665
|
-
|
|
664
|
+
appear are the numeric fields the **Color by** menu offers, so the two agree
|
|
665
|
+
about what the tree holds — plus a field with **one value on every tip**, which
|
|
666
|
+
has nothing to colour by but is a column all the same (a single-copy core gene
|
|
667
|
+
is a column of 1s); by default their left-to-right order follows
|
|
666
668
|
the order the file lists them in, which keeps a producer's grouping (core genes,
|
|
667
669
|
then resistance, then prophages) intact even where some tips are missing a
|
|
668
670
|
field — see **Order columns** below for the alternatives.
|
|
669
671
|
|
|
672
|
+
Where a cell is at least 5 px on screen, each cell is drawn on its own with a
|
|
673
|
+
**thin border in its own hue, darker** — a block of equal values still reads
|
|
674
|
+
as a block, and each cell can be counted. The border stays 0.75 px at any
|
|
675
|
+
zoom. Smaller cells, or a matrix of more than 60,000 cells, are drawn as merged
|
|
676
|
+
runs of equal colour instead, and zooming across that threshold switches
|
|
677
|
+
between the two.
|
|
678
|
+
|
|
670
679
|
A cell **nobody filled in** is drawn as an outlined empty box, never as the
|
|
671
680
|
scale's low end: on a presence/absence matrix, reading a missing field as zero
|
|
672
681
|
states the opposite of what the file says. The key beside the scale names it.
|
|
673
682
|
|
|
683
|
+
**A file can open straight into its matrix.** The desktop Archaeopteryx
|
|
684
|
+
stores its figure setting as one property of the phylogeny, directly under
|
|
685
|
+
`<phylogeny>`:
|
|
686
|
+
|
|
687
|
+
```xml
|
|
688
|
+
<property ref="aptx:figure" datatype="xsd:string" applies_to="phylogeny">v1;columns=pgfam:PGF_00019355\sMATRIX\sCIRCLE\sfalse\ppgfam:PGF_00019313\sMATRIX\sCIRCLE\sfalse</property>
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
When it names two or more `MATRIX` columns the tree has, the tree opens with the
|
|
692
|
+
heat map on, showing **only those columns, in that order** (the order is then
|
|
693
|
+
`manual`, and **Order columns** can still change it). Other column types are
|
|
694
|
+
the desktop's and are not drawn here. Only a property directly under
|
|
695
|
+
`<phylogeny>` counts: an `aptx:figure` on a clade — where desktops
|
|
696
|
+
0.11.117 to 0.11.172 wrote it — is ignored and left out of what is saved; the phylogeny's
|
|
697
|
+
own is saved back unchanged. An explicit `showHeatmap`, `heatmapColumnOrder`
|
|
698
|
+
or `heatmapManualOrder` in the config wins over the file.
|
|
699
|
+
|
|
674
700
|
**Hover any cell** for its tip, the column and its value — or `not assessed` —
|
|
675
701
|
and the scale it was coloured against. The column names stand under the matrix,
|
|
676
702
|
turned; a matrix with more columns than will fit shows a window and says so
|
|
@@ -1046,7 +1072,7 @@ a popup any more, and nothing fails silently.
|
|
|
1046
1072
|
| **Newick** / New Hampshire (`.nwk`, `.nh`, `.tre`) | in / out | The base tree: topology, names, branch lengths, and bracketed confidence values. | [1] |
|
|
1047
1073
|
| **NHX** — New Hampshire eXtended | in | `[&&NHX:...]` tags riding on Newick: taxonomy (`S=`, `T=`), sequence (`GN=`, `AC=`), support (`B=`), duplication/speciation events (`D=`). | [2] |
|
|
1048
1074
|
| **Nexus** (`.nex`, `.nexus`) | in / out | One file for the tree(s) and, in a `CHARACTERS`/`DATA` block, an aligned protein/DNA/RNA matrix (sequential or interleaved) — the tree and its alignment together. | [3] |
|
|
1049
|
-
| **phyloXML** (`.xml`) | in / out | The richest native format: taxonomy, sequences and alignments, dates, confidences, branch colours and arbitrary custom properties. | [4] |
|
|
1075
|
+
| **phyloXML** (`.xml`) | in / out | The richest native format: taxonomy, sequences and alignments, dates, confidences, branch colours and arbitrary metadata (custom properties). | [4] |
|
|
1050
1076
|
| **Auspice / Nextstrain** `dataset.json` (v2) | in | Phylodynamic builds: sampling dates and their confidence, cumulative divergence, and discrete traits (country, clade, host, ...) with their posterior distributions. | [5] |
|
|
1051
1077
|
| **BEAST** / BEAST 2 / TreeAnnotator annotations | in (embedded in Newick/Nexus) | `[&posterior=...,height_95%_HPD={lo,hi},rate=...]`-style blobs: posterior clade support, node-age confidence intervals, per-branch rates and other traits. FigTree's `!color` is read the same way. | [6, 7] |
|
|
1052
1078
|
| **MrBayes** annotations | in (embedded in Newick/Nexus) | `prob=`/`prob.stddev=` blobs: posterior-probability clade support. | [8] |
|
|
@@ -1353,8 +1379,10 @@ copy-pastable JSON.
|
|
|
1353
1379
|
| `layout` | `'rectangular'` | The starting layout: `'rectangular'`, `'circular'`, or `'unrooted'`. |
|
|
1354
1380
|
| `ladderizeTree` | `true` | Ladderize the tree on load: at each node, the larger clade first (any number of children, so a polytomy sorts too). |
|
|
1355
1381
|
| `showMsa` | tree-derived | Open with the alignment track shown. Default: on when the tree carries an aligned `mol_seq`, off otherwise — an explicit `true`/`false` overrides that. |
|
|
1382
|
+
| `showProperties` | tree-derived | Open with the nodes' metadata (their phyloXML properties) in their labels — the **Metadata** checkbox. Default: off, unless the file's figure setting (`show.SHOW_PROPERTIES=true`) says on. See [Metadata in the label](#metadata-in-the-label). |
|
|
1383
|
+
| `labelProperties` | tree-derived | Which metadata fields the label shows, in order, as an array of refs (`['BVBRC:species', 'BVBRC:host_name']`). Default: the figure setting's `labelprops`, else `null` — every property but the heat map's columns. |
|
|
1356
1384
|
| `showMsaLogo` | `false` | Open with the alignment summarised as a sequence logo instead of a conservation bar: a stack of letters per column, as tall as its information content, over the tips currently on screen. Only drawn while the alignment track is shown. |
|
|
1357
|
-
| `showHeatmap` |
|
|
1385
|
+
| `showHeatmap` | tree-derived | Open with the heat map shown. Offered whenever the tree carries two or more numeric per-tip fields, but off unless asked for — almost any annotated tree has such fields, so turning it on by itself would be an opinion about the tree rather than a service — or unless the file's own figure setting (`aptx:figure`, see [Heat maps](#heat-maps)) draws them as a matrix. |
|
|
1358
1386
|
| `heatmapColumnOrder` | tree-derived | How the heat map's columns are ordered: `'document'` (as the file lists them), `'clustered'` (Euclidean), `'clustered-presence'` (Bray–Curtis), `'alphabetical'`, `'frequency'`. The clustered modes also draw the dendrogram. Default: a **clustered** order, with the distance chosen from the values — Bray–Curtis where the matrix has zeros to ignore and nothing negative, Euclidean otherwise. An explicit value always wins and is never re-derived. |
|
|
1359
1387
|
| `heatmapManualOrder` | `null` | The heat map's columns in your own order, as an array of property refs (`['meta:recA', 'meta:gyrA', …]`). Only read while `heatmapColumnOrder` is `'manual'`. A ref the tree has not got is ignored, and a column the list does not name follows the ones it does. |
|
|
1360
1388
|
| `showDomainArchitectures` | tree-derived | Open with the domain tracks shown. Default: on when any tip carries a `<domain_architecture>`, off otherwise — an explicit `true`/`false` overrides that. |
|
|
@@ -1849,6 +1877,30 @@ rather than 300 identical "Influenz.." labels).
|
|
|
1849
1877
|
Both menus live in the single control panel, above Display Data, which is where
|
|
1850
1878
|
the desktop puts them.
|
|
1851
1879
|
|
|
1880
|
+
### Metadata in the label
|
|
1881
|
+
|
|
1882
|
+
A tip's other fields — a species, a host, a country, anything a node carries
|
|
1883
|
+
as a `<property>` — are its **metadata**, and they can go into its label, as
|
|
1884
|
+
on the desktop. **Metadata** under Display Data (offered when nodes carry
|
|
1885
|
+
any, off by default) adds the **values** after the rest of the label, joined by ` | ` as the rest of the
|
|
1886
|
+
label is: `255457.3 | Brucella lupini | host`, or `Brucella lupini` alone with
|
|
1887
|
+
Node Name off. (The desktop joins them with `, `; a pipe was chosen here
|
|
1888
|
+
because values often contain commas — a host such as `Bottlenose dolphin,
|
|
1889
|
+
Tursiops truncatus`.) A unit follows its value. With nothing chosen, every
|
|
1890
|
+
field shows in the node's own order, except the heat map's columns while it is
|
|
1891
|
+
shown — a field has one display role. **Metadata fields…** picks which fields
|
|
1892
|
+
(tick them; **All** and **None** tick and untick the lot) and in what order
|
|
1893
|
+
(drag a row, or use its arrows); Apply keeps the ticked ones in the order
|
|
1894
|
+
shown, and picking a field turns Metadata on. The checkbox and the fields
|
|
1895
|
+
travel in a shared view (`show=…,properties`, `labelFields=`), and the config keys are
|
|
1896
|
+
`showProperties` and `labelProperties`.
|
|
1897
|
+
|
|
1898
|
+
A file can choose for itself: the desktop's figure setting (`aptx:figure`,
|
|
1899
|
+
see [Heat maps](#heat-maps)) carries `labelprops=` (the fields, in order)
|
|
1900
|
+
and the display switches `show.SHOW_PROPERTIES=true` and
|
|
1901
|
+
`show.SHOW_NODE_NAMES=false`, and a tree opens labelled that way in both
|
|
1902
|
+
programs. The config wins over the file.
|
|
1903
|
+
|
|
1852
1904
|
### Initial label fields
|
|
1853
1905
|
|
|
1854
1906
|
Which of the three label checkboxes — Node Name, Taxonomy, Sequence — start
|
|
@@ -2084,8 +2136,10 @@ bottom reserve the fit allows for follows that.
|
|
|
2084
2136
|
|
|
2085
2137
|
Model (`forester.heatmapColumns(tree)` → `{refs:[{ref,label}], min, max}`,
|
|
2086
2138
|
pure, in `test/heatmap_test.js`): a column is any candidate that
|
|
2087
|
-
`forester.visualizationCandidates`
|
|
2088
|
-
candidacy is not re-invented here and the refusal
|
|
2139
|
+
`forester.visualizationCandidates(tree, {keepConstant: true})` calls a
|
|
2140
|
+
**numeric property** — so candidacy is not re-invented here and the refusal
|
|
2141
|
+
rules are inherited, except the one against a field with a single value, which
|
|
2142
|
+
suits colour-by and not a matrix — carried
|
|
2089
2143
|
by at least one **tip**; an internal node has no row, so a ref only internal
|
|
2090
2144
|
nodes hold is not a column and its values never reach the scale. `min`/`max`
|
|
2091
2145
|
span every drawn cell of the whole tree. `forester.heatmapValue(node, ref)`
|
|
@@ -2126,7 +2180,7 @@ child arrangement and `ladderizeTree` rewrites it — measured, one file once ga
|
|
|
2126
2180
|
only mode that is **not** normalised first — being left alone is the whole point
|
|
2127
2181
|
of it. It is set by the **Reorder columns…** dialog, which drags or arrow-keys a
|
|
2128
2182
|
list of the columns as drawn; applying it writes `heatmapManualOrder` and sets
|
|
2129
|
-
the mode, exactly as the desktop's
|
|
2183
|
+
the mode, exactly as the desktop's Metadata Fields arrows switch a tab to
|
|
2130
2184
|
Manual. (Theirs also chooses which fields are columns and of what type; ours
|
|
2131
2185
|
takes every numeric field automatically, so the dialog is about order alone.)
|
|
2132
2186
|
Choosing Manual from the menu with nothing arranged yet freezes the order on
|
|
@@ -2188,8 +2242,16 @@ order the columns happened to be in. Measured: without it, the 6×6 linkage
|
|
|
2188
2242
|
fixture fed in reverse clustered to `h6,h5,h3,h4,h2,h1` instead of R's
|
|
2189
2243
|
`h6,h3,h5,h4,h1,h2` — the index order drives the tie-break and the leaf order,
|
|
2190
2244
|
so the incoming order really does leak through. The normaliser deliberately does
|
|
2191
|
-
**not** go through `heatmapColumns`: that applies candidacy rules
|
|
2192
|
-
|
|
2245
|
+
**not** go through `heatmapColumns`: that applies candidacy rules, and
|
|
2246
|
+
ordering must not move with them.
|
|
2247
|
+
|
|
2248
|
+
`forester.heatmapColumns(tree, only)` keeps just the refs in `only` — the
|
|
2249
|
+
figure's MATRIX columns (`forester.figureMatrixRefs(tree)`, from
|
|
2250
|
+
`forester.readFigure`, which reads the phylogeny's own `aptx:figure` only:
|
|
2251
|
+
`"v1;key=value;…"`, values escaped as the desktop's `PropertyTextCodec` does,
|
|
2252
|
+
`columns=` a `|` list of `ref~TYPE~SHAPE~normalized`; an unknown version is no
|
|
2253
|
+
figure, unknown keys are ignored). `forester.dropCladeFigures(tree)` removes
|
|
2254
|
+
the old clade-level copies at launch.
|
|
2193
2255
|
|
|
2194
2256
|
Distances are `forester.heatmapEuclideanDistances` (R's `dist` convention for
|
|
2195
2257
|
missing values: pairwise deletion, the squared sum scaled up by `tips / used`;
|
package/archaeopteryx.d.ts
CHANGED
|
@@ -104,6 +104,10 @@ export interface ArchaeopteryxConfig {
|
|
|
104
104
|
* bar: each column a stack of letters as tall as its information content,
|
|
105
105
|
* over the tips currently on screen. Only drawn with the track shown. */
|
|
106
106
|
showMsaLogo?: boolean;
|
|
107
|
+
/** The nodes' property values in their labels (the desktop's "Properties"). */
|
|
108
|
+
showProperties?: boolean;
|
|
109
|
+
/** Which properties the label shows, in order; null for all but the heat map's columns. */
|
|
110
|
+
labelProperties?: string[] | null;
|
|
107
111
|
/** The scale axis under a phylogram: the time axis on a time tree whose
|
|
108
112
|
* branches show time, a distance axis on any other. Default: on for a
|
|
109
113
|
* time tree, off otherwise. */
|
|
@@ -169,6 +173,8 @@ export interface ViewState {
|
|
|
169
173
|
heatmap?: boolean;
|
|
170
174
|
heatmapOrder?: 'document' | 'clustered' | 'clustered-presence' | 'alphabetical' | 'frequency' | 'manual';
|
|
171
175
|
heatmapManual?: string[];
|
|
176
|
+
/** The properties the label shows, in order (labelProperties). */
|
|
177
|
+
labelFields?: string[];
|
|
172
178
|
domains?: boolean;
|
|
173
179
|
domainLabels?: 'none' | 'domains' | 'legend';
|
|
174
180
|
domainGlow?: boolean;
|
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.21.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.21.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';
|
|
@@ -456,6 +456,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
456
456
|
const SEARCH_COMBINE_SELECT = 'scmb';
|
|
457
457
|
const SEARCH_COMBINE_ROW = 'scmb_row';
|
|
458
458
|
const SEQUENCE_CB = 'seq_cb';
|
|
459
|
+
const PROPERTIES_CB = 'props_cb';
|
|
460
|
+
const LABEL_FIELDS_BUTTON = 'label_fields_b';
|
|
461
|
+
const LABEL_FIELDS_DIALOG = 'label_fields_dialog';
|
|
459
462
|
const SHORTEN_NODE_NAME_CB = 'shortennodename_cb';
|
|
460
463
|
const TAXONOMY_CB = 'tax_cb';
|
|
461
464
|
const ZOOM_IN_X = 'zoomin_x';
|
|
@@ -630,11 +633,24 @@ function (root, d3, forester, phyloXml) {
|
|
|
630
633
|
// within 1.2px of a ring boundary came out background-coloured.
|
|
631
634
|
const HEATMAP_RING_BLEED = 1.2;
|
|
632
635
|
const HEATMAP_BLANK = 'blank'; // the run-merger's stand-in for a cell nobody filled in
|
|
636
|
+
// Cell borders (Christian, 2026-09-30): each cell outlined in its own hue,
|
|
637
|
+
// darker, so a block of equal values still reads as a block while each
|
|
638
|
+
// cell can be counted. Only where a cell is big enough on SCREEN to carry
|
|
639
|
+
// one, and not on a matrix so large that a node per cell would drag.
|
|
640
|
+
const HEATMAP_BORDER_MIN_PX = 5; // the cell's smaller side on screen
|
|
641
|
+
const HEATMAP_BORDER_MAX_CELLS = 60000;
|
|
642
|
+
const HEATMAP_BORDER_W = 0.75; // screen px at any zoom (non-scaling stroke)
|
|
643
|
+
const HEATMAP_BORDER_DARKEN = 0.8; // d3's darker(k): each step x0.7
|
|
644
|
+
const HEATMAP_SEAL_W = 0.6; // a merged run's stroke in its own colour, over the seams
|
|
633
645
|
const HEATMAP_DENDRO_GAP = 5; // px between the dendrogram's leaves and the first row
|
|
634
646
|
const HEATMAP_DENDRO_MIN_BAND = 24; // the band it is drawn in, from the label font
|
|
635
647
|
const HEATMAP_DENDRO_MAX_BAND = 72;
|
|
636
648
|
let _heatmapReserve = 0; // horizontal px reserved for the matrix, set with _w
|
|
637
649
|
let _heatmapModel = null; // forester.heatmapColumns of the WHOLE tree, cached per launch
|
|
650
|
+
let _heatmapCells = null; // what was drawn: {cellMin (tree units), count, bordered}
|
|
651
|
+
let _heatmapBorderTimer = null;
|
|
652
|
+
let _figureMatrix = null; // the MATRIX refs the file's aptx:figure names, when it draws a matrix
|
|
653
|
+
let _labelFields = null; // forester.labelPropertyRefs of the tree, cached until an edit
|
|
638
654
|
let _heatmapColor = null; // the one scale every column is painted on
|
|
639
655
|
let _heatmapColOffset = 0; // first shown column, while the matrix is windowed
|
|
640
656
|
let _heatmapGeom = null; // the last draw's geometry, for the hover readout
|
|
@@ -1331,6 +1347,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1331
1347
|
_svgGroup.attr('transform', event.transform);
|
|
1332
1348
|
placeFloatingOverlays();
|
|
1333
1349
|
updateOverviewViewport();
|
|
1350
|
+
heatmapBordersFollowZoom();
|
|
1334
1351
|
}
|
|
1335
1352
|
|
|
1336
1353
|
// A strip in the floating layer. Drawn in TREE coordinates like everything
|
|
@@ -3391,7 +3408,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3391
3408
|
_svgGroup.selectAll('g.aptx-align-ext').remove();
|
|
3392
3409
|
let alignedRect = !radialDisplay() && _state.phylogram && _state.alignPhylogram;
|
|
3393
3410
|
let tipGuides = alignedRect && _state.showExternalLabels
|
|
3394
|
-
&& (_state.showNodeName || _state.showTaxonomy || _state.showSequence);
|
|
3411
|
+
&& (_state.showNodeName || _state.showTaxonomy || _state.showSequence || _state.showProperties);
|
|
3395
3412
|
// a collapsed clade's label always shows, so its guide does too: from
|
|
3396
3413
|
// past the wedge's farthest tip to the label column
|
|
3397
3414
|
let collapsedGuides = alignedRect ? nodes.filter(function (d) {
|
|
@@ -4661,6 +4678,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
4661
4678
|
}
|
|
4662
4679
|
|
|
4663
4680
|
|
|
4681
|
+
if (_state.showProperties && phynode.properties) {
|
|
4682
|
+
l = append(l, labelPropertiesOf(phynode));
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4664
4685
|
if (_nodeLabels && phynode.properties) {
|
|
4665
4686
|
const props_length = phynode.properties.length;
|
|
4666
4687
|
if (props_length > 0) {
|
|
@@ -5621,6 +5642,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
5621
5642
|
'layout',
|
|
5622
5643
|
'showMsa',
|
|
5623
5644
|
'showMsaLogo',
|
|
5645
|
+
'showProperties',
|
|
5646
|
+
'labelProperties',
|
|
5624
5647
|
'showHeatmap',
|
|
5625
5648
|
'heatmapColumnOrder',
|
|
5626
5649
|
'heatmapManualOrder',
|
|
@@ -5860,8 +5883,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
5860
5883
|
// numeric per-tip fields, which is common enough that turning it on
|
|
5861
5884
|
// by itself would be an opinion about the tree rather than a service
|
|
5862
5885
|
// -- the alignment track, by contrast, is on when there IS one.
|
|
5886
|
+
// The desktop's figure setting: its MATRIX columns open as the heat
|
|
5887
|
+
// map, only those, in the file's order. Only the phylogeny's own
|
|
5888
|
+
// aptx:figure counts; a copy on a clade (the old place) is dropped,
|
|
5889
|
+
// so a tree saved from here carries none. Here and not in launchInto:
|
|
5890
|
+
// telling whether the columns exist is a scan of every tip, which on
|
|
5891
|
+
// a big tree belongs behind the working card, not in front of it.
|
|
5892
|
+
if (_treeData) {
|
|
5893
|
+
forester.dropCladeFigures(_treeData);
|
|
5894
|
+
}
|
|
5895
|
+
_figureMatrix = _treeData ? figureMatrixOf(_treeData) : null;
|
|
5863
5896
|
if (_state.showHeatmap === undefined) {
|
|
5864
|
-
_state.showHeatmap =
|
|
5897
|
+
_state.showHeatmap = _figureMatrix !== null;
|
|
5865
5898
|
}
|
|
5866
5899
|
// The logo is a summary of the alignment, not a second view of the
|
|
5867
5900
|
// tree, so it waits to be asked for: an alignment opens showing its
|
|
@@ -5876,7 +5909,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5876
5909
|
// value always wins and is never re-derived -- including 'document',
|
|
5877
5910
|
// which keeps the order the producer wrote the fields in.
|
|
5878
5911
|
if (_state.heatmapColumnOrder === undefined) {
|
|
5879
|
-
_state.heatmapColumnOrder = null;
|
|
5912
|
+
_state.heatmapColumnOrder = _figureMatrix ? 'manual' : null;
|
|
5880
5913
|
} else if (_state.heatmapColumnOrder !== null
|
|
5881
5914
|
&& forester.HEATMAP_ORDER_MODES.indexOf(_state.heatmapColumnOrder) < 0) {
|
|
5882
5915
|
throw new Error(ERROR + '"heatmapColumnOrder" must be one of '
|
|
@@ -5887,7 +5920,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5887
5920
|
// not got is ignored rather than fatal -- an order outliving one of
|
|
5888
5921
|
// its columns is ordinary (see forester.heatmapManualOrder).
|
|
5889
5922
|
if (_state.heatmapManualOrder === undefined) {
|
|
5890
|
-
_state.heatmapManualOrder = null;
|
|
5923
|
+
_state.heatmapManualOrder = _figureMatrix ? _figureMatrix.slice() : null;
|
|
5891
5924
|
} else if (_state.heatmapManualOrder !== null
|
|
5892
5925
|
&& !(Array.isArray(_state.heatmapManualOrder)
|
|
5893
5926
|
&& _state.heatmapManualOrder.every(function (r) {
|
|
@@ -6060,6 +6093,27 @@ function (root, d3, forester, phyloXml) {
|
|
|
6060
6093
|
+ ' characters)');
|
|
6061
6094
|
}
|
|
6062
6095
|
|
|
6096
|
+
// Properties in the tip label: off unless asked for, as on the desktop,
|
|
6097
|
+
// in every user-visible property's own order unless a list says which.
|
|
6098
|
+
// The file's figure setting (aptx:figure) can say both, and whether
|
|
6099
|
+
// the names show: labelprops, show.SHOW_PROPERTIES and
|
|
6100
|
+
// show.SHOW_NODE_NAMES, the desktop's own keys. The config wins.
|
|
6101
|
+
let figure = _treeData ? forester.readFigure(_treeData) : null;
|
|
6102
|
+
if (_state.showProperties === undefined) {
|
|
6103
|
+
_state.showProperties = !!(figure && figure.show.SHOW_PROPERTIES === true);
|
|
6104
|
+
}
|
|
6105
|
+
if (_state.labelProperties === undefined) {
|
|
6106
|
+
_state.labelProperties = (figure && figure.labelProps) ? figure.labelProps.slice() : null;
|
|
6107
|
+
} else if (_state.labelProperties !== null && !(Array.isArray(_state.labelProperties)
|
|
6108
|
+
&& _state.labelProperties.every(function (r) {
|
|
6109
|
+
return typeof r === 'string';
|
|
6110
|
+
}))) {
|
|
6111
|
+
throw new Error(ERROR + '"labelProperties" must be an array of property refs');
|
|
6112
|
+
}
|
|
6113
|
+
if (figure && typeof figure.show.SHOW_NODE_NAMES === 'boolean' && _basicTreeProperties.nodeNames) {
|
|
6114
|
+
_state.showNodeName = figure.show.SHOW_NODE_NAMES;
|
|
6115
|
+
}
|
|
6116
|
+
|
|
6063
6117
|
// A small tree is drawn with a heavier stroke; hairlines are for trees
|
|
6064
6118
|
// dense enough to need them.
|
|
6065
6119
|
_state.branchWidthDefault = _basicTreeProperties.externalNodesCount <= SMALL_TREE_MAX_EXT_NODES
|
|
@@ -6518,6 +6572,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
6518
6572
|
_treeData = phylo;
|
|
6519
6573
|
_trees = trees;
|
|
6520
6574
|
_treeIndex = index;
|
|
6575
|
+
_figureMatrix = null; // read by initializeState, behind the working card
|
|
6576
|
+
_labelFields = null;
|
|
6521
6577
|
_launchConfig = config;
|
|
6522
6578
|
_container = containerEl;
|
|
6523
6579
|
assignViewIds(phylo);
|
|
@@ -7275,6 +7331,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7275
7331
|
_domain.palette = null; // the tree changed: the domain names are dealt their colours again
|
|
7276
7332
|
}
|
|
7277
7333
|
if (edited) {
|
|
7334
|
+
_labelFields = null; // the fields the labels can show, too
|
|
7278
7335
|
_heatmapModel = null; // ... and the heat map's columns and scale are derived from it again
|
|
7279
7336
|
_heatmapColor = null;
|
|
7280
7337
|
}
|
|
@@ -8192,11 +8249,28 @@ function (root, d3, forester, phyloXml) {
|
|
|
8192
8249
|
|
|
8193
8250
|
function heatmapModel() {
|
|
8194
8251
|
if (!_heatmapModel && _treeData) {
|
|
8195
|
-
_heatmapModel = forester.heatmapColumns(_treeData);
|
|
8252
|
+
_heatmapModel = forester.heatmapColumns(_treeData, _figureMatrix);
|
|
8196
8253
|
}
|
|
8197
8254
|
return _heatmapModel;
|
|
8198
8255
|
}
|
|
8199
8256
|
|
|
8257
|
+
// The file's MATRIX columns, when at least a heat map's worth of them are
|
|
8258
|
+
// numeric fields this tree has; otherwise null, and the heat map offers
|
|
8259
|
+
// every numeric field as it would without a figure.
|
|
8260
|
+
// The model it builds is kept as the heat map's own, so the scan runs once.
|
|
8261
|
+
function figureMatrixOf(tree) {
|
|
8262
|
+
let refs = forester.figureMatrixRefs(tree);
|
|
8263
|
+
if (refs.length < HEATMAP_MIN_COLUMNS) {
|
|
8264
|
+
return null;
|
|
8265
|
+
}
|
|
8266
|
+
let model = forester.heatmapColumns(tree, refs);
|
|
8267
|
+
if (model.refs.length < HEATMAP_MIN_COLUMNS) {
|
|
8268
|
+
return null;
|
|
8269
|
+
}
|
|
8270
|
+
_heatmapModel = model;
|
|
8271
|
+
return refs;
|
|
8272
|
+
}
|
|
8273
|
+
|
|
8200
8274
|
// Offered when the tree holds at least two numeric per-tip refs that
|
|
8201
8275
|
// actually carry values: one column is a stripe rather than a matrix,
|
|
8202
8276
|
// and a scale needs something to span.
|
|
@@ -8404,7 +8478,53 @@ function (root, d3, forester, phyloXml) {
|
|
|
8404
8478
|
return heatmapTopReserve();
|
|
8405
8479
|
}
|
|
8406
8480
|
|
|
8481
|
+
// Whether cells of this size (tree units) and number are drawn one by one
|
|
8482
|
+
// with borders at zoom scale k, or merged into runs.
|
|
8483
|
+
function heatmapBordered(cellMin, count, k) {
|
|
8484
|
+
return count <= HEATMAP_BORDER_MAX_CELLS && (cellMin * k) >= HEATMAP_BORDER_MIN_PX;
|
|
8485
|
+
}
|
|
8486
|
+
|
|
8487
|
+
function heatmapBorderColor(fill) {
|
|
8488
|
+
let c = d3.color(fill);
|
|
8489
|
+
return c ? c.darker(HEATMAP_BORDER_DARKEN).formatHex() : fill;
|
|
8490
|
+
}
|
|
8491
|
+
|
|
8492
|
+
// A cell (or a merged run) of colour `fill`: bordered, its edge a darker
|
|
8493
|
+
// shade of itself; or sealed, stroked in its own colour so that no seam of
|
|
8494
|
+
// background shows between two runs at a fractional zoom -- the faint lines
|
|
8495
|
+
// that came and went with the zoom level. Both strokes are a constant
|
|
8496
|
+
// screen width, so a zoom neither fattens nor loses them.
|
|
8497
|
+
function paintHeatmapCell(el, fill, bordered) {
|
|
8498
|
+
el.attr('fill', fill)
|
|
8499
|
+
.attr('stroke', bordered ? heatmapBorderColor(fill) : fill)
|
|
8500
|
+
.attr('stroke-width', bordered ? HEATMAP_BORDER_W : HEATMAP_SEAL_W)
|
|
8501
|
+
.attr('vector-effect', 'non-scaling-stroke');
|
|
8502
|
+
return el;
|
|
8503
|
+
}
|
|
8504
|
+
|
|
8505
|
+
// Called on every zoom event: when the zoom has carried the cells across
|
|
8506
|
+
// the border threshold, the heat map alone is drawn again once the zoom
|
|
8507
|
+
// settles.
|
|
8508
|
+
function heatmapBordersFollowZoom() {
|
|
8509
|
+
if (!_heatmapCells || !_baseSvg) {
|
|
8510
|
+
return;
|
|
8511
|
+
}
|
|
8512
|
+
let want = heatmapBordered(_heatmapCells.cellMin, _heatmapCells.count, currentZoomScale());
|
|
8513
|
+
if (want === _heatmapCells.bordered) {
|
|
8514
|
+
return;
|
|
8515
|
+
}
|
|
8516
|
+
if (_heatmapBorderTimer) {
|
|
8517
|
+
clearTimeout(_heatmapBorderTimer);
|
|
8518
|
+
}
|
|
8519
|
+
_heatmapBorderTimer = setTimeout(function () {
|
|
8520
|
+
_heatmapBorderTimer = null;
|
|
8521
|
+
drawHeatmapTrack();
|
|
8522
|
+
placeFloatingOverlays();
|
|
8523
|
+
}, 120);
|
|
8524
|
+
}
|
|
8525
|
+
|
|
8407
8526
|
function drawHeatmapTrack() {
|
|
8527
|
+
_heatmapCells = null;
|
|
8408
8528
|
if (!_svgGroup) {
|
|
8409
8529
|
return;
|
|
8410
8530
|
}
|
|
@@ -8487,6 +8607,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8487
8607
|
// a run of CELLS, or its top and bottom edges read as a drawn rule
|
|
8488
8608
|
// across the matrix rather than as missing data.
|
|
8489
8609
|
let mergeBlanks = cw < HEATMAP_LABEL_MIN_COL_W;
|
|
8610
|
+
let cellMin = Math.min(cw, n > 1 ? 2 * pad : cw);
|
|
8611
|
+
let bordered = heatmapBordered(cellMin, n * visible, currentZoomScale());
|
|
8612
|
+
_heatmapCells = {cellMin: cellMin, count: n * visible, bordered: bordered};
|
|
8490
8613
|
for (let r = 0; r < n; ++r) {
|
|
8491
8614
|
let d = tips[r];
|
|
8492
8615
|
let cy = Math.round(bounds[r]);
|
|
@@ -8508,7 +8631,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8508
8631
|
rect.attr('fill', _state.backgroundColorDefault)
|
|
8509
8632
|
.attr('stroke', ink).attr('stroke-opacity', 0.45).attr('stroke-width', 1);
|
|
8510
8633
|
} else {
|
|
8511
|
-
rect
|
|
8634
|
+
paintHeatmapCell(rect, runFill, bordered);
|
|
8512
8635
|
}
|
|
8513
8636
|
runStart = -1;
|
|
8514
8637
|
runFill = null;
|
|
@@ -8519,7 +8642,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
8519
8642
|
if (fill === HEATMAP_BLANK) {
|
|
8520
8643
|
blanks++;
|
|
8521
8644
|
}
|
|
8522
|
-
|
|
8645
|
+
// bordered, every cell is its own: a run would hide the cells in it
|
|
8646
|
+
if (!bordered && runFill === fill && (mergeBlanks || fill !== HEATMAP_BLANK)) {
|
|
8523
8647
|
continue; // the run extends
|
|
8524
8648
|
}
|
|
8525
8649
|
flush(i);
|
|
@@ -8829,6 +8953,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
8829
8953
|
edge[n] = ang[n - 1] + pad;
|
|
8830
8954
|
|
|
8831
8955
|
let blanks = 0;
|
|
8956
|
+
// a cell's smaller side: its ring width, or its arc at the ring's inside
|
|
8957
|
+
let cellMin = Math.min(ringW, n > 1 ? 2 * pad * start : ringW);
|
|
8958
|
+
let bordered = heatmapBordered(cellMin, n * columns.length, currentZoomScale());
|
|
8959
|
+
_heatmapCells = {cellMin: cellMin, count: n * columns.length, bordered: bordered};
|
|
8832
8960
|
for (let c = 0; c < columns.length; ++c) {
|
|
8833
8961
|
let r0 = start + (c * ringW);
|
|
8834
8962
|
let r1 = r0 + ringW;
|
|
@@ -8847,15 +8975,19 @@ function (root, d3, forester, phyloXml) {
|
|
|
8847
8975
|
// A BLANK is not bled -- it is background-coloured, so a seam
|
|
8848
8976
|
// beside it shows nothing, and bleeding it would push its
|
|
8849
8977
|
// outward OUTLINE under the next ring and cost it an edge.
|
|
8850
|
-
|
|
8978
|
+
// (bordered, the border covers the seam and a bleed would push
|
|
8979
|
+
// one cell's edge under its neighbour)
|
|
8980
|
+
let bleed = (runFill === HEATMAP_BLANK || bordered) ? 0 : HEATMAP_RING_BLEED;
|
|
8851
8981
|
let a1 = edge[endI] + (bleed / Math.max(1, r1));
|
|
8852
8982
|
let path = g.append('path')
|
|
8853
8983
|
.attr('d', heatmapSector(edge[runStart], a1, r0, r1 + bleed));
|
|
8854
8984
|
if (runFill === HEATMAP_BLANK) {
|
|
8855
8985
|
path.attr('fill', _state.backgroundColorDefault)
|
|
8856
8986
|
.attr('stroke', ink).attr('stroke-opacity', 0.45).attr('stroke-width', 1);
|
|
8987
|
+
} else if (bordered) {
|
|
8988
|
+
paintHeatmapCell(path, runFill, true);
|
|
8857
8989
|
} else {
|
|
8858
|
-
path.attr('fill', runFill);
|
|
8990
|
+
path.attr('fill', runFill); // the bleed already closes the seams here
|
|
8859
8991
|
}
|
|
8860
8992
|
runStart = -1;
|
|
8861
8993
|
runFill = null;
|
|
@@ -8866,7 +8998,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8866
8998
|
if (fill === HEATMAP_BLANK) {
|
|
8867
8999
|
blanks++;
|
|
8868
9000
|
}
|
|
8869
|
-
if (runFill === fill && (ringW < HEATMAP_LABEL_MIN_COL_W || fill !== HEATMAP_BLANK)) {
|
|
9001
|
+
if (!bordered && runFill === fill && (ringW < HEATMAP_LABEL_MIN_COL_W || fill !== HEATMAP_BLANK)) {
|
|
8870
9002
|
continue;
|
|
8871
9003
|
}
|
|
8872
9004
|
flush(i);
|
|
@@ -9095,6 +9227,63 @@ function (root, d3, forester, phyloXml) {
|
|
|
9095
9227
|
// The same rule as the desktop's resolveEdited, minus its "did the order
|
|
9096
9228
|
// actually change" test, which it needs only because its dialog does other
|
|
9097
9229
|
// things too.
|
|
9230
|
+
// Drag a row of a reorder list -- the heat map's columns, the label's
|
|
9231
|
+
// metadata fields -- to a new place. The LIVE node is moved rather than
|
|
9232
|
+
// the list redrawn (a redraw would replace the element the drag is
|
|
9233
|
+
// following), its position read back from the DOM each time, and
|
|
9234
|
+
// `working` moved with it; `draw` runs once the pointer is released. A
|
|
9235
|
+
// press on a control in the row (an arrow, a checkbox) is not a drag.
|
|
9236
|
+
// Shared, so that both dialogs drag the same way: the rows' cursor
|
|
9237
|
+
// promised a drag the fields chooser did not have (Christian, 2026-10-01).
|
|
9238
|
+
function attachReorderDrag(list, row, working, draw) {
|
|
9239
|
+
function rowAt(y) {
|
|
9240
|
+
let rows = Array.prototype.slice.call(list.children);
|
|
9241
|
+
for (let i = 0; i < rows.length; ++i) {
|
|
9242
|
+
let b = rows[i].getBoundingClientRect();
|
|
9243
|
+
if (y < b.top + (b.height / 2)) {
|
|
9244
|
+
return i;
|
|
9245
|
+
}
|
|
9246
|
+
}
|
|
9247
|
+
return rows.length;
|
|
9248
|
+
}
|
|
9249
|
+
row.addEventListener('pointerdown', function (event) {
|
|
9250
|
+
if (event.target.closest('button, input')) {
|
|
9251
|
+
return;
|
|
9252
|
+
}
|
|
9253
|
+
event.preventDefault();
|
|
9254
|
+
row.classList.add('aptx-reorder-dragging');
|
|
9255
|
+
let onMove = function (ev) {
|
|
9256
|
+
let kids = Array.prototype.slice.call(list.children);
|
|
9257
|
+
let from = kids.indexOf(row);
|
|
9258
|
+
let to = rowAt(ev.clientY);
|
|
9259
|
+
if (to > from) {
|
|
9260
|
+
to -= 1; // taking the row out shifts everything after it up one
|
|
9261
|
+
}
|
|
9262
|
+
if (to === from || to < 0 || to >= kids.length) {
|
|
9263
|
+
return;
|
|
9264
|
+
}
|
|
9265
|
+
working.splice(to, 0, working.splice(from, 1)[0]);
|
|
9266
|
+
let others = kids.filter(function (k) {
|
|
9267
|
+
return k !== row;
|
|
9268
|
+
});
|
|
9269
|
+
list.insertBefore(row, others[to] || null);
|
|
9270
|
+
Array.prototype.slice.call(list.children).forEach(function (r, k) {
|
|
9271
|
+
let btns = r.querySelectorAll('.aptx-reorder-move');
|
|
9272
|
+
btns[0].disabled = (k === 0);
|
|
9273
|
+
btns[1].disabled = (k === list.children.length - 1);
|
|
9274
|
+
});
|
|
9275
|
+
};
|
|
9276
|
+
let onUp = function () {
|
|
9277
|
+
row.classList.remove('aptx-reorder-dragging');
|
|
9278
|
+
window.removeEventListener('pointermove', onMove);
|
|
9279
|
+
window.removeEventListener('pointerup', onUp);
|
|
9280
|
+
draw();
|
|
9281
|
+
};
|
|
9282
|
+
window.addEventListener('pointermove', onMove);
|
|
9283
|
+
window.addEventListener('pointerup', onUp);
|
|
9284
|
+
});
|
|
9285
|
+
}
|
|
9286
|
+
|
|
9098
9287
|
function showHeatmapReorderDialog() {
|
|
9099
9288
|
let ordered = heatmapOrdered();
|
|
9100
9289
|
if (!ordered || ordered.columns.length < 2) {
|
|
@@ -9107,17 +9296,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
9107
9296
|
let list = repsElement(body, 'div', 'aptx-reorder');
|
|
9108
9297
|
let working = ordered.columns.slice();
|
|
9109
9298
|
|
|
9110
|
-
function rowAt(y) {
|
|
9111
|
-
let rows = Array.prototype.slice.call(list.children);
|
|
9112
|
-
for (let i = 0; i < rows.length; ++i) {
|
|
9113
|
-
let b = rows[i].getBoundingClientRect();
|
|
9114
|
-
if (y < b.top + (b.height / 2)) {
|
|
9115
|
-
return i;
|
|
9116
|
-
}
|
|
9117
|
-
}
|
|
9118
|
-
return rows.length;
|
|
9119
|
-
}
|
|
9120
|
-
|
|
9121
9299
|
function move(from, to) {
|
|
9122
9300
|
if (to < 0 || to >= working.length || from === to) {
|
|
9123
9301
|
return false;
|
|
@@ -9164,49 +9342,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9164
9342
|
list.children[i + d].focus();
|
|
9165
9343
|
}
|
|
9166
9344
|
});
|
|
9167
|
-
|
|
9168
|
-
if (event.target.classList.contains('aptx-reorder-move')) {
|
|
9169
|
-
return; // the arrows are not a drag handle
|
|
9170
|
-
}
|
|
9171
|
-
event.preventDefault();
|
|
9172
|
-
row.classList.add('aptx-reorder-dragging');
|
|
9173
|
-
// The LIVE node is moved rather than the list redrawn: a
|
|
9174
|
-
// redraw would replace the element this drag is following.
|
|
9175
|
-
// Its position is read back from the DOM each time instead
|
|
9176
|
-
// of being tracked in a variable -- the first cut kept an
|
|
9177
|
-
// index, updated it, and then used it to work out where to
|
|
9178
|
-
// insert, by which point it no longer meant what the
|
|
9179
|
-
// calculation assumed.
|
|
9180
|
-
let onMove = function (ev) {
|
|
9181
|
-
let kids = Array.prototype.slice.call(list.children);
|
|
9182
|
-
let from = kids.indexOf(row);
|
|
9183
|
-
let to = rowAt(ev.clientY);
|
|
9184
|
-
if (to > from) {
|
|
9185
|
-
to -= 1; // taking the row out shifts everything after it up one
|
|
9186
|
-
}
|
|
9187
|
-
if (to === from || to < 0 || to >= kids.length) {
|
|
9188
|
-
return;
|
|
9189
|
-
}
|
|
9190
|
-
working.splice(to, 0, working.splice(from, 1)[0]);
|
|
9191
|
-
let others = kids.filter(function (k) {
|
|
9192
|
-
return k !== row;
|
|
9193
|
-
});
|
|
9194
|
-
list.insertBefore(row, others[to] || null);
|
|
9195
|
-
Array.prototype.slice.call(list.children).forEach(function (r, k) {
|
|
9196
|
-
let btns = r.querySelectorAll('.aptx-reorder-move');
|
|
9197
|
-
btns[0].disabled = (k === 0);
|
|
9198
|
-
btns[1].disabled = (k === list.children.length - 1);
|
|
9199
|
-
});
|
|
9200
|
-
};
|
|
9201
|
-
let onUp = function () {
|
|
9202
|
-
row.classList.remove('aptx-reorder-dragging');
|
|
9203
|
-
window.removeEventListener('pointermove', onMove);
|
|
9204
|
-
window.removeEventListener('pointerup', onUp);
|
|
9205
|
-
draw();
|
|
9206
|
-
};
|
|
9207
|
-
window.addEventListener('pointermove', onMove);
|
|
9208
|
-
window.addEventListener('pointerup', onUp);
|
|
9209
|
-
});
|
|
9345
|
+
attachReorderDrag(list, row, working, draw);
|
|
9210
9346
|
});
|
|
9211
9347
|
}
|
|
9212
9348
|
draw();
|
|
@@ -9627,6 +9763,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9627
9763
|
['name', NODE_NAME_CB, 'showNodeName'],
|
|
9628
9764
|
['taxonomy', TAXONOMY_CB, 'showTaxonomy'],
|
|
9629
9765
|
['sequence', SEQUENCE_CB, 'showSequence'],
|
|
9766
|
+
['properties', PROPERTIES_CB, 'showProperties'],
|
|
9630
9767
|
['confidence', CONFIDENCE_VALUES_CB, 'showConfidenceValues'],
|
|
9631
9768
|
['madValues', MAD_VALUES_CB, 'showMadValues'],
|
|
9632
9769
|
['branchLength', BRANCH_LENGTH_VALUES_CB, 'showBranchLengthValues'],
|
|
@@ -9749,6 +9886,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
9749
9886
|
if (heatmapAvailable()) {
|
|
9750
9887
|
s.heatmap = _state.showHeatmap === true;
|
|
9751
9888
|
s.heatmapOrder = heatmapMode();
|
|
9889
|
+
if (_state.labelProperties) {
|
|
9890
|
+
s.labelFields = _state.labelProperties.slice();
|
|
9891
|
+
}
|
|
9752
9892
|
if (_state.heatmapManualOrder) {
|
|
9753
9893
|
s.heatmapManual = _state.heatmapManualOrder.slice();
|
|
9754
9894
|
}
|
|
@@ -9892,6 +10032,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
9892
10032
|
if (typeof s.heatmap === 'boolean') {
|
|
9893
10033
|
_state.showHeatmap = s.heatmap;
|
|
9894
10034
|
}
|
|
10035
|
+
if (Array.isArray(s.labelFields) && s.labelFields.every(function (r) {
|
|
10036
|
+
return typeof r === 'string';
|
|
10037
|
+
})) {
|
|
10038
|
+
_state.labelProperties = s.labelFields.slice();
|
|
10039
|
+
}
|
|
9895
10040
|
if (Array.isArray(s.heatmapManual) && s.heatmapManual.every(function (r) {
|
|
9896
10041
|
return typeof r === 'string';
|
|
9897
10042
|
})) {
|
|
@@ -10088,7 +10233,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10088
10233
|
'scaleAxis', 'scaleGrid', 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
|
|
10089
10234
|
// Lists of strings, comma-joined. A property ref carries ':' but never a
|
|
10090
10235
|
// comma, and encodeViewValue leaves both readable in a hash.
|
|
10091
|
-
const VIEW_LIST_KEYS = ['heatmapManual'];
|
|
10236
|
+
const VIEW_LIST_KEYS = ['heatmapManual', 'labelFields'];
|
|
10092
10237
|
const VIEW_SEARCH_KEYS = [['searchA', 'a'], ['searchB', 'b']];
|
|
10093
10238
|
|
|
10094
10239
|
function encodeViewValue(v) {
|
|
@@ -13593,6 +13738,194 @@ function (root, d3, forester, phyloXml) {
|
|
|
13593
13738
|
scheduleUpdate();
|
|
13594
13739
|
}
|
|
13595
13740
|
|
|
13741
|
+
// ===================== Properties in the label =====================
|
|
13742
|
+
// The desktop's "Properties" option and its Annotation Fields chooser:
|
|
13743
|
+
// a node's property values in its label, values only, " | "-joined, in
|
|
13744
|
+
// the order chosen (forester.labelPropertiesText). Nothing chosen means
|
|
13745
|
+
// every user-visible property in the node's own order, less the heat
|
|
13746
|
+
// map's columns while it is shown -- a field has one display role.
|
|
13747
|
+
|
|
13748
|
+
function labelFields() {
|
|
13749
|
+
if (_labelFields === null && _treeData) {
|
|
13750
|
+
_labelFields = forester.labelPropertyRefs(_treeData);
|
|
13751
|
+
}
|
|
13752
|
+
return _labelFields || [];
|
|
13753
|
+
}
|
|
13754
|
+
|
|
13755
|
+
// the heat map's columns while it is shown: left out of the default label
|
|
13756
|
+
function labelFieldsExcluded() {
|
|
13757
|
+
let excluded = Object.create(null);
|
|
13758
|
+
if (heatmapShown()) {
|
|
13759
|
+
let m = heatmapModel();
|
|
13760
|
+
(m ? m.refs : []).forEach(function (c) {
|
|
13761
|
+
excluded[c.ref] = true;
|
|
13762
|
+
});
|
|
13763
|
+
}
|
|
13764
|
+
return excluded;
|
|
13765
|
+
}
|
|
13766
|
+
|
|
13767
|
+
function labelPropertiesOf(node) {
|
|
13768
|
+
return forester.labelPropertiesText(node, _state.labelProperties,
|
|
13769
|
+
_state.labelProperties ? null : labelFieldsExcluded());
|
|
13770
|
+
}
|
|
13771
|
+
|
|
13772
|
+
function propertiesCbClicked() {
|
|
13773
|
+
_state.showProperties = getCheckboxValue(PROPERTIES_CB);
|
|
13774
|
+
if (_state.showProperties) {
|
|
13775
|
+
_state.showExternalLabels = true;
|
|
13776
|
+
setCheckboxValue(EXTERNAL_LABEL_CB, true);
|
|
13777
|
+
}
|
|
13778
|
+
search0();
|
|
13779
|
+
search1();
|
|
13780
|
+
scheduleUpdate();
|
|
13781
|
+
}
|
|
13782
|
+
|
|
13783
|
+
// Which properties the label shows, and in what order: each field ticked
|
|
13784
|
+
// or not, and moved by dragging, with the arrows, or with the arrow keys. Apply keeps the ticked
|
|
13785
|
+
// ones in the order shown; picking a field that was not shown turns the
|
|
13786
|
+
// Properties label on, as the desktop does, since otherwise choosing it
|
|
13787
|
+
// would visibly do nothing. "All" goes back to every field.
|
|
13788
|
+
function showLabelFieldsDialog() {
|
|
13789
|
+
let all = labelFields();
|
|
13790
|
+
if (all.length < 1) {
|
|
13791
|
+
return;
|
|
13792
|
+
}
|
|
13793
|
+
let chosen = _state.labelProperties ? _state.labelProperties.filter(function (r) {
|
|
13794
|
+
return all.indexOf(r) >= 0;
|
|
13795
|
+
}) : null;
|
|
13796
|
+
// the chosen fields first, in their order, then the rest as offered;
|
|
13797
|
+
// with none chosen, ticked is what the label shows by default
|
|
13798
|
+
let excluded = chosen ? null : labelFieldsExcluded();
|
|
13799
|
+
let working = (chosen || []).concat(all.filter(function (r) {
|
|
13800
|
+
return !chosen || chosen.indexOf(r) < 0;
|
|
13801
|
+
})).map(function (ref) {
|
|
13802
|
+
return {ref: ref, on: chosen ? chosen.indexOf(ref) >= 0 : !excluded[ref]};
|
|
13803
|
+
});
|
|
13804
|
+
if (!chosen) {
|
|
13805
|
+
// the ticked ones first, so the list reads as the label does
|
|
13806
|
+
working = working.filter(function (w) {
|
|
13807
|
+
return w.on;
|
|
13808
|
+
}).concat(working.filter(function (w) {
|
|
13809
|
+
return !w.on;
|
|
13810
|
+
}));
|
|
13811
|
+
}
|
|
13812
|
+
let before = working.filter(function (w) {
|
|
13813
|
+
return w.on && (chosen !== null || _state.showProperties);
|
|
13814
|
+
}).map(function (w) {
|
|
13815
|
+
return w.ref;
|
|
13816
|
+
});
|
|
13817
|
+
let shell = makeDialogShell(LABEL_FIELDS_DIALOG, 'Metadata fields', 340);
|
|
13818
|
+
let body = shell.body;
|
|
13819
|
+
repsElement(body, 'div', 'aptx-reps-lead',
|
|
13820
|
+
'Tick the metadata fields the labels show; drag a field, or use the arrows, to set their order.');
|
|
13821
|
+
let list = repsElement(body, 'div', 'aptx-reorder');
|
|
13822
|
+
|
|
13823
|
+
function move(from, to) {
|
|
13824
|
+
if (to < 0 || to >= working.length || from === to) {
|
|
13825
|
+
return false;
|
|
13826
|
+
}
|
|
13827
|
+
working.splice(to, 0, working.splice(from, 1)[0]);
|
|
13828
|
+
draw();
|
|
13829
|
+
return true;
|
|
13830
|
+
}
|
|
13831
|
+
|
|
13832
|
+
function draw() {
|
|
13833
|
+
list.textContent = '';
|
|
13834
|
+
working.forEach(function (w, i) {
|
|
13835
|
+
let row = repsElement(list, 'div', 'aptx-reorder-row');
|
|
13836
|
+
row.tabIndex = 0;
|
|
13837
|
+
repsElement(row, 'span', 'aptx-reorder-grip', '≡');
|
|
13838
|
+
let box = repsElement(row, 'input', 'aptx-reorder-check');
|
|
13839
|
+
box.type = 'checkbox';
|
|
13840
|
+
box.checked = w.on;
|
|
13841
|
+
box.addEventListener('change', function () {
|
|
13842
|
+
w.on = box.checked;
|
|
13843
|
+
});
|
|
13844
|
+
repsElement(row, 'span', 'aptx-reorder-name', forester.propertyDisplayName(w.ref)).title = w.ref;
|
|
13845
|
+
let up = repsElement(row, 'button', 'aptx-reorder-move', '↑');
|
|
13846
|
+
let down = repsElement(row, 'button', 'aptx-reorder-move', '↓');
|
|
13847
|
+
up.type = 'button';
|
|
13848
|
+
down.type = 'button';
|
|
13849
|
+
up.title = 'Move up';
|
|
13850
|
+
down.title = 'Move down';
|
|
13851
|
+
up.disabled = (i === 0);
|
|
13852
|
+
down.disabled = (i === working.length - 1);
|
|
13853
|
+
up.addEventListener('click', function () {
|
|
13854
|
+
if (move(i, i - 1)) {
|
|
13855
|
+
list.children[i - 1].focus();
|
|
13856
|
+
}
|
|
13857
|
+
});
|
|
13858
|
+
down.addEventListener('click', function () {
|
|
13859
|
+
if (move(i, i + 1)) {
|
|
13860
|
+
list.children[i + 1].focus();
|
|
13861
|
+
}
|
|
13862
|
+
});
|
|
13863
|
+
row.addEventListener('keydown', function (event) {
|
|
13864
|
+
if (event.key === ' ' && event.target === row) {
|
|
13865
|
+
event.preventDefault();
|
|
13866
|
+
box.checked = !box.checked;
|
|
13867
|
+
w.on = box.checked;
|
|
13868
|
+
return;
|
|
13869
|
+
}
|
|
13870
|
+
let d = (event.key === 'ArrowUp') ? -1 : ((event.key === 'ArrowDown') ? 1 : 0);
|
|
13871
|
+
if (d === 0) {
|
|
13872
|
+
return;
|
|
13873
|
+
}
|
|
13874
|
+
event.preventDefault();
|
|
13875
|
+
if (move(i, i + d)) {
|
|
13876
|
+
list.children[i + d].focus();
|
|
13877
|
+
}
|
|
13878
|
+
});
|
|
13879
|
+
attachReorderDrag(list, row, working, draw);
|
|
13880
|
+
});
|
|
13881
|
+
}
|
|
13882
|
+
draw();
|
|
13883
|
+
|
|
13884
|
+
let actions = repsElement(body, 'div', 'aptx-reps-actions');
|
|
13885
|
+
// All and None tick the boxes and leave the dialog open for Apply --
|
|
13886
|
+
// All used to mean "back to the default" and close at once, which
|
|
13887
|
+
// read as the dialog vanishing (Christian, 2026-10-01)
|
|
13888
|
+
let tickAll = function (on) {
|
|
13889
|
+
working.forEach(function (w) {
|
|
13890
|
+
w.on = on;
|
|
13891
|
+
});
|
|
13892
|
+
draw();
|
|
13893
|
+
};
|
|
13894
|
+
repsButton(actions, 'All', false, function () {
|
|
13895
|
+
tickAll(true);
|
|
13896
|
+
});
|
|
13897
|
+
repsButton(actions, 'None', false, function () {
|
|
13898
|
+
tickAll(false);
|
|
13899
|
+
});
|
|
13900
|
+
repsButton(actions, 'Cancel', false, function () {
|
|
13901
|
+
shell.dialog.close();
|
|
13902
|
+
});
|
|
13903
|
+
repsButton(actions, 'Apply', true, function () {
|
|
13904
|
+
_state.labelProperties = working.filter(function (w) {
|
|
13905
|
+
return w.on;
|
|
13906
|
+
}).map(function (w) {
|
|
13907
|
+
return w.ref;
|
|
13908
|
+
});
|
|
13909
|
+
let added = _state.labelProperties.some(function (r) {
|
|
13910
|
+
return before.indexOf(r) < 0;
|
|
13911
|
+
});
|
|
13912
|
+
if (added && !_state.showProperties) {
|
|
13913
|
+
_state.showProperties = true;
|
|
13914
|
+
_state.showExternalLabels = true;
|
|
13915
|
+
setCheckboxValue(PROPERTIES_CB, true);
|
|
13916
|
+
setCheckboxValue(EXTERNAL_LABEL_CB, true);
|
|
13917
|
+
}
|
|
13918
|
+
shell.dialog.close();
|
|
13919
|
+
search0();
|
|
13920
|
+
search1();
|
|
13921
|
+
scheduleUpdate();
|
|
13922
|
+
});
|
|
13923
|
+
shell.dialog.showModal();
|
|
13924
|
+
if (list.firstChild) {
|
|
13925
|
+
list.firstChild.focus();
|
|
13926
|
+
}
|
|
13927
|
+
}
|
|
13928
|
+
|
|
13596
13929
|
function sequenceCbClicked() {
|
|
13597
13930
|
_state.showSequence = getCheckboxValue(SEQUENCE_CB);
|
|
13598
13931
|
if (_state.showSequence) {
|
|
@@ -13783,7 +14116,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13783
14116
|
}
|
|
13784
14117
|
let part = subtreeExtentNote();
|
|
13785
14118
|
items.push({heading: part ? 'Tree: ' + part : 'Tree'});
|
|
13786
|
-
items.push({label: 'phyloXML', detail: 'keeps everything the tree carries: taxonomy, sequences, dates,
|
|
14119
|
+
items.push({label: 'phyloXML', detail: 'keeps everything the tree carries: taxonomy, sequences, dates, metadata',
|
|
13787
14120
|
action: entry(PHYLOXML_EXPORT_FORMAT)});
|
|
13788
14121
|
items.push({label: 'Newick', detail: 'names, branch lengths and support values only',
|
|
13789
14122
|
action: entry(NH_EXPORT_FORMAT)});
|
|
@@ -13794,7 +14127,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13794
14127
|
// Fasta entries) -- on any other tree the download would be empty
|
|
13795
14128
|
items.push({heading: part ? 'Tip data: ' + part : 'Tip data'});
|
|
13796
14129
|
items.push({label: 'TSV', detail: 'a table, one row per tip: names, taxonomy, sequence names, '
|
|
13797
|
-
+ 'branch lengths and every
|
|
14130
|
+
+ 'branch lengths and every metadata field', action: entry(TSV_EXPORT_FORMAT)});
|
|
13798
14131
|
if (_basicTreeProperties && _basicTreeProperties.maxMolSeqLength > 0) {
|
|
13799
14132
|
items.push({label: 'FASTA', detail: 'every molecular sequence the tips carry',
|
|
13800
14133
|
action: entry(FASTA_EXPORT_FORMAT)});
|
|
@@ -15557,7 +15890,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
15557
15890
|
}
|
|
15558
15891
|
}
|
|
15559
15892
|
if (rows) {
|
|
15560
|
-
text += '
|
|
15893
|
+
text += 'Metadata<br>' + rows;
|
|
15561
15894
|
}
|
|
15562
15895
|
}
|
|
15563
15896
|
return text;
|
|
@@ -15973,7 +16306,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
15973
16306
|
carries('Events', true, parts.join(', '));
|
|
15974
16307
|
}
|
|
15975
16308
|
st.propertyRefs.forEach(function (r) {
|
|
15976
|
-
carries('
|
|
16309
|
+
carries('Metadata ' + r.ref, true, statOf(r.nodes, st.nodes, 'nodes'));
|
|
15977
16310
|
});
|
|
15978
16311
|
if (carried === 0) {
|
|
15979
16312
|
treePropRow(box, 'Annotations', 'none, names and topology only');
|
|
@@ -16304,6 +16637,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
16304
16637
|
on(TAXONOMY_CB, 'click', taxonomyCbClicked);
|
|
16305
16638
|
|
|
16306
16639
|
on(SEQUENCE_CB, 'click', sequenceCbClicked);
|
|
16640
|
+
on(PROPERTIES_CB, 'click', propertiesCbClicked);
|
|
16641
|
+
on(LABEL_FIELDS_BUTTON, 'click', showLabelFieldsDialog);
|
|
16307
16642
|
|
|
16308
16643
|
on(CONFIDENCE_VALUES_CB, 'click', confidenceValuesCbClicked);
|
|
16309
16644
|
on(SUPPORT_DOTS_CB, 'click', supportDotsCbClicked);
|
|
@@ -16832,6 +17167,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
16832
17167
|
if (_basicTreeProperties.sequences) {
|
|
16833
17168
|
labels.push(makeCheckboxItem('Sequence', SEQUENCE_CB, 'to show/hide node sequence information'));
|
|
16834
17169
|
}
|
|
17170
|
+
if (labelFields().length > 0) {
|
|
17171
|
+
// "Metadata", not phyloXML's "properties" (Christian, 2026-10-01):
|
|
17172
|
+
// the word the users' own tools use for these fields
|
|
17173
|
+
labels.push(makeCheckboxItem('Metadata', PROPERTIES_CB, 'to show/hide the nodes\' metadata (phyloXML'
|
|
17174
|
+
+ ' properties: species, host, …) in their labels; Metadata fields… chooses which, and in what order'));
|
|
17175
|
+
}
|
|
16835
17176
|
if (_nodeLabels) {
|
|
16836
17177
|
for (const [key, value] of Object.entries(_nodeLabels)) {
|
|
16837
17178
|
if (value.label && value.propertyRef && value.description) {
|
|
@@ -16913,6 +17254,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
16913
17254
|
|
|
16914
17255
|
let h = '<fieldset><legend>Display Data</legend>';
|
|
16915
17256
|
h = h.concat(makeCheckboxGroup('Labels', labels));
|
|
17257
|
+
if (labelFields().length > 0) {
|
|
17258
|
+
h = h.concat('<div class="aptx-domrow"><span class="aptx-domlabel"></span>'
|
|
17259
|
+
+ '<input type="button" class="aptx-widebtn" value="Metadata fields…" name="' + LABEL_FIELDS_BUTTON
|
|
17260
|
+
+ '" id="' + LABEL_FIELDS_BUTTON + '" title="choose which metadata fields the labels show, and in'
|
|
17261
|
+
+ ' what order"></div>');
|
|
17262
|
+
}
|
|
16916
17263
|
h = h.concat(makeCheckboxGroup('Nodes', nodes));
|
|
16917
17264
|
h = h.concat(makeCheckboxGroup('Options', opts));
|
|
16918
17265
|
h = h.concat('</fieldset>');
|
|
@@ -17171,12 +17518,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
17171
17518
|
h = h.concat('<fieldset>');
|
|
17172
17519
|
h = h.concat('<legend>Visualizations</legend>');
|
|
17173
17520
|
if (hasColorVisualizations()) {
|
|
17174
|
-
h = h.concat(makeSelectMenu('Color by:', '<br>', LABEL_COLOR_SELECT_MENU, 'colorize the node label and the node itself
|
|
17521
|
+
h = h.concat(makeSelectMenu('Color by:', '<br>', LABEL_COLOR_SELECT_MENU, 'colorize the node label and the node itself by a field the tree carries'));
|
|
17175
17522
|
h = h.concat('<br>');
|
|
17176
17523
|
h = h.concat('<br>');
|
|
17177
17524
|
}
|
|
17178
17525
|
if (hasShapeVisualizations()) {
|
|
17179
|
-
h = h.concat(makeSelectMenu('Shape:', '<br>', NODE_SHAPE_SELECT_MENU, 'change the node shape
|
|
17526
|
+
h = h.concat(makeSelectMenu('Shape:', '<br>', NODE_SHAPE_SELECT_MENU, 'change the node shape by a field the tree carries'));
|
|
17180
17527
|
h = h.concat('<br>');
|
|
17181
17528
|
}
|
|
17182
17529
|
h = h.concat('</fieldset>');
|
|
@@ -17237,6 +17584,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
17237
17584
|
setCheckboxValue(NODE_NAME_CB, _state.showNodeName);
|
|
17238
17585
|
setCheckboxValue(TAXONOMY_CB, _state.showTaxonomy);
|
|
17239
17586
|
setCheckboxValue(SEQUENCE_CB, _state.showSequence)
|
|
17587
|
+
setCheckboxValue(PROPERTIES_CB, _state.showProperties);
|
|
17240
17588
|
setCheckboxValue(CONFIDENCE_VALUES_CB, _state.showConfidenceValues);
|
|
17241
17589
|
setCheckboxValue(SUPPORT_DOTS_CB, _state.showSupportDots);
|
|
17242
17590
|
syncMadValuesCheckbox();
|
|
@@ -17911,9 +18259,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
17911
18259
|
}
|
|
17912
18260
|
let name = _root_const.name ? String(_root_const.name).trim() : '';
|
|
17913
18261
|
// what the phylogeny itself states goes with the part: without its
|
|
17914
|
-
// branch_length_unit a subtree saved from Div opened again as time
|
|
18262
|
+
// branch_length_unit a subtree saved from Div opened again as time,
|
|
18263
|
+
// and without its properties (the figure setting) the part opened
|
|
18264
|
+
// without the heat map the whole tree opens with
|
|
17915
18265
|
let part = Object.assign({}, _root, {name: name ? name + ' (subtree)' : undefined});
|
|
17916
|
-
['rooted', 'rerootable', 'type', 'branch_length_unit'].forEach(function (k) {
|
|
18266
|
+
['rooted', 'rerootable', 'type', 'branch_length_unit', 'properties'].forEach(function (k) {
|
|
17917
18267
|
if (_root_const[k] !== undefined && _root_const[k] !== null) {
|
|
17918
18268
|
part[k] = _root_const[k];
|
|
17919
18269
|
}
|
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.21.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -2091,7 +2091,12 @@
|
|
|
2091
2091
|
return p.applies_to === 'node' || p.applies_to === 'clade';
|
|
2092
2092
|
};
|
|
2093
2093
|
|
|
2094
|
-
|
|
2094
|
+
// options.keepConstant: also return a NUMERIC field that has one value on
|
|
2095
|
+
// every tip. There is nothing to colour by in it, so the selection never
|
|
2096
|
+
// asks for it -- but a matrix does: a single-copy core gene is a column
|
|
2097
|
+
// of 1s, and dropping it from the heat map hid a gene the file lists.
|
|
2098
|
+
forester.visualizationCandidates = function (tree, options) {
|
|
2099
|
+
let keepConstant = !!(options && options.keepConstant);
|
|
2095
2100
|
let total = 0;
|
|
2096
2101
|
let stats = Object.create(null); // id -> {kind, ref, label, nodes, values:Set, multi}; null-proto: ids embed file refs
|
|
2097
2102
|
|
|
@@ -2255,7 +2260,7 @@
|
|
|
2255
2260
|
// The legend already carries the honest part: a "no value" row
|
|
2256
2261
|
// with the uncovered count, at reduced opacity.
|
|
2257
2262
|
let sparse = covered * VIS_MIN_COVERAGE_DEN < total * VIS_MIN_COVERAGE_NUM;
|
|
2258
|
-
if (distinct < 2) {
|
|
2263
|
+
if (distinct < 2 && !(keepConstant && numeric)) {
|
|
2259
2264
|
return;
|
|
2260
2265
|
}
|
|
2261
2266
|
let colorMode;
|
|
@@ -2883,11 +2888,18 @@
|
|
|
2883
2888
|
* no numeric per-tip property. `min`/`max` are over every value of every
|
|
2884
2889
|
* column and are `null` when there is nothing to scale.
|
|
2885
2890
|
*/
|
|
2886
|
-
forester.heatmapColumns = function (tree) {
|
|
2891
|
+
forester.heatmapColumns = function (tree, only) {
|
|
2892
|
+
let wanted = null; // the figure's columns, when a file names them
|
|
2893
|
+
if (Array.isArray(only)) {
|
|
2894
|
+
wanted = Object.create(null);
|
|
2895
|
+
only.forEach(function (ref) {
|
|
2896
|
+
wanted[ref] = true;
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2887
2899
|
let numeric = Object.create(null);
|
|
2888
2900
|
let label = Object.create(null);
|
|
2889
|
-
forester.visualizationCandidates(tree).forEach(function (c) {
|
|
2890
|
-
if (c && c.kind === 'property' && c.numeric === true && c.ref) {
|
|
2901
|
+
forester.visualizationCandidates(tree, {keepConstant: true}).forEach(function (c) {
|
|
2902
|
+
if (c && c.kind === 'property' && c.numeric === true && c.ref && (!wanted || wanted[c.ref])) {
|
|
2891
2903
|
numeric[c.ref] = true;
|
|
2892
2904
|
label[c.ref] = c.label || c.ref;
|
|
2893
2905
|
}
|
|
@@ -2933,6 +2945,179 @@
|
|
|
2933
2945
|
};
|
|
2934
2946
|
};
|
|
2935
2947
|
|
|
2948
|
+
// --------------------------------------------------------------
|
|
2949
|
+
// The desktop's figure setting (aptx:figure)
|
|
2950
|
+
// --------------------------------------------------------------
|
|
2951
|
+
// The desktop Archaeopteryx stores what a tree is to be drawn with in one
|
|
2952
|
+
// property of the PHYLOGENY: <property ref="aptx:figure" applies_to=
|
|
2953
|
+
// "phylogeny"> as a direct child of <phylogeny>. Its value (FigureSpec
|
|
2954
|
+
// on the desktop) is "v1;key=value;..." with every value escaped
|
|
2955
|
+
// (PropertyTextCodec): \\ \t \n \r, \_ a space, \p "|", \s "~",
|
|
2956
|
+
// \c ";", \e "=". columns= is a "|" list of ref~TYPE~SHAPE~normalized.
|
|
2957
|
+
// Only a direct child of <phylogeny> counts (Christian, 2026-09-30: "a
|
|
2958
|
+
// hard break"): desktops 0.11.117 to 0.11.172 wrote it on the root clade,
|
|
2959
|
+
// and such a copy is ignored here and dropped on the way out, as the
|
|
2960
|
+
// desktop does. An unknown version is no figure; unknown keys are ignored.
|
|
2961
|
+
forester.FIGURE_REF = 'aptx:figure';
|
|
2962
|
+
|
|
2963
|
+
forester.unescapeFigureValue = function (v) {
|
|
2964
|
+
const map = {'\\': '\\', 't': '\t', 'n': '\n', 'r': '\r', '_': ' ', 'p': '|', 's': '~', 'c': ';', 'e': '='};
|
|
2965
|
+
return String(v).replace(/\\(.)/g, function (all, c) {
|
|
2966
|
+
return map[c] !== undefined ? map[c] : all;
|
|
2967
|
+
});
|
|
2968
|
+
};
|
|
2969
|
+
|
|
2970
|
+
// {columns: [{ref, type, shape, normalized}]} from the tree's figure, or
|
|
2971
|
+
// null when it has none (or one this version cannot read).
|
|
2972
|
+
forester.readFigure = function (tree) {
|
|
2973
|
+
let props = tree && Array.isArray(tree.properties) ? tree.properties : [];
|
|
2974
|
+
let p = props.find(function (q) {
|
|
2975
|
+
return q && q.ref === forester.FIGURE_REF;
|
|
2976
|
+
});
|
|
2977
|
+
if (!p || typeof p.value !== 'string') {
|
|
2978
|
+
return null;
|
|
2979
|
+
}
|
|
2980
|
+
let parts = p.value.trim().split(';');
|
|
2981
|
+
if (parts[0] !== 'v1') {
|
|
2982
|
+
return null;
|
|
2983
|
+
}
|
|
2984
|
+
// labelProps: the properties the label shows, in order (null when the
|
|
2985
|
+
// figure does not say, [] when it says none); show: the desktop's
|
|
2986
|
+
// display switches by DisplayOption name ("show.SHOW_PROPERTIES=true")
|
|
2987
|
+
let figure = {columns: [], labelProps: null, show: {}};
|
|
2988
|
+
parts.slice(1).forEach(function (kv) {
|
|
2989
|
+
let at = kv.indexOf('=');
|
|
2990
|
+
if (at < 1) {
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
let key = kv.substring(0, at);
|
|
2994
|
+
let value = forester.unescapeFigureValue(kv.substring(at + 1));
|
|
2995
|
+
if (key === 'labelprops') {
|
|
2996
|
+
figure.labelProps = value.split('|').filter(function (r) {
|
|
2997
|
+
return r.length > 0;
|
|
2998
|
+
});
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
if (key.indexOf('show.') === 0 && (value === 'true' || value === 'false')) {
|
|
3002
|
+
figure.show[key.substring(5)] = value === 'true';
|
|
3003
|
+
return;
|
|
3004
|
+
}
|
|
3005
|
+
if (key !== 'columns') {
|
|
3006
|
+
return;
|
|
3007
|
+
}
|
|
3008
|
+
value.split('|').forEach(function (col) {
|
|
3009
|
+
let f = col.split('~');
|
|
3010
|
+
if (f[0] && f[0].length > 0) {
|
|
3011
|
+
figure.columns.push({ref: f[0], type: f[1] || '', shape: f[2] || 'CIRCLE',
|
|
3012
|
+
normalized: f[3] === 'true'});
|
|
3013
|
+
}
|
|
3014
|
+
});
|
|
3015
|
+
});
|
|
3016
|
+
return figure;
|
|
3017
|
+
};
|
|
3018
|
+
|
|
3019
|
+
// The refs the figure draws as a MATRIX, in its order ([] without one).
|
|
3020
|
+
forester.figureMatrixRefs = function (tree) {
|
|
3021
|
+
let figure = forester.readFigure(tree);
|
|
3022
|
+
let seen = Object.create(null);
|
|
3023
|
+
return figure ? figure.columns.filter(function (c) {
|
|
3024
|
+
let keep = c.type === 'MATRIX' && !seen[c.ref];
|
|
3025
|
+
seen[c.ref] = true;
|
|
3026
|
+
return keep;
|
|
3027
|
+
}).map(function (c) {
|
|
3028
|
+
return c.ref;
|
|
3029
|
+
}) : [];
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
// ---- Properties in the tip label (the desktop's "Properties" option) ----
|
|
3033
|
+
|
|
3034
|
+
// A property a reader would want to see: not the viewer's own aptx:
|
|
3035
|
+
// metadata, not a style: instruction.
|
|
3036
|
+
const LABEL_PROPERTY_SEPARATOR = ' | ';
|
|
3037
|
+
|
|
3038
|
+
forester.isUserVisiblePropertyRef = function (ref) {
|
|
3039
|
+
return typeof ref === 'string' && ref.length > 0 && ref.indexOf('aptx:') !== 0 && ref.indexOf('style:') !== 0;
|
|
3040
|
+
};
|
|
3041
|
+
|
|
3042
|
+
// Every user-visible property ref any node carries, internal nodes
|
|
3043
|
+
// included, sorted by the name the reader sees (then by ref): what the
|
|
3044
|
+
// label-field chooser offers. Deliberately BROADER than the colour-by
|
|
3045
|
+
// candidates, which drop constant, per-tip-unique and internal-only fields
|
|
3046
|
+
// -- all perfectly good label text (the desktop's userVisiblePropertyRefs).
|
|
3047
|
+
forester.labelPropertyRefs = function (tree) {
|
|
3048
|
+
let seen = Object.create(null);
|
|
3049
|
+
forester.preOrderTraversalAll(tree, function (n) {
|
|
3050
|
+
(n.properties || []).forEach(function (p) {
|
|
3051
|
+
if (p && forester.isUserVisiblePropertyRef(p.ref)
|
|
3052
|
+
&& p.value !== undefined && p.value !== null && String(p.value).trim().length > 0) {
|
|
3053
|
+
seen[p.ref] = true;
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
});
|
|
3057
|
+
return Object.keys(seen).sort(function (a, b) {
|
|
3058
|
+
let da = forester.propertyDisplayName(a).toLowerCase();
|
|
3059
|
+
let db = forester.propertyDisplayName(b).toLowerCase();
|
|
3060
|
+
return da < db ? -1 : (da > db ? 1 : (a < b ? -1 : (a > b ? 1 : 0)));
|
|
3061
|
+
});
|
|
3062
|
+
};
|
|
3063
|
+
|
|
3064
|
+
// A node's properties as one line of label text: values only, a unit
|
|
3065
|
+
// after its value (without its namespace), joined by " | " as the rest of
|
|
3066
|
+
// the label is. Not by ", " as on the desktop (Christian, 2026-09-30): a
|
|
3067
|
+
// value often holds a comma itself -- a BV-BRC host "Bottlenose dolphin,
|
|
3068
|
+
// Tursiops truncatus" -- and "host, species" then reads as three fields. `refs` picks and orders the fields; null is every
|
|
3069
|
+
// user-visible property in the node's own order, less `excluded` (the
|
|
3070
|
+
// heat map's columns: a field has one display role).
|
|
3071
|
+
forester.labelPropertiesText = function (node, refs, excluded) {
|
|
3072
|
+
let props = (node && node.properties) || [];
|
|
3073
|
+
let parts = [];
|
|
3074
|
+
let add = function (p) {
|
|
3075
|
+
if (!p || !forester.isUserVisiblePropertyRef(p.ref) || p.value === undefined || p.value === null) {
|
|
3076
|
+
return;
|
|
3077
|
+
}
|
|
3078
|
+
let v = String(p.value).trim();
|
|
3079
|
+
if (v.length < 1) {
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3082
|
+
if (typeof p.unit === 'string' && p.unit.length > 0) {
|
|
3083
|
+
v += ' ' + p.unit.substring(p.unit.lastIndexOf(':') + 1);
|
|
3084
|
+
}
|
|
3085
|
+
parts.push(v);
|
|
3086
|
+
};
|
|
3087
|
+
if (refs) {
|
|
3088
|
+
refs.forEach(function (ref) {
|
|
3089
|
+
props.forEach(function (p) {
|
|
3090
|
+
if (p && p.ref === ref) {
|
|
3091
|
+
add(p);
|
|
3092
|
+
}
|
|
3093
|
+
});
|
|
3094
|
+
});
|
|
3095
|
+
} else {
|
|
3096
|
+
props.forEach(function (p) {
|
|
3097
|
+
if (!(excluded && p && excluded[p.ref])) {
|
|
3098
|
+
add(p);
|
|
3099
|
+
}
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
return parts.join(LABEL_PROPERTY_SEPARATOR);
|
|
3103
|
+
};
|
|
3104
|
+
|
|
3105
|
+
// Removes an aptx:figure from every clade (the old place, now ignored),
|
|
3106
|
+
// so a tree saved from here carries no dead copy. Returns how many.
|
|
3107
|
+
forester.dropCladeFigures = function (tree) {
|
|
3108
|
+
let n = 0;
|
|
3109
|
+
forester.preOrderTraversalAll(tree, function (node) {
|
|
3110
|
+
if (node !== tree && Array.isArray(node.properties)) {
|
|
3111
|
+
let kept = node.properties.filter(function (q) {
|
|
3112
|
+
return !(q && q.ref === forester.FIGURE_REF);
|
|
3113
|
+
});
|
|
3114
|
+
n += node.properties.length - kept.length;
|
|
3115
|
+
node.properties = kept;
|
|
3116
|
+
}
|
|
3117
|
+
});
|
|
3118
|
+
return n;
|
|
3119
|
+
};
|
|
3120
|
+
|
|
2936
3121
|
/**
|
|
2937
3122
|
* The number in `node`'s heat-map cell for `ref`, or `null` when there is none.
|
|
2938
3123
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.21.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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"d3": "^7.9.0",
|
|
38
|
-
"phyloxml": "^1.1.
|
|
38
|
+
"phyloxml": "^1.1.7"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@eslint/js": "^9.13.0",
|