archaeopteryx 3.17.0 → 3.18.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 +68 -29
- package/archaeopteryx.js +318 -63
- package/forester.js +479 -111
- 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.18.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.18.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';
|
|
@@ -6449,11 +6449,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6449
6449
|
// and a tree whose dates carry a unit is never touched. The desktop
|
|
6450
6450
|
// converts at the same point, next to its internal-label policy
|
|
6451
6451
|
// (their HeightDateConverter, 0.11.151).
|
|
6452
|
-
//
|
|
6453
|
-
// lengths AS LOADED, before anything rewrites them
|
|
6454
|
-
//
|
|
6455
|
-
// divergence
|
|
6456
|
-
//
|
|
6452
|
+
// What the Time | Div switch needs to be lossless is kept from the
|
|
6453
|
+
// branch lengths AS LOADED, before anything rewrites them: the
|
|
6454
|
+
// lengths a tree arrived with in time, and on a BEAST clock-model
|
|
6455
|
+
// tree its divergence, each length times its rate (see
|
|
6456
|
+
// forester.captureDivergence).
|
|
6457
6457
|
trees.forEach(function (t) {
|
|
6458
6458
|
forester.captureDivergence(t);
|
|
6459
6459
|
});
|
|
@@ -7217,6 +7217,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7217
7217
|
items.push({label: deleteLabel, danger: true, action: function () {
|
|
7218
7218
|
forester.deleteSubtree(tree, d);
|
|
7219
7219
|
_treeData = tree;
|
|
7220
|
+
branchScaleAfterEdit();
|
|
7220
7221
|
_basicTreeProperties = forester.collectBasicTreeProperties(_treeData);
|
|
7221
7222
|
refreshVisualizations(true);
|
|
7222
7223
|
search0();
|
|
@@ -10286,10 +10287,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
10286
10287
|
|
|
10287
10288
|
/**
|
|
10288
10289
|
* Redraws the tree with its branch lengths taken from the chosen metric:
|
|
10289
|
-
* TIME (the
|
|
10290
|
-
*
|
|
10291
|
-
* tree
|
|
10292
|
-
*
|
|
10290
|
+
* TIME (the lengths the tree arrived with, where it arrived in time; its
|
|
10291
|
+
* date gaps otherwise) or DIVERGENCE (what its nodes record, or on a BEAST
|
|
10292
|
+
* clock-model tree each branch's time times its clock rate). Both stay on
|
|
10293
|
+
* the tree, so this is reversible and lossless -- a round trip restores
|
|
10294
|
+
* the branch lengths exactly. Always the WHOLE tree, whatever is on view.
|
|
10295
|
+
* The rules are forester.hasTimeAndDivergence's, shared with the desktop.
|
|
10293
10296
|
*
|
|
10294
10297
|
* @param scale 'time' or 'divergence'
|
|
10295
10298
|
*/
|
|
@@ -10304,13 +10307,53 @@ function (root, d3, forester, phyloXml) {
|
|
|
10304
10307
|
forester.applyDivergenceBranchLengths(_treeData);
|
|
10305
10308
|
}
|
|
10306
10309
|
// the branch lengths ARE the layout, so everything measured from them
|
|
10307
|
-
// has to be measured again
|
|
10308
|
-
|
|
10309
|
-
|
|
10310
|
-
|
|
10310
|
+
// has to be measured again -- for the tree ON VIEW, as switchToSubtree
|
|
10311
|
+
// measures it: a clade's properties are not the whole tree's
|
|
10312
|
+
_basicTreeProperties = forester.collectBasicTreeProperties(displayedRoot());
|
|
10313
|
+
syncBranchScaleControls();
|
|
10311
10314
|
zoomToFit();
|
|
10312
10315
|
}
|
|
10313
10316
|
|
|
10317
|
+
// The switch's two buttons and whether the pair is shown at all, from the
|
|
10318
|
+
// state: most trees state only one thing, and a control offering a choice
|
|
10319
|
+
// that does not exist is worse than no control.
|
|
10320
|
+
function syncBranchScaleControls() {
|
|
10321
|
+
setCheckboxValue(BRANCH_SCALE_TIME_BUTTON, _state.branchScale === 'time');
|
|
10322
|
+
setCheckboxValue(BRANCH_SCALE_DIV_BUTTON, _state.branchScale !== 'time');
|
|
10323
|
+
let scaleGroup = document.querySelector('.' + BRANCH_SCALE_CONTROLGROUP);
|
|
10324
|
+
if (scaleGroup) {
|
|
10325
|
+
scaleGroup.style.display = _branchScaleAvailable ? '' : 'none';
|
|
10326
|
+
}
|
|
10327
|
+
}
|
|
10328
|
+
|
|
10329
|
+
// The tree was EDITED (a node deleted). Whether it has two layouts is asked
|
|
10330
|
+
// of the tree as it is now, and the layout on screen is laid out again
|
|
10331
|
+
// from what the tree keeps and records: the branch that took the place of
|
|
10332
|
+
// a removed node's is as long as both were, in either layout. A tree that
|
|
10333
|
+
// was never offered the switch still has the branch lengths of its file,
|
|
10334
|
+
// so what it keeps is taken afresh -- deleting the one tip without a
|
|
10335
|
+
// clock rate leaves a tree with a rate on every branch.
|
|
10336
|
+
function branchScaleAfterEdit() {
|
|
10337
|
+
if (!_treeData) {
|
|
10338
|
+
return;
|
|
10339
|
+
}
|
|
10340
|
+
let was = _branchScaleAvailable;
|
|
10341
|
+
if (!was) {
|
|
10342
|
+
forester.captureDivergence(_treeData);
|
|
10343
|
+
}
|
|
10344
|
+
_branchScaleAvailable = forester.hasTimeAndDivergence(_treeData);
|
|
10345
|
+
if (was && _branchScaleAvailable && _state.branchScale === 'divergence') {
|
|
10346
|
+
forester.applyDivergenceBranchLengths(_treeData);
|
|
10347
|
+
} else if (was) {
|
|
10348
|
+
// in time, or no longer a tree with two layouts: back in time
|
|
10349
|
+
forester.applyTimeBranchLengths(_treeData);
|
|
10350
|
+
_state.branchScale = 'time';
|
|
10351
|
+
} else {
|
|
10352
|
+
_state.branchScale = forester.branchLengthScale(_treeData);
|
|
10353
|
+
}
|
|
10354
|
+
syncBranchScaleControls();
|
|
10355
|
+
}
|
|
10356
|
+
|
|
10314
10357
|
function branchScaleButtonClicked() {
|
|
10315
10358
|
setBranchScale(getCheckboxValue(BRANCH_SCALE_TIME_BUTTON) ? 'time' : 'divergence');
|
|
10316
10359
|
}
|
|
@@ -11523,23 +11566,37 @@ function (root, d3, forester, phyloXml) {
|
|
|
11523
11566
|
// the tree's own branch scale, so the bands line up with the branches
|
|
11524
11567
|
// even on a fossil-only tree whose youngest tip is far from the present.
|
|
11525
11568
|
|
|
11569
|
+
// Whether the branch lengths on screen measure divergence: the switch says
|
|
11570
|
+
// so where it is offered; the tree's own unit where it is not (a build
|
|
11571
|
+
// that opened in divergence, a tree saved from Div).
|
|
11572
|
+
function branchesShowDivergence() {
|
|
11573
|
+
if (_branchScaleAvailable) {
|
|
11574
|
+
return _state.branchScale === 'divergence';
|
|
11575
|
+
}
|
|
11576
|
+
return !!_treeData && typeof _treeData.branch_length_unit === 'string'
|
|
11577
|
+
&& _treeData.branch_length_unit.trim() === 'subs/site';
|
|
11578
|
+
}
|
|
11579
|
+
|
|
11526
11580
|
function timeAxisShown() {
|
|
11527
|
-
return _state.showTimeAxis === true && !
|
|
11581
|
+
return _state.showTimeAxis === true && !_state.unrootedDisplay
|
|
11528
11582
|
&& _state.phylogram === true
|
|
11529
11583
|
&& _timeInfo !== null && _timeInfo.type !== null
|
|
11530
11584
|
&& _basicTreeProperties.branchLengths === true
|
|
11531
11585
|
// 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
|
-
//
|
|
11534
|
-
//
|
|
11535
|
-
//
|
|
11536
|
-
//
|
|
11537
|
-
//
|
|
11538
|
-
|
|
11586
|
+
// so it must be hidden when the branches are showing DIVERGENCE --
|
|
11587
|
+
// calendar years against substitutions reads as a confident lie.
|
|
11588
|
+
// Whether the switch is offered is not the question: a Nextstrain
|
|
11589
|
+
// build with one undated node opens in divergence, says so in its
|
|
11590
|
+
// unit, and is refused the switch; its axis was drawn over
|
|
11591
|
+
// substitutions until 2026-09-29. A tree in time shows its axis
|
|
11592
|
+
// (a converted BEAST tree its years) whether or not it has a
|
|
11593
|
+
// second layout.
|
|
11594
|
+
&& !branchesShowDivergence();
|
|
11539
11595
|
}
|
|
11540
11596
|
|
|
11541
11597
|
function timeAxisBottomReserve() {
|
|
11542
|
-
|
|
11598
|
+
// circular draws its ruler in the tree's own gap, not under it
|
|
11599
|
+
if (!timeAxisShown() || _state.circularDisplay) {
|
|
11543
11600
|
return 0;
|
|
11544
11601
|
}
|
|
11545
11602
|
return _timeInfo.type === 'geologic' ? TIME_GEO_RESERVE : TIME_CAL_RESERVE;
|
|
@@ -11699,6 +11756,48 @@ function (root, d3, forester, phyloXml) {
|
|
|
11699
11756
|
}
|
|
11700
11757
|
|
|
11701
11758
|
let sc = info.type === 'calendar' ? -corr : corr;
|
|
11759
|
+
let circular = _state.circularDisplay;
|
|
11760
|
+
|
|
11761
|
+
// A bar spanning depths x0..x1 at node d: a band across the branch in
|
|
11762
|
+
// rectangular, a segment along the node's spoke in circular.
|
|
11763
|
+
function timeBar(d, x0, x1, thick, fill, cls) { // bound to d, for probes
|
|
11764
|
+
let el;
|
|
11765
|
+
if (circular) {
|
|
11766
|
+
// A bound older than the root's own estimate is a NEGATIVE
|
|
11767
|
+
// radius, and the bar runs on through the centre onto the far
|
|
11768
|
+
// side of its spoke's line, as the rectangular bar straddles
|
|
11769
|
+
// the root's x. As on the desktop (hpdRadialRange).
|
|
11770
|
+
let a = radialAngle(d.x);
|
|
11771
|
+
let p0 = polarXY(a, radialRadius(x0));
|
|
11772
|
+
let p1 = polarXY(a, radialRadius(x1));
|
|
11773
|
+
el = g.append('line').attr('x1', p0[0]).attr('y1', p0[1])
|
|
11774
|
+
.attr('x2', p1[0]).attr('y2', p1[1])
|
|
11775
|
+
.attr('stroke', fill).attr('stroke-width', thick);
|
|
11776
|
+
} else {
|
|
11777
|
+
el = g.append('rect').attr('x', Math.min(x0, x1)).attr('y', d.x - (thick / 2))
|
|
11778
|
+
.attr('width', Math.max(1, Math.abs(x1 - x0))).attr('height', thick)
|
|
11779
|
+
.attr('fill', fill);
|
|
11780
|
+
}
|
|
11781
|
+
el.datum(d).attr('class', cls);
|
|
11782
|
+
}
|
|
11783
|
+
// a short stroke ACROSS the branch at depth x: a fossil range's end cap
|
|
11784
|
+
function timeCap(d, x) {
|
|
11785
|
+
let p;
|
|
11786
|
+
let q;
|
|
11787
|
+
if (circular) {
|
|
11788
|
+
let a = radialAngle(d.x);
|
|
11789
|
+
let r = radialRadius(x); // negative past the centre, as the bar's end is
|
|
11790
|
+
let half = 4 / Math.max(Math.abs(r), 4); // 4px either side, as an angle
|
|
11791
|
+
p = polarXY(a - half, r);
|
|
11792
|
+
q = polarXY(a + half, r);
|
|
11793
|
+
} else {
|
|
11794
|
+
p = [x, d.x - 4];
|
|
11795
|
+
q = [x, d.x + 4];
|
|
11796
|
+
}
|
|
11797
|
+
g.append('line').attr('x1', p[0]).attr('y1', p[1]).attr('x2', q[0]).attr('y2', q[1])
|
|
11798
|
+
.attr('stroke', FOSSIL_BAR_COLOR).attr('stroke-width', 1)
|
|
11799
|
+
.datum(d).attr('class', 'aptx-fossil-cap');
|
|
11800
|
+
}
|
|
11702
11801
|
|
|
11703
11802
|
// ---- HPD age bars (internal) + tip bars: fossil ranges, or sampling dates ----
|
|
11704
11803
|
forEachDisplayed(function (d) {
|
|
@@ -11719,9 +11818,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
11719
11818
|
let s = sc;
|
|
11720
11819
|
let xa = d.y - ((max - value) * s);
|
|
11721
11820
|
let xb = d.y + ((value - min) * s);
|
|
11722
|
-
|
|
11723
|
-
|
|
11724
|
-
|
|
11821
|
+
if (circular && Math.abs(radialRadius(xb) - radialRadius(xa)) < 1) {
|
|
11822
|
+
// the 1px floor, along the spoke: from the bar's inner end
|
|
11823
|
+
// outward, as the rectangular bar is floored from its left
|
|
11824
|
+
// end and as the desktop draws it (hpdRadialRange). It was
|
|
11825
|
+
// centred on the interval until 2026-09-29.
|
|
11826
|
+
let inner = Math.min(xa, xb);
|
|
11827
|
+
xa = inner;
|
|
11828
|
+
xb = inner + (_radial.maxY / _radial.maxRad);
|
|
11829
|
+
}
|
|
11725
11830
|
// What a TIP's interval means is the axis's to say. On geologic
|
|
11726
11831
|
// time it is a fossil's observed range, FAD to LAD, and is drawn
|
|
11727
11832
|
// as one below. On CALENDAR time it is the uncertainty of a
|
|
@@ -11734,27 +11839,24 @@ function (root, d3, forester, phyloXml) {
|
|
|
11734
11839
|
// the desktop alike). The width test is the entry guard above.
|
|
11735
11840
|
let sampledTip = !d.children && info.type === 'calendar';
|
|
11736
11841
|
if (d.children) {
|
|
11737
|
-
|
|
11738
|
-
.attr('width', w).attr('height', 7)
|
|
11739
|
-
.attr('fill', HPD_BAR_COLOR);
|
|
11842
|
+
timeBar(d, xa, xb, 7, HPD_BAR_COLOR, 'aptx-age-bar');
|
|
11740
11843
|
} 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);
|
|
11844
|
+
timeBar(d, xa, xb, 5, HPD_BAR_COLOR, 'aptx-sampling-bar');
|
|
11745
11845
|
} else {
|
|
11746
|
-
|
|
11747
|
-
.attr('width', w).attr('height', 5)
|
|
11748
|
-
.attr('fill', FOSSIL_BAR_COLOR);
|
|
11846
|
+
timeBar(d, xa, xb, 5, FOSSIL_BAR_COLOR, 'aptx-fossil-bar');
|
|
11749
11847
|
// FAD/LAD end caps, so the range reads as a bracketed interval
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
});
|
|
11848
|
+
let lo = Math.min(xa, xb);
|
|
11849
|
+
let hi = Math.max(xa, xb, lo + 1);
|
|
11850
|
+
timeCap(d, lo);
|
|
11851
|
+
timeCap(d, hi);
|
|
11755
11852
|
}
|
|
11756
11853
|
});
|
|
11757
11854
|
|
|
11855
|
+
if (circular) {
|
|
11856
|
+
drawCircularTimeRuler(info, anchor, maxTipX, corr, g, grid, ink);
|
|
11857
|
+
return;
|
|
11858
|
+
}
|
|
11859
|
+
|
|
11758
11860
|
// ---- the axis itself: a FLOATING strip (see floatStripGroup) ----
|
|
11759
11861
|
let ax = floatStripGroup('aptx-time-axis', axisTop - 4, timeAxisBottomReserve() + 4);
|
|
11760
11862
|
function axisBackdrop(x0, x1) {
|
|
@@ -11911,6 +12013,151 @@ function (root, d3, forester, phyloXml) {
|
|
|
11911
12013
|
placeFloatingOverlays();
|
|
11912
12014
|
}
|
|
11913
12015
|
|
|
12016
|
+
// The circular layout's time axis. Its ruler runs along the gap between
|
|
12017
|
+
// the last tip and the first -- the one ray no branch crosses -- from the
|
|
12018
|
+
// root out to the tips, so it turns with the tree. "Time Grid" draws each
|
|
12019
|
+
// tick as a full ring behind the tree; a geologic axis lays its intervals
|
|
12020
|
+
// down as coloured rings, as the desktop does.
|
|
12021
|
+
function drawCircularTimeRuler(info, anchor, maxTipX, corr, g, grid, ink) {
|
|
12022
|
+
let depthOf; // a date -> depth, the axis the radius is drawn from
|
|
12023
|
+
let ticks = [];
|
|
12024
|
+
let from; // the root end and the tip end, as dates
|
|
12025
|
+
let to;
|
|
12026
|
+
let bands = [];
|
|
12027
|
+
if (info.type === 'geologic') {
|
|
12028
|
+
let rootAge = info.rootAge;
|
|
12029
|
+
if (!(rootAge > 0)) {
|
|
12030
|
+
return;
|
|
12031
|
+
}
|
|
12032
|
+
let anchorAge = anchor ? anchor.date.value : rootAge;
|
|
12033
|
+
let anchorX = anchor ? anchor.y : _root.y;
|
|
12034
|
+
depthOf = function (age) {
|
|
12035
|
+
return anchorX + ((anchorAge - age) * corr);
|
|
12036
|
+
};
|
|
12037
|
+
from = rootAge;
|
|
12038
|
+
to = Math.max(0, anchorAge - ((maxTipX - anchorX) / corr));
|
|
12039
|
+
forester.maAxisTickValues(rootAge).forEach(function (v) {
|
|
12040
|
+
if (v >= to - 1e-9 && v <= rootAge + 1e-9) {
|
|
12041
|
+
ticks.push({value: v, text: String(v)});
|
|
12042
|
+
}
|
|
12043
|
+
});
|
|
12044
|
+
if (to > 0) {
|
|
12045
|
+
ticks.push({value: to, text: String(Math.round(to * 100) / 100), first: true});
|
|
12046
|
+
}
|
|
12047
|
+
bands = forester.geoOverlapping(forester.geoBandRanks(to, rootAge)[1], to, rootAge);
|
|
12048
|
+
} else {
|
|
12049
|
+
let present = info.presentDate;
|
|
12050
|
+
if (!(present > 0)) {
|
|
12051
|
+
return;
|
|
12052
|
+
}
|
|
12053
|
+
let anchorYear = anchor ? anchor.date.value : present;
|
|
12054
|
+
let anchorX = anchor ? anchor.y : maxTipX;
|
|
12055
|
+
depthOf = function (yv) {
|
|
12056
|
+
return anchorX - ((anchorYear - yv) * corr);
|
|
12057
|
+
};
|
|
12058
|
+
from = anchorYear - ((anchorX - _root.y) / corr);
|
|
12059
|
+
to = present;
|
|
12060
|
+
forester.calendarTickYears(from, to).forEach(function (yv) {
|
|
12061
|
+
ticks.push({value: yv, text: String(Math.round(yv))});
|
|
12062
|
+
});
|
|
12063
|
+
}
|
|
12064
|
+
let radiusOf = function (v) {
|
|
12065
|
+
return Math.max(0, radialRadius(depthOf(v)));
|
|
12066
|
+
};
|
|
12067
|
+
let r0 = radiusOf(from);
|
|
12068
|
+
let r1 = radiusOf(to);
|
|
12069
|
+
if (!(r1 > r0)) {
|
|
12070
|
+
return;
|
|
12071
|
+
}
|
|
12072
|
+
// behind the tree: the geologic rings, then the grid rings
|
|
12073
|
+
let behind = null;
|
|
12074
|
+
if (bands.length > 0 || grid) {
|
|
12075
|
+
behind = grid || _svgGroup.insert('g', 'g').attr('class', 'aptx-timegrid')
|
|
12076
|
+
.style('pointer-events', 'none');
|
|
12077
|
+
}
|
|
12078
|
+
let ring = d3.arc().startAngle(0).endAngle(2 * Math.PI);
|
|
12079
|
+
let named = [];
|
|
12080
|
+
bands.forEach(function (iv) {
|
|
12081
|
+
let a = radiusOf(Math.min(iv.old, from));
|
|
12082
|
+
let b = radiusOf(Math.max(iv.young, to));
|
|
12083
|
+
if (b - a <= 0) {
|
|
12084
|
+
return;
|
|
12085
|
+
}
|
|
12086
|
+
behind.append('path').attr('d', ring.innerRadius(a).outerRadius(b)())
|
|
12087
|
+
.attr('fill', iv.color).attr('fill-opacity', 0.35);
|
|
12088
|
+
named.push({r: (a + b) / 2, span: b - a, text: iv.name, color: iv.color});
|
|
12089
|
+
});
|
|
12090
|
+
if (grid) {
|
|
12091
|
+
ticks.forEach(function (t) {
|
|
12092
|
+
let r = radiusOf(t.value);
|
|
12093
|
+
if (r > r0 + 0.5 && r < r1 - 0.5) {
|
|
12094
|
+
grid.append('circle').attr('r', r).attr('fill', 'none')
|
|
12095
|
+
.attr('stroke', ink).attr('stroke-opacity', 0.18).attr('stroke-width', 1);
|
|
12096
|
+
}
|
|
12097
|
+
});
|
|
12098
|
+
}
|
|
12099
|
+
|
|
12100
|
+
// the ruler, on the gap's centre line
|
|
12101
|
+
let gap = _radialRotation + _radial.angleSpan + ((2 * Math.PI - _radial.angleSpan) / 2);
|
|
12102
|
+
let screen = gap - (Math.PI / 2);
|
|
12103
|
+
let along = [Math.cos(screen), Math.sin(screen)];
|
|
12104
|
+
let across = [-along[1], along[0]];
|
|
12105
|
+
let flip = along[0] < -1e-9; // keep every number upright
|
|
12106
|
+
let deg = (screen * 180 / Math.PI) + (flip ? 180 : 0);
|
|
12107
|
+
let at = function (r, off) {
|
|
12108
|
+
return [(along[0] * r) + (across[0] * off), (along[1] * r) + (across[1] * off)];
|
|
12109
|
+
};
|
|
12110
|
+
let ax = g.append('g').attr('class', 'aptx-time-ruler');
|
|
12111
|
+
let e0 = at(r0, 0);
|
|
12112
|
+
let e1 = at(r1, 0);
|
|
12113
|
+
ax.append('line').attr('x1', e0[0]).attr('y1', e0[1]).attr('x2', e1[0]).attr('y2', e1[1])
|
|
12114
|
+
.attr('stroke', ink).attr('stroke-width', 1);
|
|
12115
|
+
// numbers on one side of the ruler, interval names on the other; the
|
|
12116
|
+
// headline value first, then the round ticks outward, none overlapping
|
|
12117
|
+
function label(r, off, text, fill, backdrop) {
|
|
12118
|
+
let p = at(r, off);
|
|
12119
|
+
let t = ax.append('g')
|
|
12120
|
+
.attr('transform', 'translate(' + p[0] + ',' + p[1] + ') rotate(' + deg + ')');
|
|
12121
|
+
let w = text.length * 5.5;
|
|
12122
|
+
if (backdrop) {
|
|
12123
|
+
t.append('rect').attr('x', -(w / 2) - 2).attr('y', -6).attr('width', w + 4).attr('height', 12)
|
|
12124
|
+
.attr('rx', 2).attr('fill', _state.backgroundColorDefault).attr('fill-opacity', 0.85);
|
|
12125
|
+
}
|
|
12126
|
+
t.append('text').attr('text-anchor', 'middle').attr('dominant-baseline', 'central')
|
|
12127
|
+
.style('font-size', '9px').style('fill', fill).text(text);
|
|
12128
|
+
}
|
|
12129
|
+
let side = flip ? -1 : 1;
|
|
12130
|
+
let placed = [];
|
|
12131
|
+
ticks.sort(function (p, q) {
|
|
12132
|
+
return (q.first ? 1 : 0) - (p.first ? 1 : 0) || radiusOf(p.value) - radiusOf(q.value);
|
|
12133
|
+
});
|
|
12134
|
+
ticks.forEach(function (t) {
|
|
12135
|
+
let r = radiusOf(t.value);
|
|
12136
|
+
let c0 = at(r, -4);
|
|
12137
|
+
let c1 = at(r, 4);
|
|
12138
|
+
ax.append('line').attr('x1', c0[0]).attr('y1', c0[1]).attr('x2', c1[0]).attr('y2', c1[1])
|
|
12139
|
+
.attr('stroke', ink).attr('stroke-width', 1);
|
|
12140
|
+
let half = t.text.length * 2.8;
|
|
12141
|
+
for (let i = 0; i < placed.length; ++i) {
|
|
12142
|
+
if (Math.abs(placed[i] - r) < (half * 2) + 6) {
|
|
12143
|
+
if (!t.first) {
|
|
12144
|
+
return;
|
|
12145
|
+
}
|
|
12146
|
+
}
|
|
12147
|
+
}
|
|
12148
|
+
label(r, side * 11, t.text, ink, true);
|
|
12149
|
+
placed.push(r);
|
|
12150
|
+
});
|
|
12151
|
+
if (info.type === 'geologic') {
|
|
12152
|
+
label(r1 + 14, 0, 'Ma', ink, false);
|
|
12153
|
+
named.forEach(function (n) {
|
|
12154
|
+
if ((n.text.length * 5.5) + 4 <= n.span) {
|
|
12155
|
+
label(n.r, -side * 11, n.text, ink, true);
|
|
12156
|
+
}
|
|
12157
|
+
});
|
|
12158
|
+
}
|
|
12159
|
+
}
|
|
12160
|
+
|
|
11914
12161
|
function timeAxisCbClicked() {
|
|
11915
12162
|
_state.showTimeAxis = getCheckboxValue(TIME_AXIS_CB);
|
|
11916
12163
|
scheduleUpdate(null, 0);
|
|
@@ -13067,12 +13314,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
13067
13314
|
syncHeatmapControls();
|
|
13068
13315
|
let timeCb = byId(TIME_AXIS_CB);
|
|
13069
13316
|
if (timeCb) {
|
|
13070
|
-
|
|
13317
|
+
// circular draws it along the gap; only unrooted cannot
|
|
13318
|
+
timeCb.disabled = _state.unrootedDisplay === true;
|
|
13071
13319
|
}
|
|
13072
13320
|
let timeGridCb = byId(TIME_GRID_CB);
|
|
13073
13321
|
if (timeGridCb) {
|
|
13074
13322
|
// grid lines hang off the time axis: no axis, nothing to grid
|
|
13075
|
-
timeGridCb.disabled =
|
|
13323
|
+
timeGridCb.disabled = _state.unrootedDisplay === true || _state.showTimeAxis !== true;
|
|
13076
13324
|
}
|
|
13077
13325
|
let minus = byId(ZOOM_OUT_X);
|
|
13078
13326
|
let plus = byId(ZOOM_IN_X);
|
|
@@ -13743,7 +13991,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13743
13991
|
const PANEL_SECTIONS_KEY = 'aptx-panel-sections';
|
|
13744
13992
|
// Download too (Christian, 2026-09-28): four rows, and not something a
|
|
13745
13993
|
// session uses often.
|
|
13746
|
-
const PANEL_SECTIONS_CLOSED_BY_DEFAULT = ['
|
|
13994
|
+
const PANEL_SECTIONS_CLOSED_BY_DEFAULT = ['View & Tools', 'Sizes', 'Domain Architectures', 'Download'];
|
|
13747
13995
|
|
|
13748
13996
|
function loadPanelSections() {
|
|
13749
13997
|
if (_panelSections) {
|
|
@@ -13758,6 +14006,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
13758
14006
|
_panelSections[k] = saved[k];
|
|
13759
14007
|
}
|
|
13760
14008
|
});
|
|
14009
|
+
// the section was called Zoom until 2026-09-30: a fold
|
|
14010
|
+
// remembered under that name still counts
|
|
14011
|
+
if (typeof saved.Zoom === 'boolean' && _panelSections['View & Tools'] === undefined) {
|
|
14012
|
+
_panelSections['View & Tools'] = saved.Zoom;
|
|
14013
|
+
}
|
|
13761
14014
|
}
|
|
13762
14015
|
} catch {
|
|
13763
14016
|
// no storage (private mode), or junk written by something else:
|
|
@@ -16362,14 +16615,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
16362
16615
|
h = h.concat(makeSegment(makeGlyph('aligned_phylogram'), PHYLOGRAM_ALIGNED_BUTTON, radioGroup, PHYLOGRAM_ALIGNED_WHAT));
|
|
16363
16616
|
h = h.concat(makeSegment(makeGlyph('cladogram'), CLADOGRAM_BUTTON, radioGroup, CLADOGRAM_WHAT));
|
|
16364
16617
|
h = h.concat('</div>');
|
|
16365
|
-
// The branch SCALE, when the tree
|
|
16366
|
-
//
|
|
16618
|
+
// The branch SCALE, when the tree can be laid out by time and by
|
|
16619
|
+
// divergence (forester.hasTimeAndDivergence). Hidden otherwise,
|
|
16367
16620
|
// which is most trees.
|
|
16368
16621
|
h = h.concat('<div class="' + BRANCH_SCALE_CONTROLGROUP + ' aptx-segmented">');
|
|
16369
16622
|
h = h.concat(makeSegment('Time', BRANCH_SCALE_TIME_BUTTON, 'branch_scale_radio',
|
|
16370
|
-
'
|
|
16623
|
+
'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
16624
|
h = h.concat(makeSegment('Div', BRANCH_SCALE_DIV_BUTTON, 'branch_scale_radio',
|
|
16372
|
-
'
|
|
16625
|
+
'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
16626
|
h = h.concat('</div>');
|
|
16374
16627
|
h = h.concat('</div>');
|
|
16375
16628
|
h = h.concat('</fieldset>');
|
|
@@ -16471,8 +16724,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
16471
16724
|
if (_timeInfo && _timeInfo.type) {
|
|
16472
16725
|
opts.push(makeCheckboxItem('Time Axis', TIME_AXIS_CB, 'to show/hide the '
|
|
16473
16726
|
+ (_timeInfo.type === 'geologic' ? 'geologic (ICS) time axis' : 'calendar time axis')
|
|
16474
|
-
+ ' and node-age bars (phylogram
|
|
16475
|
-
opts.push(makeCheckboxItem('Time Grid', TIME_GRID_CB, 'to show/hide
|
|
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 '
|
|
16476
16729
|
+ (_timeInfo.type === 'geologic' ? 'geologic interval boundaries' : 'calendar year ticks')));
|
|
16477
16730
|
}
|
|
16478
16731
|
|
|
@@ -16491,7 +16744,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
16491
16744
|
// syncZoomRowButtons). The Y buttons span the whole row.
|
|
16492
16745
|
let h = "";
|
|
16493
16746
|
h = h.concat('<fieldset>');
|
|
16494
|
-
h = h.concat('<legend>
|
|
16747
|
+
h = h.concat('<legend>View & Tools</legend>');
|
|
16495
16748
|
h = h.concat('<div class="aptx-zoomgrid">');
|
|
16496
16749
|
h = h.concat(makeButton('Y+', ZOOM_IN_Y, 'zoom in vertically (or Shift+mousewheel)'));
|
|
16497
16750
|
h = h.concat('<div class="aptx-zoomrow">');
|
|
@@ -16503,8 +16756,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
16503
16756
|
h = h.concat('</div>');
|
|
16504
16757
|
h = h.concat(makeButton('Y-', ZOOM_OUT_Y, 'zoom out vertically (or Shift+mousewheel)'));
|
|
16505
16758
|
h = h.concat('</div>');
|
|
16506
|
-
// the tool buttons share the
|
|
16507
|
-
// a fieldset of their own
|
|
16759
|
+
// the tool buttons share the section with the zoom row rather
|
|
16760
|
+
// than paying for a fieldset of their own -- hence its name
|
|
16761
|
+
// (Christian, 2026-09-30: "ZOOM" named only half of it)
|
|
16508
16762
|
h = h.concat('<div class="aptx-toolrow">');
|
|
16509
16763
|
h = h.concat(makeGlyphButton('ladderize_asc', LADDERIZE_BUTTON, 'ladderize all'));
|
|
16510
16764
|
h = h.concat(makeGlyphButton('whole_tree', RETURN_TO_SUPERTREE_BUTTON, 'return all the way to the complete tree (if in a sub-tree)'));
|
|
@@ -16936,14 +17190,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
16936
17190
|
disableCheckbox('#' + PHYLOGRAM_BUTTON);
|
|
16937
17191
|
disableCheckbox('#' + PHYLOGRAM_ALIGNED_BUTTON);
|
|
16938
17192
|
}
|
|
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
|
-
}
|
|
17193
|
+
syncBranchScaleControls();
|
|
16947
17194
|
}
|
|
16948
17195
|
|
|
16949
17196
|
|
|
@@ -17481,7 +17728,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
17481
17728
|
return _root;
|
|
17482
17729
|
}
|
|
17483
17730
|
let name = _root_const.name ? String(_root_const.name).trim() : '';
|
|
17484
|
-
|
|
17731
|
+
// what the phylogeny itself states goes with the part: without its
|
|
17732
|
+
// branch_length_unit a subtree saved from Div opened again as time
|
|
17733
|
+
let part = Object.assign({}, _root, {name: name ? name + ' (subtree)' : undefined});
|
|
17734
|
+
['rooted', 'rerootable', 'type', 'branch_length_unit'].forEach(function (k) {
|
|
17735
|
+
if (_root_const[k] !== undefined && _root_const[k] !== null) {
|
|
17736
|
+
part[k] = _root_const[k];
|
|
17737
|
+
}
|
|
17738
|
+
});
|
|
17739
|
+
return part;
|
|
17485
17740
|
}
|
|
17486
17741
|
|
|
17487
17742
|
// "subtree, 19 of 50 tips" while a subtree is shown, else null: said in
|