archaeopteryx 3.17.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 +94 -41
- package/archaeopteryx.d.ts +12 -0
- package/archaeopteryx.js +561 -124
- package/forester.js +492 -115
- package/package.json +2 -2
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
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.
|
|
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
|
|
384
|
-
const
|
|
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
|
-
'
|
|
5614
|
-
'
|
|
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
|
|
5914
|
-
// geologic ICS bands or calendar years, decided from the
|
|
5915
|
-
// elements by forester.timeAxisInfo -- again unless the caller
|
|
5916
|
-
//
|
|
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.
|
|
5926
|
-
|
|
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.
|
|
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;
|
|
@@ -6449,11 +6471,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6449
6471
|
// and a tree whose dates carry a unit is never touched. The desktop
|
|
6450
6472
|
// converts at the same point, next to its internal-label policy
|
|
6451
6473
|
// (their HeightDateConverter, 0.11.151).
|
|
6452
|
-
//
|
|
6453
|
-
// lengths AS LOADED, before anything rewrites them
|
|
6454
|
-
//
|
|
6455
|
-
// divergence
|
|
6456
|
-
//
|
|
6474
|
+
// What the Time | Div switch needs to be lossless is kept from the
|
|
6475
|
+
// branch lengths AS LOADED, before anything rewrites them: the
|
|
6476
|
+
// lengths a tree arrived with in time, and on a BEAST clock-model
|
|
6477
|
+
// tree its divergence, each length times its rate (see
|
|
6478
|
+
// forester.captureDivergence).
|
|
6457
6479
|
trees.forEach(function (t) {
|
|
6458
6480
|
forester.captureDivergence(t);
|
|
6459
6481
|
});
|
|
@@ -7217,6 +7239,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7217
7239
|
items.push({label: deleteLabel, danger: true, action: function () {
|
|
7218
7240
|
forester.deleteSubtree(tree, d);
|
|
7219
7241
|
_treeData = tree;
|
|
7242
|
+
branchScaleAfterEdit();
|
|
7220
7243
|
_basicTreeProperties = forester.collectBasicTreeProperties(_treeData);
|
|
7221
7244
|
refreshVisualizations(true);
|
|
7222
7245
|
search0();
|
|
@@ -9736,9 +9759,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
9736
9759
|
s.domainGlow = _state.domainGlow === true;
|
|
9737
9760
|
s.domainEvalue = _state.domainEvalueExponent;
|
|
9738
9761
|
}
|
|
9739
|
-
if (
|
|
9740
|
-
s.
|
|
9741
|
-
s.
|
|
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
|
|
9742
9768
|
}
|
|
9743
9769
|
let a = searchStateOf(0);
|
|
9744
9770
|
let b = searchStateOf(1);
|
|
@@ -9809,6 +9835,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
9809
9835
|
}
|
|
9810
9836
|
});
|
|
9811
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
|
+
}
|
|
9812
9848
|
_basicTreeProperties = forester.collectBasicTreeProperties(_root);
|
|
9813
9849
|
|
|
9814
9850
|
if (s.layout === 'rectangular' || s.layout === 'circular' || s.layout === 'unrooted') {
|
|
@@ -9880,11 +9916,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
9880
9916
|
_domain.palette = null; // the drawn set changed: the palette is dealt again
|
|
9881
9917
|
}
|
|
9882
9918
|
}
|
|
9883
|
-
|
|
9884
|
-
|
|
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;
|
|
9885
9924
|
}
|
|
9886
|
-
|
|
9887
|
-
|
|
9925
|
+
let grid = typeof s.scaleGrid === 'boolean' ? s.scaleGrid : s.timeGrid;
|
|
9926
|
+
if (typeof grid === 'boolean') {
|
|
9927
|
+
_state.showScaleGrid = grid;
|
|
9888
9928
|
}
|
|
9889
9929
|
if (!radialDisplay()) {
|
|
9890
9930
|
_radialLabelsHorizontal = false;
|
|
@@ -9951,6 +9991,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9951
9991
|
function syncViewControls() {
|
|
9952
9992
|
setDisplayTypeButtons();
|
|
9953
9993
|
syncZoomRowButtons();
|
|
9994
|
+
syncBranchScaleControls();
|
|
9954
9995
|
VIEW_SHOW_FLAGS.forEach(function (f) {
|
|
9955
9996
|
setCheckboxValue(f[1], _state[f[2]] === true);
|
|
9956
9997
|
});
|
|
@@ -9963,8 +10004,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
9963
10004
|
setCheckboxValue(MSA_LOGO_CB, _state.showMsaLogo === true);
|
|
9964
10005
|
setCheckboxValue(HEATMAP_CB, _state.showHeatmap === true);
|
|
9965
10006
|
syncHeatmapControls();
|
|
9966
|
-
setCheckboxValue(
|
|
9967
|
-
setCheckboxValue(
|
|
10007
|
+
setCheckboxValue(SCALE_AXIS_CB, _state.showScaleAxis === true);
|
|
10008
|
+
setCheckboxValue(SCALE_GRID_CB, _state.showScaleGrid === true);
|
|
9968
10009
|
setCheckboxValue(DOMAIN_GLOW_CB, _state.domainGlow === true);
|
|
9969
10010
|
setValue(DOMAIN_LABELS_SELECT, _state.domainLabels);
|
|
9970
10011
|
syncDomainControls();
|
|
@@ -10040,11 +10081,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
10040
10081
|
// The heat map's four keys and the alignment logo went in exactly that
|
|
10041
10082
|
// way, while the README promised a link reproduced the figure.
|
|
10042
10083
|
const VIEW_TEXT_KEYS = ['layout', 'display', 'order', 'root', 'colorBy', 'shapeBy', 'domainLabels', 'combine',
|
|
10043
|
-
'heatmapOrder'];
|
|
10084
|
+
'heatmapOrder', 'scale'];
|
|
10044
10085
|
const VIEW_INT_KEYS = ['tree', 'subtree', 'rotation', 'domainEvalue'];
|
|
10045
10086
|
const VIEW_NUMBER_KEYS = ['font', 'node', 'branch'];
|
|
10046
10087
|
const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', 'msaLogo', 'heatmap', 'domains', 'domainGlow',
|
|
10047
|
-
'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
|
|
10088
|
+
'scaleAxis', 'scaleGrid', 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
|
|
10048
10089
|
// Lists of strings, comma-joined. A property ref carries ':' but never a
|
|
10049
10090
|
// comma, and encodeViewValue leaves both readable in a hash.
|
|
10050
10091
|
const VIEW_LIST_KEYS = ['heatmapManual'];
|
|
@@ -10211,7 +10252,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10211
10252
|
{key: 'e', label: 'E', shift: true, what: 'Expand vertically until the labels fit', run: function () { zoomToExpandY(); }},
|
|
10212
10253
|
{key: 'l', label: 'L', shift: true, what: 'Next layout: rectangular, circular, unrooted', run: cycleLayout},
|
|
10213
10254
|
{key: 'd', label: 'D', shift: true, what: 'Next display type: phylogram, aligned, cladogram', run: cycleDisplayType},
|
|
10214
|
-
{key: 'x', label: 'X', shift: true, what: 'Time / divergence scale, or the
|
|
10255
|
+
{key: 'x', label: 'X', shift: true, what: 'Time / divergence scale, or the scale axis', run: toggleScaleAxis},
|
|
10215
10256
|
{key: 'o', label: 'O', shift: true, what: 'Ladderize (order the tree)', run: function () { ladderizeButtonPressed(); }},
|
|
10216
10257
|
{key: 'u', label: 'U', shift: true, what: 'Uncollapse every clade', run: function () { uncollapseAll(); }},
|
|
10217
10258
|
{key: 'f', label: 'F', what: 'Go to the search box', run: focusSearch, typing: true},
|
|
@@ -10286,10 +10327,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
10286
10327
|
|
|
10287
10328
|
/**
|
|
10288
10329
|
* Redraws the tree with its branch lengths taken from the chosen metric:
|
|
10289
|
-
* TIME (the
|
|
10290
|
-
*
|
|
10291
|
-
* tree
|
|
10292
|
-
*
|
|
10330
|
+
* TIME (the lengths the tree arrived with, where it arrived in time; its
|
|
10331
|
+
* date gaps otherwise) or DIVERGENCE (what its nodes record, or on a BEAST
|
|
10332
|
+
* clock-model tree each branch's time times its clock rate). Both stay on
|
|
10333
|
+
* the tree, so this is reversible and lossless -- a round trip restores
|
|
10334
|
+
* the branch lengths exactly. Always the WHOLE tree, whatever is on view.
|
|
10335
|
+
* The rules are forester.hasTimeAndDivergence's, shared with the desktop.
|
|
10293
10336
|
*
|
|
10294
10337
|
* @param scale 'time' or 'divergence'
|
|
10295
10338
|
*/
|
|
@@ -10304,31 +10347,71 @@ function (root, d3, forester, phyloXml) {
|
|
|
10304
10347
|
forester.applyDivergenceBranchLengths(_treeData);
|
|
10305
10348
|
}
|
|
10306
10349
|
// the branch lengths ARE the layout, so everything measured from them
|
|
10307
|
-
// has to be measured again
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10350
|
+
// has to be measured again -- for the tree ON VIEW, as switchToSubtree
|
|
10351
|
+
// measures it: a clade's properties are not the whole tree's
|
|
10352
|
+
_basicTreeProperties = forester.collectBasicTreeProperties(displayedRoot());
|
|
10353
|
+
syncBranchScaleControls();
|
|
10311
10354
|
zoomToFit();
|
|
10312
10355
|
}
|
|
10313
10356
|
|
|
10357
|
+
// The switch's two buttons and whether the pair is shown at all, from the
|
|
10358
|
+
// state: most trees state only one thing, and a control offering a choice
|
|
10359
|
+
// that does not exist is worse than no control.
|
|
10360
|
+
function syncBranchScaleControls() {
|
|
10361
|
+
setCheckboxValue(BRANCH_SCALE_TIME_BUTTON, _state.branchScale === 'time');
|
|
10362
|
+
setCheckboxValue(BRANCH_SCALE_DIV_BUTTON, _state.branchScale !== 'time');
|
|
10363
|
+
let scaleGroup = document.querySelector('.' + BRANCH_SCALE_CONTROLGROUP);
|
|
10364
|
+
if (scaleGroup) {
|
|
10365
|
+
scaleGroup.style.display = _branchScaleAvailable ? '' : 'none';
|
|
10366
|
+
}
|
|
10367
|
+
}
|
|
10368
|
+
|
|
10369
|
+
// The tree was EDITED (a node deleted). Whether it has two layouts is asked
|
|
10370
|
+
// of the tree as it is now, and the layout on screen is laid out again
|
|
10371
|
+
// from what the tree keeps and records: the branch that took the place of
|
|
10372
|
+
// a removed node's is as long as both were, in either layout. A tree that
|
|
10373
|
+
// was never offered the switch still has the branch lengths of its file,
|
|
10374
|
+
// so what it keeps is taken afresh -- deleting the one tip without a
|
|
10375
|
+
// clock rate leaves a tree with a rate on every branch.
|
|
10376
|
+
function branchScaleAfterEdit() {
|
|
10377
|
+
if (!_treeData) {
|
|
10378
|
+
return;
|
|
10379
|
+
}
|
|
10380
|
+
let was = _branchScaleAvailable;
|
|
10381
|
+
if (!was) {
|
|
10382
|
+
forester.captureDivergence(_treeData);
|
|
10383
|
+
}
|
|
10384
|
+
_branchScaleAvailable = forester.hasTimeAndDivergence(_treeData);
|
|
10385
|
+
if (was && _branchScaleAvailable && _state.branchScale === 'divergence') {
|
|
10386
|
+
forester.applyDivergenceBranchLengths(_treeData);
|
|
10387
|
+
} else if (was) {
|
|
10388
|
+
// in time, or no longer a tree with two layouts: back in time
|
|
10389
|
+
forester.applyTimeBranchLengths(_treeData);
|
|
10390
|
+
_state.branchScale = 'time';
|
|
10391
|
+
} else {
|
|
10392
|
+
_state.branchScale = forester.branchLengthScale(_treeData);
|
|
10393
|
+
}
|
|
10394
|
+
syncBranchScaleControls();
|
|
10395
|
+
}
|
|
10396
|
+
|
|
10314
10397
|
function branchScaleButtonClicked() {
|
|
10315
10398
|
setBranchScale(getCheckboxValue(BRANCH_SCALE_TIME_BUTTON) ? 'time' : 'divergence');
|
|
10316
10399
|
}
|
|
10317
10400
|
|
|
10318
10401
|
// Shift+X. The switch takes this key over where the tree offers it, as was
|
|
10319
|
-
// always intended; on every other tree it
|
|
10402
|
+
// always intended; on every other tree it works the Scale Axis
|
|
10320
10403
|
// checkbox, so the key never does nothing.
|
|
10321
|
-
function
|
|
10404
|
+
function toggleScaleAxis() {
|
|
10322
10405
|
if (_branchScaleAvailable) {
|
|
10323
10406
|
setBranchScale(_state.branchScale === 'time' ? 'divergence' : 'time');
|
|
10324
10407
|
return;
|
|
10325
10408
|
}
|
|
10326
|
-
let cb = byId(
|
|
10409
|
+
let cb = byId(SCALE_AXIS_CB);
|
|
10327
10410
|
if (!cb || cb.disabled) {
|
|
10328
10411
|
return;
|
|
10329
10412
|
}
|
|
10330
10413
|
cb.checked = !cb.checked;
|
|
10331
|
-
|
|
10414
|
+
scaleAxisCbClicked();
|
|
10332
10415
|
}
|
|
10333
10416
|
|
|
10334
10417
|
function focusSearch() {
|
|
@@ -11523,25 +11606,55 @@ function (root, d3, forester, phyloXml) {
|
|
|
11523
11606
|
// the tree's own branch scale, so the bands line up with the branches
|
|
11524
11607
|
// even on a fossil-only tree whose youngest tip is far from the present.
|
|
11525
11608
|
|
|
11526
|
-
|
|
11527
|
-
|
|
11609
|
+
// Whether the branch lengths on screen measure divergence: the switch says
|
|
11610
|
+
// so where it is offered; the tree's own unit where it is not (a build
|
|
11611
|
+
// that opened in divergence, a tree saved from Div).
|
|
11612
|
+
function branchesShowDivergence() {
|
|
11613
|
+
if (_branchScaleAvailable) {
|
|
11614
|
+
return _state.branchScale === 'divergence';
|
|
11615
|
+
}
|
|
11616
|
+
return !!_treeData && typeof _treeData.branch_length_unit === 'string'
|
|
11617
|
+
&& _treeData.branch_length_unit.trim() === 'subs/site';
|
|
11618
|
+
}
|
|
11619
|
+
|
|
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
|
|
11528
11627
|
&& _state.phylogram === true
|
|
11529
|
-
&&
|
|
11530
|
-
&& _basicTreeProperties.branchLengths === true
|
|
11531
|
-
// The axis is an overlay calibrated to the tree's own branch scale,
|
|
11532
|
-
// so it must be hidden when the branches are showing DIVERGENCE
|
|
11533
|
-
// and divergence is a different measure -- calendar years against
|
|
11534
|
-
// substitutions reads as a confident lie. Only then: where the two
|
|
11535
|
-
// metrics agree (a BEAST time tree, whose branch lengths ARE time)
|
|
11536
|
-
// there is no switch and the axis is calibrated correctly, which
|
|
11537
|
-
// is what makes a converted BEAST tree show its years at all.
|
|
11538
|
-
&& !(_branchScaleAvailable && _state.branchScale === 'divergence');
|
|
11628
|
+
&& _basicTreeProperties.branchLengths === true;
|
|
11539
11629
|
}
|
|
11540
11630
|
|
|
11541
|
-
function
|
|
11542
|
-
|
|
11631
|
+
function distanceAxisShown() {
|
|
11632
|
+
return scaleAxisShown() && !timeAxisShown();
|
|
11633
|
+
}
|
|
11634
|
+
|
|
11635
|
+
function timeAxisShown() {
|
|
11636
|
+
return scaleAxisShown()
|
|
11637
|
+
&& _timeInfo !== null && _timeInfo.type !== null
|
|
11638
|
+
// The axis is an overlay calibrated to the tree's own branch scale,
|
|
11639
|
+
// so it must be hidden when the branches are showing DIVERGENCE --
|
|
11640
|
+
// calendar years against substitutions reads as a confident lie.
|
|
11641
|
+
// Whether the switch is offered is not the question: a Nextstrain
|
|
11642
|
+
// build with one undated node opens in divergence, says so in its
|
|
11643
|
+
// unit, and is refused the switch; its axis was drawn over
|
|
11644
|
+
// substitutions until 2026-09-29. A tree in time shows its axis
|
|
11645
|
+
// (a converted BEAST tree its years) whether or not it has a
|
|
11646
|
+
// second layout.
|
|
11647
|
+
&& !branchesShowDivergence();
|
|
11648
|
+
}
|
|
11649
|
+
|
|
11650
|
+
function scaleAxisBottomReserve() {
|
|
11651
|
+
// circular draws its ruler in the tree's own gap, not under it
|
|
11652
|
+
if (!scaleAxisShown() || _state.circularDisplay) {
|
|
11543
11653
|
return 0;
|
|
11544
11654
|
}
|
|
11655
|
+
if (!timeAxisShown()) {
|
|
11656
|
+
return DISTANCE_AXIS_RESERVE;
|
|
11657
|
+
}
|
|
11545
11658
|
return _timeInfo.type === 'geologic' ? TIME_GEO_RESERVE : TIME_CAL_RESERVE;
|
|
11546
11659
|
}
|
|
11547
11660
|
|
|
@@ -11553,7 +11666,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11553
11666
|
// own bottom rows (conservation, consensus, ruler) must end above it
|
|
11554
11667
|
// or the bar covers them -- the old bare slider did exactly that.
|
|
11555
11668
|
// The scale bar sits in the same band, under the tree's last row.
|
|
11556
|
-
return Math.max(msaShown() ? msaBottomReserve() + MSA_NAV_RESERVE : 0,
|
|
11669
|
+
return Math.max(msaShown() ? msaBottomReserve() + MSA_NAV_RESERVE : 0, scaleAxisBottomReserve(),
|
|
11557
11670
|
heatmapBottomReserve(),
|
|
11558
11671
|
scaleBarShown() && !radialDisplay() ? SCALE_BAR_RESERVE : 0);
|
|
11559
11672
|
}
|
|
@@ -11563,14 +11676,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
11563
11676
|
// round number of branch-length units (forester.scaleBarLength) with
|
|
11564
11677
|
// end ticks and its length written above, drawn in the tree's own
|
|
11565
11678
|
// coordinates so it zooms and exports with the tree and its label stays
|
|
11566
|
-
// true. Not in a cladogram (nothing to measure) and not under
|
|
11567
|
-
// axis (which
|
|
11568
|
-
// 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).
|
|
11569
11683
|
const SCALE_BAR_TARGET_PX = 100;
|
|
11570
11684
|
const SCALE_BAR_RESERVE = 30;
|
|
11571
11685
|
|
|
11572
11686
|
function scaleBarShown() {
|
|
11573
|
-
return _state.phylogram === true && _basicTreeProperties.branchLengths === true && !
|
|
11687
|
+
return _state.phylogram === true && _basicTreeProperties.branchLengths === true && !scaleAxisShown();
|
|
11574
11688
|
}
|
|
11575
11689
|
|
|
11576
11690
|
// pixels per branch-length unit in the current layout's own coordinates
|
|
@@ -11646,6 +11760,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
11646
11760
|
delete _floatStrips['aptx-time-axis'];
|
|
11647
11761
|
}
|
|
11648
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
|
+
}
|
|
11649
11772
|
if (!timeAxisShown() || !_root || !_yScale) {
|
|
11650
11773
|
return;
|
|
11651
11774
|
}
|
|
@@ -11687,7 +11810,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11687
11810
|
// BEHIND the tree (inserted first in the group) at the fine geologic
|
|
11688
11811
|
// boundaries / calendar year ticks; each axis branch below fills it
|
|
11689
11812
|
let grid = null;
|
|
11690
|
-
if (_state.
|
|
11813
|
+
if (_state.showScaleGrid) {
|
|
11691
11814
|
grid = _svgGroup.insert('g', 'g').attr('class', 'aptx-timegrid')
|
|
11692
11815
|
.style('pointer-events', 'none');
|
|
11693
11816
|
}
|
|
@@ -11699,6 +11822,48 @@ function (root, d3, forester, phyloXml) {
|
|
|
11699
11822
|
}
|
|
11700
11823
|
|
|
11701
11824
|
let sc = info.type === 'calendar' ? -corr : corr;
|
|
11825
|
+
let circular = _state.circularDisplay;
|
|
11826
|
+
|
|
11827
|
+
// A bar spanning depths x0..x1 at node d: a band across the branch in
|
|
11828
|
+
// rectangular, a segment along the node's spoke in circular.
|
|
11829
|
+
function timeBar(d, x0, x1, thick, fill, cls) { // bound to d, for probes
|
|
11830
|
+
let el;
|
|
11831
|
+
if (circular) {
|
|
11832
|
+
// A bound older than the root's own estimate is a NEGATIVE
|
|
11833
|
+
// radius, and the bar runs on through the centre onto the far
|
|
11834
|
+
// side of its spoke's line, as the rectangular bar straddles
|
|
11835
|
+
// the root's x. As on the desktop (hpdRadialRange).
|
|
11836
|
+
let a = radialAngle(d.x);
|
|
11837
|
+
let p0 = polarXY(a, radialRadius(x0));
|
|
11838
|
+
let p1 = polarXY(a, radialRadius(x1));
|
|
11839
|
+
el = g.append('line').attr('x1', p0[0]).attr('y1', p0[1])
|
|
11840
|
+
.attr('x2', p1[0]).attr('y2', p1[1])
|
|
11841
|
+
.attr('stroke', fill).attr('stroke-width', thick);
|
|
11842
|
+
} else {
|
|
11843
|
+
el = g.append('rect').attr('x', Math.min(x0, x1)).attr('y', d.x - (thick / 2))
|
|
11844
|
+
.attr('width', Math.max(1, Math.abs(x1 - x0))).attr('height', thick)
|
|
11845
|
+
.attr('fill', fill);
|
|
11846
|
+
}
|
|
11847
|
+
el.datum(d).attr('class', cls);
|
|
11848
|
+
}
|
|
11849
|
+
// a short stroke ACROSS the branch at depth x: a fossil range's end cap
|
|
11850
|
+
function timeCap(d, x) {
|
|
11851
|
+
let p;
|
|
11852
|
+
let q;
|
|
11853
|
+
if (circular) {
|
|
11854
|
+
let a = radialAngle(d.x);
|
|
11855
|
+
let r = radialRadius(x); // negative past the centre, as the bar's end is
|
|
11856
|
+
let half = 4 / Math.max(Math.abs(r), 4); // 4px either side, as an angle
|
|
11857
|
+
p = polarXY(a - half, r);
|
|
11858
|
+
q = polarXY(a + half, r);
|
|
11859
|
+
} else {
|
|
11860
|
+
p = [x, d.x - 4];
|
|
11861
|
+
q = [x, d.x + 4];
|
|
11862
|
+
}
|
|
11863
|
+
g.append('line').attr('x1', p[0]).attr('y1', p[1]).attr('x2', q[0]).attr('y2', q[1])
|
|
11864
|
+
.attr('stroke', FOSSIL_BAR_COLOR).attr('stroke-width', 1)
|
|
11865
|
+
.datum(d).attr('class', 'aptx-fossil-cap');
|
|
11866
|
+
}
|
|
11702
11867
|
|
|
11703
11868
|
// ---- HPD age bars (internal) + tip bars: fossil ranges, or sampling dates ----
|
|
11704
11869
|
forEachDisplayed(function (d) {
|
|
@@ -11719,9 +11884,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
11719
11884
|
let s = sc;
|
|
11720
11885
|
let xa = d.y - ((max - value) * s);
|
|
11721
11886
|
let xb = d.y + ((value - min) * s);
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11887
|
+
if (circular && Math.abs(radialRadius(xb) - radialRadius(xa)) < 1) {
|
|
11888
|
+
// the 1px floor, along the spoke: from the bar's inner end
|
|
11889
|
+
// outward, as the rectangular bar is floored from its left
|
|
11890
|
+
// end and as the desktop draws it (hpdRadialRange). It was
|
|
11891
|
+
// centred on the interval until 2026-09-29.
|
|
11892
|
+
let inner = Math.min(xa, xb);
|
|
11893
|
+
xa = inner;
|
|
11894
|
+
xb = inner + (_radial.maxY / _radial.maxRad);
|
|
11895
|
+
}
|
|
11725
11896
|
// What a TIP's interval means is the axis's to say. On geologic
|
|
11726
11897
|
// time it is a fossil's observed range, FAD to LAD, and is drawn
|
|
11727
11898
|
// as one below. On CALENDAR time it is the uncertainty of a
|
|
@@ -11734,39 +11905,35 @@ function (root, d3, forester, phyloXml) {
|
|
|
11734
11905
|
// the desktop alike). The width test is the entry guard above.
|
|
11735
11906
|
let sampledTip = !d.children && info.type === 'calendar';
|
|
11736
11907
|
if (d.children) {
|
|
11737
|
-
|
|
11738
|
-
.attr('width', w).attr('height', 7)
|
|
11739
|
-
.attr('fill', HPD_BAR_COLOR);
|
|
11908
|
+
timeBar(d, xa, xb, 7, HPD_BAR_COLOR, 'aptx-age-bar');
|
|
11740
11909
|
} else if (sampledTip) {
|
|
11741
|
-
|
|
11742
|
-
.attr('x', left).attr('y', y - 2.5)
|
|
11743
|
-
.attr('width', w).attr('height', 5)
|
|
11744
|
-
.attr('fill', HPD_BAR_COLOR);
|
|
11910
|
+
timeBar(d, xa, xb, 5, HPD_BAR_COLOR, 'aptx-sampling-bar');
|
|
11745
11911
|
} else {
|
|
11746
|
-
|
|
11747
|
-
.attr('width', w).attr('height', 5)
|
|
11748
|
-
.attr('fill', FOSSIL_BAR_COLOR);
|
|
11912
|
+
timeBar(d, xa, xb, 5, FOSSIL_BAR_COLOR, 'aptx-fossil-bar');
|
|
11749
11913
|
// FAD/LAD end caps, so the range reads as a bracketed interval
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
});
|
|
11914
|
+
let lo = Math.min(xa, xb);
|
|
11915
|
+
let hi = Math.max(xa, xb, lo + 1);
|
|
11916
|
+
timeCap(d, lo);
|
|
11917
|
+
timeCap(d, hi);
|
|
11755
11918
|
}
|
|
11756
11919
|
});
|
|
11757
11920
|
|
|
11921
|
+
if (circular) {
|
|
11922
|
+
drawCircularTimeRuler(info, anchor, maxTipX, corr, g, grid, ink);
|
|
11923
|
+
return;
|
|
11924
|
+
}
|
|
11925
|
+
|
|
11758
11926
|
// ---- the axis itself: a FLOATING strip (see floatStripGroup) ----
|
|
11759
|
-
|
|
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);
|
|
11760
11930
|
function axisBackdrop(x0, x1) {
|
|
11761
11931
|
// opaque, in the background colour, so tips panned under the
|
|
11762
11932
|
// strip do not show through between the bands and the ruler
|
|
11763
11933
|
let left = Math.min(x0, x1);
|
|
11764
11934
|
ax.append('rect').attr('x', left - 2).attr('y', axisTop - 4)
|
|
11765
|
-
.attr('width', Math.abs(x1 - x0) + 4).attr('height',
|
|
11935
|
+
.attr('width', Math.abs(x1 - x0) + 4).attr('height', scaleAxisBottomReserve() + 4)
|
|
11766
11936
|
.attr('fill', _state.backgroundColorDefault);
|
|
11767
|
-
ax.append('line').attr('x1', left - 2).attr('x2', left - 2 + Math.abs(x1 - x0) + 4)
|
|
11768
|
-
.attr('y1', axisTop - 4).attr('y2', axisTop - 4)
|
|
11769
|
-
.attr('stroke', ink).attr('stroke-opacity', 0.25).attr('stroke-width', 1);
|
|
11770
11937
|
}
|
|
11771
11938
|
if (info.type === 'geologic') {
|
|
11772
11939
|
let rootAge = info.rootAge;
|
|
@@ -11911,13 +12078,269 @@ function (root, d3, forester, phyloXml) {
|
|
|
11911
12078
|
placeFloatingOverlays();
|
|
11912
12079
|
}
|
|
11913
12080
|
|
|
11914
|
-
|
|
11915
|
-
|
|
12081
|
+
// The circular layout's time axis. Its ruler runs along the gap between
|
|
12082
|
+
// the last tip and the first -- the one ray no branch crosses -- from the
|
|
12083
|
+
// root out to the tips, so it turns with the tree. "Time Grid" draws each
|
|
12084
|
+
// tick as a full ring behind the tree; a geologic axis lays its intervals
|
|
12085
|
+
// down as coloured rings, as the desktop does.
|
|
12086
|
+
function drawCircularTimeRuler(info, anchor, maxTipX, corr, g, grid, ink) {
|
|
12087
|
+
let depthOf; // a date -> depth, the axis the radius is drawn from
|
|
12088
|
+
let ticks = [];
|
|
12089
|
+
let from; // the root end and the tip end, as dates
|
|
12090
|
+
let to;
|
|
12091
|
+
let bands = [];
|
|
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') {
|
|
12105
|
+
let rootAge = info.rootAge;
|
|
12106
|
+
if (!(rootAge > 0)) {
|
|
12107
|
+
return;
|
|
12108
|
+
}
|
|
12109
|
+
let anchorAge = anchor ? anchor.date.value : rootAge;
|
|
12110
|
+
let anchorX = anchor ? anchor.y : _root.y;
|
|
12111
|
+
depthOf = function (age) {
|
|
12112
|
+
return anchorX + ((anchorAge - age) * corr);
|
|
12113
|
+
};
|
|
12114
|
+
from = rootAge;
|
|
12115
|
+
to = Math.max(0, anchorAge - ((maxTipX - anchorX) / corr));
|
|
12116
|
+
forester.maAxisTickValues(rootAge).forEach(function (v) {
|
|
12117
|
+
if (v >= to - 1e-9 && v <= rootAge + 1e-9) {
|
|
12118
|
+
ticks.push({value: v, text: String(v)});
|
|
12119
|
+
}
|
|
12120
|
+
});
|
|
12121
|
+
if (to > 0) {
|
|
12122
|
+
ticks.push({value: to, text: String(Math.round(to * 100) / 100), first: true});
|
|
12123
|
+
}
|
|
12124
|
+
bands = forester.geoOverlapping(forester.geoBandRanks(to, rootAge)[1], to, rootAge);
|
|
12125
|
+
unit = 'Ma';
|
|
12126
|
+
} else {
|
|
12127
|
+
let present = info.presentDate;
|
|
12128
|
+
if (!(present > 0)) {
|
|
12129
|
+
return;
|
|
12130
|
+
}
|
|
12131
|
+
let anchorYear = anchor ? anchor.date.value : present;
|
|
12132
|
+
let anchorX = anchor ? anchor.y : maxTipX;
|
|
12133
|
+
depthOf = function (yv) {
|
|
12134
|
+
return anchorX - ((anchorYear - yv) * corr);
|
|
12135
|
+
};
|
|
12136
|
+
from = anchorYear - ((anchorX - _root.y) / corr);
|
|
12137
|
+
to = present;
|
|
12138
|
+
forester.calendarTickYears(from, to).forEach(function (yv) {
|
|
12139
|
+
ticks.push({value: yv, text: String(Math.round(yv))});
|
|
12140
|
+
});
|
|
12141
|
+
}
|
|
12142
|
+
let radiusOf = function (v) {
|
|
12143
|
+
return Math.max(0, radialRadius(depthOf(v)));
|
|
12144
|
+
};
|
|
12145
|
+
let r0 = radiusOf(from);
|
|
12146
|
+
let r1 = radiusOf(to);
|
|
12147
|
+
if (!(r1 > r0)) {
|
|
12148
|
+
return;
|
|
12149
|
+
}
|
|
12150
|
+
// behind the tree: the geologic rings, then the grid rings
|
|
12151
|
+
let behind = null;
|
|
12152
|
+
if (bands.length > 0 || grid) {
|
|
12153
|
+
behind = grid || _svgGroup.insert('g', 'g').attr('class', 'aptx-timegrid')
|
|
12154
|
+
.style('pointer-events', 'none');
|
|
12155
|
+
}
|
|
12156
|
+
let ring = d3.arc().startAngle(0).endAngle(2 * Math.PI);
|
|
12157
|
+
let named = [];
|
|
12158
|
+
bands.forEach(function (iv) {
|
|
12159
|
+
let a = radiusOf(Math.min(iv.old, from));
|
|
12160
|
+
let b = radiusOf(Math.max(iv.young, to));
|
|
12161
|
+
if (b - a <= 0) {
|
|
12162
|
+
return;
|
|
12163
|
+
}
|
|
12164
|
+
behind.append('path').attr('d', ring.innerRadius(a).outerRadius(b)())
|
|
12165
|
+
.attr('fill', iv.color).attr('fill-opacity', 0.35);
|
|
12166
|
+
named.push({r: (a + b) / 2, span: b - a, text: iv.name, color: iv.color});
|
|
12167
|
+
});
|
|
12168
|
+
if (grid) {
|
|
12169
|
+
ticks.forEach(function (t) {
|
|
12170
|
+
let r = radiusOf(t.value);
|
|
12171
|
+
if (r > r0 + 0.5 && r < r1 - 0.5) {
|
|
12172
|
+
grid.append('circle').attr('r', r).attr('fill', 'none')
|
|
12173
|
+
.attr('stroke', ink).attr('stroke-opacity', 0.18).attr('stroke-width', 1);
|
|
12174
|
+
}
|
|
12175
|
+
});
|
|
12176
|
+
}
|
|
12177
|
+
|
|
12178
|
+
// the ruler, on the gap's centre line
|
|
12179
|
+
let gap = _radialRotation + _radial.angleSpan + ((2 * Math.PI - _radial.angleSpan) / 2);
|
|
12180
|
+
let screen = gap - (Math.PI / 2);
|
|
12181
|
+
let along = [Math.cos(screen), Math.sin(screen)];
|
|
12182
|
+
let across = [-along[1], along[0]];
|
|
12183
|
+
let flip = along[0] < -1e-9; // keep every number upright
|
|
12184
|
+
let deg = (screen * 180 / Math.PI) + (flip ? 180 : 0);
|
|
12185
|
+
let at = function (r, off) {
|
|
12186
|
+
return [(along[0] * r) + (across[0] * off), (along[1] * r) + (across[1] * off)];
|
|
12187
|
+
};
|
|
12188
|
+
let ax = g.append('g').attr('class', 'aptx-time-ruler').style('font-family', FONT_DEFAULTS);
|
|
12189
|
+
let e0 = at(r0, 0);
|
|
12190
|
+
let e1 = at(r1, 0);
|
|
12191
|
+
ax.append('line').attr('x1', e0[0]).attr('y1', e0[1]).attr('x2', e1[0]).attr('y2', e1[1])
|
|
12192
|
+
.attr('stroke', ink).attr('stroke-width', 1);
|
|
12193
|
+
// numbers on one side of the ruler, interval names on the other; the
|
|
12194
|
+
// headline value first, then the round ticks outward, none overlapping
|
|
12195
|
+
function label(r, off, text, fill, backdrop) {
|
|
12196
|
+
let p = at(r, off);
|
|
12197
|
+
let t = ax.append('g')
|
|
12198
|
+
.attr('transform', 'translate(' + p[0] + ',' + p[1] + ') rotate(' + deg + ')');
|
|
12199
|
+
let w = text.length * 5.5;
|
|
12200
|
+
if (backdrop) {
|
|
12201
|
+
t.append('rect').attr('x', -(w / 2) - 2).attr('y', -6).attr('width', w + 4).attr('height', 12)
|
|
12202
|
+
.attr('rx', 2).attr('fill', _state.backgroundColorDefault).attr('fill-opacity', 0.85);
|
|
12203
|
+
}
|
|
12204
|
+
t.append('text').attr('text-anchor', 'middle').attr('dominant-baseline', 'central')
|
|
12205
|
+
.style('font-size', '9px').style('fill', fill).text(text);
|
|
12206
|
+
}
|
|
12207
|
+
let side = flip ? -1 : 1;
|
|
12208
|
+
let placed = [];
|
|
12209
|
+
ticks.sort(function (p, q) {
|
|
12210
|
+
return (q.first ? 1 : 0) - (p.first ? 1 : 0) || radiusOf(p.value) - radiusOf(q.value);
|
|
12211
|
+
});
|
|
12212
|
+
ticks.forEach(function (t) {
|
|
12213
|
+
let r = radiusOf(t.value);
|
|
12214
|
+
let c0 = at(r, -4);
|
|
12215
|
+
let c1 = at(r, 4);
|
|
12216
|
+
ax.append('line').attr('x1', c0[0]).attr('y1', c0[1]).attr('x2', c1[0]).attr('y2', c1[1])
|
|
12217
|
+
.attr('stroke', ink).attr('stroke-width', 1);
|
|
12218
|
+
let half = t.text.length * 2.8;
|
|
12219
|
+
for (let i = 0; i < placed.length; ++i) {
|
|
12220
|
+
if (Math.abs(placed[i] - r) < (half * 2) + 6) {
|
|
12221
|
+
if (!t.first) {
|
|
12222
|
+
return;
|
|
12223
|
+
}
|
|
12224
|
+
}
|
|
12225
|
+
}
|
|
12226
|
+
label(r, side * 11, t.text, ink, true);
|
|
12227
|
+
placed.push(r);
|
|
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
|
+
}
|
|
12233
|
+
if (info.type === 'geologic') {
|
|
12234
|
+
named.forEach(function (n) {
|
|
12235
|
+
if ((n.text.length * 5.5) + 4 <= n.span) {
|
|
12236
|
+
label(n.r, -side * 11, n.text, ink, true);
|
|
12237
|
+
}
|
|
12238
|
+
});
|
|
12239
|
+
}
|
|
12240
|
+
}
|
|
12241
|
+
|
|
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);
|
|
11916
12339
|
scheduleUpdate(null, 0);
|
|
11917
12340
|
}
|
|
11918
12341
|
|
|
11919
|
-
function
|
|
11920
|
-
_state.
|
|
12342
|
+
function scaleGridCbClicked() {
|
|
12343
|
+
_state.showScaleGrid = getCheckboxValue(SCALE_GRID_CB);
|
|
11921
12344
|
scheduleUpdate(null, 0);
|
|
11922
12345
|
}
|
|
11923
12346
|
|
|
@@ -13065,14 +13488,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
13065
13488
|
heatCb.disabled = _state.unrootedDisplay === true;
|
|
13066
13489
|
}
|
|
13067
13490
|
syncHeatmapControls();
|
|
13068
|
-
let
|
|
13069
|
-
if (
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
13074
|
-
|
|
13075
|
-
|
|
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;
|
|
13076
13502
|
}
|
|
13077
13503
|
let minus = byId(ZOOM_OUT_X);
|
|
13078
13504
|
let plus = byId(ZOOM_IN_X);
|
|
@@ -13743,7 +14169,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13743
14169
|
const PANEL_SECTIONS_KEY = 'aptx-panel-sections';
|
|
13744
14170
|
// Download too (Christian, 2026-09-28): four rows, and not something a
|
|
13745
14171
|
// session uses often.
|
|
13746
|
-
const PANEL_SECTIONS_CLOSED_BY_DEFAULT = ['
|
|
14172
|
+
const PANEL_SECTIONS_CLOSED_BY_DEFAULT = ['View & Tools', 'Sizes', 'Domain Architectures', 'Download'];
|
|
13747
14173
|
|
|
13748
14174
|
function loadPanelSections() {
|
|
13749
14175
|
if (_panelSections) {
|
|
@@ -13758,6 +14184,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
13758
14184
|
_panelSections[k] = saved[k];
|
|
13759
14185
|
}
|
|
13760
14186
|
});
|
|
14187
|
+
// the section was called Zoom until 2026-09-30: a fold
|
|
14188
|
+
// remembered under that name still counts
|
|
14189
|
+
if (typeof saved.Zoom === 'boolean' && _panelSections['View & Tools'] === undefined) {
|
|
14190
|
+
_panelSections['View & Tools'] = saved.Zoom;
|
|
14191
|
+
}
|
|
13761
14192
|
}
|
|
13762
14193
|
} catch {
|
|
13763
14194
|
// no storage (private mode), or junk written by something else:
|
|
@@ -15922,8 +16353,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
15922
16353
|
on(DOMAIN_EVALUE_INC, 'click', function () { domainEvalueStep(1); });
|
|
15923
16354
|
on(DOMAIN_LABELS_SELECT, 'change', domainLabelsChanged);
|
|
15924
16355
|
on(DOMAIN_GLOW_CB, 'click', domainGlowCbClicked);
|
|
15925
|
-
on(
|
|
15926
|
-
on(
|
|
16356
|
+
on(SCALE_AXIS_CB, 'click', scaleAxisCbClicked);
|
|
16357
|
+
on(SCALE_GRID_CB, 'click', scaleGridCbClicked);
|
|
15927
16358
|
|
|
15928
16359
|
on(LAYOUT_RECT_BUTTON, 'click', layoutButtonClicked);
|
|
15929
16360
|
on(BRANCH_SCALE_TIME_BUTTON, 'click', branchScaleButtonClicked);
|
|
@@ -16362,14 +16793,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
16362
16793
|
h = h.concat(makeSegment(makeGlyph('aligned_phylogram'), PHYLOGRAM_ALIGNED_BUTTON, radioGroup, PHYLOGRAM_ALIGNED_WHAT));
|
|
16363
16794
|
h = h.concat(makeSegment(makeGlyph('cladogram'), CLADOGRAM_BUTTON, radioGroup, CLADOGRAM_WHAT));
|
|
16364
16795
|
h = h.concat('</div>');
|
|
16365
|
-
// The branch SCALE, when the tree
|
|
16366
|
-
//
|
|
16796
|
+
// The branch SCALE, when the tree can be laid out by time and by
|
|
16797
|
+
// divergence (forester.hasTimeAndDivergence). Hidden otherwise,
|
|
16367
16798
|
// which is most trees.
|
|
16368
16799
|
h = h.concat('<div class="' + BRANCH_SCALE_CONTROLGROUP + ' aptx-segmented">');
|
|
16369
16800
|
h = h.concat(makeSegment('Time', BRANCH_SCALE_TIME_BUTTON, 'branch_scale_radio',
|
|
16370
|
-
'
|
|
16801
|
+
'lay the tree out by TIME (each branch at its stated length in time; from the node dates where the tree arrived showing divergence)'));
|
|
16371
16802
|
h = h.concat(makeSegment('Div', BRANCH_SCALE_DIV_BUTTON, 'branch_scale_radio',
|
|
16372
|
-
'
|
|
16803
|
+
'lay the tree out by DIVERGENCE (substitutions per site: as the tree records it, or on a BEAST tree each branch\'s time x its clock rate)'));
|
|
16373
16804
|
h = h.concat('</div>');
|
|
16374
16805
|
h = h.concat('</div>');
|
|
16375
16806
|
h = h.concat('</fieldset>');
|
|
@@ -16468,12 +16899,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
16468
16899
|
if (heatmapAvailable()) {
|
|
16469
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)'));
|
|
16470
16901
|
}
|
|
16471
|
-
if (
|
|
16472
|
-
|
|
16473
|
-
|
|
16474
|
-
|
|
16475
|
-
opts.push(makeCheckboxItem('
|
|
16476
|
-
+ (
|
|
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')));
|
|
16477
16912
|
}
|
|
16478
16913
|
|
|
16479
16914
|
let h = '<fieldset><legend>Display Data</legend>';
|
|
@@ -16491,7 +16926,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
16491
16926
|
// syncZoomRowButtons). The Y buttons span the whole row.
|
|
16492
16927
|
let h = "";
|
|
16493
16928
|
h = h.concat('<fieldset>');
|
|
16494
|
-
h = h.concat('<legend>
|
|
16929
|
+
h = h.concat('<legend>View & Tools</legend>');
|
|
16495
16930
|
h = h.concat('<div class="aptx-zoomgrid">');
|
|
16496
16931
|
h = h.concat(makeButton('Y+', ZOOM_IN_Y, 'zoom in vertically (or Shift+mousewheel)'));
|
|
16497
16932
|
h = h.concat('<div class="aptx-zoomrow">');
|
|
@@ -16503,8 +16938,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
16503
16938
|
h = h.concat('</div>');
|
|
16504
16939
|
h = h.concat(makeButton('Y-', ZOOM_OUT_Y, 'zoom out vertically (or Shift+mousewheel)'));
|
|
16505
16940
|
h = h.concat('</div>');
|
|
16506
|
-
// the tool buttons share the
|
|
16507
|
-
// a fieldset of their own
|
|
16941
|
+
// the tool buttons share the section with the zoom row rather
|
|
16942
|
+
// than paying for a fieldset of their own -- hence its name
|
|
16943
|
+
// (Christian, 2026-09-30: "ZOOM" named only half of it)
|
|
16508
16944
|
h = h.concat('<div class="aptx-toolrow">');
|
|
16509
16945
|
h = h.concat(makeGlyphButton('ladderize_asc', LADDERIZE_BUTTON, 'ladderize all'));
|
|
16510
16946
|
h = h.concat(makeGlyphButton('whole_tree', RETURN_TO_SUPERTREE_BUTTON, 'return all the way to the complete tree (if in a sub-tree)'));
|
|
@@ -16838,8 +17274,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
16838
17274
|
setCheckboxValue(HEATMAP_CB, _state.showHeatmap);
|
|
16839
17275
|
syncDomainControls();
|
|
16840
17276
|
syncHeatmapControls();
|
|
16841
|
-
setCheckboxValue(
|
|
16842
|
-
setCheckboxValue(
|
|
17277
|
+
setCheckboxValue(SCALE_AXIS_CB, _state.showScaleAxis);
|
|
17278
|
+
setCheckboxValue(SCALE_GRID_CB, _state.showScaleGrid);
|
|
16843
17279
|
setCheckboxValue(SHORTEN_NODE_NAME_CB, _state.shortenNodeNames);
|
|
16844
17280
|
populateVisualizationMenus();
|
|
16845
17281
|
initializeSearchOptions();
|
|
@@ -16936,14 +17372,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
16936
17372
|
disableCheckbox('#' + PHYLOGRAM_BUTTON);
|
|
16937
17373
|
disableCheckbox('#' + PHYLOGRAM_ALIGNED_BUTTON);
|
|
16938
17374
|
}
|
|
16939
|
-
|
|
16940
|
-
setCheckboxValue(BRANCH_SCALE_DIV_BUTTON, _state.branchScale !== 'time');
|
|
16941
|
-
// most trees state only one thing, and a control offering a choice
|
|
16942
|
-
// that does not exist is worse than no control
|
|
16943
|
-
let scaleGroup = document.querySelector('.' + BRANCH_SCALE_CONTROLGROUP);
|
|
16944
|
-
if (scaleGroup) {
|
|
16945
|
-
scaleGroup.style.display = _branchScaleAvailable ? '' : 'none';
|
|
16946
|
-
}
|
|
17375
|
+
syncBranchScaleControls();
|
|
16947
17376
|
}
|
|
16948
17377
|
|
|
16949
17378
|
|
|
@@ -17481,7 +17910,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
17481
17910
|
return _root;
|
|
17482
17911
|
}
|
|
17483
17912
|
let name = _root_const.name ? String(_root_const.name).trim() : '';
|
|
17484
|
-
|
|
17913
|
+
// what the phylogeny itself states goes with the part: without its
|
|
17914
|
+
// branch_length_unit a subtree saved from Div opened again as time
|
|
17915
|
+
let part = Object.assign({}, _root, {name: name ? name + ' (subtree)' : undefined});
|
|
17916
|
+
['rooted', 'rerootable', 'type', 'branch_length_unit'].forEach(function (k) {
|
|
17917
|
+
if (_root_const[k] !== undefined && _root_const[k] !== null) {
|
|
17918
|
+
part[k] = _root_const[k];
|
|
17919
|
+
}
|
|
17920
|
+
});
|
|
17921
|
+
return part;
|
|
17485
17922
|
}
|
|
17486
17923
|
|
|
17487
17924
|
// "subtree, 19 of 50 tips" while a subtree is shown, else null: said in
|