archaeopteryx 3.19.0 → 3.20.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 +67 -7
- package/archaeopteryx.d.ts +6 -0
- package/archaeopteryx.js +347 -15
- package/forester.js +191 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -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
|
|
@@ -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' property values in their labels. Default: off, unless the file's figure setting (`show.SHOW_PROPERTIES=true`) says on. See [Properties in the label](#properties-in-the-label). |
|
|
1383
|
+
| `labelProperties` | tree-derived | Which properties 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,28 @@ 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
|
+
### Properties in the label
|
|
1881
|
+
|
|
1882
|
+
A tip's other fields — a species, a host, a country, anything a node carries
|
|
1883
|
+
as a `<property>` — can go into its label, as on the desktop. **Properties**
|
|
1884
|
+
under Display Data (offered when nodes carry properties, off by default) adds
|
|
1885
|
+
their **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 property shows in the
|
|
1890
|
+
node's own order, except the heat map's columns while it is shown — a field
|
|
1891
|
+
has one display role. **Label fields…** picks which properties, and in what
|
|
1892
|
+
order; picking one turns Properties on. The checkbox and the fields travel in
|
|
1893
|
+
a shared view (`show=…,properties`, `labelFields=`), and the config keys are
|
|
1894
|
+
`showProperties` and `labelProperties`.
|
|
1895
|
+
|
|
1896
|
+
A file can choose for itself: the desktop's figure setting (`aptx:figure`,
|
|
1897
|
+
see [Heat maps](#heat-maps)) carries `labelprops=` (the properties, in order)
|
|
1898
|
+
and the display switches `show.SHOW_PROPERTIES=true` and
|
|
1899
|
+
`show.SHOW_NODE_NAMES=false`, and a tree opens labelled that way in both
|
|
1900
|
+
programs. The config wins over the file.
|
|
1901
|
+
|
|
1852
1902
|
### Initial label fields
|
|
1853
1903
|
|
|
1854
1904
|
Which of the three label checkboxes — Node Name, Taxonomy, Sequence — start
|
|
@@ -2084,8 +2134,10 @@ bottom reserve the fit allows for follows that.
|
|
|
2084
2134
|
|
|
2085
2135
|
Model (`forester.heatmapColumns(tree)` → `{refs:[{ref,label}], min, max}`,
|
|
2086
2136
|
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
|
|
2137
|
+
`forester.visualizationCandidates(tree, {keepConstant: true})` calls a
|
|
2138
|
+
**numeric property** — so candidacy is not re-invented here and the refusal
|
|
2139
|
+
rules are inherited, except the one against a field with a single value, which
|
|
2140
|
+
suits colour-by and not a matrix — carried
|
|
2089
2141
|
by at least one **tip**; an internal node has no row, so a ref only internal
|
|
2090
2142
|
nodes hold is not a column and its values never reach the scale. `min`/`max`
|
|
2091
2143
|
span every drawn cell of the whole tree. `forester.heatmapValue(node, ref)`
|
|
@@ -2188,8 +2240,16 @@ order the columns happened to be in. Measured: without it, the 6×6 linkage
|
|
|
2188
2240
|
fixture fed in reverse clustered to `h6,h5,h3,h4,h2,h1` instead of R's
|
|
2189
2241
|
`h6,h3,h5,h4,h1,h2` — the index order drives the tie-break and the leaf order,
|
|
2190
2242
|
so the incoming order really does leak through. The normaliser deliberately does
|
|
2191
|
-
**not** go through `heatmapColumns`: that applies candidacy rules
|
|
2192
|
-
|
|
2243
|
+
**not** go through `heatmapColumns`: that applies candidacy rules, and
|
|
2244
|
+
ordering must not move with them.
|
|
2245
|
+
|
|
2246
|
+
`forester.heatmapColumns(tree, only)` keeps just the refs in `only` — the
|
|
2247
|
+
figure's MATRIX columns (`forester.figureMatrixRefs(tree)`, from
|
|
2248
|
+
`forester.readFigure`, which reads the phylogeny's own `aptx:figure` only:
|
|
2249
|
+
`"v1;key=value;…"`, values escaped as the desktop's `PropertyTextCodec` does,
|
|
2250
|
+
`columns=` a `|` list of `ref~TYPE~SHAPE~normalized`; an unknown version is no
|
|
2251
|
+
figure, unknown keys are ignored). `forester.dropCladeFigures(tree)` removes
|
|
2252
|
+
the old clade-level copies at launch.
|
|
2193
2253
|
|
|
2194
2254
|
Distances are `forester.heatmapEuclideanDistances` (R's `dist` convention for
|
|
2195
2255
|
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.20.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.20.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);
|
|
@@ -9627,6 +9759,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9627
9759
|
['name', NODE_NAME_CB, 'showNodeName'],
|
|
9628
9760
|
['taxonomy', TAXONOMY_CB, 'showTaxonomy'],
|
|
9629
9761
|
['sequence', SEQUENCE_CB, 'showSequence'],
|
|
9762
|
+
['properties', PROPERTIES_CB, 'showProperties'],
|
|
9630
9763
|
['confidence', CONFIDENCE_VALUES_CB, 'showConfidenceValues'],
|
|
9631
9764
|
['madValues', MAD_VALUES_CB, 'showMadValues'],
|
|
9632
9765
|
['branchLength', BRANCH_LENGTH_VALUES_CB, 'showBranchLengthValues'],
|
|
@@ -9749,6 +9882,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
9749
9882
|
if (heatmapAvailable()) {
|
|
9750
9883
|
s.heatmap = _state.showHeatmap === true;
|
|
9751
9884
|
s.heatmapOrder = heatmapMode();
|
|
9885
|
+
if (_state.labelProperties) {
|
|
9886
|
+
s.labelFields = _state.labelProperties.slice();
|
|
9887
|
+
}
|
|
9752
9888
|
if (_state.heatmapManualOrder) {
|
|
9753
9889
|
s.heatmapManual = _state.heatmapManualOrder.slice();
|
|
9754
9890
|
}
|
|
@@ -9892,6 +10028,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
9892
10028
|
if (typeof s.heatmap === 'boolean') {
|
|
9893
10029
|
_state.showHeatmap = s.heatmap;
|
|
9894
10030
|
}
|
|
10031
|
+
if (Array.isArray(s.labelFields) && s.labelFields.every(function (r) {
|
|
10032
|
+
return typeof r === 'string';
|
|
10033
|
+
})) {
|
|
10034
|
+
_state.labelProperties = s.labelFields.slice();
|
|
10035
|
+
}
|
|
9895
10036
|
if (Array.isArray(s.heatmapManual) && s.heatmapManual.every(function (r) {
|
|
9896
10037
|
return typeof r === 'string';
|
|
9897
10038
|
})) {
|
|
@@ -10088,7 +10229,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10088
10229
|
'scaleAxis', 'scaleGrid', 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
|
|
10089
10230
|
// Lists of strings, comma-joined. A property ref carries ':' but never a
|
|
10090
10231
|
// comma, and encodeViewValue leaves both readable in a hash.
|
|
10091
|
-
const VIEW_LIST_KEYS = ['heatmapManual'];
|
|
10232
|
+
const VIEW_LIST_KEYS = ['heatmapManual', 'labelFields'];
|
|
10092
10233
|
const VIEW_SEARCH_KEYS = [['searchA', 'a'], ['searchB', 'b']];
|
|
10093
10234
|
|
|
10094
10235
|
function encodeViewValue(v) {
|
|
@@ -13593,6 +13734,182 @@ function (root, d3, forester, phyloXml) {
|
|
|
13593
13734
|
scheduleUpdate();
|
|
13594
13735
|
}
|
|
13595
13736
|
|
|
13737
|
+
// ===================== Properties in the label =====================
|
|
13738
|
+
// The desktop's "Properties" option and its Annotation Fields chooser:
|
|
13739
|
+
// a node's property values in its label, values only, " | "-joined, in
|
|
13740
|
+
// the order chosen (forester.labelPropertiesText). Nothing chosen means
|
|
13741
|
+
// every user-visible property in the node's own order, less the heat
|
|
13742
|
+
// map's columns while it is shown -- a field has one display role.
|
|
13743
|
+
|
|
13744
|
+
function labelFields() {
|
|
13745
|
+
if (_labelFields === null && _treeData) {
|
|
13746
|
+
_labelFields = forester.labelPropertyRefs(_treeData);
|
|
13747
|
+
}
|
|
13748
|
+
return _labelFields || [];
|
|
13749
|
+
}
|
|
13750
|
+
|
|
13751
|
+
// the heat map's columns while it is shown: left out of the default label
|
|
13752
|
+
function labelFieldsExcluded() {
|
|
13753
|
+
let excluded = Object.create(null);
|
|
13754
|
+
if (heatmapShown()) {
|
|
13755
|
+
let m = heatmapModel();
|
|
13756
|
+
(m ? m.refs : []).forEach(function (c) {
|
|
13757
|
+
excluded[c.ref] = true;
|
|
13758
|
+
});
|
|
13759
|
+
}
|
|
13760
|
+
return excluded;
|
|
13761
|
+
}
|
|
13762
|
+
|
|
13763
|
+
function labelPropertiesOf(node) {
|
|
13764
|
+
return forester.labelPropertiesText(node, _state.labelProperties,
|
|
13765
|
+
_state.labelProperties ? null : labelFieldsExcluded());
|
|
13766
|
+
}
|
|
13767
|
+
|
|
13768
|
+
function propertiesCbClicked() {
|
|
13769
|
+
_state.showProperties = getCheckboxValue(PROPERTIES_CB);
|
|
13770
|
+
if (_state.showProperties) {
|
|
13771
|
+
_state.showExternalLabels = true;
|
|
13772
|
+
setCheckboxValue(EXTERNAL_LABEL_CB, true);
|
|
13773
|
+
}
|
|
13774
|
+
search0();
|
|
13775
|
+
search1();
|
|
13776
|
+
scheduleUpdate();
|
|
13777
|
+
}
|
|
13778
|
+
|
|
13779
|
+
// Which properties the label shows, and in what order: each field ticked
|
|
13780
|
+
// or not, and moved with the arrows (or the arrow keys). Apply keeps the ticked
|
|
13781
|
+
// ones in the order shown; picking a field that was not shown turns the
|
|
13782
|
+
// Properties label on, as the desktop does, since otherwise choosing it
|
|
13783
|
+
// would visibly do nothing. "All" goes back to every field.
|
|
13784
|
+
function showLabelFieldsDialog() {
|
|
13785
|
+
let all = labelFields();
|
|
13786
|
+
if (all.length < 1) {
|
|
13787
|
+
return;
|
|
13788
|
+
}
|
|
13789
|
+
let chosen = _state.labelProperties ? _state.labelProperties.filter(function (r) {
|
|
13790
|
+
return all.indexOf(r) >= 0;
|
|
13791
|
+
}) : null;
|
|
13792
|
+
// the chosen fields first, in their order, then the rest as offered;
|
|
13793
|
+
// with none chosen, ticked is what the label shows by default
|
|
13794
|
+
let excluded = chosen ? null : labelFieldsExcluded();
|
|
13795
|
+
let working = (chosen || []).concat(all.filter(function (r) {
|
|
13796
|
+
return !chosen || chosen.indexOf(r) < 0;
|
|
13797
|
+
})).map(function (ref) {
|
|
13798
|
+
return {ref: ref, on: chosen ? chosen.indexOf(ref) >= 0 : !excluded[ref]};
|
|
13799
|
+
});
|
|
13800
|
+
if (!chosen) {
|
|
13801
|
+
// the ticked ones first, so the list reads as the label does
|
|
13802
|
+
working = working.filter(function (w) {
|
|
13803
|
+
return w.on;
|
|
13804
|
+
}).concat(working.filter(function (w) {
|
|
13805
|
+
return !w.on;
|
|
13806
|
+
}));
|
|
13807
|
+
}
|
|
13808
|
+
let before = working.filter(function (w) {
|
|
13809
|
+
return w.on && (chosen !== null || _state.showProperties);
|
|
13810
|
+
}).map(function (w) {
|
|
13811
|
+
return w.ref;
|
|
13812
|
+
});
|
|
13813
|
+
let shell = makeDialogShell(LABEL_FIELDS_DIALOG, 'Label fields', 340);
|
|
13814
|
+
let body = shell.body;
|
|
13815
|
+
repsElement(body, 'div', 'aptx-reps-lead',
|
|
13816
|
+
'Tick the properties the labels show, and set their order with the arrows.');
|
|
13817
|
+
let list = repsElement(body, 'div', 'aptx-reorder');
|
|
13818
|
+
|
|
13819
|
+
function move(from, to) {
|
|
13820
|
+
if (to < 0 || to >= working.length || from === to) {
|
|
13821
|
+
return false;
|
|
13822
|
+
}
|
|
13823
|
+
working.splice(to, 0, working.splice(from, 1)[0]);
|
|
13824
|
+
draw();
|
|
13825
|
+
return true;
|
|
13826
|
+
}
|
|
13827
|
+
|
|
13828
|
+
function draw() {
|
|
13829
|
+
list.textContent = '';
|
|
13830
|
+
working.forEach(function (w, i) {
|
|
13831
|
+
let row = repsElement(list, 'div', 'aptx-reorder-row');
|
|
13832
|
+
row.tabIndex = 0;
|
|
13833
|
+
let box = repsElement(row, 'input', 'aptx-reorder-check');
|
|
13834
|
+
box.type = 'checkbox';
|
|
13835
|
+
box.checked = w.on;
|
|
13836
|
+
box.addEventListener('change', function () {
|
|
13837
|
+
w.on = box.checked;
|
|
13838
|
+
});
|
|
13839
|
+
repsElement(row, 'span', 'aptx-reorder-name', forester.propertyDisplayName(w.ref)).title = w.ref;
|
|
13840
|
+
let up = repsElement(row, 'button', 'aptx-reorder-move', '↑');
|
|
13841
|
+
let down = repsElement(row, 'button', 'aptx-reorder-move', '↓');
|
|
13842
|
+
up.type = 'button';
|
|
13843
|
+
down.type = 'button';
|
|
13844
|
+
up.title = 'Move up';
|
|
13845
|
+
down.title = 'Move down';
|
|
13846
|
+
up.disabled = (i === 0);
|
|
13847
|
+
down.disabled = (i === working.length - 1);
|
|
13848
|
+
up.addEventListener('click', function () {
|
|
13849
|
+
if (move(i, i - 1)) {
|
|
13850
|
+
list.children[i - 1].focus();
|
|
13851
|
+
}
|
|
13852
|
+
});
|
|
13853
|
+
down.addEventListener('click', function () {
|
|
13854
|
+
if (move(i, i + 1)) {
|
|
13855
|
+
list.children[i + 1].focus();
|
|
13856
|
+
}
|
|
13857
|
+
});
|
|
13858
|
+
row.addEventListener('keydown', function (event) {
|
|
13859
|
+
if (event.key === ' ' && event.target === row) {
|
|
13860
|
+
event.preventDefault();
|
|
13861
|
+
box.checked = !box.checked;
|
|
13862
|
+
w.on = box.checked;
|
|
13863
|
+
return;
|
|
13864
|
+
}
|
|
13865
|
+
let d = (event.key === 'ArrowUp') ? -1 : ((event.key === 'ArrowDown') ? 1 : 0);
|
|
13866
|
+
if (d === 0) {
|
|
13867
|
+
return;
|
|
13868
|
+
}
|
|
13869
|
+
event.preventDefault();
|
|
13870
|
+
if (move(i, i + d)) {
|
|
13871
|
+
list.children[i + d].focus();
|
|
13872
|
+
}
|
|
13873
|
+
});
|
|
13874
|
+
});
|
|
13875
|
+
}
|
|
13876
|
+
draw();
|
|
13877
|
+
|
|
13878
|
+
let actions = repsElement(body, 'div', 'aptx-reps-actions');
|
|
13879
|
+
repsButton(actions, 'All', false, function () {
|
|
13880
|
+
_state.labelProperties = null;
|
|
13881
|
+
shell.dialog.close();
|
|
13882
|
+
scheduleUpdate();
|
|
13883
|
+
});
|
|
13884
|
+
repsButton(actions, 'Cancel', false, function () {
|
|
13885
|
+
shell.dialog.close();
|
|
13886
|
+
});
|
|
13887
|
+
repsButton(actions, 'Apply', true, function () {
|
|
13888
|
+
_state.labelProperties = working.filter(function (w) {
|
|
13889
|
+
return w.on;
|
|
13890
|
+
}).map(function (w) {
|
|
13891
|
+
return w.ref;
|
|
13892
|
+
});
|
|
13893
|
+
let added = _state.labelProperties.some(function (r) {
|
|
13894
|
+
return before.indexOf(r) < 0;
|
|
13895
|
+
});
|
|
13896
|
+
if (added && !_state.showProperties) {
|
|
13897
|
+
_state.showProperties = true;
|
|
13898
|
+
_state.showExternalLabels = true;
|
|
13899
|
+
setCheckboxValue(PROPERTIES_CB, true);
|
|
13900
|
+
setCheckboxValue(EXTERNAL_LABEL_CB, true);
|
|
13901
|
+
}
|
|
13902
|
+
shell.dialog.close();
|
|
13903
|
+
search0();
|
|
13904
|
+
search1();
|
|
13905
|
+
scheduleUpdate();
|
|
13906
|
+
});
|
|
13907
|
+
shell.dialog.showModal();
|
|
13908
|
+
if (list.firstChild) {
|
|
13909
|
+
list.firstChild.focus();
|
|
13910
|
+
}
|
|
13911
|
+
}
|
|
13912
|
+
|
|
13596
13913
|
function sequenceCbClicked() {
|
|
13597
13914
|
_state.showSequence = getCheckboxValue(SEQUENCE_CB);
|
|
13598
13915
|
if (_state.showSequence) {
|
|
@@ -16304,6 +16621,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
16304
16621
|
on(TAXONOMY_CB, 'click', taxonomyCbClicked);
|
|
16305
16622
|
|
|
16306
16623
|
on(SEQUENCE_CB, 'click', sequenceCbClicked);
|
|
16624
|
+
on(PROPERTIES_CB, 'click', propertiesCbClicked);
|
|
16625
|
+
on(LABEL_FIELDS_BUTTON, 'click', showLabelFieldsDialog);
|
|
16307
16626
|
|
|
16308
16627
|
on(CONFIDENCE_VALUES_CB, 'click', confidenceValuesCbClicked);
|
|
16309
16628
|
on(SUPPORT_DOTS_CB, 'click', supportDotsCbClicked);
|
|
@@ -16832,6 +17151,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
16832
17151
|
if (_basicTreeProperties.sequences) {
|
|
16833
17152
|
labels.push(makeCheckboxItem('Sequence', SEQUENCE_CB, 'to show/hide node sequence information'));
|
|
16834
17153
|
}
|
|
17154
|
+
if (labelFields().length > 0) {
|
|
17155
|
+
labels.push(makeCheckboxItem('Properties', PROPERTIES_CB, 'to show/hide the nodes\' property values'
|
|
17156
|
+
+ ' (Label fields… chooses which, and in what order)'));
|
|
17157
|
+
}
|
|
16835
17158
|
if (_nodeLabels) {
|
|
16836
17159
|
for (const [key, value] of Object.entries(_nodeLabels)) {
|
|
16837
17160
|
if (value.label && value.propertyRef && value.description) {
|
|
@@ -16913,6 +17236,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
16913
17236
|
|
|
16914
17237
|
let h = '<fieldset><legend>Display Data</legend>';
|
|
16915
17238
|
h = h.concat(makeCheckboxGroup('Labels', labels));
|
|
17239
|
+
if (labelFields().length > 0) {
|
|
17240
|
+
h = h.concat('<div class="aptx-domrow"><span class="aptx-domlabel"></span>'
|
|
17241
|
+
+ '<input type="button" class="aptx-widebtn" value="Label fields…" name="' + LABEL_FIELDS_BUTTON
|
|
17242
|
+
+ '" id="' + LABEL_FIELDS_BUTTON + '" title="choose which properties the labels show, and in what'
|
|
17243
|
+
+ ' order"></div>');
|
|
17244
|
+
}
|
|
16916
17245
|
h = h.concat(makeCheckboxGroup('Nodes', nodes));
|
|
16917
17246
|
h = h.concat(makeCheckboxGroup('Options', opts));
|
|
16918
17247
|
h = h.concat('</fieldset>');
|
|
@@ -17237,6 +17566,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
17237
17566
|
setCheckboxValue(NODE_NAME_CB, _state.showNodeName);
|
|
17238
17567
|
setCheckboxValue(TAXONOMY_CB, _state.showTaxonomy);
|
|
17239
17568
|
setCheckboxValue(SEQUENCE_CB, _state.showSequence)
|
|
17569
|
+
setCheckboxValue(PROPERTIES_CB, _state.showProperties);
|
|
17240
17570
|
setCheckboxValue(CONFIDENCE_VALUES_CB, _state.showConfidenceValues);
|
|
17241
17571
|
setCheckboxValue(SUPPORT_DOTS_CB, _state.showSupportDots);
|
|
17242
17572
|
syncMadValuesCheckbox();
|
|
@@ -17911,9 +18241,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
17911
18241
|
}
|
|
17912
18242
|
let name = _root_const.name ? String(_root_const.name).trim() : '';
|
|
17913
18243
|
// what the phylogeny itself states goes with the part: without its
|
|
17914
|
-
// branch_length_unit a subtree saved from Div opened again as time
|
|
18244
|
+
// branch_length_unit a subtree saved from Div opened again as time,
|
|
18245
|
+
// and without its properties (the figure setting) the part opened
|
|
18246
|
+
// without the heat map the whole tree opens with
|
|
17915
18247
|
let part = Object.assign({}, _root, {name: name ? name + ' (subtree)' : undefined});
|
|
17916
|
-
['rooted', 'rerootable', 'type', 'branch_length_unit'].forEach(function (k) {
|
|
18248
|
+
['rooted', 'rerootable', 'type', 'branch_length_unit', 'properties'].forEach(function (k) {
|
|
17917
18249
|
if (_root_const[k] !== undefined && _root_const[k] !== null) {
|
|
17918
18250
|
part[k] = _root_const[k];
|
|
17919
18251
|
}
|
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.20.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.20.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",
|