archaeopteryx 3.24.0 → 3.26.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 CHANGED
@@ -64,23 +64,32 @@ Archaeopteryx.js has the following dependencies:
64
64
  * d3.js (version 7): https://www.npmjs.com/package/d3
65
65
  * sax.js (1.6.1): https://www.npmjs.com/package/sax/v/1.6.1
66
66
 
67
- For **raster PNG export** (optional — the PNG entry appears in the Download
68
- menu only when `window.Canvg` is present):
69
- * canvg (4.x): https://www.npmjs.com/package/canvg — publishes ES modules
70
- only (no classic-script global build), so bridge it yourself:
71
- ```html
72
- <script type="module">
73
- import {Canvg} from 'http://path/to/canvg.js'; // a self-contained build; see below
74
- window.Canvg = Canvg;
75
- </script>
76
- ```
77
- A module script is deferred regardless of where it sits in the page, and
78
- PNG export only runs later from a Download click, so this can go anywhere
79
- before the click — it does not need to precede archaeopteryx.js's own
80
- `<script>` tag. `docs/lib/canvg.js` in this repository is one such
81
- self-contained build (npm's `canvg@4.0.3` bundled into one file, e.g. via
82
- `esm.sh/canvg@4?bundle`, with its one unnecessary Node-environment shim
83
- import removed — see the file's own header comment).
67
+ For **raster PNG export** (optional — the PNG entry, the Copy PNG button and
68
+ the PNG-scale row appear only when `window.Canvg` is present):
69
+ * canvg (4.x): https://www.npmjs.com/package/canvg. **Load it before
70
+ `launch()` is called:** the Download menu looks for it at click time, but
71
+ the Copy PNG button and the PNG-scale row are built with the control
72
+ panel at launch, so a page that provides it later gets a PNG entry and
73
+ nothing else. canvg publishes ES modules only (no classic-script global
74
+ build); this repository ships two ready-made files:
75
+ * `docs/lib/canvg.global.js` — a classic-script build for a plain
76
+ `<script src="canvg.global.js"></script>`, or for one more file in a
77
+ site's concatenated bundle; it defines `window.Canvg` itself. This is
78
+ what the demo pages use. (Generated from `canvg.js` by
79
+ `test/make_canvg_global.js`; a unit test keeps the two in step.)
80
+ * `docs/lib/canvg.js` — the self-contained ES module build (npm's
81
+ `canvg@4.0.3` bundled into one file via `esm.sh/canvg@4?bundle`, with
82
+ its one unnecessary Node-environment shim import removed — see the
83
+ file's own header), for a page that prefers a module bridge of its own:
84
+ ```html
85
+ <script type="module">
86
+ import {Canvg} from 'http://path/to/canvg.js';
87
+ window.Canvg = Canvg;
88
+ </script>
89
+ ```
90
+ A module script runs after the document is parsed, so a page that calls
91
+ `launch()` from an inline script during parsing must use the classic
92
+ build instead.
84
93
 
85
94
  For **vector PDF export** (optional — the PDF entry appears in the Download
86
95
  menu only when both are loaded before archaeopteryx.js):
@@ -246,10 +255,14 @@ program's.
246
255
 
247
256
  A phylogram carries a **scale bar** at the bottom left: a round number of
248
257
  branch-length units (1, 2 or 5 × 10ᵏ, whichever makes the bar about 100 px)
249
- with its length written above it. It is drawn with the tree, so it zooms and
250
- exports with it and its label always holds. A cladogram has nothing to
251
- measure and shows none, and a tree under a scale axis leaves the measuring to
252
- the axis.
258
+ with its length written above it, followed by the unit the tree states
259
+ (phyloXML's `branch_length_unit`, e.g. `0.05 subs/site`) and by nothing when
260
+ it states none: a tree that does not say what its lengths measure gets no
261
+ guess. It is drawn with the tree, so it zooms and exports with it and its
262
+ label always holds. A cladogram has nothing to measure and shows none, and a
263
+ tree under a scale axis leaves the measuring to the axis. **Scale** under
264
+ Display Data > Options hides it (on by default; config `showScaleBar`, view
265
+ key `scaleBar`; the desktop's switch has the same name and defaults to off).
253
266
 
254
267
  **Scale Axis** under Display Data (off by default, as on the desktop) draws a
255
268
  labelled ruler under a phylogram, from 0 at the root out to the deepest tip, in
@@ -1056,8 +1069,8 @@ Both entry points take **exactly** the arguments shown — a call with the old
1056
1069
  trailing arguments (the separate settings bag, `nodeVisualizations`,
1057
1070
  `nodeLabels`, `specialVisualizations`, or the positional Newick parse
1058
1071
  options) **throws** with a message saying where each one went: everything now
1059
- lives in the **one config object** (`nodeLabels` and
1060
- `internalNumericLabels` are config keys). `config` itself is optional — `archaeopteryx.launch('#phylogram1',
1072
+ lives in the **one config object** (`internalNumericLabels` is a config key;
1073
+ `nodeLabels` is retired, see the deprecated keys below). `config` itself is optional — `archaeopteryx.launch('#phylogram1',
1061
1074
  tree)` works.
1062
1075
 
1063
1076
  `container` is a **CSS selector or the DOM element itself** (frameworks hand
@@ -1543,6 +1556,7 @@ copy-pastable JSON.
1543
1556
  | `geneFamily` | `'auto'` | The annotation namespace that colours the genes and anchors the rows: `'auto'` (the one most genes carry) or a prefix such as `'pgfam'`. |
1544
1557
  | `geneLabels` | `'genes'` | Where gene symbols go: `'genes'` (on the arrows), `'legend'` (a card of the families), or `'none'`. |
1545
1558
  | `showScaleAxis` | tree-derived | Open with the scale axis shown: the time axis on a time tree whose branches show time, a distance axis on any other phylogram. Default: on when the tree carries `<date>` elements and opens shown in time, off otherwise (a dated tree that opens in divergence is a divergence tree on screen) — an explicit `true`/`false` overrides that. |
1559
+ | `showScaleBar` | `true` | Open with the scale bar at the bottom left of a phylogram (a round length in the tree's branch-length unit). It gives way while the scale axis is shown either way. |
1546
1560
  | `showScaleGrid` | `false` | Open with the Scale Grid lines on (only meaningful — and only offered as a checkbox — while the scale axis itself is shown). |
1547
1561
  | `showSupportDots` | `false` | Open with the Support Dots marks on (the checkbox appears whenever the tree has confidences). |
1548
1562
  | `supportDotMinimum` | `95` | Support Dots threshold, as a percentage. On a tree whose confidences top out at 1 (posterior probabilities) it is read on the 0–1 scale, so the default means ≥ 0.95 there and ≥ 95 on a bootstrap tree. |
@@ -1551,14 +1565,13 @@ copy-pastable JSON.
1551
1565
  | `onViewChange` | `null` | `function (state, encoded)`, called once per settled redraw when the view changed: `state` as `getViewState()` returns it, `encoded` its hash-ready string. The demo pages write it into the URL. |
1552
1566
  | `searchBinitialValue` | `null` | Prefill search box B. |
1553
1567
  | `enableVisualizations` | `true` | Offer the Color / Shape visualizations (which fields they cover is decided from the tree). |
1554
- | `initialVisualization` | `null` | The visualization to open with, by its Color-menu name (e.g. `'Host'`; case-insensitive). A name the tree cannot honour logs a console warning and falls back to the automatic choice, so a site-wide value is safe on trees without that field. Default: Archaeopteryx.js picks the most informative field itself. |
1568
+ | `initialVisualization` | `null` | The visualization to open with, by its Color-menu name (e.g. `'Host'`; case-insensitive). A name the tree cannot honour logs a console warning and falls back to the automatic choice, so a site-wide value is safe on trees without that field. A name the tree *can* honour is always honoured, but when it is a field the automatic choice would not have opened with -- more than 20 values, or a value on fewer than two thirds of the tips -- a console warning says so and names the field the viewer would have chosen. Default: Archaeopteryx.js picks the most informative field itself. |
1555
1569
  | `visualizationsLegendXpos` | `254` | Legend position, x. |
1556
1570
  | `visualizationsLegendYpos` | `30` | Legend position, y. |
1557
1571
  | `enableDownloads` | `true` | Offer the download buttons. |
1558
1572
  | `pngExportScale` | `4` | The PNG resolution the Download section starts on: 2, 4 or 8 (any other value is snapped to the nearest, with a console warning). |
1559
1573
  | `nhExportWriteConfidences` | `true` | Write confidences into exported Newick. |
1560
1574
  | `internalNumericLabels` | `'auto'` | Newick / Nexus parsing: how a bare numeric internal label (`)100:0.05`) is read. `'auto'` reads them as confidence values only when *every* internal label looks like support; `'confidence'` reads every numeric label as one, whatever its value; `'label'` keeps them as names. Replaces `nhConfidenceValuesAsInternalNames` (still accepted, with a warning; its `true` maps to `'confidence'`). |
1561
- | `nodeLabels` | `null` | Custom label-field checkboxes: `{key: {label, description, propertyRef, showButton, selected}}` — each adds a panel checkbox labelling nodes with the named property's value. (Was `launch()`'s sixth positional argument.) |
1562
1575
  | `enableSubtreeDeletion` | `true` | Offer node / subtree deletion in the node menu. |
1563
1576
  | `enableAccessToDatabases` | `true` | Offer the “Access DB” link in the node menu. |
1564
1577
  | `enableManualNodeSelection` | `false` | Add the Select/Deselect entries to the node menu. |
@@ -1581,7 +1594,7 @@ whatever it names.
1581
1594
 
1582
1595
  ### Accepted, with a warning
1583
1596
 
1584
- Four keys are neither current nor removed: they are accepted so an existing
1597
+ Five keys are neither current nor removed: they are accepted so an existing
1585
1598
  embed keeps working, and warn on the console.
1586
1599
 
1587
1600
  | Key | What happens |
@@ -1590,6 +1603,7 @@ embed keeps working, and warn on the console.
1590
1603
  | `nhConfidenceValuesInBrackets` | Retired: ignored. It gated whether `[95]` is read as a confidence, but setting it `false` never reinterpreted the bracket — it *discarded* it, so the option's only effect was to throw support values away. A bracket that is not a number is a Newick comment and was ignored either way, and NHX / BEAST blobs go through a different path. Bracketed values are now always read as confidences. |
1591
1604
  | `showTimeAxis` | Translated to `showScaleAxis`, value unchanged: the switch now also draws a distance axis on a tree that is not in time. An explicit `showScaleAxis` always wins. |
1592
1605
  | `timeAxisGrid` | Translated to `showScaleGrid`, value unchanged. |
1606
+ | `nodeLabels` | Retired in 3.25.0: ignored, with a warning. The custom label checkboxes it declared predate the Metadata checkbox and its "Metadata fields…" chooser, which offer exactly the fields the tree carries; a fixed site-wide list put checkboxes for absent fields on every tree. `labelProperties` sets the starting fields where the automatic choice is not wanted. Removed in a later release. |
1593
1607
 
1594
1608
  ### What replaced the rest
1595
1609
 
@@ -1734,6 +1748,21 @@ All 118 of them, alphabetically:
1734
1748
  | `visualizationsLegendXposOrig` | Internal bookkeeping; set visualizationsLegendXpos. |
1735
1749
  | `visualizationsLegendYposOrig` | Internal bookkeeping; set visualizationsLegendYpos. |
1736
1750
 
1751
+ ### Removed from `forester` — calling these throws `TypeError`
1752
+
1753
+ 3.0.0 also dropped twelve exports of `forester.js` that the viewer no longer
1754
+ used. They are gone, not stubbed, so a 2.x embedding that calls one fails with
1755
+ `TypeError: forester.<name> is not a function` — inside its own parse step, if
1756
+ that is where it called it.
1757
+
1758
+ | Function | Why, and what to do instead |
1759
+ | --- | --- |
1760
+ | `collectPropertyRefs` | Nothing to call. The viewer derives the Color-by candidates (`forester.visualizationCandidates(tree)`, below) and the label-field chooser's list from the tree itself, and substitutes a readable name property (`genome_name`) for identifier-like tip names on its own. A 2.x embedding that built `nodeLabels` from this function's result should delete that block together with the `nodeLabels` key; `labelProperties` sets the starting label fields where the automatic choice is not wanted. |
1761
+ | `collapse`, `unCollapse` | The subtree-collapse feature was removed; they were its only writers. |
1762
+ | `getChildren` | Only meant something while collapse existed; read `node.children`. |
1763
+ | `findByTaxonomyCode`, `findByTaxonomyScientificName` | Superseded by the search (the panel's search fields). |
1764
+ | `calcAverageTreeHeight`, `calcMaxDepth`, `calcBranchLengthSimpleStatistics`, `removeMaxBranchLength`, `isHasNodeData`, `getOneDistinctTaxonomy` | Never used by the viewer; nothing replaces them. |
1765
+
1737
1766
  ## The automatic visualization system (developer spec)
1738
1767
 
1739
1768
  This section, together with **Value grouping** below, is written to be
@@ -1895,17 +1924,29 @@ exists, and clears the per-legend chip states.
1895
1924
  Highest first, at each paint:
1896
1925
 
1897
1926
  * **Label colour**: search/selection highlight → active Color visualization
1898
- → `style:font_color` → phyloXML branch colour → theme ink.
1927
+ (its colour; the theme ink for a node without a value) → `style:font_color`
1928
+ → phyloXML branch colour → theme ink.
1899
1929
  * **Node fill**: highlight → duplication/speciation event colour → active
1900
- Color visualization → `style:node_color` (else `style:font_color`) →
1901
- background.
1930
+ Color visualization (its colour; a node without a value draws no dot,
1931
+ or a hollow one while Node events are shown) → `style:node_color` (else
1932
+ `style:font_color`) → background.
1902
1933
  * **Node outline**: darkened highlight → event colour → visualization fill
1903
- → style colour → branch colour → branch default.
1934
+ → style colour (not while a Color visualization is active) → branch colour
1935
+ → branch default.
1904
1936
  * **Node shape path**: suppressed for highlighted/event nodes; chosen Shape
1905
1937
  visualization → `style:node_shape`. A node earns its default dot when a
1906
1938
  Color visualization is active (and no shape was drawn), or when it
1907
1939
  carries `style:node_color` — `font_color` alone paints only the label.
1908
1940
 
1941
+ While a Color visualization is active the style colours are **not consulted
1942
+ for any node**, valued or not: not for the label, the dot, its outline, the
1943
+ hover glow or the clock plot's points. The visualization owns the colours and
1944
+ its legend explains them; a node without a value takes the default label
1945
+ colour and draws no dot. That is the desktop's rule (its TreePanel.setColor).
1946
+ Clearing the Color menu brings the file's colours back. Branch colours,
1947
+ shapes, font sizes and styles are not colours of the visualization's kind
1948
+ and stay.
1949
+
1909
1950
  ### Legend anatomy
1910
1951
 
1911
1952
  One draggable SVG card per active visualization (colour, then shape,
@@ -2102,8 +2143,9 @@ honoured: `style:font_color`, `style:node_color`, `style:node_shape`
2102
2143
  The **Visual Styles** checkbox — the desktop's "Visual Styles/Branch Colors",
2103
2144
  shown when the tree carries either, on by default — turns them off and on,
2104
2145
  and gates phyloXML `<color>` branch colours too, exactly as on the desktop.
2105
- An active Color visualization outranks `style:font_color`, also as on the
2106
- desktop: set the Color menu to default to see the tree as its file styled it.
2146
+ An active Color visualization outranks the style colours for every node,
2147
+ valued or not, on labels and node dots alike, as on the desktop: set the
2148
+ Color menu to default to see the tree as its file styled it.
2107
2149
  `style:` never appears in the Color / Shape menus — it is rendering, not data.
2108
2150
 
2109
2151
  ### Moving the legends
@@ -29,9 +29,13 @@ export interface Phylogeny extends PhylogenyNode {
29
29
 
30
30
  export type Layout = 'rectangular' | 'circular' | 'unrooted';
31
31
 
32
- /** One custom label-field entry (the config's nodeLabels values).
32
+ /** @deprecated Retired in 3.25.0 with `nodeLabels`: accepted with a console
33
+ * warning, no effect; removed in a later release. The Metadata checkbox and
34
+ * its "Metadata fields…" chooser offer the tree's own fields.
33
35
  *
34
- * Two modes:
36
+ * One custom label-field entry (the config's nodeLabels values).
37
+ *
38
+ * Two modes (3.0.0 to 3.24.0):
35
39
  * - a PANEL CHECKBOX needs label, description AND propertyRef set, plus
36
40
  * showButton: true -- omit showButton (or any of the three) and no
37
41
  * checkbox appears;
@@ -85,6 +89,10 @@ export interface ArchaeopteryxConfig {
85
89
  nhConfidenceValuesAsInternalNames?: boolean;
86
90
  nhConfidenceValuesInBrackets?: boolean;
87
91
  nhExportWriteConfidences?: boolean;
92
+ /** @deprecated Retired in 3.25.0: accepted with a console warning, no
93
+ * effect; removed in a later release. The Metadata checkbox and its
94
+ * "Metadata fields…" chooser offer the tree's own fields; labelProperties
95
+ * sets the starting fields. */
88
96
  nodeLabels?: Record<string, NodeLabelSpec> | null;
89
97
  /** Called once per settled redraw when the view changed: the state as
90
98
  * getViewState() returns it, and its hash-ready string. */
@@ -130,6 +138,8 @@ export interface ArchaeopteryxConfig {
130
138
  * branches show time, a distance axis on any other. Default: on for a
131
139
  * time tree, off otherwise. */
132
140
  showScaleAxis?: boolean;
141
+ /** The scale bar at the bottom left of a phylogram; on unless false. */
142
+ showScaleBar?: boolean;
133
143
  /** Grid lines (rings, in circular) at the scale axis's ticks. */
134
144
  showScaleGrid?: boolean;
135
145
  showSupportDots?: boolean;
@@ -176,8 +186,8 @@ export interface ViewState {
176
186
  shapeBy?: string;
177
187
  /** The panel's checked boxes: name, taxonomy, sequence, confidence,
178
188
  * madValues, branchLength, external, internal, nodeEvents, branchEvents,
179
- * supportDots, shortNames, autoHide, visualizations, visualStyles, and
180
- * custom:<key> for a nodeLabels checkbox. */
189
+ * supportDots, shortNames, autoHide, visualizations, visualStyles. (A
190
+ * custom:<key> flag from a 3.24.0 hash is read and ignored.) */
181
191
  show?: string[];
182
192
  font?: number;
183
193
  node?: number;
@@ -205,6 +215,7 @@ export interface ViewState {
205
215
  geneLabels?: 'none' | 'genes' | 'legend';
206
216
  scaleAxis?: boolean;
207
217
  scaleGrid?: boolean;
218
+ scaleBar?: boolean;
208
219
  /** @deprecated the name of scaleAxis in older links; still read. */
209
220
  timeAxis?: boolean;
210
221
  /** @deprecated the name of scaleGrid in older links; still read. */
package/archaeopteryx.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.24.0
23
+ // v 3.26.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.24.0';
106
+ const VERSION = '3.26.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';
@@ -390,6 +390,7 @@ function (root, d3, forester, phyloXml) {
390
390
  const GENE_LABELS_SELECT = 'gene_labels';
391
391
  const SCALE_AXIS_CB = 'scaleaxis_cb';
392
392
  const SCALE_GRID_CB = 'scalegrid_cb';
393
+ const SCALE_BAR_CB = 'scalebar_cb';
393
394
  const MSA_SCROLL_ID = 'aptxmsascroll';
394
395
  const FONT_SIZE_SLIDER = 'fs_sl';
395
396
  const EXTERNAL_LABEL_CB = 'extl_cb';
@@ -539,7 +540,6 @@ function (root, d3, forester, phyloXml) {
539
540
  let _container = null; // the resolved container ELEMENT (never a selector string)
540
541
  let _intervalId = 0;
541
542
  let _maxLabelLength = 0;
542
- let _nodeLabels = null;
543
543
  let _state = null; // live display state: what the control panel writes to
544
544
  let _root = null;
545
545
  let _root_const = null;
@@ -606,6 +606,7 @@ function (root, d3, forester, phyloXml) {
606
606
  const DOMAIN_SCALE_HEADROOM = 0.9; // drawn length over reserved length
607
607
  const DOMAIN_BOX_MIN_H = 6;
608
608
  const DOMAIN_BOX_MAX_H = 16;
609
+ const LEGEND_MAX_ROWS = 20; // value rows a legend lists before "n more"; the rest fold
609
610
  const DOMAIN_BACKBONE_COLOR = 'rgb(100,100,100)'; // the same in both themes and in exports
610
611
  const DOMAIN_SHADOW_COLOR = 'rgb(8,18,21)';
611
612
  const DOMAIN_SHADOWS = [[0.4, 0.7, 40], [0.9, 1.5, 28], [1.6, 2.5, 17]]; // dx, dy, alpha of 255, in drawing order
@@ -1447,8 +1448,9 @@ function (root, d3, forester, phyloXml) {
1447
1448
  // a node coloured by Host Group glows in its host colour -- and the UI
1448
1449
  // accent otherwise. As on the desktop (TreePanel.hoverGlowColor). The
1449
1450
  // precedence is the one the node itself is painted by: the Color-by
1450
- // value, then a per-node visual style, then an event colour, then a
1451
- // colourised clade's branch colour. NEVER the found/search colour: a
1451
+ // value, then a per-node visual style (not while a Color-by is active,
1452
+ // see nodeStyleColor), then an event colour, then a colourised clade's
1453
+ // branch colour. NEVER the found/search colour: a
1452
1454
  // focus ring must not read as a selection state.
1453
1455
  //
1454
1456
  // The glow is a 13-23% alpha wash, so a dark or a pale colour would
@@ -1462,10 +1464,7 @@ function (root, d3, forester, phyloXml) {
1462
1464
  c = vis;
1463
1465
  }
1464
1466
  if (!c) {
1465
- let style = nodeStyle(d);
1466
- if (style && (style.nodeColor || style.fontColor)) {
1467
- c = style.nodeColor || style.fontColor;
1468
- }
1467
+ c = nodeStyleColor(d);
1469
1468
  }
1470
1469
  if (!c && _state.showNodeEvents && d.events && d.children) {
1471
1470
  c = makeNodeEventsDependentColor(d.events);
@@ -1948,7 +1947,32 @@ function (root, d3, forester, phyloXml) {
1948
1947
  // an explicit request is honoured even for a wide field (21+
1949
1948
  // values) the automatic path would only offer, never impose;
1950
1949
  // autoColorId too, so it survives subtree views like the
1951
- // automatic pick does
1950
+ // automatic pick does.
1951
+ //
1952
+ // Honoured, but not in silence. A request for a field the
1953
+ // automatic choice would not open with -- wide, or sparse (on
1954
+ // fewer than 2/3 of the tips; it opens only when nothing denser
1955
+ // exists) -- says so in the console and names the field the
1956
+ // viewer would have chosen. BV-BRC's virus trees carried a
1957
+ // site-wide 'Host' from the influenza days: family trees opened
1958
+ // coloured by 39 to 168 hosts while Genus (4 to 15 values) sat
1959
+ // in the file, and nothing anywhere said the viewer had been
1960
+ // overruled (Christian, 2026-10-08: a console warning when a
1961
+ // sub-optimal visualization is forced is needed). A request for
1962
+ // a clean field that merely ranks below the automatic pick is a
1963
+ // site's taste and warns nothing; nor does a sparse field that
1964
+ // is the automatic pick itself.
1965
+ let auto = forester.openingVisualization(_vis.candidates);
1966
+ if ((requested.wide || requested.sparse) && !(auto && auto.id === requested.id)) {
1967
+ let why = requested.wide
1968
+ ? requested.values.length + ' distinct values on this tree, more than the '
1969
+ + LEGEND_MAX_ROWS + ' the legend lists'
1970
+ : 'a value on ' + requested.coverage + ' of the tree\'s ' + requested.total + ' tips';
1971
+ console.warn(WARNING + ': initialVisualization "' + _settings.initialVisualization
1972
+ + '" names a field with ' + why + '; left to itself the viewer would '
1973
+ + (auto ? 'open with "' + auto.label + '" (' + auto.values.length + ' values)'
1974
+ : 'open uncoloured'));
1975
+ }
1952
1976
  _vis.autoColorId = requested.id;
1953
1977
  _vis.colorId = requested.id;
1954
1978
  _state.showVisualizations = true;
@@ -2144,6 +2168,7 @@ function (root, d3, forester, phyloXml) {
2144
2168
  }
2145
2169
  dialog.addEventListener('close', close);
2146
2170
 
2171
+ let placed = false; // set by place() once the picker has been put somewhere
2147
2172
  render(false);
2148
2173
  dialog.show();
2149
2174
  place();
@@ -2151,40 +2176,67 @@ function (root, d3, forester, phyloXml) {
2151
2176
  // Beside the legend it edits, never on top of it -- a picker that hides
2152
2177
  // the swatch it is changing defeats the live preview.
2153
2178
  //
2154
- // Re-measured rather than remembered, for two reasons: every redraw
2155
- // DESTROYS and rebuilds the legend group, so a held reference goes
2156
- // stale and measures zero; and the card grows when the [reset colors]
2157
- // chip appears on the first pick, which would leave a
2158
- // positioned-once picker sitting over the counts. It only MOVES when
2159
- // it would otherwise overlap, so the dialog does not chase the legend
2160
- // around while the user drags.
2179
+ // Placed OUTRIGHT the first time, viewport-fixed. A non-modal <dialog>
2180
+ // appended to the body sits at its static position, the end of the
2181
+ // document: on the demo page that is the top of the window (the
2182
+ // container is out of flow), on a page that flows, such as BV-BRC's,
2183
+ // it is below everything on it. Until 2026-10-08 the picker moved
2184
+ // only when that spot overlapped the card, so on BV-BRC it opened
2185
+ // under the site's own bars, off the bottom of the window
2186
+ // (Christian's screenshot). Fixed positioning also keeps a scrolling
2187
+ // page from carrying it away.
2188
+ //
2189
+ // Re-measured rather than remembered afterwards, for two reasons:
2190
+ // every redraw DESTROYS and rebuilds the legend group, so a held
2191
+ // reference goes stale and measures zero; and the card grows when the
2192
+ // [reset colors] chip appears on the first pick, which would leave a
2193
+ // positioned-once picker sitting over the counts. Once placed it only
2194
+ // MOVES when it would otherwise overlap, so the dialog does not chase
2195
+ // the legend around while the user drags.
2161
2196
  function place() {
2162
2197
  let el = anchorClass ? document.querySelector('g.' + anchorClass) : null;
2163
2198
  let box = el ? el.getBoundingClientRect() : null;
2164
- if (!box || !box.width) {
2165
- return;
2199
+ if (box && !box.width) {
2200
+ box = null;
2166
2201
  }
2167
- let now = dialog.getBoundingClientRect();
2168
- let overlaps = now.width > 0
2169
- && !(now.left >= box.right || now.right <= box.left
2170
- || now.top >= box.bottom || now.bottom <= box.top);
2171
- if (now.width > 0 && !overlaps) {
2172
- return;
2202
+ if (placed) {
2203
+ if (!box) {
2204
+ return;
2205
+ }
2206
+ let now = dialog.getBoundingClientRect();
2207
+ let overlaps = now.width > 0
2208
+ && !(now.left >= box.right || now.right <= box.left
2209
+ || now.top >= box.bottom || now.bottom <= box.top);
2210
+ if (!overlaps) {
2211
+ return;
2212
+ }
2173
2213
  }
2174
2214
  let w = dialog.offsetWidth;
2175
2215
  let h = dialog.offsetHeight;
2176
- // to the right of the card, or to its left when that would leave
2177
- // the viewport
2178
- let left = box.right + 10;
2179
- if (left + w > window.innerWidth - 6) {
2180
- left = box.left - w - 10;
2216
+ let left;
2217
+ let top;
2218
+ if (box) {
2219
+ // to the right of the card, or to its left when that would
2220
+ // leave the viewport
2221
+ left = box.right + 10;
2222
+ if (left + w > window.innerWidth - 6) {
2223
+ left = box.left - w - 10;
2224
+ }
2225
+ top = box.top;
2226
+ } else {
2227
+ // no card to sit beside (the view has no swatch for the value):
2228
+ // the tree area's top left corner, inside the window
2229
+ let area = _container ? _container.getBoundingClientRect() : null;
2230
+ left = area ? area.left + 14 : 10;
2231
+ top = area ? Math.max(area.top, 0) + 14 : 10;
2181
2232
  }
2182
2233
  setStyles(dialog, {
2183
2234
  'position': 'fixed',
2184
2235
  'margin': '0',
2185
2236
  'left': Math.max(6, Math.min(window.innerWidth - w - 6, left)) + 'px',
2186
- 'top': Math.max(6, Math.min(window.innerHeight - h - 6, box.top)) + 'px'
2237
+ 'top': Math.max(6, Math.min(window.innerHeight - h - 6, top)) + 'px'
2187
2238
  });
2239
+ placed = true;
2188
2240
  }
2189
2241
  setTimeout(function () {
2190
2242
  document.addEventListener('pointerdown', onOutside, true);
@@ -2377,6 +2429,36 @@ function (root, d3, forester, phyloXml) {
2377
2429
  return ((_vis && _vis.colorId) ? _vis.byId[_vis.colorId] : null) || null;
2378
2430
  }
2379
2431
 
2432
+ // A Color visualization is on screen: it owns every node's colour, and the
2433
+ // file's style colours wait until the Color menu is cleared (the desktop's
2434
+ // rule; see makeLabelColor and makeNodeFillColor).
2435
+ function colorVisActive() {
2436
+ return !!_state.showVisualizations && !!currentColorVis();
2437
+ }
2438
+
2439
+ // The colour a node's style gives it, or null: style:node_color (else
2440
+ // style:font_color) for the node's mark, style:font_color for its label.
2441
+ // Null while a Color visualization is active -- the desktop's rule
2442
+ // (TreePanel.setColor): the visualization owns every node's colour and
2443
+ // its legend explains them; a node without a value takes the default ink
2444
+ // and no dot rather than the file's colour. Until 2026-10-08 the valueless
2445
+ // nodes fell through to the file's colour on their dots, so BV-BRC's
2446
+ // trees -- style:font_color on every tip, following Genus -- showed two
2447
+ // palettes under Color by Host (Christian: adopt the desktop's rule).
2448
+ // EVERY reader of a style colour goes through here, so none can forget
2449
+ // the rule: the label, the dot, its outline, the hover glow, the clock
2450
+ // plot's points (which a first version of the rule missed).
2451
+ function nodeStyleColor(node, forLabel) {
2452
+ if (colorVisActive()) {
2453
+ return null;
2454
+ }
2455
+ let style = nodeStyle(node);
2456
+ if (!style) {
2457
+ return null;
2458
+ }
2459
+ return (forLabel ? style.fontColor : (style.nodeColor || style.fontColor)) || null;
2460
+ }
2461
+
2380
2462
  function currentShapeVis() {
2381
2463
  return ((_vis && _vis.shapeId) ? _vis.byId[_vis.shapeId] : null) || null;
2382
2464
  }
@@ -2521,7 +2603,6 @@ function (root, d3, forester, phyloXml) {
2521
2603
  // is then just its "no value" row
2522
2604
  const isRange = kind === 'color' && colorModeOf(vis) === 'range' && vis.values.length > 0;
2523
2605
 
2524
- const LEGEND_MAX_ROWS = 20;
2525
2606
  let rows = [];
2526
2607
  let hidden = 0;
2527
2608
  if (!isRange) {
@@ -3624,11 +3705,9 @@ function (root, d3, forester, phyloXml) {
3624
3705
  if (visColor !== _state.backgroundColorDefault) {
3625
3706
  return visColor;
3626
3707
  }
3627
- let style = nodeStyle(phynode);
3628
- if (style && (style.nodeColor || style.fontColor)) {
3629
- return style.nodeColor || style.fontColor;
3630
- }
3631
- return visColor;
3708
+ // the style colour, or the background -- which, under an active Color
3709
+ // visualization, drawTreeGeometry turns into no dot at all
3710
+ return nodeStyleColor(phynode) || visColor;
3632
3711
  };
3633
3712
 
3634
3713
  // A darker shade of a node's found/selected highlight color, used as a thin
@@ -3661,10 +3740,11 @@ function (root, d3, forester, phyloXml) {
3661
3740
  return v;
3662
3741
  }
3663
3742
  }
3664
- let style = nodeStyle(phynode);
3665
- if (style && (style.nodeColor || style.fontColor)) {
3666
- return style.nodeColor || style.fontColor;
3743
+ let styleColor = nodeStyleColor(phynode);
3744
+ if (styleColor) {
3745
+ return styleColor;
3667
3746
  }
3747
+ // the branch colour, unlike a style colour, holds under a Color-by (desktop)
3668
3748
  if (_state.useVisualStyles && phynode.color) {
3669
3749
  let c = phynode.color;
3670
3750
  return "rgb(" + c.red + "," + c.green + "," + c.blue + ")";
@@ -3677,17 +3757,15 @@ function (root, d3, forester, phyloXml) {
3677
3757
  if (foundColor) {
3678
3758
  return foundColor;
3679
3759
  }
3680
- if (_state.showVisualizations && currentColorVis()) {
3681
- let color = makeVisLabelColor(phynode);
3682
- if (color) {
3683
- return color;
3684
- }
3760
+ // an active Color visualization owns the colours: its colour where
3761
+ // the node has a value, the default ink where it has none (and
3762
+ // nodeStyleColor is null meanwhile, see there)
3763
+ if (colorVisActive()) {
3764
+ return makeVisLabelColor(phynode);
3685
3765
  }
3686
- // an active Color visualization outranks style:font_color, as on the
3687
- // desktop; clear the Color menu to see the tree as its file styled it
3688
- let style = nodeStyle(phynode);
3689
- if (style && style.fontColor) {
3690
- return style.fontColor;
3766
+ let fontColor = nodeStyleColor(phynode, true);
3767
+ if (fontColor) {
3768
+ return fontColor;
3691
3769
  }
3692
3770
  if (_state.useVisualStyles && phynode.color) {
3693
3771
  let c = phynode.color;
@@ -4725,25 +4803,6 @@ function (root, d3, forester, phyloXml) {
4725
4803
  l = append(l, labelPropertiesOf(phynode));
4726
4804
  }
4727
4805
 
4728
- if (_nodeLabels && phynode.properties) {
4729
- const props_length = phynode.properties.length;
4730
- if (props_length > 0) {
4731
- for (const value of Object.values(_nodeLabels)) {
4732
- if (value.selected === true && value.propertyRef) {
4733
- let prop_text = '';
4734
- for (let pm = 0; pm < props_length; ++pm) {
4735
- if (phynode.properties[pm].ref === value.propertyRef && phynode.properties[pm].datatype === 'xsd:string' && forester.isNodeScopedProperty(phynode.properties[pm])) {
4736
- if (prop_text.length > 0) {
4737
- prop_text += ', '
4738
- }
4739
- prop_text += phynode.properties[pm].value;
4740
- }
4741
- }
4742
- l = append(l, prop_text);
4743
- }
4744
- }
4745
- }
4746
- }
4747
4806
 
4748
4807
 
4749
4808
  if (_state.showDistributions && phynode.distributions && phynode.distributions.length > 0) {
@@ -5522,6 +5581,21 @@ function (root, d3, forester, phyloXml) {
5522
5581
  return v;
5523
5582
  },
5524
5583
  note: 'the grid follows the scale axis, time or distance'
5584
+ },
5585
+ // Retired in 3.25.0 (Christian, 2026-10-08: "go with the deprecation").
5586
+ // The custom label checkboxes it declared -- a site's fixed list of
5587
+ // property refs, one panel checkbox each, labelling nodes with the
5588
+ // property's value -- predate the Metadata checkbox and its
5589
+ // "Metadata fields…" chooser, which offer exactly the fields the tree
5590
+ // carries and replace identifier-like tip names with a name property
5591
+ // on their own. On BV-BRC the list, written for influenza, put eight
5592
+ // checkboxes on every virus tree, six of them for fields the tree
5593
+ // did not have. The checkbox code went with the key (the label
5594
+ // text, the panel rows, the view state's custom:<key> flags, which
5595
+ // an old hash may still carry and the reader now ignores). Accepted
5596
+ // with this warning for one release cycle, then REMOVED_CONFIG.
5597
+ nodeLabels: {
5598
+ note: 'the Metadata checkbox and its "Metadata fields…" chooser offer the tree\'s own fields; labelProperties sets the starting fields'
5525
5599
  }
5526
5600
  };
5527
5601
 
@@ -5699,6 +5773,7 @@ function (root, d3, forester, phyloXml) {
5699
5773
  'geneFamily',
5700
5774
  'geneLabels',
5701
5775
  'showScaleAxis',
5776
+ 'showScaleBar',
5702
5777
  'showScaleGrid',
5703
5778
  'showSupportDots',
5704
5779
  'searchAinitialValue',
@@ -5721,7 +5796,6 @@ function (root, d3, forester, phyloXml) {
5721
5796
  'internalNumericLabels',
5722
5797
  'ladderizeTree',
5723
5798
  'nhExportWriteConfidences',
5724
- 'nodeLabels',
5725
5799
  'onViewChange',
5726
5800
  'panelDensity',
5727
5801
  'pngExportScale',
@@ -6066,6 +6140,8 @@ function (root, d3, forester, phyloXml) {
6066
6140
  _state.showScaleAxis = !!(_timeInfo && _timeInfo.type) && !branchesShowDivergence();
6067
6141
  }
6068
6142
  _state.showScaleGrid = _state.showScaleGrid === true; // desktop default: off
6143
+ // the scale bar is on unless the caller says no; it gives way to the axis anyway
6144
+ _state.showScaleBar = _state.showScaleBar !== false;
6069
6145
  _state.showSupportDots = _state.showSupportDots === true;
6070
6146
  _state.showNodeEvents = _basicTreeProperties.nodeEvents === true;
6071
6147
  _state.showBranchEvents = _basicTreeProperties.branchEvents === true;
@@ -6321,19 +6397,6 @@ function (root, d3, forester, phyloXml) {
6321
6397
  } else if (typeof _settings.onViewChange !== 'function') {
6322
6398
  throw new Error(ERROR + '"onViewChange" must be a function (state, encoded) or null');
6323
6399
  }
6324
- if (_settings.nodeLabels === undefined) {
6325
- _settings.nodeLabels = null;
6326
- } else if (_settings.nodeLabels !== null && typeof _settings.nodeLabels !== 'object') {
6327
- throw new Error('nodeLabels must be an object (see the README) or null');
6328
- }
6329
- if (_settings.nodeLabels) {
6330
- // a DEEP copy: the viewer writes runtime state into these specs
6331
- // (the checkbox element id, the live checked state), and the
6332
- // caller's config object must stay declarative -- an embedder
6333
- // reusing one nodeLabels constant across launches must not
6334
- // inherit the previous session's checkbox state
6335
- _settings.nodeLabels = JSON.parse(JSON.stringify(_settings.nodeLabels));
6336
- }
6337
6400
  if (_settings.pngExportScale === undefined) {
6338
6401
  _settings.pngExportScale = 4;
6339
6402
  } else if (typeof _settings.pngExportScale !== 'number'
@@ -6593,7 +6656,7 @@ function (root, d3, forester, phyloXml) {
6593
6656
  + ' trailing arguments were removed. The separate settings bag merged into the one'
6594
6657
  + ' config object; "nodeVisualizations" and "specialVisualizations" are gone for good'
6595
6658
  + ' (visualizations are derived automatically from the tree itself); "nodeLabels"'
6596
- + ' moved into the config as its "nodeLabels" key.');
6659
+ + ' is retired (the Metadata checkbox offers the tree\'s own fields).');
6597
6660
  }
6598
6661
  if (phylo === undefined || phylo === null) {
6599
6662
  throw new Error(ERROR + 'input tree is undefined or null');
@@ -6684,9 +6747,8 @@ function (root, d3, forester, phyloXml) {
6684
6747
  .on('zoom', zoom);
6685
6748
  _basicTreeProperties = forester.collectBasicTreeProperties(_treeData);
6686
6749
 
6687
- // Every launch starts from a clean slate: _vis is rebuilt below and
6688
- // _nodeLabels reassigned, never inherited from a previous launch in
6689
- // the same page.
6750
+ // Every launch starts from a clean slate: _vis is rebuilt below, never
6751
+ // inherited from a previous launch in the same page.
6690
6752
  _radialRotation = 0;
6691
6753
  _radialLabelsHorizontal = false;
6692
6754
  _msaColOffset = 0;
@@ -6704,7 +6766,6 @@ function (root, d3, forester, phyloXml) {
6704
6766
  // candidate ranking over 13k tips before the card could even paint.
6705
6767
  checkLayoutValue(cfg.state ? cfg.state.layout : undefined);
6706
6768
  initializeSettings(cfg.settings);
6707
- _nodeLabels = _settings.nodeLabels || null;
6708
6769
 
6709
6770
  // whatever a previous launch left in this container (or in the
6710
6771
  // container before it) goes now, so the old tree does not sit under
@@ -9896,20 +9957,6 @@ function (root, d3, forester, phyloXml) {
9896
9957
  return found;
9897
9958
  }
9898
9959
 
9899
- // The config's custom label checkboxes (nodeLabels) that are offered
9900
- function customLabelEntries() {
9901
- let out = [];
9902
- if (_nodeLabels) {
9903
- Object.keys(_nodeLabels).forEach(function (key) {
9904
- let v = _nodeLabels[key];
9905
- if (v && v.label && v.showButton === true && v.propertyRef && v.description) {
9906
- out.push({key: key, entry: v});
9907
- }
9908
- });
9909
- }
9910
- return out;
9911
- }
9912
-
9913
9960
  function searchStateOf(idx) {
9914
9961
  let spec = currentSearchSpec(idx);
9915
9962
  let value = (spec.value === null || spec.value === undefined) ? '' : String(spec.value);
@@ -9957,11 +10004,6 @@ function (root, d3, forester, phyloXml) {
9957
10004
  }).map(function (f) {
9958
10005
  return f[0];
9959
10006
  });
9960
- customLabelEntries().forEach(function (c) {
9961
- if (c.entry.selected === true) {
9962
- s.show.push('custom:' + c.key);
9963
- }
9964
- });
9965
10007
  s.font = _state.externalNodeFontSize;
9966
10008
  s.node = _state.nodeSizeDefault;
9967
10009
  s.branch = _state.branchWidthDefault;
@@ -10005,6 +10047,7 @@ function (root, d3, forester, phyloXml) {
10005
10047
  if (_basicTreeProperties.branchLengths === true) {
10006
10048
  s.scaleAxis = _state.showScaleAxis === true;
10007
10049
  s.scaleGrid = _state.showScaleGrid === true;
10050
+ s.scaleBar = _state.showScaleBar === true;
10008
10051
  }
10009
10052
  if (_branchScaleAvailable) {
10010
10053
  s.scale = _state.branchScale; // time or divergence: which layout the branches show
@@ -10107,9 +10150,6 @@ function (root, d3, forester, phyloXml) {
10107
10150
  VIEW_SHOW_FLAGS.forEach(function (f) {
10108
10151
  _state[f[2]] = on.has(f[0]);
10109
10152
  });
10110
- customLabelEntries().forEach(function (c) {
10111
- c.entry.selected = on.has('custom:' + c.key);
10112
- });
10113
10153
  }
10114
10154
  if (isFinite(s.font)) {
10115
10155
  setFontSizes(Number(s.font));
@@ -10189,6 +10229,9 @@ function (root, d3, forester, phyloXml) {
10189
10229
  if (typeof grid === 'boolean') {
10190
10230
  _state.showScaleGrid = grid;
10191
10231
  }
10232
+ if (typeof s.scaleBar === 'boolean') {
10233
+ _state.showScaleBar = s.scaleBar;
10234
+ }
10192
10235
  if (!radialDisplay()) {
10193
10236
  _radialLabelsHorizontal = false;
10194
10237
  } else if ((_state.showDomainArchitectures && _basicTreeProperties.domainArchitectures)
@@ -10259,17 +10302,13 @@ function (root, d3, forester, phyloXml) {
10259
10302
  VIEW_SHOW_FLAGS.forEach(function (f) {
10260
10303
  setCheckboxValue(f[1], _state[f[2]] === true);
10261
10304
  });
10262
- customLabelEntries().forEach(function (c) {
10263
- if (c.entry.cb_id) {
10264
- setCheckboxValue(c.entry.cb_id, c.entry.selected === true);
10265
- }
10266
- });
10267
10305
  setCheckboxValue(MSA_CB, _state.showMsa === true);
10268
10306
  setCheckboxValue(MSA_LOGO_CB, _state.showMsaLogo === true);
10269
10307
  setCheckboxValue(HEATMAP_CB, _state.showHeatmap === true);
10270
10308
  syncHeatmapControls();
10271
10309
  setCheckboxValue(SCALE_AXIS_CB, _state.showScaleAxis === true);
10272
10310
  setCheckboxValue(SCALE_GRID_CB, _state.showScaleGrid === true);
10311
+ setCheckboxValue(SCALE_BAR_CB, _state.showScaleBar === true);
10273
10312
  setCheckboxValue(DOMAIN_GLOW_CB, _state.domainGlow === true);
10274
10313
  setValue(DOMAIN_LABELS_SELECT, _state.domainLabels);
10275
10314
  syncDomainControls();
@@ -10350,7 +10389,7 @@ function (root, d3, forester, phyloXml) {
10350
10389
  const VIEW_INT_KEYS = ['tree', 'subtree', 'rotation', 'domainEvalue'];
10351
10390
  const VIEW_NUMBER_KEYS = ['font', 'node', 'branch'];
10352
10391
  const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', 'msaLogo', 'heatmap', 'domains', 'domainGlow', 'genes',
10353
- 'scaleAxis', 'scaleGrid', 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
10392
+ 'scaleAxis', 'scaleGrid', 'scaleBar', 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
10354
10393
  // Lists of strings, comma-joined. A property ref carries ':' but never a
10355
10394
  // comma, and encodeViewValue leaves both readable in a hash.
10356
10395
  const VIEW_LIST_KEYS = ['heatmapManual', 'labelFields'];
@@ -11345,9 +11384,9 @@ function (root, d3, forester, phyloXml) {
11345
11384
  if (vis) {
11346
11385
  return vis;
11347
11386
  }
11348
- let style = nodeStyle(node);
11349
- if (style && (style.nodeColor || style.fontColor)) {
11350
- return style.nodeColor || style.fontColor;
11387
+ let styleColor = nodeStyleColor(node); // null under an active Color-by, as in the tree
11388
+ if (styleColor) {
11389
+ return styleColor;
11351
11390
  }
11352
11391
  if (_state.useVisualStyles && node.color) {
11353
11392
  return 'rgb(' + node.color.red + ',' + node.color.green + ',' + node.color.blue + ')';
@@ -13496,7 +13535,8 @@ function (root, d3, forester, phyloXml) {
13496
13535
  const SCALE_BAR_RESERVE = 30;
13497
13536
 
13498
13537
  function scaleBarShown() {
13499
- return _state.phylogram === true && _basicTreeProperties.branchLengths === true && !scaleAxisShown();
13538
+ return _state.phylogram === true && _basicTreeProperties.branchLengths === true && !scaleAxisShown()
13539
+ && _state.showScaleBar !== false;
13500
13540
  }
13501
13541
 
13502
13542
  // pixels per branch-length unit in the current layout's own coordinates
@@ -13522,7 +13562,9 @@ function (root, d3, forester, phyloXml) {
13522
13562
  if (!scaleBarShown()) {
13523
13563
  return;
13524
13564
  }
13525
- let bar = forester.scaleBarLength(branchLengthPixelsPerUnit(), SCALE_BAR_TARGET_PX);
13565
+ // the unit the file states, as the distance axis prints it; none when it states none
13566
+ let unit = _treeData && typeof _treeData.branch_length_unit === 'string' ? _treeData.branch_length_unit.trim() : '';
13567
+ let bar = forester.scaleBarLength(branchLengthPixelsPerUnit(), SCALE_BAR_TARGET_PX, unit);
13526
13568
  if (!bar) {
13527
13569
  return;
13528
13570
  }
@@ -14156,6 +14198,11 @@ function (root, d3, forester, phyloXml) {
14156
14198
  scheduleUpdate(null, 0);
14157
14199
  }
14158
14200
 
14201
+ function scaleBarCbClicked() {
14202
+ _state.showScaleBar = getCheckboxValue(SCALE_BAR_CB);
14203
+ scheduleUpdate(null, 0);
14204
+ }
14205
+
14159
14206
  // Fit the circular tree into the viewport, centred. The root is at the
14160
14207
  // group origin (0,0), so we place that at the viewport centre and scale so
14161
14208
  // the outer label ring fits. Computed from the known radial extent rather
@@ -15314,6 +15361,12 @@ function (root, d3, forester, phyloXml) {
15314
15361
  gridCb.disabled = _state.unrootedDisplay === true || _state.phylogram !== true
15315
15362
  || _state.showScaleAxis !== true;
15316
15363
  }
15364
+ let barCb = byId(SCALE_BAR_CB);
15365
+ if (barCb) {
15366
+ // a cladogram has nothing to measure, and under the axis the bar
15367
+ // has given way; every layout can carry it otherwise
15368
+ barCb.disabled = _state.phylogram !== true || scaleAxisShown();
15369
+ }
15317
15370
  let minus = byId(ZOOM_OUT_X);
15318
15371
  let plus = byId(ZOOM_IN_X);
15319
15372
  let expandV = byId(ZOOM_TO_EXPAND_Y);
@@ -15380,22 +15433,6 @@ function (root, d3, forester, phyloXml) {
15380
15433
  }
15381
15434
 
15382
15435
 
15383
- function customCbClicked(cb_id) {
15384
- if (_nodeLabels) {
15385
- const cb_value = getCheckboxValue(cb_id);
15386
- for (const value of Object.values(_nodeLabels)) {
15387
- if (value.label && value.showButton === true && value.propertyRef && value.description) {
15388
- if (value.cb_id === cb_id) {
15389
- value.selected = cb_value;
15390
- }
15391
- }
15392
- }
15393
- search0();
15394
- search1();
15395
- update();
15396
- }
15397
- }
15398
-
15399
15436
  function taxonomyCbClicked() {
15400
15437
  _state.showTaxonomy = getCheckboxValue(TAXONOMY_CB);
15401
15438
  if (_state.showTaxonomy) {
@@ -18418,6 +18455,7 @@ function (root, d3, forester, phyloXml) {
18418
18455
  on(GENE_ANCHOR_SELECT, 'change', geneAnchorChanged);
18419
18456
  on(GENE_FAMILY_SELECT, 'change', geneFamilyChanged);
18420
18457
  on(GENE_LABELS_SELECT, 'change', geneLabelsChanged);
18458
+ on(SCALE_BAR_CB, 'click', scaleBarCbClicked);
18421
18459
  on(SCALE_AXIS_CB, 'click', scaleAxisCbClicked);
18422
18460
  on(SCALE_GRID_CB, 'click', scaleGridCbClicked);
18423
18461
 
@@ -18431,20 +18469,6 @@ function (root, d3, forester, phyloXml) {
18431
18469
 
18432
18470
  on(SHORTEN_NODE_NAME_CB, 'click', shortenCbClicked);
18433
18471
 
18434
- if (_nodeLabels) {
18435
- for (const [key, value] of Object.entries(_nodeLabels)) {
18436
- if (value.label && value.showButton === true && value.propertyRef && value.description) {
18437
- const cb_id = makeIdForCustomCheckboxButton(key);
18438
- on(cb_id, 'click', function () {
18439
- customCbClicked(cb_id);
18440
- });
18441
- if (value.selected === true) {
18442
- setCheckboxValue(cb_id, true);
18443
- }
18444
- }
18445
- }
18446
- }
18447
-
18448
18472
  on(LABEL_COLOR_SELECT_MENU, 'change', function () {
18449
18473
  let v = this.value;
18450
18474
  if (v && v !== DEFAULT && _vis && _vis.byId[v]) {
@@ -18885,10 +18909,6 @@ function (root, d3, forester, phyloXml) {
18885
18909
  return '<label class="aptx-seg" title="' + tooltip + '"><input type="radio" name="' + radioGroup + '" id="' + id + '">' + content + '</label>';
18886
18910
  }
18887
18911
 
18888
- function makeIdForCustomCheckboxButton(key) {
18889
- return key + '__cb';
18890
- }
18891
-
18892
18912
  function makeDisplayControl() {
18893
18913
  let labels = [];
18894
18914
  let nodes = [];
@@ -18910,17 +18930,6 @@ function (root, d3, forester, phyloXml) {
18910
18930
  labels.push(makeCheckboxItem('Metadata', PROPERTIES_CB, 'to show/hide the nodes\' metadata (phyloXML'
18911
18931
  + ' properties: species, host, …) in their labels; Metadata fields… chooses which, and in what order'));
18912
18932
  }
18913
- if (_nodeLabels) {
18914
- for (const [key, value] of Object.entries(_nodeLabels)) {
18915
- if (value.label && value.propertyRef && value.description) {
18916
- const cb_id = makeIdForCustomCheckboxButton(key);
18917
- if (value.showButton === true) {
18918
- labels.push(makeCheckboxItem(value.label, cb_id, value.description));
18919
- }
18920
- value.cb_id = cb_id;
18921
- }
18922
- }
18923
- }
18924
18933
  if (_basicTreeProperties.confidences) {
18925
18934
  labels.push(makeCheckboxItem('Confidence', CONFIDENCE_VALUES_CB, 'to show/hide confidence values'));
18926
18935
  }
@@ -18984,6 +18993,9 @@ function (root, d3, forester, phyloXml) {
18984
18993
  let timeAxis = _timeInfo && _timeInfo.type
18985
18994
  ? (_timeInfo.type === 'geologic' ? 'the geologic (ICS) time axis' : 'the calendar time axis')
18986
18995
  : null;
18996
+ // "Scale", the desktop's label for the same switch (its Settings > Overlays > Scale & Grid)
18997
+ opts.push(makeCheckboxItem('Scale', SCALE_BAR_CB, 'to show/hide the scale bar at the bottom left of a'
18998
+ + ' phylogram: a round length in the tree\'s branch-length unit (it gives way while the scale axis is on)'));
18987
18999
  opts.push(makeCheckboxItem('Scale Axis', SCALE_AXIS_CB, 'to show/hide '
18988
19000
  + (timeAxis ? timeAxis + ' and node-age bars (a distance axis while the branches show divergence)'
18989
19001
  : 'a labeled distance axis from the root to the deepest tip')
@@ -19399,6 +19411,7 @@ function (root, d3, forester, phyloXml) {
19399
19411
  syncHeatmapControls();
19400
19412
  setCheckboxValue(SCALE_AXIS_CB, _state.showScaleAxis);
19401
19413
  setCheckboxValue(SCALE_GRID_CB, _state.showScaleGrid);
19414
+ setCheckboxValue(SCALE_BAR_CB, _state.showScaleBar);
19402
19415
  setCheckboxValue(SHORTEN_NODE_NAME_CB, _state.shortenNodeNames);
19403
19416
  populateVisualizationMenus();
19404
19417
  initializeSearchOptions();
package/forester.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.24.0
23
+ // v 3.26.0
24
24
  // 2026-09-17
25
25
  //
26
26
  // forester.js is a general suite for dealing with phylogenetic trees.
@@ -8784,7 +8784,11 @@
8784
8784
  // -- 1, 2 or 5 times a power of ten -- chosen so the bar comes out about
8785
8785
  // targetPx long at pxPerUnit pixels per unit. Returns {length, label,
8786
8786
  // px}, or null when the scale is unusable (zero, negative or infinite).
8787
- forester.scaleBarLength = function (pxPerUnit, targetPx) {
8787
+ // The label carries the tree's stated unit after the number ("0.05
8788
+ // subs/site"), as the distance axis does at its end, and nothing when
8789
+ // the file states none: a tree that does not say what its lengths
8790
+ // measure gets no guess (a BV-BRC user asked for units, 2026-10-08).
8791
+ forester.scaleBarLength = function (pxPerUnit, targetPx, unit) {
8788
8792
  if (!(pxPerUnit > 0) || !isFinite(pxPerUnit)) {
8789
8793
  return null;
8790
8794
  }
@@ -8793,7 +8797,8 @@
8793
8797
  let base = raw / Math.pow(10, k);
8794
8798
  let nice = base < 1.5 ? 1 : (base < 3.5 ? 2 : (base < 7.5 ? 5 : 10));
8795
8799
  let length = Number((nice * Math.pow(10, k)).toPrecision(2));
8796
- return {length: length, label: String(length), px: length * pxPerUnit};
8800
+ let u = typeof unit === 'string' ? unit.trim() : '';
8801
+ return {length: length, label: String(length) + (u ? ' ' + u : ''), px: length * pxPerUnit};
8797
8802
  };
8798
8803
 
8799
8804
  // --------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archaeopteryx",
3
- "version": "3.24.0",
3
+ "version": "3.26.0",
4
4
  "description": "Archaeopteryx.js is a software tool for the visualization and analysis of highly annotated phylogenetic trees.",
5
5
  "main": "archaeopteryx.js",
6
6
  "types": "archaeopteryx.d.ts",
@@ -20,7 +20,7 @@
20
20
  "LICENSE"
21
21
  ],
22
22
  "scripts": {
23
- "test": "node test/forester_test.js && node test/search_test.js && node test/visualization_test.js && node test/domain_test.js && node test/gene_test.js && node test/clock_test.js && node test/metadata_test.js && node test/heatmap_test.js && node test/clustering_test.js",
23
+ "test": "node test/forester_test.js && node test/search_test.js && node test/visualization_test.js && node test/domain_test.js && node test/gene_test.js && node test/clock_test.js && node test/metadata_test.js && node test/heatmap_test.js && node test/clustering_test.js && node test/canvg_global_test.js",
24
24
  "lint": "eslint .",
25
25
  "lint:fix": "eslint . --fix",
26
26
  "format": "prettier --write .",