archaeopteryx 3.7.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -7
- package/archaeopteryx.d.ts +6 -1
- package/archaeopteryx.js +336 -31
- package/forester.js +1225 -80
- package/package.json +2 -2
package/archaeopteryx.js
CHANGED
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
24
|
-
// 2026-09-
|
|
23
|
+
// v 3.9.0
|
|
24
|
+
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// Archaeopteryx.js is a software tool for the visualization and
|
|
27
27
|
// analysis of highly annotated phylogenetic trees.
|
|
@@ -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.9.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';
|
|
@@ -277,6 +277,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
277
277
|
const PANEL_STYLE_ID = 'aptx-panel-styles';
|
|
278
278
|
// How wide a legend row is treated as, for grabbing it with the mouse.
|
|
279
279
|
const PANEL_WIDTH = 214; // fixed control-panel width; shared by the .aptx-panel CSS and leftPanelClearance() so the two can't drift
|
|
280
|
+
const PANEL_WIDTH_COMPACT = 196; // the same, at panelDensity 'compact'; panelWidth() picks between them
|
|
280
281
|
const SLIDER_CLASS = 'aptx-slider';
|
|
281
282
|
const SLIDER_STEP = 0.5;
|
|
282
283
|
const SPECIATION_COLOR = '#009E73';
|
|
@@ -517,6 +518,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
517
518
|
let _trees = []; // every tree of the launch (one, or all a file held); _treeData is _trees[_treeIndex]
|
|
518
519
|
let _treeIndex = 0;
|
|
519
520
|
let _launchConfig = null; // the config as launch() received it, for showTree()
|
|
521
|
+
let _treeViews = null; // tree object -> the view it was last left in (see viewForTree)
|
|
522
|
+
let _panelSections = null; // panel section name -> true when folded; the user's furniture, remembered (see loadPanelSections)
|
|
523
|
+
let _panelOpenOrder = new Map(); // section name -> when it was opened, so the fit rule folds the oldest first
|
|
524
|
+
let _panelOpenSeq = 0;
|
|
520
525
|
let _viewOps = {order: null, root: null}; // the tree operations a shared view replays: ladderize direction, midpoint rooting
|
|
521
526
|
let _lastViewEncoded; // the view last reported to onViewChange; undefined = not settled yet
|
|
522
527
|
let _viewNotifyPending = false;
|
|
@@ -4864,6 +4869,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4864
4869
|
'nhExportWriteConfidences',
|
|
4865
4870
|
'nodeLabels',
|
|
4866
4871
|
'onViewChange',
|
|
4872
|
+
'panelDensity',
|
|
4867
4873
|
'pngExportScale',
|
|
4868
4874
|
'rootOffset',
|
|
4869
4875
|
'supportDotMinimum',
|
|
@@ -4939,8 +4945,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
4939
4945
|
// the visualizations legend need this, and neither may hard-code it: the
|
|
4940
4946
|
// panel's own geometry is the only honest source, or the two drift apart --
|
|
4941
4947
|
// which is exactly how the root ended up drawn behind the panel once.
|
|
4948
|
+
// The panel's width for the density in force. The CSS and this function
|
|
4949
|
+
// must agree, or the tree's left margin and the panel drift apart.
|
|
4950
|
+
function panelWidth() {
|
|
4951
|
+
return (_settings && _settings.panelDensity === 'compact') ? PANEL_WIDTH_COMPACT : PANEL_WIDTH;
|
|
4952
|
+
}
|
|
4953
|
+
|
|
4942
4954
|
function leftPanelClearance() {
|
|
4943
|
-
return CONTROLS_0_LEFT_DEFAULT +
|
|
4955
|
+
return CONTROLS_0_LEFT_DEFAULT + panelWidth() + ROOT_CLEARANCE;
|
|
4944
4956
|
}
|
|
4945
4957
|
|
|
4946
4958
|
// The only validation initializeState() performs. Hoisted so launch() can
|
|
@@ -5283,6 +5295,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
5283
5295
|
if (_settings.collapseControlPanel === undefined) {
|
|
5284
5296
|
_settings.collapseControlPanel = false;
|
|
5285
5297
|
}
|
|
5298
|
+
if (_settings.panelDensity === undefined) {
|
|
5299
|
+
_settings.panelDensity = 'comfortable';
|
|
5300
|
+
} else if (_settings.panelDensity !== 'comfortable' && _settings.panelDensity !== 'compact') {
|
|
5301
|
+
throw new Error(ERROR + '"panelDensity" must be "comfortable" or "compact"');
|
|
5302
|
+
}
|
|
5286
5303
|
if (_settings.enableDynamicSizing === undefined) {
|
|
5287
5304
|
_settings.enableDynamicSizing = true;
|
|
5288
5305
|
}
|
|
@@ -5549,6 +5566,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5549
5566
|
_trees = [];
|
|
5550
5567
|
_treeIndex = 0;
|
|
5551
5568
|
_launchConfig = null;
|
|
5569
|
+
_treeViews = null;
|
|
5552
5570
|
_lastViewEncoded = undefined;
|
|
5553
5571
|
_viewOps = {order: null, root: null};
|
|
5554
5572
|
_basicTreeProperties = null;
|
|
@@ -5624,6 +5642,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
5624
5642
|
+ ' is empty or illegally formatted');
|
|
5625
5643
|
}
|
|
5626
5644
|
});
|
|
5645
|
+
// A tip-dated BEAST tree states its node ages as unitless `height`s --
|
|
5646
|
+
// time before the youngest tip -- which say nothing about WHEN. Where
|
|
5647
|
+
// the tip labels carry sampling dates and they agree on the date of
|
|
5648
|
+
// height 0, the tree is placed in calendar time here, before anything
|
|
5649
|
+
// reads a date: the axis, Color-by and the visualization candidates all
|
|
5650
|
+
// depend on it. A tree that does not qualify is left exactly as it was,
|
|
5651
|
+
// and a tree whose dates carry a unit is never touched. The desktop
|
|
5652
|
+
// converts at the same point, next to its internal-label policy
|
|
5653
|
+
// (their HeightDateConverter, 0.11.151).
|
|
5654
|
+
forester.convertLoadedHeightsToDates(trees);
|
|
5627
5655
|
// the two view keys are checked up here with the other arguments,
|
|
5628
5656
|
// before anything is touched (and where Node can test it)
|
|
5629
5657
|
if (config && config.view !== undefined && config.view !== null && typeof config.view !== 'object') {
|
|
@@ -5632,9 +5660,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
5632
5660
|
if (config && config.onViewChange !== undefined && config.onViewChange !== null && typeof config.onViewChange !== 'function') {
|
|
5633
5661
|
throw new Error(ERROR + '"onViewChange" must be a function (state, encoded) or null');
|
|
5634
5662
|
}
|
|
5663
|
+
// checked up here with the others so an embedder's typo fails at the
|
|
5664
|
+
// call rather than three frames deep, and so Node can test it
|
|
5665
|
+
if (config && config.panelDensity !== undefined
|
|
5666
|
+
&& config.panelDensity !== 'comfortable' && config.panelDensity !== 'compact') {
|
|
5667
|
+
throw new Error(ERROR + '"panelDensity" must be "comfortable" or "compact"');
|
|
5668
|
+
}
|
|
5635
5669
|
// a view names the tree it was made on; a fresh launch reports no
|
|
5636
5670
|
// view until one changes (settleViewNotification)
|
|
5637
5671
|
_lastViewEncoded = undefined;
|
|
5672
|
+
// each tree's own view is remembered for the life of this launch,
|
|
5673
|
+
// and only for it: a new launch starts everything fresh
|
|
5674
|
+
_treeViews = new Map();
|
|
5638
5675
|
let view = config && config.view;
|
|
5639
5676
|
let start = (view && Number.isInteger(view.tree) && view.tree > 0 && view.tree < trees.length) ? view.tree : 0;
|
|
5640
5677
|
return launchInto(container, trees, start, config);
|
|
@@ -5818,10 +5855,91 @@ function (root, d3, forester, phyloXml) {
|
|
|
5818
5855
|
return makeViewerHandle();
|
|
5819
5856
|
}
|
|
5820
5857
|
|
|
5858
|
+
// ================= each tree keeps its own view =====================
|
|
5859
|
+
// In a file holding several trees, every tree is its own workspace: you
|
|
5860
|
+
// leave one as you had it -- layout, display type, labels, colours,
|
|
5861
|
+
// sizes, tracks, both searches, the clade you switched to and the ones
|
|
5862
|
+
// you collapsed -- and it comes back that way (Christian, 2026-09-16).
|
|
5863
|
+
// This replaces "every tree opens fresh", and with it the rule that a
|
|
5864
|
+
// switch cleared collapsing (Christian, 2026-09-14, made when a switch
|
|
5865
|
+
// discarded everything else too).
|
|
5866
|
+
//
|
|
5867
|
+
// The memory IS the view state, so a remembered tree opens down exactly
|
|
5868
|
+
// the path a shared link opens: launchInto with cfg.view. Nothing here
|
|
5869
|
+
// knows about layouts or checkboxes; whatever getViewState covers is
|
|
5870
|
+
// remembered, and what it leaves out (zoom and pan, the legend's
|
|
5871
|
+
// position, the selection) stays out here too.
|
|
5872
|
+
//
|
|
5873
|
+
// A tree you have NOT opened yet is not handed the whole view -- what a
|
|
5874
|
+
// tree shows and colours by is read from its own content, and imposing
|
|
5875
|
+
// another tree's answer would waste that. It inherits the geometry
|
|
5876
|
+
// alone: the settings that say nothing about what is in the tree. So
|
|
5877
|
+
// stepping through ten trees of one file in the circular layout keeps
|
|
5878
|
+
// the circular layout, while each tree still labels itself its own way.
|
|
5879
|
+
const VIEW_CARRY_KEYS = ['layout', 'display', 'font', 'node', 'branch', 'rotation', 'horizontalLabels'];
|
|
5880
|
+
|
|
5881
|
+
// The tree on screen, as it stands, filed under the tree OBJECT: indices
|
|
5882
|
+
// shift when a tree is appended (representative tips), objects do not.
|
|
5883
|
+
// The `tree` key goes: it names the index this view came from, and
|
|
5884
|
+
// applying it back would read as a switch to itself.
|
|
5885
|
+
function rememberTreeView() {
|
|
5886
|
+
if (!_treeViews || !_trees[_treeIndex] || !_root_const) {
|
|
5887
|
+
return;
|
|
5888
|
+
}
|
|
5889
|
+
let state = getViewState();
|
|
5890
|
+
delete state.tree;
|
|
5891
|
+
_treeViews.set(_trees[_treeIndex], state);
|
|
5892
|
+
}
|
|
5893
|
+
|
|
5894
|
+
// What to open a tree with: the view it was left in, or -- a tree not
|
|
5895
|
+
// opened yet -- the current tree's geometry alone.
|
|
5896
|
+
function viewForTree(tree) {
|
|
5897
|
+
let remembered = _treeViews && _treeViews.get(tree);
|
|
5898
|
+
if (remembered) {
|
|
5899
|
+
return remembered;
|
|
5900
|
+
}
|
|
5901
|
+
let current = _root_const ? getViewState() : {};
|
|
5902
|
+
let carried = {};
|
|
5903
|
+
VIEW_CARRY_KEYS.forEach(function (k) {
|
|
5904
|
+
if (current[k] !== undefined) {
|
|
5905
|
+
carried[k] = current[k];
|
|
5906
|
+
}
|
|
5907
|
+
});
|
|
5908
|
+
return carried;
|
|
5909
|
+
}
|
|
5910
|
+
|
|
5911
|
+
// The one way from one tree of the list to another: the tree being left
|
|
5912
|
+
// is remembered first, and the launch config's own view (a shared link's)
|
|
5913
|
+
// never leaks onto the next tree -- _launchConfig is put back afterwards,
|
|
5914
|
+
// so every later switch still opens each tree its own way. `view` is for
|
|
5915
|
+
// a caller that already has one (a hash naming another tree).
|
|
5916
|
+
function switchToTree(index, view) {
|
|
5917
|
+
rememberTreeView();
|
|
5918
|
+
// The tree being left goes back to the way it was PARSED: its collapsed
|
|
5919
|
+
// clades live in the view just recorded, not on its nodes. Leaving the
|
|
5920
|
+
// flags on the object would change the COLOURS it comes back with --
|
|
5921
|
+
// the palette is dealt once per launch, over the values summarizeView
|
|
5922
|
+
// can see, and a tree whose nodes still carried collapse would deal it
|
|
5923
|
+
// over the reduced set and hand every value a different colour. A
|
|
5924
|
+
// shared link into a collapsed view never does that: it opens a freshly
|
|
5925
|
+
// parsed tree, deals over all of it, and collapses afterwards. A tree
|
|
5926
|
+
// coming back must not either. (Measured on the flavivirus demo: the
|
|
5927
|
+
// legend came back re-coloured until this line went in.)
|
|
5928
|
+
if (_trees[_treeIndex]) {
|
|
5929
|
+
clearCollapsedFlags(_trees[_treeIndex]);
|
|
5930
|
+
}
|
|
5931
|
+
let launchConfig = _launchConfig;
|
|
5932
|
+
let cfg = Object.assign({}, launchConfig || {});
|
|
5933
|
+
cfg.view = view || viewForTree(_trees[index]);
|
|
5934
|
+
let handle = launchInto(_container, _trees, index, cfg);
|
|
5935
|
+
_launchConfig = launchConfig;
|
|
5936
|
+
return handle;
|
|
5937
|
+
}
|
|
5938
|
+
|
|
5821
5939
|
// Shows another tree of the launched list in the same container under
|
|
5822
|
-
// the same config
|
|
5823
|
-
//
|
|
5824
|
-
//
|
|
5940
|
+
// the same config: as you left it, or -- never opened -- on its own
|
|
5941
|
+
// presets in the current geometry. The panel's picker and previous /
|
|
5942
|
+
// next buttons come here, as does the handle's showTree().
|
|
5825
5943
|
function showTree(index) {
|
|
5826
5944
|
if (!_container) {
|
|
5827
5945
|
throw new Error(ERROR + 'showTree(): nothing is launched');
|
|
@@ -5832,11 +5950,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
5832
5950
|
if (index === _treeIndex) {
|
|
5833
5951
|
return makeViewerHandle();
|
|
5834
5952
|
}
|
|
5835
|
-
//
|
|
5836
|
-
//
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
return
|
|
5953
|
+
// Neither tree's collapsed flags are carried on the nodes: the tree
|
|
5954
|
+
// being left has them recorded in its view and then cleared
|
|
5955
|
+
// (switchToTree), and the one arriving has applyViewState clear and
|
|
5956
|
+
// re-apply its own -- a view is always passed, so that always runs.
|
|
5957
|
+
return switchToTree(index, null);
|
|
5840
5958
|
}
|
|
5841
5959
|
|
|
5842
5960
|
archaeopteryx.parsePhyloXML = function (data) {
|
|
@@ -7639,9 +7757,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7639
7757
|
return;
|
|
7640
7758
|
}
|
|
7641
7759
|
if (!initial && Number.isInteger(s.tree) && s.tree !== _treeIndex && s.tree >= 0 && s.tree < _trees.length) {
|
|
7642
|
-
|
|
7643
|
-
cfg.view = s;
|
|
7644
|
-
launchInto(_container, _trees, s.tree, cfg);
|
|
7760
|
+
switchToTree(s.tree, s); // the view given names another tree: it wins over what that tree remembers
|
|
7645
7761
|
return;
|
|
7646
7762
|
}
|
|
7647
7763
|
if ((s.root === 'midpoint' || s.root === 'mad') && _viewOps.root !== s.root
|
|
@@ -7793,6 +7909,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
7793
7909
|
setValue(idx === 0 ? SEARCH_VALUE2_0 : SEARCH_VALUE2_1, want.value2 ? String(want.value2) : '');
|
|
7794
7910
|
updateSearchValue2Visibility(idx);
|
|
7795
7911
|
updateSearchAutocomplete(idx);
|
|
7912
|
+
if (want.value) {
|
|
7913
|
+
openPanelSection('Search'); // a view carrying a search shows it
|
|
7914
|
+
}
|
|
7796
7915
|
if (idx === 1 && want.value) {
|
|
7797
7916
|
showSearchB();
|
|
7798
7917
|
}
|
|
@@ -8115,6 +8234,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8115
8234
|
}
|
|
8116
8235
|
|
|
8117
8236
|
function focusSearch() {
|
|
8237
|
+
openPanelSection('Search'); // jumping to a box inside a folded section has to open it
|
|
8118
8238
|
let f = byId(SEARCH_FIELD_0);
|
|
8119
8239
|
if (f) {
|
|
8120
8240
|
f.focus();
|
|
@@ -8814,6 +8934,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8814
8934
|
// Search B is folded away until wanted: the '+ Search B' link (which
|
|
8815
8935
|
// focuses it) or a view that carries a second search.
|
|
8816
8936
|
function showSearchB() {
|
|
8937
|
+
openPanelSection('Search');
|
|
8817
8938
|
let wrap = byId(SEARCH_B_WRAP);
|
|
8818
8939
|
if (wrap) {
|
|
8819
8940
|
wrap.style.display = '';
|
|
@@ -9009,10 +9130,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
9009
9130
|
|
|
9010
9131
|
let sc = info.type === 'calendar' ? -corr : corr;
|
|
9011
9132
|
|
|
9012
|
-
// ---- HPD age bars (internal) + fossil
|
|
9133
|
+
// ---- HPD age bars (internal) + tip bars: fossil ranges, or sampling dates ----
|
|
9013
9134
|
forEachDisplayed(function (d) {
|
|
9014
|
-
|
|
9015
|
-
|
|
9135
|
+
// forester.isGenuineDateInterval, not a bare min/max test: the
|
|
9136
|
+
// bars are floored at 1px below, so a bound pair that differs only
|
|
9137
|
+
// by floating-point noise would draw as a visible bracket. This
|
|
9138
|
+
// guard is on EVERY node, tip and internal, geologic and calendar.
|
|
9139
|
+
if (!forester.isGenuineDateInterval(d.date) || d.y === undefined) {
|
|
9016
9140
|
return;
|
|
9017
9141
|
}
|
|
9018
9142
|
let min = d.date.minimum;
|
|
@@ -9028,10 +9152,26 @@ function (root, d3, forester, phyloXml) {
|
|
|
9028
9152
|
let left = Math.min(xa, xb);
|
|
9029
9153
|
let w = Math.max(1, Math.abs(xb - xa));
|
|
9030
9154
|
let y = d.x;
|
|
9155
|
+
// What a TIP's interval means is the axis's to say. On geologic
|
|
9156
|
+
// time it is a fossil's observed range, FAD to LAD, and is drawn
|
|
9157
|
+
// as one below. On CALENDAR time it is the uncertainty of a
|
|
9158
|
+
// sampling date -- a virus sample dated only to its month or year
|
|
9159
|
+
// -- which is the same kind of thing as an internal node's age
|
|
9160
|
+
// interval and is drawn the same way, slimmer. Either way it is
|
|
9161
|
+
// drawn only when it HAS a width: a tip dated to the day states
|
|
9162
|
+
// {d,d}, and a capped tick on every such tip is what made the
|
|
9163
|
+
// readers throw tip intervals away until 2026-09-17 (Christian;
|
|
9164
|
+
// the desktop alike). The width test is the entry guard above.
|
|
9165
|
+
let sampledTip = !d.children && info.type === 'calendar';
|
|
9031
9166
|
if (d.children) {
|
|
9032
9167
|
g.append('rect').attr('x', left).attr('y', y - 3.5)
|
|
9033
9168
|
.attr('width', w).attr('height', 7)
|
|
9034
9169
|
.attr('fill', HPD_BAR_COLOR);
|
|
9170
|
+
} else if (sampledTip) {
|
|
9171
|
+
g.append('rect').attr('class', 'aptx-sampling-bar')
|
|
9172
|
+
.attr('x', left).attr('y', y - 2.5)
|
|
9173
|
+
.attr('width', w).attr('height', 5)
|
|
9174
|
+
.attr('fill', HPD_BAR_COLOR);
|
|
9035
9175
|
} else {
|
|
9036
9176
|
g.append('rect').attr('x', left).attr('y', y - 2.5)
|
|
9037
9177
|
.attr('width', w).attr('height', 5)
|
|
@@ -9675,6 +9815,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9675
9815
|
// found nodes: A's colour, the hit count, the step-through. The next
|
|
9676
9816
|
// search replaces them, and resetting search A clears them.
|
|
9677
9817
|
function showRepresentativesAsHits(result) {
|
|
9818
|
+
openPanelSection('Search'); // the tips kept are search A's hits: its navigator has to be visible
|
|
9678
9819
|
_foundNodes0 = new Set(result.keptTips);
|
|
9679
9820
|
_searchBox0Empty = false;
|
|
9680
9821
|
_searchHitIndex = -1;
|
|
@@ -9722,14 +9863,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
9722
9863
|
forester.stripShortExtension(parentName), result.tipCount);
|
|
9723
9864
|
copy.description = copy.description ? copy.description + ' ' + provenance : provenance;
|
|
9724
9865
|
|
|
9725
|
-
let display = _state.phylogram ? (_state.alignPhylogram ? 'aligned' : 'phylogram') : 'cladogram';
|
|
9726
|
-
clearCollapsedFlags(_trees[_treeIndex]);
|
|
9727
9866
|
_trees.push(copy);
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9867
|
+
// a tree nobody has opened: switchToTree gives it this tree's
|
|
9868
|
+
// geometry, which is what "drawn as the tree was" amounts to --
|
|
9869
|
+
// the display type (phylogram, aligned or cladogram) and the
|
|
9870
|
+
// layout. The tree it came from is remembered as it stands.
|
|
9871
|
+
switchToTree(_trees.length - 1, null);
|
|
9733
9872
|
}
|
|
9734
9873
|
|
|
9735
9874
|
function escPressed() {
|
|
@@ -10794,6 +10933,120 @@ function (root, d3, forester, phyloXml) {
|
|
|
10794
10933
|
}
|
|
10795
10934
|
}
|
|
10796
10935
|
|
|
10936
|
+
// ============ the panel's sections: what is open, and what fits ========
|
|
10937
|
+
// Three rules, deliberately separate, for hosts with little room to give
|
|
10938
|
+
// (BV-BRC embeds the viewer in a page that already has its own chrome):
|
|
10939
|
+
//
|
|
10940
|
+
// - DEFAULTS. A tree arrives with the sections that DESCRIBE it open --
|
|
10941
|
+
// what it can be coloured by, what it shows -- plus Search, which is
|
|
10942
|
+
// reached for constantly (Christian, 2026-09-16). Folded are the ones
|
|
10943
|
+
// that are adjustments for later: zoom, sizes, the domain controls.
|
|
10944
|
+
// Measured on the 13-property BV-BRC demo in a 700px window: 766px of
|
|
10945
|
+
// panel content becomes 621px, with nothing behind a scroll.
|
|
10946
|
+
// - MEMORY. What the user opens and closes is theirs, and it outlives
|
|
10947
|
+
// the tree: kept for the page and in localStorage, like the light/dark
|
|
10948
|
+
// choice. It is NOT part of a tree's view -- the panel is the user's
|
|
10949
|
+
// furniture, not a property of the tree -- which is why it is not in
|
|
10950
|
+
// getViewState, and why a tree switch no longer reopens everything.
|
|
10951
|
+
// - FIT. Opening a section in a short window folds the section opened
|
|
10952
|
+
// longest ago, and ONLY while the panel would not otherwise fit. On a
|
|
10953
|
+
// tall screen it never fires. It is bounded by PIXELS, not by a count
|
|
10954
|
+
// of open sections: three open sections measure anywhere from 215px to
|
|
10955
|
+
// 562px, so a count would not have bounded the height at all.
|
|
10956
|
+
const PANEL_SECTIONS_KEY = 'aptx-panel-sections';
|
|
10957
|
+
const PANEL_SECTIONS_CLOSED_BY_DEFAULT = ['Zoom', 'Sizes', 'Domain Architectures'];
|
|
10958
|
+
|
|
10959
|
+
function loadPanelSections() {
|
|
10960
|
+
if (_panelSections) {
|
|
10961
|
+
return _panelSections;
|
|
10962
|
+
}
|
|
10963
|
+
_panelSections = {};
|
|
10964
|
+
try {
|
|
10965
|
+
let saved = JSON.parse(localStorage.getItem(PANEL_SECTIONS_KEY));
|
|
10966
|
+
if (saved && typeof saved === 'object') {
|
|
10967
|
+
Object.keys(saved).forEach(function (k) {
|
|
10968
|
+
if (typeof saved[k] === 'boolean') {
|
|
10969
|
+
_panelSections[k] = saved[k];
|
|
10970
|
+
}
|
|
10971
|
+
});
|
|
10972
|
+
}
|
|
10973
|
+
} catch {
|
|
10974
|
+
// no storage (private mode), or junk written by something else:
|
|
10975
|
+
// the defaults are a complete answer on their own
|
|
10976
|
+
}
|
|
10977
|
+
return _panelSections;
|
|
10978
|
+
}
|
|
10979
|
+
|
|
10980
|
+
function savePanelSections() {
|
|
10981
|
+
try {
|
|
10982
|
+
localStorage.setItem(PANEL_SECTIONS_KEY, JSON.stringify(_panelSections || {}));
|
|
10983
|
+
} catch {
|
|
10984
|
+
// the choice just won't survive a reload
|
|
10985
|
+
}
|
|
10986
|
+
}
|
|
10987
|
+
|
|
10988
|
+
// Every collapsible section of a panel, in the order they are drawn.
|
|
10989
|
+
function panelSections(panel) {
|
|
10990
|
+
let out = [];
|
|
10991
|
+
panel.querySelectorAll('fieldset').forEach(function (f) {
|
|
10992
|
+
let lg = f.querySelector(':scope > legend.aptx-legend-toggle');
|
|
10993
|
+
if (lg) {
|
|
10994
|
+
out.push({name: lg.textContent.trim(), fieldset: f});
|
|
10995
|
+
}
|
|
10996
|
+
});
|
|
10997
|
+
return out;
|
|
10998
|
+
}
|
|
10999
|
+
|
|
11000
|
+
function sectionFoldedInitially(name) {
|
|
11001
|
+
let remembered = loadPanelSections()[name];
|
|
11002
|
+
return (typeof remembered === 'boolean')
|
|
11003
|
+
? remembered : PANEL_SECTIONS_CLOSED_BY_DEFAULT.indexOf(name) >= 0;
|
|
11004
|
+
}
|
|
11005
|
+
|
|
11006
|
+
// By how many pixels the panel's body overflows the room it has.
|
|
11007
|
+
function panelOverflow(panel) {
|
|
11008
|
+
let body = panel.querySelector(':scope > .aptx-body');
|
|
11009
|
+
return body ? (body.scrollHeight - body.clientHeight) : 0;
|
|
11010
|
+
}
|
|
11011
|
+
|
|
11012
|
+
// Keeps the panel to one screen: while it overflows, fold the section
|
|
11013
|
+
// opened longest ago. Never the one just opened -- that is the one the
|
|
11014
|
+
// user asked for -- and never the last one standing.
|
|
11015
|
+
function fitPanelSections(panel, justOpened) {
|
|
11016
|
+
let guard = 0;
|
|
11017
|
+
while (panelOverflow(panel) > 0 && ++guard < 20) {
|
|
11018
|
+
let open = panelSections(panel).filter(function (s) {
|
|
11019
|
+
return !s.fieldset.classList.contains('aptx-collapsed') && s.name !== justOpened;
|
|
11020
|
+
});
|
|
11021
|
+
if (open.length === 0) {
|
|
11022
|
+
return;
|
|
11023
|
+
}
|
|
11024
|
+
open.sort(function (a, b) {
|
|
11025
|
+
return (_panelOpenOrder.get(a.name) || 0) - (_panelOpenOrder.get(b.name) || 0);
|
|
11026
|
+
});
|
|
11027
|
+
open[0].fieldset.classList.add('aptx-collapsed');
|
|
11028
|
+
loadPanelSections()[open[0].name] = true;
|
|
11029
|
+
}
|
|
11030
|
+
}
|
|
11031
|
+
|
|
11032
|
+
// Unfolds a section because something the user just did has to be SEEN
|
|
11033
|
+
// inside it: the search box they jumped to, the hits a tool marked.
|
|
11034
|
+
function openPanelSection(name) {
|
|
11035
|
+
let panel = _container && _container.querySelector(':scope > .aptx-panel');
|
|
11036
|
+
if (!panel) {
|
|
11037
|
+
return;
|
|
11038
|
+
}
|
|
11039
|
+
panelSections(panel).forEach(function (s) {
|
|
11040
|
+
if (s.name === name && s.fieldset.classList.contains('aptx-collapsed')) {
|
|
11041
|
+
s.fieldset.classList.remove('aptx-collapsed');
|
|
11042
|
+
loadPanelSections()[name] = false;
|
|
11043
|
+
_panelOpenOrder.set(name, ++_panelOpenSeq);
|
|
11044
|
+
savePanelSections();
|
|
11045
|
+
fitPanelSections(panel, name);
|
|
11046
|
+
}
|
|
11047
|
+
});
|
|
11048
|
+
}
|
|
11049
|
+
|
|
10797
11050
|
// ===================== Control-panel glyphs =====================
|
|
10798
11051
|
// Vector glyphs ported from the desktop Archaeopteryx control panel (its
|
|
10799
11052
|
// DisplayTypeIcon / ControlButtonIcon / LayoutIcon / ThemeToggleIcon /
|
|
@@ -11496,7 +11749,28 @@ function (root, d3, forester, phyloXml) {
|
|
|
11496
11749
|
+ '.aptx-panel .aptx-hide-btn { flex:none; width:20px; height:20px; display:grid; place-items:center; padding:0; border:1px solid var(--p-line-strong); border-radius:6px; background:var(--p-surface2); color:var(--p-muted); cursor:pointer; font-size:15px; line-height:1; }'
|
|
11497
11750
|
+ '.aptx-panel .aptx-hide-btn:hover { background:var(--p-accent-weak); color:var(--p-accent-ink); border-color:var(--p-accent); }'
|
|
11498
11751
|
+ '.aptx-panel.aptx-hidden > .aptx-body { display:none; }'
|
|
11499
|
-
+ '.aptx-panel.aptx-hidden > .' + PROG_NAME + ' { border-bottom:0; }'
|
|
11752
|
+
+ '.aptx-panel.aptx-hidden > .' + PROG_NAME + ' { border-bottom:0; }'
|
|
11753
|
+
// --- compact density (the panelDensity config key) ---------------
|
|
11754
|
+
// Nothing is removed and nothing is hidden: the same controls,
|
|
11755
|
+
// tighter, for a host with little room to give. The width lives
|
|
11756
|
+
// here as well, and panelWidth() returns the same number so the
|
|
11757
|
+
// tree's left margin follows it (leftPanelClearance).
|
|
11758
|
+
+ '.aptx-panel.aptx-compact { width:' + PANEL_WIDTH_COMPACT + 'px; font-size:10.5px; }'
|
|
11759
|
+
+ '.aptx-panel.aptx-compact fieldset { padding:4px 9px; }'
|
|
11760
|
+
+ '.aptx-panel.aptx-compact legend { margin:0 0 3px; }'
|
|
11761
|
+
+ '.aptx-panel.aptx-compact .' + PROG_NAME + ' { padding:6px 9px; }'
|
|
11762
|
+
// the title is a fixed 12px, which wrapped onto a second line once
|
|
11763
|
+
// the panel narrowed: it comes down with everything else
|
|
11764
|
+
+ '.aptx-panel.aptx-compact .' + PROGNAMELINK + ' { font-size:10.5px; }'
|
|
11765
|
+
+ '.aptx-panel.aptx-compact .aptx-checkgrid { gap:2px 8px; }'
|
|
11766
|
+
+ '.aptx-panel.aptx-compact .aptx-field-label { margin:5px 0 2px; }'
|
|
11767
|
+
+ '.aptx-panel.aptx-compact .aptx-subhead { margin:4px 0 2px; }'
|
|
11768
|
+
+ '.aptx-panel.aptx-compact input[type=range] { margin:1px 0 6px; }'
|
|
11769
|
+
+ '.aptx-panel.aptx-compact .aptx-slider-row { margin:2px 0; }'
|
|
11770
|
+
+ '.aptx-panel.aptx-compact input[type=button], .aptx-panel.aptx-compact .aptx-gbtn { height:22px; margin:1px 2px 1px 0; }'
|
|
11771
|
+
+ '.aptx-panel.aptx-compact .aptx-search-row input[type=text], .aptx-panel.aptx-compact .aptx-search-row input[type=button] { height:24px; }'
|
|
11772
|
+
+ '.aptx-panel.aptx-compact .aptx-search-menus select { height:22px; }'
|
|
11773
|
+
+ '.aptx-panel.aptx-compact .aptx-toolrow { margin-top:3px; }';
|
|
11500
11774
|
let style = document.createElement('style');
|
|
11501
11775
|
style.id = PANEL_STYLE_ID;
|
|
11502
11776
|
style.textContent = css;
|
|
@@ -11585,6 +11859,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11585
11859
|
// Collapsible sections: wrap each titled fieldset's content in a body
|
|
11586
11860
|
// element (so collapsing hides everything under the legend, including
|
|
11587
11861
|
// bare text labels like the slider captions) and toggle it via the legend.
|
|
11862
|
+
let initiallyOpen = [];
|
|
11588
11863
|
let fieldsets = body.querySelectorAll('fieldset');
|
|
11589
11864
|
for (let i = 0; i < fieldsets.length; ++i) {
|
|
11590
11865
|
let fieldset = fieldsets[i];
|
|
@@ -11602,10 +11877,34 @@ function (root, d3, forester, phyloXml) {
|
|
|
11602
11877
|
}
|
|
11603
11878
|
fieldset.appendChild(fsBody);
|
|
11604
11879
|
legend.classList.add('aptx-legend-toggle');
|
|
11880
|
+
let name = legend.textContent.trim();
|
|
11881
|
+
if (sectionFoldedInitially(name)) {
|
|
11882
|
+
fieldset.classList.add('aptx-collapsed');
|
|
11883
|
+
} else {
|
|
11884
|
+
initiallyOpen.push(name);
|
|
11885
|
+
}
|
|
11605
11886
|
legend.addEventListener('click', function () {
|
|
11606
|
-
fieldset.classList.toggle('aptx-collapsed');
|
|
11887
|
+
let opened = !fieldset.classList.toggle('aptx-collapsed');
|
|
11888
|
+
loadPanelSections()[name] = !opened;
|
|
11889
|
+
if (opened) {
|
|
11890
|
+
_panelOpenOrder.set(name, ++_panelOpenSeq);
|
|
11891
|
+
fitPanelSections(panel, name);
|
|
11892
|
+
}
|
|
11893
|
+
savePanelSections();
|
|
11607
11894
|
});
|
|
11608
11895
|
}
|
|
11896
|
+
// What arrived open counts as opened, oldest at the BOTTOM: a panel is
|
|
11897
|
+
// read top-down, so when a short window forces something to fold on
|
|
11898
|
+
// arrival it should be the last section rather than the one describing
|
|
11899
|
+
// the tree at the top. Anything the user opens later is newer than all
|
|
11900
|
+
// of these, and folds after them.
|
|
11901
|
+
for (let i = initiallyOpen.length - 1; i >= 0; --i) {
|
|
11902
|
+
_panelOpenOrder.set(initiallyOpen[i], ++_panelOpenSeq);
|
|
11903
|
+
}
|
|
11904
|
+
// A remembered set of open sections can be taller than this window,
|
|
11905
|
+
// so the fit rule runs once on arrival too -- with no section to
|
|
11906
|
+
// spare, since the user opened none of them just now.
|
|
11907
|
+
fitPanelSections(panel, null);
|
|
11609
11908
|
|
|
11610
11909
|
// Apply the current light/dark choice to this (and every) panel.
|
|
11611
11910
|
applyPanelTheme();
|
|
@@ -11903,7 +12202,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
11903
12202
|
return null;
|
|
11904
12203
|
}
|
|
11905
12204
|
let hasValue = typeof date.value === 'number';
|
|
11906
|
-
|
|
12205
|
+
// the same predicate the bars use: a bound pair that differs only by
|
|
12206
|
+
// floating-point noise is one number printed twice, and reading it as a
|
|
12207
|
+
// range put "[12.059999999999942 - 12.059999999999949]" in the tooltip
|
|
12208
|
+
let hasRange = forester.isGenuineDateInterval(date);
|
|
11907
12209
|
let s = hasValue ? String(date.value) : '';
|
|
11908
12210
|
if (hasRange) {
|
|
11909
12211
|
s += ' [' + date.minimum + ' - ' + date.maximum + ']';
|
|
@@ -12272,6 +12574,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
12272
12574
|
|
|
12273
12575
|
if (c0) {
|
|
12274
12576
|
c0.classList.add('aptx-panel');
|
|
12577
|
+
if (_settings.panelDensity === 'compact') {
|
|
12578
|
+
c0.classList.add('aptx-compact');
|
|
12579
|
+
}
|
|
12275
12580
|
setStyles(c0, {
|
|
12276
12581
|
'position': 'absolute',
|
|
12277
12582
|
'left': CONTROLS_0_LEFT_DEFAULT,
|
|
@@ -12746,9 +13051,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
12746
13051
|
|
|
12747
13052
|
// A file with several trees: previous / picker / next in one row,
|
|
12748
13053
|
// the picker listing every tree by its name (or its number). Each
|
|
12749
|
-
// opens
|
|
12750
|
-
// disabled at the ends: the panel is rebuilt per tree,
|
|
12751
|
-
// state set here is always current.
|
|
13054
|
+
// opens in the same container as you left it (showTree). The end
|
|
13055
|
+
// buttons are disabled at the ends: the panel is rebuilt per tree,
|
|
13056
|
+
// so the state set here is always current.
|
|
12752
13057
|
function makeTreePicker() {
|
|
12753
13058
|
let n = _trees.length;
|
|
12754
13059
|
let row = document.createElement('div');
|