archaeopteryx 3.18.0 → 3.19.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
@@ -247,9 +247,20 @@ A phylogram carries a **scale bar** at the bottom left: a round number of
247
247
  branch-length units (1, 2 or 5 × 10ᵏ, whichever makes the bar about 100 px)
248
248
  with its length written above it. It is drawn with the tree, so it zooms and
249
249
  exports with it and its label always holds. A cladogram has nothing to
250
- measure and shows none, and a tree under a time axis leaves the measuring to
250
+ measure and shows none, and a tree under a scale axis leaves the measuring to
251
251
  the axis.
252
252
 
253
+ **Scale Axis** under Display Data (off by default, as on the desktop) draws a
254
+ labelled ruler under a phylogram, from 0 at the root out to the deepest tip, in
255
+ the tree's branch-length units, with the unit at its end when the tree states
256
+ one. **Scale Grid** adds a faint line at every tick, behind the tree. In the
257
+ circular layout the ruler runs out from the root along the gap between the last
258
+ tip and the first, and the grid lines become rings; the unrooted layout has no
259
+ axis. The ruler floats at the bottom of the window as the tree scrolls, and it
260
+ starts at a subtree view's own root. On a time tree the same switch draws the
261
+ time axis instead (below): a tree flipped to **Div** gets the distance ruler,
262
+ and one flipped back gets its time axis.
263
+
253
264
  In the two radial layouts the zoom row changes meaning, exactly as on the
254
265
  desktop: **Y+ / Y− become the plain + / − zoom** (a circle has one diameter;
255
266
  the mouse wheel zooms too, and never rotates), **X− / X+ become rotate** (a
@@ -479,7 +490,7 @@ too.
479
490
  | ⌘⇧E | Ctrl+Shift+E | Expand vertically until the labels fit |
480
491
  | ⌘⇧L | Ctrl+Shift+L | Next layout: rectangular, circular, unrooted |
481
492
  | ⌘⇧D | Ctrl+Shift+D | Next display type: phylogram, aligned, cladogram |
482
- | ⌘⇧X | Ctrl+Shift+X | Time axis on / off |
493
+ | ⌘⇧X | Ctrl+Shift+X | Time \| Div, where the tree offers it; the Scale Axis on / off otherwise |
483
494
  | ⌘⇧O | Ctrl+Shift+O | Ladderize |
484
495
  | ⌘⇧U | Ctrl+Shift+U | Uncollapse every clade |
485
496
  | ⌘F | Ctrl+F | Go to the search box |
@@ -504,9 +515,10 @@ desktop's Unicode entry: it stays with the text field there, as it should.
504
515
  A view is what you made of a tree with the panel: the layout and display
505
516
  type, which labels show, the colour and shape fields, both searches, the
506
517
  clade you switched to, the clades you collapsed, the font, node and branch
507
- sizes, the rotation, and the tracks — which track is shown, the heat map's
508
- column order (including one you arranged by hand) and whether the alignment
509
- is summarised as a logo. On the demo pages it rides in the URL's
518
+ sizes, the rotation, whether the branches show time or divergence, and the
519
+ tracks — which track is shown, the heat map's column order (including one
520
+ you arranged by hand) and whether the alignment is summarised as a logo. On
521
+ the demo pages it rides in the URL's
510
522
  `#` hash and follows every change, so the address bar is always a link to
511
523
  what is on screen: copy it (**Copy link to this view** in the toolbar) and
512
524
  the recipient opens the same tree in the same view. Opening your own file
@@ -733,12 +745,12 @@ nodes, and on tips whatever the axis says the range means — on geologic
733
745
  time a fossil's observed range, sepia **FAD/LAD bars with end caps**; on
734
746
  calendar time the uncertainty of a **sampling date**, drawn like an age bar,
735
747
  slimmer, and only when it has a width. Node tooltips show the date. The
736
- **Time Axis** checkbox under Display Data toggles everything; the axis needs
748
+ **Scale Axis** checkbox under Display Data toggles everything; the axis needs
737
749
  a phylogram and the rectangular or circular layout. In **circular** the bars
738
750
  run along each node's spoke — on through the centre where a range reaches
739
751
  back past the root's own age — the ruler runs out from the root along the
740
752
  gap between the last tip and the first, turning with the tree, and a
741
- geologic axis lays its intervals down as coloured rings. **Time Grid** (off
753
+ geologic axis lays its intervals down as coloured rings. **Scale Grid** (off
742
754
  by default) adds faint lines, or rings, at the fine interval boundaries or
743
755
  the calendar years.
744
756
 
@@ -767,7 +779,7 @@ builds state — keeps its sign, and is drawn at length 0 like any negative
767
779
  branch length. The switch acts on the whole tree, also from a subtree view,
768
780
  and a node deleted meanwhile leaves the branch that takes its place spanning
769
781
  what both spanned, in either layout. The time axis is hidden while the
770
- branches show divergence. Each layout stamps the tree's `branch_length_unit`
782
+ branches show divergence, and the Scale Axis measures distance instead. Each layout stamps the tree's `branch_length_unit`
771
783
  (`subs/site`; for time the dates' unit, or `time`), so a tree saved while
772
784
  Div is on screen says so and opens again showing divergence. All of this is
773
785
  shared with the desktop.
@@ -1164,7 +1176,8 @@ developer spec below). The checkboxes are still there to override it.
1164
1176
 
1165
1177
  The same data-driven rule applies to the two big overlays: a tree whose tips
1166
1178
  carry an aligned `mol_seq` opens with its **sequence alignment** showing, and
1167
- a tree with phyloXML `<date>` elements opens with its **time axis** drawn —
1179
+ a tree with phyloXML `<date>` elements that opens shown in time has its
1180
+ **time axis** drawn —
1168
1181
  each with a checkbox under Display Data to turn it off.
1169
1182
 
1170
1183
  Support and branch-length values draw **2 px smaller than the label font**
@@ -1348,8 +1361,8 @@ copy-pastable JSON.
1348
1361
  | `domainLabels` | `'domains'` | Where domain names go: `'domains'` (on the boxes), `'legend'` (a card), or `'none'`. |
1349
1362
  | `domainGlow` | `false` | Open with the glow around each domain box on. |
1350
1363
  | `domainEvalueExponent` | `-3` | The E-value threshold's exponent at launch, an integer from `-20` to `3`: domains with an E-value at or under `10^exponent` are drawn. |
1351
- | `showTimeAxis` | tree-derived | Open with the time axis shown. Default: on when the tree carries `<date>` elements, off otherwise — an explicit `true`/`false` overrides that. |
1352
- | `timeAxisGrid` | `false` | Open with the Time Grid vertical lines on (only meaningful — and only offered as a checkbox — while the time axis itself is shown). |
1364
+ | `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. |
1365
+ | `showScaleGrid` | `false` | Open with the Scale Grid lines on (only meaningful — and only offered as a checkbox — while the scale axis itself is shown). |
1353
1366
  | `showSupportDots` | `false` | Open with the Support Dots marks on (the checkbox appears whenever the tree has confidences). |
1354
1367
  | `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. |
1355
1368
  | `searchAinitialValue` | `null` | Prefill search box A. |
@@ -1372,7 +1385,7 @@ copy-pastable JSON.
1372
1385
  ### Anything else throws
1373
1386
 
1374
1387
  An unrecognised key is an error, whether it was removed in this modernization
1375
- or simply mistyped (the two deprecated keys below are the exception — they
1388
+ or simply mistyped (the deprecated keys below are the exception — they
1376
1389
  warn rather than throw):
1377
1390
 
1378
1391
  ```
@@ -1387,14 +1400,15 @@ whatever it names.
1387
1400
 
1388
1401
  ### Accepted, with a warning
1389
1402
 
1390
- Two keys are neither current nor removed: they are accepted so an existing
1391
- embed keeps working, and warn on the console. Both concern Newick support
1392
- values.
1403
+ Four keys are neither current nor removed: they are accepted so an existing
1404
+ embed keeps working, and warn on the console.
1393
1405
 
1394
1406
  | Key | What happens |
1395
1407
  |---|---|
1396
1408
  | `nhConfidenceValuesAsInternalNames` | Translated to `internalNumericLabels`. **`true` becomes `'confidence'`, not `'auto'`** — `'auto'` is all-or-nothing and promotes nothing in a tree that mixes clade names with support, so a caller moved to it silently would lose promotions they had. An explicit `internalNumericLabels` always wins. |
1397
1409
  | `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. |
1410
+ | `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. |
1411
+ | `timeAxisGrid` | Translated to `showScaleGrid`, value unchanged. |
1398
1412
 
1399
1413
  ### What replaced the rest
1400
1414
 
@@ -104,9 +104,17 @@ 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 scale axis under a phylogram: the time axis on a time tree whose
108
+ * branches show time, a distance axis on any other. Default: on for a
109
+ * time tree, off otherwise. */
110
+ showScaleAxis?: boolean;
111
+ /** Grid lines (rings, in circular) at the scale axis's ticks. */
112
+ showScaleGrid?: boolean;
107
113
  showSupportDots?: boolean;
114
+ /** @deprecated renamed showScaleAxis; still read, with a warning. */
108
115
  showTimeAxis?: boolean;
109
116
  supportDotMinimum?: number;
117
+ /** @deprecated renamed showScaleGrid; still read, with a warning. */
110
118
  timeAxisGrid?: boolean;
111
119
  /** Open straight into a view (getViewState / decodeViewState). */
112
120
  view?: ViewState | null;
@@ -165,7 +173,11 @@ export interface ViewState {
165
173
  domainLabels?: 'none' | 'domains' | 'legend';
166
174
  domainGlow?: boolean;
167
175
  domainEvalue?: number;
176
+ scaleAxis?: boolean;
177
+ scaleGrid?: boolean;
178
+ /** @deprecated the name of scaleAxis in older links; still read. */
168
179
  timeAxis?: boolean;
180
+ /** @deprecated the name of scaleGrid in older links; still read. */
169
181
  timeGrid?: boolean;
170
182
  searchA?: ViewSearch;
171
183
  searchB?: ViewSearch;
package/archaeopteryx.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.18.0
23
+ // v 3.19.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.18.0';
106
+ const VERSION = '3.19.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';
@@ -380,8 +380,8 @@ function (root, d3, forester, phyloXml) {
380
380
  const DOMAIN_EVALUE_READOUT = 'domain_evalue';
381
381
  const DOMAIN_LABELS_SELECT = 'domain_labels';
382
382
  const DOMAIN_GLOW_CB = 'domain_glow_cb';
383
- const TIME_AXIS_CB = 'timeaxis_cb';
384
- const TIME_GRID_CB = 'timegrid_cb';
383
+ const SCALE_AXIS_CB = 'scaleaxis_cb';
384
+ const SCALE_GRID_CB = 'scalegrid_cb';
385
385
  const MSA_SCROLL_ID = 'aptxmsascroll';
386
386
  const FONT_SIZE_SLIDER = 'fs_sl';
387
387
  const EXTERNAL_LABEL_CB = 'extl_cb';
@@ -641,6 +641,7 @@ function (root, d3, forester, phyloXml) {
641
641
  // ------ time axis (the desktop's geologic / calendar overlays) ------
642
642
  const TIME_GEO_RESERVE = 52; // two ICS band rows + the Ma ruler
643
643
  const TIME_CAL_RESERVE = 26; // the calendar year ruler
644
+ const DISTANCE_AXIS_RESERVE = 26; // the distance ruler, one label row
644
645
  const TIME_BAND_ROW_H = 13;
645
646
  const HPD_BAR_COLOR = 'rgba(70,130,220,0.35)'; // translucent blue, the usual HPD tint
646
647
  const FOSSIL_BAR_COLOR = 'rgba(150,100,55,0.86)'; // opaque-ish sepia
@@ -5440,6 +5441,23 @@ function (root, d3, forester, phyloXml) {
5440
5441
  return v === true ? 'confidence' : PARSE_DEFAULTS.internalNumericLabels;
5441
5442
  },
5442
5443
  note: 'use \'confidence\' for what this flag did (\'auto\' is the new default, and does not promote mixed trees)'
5444
+ },
5445
+ // Renamed in 3.19.0, when the axis stopped being time-only: a tree
5446
+ // whose branches measure divergence gets a distance axis from the
5447
+ // same switch. The meaning on a time tree is unchanged.
5448
+ showTimeAxis: {
5449
+ to: 'showScaleAxis',
5450
+ map: function (v) {
5451
+ return v;
5452
+ },
5453
+ note: 'the one switch now draws the time axis on a time tree and a distance axis on any other phylogram'
5454
+ },
5455
+ timeAxisGrid: {
5456
+ to: 'showScaleGrid',
5457
+ map: function (v) {
5458
+ return v;
5459
+ },
5460
+ note: 'the grid follows the scale axis, time or distance'
5443
5461
  }
5444
5462
  };
5445
5463
 
@@ -5610,8 +5628,8 @@ function (root, d3, forester, phyloXml) {
5610
5628
  'domainLabels',
5611
5629
  'domainGlow',
5612
5630
  'domainEvalueExponent',
5613
- 'showTimeAxis',
5614
- 'timeAxisGrid',
5631
+ 'showScaleAxis',
5632
+ 'showScaleGrid',
5615
5633
  'showSupportDots',
5616
5634
  'searchAinitialValue',
5617
5635
  'searchBinitialValue',
@@ -5910,10 +5928,11 @@ function (root, d3, forester, phyloXml) {
5910
5928
  + ' with a missing or impossible from / to / E-value ignored');
5911
5929
  }
5912
5930
  }
5913
- // Likewise a dated tree draws its time axis from the start --
5914
- // geologic ICS bands or calendar years, decided from the <date>
5915
- // elements by forester.timeAxisInfo -- again unless the caller set
5916
- // showTimeAxis explicitly.
5931
+ // Likewise a dated tree draws its scale axis from the start -- a
5932
+ // time axis of geologic ICS bands or calendar years, decided from the
5933
+ // <date> elements by forester.timeAxisInfo -- again unless the caller
5934
+ // set showScaleAxis explicitly. Any other tree opens without one, as
5935
+ // on the desktop: its distance axis is asked for.
5917
5936
  _timeInfo = _treeData ? forester.timeAxisInfo(forester.getTreeRoot(_treeData)) : null;
5918
5937
  _timeTree = _treeData ? forester.isTimeTree(_treeData) : false;
5919
5938
  // Whether this tree says two different things -- when its nodes sit in
@@ -5922,10 +5941,13 @@ function (root, d3, forester, phyloXml) {
5922
5941
  // BEAST or Newick file arrives stating its own branch lengths.
5923
5942
  _branchScaleAvailable = _treeData ? forester.hasTimeAndDivergence(_treeData) : false;
5924
5943
  _state.branchScale = _treeData ? forester.branchLengthScale(_treeData) : 'divergence';
5925
- if (_state.showTimeAxis === undefined) {
5926
- _state.showTimeAxis = !!(_timeInfo && _timeInfo.type);
5944
+ if (_state.showScaleAxis === undefined) {
5945
+ // on for a tree SHOWN in time; a dated tree that opens in
5946
+ // divergence (a build refused the switch) is a divergence tree
5947
+ // on screen, and opens without its distance axis like any other
5948
+ _state.showScaleAxis = !!(_timeInfo && _timeInfo.type) && !branchesShowDivergence();
5927
5949
  }
5928
- _state.timeAxisGrid = _state.timeAxisGrid === true; // desktop default: off
5950
+ _state.showScaleGrid = _state.showScaleGrid === true; // desktop default: off
5929
5951
  _state.showSupportDots = _state.showSupportDots === true;
5930
5952
  _state.showNodeEvents = _basicTreeProperties.nodeEvents === true;
5931
5953
  _state.showBranchEvents = _basicTreeProperties.branchEvents === true;
@@ -9737,9 +9759,12 @@ function (root, d3, forester, phyloXml) {
9737
9759
  s.domainGlow = _state.domainGlow === true;
9738
9760
  s.domainEvalue = _state.domainEvalueExponent;
9739
9761
  }
9740
- if (_timeInfo && _timeInfo.type) {
9741
- s.timeAxis = _state.showTimeAxis === true;
9742
- s.timeGrid = _state.timeAxisGrid === true;
9762
+ if (_basicTreeProperties.branchLengths === true) {
9763
+ s.scaleAxis = _state.showScaleAxis === true;
9764
+ s.scaleGrid = _state.showScaleGrid === true;
9765
+ }
9766
+ if (_branchScaleAvailable) {
9767
+ s.scale = _state.branchScale; // time or divergence: which layout the branches show
9743
9768
  }
9744
9769
  let a = searchStateOf(0);
9745
9770
  let b = searchStateOf(1);
@@ -9810,6 +9835,16 @@ function (root, d3, forester, phyloXml) {
9810
9835
  }
9811
9836
  });
9812
9837
  }
9838
+ // the branches' layout, time or divergence, on a tree that has both:
9839
+ // laid out before the properties are measured, since the lengths are
9840
+ // the layout. A tree with one layout ignores the key.
9841
+ if (_branchScaleAvailable && (s.scale === 'time' || s.scale === 'divergence') && s.scale !== _state.branchScale) {
9842
+ let laidOut = (s.scale === 'time') ? forester.applyTimeBranchLengths(_treeData)
9843
+ : forester.applyDivergenceBranchLengths(_treeData);
9844
+ if (laidOut) {
9845
+ _state.branchScale = s.scale;
9846
+ }
9847
+ }
9813
9848
  _basicTreeProperties = forester.collectBasicTreeProperties(_root);
9814
9849
 
9815
9850
  if (s.layout === 'rectangular' || s.layout === 'circular' || s.layout === 'unrooted') {
@@ -9881,11 +9916,15 @@ function (root, d3, forester, phyloXml) {
9881
9916
  _domain.palette = null; // the drawn set changed: the palette is dealt again
9882
9917
  }
9883
9918
  }
9884
- if (typeof s.timeAxis === 'boolean') {
9885
- _state.showTimeAxis = s.timeAxis;
9919
+ // timeAxis / timeGrid: the names before the axis also measured
9920
+ // distance, still read so that older links keep their axis
9921
+ let axis = typeof s.scaleAxis === 'boolean' ? s.scaleAxis : s.timeAxis;
9922
+ if (typeof axis === 'boolean') {
9923
+ _state.showScaleAxis = axis;
9886
9924
  }
9887
- if (typeof s.timeGrid === 'boolean') {
9888
- _state.timeAxisGrid = s.timeGrid;
9925
+ let grid = typeof s.scaleGrid === 'boolean' ? s.scaleGrid : s.timeGrid;
9926
+ if (typeof grid === 'boolean') {
9927
+ _state.showScaleGrid = grid;
9889
9928
  }
9890
9929
  if (!radialDisplay()) {
9891
9930
  _radialLabelsHorizontal = false;
@@ -9952,6 +9991,7 @@ function (root, d3, forester, phyloXml) {
9952
9991
  function syncViewControls() {
9953
9992
  setDisplayTypeButtons();
9954
9993
  syncZoomRowButtons();
9994
+ syncBranchScaleControls();
9955
9995
  VIEW_SHOW_FLAGS.forEach(function (f) {
9956
9996
  setCheckboxValue(f[1], _state[f[2]] === true);
9957
9997
  });
@@ -9964,8 +10004,8 @@ function (root, d3, forester, phyloXml) {
9964
10004
  setCheckboxValue(MSA_LOGO_CB, _state.showMsaLogo === true);
9965
10005
  setCheckboxValue(HEATMAP_CB, _state.showHeatmap === true);
9966
10006
  syncHeatmapControls();
9967
- setCheckboxValue(TIME_AXIS_CB, _state.showTimeAxis === true);
9968
- setCheckboxValue(TIME_GRID_CB, _state.timeAxisGrid === true);
10007
+ setCheckboxValue(SCALE_AXIS_CB, _state.showScaleAxis === true);
10008
+ setCheckboxValue(SCALE_GRID_CB, _state.showScaleGrid === true);
9969
10009
  setCheckboxValue(DOMAIN_GLOW_CB, _state.domainGlow === true);
9970
10010
  setValue(DOMAIN_LABELS_SELECT, _state.domainLabels);
9971
10011
  syncDomainControls();
@@ -10041,11 +10081,11 @@ function (root, d3, forester, phyloXml) {
10041
10081
  // The heat map's four keys and the alignment logo went in exactly that
10042
10082
  // way, while the README promised a link reproduced the figure.
10043
10083
  const VIEW_TEXT_KEYS = ['layout', 'display', 'order', 'root', 'colorBy', 'shapeBy', 'domainLabels', 'combine',
10044
- 'heatmapOrder'];
10084
+ 'heatmapOrder', 'scale'];
10045
10085
  const VIEW_INT_KEYS = ['tree', 'subtree', 'rotation', 'domainEvalue'];
10046
10086
  const VIEW_NUMBER_KEYS = ['font', 'node', 'branch'];
10047
10087
  const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', 'msaLogo', 'heatmap', 'domains', 'domainGlow',
10048
- 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
10088
+ 'scaleAxis', 'scaleGrid', 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
10049
10089
  // Lists of strings, comma-joined. A property ref carries ':' but never a
10050
10090
  // comma, and encodeViewValue leaves both readable in a hash.
10051
10091
  const VIEW_LIST_KEYS = ['heatmapManual'];
@@ -10212,7 +10252,7 @@ function (root, d3, forester, phyloXml) {
10212
10252
  {key: 'e', label: 'E', shift: true, what: 'Expand vertically until the labels fit', run: function () { zoomToExpandY(); }},
10213
10253
  {key: 'l', label: 'L', shift: true, what: 'Next layout: rectangular, circular, unrooted', run: cycleLayout},
10214
10254
  {key: 'd', label: 'D', shift: true, what: 'Next display type: phylogram, aligned, cladogram', run: cycleDisplayType},
10215
- {key: 'x', label: 'X', shift: true, what: 'Time / divergence scale, or the time axis', run: toggleTimeAxis},
10255
+ {key: 'x', label: 'X', shift: true, what: 'Time / divergence scale, or the scale axis', run: toggleScaleAxis},
10216
10256
  {key: 'o', label: 'O', shift: true, what: 'Ladderize (order the tree)', run: function () { ladderizeButtonPressed(); }},
10217
10257
  {key: 'u', label: 'U', shift: true, what: 'Uncollapse every clade', run: function () { uncollapseAll(); }},
10218
10258
  {key: 'f', label: 'F', what: 'Go to the search box', run: focusSearch, typing: true},
@@ -10359,19 +10399,19 @@ function (root, d3, forester, phyloXml) {
10359
10399
  }
10360
10400
 
10361
10401
  // Shift+X. The switch takes this key over where the tree offers it, as was
10362
- // always intended; on every other tree it still works the Time Axis
10402
+ // always intended; on every other tree it works the Scale Axis
10363
10403
  // checkbox, so the key never does nothing.
10364
- function toggleTimeAxis() {
10404
+ function toggleScaleAxis() {
10365
10405
  if (_branchScaleAvailable) {
10366
10406
  setBranchScale(_state.branchScale === 'time' ? 'divergence' : 'time');
10367
10407
  return;
10368
10408
  }
10369
- let cb = byId(TIME_AXIS_CB);
10409
+ let cb = byId(SCALE_AXIS_CB);
10370
10410
  if (!cb || cb.disabled) {
10371
10411
  return;
10372
10412
  }
10373
10413
  cb.checked = !cb.checked;
10374
- timeAxisCbClicked();
10414
+ scaleAxisCbClicked();
10375
10415
  }
10376
10416
 
10377
10417
  function focusSearch() {
@@ -11577,11 +11617,24 @@ function (root, d3, forester, phyloXml) {
11577
11617
  && _treeData.branch_length_unit.trim() === 'subs/site';
11578
11618
  }
11579
11619
 
11580
- function timeAxisShown() {
11581
- return _state.showTimeAxis === true && !_state.unrootedDisplay
11620
+ // The Scale Axis switch, as on the desktop: an axis under a rectangular
11621
+ // or circular phylogram. What it measures is the tree's to say -- time
11622
+ // on a time tree whose branches show time, distance from the root on
11623
+ // every other -- so flipping Time | Div changes what the axis reads,
11624
+ // never whether there is one.
11625
+ function scaleAxisShown() {
11626
+ return _state.showScaleAxis === true && !_state.unrootedDisplay
11582
11627
  && _state.phylogram === true
11628
+ && _basicTreeProperties.branchLengths === true;
11629
+ }
11630
+
11631
+ function distanceAxisShown() {
11632
+ return scaleAxisShown() && !timeAxisShown();
11633
+ }
11634
+
11635
+ function timeAxisShown() {
11636
+ return scaleAxisShown()
11583
11637
  && _timeInfo !== null && _timeInfo.type !== null
11584
- && _basicTreeProperties.branchLengths === true
11585
11638
  // The axis is an overlay calibrated to the tree's own branch scale,
11586
11639
  // so it must be hidden when the branches are showing DIVERGENCE --
11587
11640
  // calendar years against substitutions reads as a confident lie.
@@ -11594,11 +11647,14 @@ function (root, d3, forester, phyloXml) {
11594
11647
  && !branchesShowDivergence();
11595
11648
  }
11596
11649
 
11597
- function timeAxisBottomReserve() {
11650
+ function scaleAxisBottomReserve() {
11598
11651
  // circular draws its ruler in the tree's own gap, not under it
11599
- if (!timeAxisShown() || _state.circularDisplay) {
11652
+ if (!scaleAxisShown() || _state.circularDisplay) {
11600
11653
  return 0;
11601
11654
  }
11655
+ if (!timeAxisShown()) {
11656
+ return DISTANCE_AXIS_RESERVE;
11657
+ }
11602
11658
  return _timeInfo.type === 'geologic' ? TIME_GEO_RESERVE : TIME_CAL_RESERVE;
11603
11659
  }
11604
11660
 
@@ -11610,7 +11666,7 @@ function (root, d3, forester, phyloXml) {
11610
11666
  // own bottom rows (conservation, consensus, ruler) must end above it
11611
11667
  // or the bar covers them -- the old bare slider did exactly that.
11612
11668
  // The scale bar sits in the same band, under the tree's last row.
11613
- return Math.max(msaShown() ? msaBottomReserve() + MSA_NAV_RESERVE : 0, timeAxisBottomReserve(),
11669
+ return Math.max(msaShown() ? msaBottomReserve() + MSA_NAV_RESERVE : 0, scaleAxisBottomReserve(),
11614
11670
  heatmapBottomReserve(),
11615
11671
  scaleBarShown() && !radialDisplay() ? SCALE_BAR_RESERVE : 0);
11616
11672
  }
@@ -11620,14 +11676,15 @@ function (root, d3, forester, phyloXml) {
11620
11676
  // round number of branch-length units (forester.scaleBarLength) with
11621
11677
  // end ticks and its length written above, drawn in the tree's own
11622
11678
  // coordinates so it zooms and exports with the tree and its label stays
11623
- // true. Not in a cladogram (nothing to measure) and not under a time
11624
- // axis (which is a scale already); in the radial layouts it sits below
11625
- // the fan at its left edge.
11679
+ // true. Not in a cladogram (nothing to measure) and not under the scale
11680
+ // axis (which says the same, and more); in the radial layouts it sits
11681
+ // below the fan at its left edge. The desktop keeps its bar beside its
11682
+ // axis; ours gives way (Christian, 2026-09-30).
11626
11683
  const SCALE_BAR_TARGET_PX = 100;
11627
11684
  const SCALE_BAR_RESERVE = 30;
11628
11685
 
11629
11686
  function scaleBarShown() {
11630
- return _state.phylogram === true && _basicTreeProperties.branchLengths === true && !timeAxisShown();
11687
+ return _state.phylogram === true && _basicTreeProperties.branchLengths === true && !scaleAxisShown();
11631
11688
  }
11632
11689
 
11633
11690
  // pixels per branch-length unit in the current layout's own coordinates
@@ -11703,6 +11760,15 @@ function (root, d3, forester, phyloXml) {
11703
11760
  delete _floatStrips['aptx-time-axis'];
11704
11761
  }
11705
11762
  _svgGroup.selectAll('g.aptx-timegrid').remove();
11763
+ _svgGroup.selectAll('g.aptx-distance, g.aptx-distance-grid').remove();
11764
+ if (_floatGroup) {
11765
+ _floatGroup.selectAll('g.aptx-distance-axis').remove();
11766
+ delete _floatStrips['aptx-distance-axis'];
11767
+ }
11768
+ if (distanceAxisShown()) {
11769
+ drawDistanceAxis();
11770
+ return;
11771
+ }
11706
11772
  if (!timeAxisShown() || !_root || !_yScale) {
11707
11773
  return;
11708
11774
  }
@@ -11744,7 +11810,7 @@ function (root, d3, forester, phyloXml) {
11744
11810
  // BEHIND the tree (inserted first in the group) at the fine geologic
11745
11811
  // boundaries / calendar year ticks; each axis branch below fills it
11746
11812
  let grid = null;
11747
- if (_state.timeAxisGrid) {
11813
+ if (_state.showScaleGrid) {
11748
11814
  grid = _svgGroup.insert('g', 'g').attr('class', 'aptx-timegrid')
11749
11815
  .style('pointer-events', 'none');
11750
11816
  }
@@ -11858,17 +11924,16 @@ function (root, d3, forester, phyloXml) {
11858
11924
  }
11859
11925
 
11860
11926
  // ---- the axis itself: a FLOATING strip (see floatStripGroup) ----
11861
- let ax = floatStripGroup('aptx-time-axis', axisTop - 4, timeAxisBottomReserve() + 4);
11927
+ // the viewer's sans-serif: an SVG text inherits the browser's serif
11928
+ let ax = floatStripGroup('aptx-time-axis', axisTop - 4, scaleAxisBottomReserve() + 4)
11929
+ .style('font-family', FONT_DEFAULTS);
11862
11930
  function axisBackdrop(x0, x1) {
11863
11931
  // opaque, in the background colour, so tips panned under the
11864
11932
  // strip do not show through between the bands and the ruler
11865
11933
  let left = Math.min(x0, x1);
11866
11934
  ax.append('rect').attr('x', left - 2).attr('y', axisTop - 4)
11867
- .attr('width', Math.abs(x1 - x0) + 4).attr('height', timeAxisBottomReserve() + 4)
11935
+ .attr('width', Math.abs(x1 - x0) + 4).attr('height', scaleAxisBottomReserve() + 4)
11868
11936
  .attr('fill', _state.backgroundColorDefault);
11869
- ax.append('line').attr('x1', left - 2).attr('x2', left - 2 + Math.abs(x1 - x0) + 4)
11870
- .attr('y1', axisTop - 4).attr('y2', axisTop - 4)
11871
- .attr('stroke', ink).attr('stroke-opacity', 0.25).attr('stroke-width', 1);
11872
11937
  }
11873
11938
  if (info.type === 'geologic') {
11874
11939
  let rootAge = info.rootAge;
@@ -12024,7 +12089,19 @@ function (root, d3, forester, phyloXml) {
12024
12089
  let from; // the root end and the tip end, as dates
12025
12090
  let to;
12026
12091
  let bands = [];
12027
- if (info.type === 'geologic') {
12092
+ let unit = null; // written once past the tip end
12093
+ if (info.type === 'distance') {
12094
+ // not a date: a distance from the displayed root, 0 at its centre
12095
+ depthOf = function (v) {
12096
+ return info.rootDepth + (v * corr);
12097
+ };
12098
+ from = 0;
12099
+ to = info.maxDist;
12100
+ ticks = info.ticks.map(function (v) {
12101
+ return {value: v, text: String(v)};
12102
+ });
12103
+ unit = info.unit;
12104
+ } else if (info.type === 'geologic') {
12028
12105
  let rootAge = info.rootAge;
12029
12106
  if (!(rootAge > 0)) {
12030
12107
  return;
@@ -12045,6 +12122,7 @@ function (root, d3, forester, phyloXml) {
12045
12122
  ticks.push({value: to, text: String(Math.round(to * 100) / 100), first: true});
12046
12123
  }
12047
12124
  bands = forester.geoOverlapping(forester.geoBandRanks(to, rootAge)[1], to, rootAge);
12125
+ unit = 'Ma';
12048
12126
  } else {
12049
12127
  let present = info.presentDate;
12050
12128
  if (!(present > 0)) {
@@ -12107,7 +12185,7 @@ function (root, d3, forester, phyloXml) {
12107
12185
  let at = function (r, off) {
12108
12186
  return [(along[0] * r) + (across[0] * off), (along[1] * r) + (across[1] * off)];
12109
12187
  };
12110
- let ax = g.append('g').attr('class', 'aptx-time-ruler');
12188
+ let ax = g.append('g').attr('class', 'aptx-time-ruler').style('font-family', FONT_DEFAULTS);
12111
12189
  let e0 = at(r0, 0);
12112
12190
  let e1 = at(r1, 0);
12113
12191
  ax.append('line').attr('x1', e0[0]).attr('y1', e0[1]).attr('x2', e1[0]).attr('y2', e1[1])
@@ -12148,8 +12226,11 @@ function (root, d3, forester, phyloXml) {
12148
12226
  label(r, side * 11, t.text, ink, true);
12149
12227
  placed.push(r);
12150
12228
  });
12229
+ if (unit) {
12230
+ // along the ruler past its end, so a long unit reads outward
12231
+ label(r1 + 8 + (unit.length * 2.8), 0, unit, ink, false);
12232
+ }
12151
12233
  if (info.type === 'geologic') {
12152
- label(r1 + 14, 0, 'Ma', ink, false);
12153
12234
  named.forEach(function (n) {
12154
12235
  if ((n.text.length * 5.5) + 4 <= n.span) {
12155
12236
  label(n.r, -side * 11, n.text, ink, true);
@@ -12158,13 +12239,108 @@ function (root, d3, forester, phyloXml) {
12158
12239
  }
12159
12240
  }
12160
12241
 
12161
- function timeAxisCbClicked() {
12162
- _state.showTimeAxis = getCheckboxValue(TIME_AXIS_CB);
12242
+ // The distance axis, the desktop's "Scale Axis": a ruler from 0 at the
12243
+ // displayed root out to the deepest tip, in branch-length units, with
12244
+ // round ticks and the tree's unit at its end; "Scale Grid" draws each
12245
+ // tick as a faint line (a ring, in circular) behind the tree. Measured
12246
+ // per draw, so a subtree view's axis starts at ITS root.
12247
+ function drawDistanceAxis() {
12248
+ if (!_root || !_yScale) {
12249
+ return;
12250
+ }
12251
+ let corr = Math.abs(_yScale(1) - _yScale(0));
12252
+ if (!isFinite(corr) || corr <= 0) {
12253
+ return;
12254
+ }
12255
+ let rootDepth = (topNode() || _root).y;
12256
+ let maxTipX = rootDepth;
12257
+ forEachDisplayed(function (n) {
12258
+ if (!n.children && n.y > maxTipX) {
12259
+ maxTipX = n.y;
12260
+ }
12261
+ });
12262
+ let maxDist = (maxTipX - rootDepth) / corr;
12263
+ let ticks = forester.distanceAxisTickValues(maxDist);
12264
+ if (ticks.length === 0) {
12265
+ return;
12266
+ }
12267
+ let unit = _treeData && typeof _treeData.branch_length_unit === 'string'
12268
+ ? _treeData.branch_length_unit.trim() : '';
12269
+ let ink = _state.branchColorDefault;
12270
+ let grid = null;
12271
+ if (_state.showScaleGrid) {
12272
+ grid = _svgGroup.insert('g', 'g').attr('class', 'aptx-distance-grid')
12273
+ .style('pointer-events', 'none');
12274
+ }
12275
+ if (_state.circularDisplay) {
12276
+ let g = _svgGroup.append('g').attr('class', 'aptx-distance').style('pointer-events', 'none');
12277
+ drawCircularTimeRuler({type: 'distance', rootDepth: rootDepth, maxDist: maxDist, ticks: ticks,
12278
+ unit: unit || null}, null, maxTipX, corr, g, grid, ink);
12279
+ return;
12280
+ }
12281
+ let xOf = function (v) {
12282
+ return rootDepth + (v * corr);
12283
+ };
12284
+ let axisTop = _clusterH + 6;
12285
+ if (grid) {
12286
+ // the root and tip ends excluded: they would retrace the tree
12287
+ ticks.forEach(function (v) {
12288
+ if (v > 0 && v < maxDist - 1e-9) {
12289
+ grid.append('line').attr('x1', xOf(v)).attr('x2', xOf(v))
12290
+ .attr('y1', 0).attr('y2', axisTop)
12291
+ .attr('stroke', ink).attr('stroke-opacity', 0.18)
12292
+ .attr('stroke-width', 1);
12293
+ }
12294
+ });
12295
+ }
12296
+ let reserve = scaleAxisBottomReserve();
12297
+ let ax = floatStripGroup('aptx-distance-axis', axisTop - 4, reserve + 4)
12298
+ .style('font-family', FONT_DEFAULTS);
12299
+ let x0 = xOf(0);
12300
+ let x1 = xOf(maxDist);
12301
+ // opaque, as the time axis's, so tips panned under it do not show
12302
+ ax.append('rect').attr('x', x0 - 2).attr('y', axisTop - 4)
12303
+ .attr('width', (x1 - x0) + 4).attr('height', reserve + 4)
12304
+ .attr('fill', _state.backgroundColorDefault);
12305
+ let rulerY = axisTop + 4;
12306
+ ax.append('line').attr('x1', x0).attr('x2', x1)
12307
+ .attr('y1', rulerY).attr('y2', rulerY)
12308
+ .attr('stroke', ink).attr('stroke-width', 1);
12309
+ // every tick drawn, its number only where it clears the last one
12310
+ let lastRight = -Infinity;
12311
+ ticks.forEach(function (v) {
12312
+ let x = xOf(v);
12313
+ ax.append('line').attr('x1', x).attr('x2', x)
12314
+ .attr('y1', rulerY).attr('y2', rulerY + 4)
12315
+ .attr('stroke', ink).attr('stroke-width', 1);
12316
+ let text = String(v);
12317
+ let half = text.length * 2.8;
12318
+ if ((x - half) >= lastRight + 4) {
12319
+ ax.append('text').attr('x', x).attr('y', rulerY + 14)
12320
+ .attr('text-anchor', 'middle')
12321
+ .style('font-size', '9px').style('fill', ink)
12322
+ .text(text);
12323
+ lastRight = x + half;
12324
+ }
12325
+ });
12326
+ if (unit) {
12327
+ // past the ruler's end, and past the last number, which is
12328
+ // centred on that end
12329
+ ax.append('text').attr('x', Math.max(x1 + 8, lastRight + 6)).attr('y', rulerY + 14)
12330
+ .attr('text-anchor', 'start')
12331
+ .style('font-size', '9px').style('fill', ink)
12332
+ .text(unit);
12333
+ }
12334
+ placeFloatingOverlays();
12335
+ }
12336
+
12337
+ function scaleAxisCbClicked() {
12338
+ _state.showScaleAxis = getCheckboxValue(SCALE_AXIS_CB);
12163
12339
  scheduleUpdate(null, 0);
12164
12340
  }
12165
12341
 
12166
- function timeGridCbClicked() {
12167
- _state.timeAxisGrid = getCheckboxValue(TIME_GRID_CB);
12342
+ function scaleGridCbClicked() {
12343
+ _state.showScaleGrid = getCheckboxValue(SCALE_GRID_CB);
12168
12344
  scheduleUpdate(null, 0);
12169
12345
  }
12170
12346
 
@@ -13312,15 +13488,17 @@ function (root, d3, forester, phyloXml) {
13312
13488
  heatCb.disabled = _state.unrootedDisplay === true;
13313
13489
  }
13314
13490
  syncHeatmapControls();
13315
- let timeCb = byId(TIME_AXIS_CB);
13316
- if (timeCb) {
13317
- // circular draws it along the gap; only unrooted cannot
13318
- timeCb.disabled = _state.unrootedDisplay === true;
13319
- }
13320
- let timeGridCb = byId(TIME_GRID_CB);
13321
- if (timeGridCb) {
13322
- // grid lines hang off the time axis: no axis, nothing to grid
13323
- timeGridCb.disabled = _state.unrootedDisplay === true || _state.showTimeAxis !== true;
13491
+ let axisCb = byId(SCALE_AXIS_CB);
13492
+ if (axisCb) {
13493
+ // circular draws it along the gap; unrooted cannot, and a
13494
+ // cladogram has nothing to measure
13495
+ axisCb.disabled = _state.unrootedDisplay === true || _state.phylogram !== true;
13496
+ }
13497
+ let gridCb = byId(SCALE_GRID_CB);
13498
+ if (gridCb) {
13499
+ // grid lines hang off the scale axis: no axis, nothing to grid
13500
+ gridCb.disabled = _state.unrootedDisplay === true || _state.phylogram !== true
13501
+ || _state.showScaleAxis !== true;
13324
13502
  }
13325
13503
  let minus = byId(ZOOM_OUT_X);
13326
13504
  let plus = byId(ZOOM_IN_X);
@@ -16175,8 +16353,8 @@ function (root, d3, forester, phyloXml) {
16175
16353
  on(DOMAIN_EVALUE_INC, 'click', function () { domainEvalueStep(1); });
16176
16354
  on(DOMAIN_LABELS_SELECT, 'change', domainLabelsChanged);
16177
16355
  on(DOMAIN_GLOW_CB, 'click', domainGlowCbClicked);
16178
- on(TIME_AXIS_CB, 'click', timeAxisCbClicked);
16179
- on(TIME_GRID_CB, 'click', timeGridCbClicked);
16356
+ on(SCALE_AXIS_CB, 'click', scaleAxisCbClicked);
16357
+ on(SCALE_GRID_CB, 'click', scaleGridCbClicked);
16180
16358
 
16181
16359
  on(LAYOUT_RECT_BUTTON, 'click', layoutButtonClicked);
16182
16360
  on(BRANCH_SCALE_TIME_BUTTON, 'click', branchScaleButtonClicked);
@@ -16721,12 +16899,16 @@ function (root, d3, forester, phyloXml) {
16721
16899
  if (heatmapAvailable()) {
16722
16900
  opts.push(makeCheckboxItem('Heat Map', HEATMAP_CB, 'to show/hide a cell per tip and numeric field beside the tree, every column on one color scale; concentric rings in the circular layout (not in the unrooted one, where a column has no ring to be)'));
16723
16901
  }
16724
- if (_timeInfo && _timeInfo.type) {
16725
- opts.push(makeCheckboxItem('Time Axis', TIME_AXIS_CB, 'to show/hide the '
16726
- + (_timeInfo.type === 'geologic' ? 'geologic (ICS) time axis' : 'calendar time axis')
16727
- + ' and node-age bars (phylogram; rectangular and circular layouts)'));
16728
- opts.push(makeCheckboxItem('Time Grid', TIME_GRID_CB, 'to show/hide grid lines (rings, in circular) at the '
16729
- + (_timeInfo.type === 'geologic' ? 'geologic interval boundaries' : 'calendar year ticks')));
16902
+ if (_basicTreeProperties.branchLengths === true) {
16903
+ let timeAxis = _timeInfo && _timeInfo.type
16904
+ ? (_timeInfo.type === 'geologic' ? 'the geologic (ICS) time axis' : 'the calendar time axis')
16905
+ : null;
16906
+ opts.push(makeCheckboxItem('Scale Axis', SCALE_AXIS_CB, 'to show/hide '
16907
+ + (timeAxis ? timeAxis + ' and node-age bars (a distance axis while the branches show divergence)'
16908
+ : 'a labeled distance axis from the root to the deepest tip')
16909
+ + ' (phylogram; rectangular and circular layouts)'));
16910
+ opts.push(makeCheckboxItem('Scale Grid', SCALE_GRID_CB, 'to show/hide grid lines (rings, in circular) at the '
16911
+ + (timeAxis ? 'time axis\'s intervals or ticks' : 'scale axis ticks')));
16730
16912
  }
16731
16913
 
16732
16914
  let h = '<fieldset><legend>Display Data</legend>';
@@ -17092,8 +17274,8 @@ function (root, d3, forester, phyloXml) {
17092
17274
  setCheckboxValue(HEATMAP_CB, _state.showHeatmap);
17093
17275
  syncDomainControls();
17094
17276
  syncHeatmapControls();
17095
- setCheckboxValue(TIME_AXIS_CB, _state.showTimeAxis);
17096
- setCheckboxValue(TIME_GRID_CB, _state.timeAxisGrid);
17277
+ setCheckboxValue(SCALE_AXIS_CB, _state.showScaleAxis);
17278
+ setCheckboxValue(SCALE_GRID_CB, _state.showScaleGrid);
17097
17279
  setCheckboxValue(SHORTEN_NODE_NAME_CB, _state.shortenNodeNames);
17098
17280
  populateVisualizationMenus();
17099
17281
  initializeSearchOptions();
package/forester.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.18.0
23
+ // v 3.19.0
24
24
  // 2026-09-17
25
25
  //
26
26
  // forester.js is a general suite for dealing with phylogenetic trees.
@@ -9461,13 +9461,22 @@
9461
9461
  // Tick ages for a "Ma before present" ruler: ~8 nice steps from 0 back
9462
9462
  // to the root age.
9463
9463
  forester.maAxisTickValues = function (rootAge) {
9464
- if (!(rootAge > 0) || !isFinite(rootAge)) {
9464
+ return forester.distanceAxisTickValues(rootAge);
9465
+ };
9466
+
9467
+ // Tick values for a distance axis: ~8 nice steps from 0 (the root) out
9468
+ // to maxDist (the deepest tip), in the tree's branch-length units.
9469
+ forester.distanceAxisTickValues = function (maxDist) {
9470
+ if (!(maxDist > 0) || !isFinite(maxDist)) {
9471
+ return [];
9472
+ }
9473
+ let step = forester.niceAxisStep(maxDist / 8);
9474
+ if (!(step > 0)) {
9465
9475
  return [];
9466
9476
  }
9467
- let step = forester.niceAxisStep(rootAge / 8);
9468
9477
  let vals = [];
9469
- for (let v = 0; v <= rootAge + 1e-9; v += step) {
9470
- vals.push(Math.round(v * 1e6) / 1e6);
9478
+ for (let i = 0; (i * step) <= maxDist + (step * 1e-9); ++i) {
9479
+ vals.push(Number((i * step).toPrecision(12)));
9471
9480
  }
9472
9481
  return vals;
9473
9482
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archaeopteryx",
3
- "version": "3.18.0",
3
+ "version": "3.19.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",