archaeopteryx 3.7.0 → 3.8.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 +318 -27
- package/forester.js +640 -58
- package/package.json +1 -1
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.8.0
|
|
24
24
|
// 2026-09-10
|
|
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.8.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;
|
|
@@ -5632,9 +5650,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
5632
5650
|
if (config && config.onViewChange !== undefined && config.onViewChange !== null && typeof config.onViewChange !== 'function') {
|
|
5633
5651
|
throw new Error(ERROR + '"onViewChange" must be a function (state, encoded) or null');
|
|
5634
5652
|
}
|
|
5653
|
+
// checked up here with the others so an embedder's typo fails at the
|
|
5654
|
+
// call rather than three frames deep, and so Node can test it
|
|
5655
|
+
if (config && config.panelDensity !== undefined
|
|
5656
|
+
&& config.panelDensity !== 'comfortable' && config.panelDensity !== 'compact') {
|
|
5657
|
+
throw new Error(ERROR + '"panelDensity" must be "comfortable" or "compact"');
|
|
5658
|
+
}
|
|
5635
5659
|
// a view names the tree it was made on; a fresh launch reports no
|
|
5636
5660
|
// view until one changes (settleViewNotification)
|
|
5637
5661
|
_lastViewEncoded = undefined;
|
|
5662
|
+
// each tree's own view is remembered for the life of this launch,
|
|
5663
|
+
// and only for it: a new launch starts everything fresh
|
|
5664
|
+
_treeViews = new Map();
|
|
5638
5665
|
let view = config && config.view;
|
|
5639
5666
|
let start = (view && Number.isInteger(view.tree) && view.tree > 0 && view.tree < trees.length) ? view.tree : 0;
|
|
5640
5667
|
return launchInto(container, trees, start, config);
|
|
@@ -5818,10 +5845,91 @@ function (root, d3, forester, phyloXml) {
|
|
|
5818
5845
|
return makeViewerHandle();
|
|
5819
5846
|
}
|
|
5820
5847
|
|
|
5848
|
+
// ================= each tree keeps its own view =====================
|
|
5849
|
+
// In a file holding several trees, every tree is its own workspace: you
|
|
5850
|
+
// leave one as you had it -- layout, display type, labels, colours,
|
|
5851
|
+
// sizes, tracks, both searches, the clade you switched to and the ones
|
|
5852
|
+
// you collapsed -- and it comes back that way (Christian, 2026-09-16).
|
|
5853
|
+
// This replaces "every tree opens fresh", and with it the rule that a
|
|
5854
|
+
// switch cleared collapsing (Christian, 2026-09-14, made when a switch
|
|
5855
|
+
// discarded everything else too).
|
|
5856
|
+
//
|
|
5857
|
+
// The memory IS the view state, so a remembered tree opens down exactly
|
|
5858
|
+
// the path a shared link opens: launchInto with cfg.view. Nothing here
|
|
5859
|
+
// knows about layouts or checkboxes; whatever getViewState covers is
|
|
5860
|
+
// remembered, and what it leaves out (zoom and pan, the legend's
|
|
5861
|
+
// position, the selection) stays out here too.
|
|
5862
|
+
//
|
|
5863
|
+
// A tree you have NOT opened yet is not handed the whole view -- what a
|
|
5864
|
+
// tree shows and colours by is read from its own content, and imposing
|
|
5865
|
+
// another tree's answer would waste that. It inherits the geometry
|
|
5866
|
+
// alone: the settings that say nothing about what is in the tree. So
|
|
5867
|
+
// stepping through ten trees of one file in the circular layout keeps
|
|
5868
|
+
// the circular layout, while each tree still labels itself its own way.
|
|
5869
|
+
const VIEW_CARRY_KEYS = ['layout', 'display', 'font', 'node', 'branch', 'rotation', 'horizontalLabels'];
|
|
5870
|
+
|
|
5871
|
+
// The tree on screen, as it stands, filed under the tree OBJECT: indices
|
|
5872
|
+
// shift when a tree is appended (representative tips), objects do not.
|
|
5873
|
+
// The `tree` key goes: it names the index this view came from, and
|
|
5874
|
+
// applying it back would read as a switch to itself.
|
|
5875
|
+
function rememberTreeView() {
|
|
5876
|
+
if (!_treeViews || !_trees[_treeIndex] || !_root_const) {
|
|
5877
|
+
return;
|
|
5878
|
+
}
|
|
5879
|
+
let state = getViewState();
|
|
5880
|
+
delete state.tree;
|
|
5881
|
+
_treeViews.set(_trees[_treeIndex], state);
|
|
5882
|
+
}
|
|
5883
|
+
|
|
5884
|
+
// What to open a tree with: the view it was left in, or -- a tree not
|
|
5885
|
+
// opened yet -- the current tree's geometry alone.
|
|
5886
|
+
function viewForTree(tree) {
|
|
5887
|
+
let remembered = _treeViews && _treeViews.get(tree);
|
|
5888
|
+
if (remembered) {
|
|
5889
|
+
return remembered;
|
|
5890
|
+
}
|
|
5891
|
+
let current = _root_const ? getViewState() : {};
|
|
5892
|
+
let carried = {};
|
|
5893
|
+
VIEW_CARRY_KEYS.forEach(function (k) {
|
|
5894
|
+
if (current[k] !== undefined) {
|
|
5895
|
+
carried[k] = current[k];
|
|
5896
|
+
}
|
|
5897
|
+
});
|
|
5898
|
+
return carried;
|
|
5899
|
+
}
|
|
5900
|
+
|
|
5901
|
+
// The one way from one tree of the list to another: the tree being left
|
|
5902
|
+
// is remembered first, and the launch config's own view (a shared link's)
|
|
5903
|
+
// never leaks onto the next tree -- _launchConfig is put back afterwards,
|
|
5904
|
+
// so every later switch still opens each tree its own way. `view` is for
|
|
5905
|
+
// a caller that already has one (a hash naming another tree).
|
|
5906
|
+
function switchToTree(index, view) {
|
|
5907
|
+
rememberTreeView();
|
|
5908
|
+
// The tree being left goes back to the way it was PARSED: its collapsed
|
|
5909
|
+
// clades live in the view just recorded, not on its nodes. Leaving the
|
|
5910
|
+
// flags on the object would change the COLOURS it comes back with --
|
|
5911
|
+
// the palette is dealt once per launch, over the values summarizeView
|
|
5912
|
+
// can see, and a tree whose nodes still carried collapse would deal it
|
|
5913
|
+
// over the reduced set and hand every value a different colour. A
|
|
5914
|
+
// shared link into a collapsed view never does that: it opens a freshly
|
|
5915
|
+
// parsed tree, deals over all of it, and collapses afterwards. A tree
|
|
5916
|
+
// coming back must not either. (Measured on the flavivirus demo: the
|
|
5917
|
+
// legend came back re-coloured until this line went in.)
|
|
5918
|
+
if (_trees[_treeIndex]) {
|
|
5919
|
+
clearCollapsedFlags(_trees[_treeIndex]);
|
|
5920
|
+
}
|
|
5921
|
+
let launchConfig = _launchConfig;
|
|
5922
|
+
let cfg = Object.assign({}, launchConfig || {});
|
|
5923
|
+
cfg.view = view || viewForTree(_trees[index]);
|
|
5924
|
+
let handle = launchInto(_container, _trees, index, cfg);
|
|
5925
|
+
_launchConfig = launchConfig;
|
|
5926
|
+
return handle;
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5821
5929
|
// Shows another tree of the launched list in the same container under
|
|
5822
|
-
// the same config
|
|
5823
|
-
//
|
|
5824
|
-
//
|
|
5930
|
+
// the same config: as you left it, or -- never opened -- on its own
|
|
5931
|
+
// presets in the current geometry. The panel's picker and previous /
|
|
5932
|
+
// next buttons come here, as does the handle's showTree().
|
|
5825
5933
|
function showTree(index) {
|
|
5826
5934
|
if (!_container) {
|
|
5827
5935
|
throw new Error(ERROR + 'showTree(): nothing is launched');
|
|
@@ -5832,11 +5940,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
5832
5940
|
if (index === _treeIndex) {
|
|
5833
5941
|
return makeViewerHandle();
|
|
5834
5942
|
}
|
|
5835
|
-
//
|
|
5836
|
-
//
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
return
|
|
5943
|
+
// Neither tree's collapsed flags are carried on the nodes: the tree
|
|
5944
|
+
// being left has them recorded in its view and then cleared
|
|
5945
|
+
// (switchToTree), and the one arriving has applyViewState clear and
|
|
5946
|
+
// re-apply its own -- a view is always passed, so that always runs.
|
|
5947
|
+
return switchToTree(index, null);
|
|
5840
5948
|
}
|
|
5841
5949
|
|
|
5842
5950
|
archaeopteryx.parsePhyloXML = function (data) {
|
|
@@ -7639,9 +7747,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7639
7747
|
return;
|
|
7640
7748
|
}
|
|
7641
7749
|
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);
|
|
7750
|
+
switchToTree(s.tree, s); // the view given names another tree: it wins over what that tree remembers
|
|
7645
7751
|
return;
|
|
7646
7752
|
}
|
|
7647
7753
|
if ((s.root === 'midpoint' || s.root === 'mad') && _viewOps.root !== s.root
|
|
@@ -7793,6 +7899,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
7793
7899
|
setValue(idx === 0 ? SEARCH_VALUE2_0 : SEARCH_VALUE2_1, want.value2 ? String(want.value2) : '');
|
|
7794
7900
|
updateSearchValue2Visibility(idx);
|
|
7795
7901
|
updateSearchAutocomplete(idx);
|
|
7902
|
+
if (want.value) {
|
|
7903
|
+
openPanelSection('Search'); // a view carrying a search shows it
|
|
7904
|
+
}
|
|
7796
7905
|
if (idx === 1 && want.value) {
|
|
7797
7906
|
showSearchB();
|
|
7798
7907
|
}
|
|
@@ -8115,6 +8224,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8115
8224
|
}
|
|
8116
8225
|
|
|
8117
8226
|
function focusSearch() {
|
|
8227
|
+
openPanelSection('Search'); // jumping to a box inside a folded section has to open it
|
|
8118
8228
|
let f = byId(SEARCH_FIELD_0);
|
|
8119
8229
|
if (f) {
|
|
8120
8230
|
f.focus();
|
|
@@ -8814,6 +8924,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8814
8924
|
// Search B is folded away until wanted: the '+ Search B' link (which
|
|
8815
8925
|
// focuses it) or a view that carries a second search.
|
|
8816
8926
|
function showSearchB() {
|
|
8927
|
+
openPanelSection('Search');
|
|
8817
8928
|
let wrap = byId(SEARCH_B_WRAP);
|
|
8818
8929
|
if (wrap) {
|
|
8819
8930
|
wrap.style.display = '';
|
|
@@ -9009,7 +9120,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9009
9120
|
|
|
9010
9121
|
let sc = info.type === 'calendar' ? -corr : corr;
|
|
9011
9122
|
|
|
9012
|
-
// ---- HPD age bars (internal) + fossil
|
|
9123
|
+
// ---- HPD age bars (internal) + tip bars: fossil ranges, or sampling dates ----
|
|
9013
9124
|
forEachDisplayed(function (d) {
|
|
9014
9125
|
if (!d.date || typeof d.date.minimum !== 'number' || typeof d.date.maximum !== 'number'
|
|
9015
9126
|
|| d.y === undefined) {
|
|
@@ -9028,10 +9139,28 @@ function (root, d3, forester, phyloXml) {
|
|
|
9028
9139
|
let left = Math.min(xa, xb);
|
|
9029
9140
|
let w = Math.max(1, Math.abs(xb - xa));
|
|
9030
9141
|
let y = d.x;
|
|
9142
|
+
// What a TIP's interval means is the axis's to say. On geologic
|
|
9143
|
+
// time it is a fossil's observed range, FAD to LAD, and is drawn
|
|
9144
|
+
// as one below. On CALENDAR time it is the uncertainty of a
|
|
9145
|
+
// sampling date -- a virus sample dated only to its month or year
|
|
9146
|
+
// -- which is the same kind of thing as an internal node's age
|
|
9147
|
+
// interval and is drawn the same way, slimmer, and only when it
|
|
9148
|
+
// HAS a width: a tip dated to the day states {d,d}, and a capped
|
|
9149
|
+
// tick on every such tip is what made the readers throw tip
|
|
9150
|
+
// intervals away until 2026-09-17 (Christian; the desktop alike).
|
|
9151
|
+
let sampledTip = !d.children && info.type === 'calendar';
|
|
9152
|
+
if (sampledTip && !(max > min)) {
|
|
9153
|
+
return;
|
|
9154
|
+
}
|
|
9031
9155
|
if (d.children) {
|
|
9032
9156
|
g.append('rect').attr('x', left).attr('y', y - 3.5)
|
|
9033
9157
|
.attr('width', w).attr('height', 7)
|
|
9034
9158
|
.attr('fill', HPD_BAR_COLOR);
|
|
9159
|
+
} else if (sampledTip) {
|
|
9160
|
+
g.append('rect').attr('class', 'aptx-sampling-bar')
|
|
9161
|
+
.attr('x', left).attr('y', y - 2.5)
|
|
9162
|
+
.attr('width', w).attr('height', 5)
|
|
9163
|
+
.attr('fill', HPD_BAR_COLOR);
|
|
9035
9164
|
} else {
|
|
9036
9165
|
g.append('rect').attr('x', left).attr('y', y - 2.5)
|
|
9037
9166
|
.attr('width', w).attr('height', 5)
|
|
@@ -9675,6 +9804,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9675
9804
|
// found nodes: A's colour, the hit count, the step-through. The next
|
|
9676
9805
|
// search replaces them, and resetting search A clears them.
|
|
9677
9806
|
function showRepresentativesAsHits(result) {
|
|
9807
|
+
openPanelSection('Search'); // the tips kept are search A's hits: its navigator has to be visible
|
|
9678
9808
|
_foundNodes0 = new Set(result.keptTips);
|
|
9679
9809
|
_searchBox0Empty = false;
|
|
9680
9810
|
_searchHitIndex = -1;
|
|
@@ -9722,14 +9852,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
9722
9852
|
forester.stripShortExtension(parentName), result.tipCount);
|
|
9723
9853
|
copy.description = copy.description ? copy.description + ' ' + provenance : provenance;
|
|
9724
9854
|
|
|
9725
|
-
let display = _state.phylogram ? (_state.alignPhylogram ? 'aligned' : 'phylogram') : 'cladogram';
|
|
9726
|
-
clearCollapsedFlags(_trees[_treeIndex]);
|
|
9727
9855
|
_trees.push(copy);
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9856
|
+
// a tree nobody has opened: switchToTree gives it this tree's
|
|
9857
|
+
// geometry, which is what "drawn as the tree was" amounts to --
|
|
9858
|
+
// the display type (phylogram, aligned or cladogram) and the
|
|
9859
|
+
// layout. The tree it came from is remembered as it stands.
|
|
9860
|
+
switchToTree(_trees.length - 1, null);
|
|
9733
9861
|
}
|
|
9734
9862
|
|
|
9735
9863
|
function escPressed() {
|
|
@@ -10794,6 +10922,120 @@ function (root, d3, forester, phyloXml) {
|
|
|
10794
10922
|
}
|
|
10795
10923
|
}
|
|
10796
10924
|
|
|
10925
|
+
// ============ the panel's sections: what is open, and what fits ========
|
|
10926
|
+
// Three rules, deliberately separate, for hosts with little room to give
|
|
10927
|
+
// (BV-BRC embeds the viewer in a page that already has its own chrome):
|
|
10928
|
+
//
|
|
10929
|
+
// - DEFAULTS. A tree arrives with the sections that DESCRIBE it open --
|
|
10930
|
+
// what it can be coloured by, what it shows -- plus Search, which is
|
|
10931
|
+
// reached for constantly (Christian, 2026-09-16). Folded are the ones
|
|
10932
|
+
// that are adjustments for later: zoom, sizes, the domain controls.
|
|
10933
|
+
// Measured on the 13-property BV-BRC demo in a 700px window: 766px of
|
|
10934
|
+
// panel content becomes 621px, with nothing behind a scroll.
|
|
10935
|
+
// - MEMORY. What the user opens and closes is theirs, and it outlives
|
|
10936
|
+
// the tree: kept for the page and in localStorage, like the light/dark
|
|
10937
|
+
// choice. It is NOT part of a tree's view -- the panel is the user's
|
|
10938
|
+
// furniture, not a property of the tree -- which is why it is not in
|
|
10939
|
+
// getViewState, and why a tree switch no longer reopens everything.
|
|
10940
|
+
// - FIT. Opening a section in a short window folds the section opened
|
|
10941
|
+
// longest ago, and ONLY while the panel would not otherwise fit. On a
|
|
10942
|
+
// tall screen it never fires. It is bounded by PIXELS, not by a count
|
|
10943
|
+
// of open sections: three open sections measure anywhere from 215px to
|
|
10944
|
+
// 562px, so a count would not have bounded the height at all.
|
|
10945
|
+
const PANEL_SECTIONS_KEY = 'aptx-panel-sections';
|
|
10946
|
+
const PANEL_SECTIONS_CLOSED_BY_DEFAULT = ['Zoom', 'Sizes', 'Domain Architectures'];
|
|
10947
|
+
|
|
10948
|
+
function loadPanelSections() {
|
|
10949
|
+
if (_panelSections) {
|
|
10950
|
+
return _panelSections;
|
|
10951
|
+
}
|
|
10952
|
+
_panelSections = {};
|
|
10953
|
+
try {
|
|
10954
|
+
let saved = JSON.parse(localStorage.getItem(PANEL_SECTIONS_KEY));
|
|
10955
|
+
if (saved && typeof saved === 'object') {
|
|
10956
|
+
Object.keys(saved).forEach(function (k) {
|
|
10957
|
+
if (typeof saved[k] === 'boolean') {
|
|
10958
|
+
_panelSections[k] = saved[k];
|
|
10959
|
+
}
|
|
10960
|
+
});
|
|
10961
|
+
}
|
|
10962
|
+
} catch {
|
|
10963
|
+
// no storage (private mode), or junk written by something else:
|
|
10964
|
+
// the defaults are a complete answer on their own
|
|
10965
|
+
}
|
|
10966
|
+
return _panelSections;
|
|
10967
|
+
}
|
|
10968
|
+
|
|
10969
|
+
function savePanelSections() {
|
|
10970
|
+
try {
|
|
10971
|
+
localStorage.setItem(PANEL_SECTIONS_KEY, JSON.stringify(_panelSections || {}));
|
|
10972
|
+
} catch {
|
|
10973
|
+
// the choice just won't survive a reload
|
|
10974
|
+
}
|
|
10975
|
+
}
|
|
10976
|
+
|
|
10977
|
+
// Every collapsible section of a panel, in the order they are drawn.
|
|
10978
|
+
function panelSections(panel) {
|
|
10979
|
+
let out = [];
|
|
10980
|
+
panel.querySelectorAll('fieldset').forEach(function (f) {
|
|
10981
|
+
let lg = f.querySelector(':scope > legend.aptx-legend-toggle');
|
|
10982
|
+
if (lg) {
|
|
10983
|
+
out.push({name: lg.textContent.trim(), fieldset: f});
|
|
10984
|
+
}
|
|
10985
|
+
});
|
|
10986
|
+
return out;
|
|
10987
|
+
}
|
|
10988
|
+
|
|
10989
|
+
function sectionFoldedInitially(name) {
|
|
10990
|
+
let remembered = loadPanelSections()[name];
|
|
10991
|
+
return (typeof remembered === 'boolean')
|
|
10992
|
+
? remembered : PANEL_SECTIONS_CLOSED_BY_DEFAULT.indexOf(name) >= 0;
|
|
10993
|
+
}
|
|
10994
|
+
|
|
10995
|
+
// By how many pixels the panel's body overflows the room it has.
|
|
10996
|
+
function panelOverflow(panel) {
|
|
10997
|
+
let body = panel.querySelector(':scope > .aptx-body');
|
|
10998
|
+
return body ? (body.scrollHeight - body.clientHeight) : 0;
|
|
10999
|
+
}
|
|
11000
|
+
|
|
11001
|
+
// Keeps the panel to one screen: while it overflows, fold the section
|
|
11002
|
+
// opened longest ago. Never the one just opened -- that is the one the
|
|
11003
|
+
// user asked for -- and never the last one standing.
|
|
11004
|
+
function fitPanelSections(panel, justOpened) {
|
|
11005
|
+
let guard = 0;
|
|
11006
|
+
while (panelOverflow(panel) > 0 && ++guard < 20) {
|
|
11007
|
+
let open = panelSections(panel).filter(function (s) {
|
|
11008
|
+
return !s.fieldset.classList.contains('aptx-collapsed') && s.name !== justOpened;
|
|
11009
|
+
});
|
|
11010
|
+
if (open.length === 0) {
|
|
11011
|
+
return;
|
|
11012
|
+
}
|
|
11013
|
+
open.sort(function (a, b) {
|
|
11014
|
+
return (_panelOpenOrder.get(a.name) || 0) - (_panelOpenOrder.get(b.name) || 0);
|
|
11015
|
+
});
|
|
11016
|
+
open[0].fieldset.classList.add('aptx-collapsed');
|
|
11017
|
+
loadPanelSections()[open[0].name] = true;
|
|
11018
|
+
}
|
|
11019
|
+
}
|
|
11020
|
+
|
|
11021
|
+
// Unfolds a section because something the user just did has to be SEEN
|
|
11022
|
+
// inside it: the search box they jumped to, the hits a tool marked.
|
|
11023
|
+
function openPanelSection(name) {
|
|
11024
|
+
let panel = _container && _container.querySelector(':scope > .aptx-panel');
|
|
11025
|
+
if (!panel) {
|
|
11026
|
+
return;
|
|
11027
|
+
}
|
|
11028
|
+
panelSections(panel).forEach(function (s) {
|
|
11029
|
+
if (s.name === name && s.fieldset.classList.contains('aptx-collapsed')) {
|
|
11030
|
+
s.fieldset.classList.remove('aptx-collapsed');
|
|
11031
|
+
loadPanelSections()[name] = false;
|
|
11032
|
+
_panelOpenOrder.set(name, ++_panelOpenSeq);
|
|
11033
|
+
savePanelSections();
|
|
11034
|
+
fitPanelSections(panel, name);
|
|
11035
|
+
}
|
|
11036
|
+
});
|
|
11037
|
+
}
|
|
11038
|
+
|
|
10797
11039
|
// ===================== Control-panel glyphs =====================
|
|
10798
11040
|
// Vector glyphs ported from the desktop Archaeopteryx control panel (its
|
|
10799
11041
|
// DisplayTypeIcon / ControlButtonIcon / LayoutIcon / ThemeToggleIcon /
|
|
@@ -11496,7 +11738,28 @@ function (root, d3, forester, phyloXml) {
|
|
|
11496
11738
|
+ '.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
11739
|
+ '.aptx-panel .aptx-hide-btn:hover { background:var(--p-accent-weak); color:var(--p-accent-ink); border-color:var(--p-accent); }'
|
|
11498
11740
|
+ '.aptx-panel.aptx-hidden > .aptx-body { display:none; }'
|
|
11499
|
-
+ '.aptx-panel.aptx-hidden > .' + PROG_NAME + ' { border-bottom:0; }'
|
|
11741
|
+
+ '.aptx-panel.aptx-hidden > .' + PROG_NAME + ' { border-bottom:0; }'
|
|
11742
|
+
// --- compact density (the panelDensity config key) ---------------
|
|
11743
|
+
// Nothing is removed and nothing is hidden: the same controls,
|
|
11744
|
+
// tighter, for a host with little room to give. The width lives
|
|
11745
|
+
// here as well, and panelWidth() returns the same number so the
|
|
11746
|
+
// tree's left margin follows it (leftPanelClearance).
|
|
11747
|
+
+ '.aptx-panel.aptx-compact { width:' + PANEL_WIDTH_COMPACT + 'px; font-size:10.5px; }'
|
|
11748
|
+
+ '.aptx-panel.aptx-compact fieldset { padding:4px 9px; }'
|
|
11749
|
+
+ '.aptx-panel.aptx-compact legend { margin:0 0 3px; }'
|
|
11750
|
+
+ '.aptx-panel.aptx-compact .' + PROG_NAME + ' { padding:6px 9px; }'
|
|
11751
|
+
// the title is a fixed 12px, which wrapped onto a second line once
|
|
11752
|
+
// the panel narrowed: it comes down with everything else
|
|
11753
|
+
+ '.aptx-panel.aptx-compact .' + PROGNAMELINK + ' { font-size:10.5px; }'
|
|
11754
|
+
+ '.aptx-panel.aptx-compact .aptx-checkgrid { gap:2px 8px; }'
|
|
11755
|
+
+ '.aptx-panel.aptx-compact .aptx-field-label { margin:5px 0 2px; }'
|
|
11756
|
+
+ '.aptx-panel.aptx-compact .aptx-subhead { margin:4px 0 2px; }'
|
|
11757
|
+
+ '.aptx-panel.aptx-compact input[type=range] { margin:1px 0 6px; }'
|
|
11758
|
+
+ '.aptx-panel.aptx-compact .aptx-slider-row { margin:2px 0; }'
|
|
11759
|
+
+ '.aptx-panel.aptx-compact input[type=button], .aptx-panel.aptx-compact .aptx-gbtn { height:22px; margin:1px 2px 1px 0; }'
|
|
11760
|
+
+ '.aptx-panel.aptx-compact .aptx-search-row input[type=text], .aptx-panel.aptx-compact .aptx-search-row input[type=button] { height:24px; }'
|
|
11761
|
+
+ '.aptx-panel.aptx-compact .aptx-search-menus select { height:22px; }'
|
|
11762
|
+
+ '.aptx-panel.aptx-compact .aptx-toolrow { margin-top:3px; }';
|
|
11500
11763
|
let style = document.createElement('style');
|
|
11501
11764
|
style.id = PANEL_STYLE_ID;
|
|
11502
11765
|
style.textContent = css;
|
|
@@ -11585,6 +11848,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11585
11848
|
// Collapsible sections: wrap each titled fieldset's content in a body
|
|
11586
11849
|
// element (so collapsing hides everything under the legend, including
|
|
11587
11850
|
// bare text labels like the slider captions) and toggle it via the legend.
|
|
11851
|
+
let initiallyOpen = [];
|
|
11588
11852
|
let fieldsets = body.querySelectorAll('fieldset');
|
|
11589
11853
|
for (let i = 0; i < fieldsets.length; ++i) {
|
|
11590
11854
|
let fieldset = fieldsets[i];
|
|
@@ -11602,10 +11866,34 @@ function (root, d3, forester, phyloXml) {
|
|
|
11602
11866
|
}
|
|
11603
11867
|
fieldset.appendChild(fsBody);
|
|
11604
11868
|
legend.classList.add('aptx-legend-toggle');
|
|
11869
|
+
let name = legend.textContent.trim();
|
|
11870
|
+
if (sectionFoldedInitially(name)) {
|
|
11871
|
+
fieldset.classList.add('aptx-collapsed');
|
|
11872
|
+
} else {
|
|
11873
|
+
initiallyOpen.push(name);
|
|
11874
|
+
}
|
|
11605
11875
|
legend.addEventListener('click', function () {
|
|
11606
|
-
fieldset.classList.toggle('aptx-collapsed');
|
|
11876
|
+
let opened = !fieldset.classList.toggle('aptx-collapsed');
|
|
11877
|
+
loadPanelSections()[name] = !opened;
|
|
11878
|
+
if (opened) {
|
|
11879
|
+
_panelOpenOrder.set(name, ++_panelOpenSeq);
|
|
11880
|
+
fitPanelSections(panel, name);
|
|
11881
|
+
}
|
|
11882
|
+
savePanelSections();
|
|
11607
11883
|
});
|
|
11608
11884
|
}
|
|
11885
|
+
// What arrived open counts as opened, oldest at the BOTTOM: a panel is
|
|
11886
|
+
// read top-down, so when a short window forces something to fold on
|
|
11887
|
+
// arrival it should be the last section rather than the one describing
|
|
11888
|
+
// the tree at the top. Anything the user opens later is newer than all
|
|
11889
|
+
// of these, and folds after them.
|
|
11890
|
+
for (let i = initiallyOpen.length - 1; i >= 0; --i) {
|
|
11891
|
+
_panelOpenOrder.set(initiallyOpen[i], ++_panelOpenSeq);
|
|
11892
|
+
}
|
|
11893
|
+
// A remembered set of open sections can be taller than this window,
|
|
11894
|
+
// so the fit rule runs once on arrival too -- with no section to
|
|
11895
|
+
// spare, since the user opened none of them just now.
|
|
11896
|
+
fitPanelSections(panel, null);
|
|
11609
11897
|
|
|
11610
11898
|
// Apply the current light/dark choice to this (and every) panel.
|
|
11611
11899
|
applyPanelTheme();
|
|
@@ -12272,6 +12560,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
12272
12560
|
|
|
12273
12561
|
if (c0) {
|
|
12274
12562
|
c0.classList.add('aptx-panel');
|
|
12563
|
+
if (_settings.panelDensity === 'compact') {
|
|
12564
|
+
c0.classList.add('aptx-compact');
|
|
12565
|
+
}
|
|
12275
12566
|
setStyles(c0, {
|
|
12276
12567
|
'position': 'absolute',
|
|
12277
12568
|
'left': CONTROLS_0_LEFT_DEFAULT,
|
|
@@ -12746,9 +13037,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
12746
13037
|
|
|
12747
13038
|
// A file with several trees: previous / picker / next in one row,
|
|
12748
13039
|
// 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.
|
|
13040
|
+
// opens in the same container as you left it (showTree). The end
|
|
13041
|
+
// buttons are disabled at the ends: the panel is rebuilt per tree,
|
|
13042
|
+
// so the state set here is always current.
|
|
12752
13043
|
function makeTreePicker() {
|
|
12753
13044
|
let n = _trees.length;
|
|
12754
13045
|
let row = document.createElement('div');
|