archaeopteryx 3.25.0 → 3.27.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 +93 -108
- package/archaeopteryx.d.ts +59 -0
- package/archaeopteryx.js +944 -61
- package/forester.js +15 -3
- 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.27.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.27.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';
|
|
@@ -303,6 +303,52 @@ function (root, d3, forester, phyloXml) {
|
|
|
303
303
|
const MESSAGE = 'ArchaeopteryxJS: ';
|
|
304
304
|
const ERROR = 'ArchaeopteryxJS: ERROR: ';
|
|
305
305
|
|
|
306
|
+
// What this launch had to say, kept as well as printed. The console is
|
|
307
|
+
// where a warning goes and where nobody embedding the viewer looks: a
|
|
308
|
+
// site passed a retired key for a month and a forced visualization for
|
|
309
|
+
// longer (BV-BRC, 2026-10-08), each announced on every page load. The
|
|
310
|
+
// Diagnostics report (archaeopteryx.doctor, further down) reads this
|
|
311
|
+
// list; 'problem' entries are somebody's mistake and light the dot on
|
|
312
|
+
// the program name, 'note' entries are only stated. The same line twice
|
|
313
|
+
// is one entry with a count.
|
|
314
|
+
//
|
|
315
|
+
// The list belongs to the tree on view. It starts afresh whenever the
|
|
316
|
+
// launch proper (launchInto) begins for a tree -- a new launch, or
|
|
317
|
+
// showTree() moving to another tree of the file, whose config warnings
|
|
318
|
+
// are then said again and whose per-tree ones are its own. A launch that
|
|
319
|
+
// is REJECTED while an earlier viewer is still on the page gives that
|
|
320
|
+
// viewer its list back and adds one line saying what failed (the shell
|
|
321
|
+
// round launch()): the page still shows the earlier tree, and the report
|
|
322
|
+
// describes the page.
|
|
323
|
+
const DIAGNOSTICS_MAX = 40;
|
|
324
|
+
let _diagnostics = [];
|
|
325
|
+
// What is on view, noted at the moment a launch takes the page over (it
|
|
326
|
+
// clears the container): the report's "launch" section and the dot read
|
|
327
|
+
// this, never the module's working state, which a launch writes to
|
|
328
|
+
// before the checks that can still reject it.
|
|
329
|
+
let _onView = null;
|
|
330
|
+
let _tookOver = false;
|
|
331
|
+
let _recordedErrors = (typeof WeakSet === 'function') ? new WeakSet() : null;
|
|
332
|
+
function recordDiagnostic(level, line) {
|
|
333
|
+
let text = String(line).replace(/^ArchaeopteryxJS: (WARNING: |ERROR: )?/, '');
|
|
334
|
+
let seen = _diagnostics.find(function (d) {
|
|
335
|
+
return d.level === level && d.text === text;
|
|
336
|
+
});
|
|
337
|
+
if (seen) {
|
|
338
|
+
seen.count += 1;
|
|
339
|
+
} else if (_diagnostics.length < DIAGNOSTICS_MAX) {
|
|
340
|
+
_diagnostics.push({level: level, text: text, count: 1});
|
|
341
|
+
}
|
|
342
|
+
if (level === 'problem') {
|
|
343
|
+
syncDoctorDot();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Print as before (same console method, same line) and keep it.
|
|
347
|
+
function say(level, method, line) {
|
|
348
|
+
console[method](line);
|
|
349
|
+
recordDiagnostic(level, line);
|
|
350
|
+
}
|
|
351
|
+
|
|
306
352
|
// Call-time dependency guards (the UMD header explains why they are not
|
|
307
353
|
// load-time): each failure names exactly what is missing.
|
|
308
354
|
function requireD3() {
|
|
@@ -390,6 +436,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
390
436
|
const GENE_LABELS_SELECT = 'gene_labels';
|
|
391
437
|
const SCALE_AXIS_CB = 'scaleaxis_cb';
|
|
392
438
|
const SCALE_GRID_CB = 'scalegrid_cb';
|
|
439
|
+
const SCALE_BAR_CB = 'scalebar_cb';
|
|
393
440
|
const MSA_SCROLL_ID = 'aptxmsascroll';
|
|
394
441
|
const FONT_SIZE_SLIDER = 'fs_sl';
|
|
395
442
|
const EXTERNAL_LABEL_CB = 'extl_cb';
|
|
@@ -439,6 +486,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
439
486
|
const CLOCK_PLOT_BUTTON = 'clock_plot_b';
|
|
440
487
|
const CLOCK_PLOT_DIALOG = 'aptx_clock_plot';
|
|
441
488
|
const ABOUT_DIALOG = 'aptx_about';
|
|
489
|
+
const DOCTOR_DIALOG = 'aptx_doctor';
|
|
442
490
|
const TREE_PROPERTIES_DIALOG = 'aptx_tree_props';
|
|
443
491
|
const PROG_NAME = 'progname';
|
|
444
492
|
const PROGNAMELINK = 'prognamelink';
|
|
@@ -1447,8 +1495,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
1447
1495
|
// a node coloured by Host Group glows in its host colour -- and the UI
|
|
1448
1496
|
// accent otherwise. As on the desktop (TreePanel.hoverGlowColor). The
|
|
1449
1497
|
// precedence is the one the node itself is painted by: the Color-by
|
|
1450
|
-
// value, then a per-node visual style
|
|
1451
|
-
//
|
|
1498
|
+
// value, then a per-node visual style (not while a Color-by is active,
|
|
1499
|
+
// see nodeStyleColor), then an event colour, then a colourised clade's
|
|
1500
|
+
// branch colour. NEVER the found/search colour: a
|
|
1452
1501
|
// focus ring must not read as a selection state.
|
|
1453
1502
|
//
|
|
1454
1503
|
// The glow is a 13-23% alpha wash, so a dark or a pale colour would
|
|
@@ -1462,10 +1511,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1462
1511
|
c = vis;
|
|
1463
1512
|
}
|
|
1464
1513
|
if (!c) {
|
|
1465
|
-
|
|
1466
|
-
if (style && (style.nodeColor || style.fontColor)) {
|
|
1467
|
-
c = style.nodeColor || style.fontColor;
|
|
1468
|
-
}
|
|
1514
|
+
c = nodeStyleColor(d);
|
|
1469
1515
|
}
|
|
1470
1516
|
if (!c && _state.showNodeEvents && d.events && d.children) {
|
|
1471
1517
|
c = makeNodeEventsDependentColor(d.events);
|
|
@@ -1939,7 +1985,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1939
1985
|
|| (c.label && String(c.label).toLowerCase() === want);
|
|
1940
1986
|
}) || null;
|
|
1941
1987
|
if (!requested) {
|
|
1942
|
-
|
|
1988
|
+
say('problem', 'warn', WARNING + ': initialVisualization "'
|
|
1943
1989
|
+ _settings.initialVisualization
|
|
1944
1990
|
+ '" names no visualization of this tree; using the automatic choice');
|
|
1945
1991
|
}
|
|
@@ -1969,7 +2015,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1969
2015
|
? requested.values.length + ' distinct values on this tree, more than the '
|
|
1970
2016
|
+ LEGEND_MAX_ROWS + ' the legend lists'
|
|
1971
2017
|
: 'a value on ' + requested.coverage + ' of the tree\'s ' + requested.total + ' tips';
|
|
1972
|
-
|
|
2018
|
+
say('problem', 'warn', WARNING + ': initialVisualization "' + _settings.initialVisualization
|
|
1973
2019
|
+ '" names a field with ' + why + '; left to itself the viewer would '
|
|
1974
2020
|
+ (auto ? 'open with "' + auto.label + '" (' + auto.values.length + ' values)'
|
|
1975
2021
|
: 'open uncoloured'));
|
|
@@ -2430,6 +2476,36 @@ function (root, d3, forester, phyloXml) {
|
|
|
2430
2476
|
return ((_vis && _vis.colorId) ? _vis.byId[_vis.colorId] : null) || null;
|
|
2431
2477
|
}
|
|
2432
2478
|
|
|
2479
|
+
// A Color visualization is on screen: it owns every node's colour, and the
|
|
2480
|
+
// file's style colours wait until the Color menu is cleared (the desktop's
|
|
2481
|
+
// rule; see makeLabelColor and makeNodeFillColor).
|
|
2482
|
+
function colorVisActive() {
|
|
2483
|
+
return !!_state.showVisualizations && !!currentColorVis();
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
// The colour a node's style gives it, or null: style:node_color (else
|
|
2487
|
+
// style:font_color) for the node's mark, style:font_color for its label.
|
|
2488
|
+
// Null while a Color visualization is active -- the desktop's rule
|
|
2489
|
+
// (TreePanel.setColor): the visualization owns every node's colour and
|
|
2490
|
+
// its legend explains them; a node without a value takes the default ink
|
|
2491
|
+
// and no dot rather than the file's colour. Until 2026-10-08 the valueless
|
|
2492
|
+
// nodes fell through to the file's colour on their dots, so BV-BRC's
|
|
2493
|
+
// trees -- style:font_color on every tip, following Genus -- showed two
|
|
2494
|
+
// palettes under Color by Host (Christian: adopt the desktop's rule).
|
|
2495
|
+
// EVERY reader of a style colour goes through here, so none can forget
|
|
2496
|
+
// the rule: the label, the dot, its outline, the hover glow, the clock
|
|
2497
|
+
// plot's points (which a first version of the rule missed).
|
|
2498
|
+
function nodeStyleColor(node, forLabel) {
|
|
2499
|
+
if (colorVisActive()) {
|
|
2500
|
+
return null;
|
|
2501
|
+
}
|
|
2502
|
+
let style = nodeStyle(node);
|
|
2503
|
+
if (!style) {
|
|
2504
|
+
return null;
|
|
2505
|
+
}
|
|
2506
|
+
return (forLabel ? style.fontColor : (style.nodeColor || style.fontColor)) || null;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2433
2509
|
function currentShapeVis() {
|
|
2434
2510
|
return ((_vis && _vis.shapeId) ? _vis.byId[_vis.shapeId] : null) || null;
|
|
2435
2511
|
}
|
|
@@ -3676,11 +3752,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
3676
3752
|
if (visColor !== _state.backgroundColorDefault) {
|
|
3677
3753
|
return visColor;
|
|
3678
3754
|
}
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
}
|
|
3683
|
-
return visColor;
|
|
3755
|
+
// the style colour, or the background -- which, under an active Color
|
|
3756
|
+
// visualization, drawTreeGeometry turns into no dot at all
|
|
3757
|
+
return nodeStyleColor(phynode) || visColor;
|
|
3684
3758
|
};
|
|
3685
3759
|
|
|
3686
3760
|
// A darker shade of a node's found/selected highlight color, used as a thin
|
|
@@ -3713,10 +3787,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
3713
3787
|
return v;
|
|
3714
3788
|
}
|
|
3715
3789
|
}
|
|
3716
|
-
let
|
|
3717
|
-
if (
|
|
3718
|
-
return
|
|
3790
|
+
let styleColor = nodeStyleColor(phynode);
|
|
3791
|
+
if (styleColor) {
|
|
3792
|
+
return styleColor;
|
|
3719
3793
|
}
|
|
3794
|
+
// the branch colour, unlike a style colour, holds under a Color-by (desktop)
|
|
3720
3795
|
if (_state.useVisualStyles && phynode.color) {
|
|
3721
3796
|
let c = phynode.color;
|
|
3722
3797
|
return "rgb(" + c.red + "," + c.green + "," + c.blue + ")";
|
|
@@ -3729,17 +3804,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
3729
3804
|
if (foundColor) {
|
|
3730
3805
|
return foundColor;
|
|
3731
3806
|
}
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3807
|
+
// an active Color visualization owns the colours: its colour where
|
|
3808
|
+
// the node has a value, the default ink where it has none (and
|
|
3809
|
+
// nodeStyleColor is null meanwhile, see there)
|
|
3810
|
+
if (colorVisActive()) {
|
|
3811
|
+
return makeVisLabelColor(phynode);
|
|
3737
3812
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
if (style && style.fontColor) {
|
|
3742
|
-
return style.fontColor;
|
|
3813
|
+
let fontColor = nodeStyleColor(phynode, true);
|
|
3814
|
+
if (fontColor) {
|
|
3815
|
+
return fontColor;
|
|
3743
3816
|
}
|
|
3744
3817
|
if (_state.useVisualStyles && phynode.color) {
|
|
3745
3818
|
let c = phynode.color;
|
|
@@ -5747,6 +5820,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5747
5820
|
'geneFamily',
|
|
5748
5821
|
'geneLabels',
|
|
5749
5822
|
'showScaleAxis',
|
|
5823
|
+
'showScaleBar',
|
|
5750
5824
|
'showScaleGrid',
|
|
5751
5825
|
'showSupportDots',
|
|
5752
5826
|
'searchAinitialValue',
|
|
@@ -5798,14 +5872,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
5798
5872
|
return;
|
|
5799
5873
|
}
|
|
5800
5874
|
if (dep.to) {
|
|
5801
|
-
|
|
5875
|
+
say('problem', 'warn', WARNING + ': "' + k + '" was replaced by "' + dep.to
|
|
5802
5876
|
+ '": ' + dep.note);
|
|
5803
5877
|
// An explicit new key always wins over the old alias.
|
|
5804
5878
|
if (given[dep.to] === undefined) {
|
|
5805
5879
|
given[dep.to] = dep.map(given[k]);
|
|
5806
5880
|
}
|
|
5807
5881
|
} else {
|
|
5808
|
-
|
|
5882
|
+
say('problem', 'warn', WARNING + ': "' + k + '" is retired and has no effect: ' + dep.note);
|
|
5809
5883
|
}
|
|
5810
5884
|
delete given[k];
|
|
5811
5885
|
});
|
|
@@ -6051,7 +6125,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6051
6125
|
// malformed domains are skipped, never fatal -- said once, here
|
|
6052
6126
|
let ignored = forester.domainArchitectureStats(_treeData).ignored;
|
|
6053
6127
|
if (ignored > 0) {
|
|
6054
|
-
|
|
6128
|
+
say('note', 'warn', MESSAGE + ignored + ' protein domain' + (ignored === 1 ? '' : 's')
|
|
6055
6129
|
+ ' with a missing or impossible from / to / E-value ignored');
|
|
6056
6130
|
}
|
|
6057
6131
|
}
|
|
@@ -6084,11 +6158,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6084
6158
|
// are left out, never fatal -- said once, here
|
|
6085
6159
|
let gs = forester.geneRegionStats(_treeData);
|
|
6086
6160
|
if (gs.ignored > 0) {
|
|
6087
|
-
|
|
6161
|
+
say('note', 'warn', MESSAGE + gs.ignored + ' sequence location' + (gs.ignored === 1 ? '' : 's')
|
|
6088
6162
|
+ ' not in the form contig:start-end(+) ignored');
|
|
6089
6163
|
}
|
|
6090
6164
|
if (gs.otherContigs > 0) {
|
|
6091
|
-
|
|
6165
|
+
say('note', 'warn', MESSAGE + gs.otherContigs + ' gene' + (gs.otherContigs === 1 ? '' : 's')
|
|
6092
6166
|
+ ' on a genome\'s other contigs not drawn: the gene track shows one contig per genome');
|
|
6093
6167
|
}
|
|
6094
6168
|
}
|
|
@@ -6113,6 +6187,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
6113
6187
|
_state.showScaleAxis = !!(_timeInfo && _timeInfo.type) && !branchesShowDivergence();
|
|
6114
6188
|
}
|
|
6115
6189
|
_state.showScaleGrid = _state.showScaleGrid === true; // desktop default: off
|
|
6190
|
+
// the scale bar is on unless the caller says no; it gives way to the axis anyway
|
|
6191
|
+
_state.showScaleBar = _state.showScaleBar !== false;
|
|
6116
6192
|
_state.showSupportDots = _state.showSupportDots === true;
|
|
6117
6193
|
_state.showNodeEvents = _basicTreeProperties.nodeEvents === true;
|
|
6118
6194
|
_state.showBranchEvents = _basicTreeProperties.branchEvents === true;
|
|
@@ -6318,11 +6394,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6318
6394
|
_settings.enableDynamicSizing = true;
|
|
6319
6395
|
}
|
|
6320
6396
|
if (_settings.displayWidth && _settings.enableDynamicSizing === true) {
|
|
6321
|
-
|
|
6397
|
+
say('note', 'log', WARNING + ': dynamic sizing is turned on, will ignore displayWidth setting');
|
|
6322
6398
|
_settings.displayWidth = 0;
|
|
6323
6399
|
}
|
|
6324
6400
|
if (_settings.displayHeight && _settings.enableDynamicSizing === true) {
|
|
6325
|
-
|
|
6401
|
+
say('note', 'log', WARNING + ': dynamic sizing is turned on, will ignore displayHeight setting');
|
|
6326
6402
|
_settings.displayHeight = 0;
|
|
6327
6403
|
}
|
|
6328
6404
|
if ((!_settings.displayWidth) && (!_settings.enableDynamicSizing)) {
|
|
@@ -6611,7 +6687,40 @@ function (root, d3, forester, phyloXml) {
|
|
|
6611
6687
|
|
|
6612
6688
|
// launch(container, tree, config) -> a viewer handle {getSelectedNodes,
|
|
6613
6689
|
// destroy}. Exactly three arguments; config is THE one config object.
|
|
6614
|
-
|
|
6690
|
+
// launch() proper is launchChecked below. This shell keeps what a
|
|
6691
|
+
// rejected launch said: the thrown message reaches whoever called, which
|
|
6692
|
+
// on an embedding site is often a catch block nobody reads, and
|
|
6693
|
+
// archaeopteryx.doctor() can still tell. (launchArchaeopteryx has its own
|
|
6694
|
+
// failures -- a file that does not parse -- and reports them through the
|
|
6695
|
+
// same function; an error is recorded once, whoever catches it first.)
|
|
6696
|
+
function recordLaunchFailure(e) {
|
|
6697
|
+
if (_recordedErrors && e && typeof e === 'object') {
|
|
6698
|
+
if (_recordedErrors.has(e)) {
|
|
6699
|
+
return;
|
|
6700
|
+
}
|
|
6701
|
+
_recordedErrors.add(e);
|
|
6702
|
+
}
|
|
6703
|
+
recordDiagnostic('problem', 'launch failed: '
|
|
6704
|
+
+ String(e && e.message ? e.message : e).replace(/^ArchaeopteryxJS: ERROR: /, ''));
|
|
6705
|
+
}
|
|
6706
|
+
archaeopteryx.launch = function () {
|
|
6707
|
+
let before = _diagnostics;
|
|
6708
|
+
let wasOnView = _onView;
|
|
6709
|
+
_tookOver = false;
|
|
6710
|
+
try {
|
|
6711
|
+
return launchChecked.apply(null, arguments);
|
|
6712
|
+
} catch (e) {
|
|
6713
|
+
if (wasOnView && !_tookOver) {
|
|
6714
|
+
// rejected before it touched the page: the earlier viewer is
|
|
6715
|
+
// still there, and so is everything said about it
|
|
6716
|
+
_diagnostics = before;
|
|
6717
|
+
}
|
|
6718
|
+
recordLaunchFailure(e);
|
|
6719
|
+
throw e;
|
|
6720
|
+
}
|
|
6721
|
+
};
|
|
6722
|
+
|
|
6723
|
+
function launchChecked(container, phylo, config) {
|
|
6615
6724
|
|
|
6616
6725
|
// Bad input is the caller's bug, so it is thrown at the caller. It used
|
|
6617
6726
|
// to pop a browser alert and return, which blocks the whole tab and
|
|
@@ -6686,12 +6795,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
6686
6795
|
let view = config && config.view;
|
|
6687
6796
|
let start = (view && Number.isInteger(view.tree) && view.tree > 0 && view.tree < trees.length) ? view.tree : 0;
|
|
6688
6797
|
return launchInto(container, trees, start, config);
|
|
6689
|
-
}
|
|
6798
|
+
}
|
|
6690
6799
|
|
|
6691
6800
|
// The launch proper, for one tree of the list: launch() validates and
|
|
6692
6801
|
// starts at the first, showTree() re-enters here for another.
|
|
6693
6802
|
function launchInto(container, trees, index, config) {
|
|
6694
6803
|
let phylo = trees[index];
|
|
6804
|
+
// what is said from here on is said about THIS tree (see _diagnostics)
|
|
6805
|
+
_diagnostics = [];
|
|
6695
6806
|
let cfg = readConfig(config);
|
|
6696
6807
|
requireForester();
|
|
6697
6808
|
requireD3();
|
|
@@ -6738,6 +6849,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6738
6849
|
checkLayoutValue(cfg.state ? cfg.state.layout : undefined);
|
|
6739
6850
|
initializeSettings(cfg.settings);
|
|
6740
6851
|
|
|
6852
|
+
// From here the page is this launch's: nothing below is expected to
|
|
6853
|
+
// throw, and what a previous one drew goes now.
|
|
6854
|
+
_tookOver = true;
|
|
6855
|
+
_onView = {config: config, container: containerEl, tips: _basicTreeProperties.externalNodesCount,
|
|
6856
|
+
tree: index + 1, trees: trees.length};
|
|
6741
6857
|
// whatever a previous launch left in this container (or in the
|
|
6742
6858
|
// container before it) goes now, so the old tree does not sit under
|
|
6743
6859
|
// the card while the new one is prepared
|
|
@@ -6948,6 +7064,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6948
7064
|
cfg.view = view || viewForTree(_trees[index]);
|
|
6949
7065
|
let handle = launchInto(_container, _trees, index, cfg);
|
|
6950
7066
|
_launchConfig = launchConfig;
|
|
7067
|
+
// the report shows the config the SITE passed, not this copy with
|
|
7068
|
+
// the tree's own view added
|
|
7069
|
+
if (_onView) {
|
|
7070
|
+
_onView.config = launchConfig;
|
|
7071
|
+
}
|
|
6951
7072
|
return handle;
|
|
6952
7073
|
}
|
|
6953
7074
|
|
|
@@ -10018,6 +10139,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10018
10139
|
if (_basicTreeProperties.branchLengths === true) {
|
|
10019
10140
|
s.scaleAxis = _state.showScaleAxis === true;
|
|
10020
10141
|
s.scaleGrid = _state.showScaleGrid === true;
|
|
10142
|
+
s.scaleBar = _state.showScaleBar === true;
|
|
10021
10143
|
}
|
|
10022
10144
|
if (_branchScaleAvailable) {
|
|
10023
10145
|
s.scale = _state.branchScale; // time or divergence: which layout the branches show
|
|
@@ -10199,6 +10321,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
10199
10321
|
if (typeof grid === 'boolean') {
|
|
10200
10322
|
_state.showScaleGrid = grid;
|
|
10201
10323
|
}
|
|
10324
|
+
if (typeof s.scaleBar === 'boolean') {
|
|
10325
|
+
_state.showScaleBar = s.scaleBar;
|
|
10326
|
+
}
|
|
10202
10327
|
if (!radialDisplay()) {
|
|
10203
10328
|
_radialLabelsHorizontal = false;
|
|
10204
10329
|
} else if ((_state.showDomainArchitectures && _basicTreeProperties.domainArchitectures)
|
|
@@ -10275,6 +10400,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10275
10400
|
syncHeatmapControls();
|
|
10276
10401
|
setCheckboxValue(SCALE_AXIS_CB, _state.showScaleAxis === true);
|
|
10277
10402
|
setCheckboxValue(SCALE_GRID_CB, _state.showScaleGrid === true);
|
|
10403
|
+
setCheckboxValue(SCALE_BAR_CB, _state.showScaleBar === true);
|
|
10278
10404
|
setCheckboxValue(DOMAIN_GLOW_CB, _state.domainGlow === true);
|
|
10279
10405
|
setValue(DOMAIN_LABELS_SELECT, _state.domainLabels);
|
|
10280
10406
|
syncDomainControls();
|
|
@@ -10336,7 +10462,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10336
10462
|
try {
|
|
10337
10463
|
_settings.onViewChange(state, encoded);
|
|
10338
10464
|
} catch (e) {
|
|
10339
|
-
|
|
10465
|
+
say('problem', 'error', ERROR + 'onViewChange threw: ' + (e && e.message ? e.message : e));
|
|
10340
10466
|
}
|
|
10341
10467
|
}
|
|
10342
10468
|
|
|
@@ -10355,7 +10481,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10355
10481
|
const VIEW_INT_KEYS = ['tree', 'subtree', 'rotation', 'domainEvalue'];
|
|
10356
10482
|
const VIEW_NUMBER_KEYS = ['font', 'node', 'branch'];
|
|
10357
10483
|
const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', 'msaLogo', 'heatmap', 'domains', 'domainGlow', 'genes',
|
|
10358
|
-
'scaleAxis', 'scaleGrid', 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
|
|
10484
|
+
'scaleAxis', 'scaleGrid', 'scaleBar', 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
|
|
10359
10485
|
// Lists of strings, comma-joined. A property ref carries ':' but never a
|
|
10360
10486
|
// comma, and encodeViewValue leaves both readable in a hash.
|
|
10361
10487
|
const VIEW_LIST_KEYS = ['heatmapManual', 'labelFields'];
|
|
@@ -11350,9 +11476,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
11350
11476
|
if (vis) {
|
|
11351
11477
|
return vis;
|
|
11352
11478
|
}
|
|
11353
|
-
let
|
|
11354
|
-
if (
|
|
11355
|
-
return
|
|
11479
|
+
let styleColor = nodeStyleColor(node); // null under an active Color-by, as in the tree
|
|
11480
|
+
if (styleColor) {
|
|
11481
|
+
return styleColor;
|
|
11356
11482
|
}
|
|
11357
11483
|
if (_state.useVisualStyles && node.color) {
|
|
11358
11484
|
return 'rgb(' + node.color.red + ',' + node.color.green + ',' + node.color.blue + ')';
|
|
@@ -13501,7 +13627,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
13501
13627
|
const SCALE_BAR_RESERVE = 30;
|
|
13502
13628
|
|
|
13503
13629
|
function scaleBarShown() {
|
|
13504
|
-
return _state.phylogram === true && _basicTreeProperties.branchLengths === true && !scaleAxisShown()
|
|
13630
|
+
return _state.phylogram === true && _basicTreeProperties.branchLengths === true && !scaleAxisShown()
|
|
13631
|
+
&& _state.showScaleBar !== false;
|
|
13505
13632
|
}
|
|
13506
13633
|
|
|
13507
13634
|
// pixels per branch-length unit in the current layout's own coordinates
|
|
@@ -13527,7 +13654,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
13527
13654
|
if (!scaleBarShown()) {
|
|
13528
13655
|
return;
|
|
13529
13656
|
}
|
|
13530
|
-
|
|
13657
|
+
// the unit the file states, as the distance axis prints it; none when it states none
|
|
13658
|
+
let unit = _treeData && typeof _treeData.branch_length_unit === 'string' ? _treeData.branch_length_unit.trim() : '';
|
|
13659
|
+
let bar = forester.scaleBarLength(branchLengthPixelsPerUnit(), SCALE_BAR_TARGET_PX, unit);
|
|
13531
13660
|
if (!bar) {
|
|
13532
13661
|
return;
|
|
13533
13662
|
}
|
|
@@ -14161,6 +14290,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
14161
14290
|
scheduleUpdate(null, 0);
|
|
14162
14291
|
}
|
|
14163
14292
|
|
|
14293
|
+
function scaleBarCbClicked() {
|
|
14294
|
+
_state.showScaleBar = getCheckboxValue(SCALE_BAR_CB);
|
|
14295
|
+
scheduleUpdate(null, 0);
|
|
14296
|
+
}
|
|
14297
|
+
|
|
14164
14298
|
// Fit the circular tree into the viewport, centred. The root is at the
|
|
14165
14299
|
// group origin (0,0), so we place that at the viewport centre and scale so
|
|
14166
14300
|
// the outer label ring fits. Computed from the known radial extent rather
|
|
@@ -15319,6 +15453,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
15319
15453
|
gridCb.disabled = _state.unrootedDisplay === true || _state.phylogram !== true
|
|
15320
15454
|
|| _state.showScaleAxis !== true;
|
|
15321
15455
|
}
|
|
15456
|
+
let barCb = byId(SCALE_BAR_CB);
|
|
15457
|
+
if (barCb) {
|
|
15458
|
+
// a cladogram has nothing to measure, and under the axis the bar
|
|
15459
|
+
// has given way; every layout can carry it otherwise
|
|
15460
|
+
barCb.disabled = _state.phylogram !== true || scaleAxisShown();
|
|
15461
|
+
}
|
|
15322
15462
|
let minus = byId(ZOOM_OUT_X);
|
|
15323
15463
|
let plus = byId(ZOOM_IN_X);
|
|
15324
15464
|
let expandV = byId(ZOOM_TO_EXPAND_Y);
|
|
@@ -15856,14 +15996,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
15856
15996
|
// The async clipboard API where the page allows it (a secure context);
|
|
15857
15997
|
// otherwise the old select-and-copy, so a page served over plain http
|
|
15858
15998
|
// still copies. Resolves to whether the text made it.
|
|
15859
|
-
|
|
15999
|
+
//
|
|
16000
|
+
// `host` is the open modal dialog the copy is made from, if any: under a
|
|
16001
|
+
// modal everything outside it is inert, a textarea on the body included.
|
|
16002
|
+
function copyText(text, host) {
|
|
15860
16003
|
let legacy = function () {
|
|
15861
16004
|
let ta = document.createElement('textarea');
|
|
15862
16005
|
ta.value = text;
|
|
15863
16006
|
ta.setAttribute('readonly', '');
|
|
15864
16007
|
ta.style.position = 'fixed';
|
|
15865
16008
|
ta.style.left = '-10000px';
|
|
15866
|
-
document.body.appendChild(ta);
|
|
16009
|
+
(host || document.body).appendChild(ta);
|
|
15867
16010
|
ta.select();
|
|
15868
16011
|
let ok = false;
|
|
15869
16012
|
try {
|
|
@@ -16769,6 +16912,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
16769
16912
|
+ '.aptx-dialog-close:hover { background:var(--p-accent-weak); color:var(--p-accent-ink);'
|
|
16770
16913
|
+ ' border-color:var(--p-accent); }'
|
|
16771
16914
|
+ '.aptx-dialog-body { padding:10px 13px 12px; overflow:auto; }'
|
|
16915
|
+
// a dialog that scrolls gets the panel's own scrollbar: the
|
|
16916
|
+
// browser's default track is white in the dark theme too
|
|
16917
|
+
+ '.aptx-dialog-body::-webkit-scrollbar { width:9px; height:9px; }'
|
|
16918
|
+
+ '.aptx-dialog-body::-webkit-scrollbar-thumb { background:var(--p-line-strong); border-radius:9px; border:2px solid var(--p-bg); }'
|
|
16919
|
+
+ '.aptx-dialog-body::-webkit-scrollbar-track, .aptx-dialog-body::-webkit-scrollbar-corner { background:transparent; }'
|
|
16772
16920
|
+ '.aptx-cp-sv { position:relative; width:100%; box-sizing:border-box; height:104px; border-radius:7px;'
|
|
16773
16921
|
+ ' border:1px solid var(--p-line-strong); cursor:crosshair; touch-action:none; }'
|
|
16774
16922
|
+ '.aptx-cp-hue { position:relative; width:100%; box-sizing:border-box; height:13px; margin-top:9px;'
|
|
@@ -16926,6 +17074,23 @@ function (root, d3, forester, phyloXml) {
|
|
|
16926
17074
|
+ '.aptx-panel .' + PROGNAMELINK + ' { background:none; padding:0; margin:0; cursor:pointer;'
|
|
16927
17075
|
+ ' font-family:inherit; font-weight:inherit; letter-spacing:inherit; }'
|
|
16928
17076
|
+ '.aptx-panel .' + PROGNAMELINK + ':hover { text-decoration:underline; }'
|
|
17077
|
+
+ '.aptx-doctor-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--p-muted); vertical-align:middle; }'
|
|
17078
|
+
+ '.aptx-doctor-dot[hidden] { display:none; }'
|
|
17079
|
+
+ '.aptx-panel .' + PROGNAMELINK + ' .aptx-doctor-dot { margin-left:6px; }'
|
|
17080
|
+
+ '.aptx-about .aptx-doctor-dot { margin-right:6px; }'
|
|
17081
|
+
+ '.aptx-dialog-body.aptx-doctor { max-height:min(70vh,560px); }'
|
|
17082
|
+
+ '.aptx-doctor .aptx-dialog-key { flex-basis:34%; }'
|
|
17083
|
+
+ '.aptx-doctor-head { display:flex; align-items:center; gap:10px; margin-bottom:6px; }'
|
|
17084
|
+
+ '.aptx-doctor-status { flex:1 1 auto; min-width:0; font-weight:650; }'
|
|
17085
|
+
+ '.aptx-doctor-status.aptx-doctor-bad { box-shadow:inset 3px 0 0 var(--p-warn); padding-left:9px; }'
|
|
17086
|
+
+ '.aptx-doctor-item { position:relative; padding:2px 0 2px 14px; line-height:1.35; overflow-wrap:anywhere; }'
|
|
17087
|
+
+ '.aptx-doctor-item::before { content:""; position:absolute; left:2px; top:0.62em; width:6px; height:6px; border-radius:50%; background:var(--p-faint); }'
|
|
17088
|
+
+ '.aptx-doctor-item.aptx-doctor-bad::before { background:var(--p-warn); }'
|
|
17089
|
+
+ '.aptx-doctor-item.aptx-doctor-note { color:var(--p-muted); }'
|
|
17090
|
+
+ '.aptx-doctor-section { margin:11px 0 3px; font-size:10.5px; font-weight:650; letter-spacing:0.05em; text-transform:uppercase; color:var(--p-muted); }'
|
|
17091
|
+
+ '.aptx-doctor-detail { display:block; color:var(--p-muted); line-height:1.35; }'
|
|
17092
|
+
+ '.aptx-doctor .aptx-dialog-line.aptx-doctor-bad .aptx-doctor-value { color:var(--p-warn); font-weight:600; }'
|
|
17093
|
+
+ '.aptx-doctor-copy { flex:none; }'
|
|
16929
17094
|
+ '.aptx-dialog-body.aptx-about { padding-top:13px; }'
|
|
16930
17095
|
// the label column is narrower here than for node data: these values
|
|
16931
17096
|
// are URLs, and they should not have to wrap mid-word
|
|
@@ -17207,6 +17372,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
17207
17372
|
e.stopPropagation();
|
|
17208
17373
|
showAboutDialog();
|
|
17209
17374
|
});
|
|
17375
|
+
// config warnings are recorded before this header exists
|
|
17376
|
+
syncDoctorDot();
|
|
17377
|
+
// ... and a library can arrive after it (see syncDoctorDot)
|
|
17378
|
+
let panel = header.closest('.aptx-panel');
|
|
17379
|
+
if (panel) {
|
|
17380
|
+
panel.addEventListener('pointerenter', syncDoctorDot);
|
|
17381
|
+
}
|
|
17382
|
+
if (document.readyState !== 'complete') {
|
|
17383
|
+
window.addEventListener('load', syncDoctorDot, {once: true});
|
|
17384
|
+
}
|
|
17210
17385
|
|
|
17211
17386
|
let themeBtn = document.createElement('button');
|
|
17212
17387
|
themeBtn.type = 'button';
|
|
@@ -18040,10 +18215,693 @@ function (root, d3, forester, phyloXml) {
|
|
|
18040
18215
|
shell.dialog.showModal();
|
|
18041
18216
|
}
|
|
18042
18217
|
|
|
18218
|
+
// ===================== Diagnostics =====================
|
|
18219
|
+
// "Dependencies are checked when used, never at load time" makes the
|
|
18220
|
+
// viewer hard to break and easy to run half-equipped without knowing: on
|
|
18221
|
+
// BV-BRC it ran for weeks with no PDF or PNG export, a retired config key
|
|
18222
|
+
// and a forced visualization, and every one of those was found by reading
|
|
18223
|
+
// the site's source (Christian, 2026-10-09: a "check engine light" and a
|
|
18224
|
+
// doctor behind it). This is the one place that says what is loaded, what
|
|
18225
|
+
// that switches off, and what the launch complained about.
|
|
18226
|
+
//
|
|
18227
|
+
// Two rules decide what is a PROBLEM (lights the dot) and what is only
|
|
18228
|
+
// stated:
|
|
18229
|
+
// * an optional library that is simply absent is a site's choice, and
|
|
18230
|
+
// is stated ("PDF export: off"), never flagged;
|
|
18231
|
+
// * something nobody chooses is a problem: half of a pair (jspdf
|
|
18232
|
+
// without svg2pdf.js), a library from the 2.x days under its old
|
|
18233
|
+
// global, a d3 that is not 7, forester.js from another release, a
|
|
18234
|
+
// phyloxml that no longer writes what this release expects, and every
|
|
18235
|
+
// warning or error the launch printed about its config.
|
|
18236
|
+
// Only d3, jsPDF and forester state a version, so everything else is
|
|
18237
|
+
// judged by what it can do.
|
|
18238
|
+
//
|
|
18239
|
+
// Nothing in here may throw: every check runs in its own try, and a check
|
|
18240
|
+
// that fails says so in its own row.
|
|
18241
|
+
|
|
18242
|
+
// phyloxml states no version, so a small tree goes through it and what
|
|
18243
|
+
// comes back is read for what an OLDER release is known to do. Each of
|
|
18244
|
+
// these is a named fault, and each published release before 1.1.7 shows
|
|
18245
|
+
// at least one (measured on 1.0.2, 1.1.3, 1.1.4, 1.1.5 and 1.1.6):
|
|
18246
|
+
// * it drops the domain architecture, or a <phylogeny>'s own property
|
|
18247
|
+
// (before 1.1.5 / 1.1.3): a saved tree LOSES data;
|
|
18248
|
+
// * it writes a style colour as it came, or the style property before
|
|
18249
|
+
// the node's other properties (before 1.1.7): the saved file differs
|
|
18250
|
+
// from the desktop program's.
|
|
18251
|
+
// Whether the output is byte for byte what 1.1.7 writes is looked at too,
|
|
18252
|
+
// but a difference there alone is only a NOTE. package.json admits any
|
|
18253
|
+
// 1.1.x from 1.1.7 up, a release of this viewer cannot be corrected
|
|
18254
|
+
// after it is published, and a newer phyloxml that lays a file out
|
|
18255
|
+
// differently must not light a dot on every page that has it. (A first
|
|
18256
|
+
// version demanded the bytes and called every difference "an older
|
|
18257
|
+
// copy".)
|
|
18258
|
+
const PHYLOXML_PROBE_IN = '<phyloxml xmlns="http://www.phyloxml.org"><phylogeny rooted="true"><clade>'
|
|
18259
|
+
+ '<clade><name>a</name><branch_length>1</branch_length>'
|
|
18260
|
+
+ '<sequence><domain_architecture length="9"><domain from="1" to="5" confidence="0.1">D</domain>'
|
|
18261
|
+
+ '</domain_architecture></sequence>'
|
|
18262
|
+
+ '<property ref="style:font_color" datatype="xsd:token" applies_to="node">#CE1616</property>'
|
|
18263
|
+
+ '<property ref="x:y" datatype="xsd:string" applies_to="node">v</property></clade>'
|
|
18264
|
+
+ '<clade><name>b</name><branch_length>2</branch_length></clade></clade>'
|
|
18265
|
+
+ '<property ref="p:q" datatype="xsd:string" applies_to="phylogeny">w</property></phylogeny></phyloxml>';
|
|
18266
|
+
const PHYLOXML_PROBE_OUT = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
18267
|
+
'<phyloxml xmlns="http://www.phyloxml.org">',
|
|
18268
|
+
'<phylogeny rooted="true" rerootable="true">',
|
|
18269
|
+
' <clade>',
|
|
18270
|
+
' <clade>',
|
|
18271
|
+
' <name>a</name>',
|
|
18272
|
+
' <branch_length>1</branch_length>',
|
|
18273
|
+
' <sequence>',
|
|
18274
|
+
' <domain_architecture length="9">',
|
|
18275
|
+
' <domain from="1" to="5" confidence="0.1">D</domain>',
|
|
18276
|
+
' </domain_architecture>',
|
|
18277
|
+
' </sequence>',
|
|
18278
|
+
' <property ref="x:y" datatype="xsd:string" applies_to="node">v</property>',
|
|
18279
|
+
' <property ref="style:font_color" datatype="xsd:token" applies_to="node">#ce1616</property>',
|
|
18280
|
+
' </clade>',
|
|
18281
|
+
' <clade>',
|
|
18282
|
+
' <name>b</name>',
|
|
18283
|
+
' <branch_length>2</branch_length>',
|
|
18284
|
+
' </clade>',
|
|
18285
|
+
' </clade>',
|
|
18286
|
+
' <property ref="p:q" datatype="xsd:string" applies_to="phylogeny">w</property>',
|
|
18287
|
+
'</phylogeny>',
|
|
18288
|
+
'</phyloxml>'].join('\n');
|
|
18289
|
+
const PHYLOXML_EXPECTED = '1.1.7';
|
|
18290
|
+
const D3_TESTED_MINOR = 9; // package.json: d3 ^7.9.0
|
|
18291
|
+
const JSPDF_TESTED_MAJOR = 4; // package.json: jspdf ^4.2.1
|
|
18292
|
+
|
|
18293
|
+
// The probe's answer for the library object it was asked about: the
|
|
18294
|
+
// dot asks often, and the answer cannot change.
|
|
18295
|
+
// threw / stage reading ('read') or writing ('write') failed, with the message
|
|
18296
|
+
// lost what a saved tree no longer has
|
|
18297
|
+
// old what is written as releases before 1.1.7 wrote it
|
|
18298
|
+
// same the output is byte for byte 1.1.7's
|
|
18299
|
+
let _phyloXmlProbe = null;
|
|
18300
|
+
function probePhyloXml(px) {
|
|
18301
|
+
if (_phyloXmlProbe && _phyloXmlProbe.lib === px) {
|
|
18302
|
+
return _phyloXmlProbe.result;
|
|
18303
|
+
}
|
|
18304
|
+
let result = {threw: null, stage: null, lost: [], old: [], same: false};
|
|
18305
|
+
let message = function (e) {
|
|
18306
|
+
return String(e && e.message ? e.message : e);
|
|
18307
|
+
};
|
|
18308
|
+
let tree = null;
|
|
18309
|
+
try {
|
|
18310
|
+
tree = px.parse(PHYLOXML_PROBE_IN, {trim: true, normalize: true})[0];
|
|
18311
|
+
} catch (e) {
|
|
18312
|
+
result.threw = message(e);
|
|
18313
|
+
result.stage = 'read';
|
|
18314
|
+
}
|
|
18315
|
+
let written = null;
|
|
18316
|
+
if (!result.threw) {
|
|
18317
|
+
try {
|
|
18318
|
+
written = String(px.toPhyloXML(tree, 9));
|
|
18319
|
+
} catch (e) {
|
|
18320
|
+
result.threw = message(e);
|
|
18321
|
+
result.stage = 'write';
|
|
18322
|
+
}
|
|
18323
|
+
}
|
|
18324
|
+
if (written !== null) {
|
|
18325
|
+
result.same = written === PHYLOXML_PROBE_OUT;
|
|
18326
|
+
if (!/<domain from="1" to="5"[^>]*>D<\/domain>/.test(written)) {
|
|
18327
|
+
result.lost.push('protein domains');
|
|
18328
|
+
}
|
|
18329
|
+
if (written.indexOf('ref="p:q"') < 0) {
|
|
18330
|
+
result.lost.push('a tree\'s own properties');
|
|
18331
|
+
}
|
|
18332
|
+
if (written.indexOf('ref="x:y"') < 0) {
|
|
18333
|
+
result.lost.push('node properties');
|
|
18334
|
+
}
|
|
18335
|
+
let style = written.indexOf('ref="style:font_color"');
|
|
18336
|
+
if (style < 0) {
|
|
18337
|
+
result.lost.push('node styles');
|
|
18338
|
+
} else {
|
|
18339
|
+
if (written.indexOf('>#ce1616<') < 0) {
|
|
18340
|
+
result.old.push('a style colour as it came, not in lower case');
|
|
18341
|
+
}
|
|
18342
|
+
if (written.indexOf('ref="x:y"') > style) {
|
|
18343
|
+
result.old.push('a node\'s style before its other properties');
|
|
18344
|
+
}
|
|
18345
|
+
}
|
|
18346
|
+
}
|
|
18347
|
+
_phyloXmlProbe = {lib: px, result: result};
|
|
18348
|
+
return result;
|
|
18349
|
+
}
|
|
18350
|
+
|
|
18351
|
+
// One library's row. `judge` returns {loaded, version, status, detail};
|
|
18352
|
+
// status is 'ok', 'absent' (optional and not there: stated), 'note' or
|
|
18353
|
+
// 'problem'.
|
|
18354
|
+
function doctorLibrary(name, role, judge) {
|
|
18355
|
+
let row = {name: name, role: role, loaded: false, version: null, status: 'problem', detail: ''};
|
|
18356
|
+
try {
|
|
18357
|
+
let j = judge();
|
|
18358
|
+
row.loaded = !!j.loaded;
|
|
18359
|
+
row.version = j.version || null;
|
|
18360
|
+
row.status = j.status;
|
|
18361
|
+
row.detail = j.detail || '';
|
|
18362
|
+
} catch (e) {
|
|
18363
|
+
row.detail = 'this check failed: ' + (e && e.message ? e.message : e);
|
|
18364
|
+
}
|
|
18365
|
+
return row;
|
|
18366
|
+
}
|
|
18367
|
+
|
|
18368
|
+
function doctorLibraries() {
|
|
18369
|
+
let g = root || {};
|
|
18370
|
+
// without a document there is nothing to launch into: the parsers
|
|
18371
|
+
// work with no d3 at all, so its absence there is not a fault
|
|
18372
|
+
let inBrowser = typeof g.document !== 'undefined';
|
|
18373
|
+
let libs = [];
|
|
18374
|
+
|
|
18375
|
+
libs.push(doctorLibrary('forester.js', 'required', function () {
|
|
18376
|
+
let f = forester || g.forester;
|
|
18377
|
+
if (!f) {
|
|
18378
|
+
return {loaded: false, status: 'problem', detail: 'not loaded: nothing can be read or drawn'};
|
|
18379
|
+
}
|
|
18380
|
+
if (f.VERSION === VERSION) {
|
|
18381
|
+
return {loaded: true, version: f.VERSION, status: 'ok'};
|
|
18382
|
+
}
|
|
18383
|
+
return {loaded: true, version: f.VERSION || null, status: 'problem',
|
|
18384
|
+
detail: (f.VERSION ? 'forester.js is ' + f.VERSION : 'forester.js states no version, so it is an older file')
|
|
18385
|
+
+ ', archaeopteryx.js is ' + VERSION + ': the two must come from the same release'};
|
|
18386
|
+
}));
|
|
18387
|
+
|
|
18388
|
+
libs.push(doctorLibrary('d3', 'required', function () {
|
|
18389
|
+
// THE d3 THE VIEWER USES, chosen as requireD3 chooses it: the one
|
|
18390
|
+
// it was handed at load, and the page's global only where that
|
|
18391
|
+
// one is missing or unusable. (A first version looked at the
|
|
18392
|
+
// global first and could report on a d3 nothing was drawn with.)
|
|
18393
|
+
let d = (g.d3 && (!d3 || typeof d3.zoom !== 'function')) ? g.d3 : d3;
|
|
18394
|
+
if (!d) {
|
|
18395
|
+
return inBrowser
|
|
18396
|
+
? {loaded: false, status: 'problem', detail: 'not loaded: launch() fails without d3 version 7'}
|
|
18397
|
+
: {loaded: false, status: 'absent', detail: 'not loaded: only launch() needs it'};
|
|
18398
|
+
}
|
|
18399
|
+
let usable = typeof d.zoom === 'function' && typeof d.cluster === 'function';
|
|
18400
|
+
let version = typeof d.version === 'string' ? d.version : null;
|
|
18401
|
+
if (version === null) {
|
|
18402
|
+
// d3's ES-module build exports no `version`: a bundler's
|
|
18403
|
+
// `import * as d3`, jsdelivr's +esm, Node's require('d3'). It
|
|
18404
|
+
// is judged by what only d3 7 has -- InternMap arrived with
|
|
18405
|
+
// d3-array 3, which is d3 7.0.0 -- and never called outdated
|
|
18406
|
+
// for not saying its number.
|
|
18407
|
+
if (usable && typeof d.InternMap === 'function') {
|
|
18408
|
+
return {loaded: true, status: 'ok', detail: 'states no version (an ES-module build); has what d3 7 has'};
|
|
18409
|
+
}
|
|
18410
|
+
return {loaded: true, status: inBrowser ? 'problem' : 'absent',
|
|
18411
|
+
detail: inBrowser ? 'states no version and lacks what d3 7 has: Archaeopteryx.js needs d3 version 7'
|
|
18412
|
+
: 'not usable here as d3 version 7: only launch() needs it'};
|
|
18413
|
+
}
|
|
18414
|
+
let parts = version.split('.').map(Number);
|
|
18415
|
+
if (parts[0] !== 7 || !usable) {
|
|
18416
|
+
return {loaded: true, version: version, status: inBrowser ? 'problem' : 'absent',
|
|
18417
|
+
detail: inBrowser ? 'Archaeopteryx.js needs d3 version 7'
|
|
18418
|
+
: 'not usable here as d3 version 7: only launch() needs it'};
|
|
18419
|
+
}
|
|
18420
|
+
if (parts[1] < D3_TESTED_MINOR) {
|
|
18421
|
+
return {loaded: true, version: version, status: 'note',
|
|
18422
|
+
detail: 'older than the 7.' + D3_TESTED_MINOR + ' this release is tested with'};
|
|
18423
|
+
}
|
|
18424
|
+
return {loaded: true, version: version, status: 'ok'};
|
|
18425
|
+
}));
|
|
18426
|
+
|
|
18427
|
+
let px = null;
|
|
18428
|
+
let probe = null;
|
|
18429
|
+
libs.push(doctorLibrary('phyloxml', 'required', function () {
|
|
18430
|
+
px = phyloXml || g.phyloXml;
|
|
18431
|
+
probe = px ? probePhyloXml(px) : null;
|
|
18432
|
+
if (!px) {
|
|
18433
|
+
return {loaded: false, status: 'problem',
|
|
18434
|
+
detail: 'not loaded: phyloXML cannot be read, and the phyloXML download fails'};
|
|
18435
|
+
}
|
|
18436
|
+
if (probe.threw) {
|
|
18437
|
+
// on a page phyloxml takes sax from the global, and a missing
|
|
18438
|
+
// one is the usual reason reading fails; anywhere else, and
|
|
18439
|
+
// for any other failure, the message itself is the finding
|
|
18440
|
+
return {loaded: true, status: 'problem', detail: (probe.stage === 'read' && inBrowser && !g.sax)
|
|
18441
|
+
? 'cannot read phyloXML: sax.js is not loaded'
|
|
18442
|
+
: (probe.stage === 'read' ? 'reading' : 'writing') + ' a small tree failed: ' + probe.threw};
|
|
18443
|
+
}
|
|
18444
|
+
if (probe.lost.length > 0) {
|
|
18445
|
+
return {loaded: true, status: 'problem', detail: 'an older copy: saving a tree as phyloXML drops '
|
|
18446
|
+
+ probe.lost.join(', ') + ' (phyloxml ' + PHYLOXML_EXPECTED + ' keeps them)'};
|
|
18447
|
+
}
|
|
18448
|
+
if (probe.old.length > 0) {
|
|
18449
|
+
return {loaded: true, status: 'problem', detail: 'an older copy: it writes ' + probe.old.join(', and ')
|
|
18450
|
+
+ ', so a saved tree differs from the desktop program\'s file (phyloxml ' + PHYLOXML_EXPECTED
|
|
18451
|
+
+ ' does not)'};
|
|
18452
|
+
}
|
|
18453
|
+
if (!probe.same) {
|
|
18454
|
+
return {loaded: true, status: 'note', detail: 'writes a small tree differently from phyloxml '
|
|
18455
|
+
+ PHYLOXML_EXPECTED + ', losing nothing: a newer release, or an altered copy'};
|
|
18456
|
+
}
|
|
18457
|
+
return {loaded: true, status: 'ok', detail: 'writes phyloXML as ' + PHYLOXML_EXPECTED + ' does'};
|
|
18458
|
+
}));
|
|
18459
|
+
libs.push(doctorLibrary('sax', 'required', function () {
|
|
18460
|
+
// A page hands sax over as a global. Anywhere else (Node, a
|
|
18461
|
+
// bundler) phyloxml gets it by require() when it loads, so a
|
|
18462
|
+
// phyloxml that loaded has it, and a tree it read proves it.
|
|
18463
|
+
if (g.sax || (probe && probe.stage !== 'read')) {
|
|
18464
|
+
return {loaded: true, status: 'ok'};
|
|
18465
|
+
}
|
|
18466
|
+
if (!inBrowser && px) {
|
|
18467
|
+
return {loaded: true, status: 'ok', detail: 'taken by phyloxml when it loaded'};
|
|
18468
|
+
}
|
|
18469
|
+
return {loaded: false, status: 'problem', detail: 'not loaded: phyloxml needs it to read phyloXML'};
|
|
18470
|
+
}));
|
|
18471
|
+
|
|
18472
|
+
libs.push(doctorLibrary('canvg', 'optional', function () {
|
|
18473
|
+
if (g.Canvg && typeof g.Canvg.fromString === 'function') {
|
|
18474
|
+
return {loaded: true, status: 'ok'};
|
|
18475
|
+
}
|
|
18476
|
+
if (g.Canvg) {
|
|
18477
|
+
return {loaded: true, status: 'problem',
|
|
18478
|
+
detail: 'window.Canvg has no fromString: not canvg 4, so PNG export is off'};
|
|
18479
|
+
}
|
|
18480
|
+
if (g.canvg) {
|
|
18481
|
+
return {loaded: true, status: 'problem', detail: 'loaded under its old global "canvg" (canvg 1 to 3);'
|
|
18482
|
+
+ ' PNG export needs canvg 4 as window.Canvg, so it is off'};
|
|
18483
|
+
}
|
|
18484
|
+
return {loaded: false, status: 'absent', detail: 'not loaded: PNG export and Copy PNG are off'};
|
|
18485
|
+
}));
|
|
18486
|
+
|
|
18487
|
+
// The pair is read inside each row's own guard: whatever a page has
|
|
18488
|
+
// put under these names, a lookup that throws costs its row and no
|
|
18489
|
+
// other.
|
|
18490
|
+
let pdfPair = function () {
|
|
18491
|
+
let J = g.jspdf && g.jspdf.jsPDF;
|
|
18492
|
+
return {J: J, attached: !!(J && J.API && J.API.svg), svg2pdf: !!g.svg2pdf};
|
|
18493
|
+
};
|
|
18494
|
+
libs.push(doctorLibrary('jspdf', 'optional', function () {
|
|
18495
|
+
let pair = pdfPair();
|
|
18496
|
+
let J = pair.J;
|
|
18497
|
+
let attached = pair.attached;
|
|
18498
|
+
if (J) {
|
|
18499
|
+
let version = typeof J.version === 'string' ? J.version : null;
|
|
18500
|
+
if (!attached && !g.svg2pdf) {
|
|
18501
|
+
return {loaded: true, version: version, status: 'problem',
|
|
18502
|
+
detail: 'loaded without svg2pdf.js: PDF export needs both, so it is off'};
|
|
18503
|
+
}
|
|
18504
|
+
if (version && Number(version.split('.')[0]) !== JSPDF_TESTED_MAJOR) {
|
|
18505
|
+
return {loaded: true, version: version, status: 'note',
|
|
18506
|
+
detail: 'this release is tested with jspdf ' + JSPDF_TESTED_MAJOR};
|
|
18507
|
+
}
|
|
18508
|
+
return {loaded: true, version: version, status: 'ok'};
|
|
18509
|
+
}
|
|
18510
|
+
if (g.jsPDF) {
|
|
18511
|
+
return {loaded: true, status: 'problem', detail: 'loaded under its old global "jsPDF" (jsPDF 1);'
|
|
18512
|
+
+ ' PDF export needs jspdf ' + JSPDF_TESTED_MAJOR + ' as window.jspdf, so it is off'};
|
|
18513
|
+
}
|
|
18514
|
+
return {loaded: false, status: 'absent', detail: 'not loaded: PDF export is off'};
|
|
18515
|
+
}));
|
|
18516
|
+
libs.push(doctorLibrary('svg2pdf.js', 'optional', function () {
|
|
18517
|
+
let pair = pdfPair();
|
|
18518
|
+
let J = pair.J;
|
|
18519
|
+
let attached = pair.attached;
|
|
18520
|
+
if (attached) {
|
|
18521
|
+
return {loaded: true, status: 'ok'};
|
|
18522
|
+
}
|
|
18523
|
+
if (g.svg2pdf && J) {
|
|
18524
|
+
return {loaded: true, status: 'problem', detail: 'loaded, but not attached to jspdf (jsPDF.API.svg'
|
|
18525
|
+
+ ' is missing): load jspdf first. PDF export is off'};
|
|
18526
|
+
}
|
|
18527
|
+
if (g.svg2pdf) {
|
|
18528
|
+
return {loaded: true, status: 'problem',
|
|
18529
|
+
detail: 'loaded without jspdf: PDF export needs both, so it is off'};
|
|
18530
|
+
}
|
|
18531
|
+
return {loaded: false, status: 'absent',
|
|
18532
|
+
detail: J ? 'not loaded' : 'not loaded: PDF export is off'};
|
|
18533
|
+
}));
|
|
18534
|
+
return libs;
|
|
18535
|
+
}
|
|
18536
|
+
|
|
18537
|
+
// What the user gets or does not get, each with the reason.
|
|
18538
|
+
function doctorFunctions(libs) {
|
|
18539
|
+
let lib = function (name) {
|
|
18540
|
+
return libs.find(function (l) {
|
|
18541
|
+
return l.name === name;
|
|
18542
|
+
});
|
|
18543
|
+
};
|
|
18544
|
+
let inBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
18545
|
+
let fn = function (name, test, whyNot) {
|
|
18546
|
+
let available = false;
|
|
18547
|
+
let detail = '';
|
|
18548
|
+
try {
|
|
18549
|
+
available = inBrowser && !!test();
|
|
18550
|
+
detail = available ? '' : (inBrowser ? whyNot() : 'needs a browser');
|
|
18551
|
+
} catch (e) {
|
|
18552
|
+
detail = 'this check failed: ' + (e && e.message ? e.message : e);
|
|
18553
|
+
}
|
|
18554
|
+
return {name: name, available: available, detail: detail};
|
|
18555
|
+
};
|
|
18556
|
+
// why a function is off, in the library's name: its row's reason
|
|
18557
|
+
// reads "not loaded: ..." under its own name and needs the subject here
|
|
18558
|
+
let why = function (l) {
|
|
18559
|
+
return l.status === 'absent' ? l.name + ' is not loaded' : l.name + ': ' + l.detail;
|
|
18560
|
+
};
|
|
18561
|
+
let out = [];
|
|
18562
|
+
try {
|
|
18563
|
+
// ON whenever phyloxml reads and writes at all -- which is what
|
|
18564
|
+
// the Download menu's phyloXML entry does, and it is always
|
|
18565
|
+
// offered. An older copy still works: its row above says what a
|
|
18566
|
+
// saved file loses, and this row repeats it without turning off.
|
|
18567
|
+
let px = lib('phyloxml');
|
|
18568
|
+
let works = px.loaded && !/(cannot read phyloXML|a small tree failed|this check failed)/.test(px.detail);
|
|
18569
|
+
out.push({name: 'Read and save phyloXML', available: works,
|
|
18570
|
+
detail: px.status === 'ok' ? '' : (works ? px.detail : why(px))});
|
|
18571
|
+
} catch (e) {
|
|
18572
|
+
out.push({name: 'Read and save phyloXML', available: false,
|
|
18573
|
+
detail: 'this check failed: ' + (e && e.message ? e.message : e)});
|
|
18574
|
+
}
|
|
18575
|
+
out.push(fn('PNG export', pngExportAvailable, function () {
|
|
18576
|
+
return why(lib('canvg'));
|
|
18577
|
+
}));
|
|
18578
|
+
out.push(fn('Copy PNG', pngCopyAvailable, function () {
|
|
18579
|
+
if (!pngExportAvailable()) {
|
|
18580
|
+
return why(lib('canvg'));
|
|
18581
|
+
}
|
|
18582
|
+
return window.isSecureContext ? 'this browser cannot write an image to the clipboard'
|
|
18583
|
+
: 'needs a secure page (https, or localhost)';
|
|
18584
|
+
}));
|
|
18585
|
+
out.push(fn('PDF export', pdfExportAvailable, function () {
|
|
18586
|
+
let pair = [lib('jspdf'), lib('svg2pdf.js')];
|
|
18587
|
+
let bad = pair.find(function (l) {
|
|
18588
|
+
return l.status === 'problem';
|
|
18589
|
+
});
|
|
18590
|
+
if (bad) {
|
|
18591
|
+
return why(bad);
|
|
18592
|
+
}
|
|
18593
|
+
return pair.every(function (l) {
|
|
18594
|
+
return l.status === 'absent';
|
|
18595
|
+
}) ? 'jspdf and svg2pdf.js are not loaded' : why(pair.find(function (l) {
|
|
18596
|
+
return l.status !== 'ok';
|
|
18597
|
+
}) || pair[0]);
|
|
18598
|
+
}));
|
|
18599
|
+
return out;
|
|
18600
|
+
}
|
|
18601
|
+
|
|
18602
|
+
// A config value as one short word or number: enough to see what a site
|
|
18603
|
+
// passed, never the value itself where that could be a tree or a callback.
|
|
18604
|
+
function doctorConfigValue(v) {
|
|
18605
|
+
if (v === null || typeof v === 'boolean' || typeof v === 'number') {
|
|
18606
|
+
return v;
|
|
18607
|
+
}
|
|
18608
|
+
if (typeof v === 'string') {
|
|
18609
|
+
return v.length > 80 ? v.substring(0, 77) + '...' : v;
|
|
18610
|
+
}
|
|
18611
|
+
if (typeof v === 'function') {
|
|
18612
|
+
return '(function)';
|
|
18613
|
+
}
|
|
18614
|
+
if (Array.isArray(v)) {
|
|
18615
|
+
return '(list of ' + v.length + ')';
|
|
18616
|
+
}
|
|
18617
|
+
return '(' + typeof v + ')';
|
|
18618
|
+
}
|
|
18619
|
+
|
|
18620
|
+
// The tree on view, as noted when its launch took the page over
|
|
18621
|
+
// (_onView): null until one has, so a first launch that was rejected
|
|
18622
|
+
// describes no tree. The tip count is the one the launch computed.
|
|
18623
|
+
function doctorLaunch() {
|
|
18624
|
+
if (!_onView) {
|
|
18625
|
+
return null;
|
|
18626
|
+
}
|
|
18627
|
+
let launch = {config: {}, tips: null, tree: _onView.tree, trees: _onView.trees, container: null};
|
|
18628
|
+
try {
|
|
18629
|
+
Object.keys(_onView.config || {}).sort().forEach(function (k) {
|
|
18630
|
+
launch.config[k] = doctorConfigValue(_onView.config[k]);
|
|
18631
|
+
});
|
|
18632
|
+
} catch {
|
|
18633
|
+
launch.config = {};
|
|
18634
|
+
}
|
|
18635
|
+
launch.tips = (typeof _onView.tips === 'number') ? _onView.tips : null;
|
|
18636
|
+
try {
|
|
18637
|
+
launch.container = {width: _onView.container.clientWidth, height: _onView.container.clientHeight};
|
|
18638
|
+
} catch {
|
|
18639
|
+
launch.container = null;
|
|
18640
|
+
}
|
|
18641
|
+
return launch;
|
|
18642
|
+
}
|
|
18643
|
+
|
|
18644
|
+
function doctorText(rep) {
|
|
18645
|
+
let pad = function (text, n) {
|
|
18646
|
+
text = String(text);
|
|
18647
|
+
return text + ' '.repeat(Math.max(1, n - text.length));
|
|
18648
|
+
};
|
|
18649
|
+
let lines = [NAME + ' ' + rep.version + ' diagnostics', ''];
|
|
18650
|
+
lines.push('Status: ' + rep.summary);
|
|
18651
|
+
rep.problems.forEach(function (p) {
|
|
18652
|
+
lines.push(' ! ' + p);
|
|
18653
|
+
});
|
|
18654
|
+
lines.push('', 'Libraries');
|
|
18655
|
+
rep.libraries.forEach(function (l) {
|
|
18656
|
+
lines.push(' ' + pad(l.name, 13) + pad(l.version || (l.loaded ? 'loaded' : 'not loaded'), 12)
|
|
18657
|
+
+ pad(l.status === 'absent' ? l.role : l.status, 9) + l.detail);
|
|
18658
|
+
});
|
|
18659
|
+
lines.push('', 'Functions');
|
|
18660
|
+
rep.functions.forEach(function (f) {
|
|
18661
|
+
lines.push(' ' + pad(f.name, 24) + (f.available ? 'on' : 'off') + (f.detail ? ' ' + f.detail : ''));
|
|
18662
|
+
});
|
|
18663
|
+
if (rep.notes.length > 0) {
|
|
18664
|
+
lines.push('', 'Notes');
|
|
18665
|
+
rep.notes.forEach(function (n) {
|
|
18666
|
+
lines.push(' - ' + n);
|
|
18667
|
+
});
|
|
18668
|
+
}
|
|
18669
|
+
lines.push('', 'Launch');
|
|
18670
|
+
if (rep.launch) {
|
|
18671
|
+
lines.push(' config: ' + JSON.stringify(rep.launch.config));
|
|
18672
|
+
if (rep.launch.tips !== null) {
|
|
18673
|
+
lines.push(' tree: ' + rep.launch.tips + ' tips' + (rep.launch.trees > 1
|
|
18674
|
+
? ' (tree ' + rep.launch.tree + ' of ' + rep.launch.trees + ')' : ''));
|
|
18675
|
+
}
|
|
18676
|
+
if (rep.launch.container) {
|
|
18677
|
+
lines.push(' container: ' + rep.launch.container.width + ' x ' + rep.launch.container.height + ' px');
|
|
18678
|
+
}
|
|
18679
|
+
} else {
|
|
18680
|
+
lines.push(' no tree is on view');
|
|
18681
|
+
}
|
|
18682
|
+
lines.push('', 'Environment');
|
|
18683
|
+
lines.push(' ' + (rep.environment.userAgent || 'no browser'));
|
|
18684
|
+
if (rep.environment.secureContext !== null) {
|
|
18685
|
+
lines.push(' secure page: ' + (rep.environment.secureContext ? 'yes' : 'no'));
|
|
18686
|
+
}
|
|
18687
|
+
return lines.join('\n');
|
|
18688
|
+
}
|
|
18689
|
+
|
|
18690
|
+
// The problems and the notes: the libraries' and what the launch said.
|
|
18691
|
+
// This is all the dot needs, so it is kept apart from the report, which
|
|
18692
|
+
// also works out the functions, the launch section and the text.
|
|
18693
|
+
function doctorFindings() {
|
|
18694
|
+
let libs;
|
|
18695
|
+
try {
|
|
18696
|
+
libs = doctorLibraries();
|
|
18697
|
+
} catch (e) {
|
|
18698
|
+
libs = [{name: 'libraries', role: 'required', loaded: false, version: null, status: 'problem',
|
|
18699
|
+
detail: 'the checks failed: ' + (e && e.message ? e.message : e)}];
|
|
18700
|
+
}
|
|
18701
|
+
let problems = [];
|
|
18702
|
+
let notes = [];
|
|
18703
|
+
libs.forEach(function (l) {
|
|
18704
|
+
if (l.status === 'problem') {
|
|
18705
|
+
problems.push(l.name + ': ' + l.detail);
|
|
18706
|
+
} else if (l.status === 'note') {
|
|
18707
|
+
notes.push(l.name + (l.version ? ' ' + l.version : '') + ': ' + l.detail);
|
|
18708
|
+
}
|
|
18709
|
+
});
|
|
18710
|
+
_diagnostics.forEach(function (d) {
|
|
18711
|
+
(d.level === 'problem' ? problems : notes).push(d.text + (d.count > 1 ? ' (' + d.count + ' times)' : ''));
|
|
18712
|
+
});
|
|
18713
|
+
return {libraries: libs, problems: problems, notes: notes,
|
|
18714
|
+
summary: problems.length === 0 ? 'nothing to report'
|
|
18715
|
+
: problems.length + (problems.length === 1 ? ' thing' : ' things') + ' to check'};
|
|
18716
|
+
}
|
|
18717
|
+
|
|
18718
|
+
function doctorReport() {
|
|
18719
|
+
let found = doctorFindings();
|
|
18720
|
+
let libs = found.libraries;
|
|
18721
|
+
let problems = found.problems;
|
|
18722
|
+
let notes = found.notes;
|
|
18723
|
+
let functions;
|
|
18724
|
+
try {
|
|
18725
|
+
functions = doctorFunctions(libs);
|
|
18726
|
+
} catch {
|
|
18727
|
+
functions = [];
|
|
18728
|
+
}
|
|
18729
|
+
let launch = null;
|
|
18730
|
+
try {
|
|
18731
|
+
launch = doctorLaunch();
|
|
18732
|
+
} catch {
|
|
18733
|
+
launch = null;
|
|
18734
|
+
}
|
|
18735
|
+
let nav = (typeof navigator !== 'undefined') ? navigator : null;
|
|
18736
|
+
let rep = {
|
|
18737
|
+
program: NAME,
|
|
18738
|
+
version: VERSION,
|
|
18739
|
+
ok: problems.length === 0,
|
|
18740
|
+
summary: found.summary,
|
|
18741
|
+
problems: problems,
|
|
18742
|
+
notes: notes,
|
|
18743
|
+
libraries: libs,
|
|
18744
|
+
functions: functions,
|
|
18745
|
+
launch: launch,
|
|
18746
|
+
environment: {
|
|
18747
|
+
userAgent: nav && nav.userAgent ? nav.userAgent : null,
|
|
18748
|
+
secureContext: (typeof window !== 'undefined' && typeof window.isSecureContext === 'boolean')
|
|
18749
|
+
? window.isSecureContext : null
|
|
18750
|
+
}
|
|
18751
|
+
};
|
|
18752
|
+
try {
|
|
18753
|
+
rep.text = doctorText(rep);
|
|
18754
|
+
} catch (e) {
|
|
18755
|
+
rep.text = NAME + ' ' + VERSION + ' diagnostics: the report could not be written ('
|
|
18756
|
+
+ (e && e.message ? e.message : e) + ')';
|
|
18757
|
+
}
|
|
18758
|
+
return rep;
|
|
18759
|
+
}
|
|
18760
|
+
|
|
18761
|
+
/**
|
|
18762
|
+
* The state of the program on this page: which libraries are loaded (and
|
|
18763
|
+
* their versions, where a library states one), which functions that
|
|
18764
|
+
* switches off, and what the last launch() warned about. Works before a
|
|
18765
|
+
* launch and after a failed one, and in Node. Never throws.
|
|
18766
|
+
*
|
|
18767
|
+
* @returns {{program: string, version: string, ok: boolean, summary: string,
|
|
18768
|
+
* problems: string[], notes: string[], libraries: Object[], functions: Object[],
|
|
18769
|
+
* launch: Object|null, environment: Object, text: string}} `ok` is false when
|
|
18770
|
+
* something is wrong that nobody chooses (see README "Diagnostics");
|
|
18771
|
+
* `text` is the whole report as plain text, for pasting into an issue.
|
|
18772
|
+
*/
|
|
18773
|
+
archaeopteryx.doctor = function () {
|
|
18774
|
+
let rep = doctorReport();
|
|
18775
|
+
syncDoctorDot(); // whoever asks sees a dot that agrees with the answer
|
|
18776
|
+
return rep;
|
|
18777
|
+
};
|
|
18778
|
+
|
|
18779
|
+
// The "check engine light": a small dot after the program name in the
|
|
18780
|
+
// panel header, there only while the report has a problem. Neutral, not
|
|
18781
|
+
// a warning colour: the people who see it are mostly the site's users,
|
|
18782
|
+
// who can do nothing about it, and it is for the one who can.
|
|
18783
|
+
//
|
|
18784
|
+
// What a page has loaded can change under a viewer that is already up (a
|
|
18785
|
+
// library arriving late, a script replacing a global) and nothing
|
|
18786
|
+
// announces it, so the dot is brought up to date whenever it could be
|
|
18787
|
+
// looked at or asked about: when a problem is recorded, when the header
|
|
18788
|
+
// is built, when the page finishes loading, when the pointer enters the
|
|
18789
|
+
// panel, when the About box opens and when doctor() is called.
|
|
18790
|
+
function syncDoctorDot() {
|
|
18791
|
+
try {
|
|
18792
|
+
let host = _onView && _onView.container;
|
|
18793
|
+
if (!host || typeof host.querySelector !== 'function') {
|
|
18794
|
+
return;
|
|
18795
|
+
}
|
|
18796
|
+
let button = host.querySelector('.aptx-panel .' + PROGNAMELINK);
|
|
18797
|
+
let dot = button && button.querySelector('.aptx-doctor-dot');
|
|
18798
|
+
if (!dot) {
|
|
18799
|
+
return;
|
|
18800
|
+
}
|
|
18801
|
+
let found = doctorFindings();
|
|
18802
|
+
let ok = found.problems.length === 0;
|
|
18803
|
+
dot.hidden = ok;
|
|
18804
|
+
button.title = 'About ' + NAME + (ok ? '' : ': ' + found.summary);
|
|
18805
|
+
} catch {
|
|
18806
|
+
// a light that cannot be lit must not take the viewer with it
|
|
18807
|
+
}
|
|
18808
|
+
}
|
|
18809
|
+
|
|
18810
|
+
function showDoctorDialog() {
|
|
18811
|
+
let rep = doctorReport();
|
|
18812
|
+
let shell = makeDialogShell(DOCTOR_DIALOG, 'Diagnostics', 500);
|
|
18813
|
+
shell.body.classList.add('aptx-doctor');
|
|
18814
|
+
|
|
18815
|
+
// The summary and Copy report share the first line: the report is
|
|
18816
|
+
// longer than a small window, and the one button must not be the
|
|
18817
|
+
// thing scrolled out of sight.
|
|
18818
|
+
let head = document.createElement('div');
|
|
18819
|
+
head.className = 'aptx-doctor-head';
|
|
18820
|
+
let status = document.createElement('div');
|
|
18821
|
+
status.className = 'aptx-doctor-status' + (rep.ok ? '' : ' aptx-doctor-bad');
|
|
18822
|
+
status.textContent = NAME + ' ' + rep.version + ': ' + rep.summary;
|
|
18823
|
+
let copy = document.createElement('button');
|
|
18824
|
+
copy.type = 'button';
|
|
18825
|
+
copy.className = 'aptx-reps-button aptx-doctor-copy';
|
|
18826
|
+
copy.textContent = 'Copy report';
|
|
18827
|
+
copy.title = 'copy this report as text, with the launch configuration and the browser added,'
|
|
18828
|
+
+ ' to paste into an issue or an e-mail';
|
|
18829
|
+
copy.addEventListener('click', function () {
|
|
18830
|
+
copyText(rep.text, shell.dialog).then(function (ok) {
|
|
18831
|
+
flashButton(copy, ok ? 'Copied \u2713' : 'Copy failed');
|
|
18832
|
+
});
|
|
18833
|
+
});
|
|
18834
|
+
head.appendChild(status);
|
|
18835
|
+
head.appendChild(copy);
|
|
18836
|
+
shell.body.appendChild(head);
|
|
18837
|
+
|
|
18838
|
+
let list = function (items, cls) {
|
|
18839
|
+
items.forEach(function (text) {
|
|
18840
|
+
let item = document.createElement('div');
|
|
18841
|
+
item.className = 'aptx-doctor-item ' + cls;
|
|
18842
|
+
item.textContent = text; // may quote a config value: never markup
|
|
18843
|
+
shell.body.appendChild(item);
|
|
18844
|
+
});
|
|
18845
|
+
};
|
|
18846
|
+
let section = function (title) {
|
|
18847
|
+
let h = document.createElement('div');
|
|
18848
|
+
h.className = 'aptx-doctor-section';
|
|
18849
|
+
h.textContent = title;
|
|
18850
|
+
shell.body.appendChild(h);
|
|
18851
|
+
};
|
|
18852
|
+
let row = function (key, value, detail, bad) {
|
|
18853
|
+
let line = document.createElement('div');
|
|
18854
|
+
line.className = 'aptx-dialog-line' + (bad ? ' aptx-doctor-bad' : '');
|
|
18855
|
+
let k = document.createElement('span');
|
|
18856
|
+
k.className = 'aptx-dialog-key';
|
|
18857
|
+
k.textContent = key;
|
|
18858
|
+
let v = document.createElement('span');
|
|
18859
|
+
v.className = 'aptx-dialog-val';
|
|
18860
|
+
let main = document.createElement('span');
|
|
18861
|
+
main.className = 'aptx-doctor-value';
|
|
18862
|
+
main.textContent = value;
|
|
18863
|
+
v.appendChild(main);
|
|
18864
|
+
if (detail) {
|
|
18865
|
+
let d = document.createElement('span');
|
|
18866
|
+
d.className = 'aptx-doctor-detail';
|
|
18867
|
+
d.textContent = detail;
|
|
18868
|
+
v.appendChild(d);
|
|
18869
|
+
}
|
|
18870
|
+
line.appendChild(k);
|
|
18871
|
+
line.appendChild(v);
|
|
18872
|
+
shell.body.appendChild(line);
|
|
18873
|
+
};
|
|
18874
|
+
list(rep.problems, 'aptx-doctor-bad');
|
|
18875
|
+
|
|
18876
|
+
section('Libraries');
|
|
18877
|
+
rep.libraries.forEach(function (l) {
|
|
18878
|
+
// "not loaded" is the value: a reason that opens with the same
|
|
18879
|
+
// two words and a colon does not say them again. (Only that
|
|
18880
|
+
// exact opening: "loaded, but not attached..." keeps its start.)
|
|
18881
|
+
let value = l.version || (l.loaded ? 'loaded' : 'not loaded');
|
|
18882
|
+
let detail = l.detail === value ? ''
|
|
18883
|
+
: (l.detail.indexOf('not loaded: ') === 0 && value === 'not loaded' ? l.detail.substring(12) : l.detail);
|
|
18884
|
+
row(l.name, value, detail, l.status === 'problem');
|
|
18885
|
+
});
|
|
18886
|
+
section('Functions');
|
|
18887
|
+
rep.functions.forEach(function (f) {
|
|
18888
|
+
row(f.name, f.available ? 'on' : 'off', f.detail, false);
|
|
18889
|
+
});
|
|
18890
|
+
if (rep.notes.length > 0) {
|
|
18891
|
+
section('Notes');
|
|
18892
|
+
list(rep.notes, 'aptx-doctor-note');
|
|
18893
|
+
}
|
|
18894
|
+
|
|
18895
|
+
shell.dialog.showModal();
|
|
18896
|
+
}
|
|
18897
|
+
|
|
18043
18898
|
// The About box behind the panel's title. Everything here is fixed text and
|
|
18044
18899
|
// fixed URLs -- no tree data reaches it -- so the links are built as real
|
|
18045
|
-
// elements rather than markup.
|
|
18900
|
+
// elements rather than markup. (The Diagnostics row's text is the doctor's
|
|
18901
|
+
// own summary: a count, never a value.)
|
|
18046
18902
|
function showAboutDialog() {
|
|
18903
|
+
let rep = doctorReport();
|
|
18904
|
+
syncDoctorDot();
|
|
18047
18905
|
let shell = makeDialogShell(ABOUT_DIALOG, 'About', 380);
|
|
18048
18906
|
shell.body.classList.add('aptx-about');
|
|
18049
18907
|
|
|
@@ -18075,8 +18933,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
18075
18933
|
['Desktop version', DESKTOP_WEBSITE, 'cmzmasek.github.io/archaeopteryx'],
|
|
18076
18934
|
['Source code', SOURCE_WEBSITE, 'github.com/cmzmasek/archaeopteryx-js'],
|
|
18077
18935
|
['License', LICENSE_WEBSITE, LICENSE_NAME],
|
|
18078
|
-
['Keyboard', null, 'shortcuts (' + (IS_MAC ? '\u2318 /' : 'Ctrl+/') + ')'],
|
|
18079
|
-
['Control panel', null, 'what each control does']
|
|
18936
|
+
['Keyboard', null, 'shortcuts (' + (IS_MAC ? '\u2318 /' : 'Ctrl+/') + ')', showShortcutsDialog],
|
|
18937
|
+
['Control panel', null, 'what each control does', showCheatSheetDialog],
|
|
18938
|
+
['Diagnostics', null, rep.summary, showDoctorDialog]].forEach(function (row) {
|
|
18080
18939
|
let line = document.createElement('div');
|
|
18081
18940
|
line.className = 'aptx-dialog-line';
|
|
18082
18941
|
let key = document.createElement('span');
|
|
@@ -18090,9 +18949,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
18090
18949
|
a.target = '_blank';
|
|
18091
18950
|
a.rel = 'noopener noreferrer';
|
|
18092
18951
|
} else {
|
|
18093
|
-
// the
|
|
18094
|
-
//
|
|
18095
|
-
let opener = row[
|
|
18952
|
+
// the rows with no URL open another dialog in place of this
|
|
18953
|
+
// one: the key list, the control-panel sheet, the diagnostics
|
|
18954
|
+
let opener = row[3];
|
|
18096
18955
|
a.href = '#';
|
|
18097
18956
|
a.addEventListener('click', function (e) {
|
|
18098
18957
|
e.preventDefault();
|
|
@@ -18101,6 +18960,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
18101
18960
|
});
|
|
18102
18961
|
}
|
|
18103
18962
|
a.textContent = row[2];
|
|
18963
|
+
if (row[0] === 'Diagnostics') {
|
|
18964
|
+
a.className = 'aptx-doctor-link';
|
|
18965
|
+
if (!rep.ok) {
|
|
18966
|
+
let dot = document.createElement('span');
|
|
18967
|
+
dot.className = 'aptx-doctor-dot';
|
|
18968
|
+
val.appendChild(dot);
|
|
18969
|
+
}
|
|
18970
|
+
}
|
|
18104
18971
|
val.appendChild(a);
|
|
18105
18972
|
line.appendChild(key);
|
|
18106
18973
|
line.appendChild(val);
|
|
@@ -18407,6 +19274,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
18407
19274
|
on(GENE_ANCHOR_SELECT, 'change', geneAnchorChanged);
|
|
18408
19275
|
on(GENE_FAMILY_SELECT, 'change', geneFamilyChanged);
|
|
18409
19276
|
on(GENE_LABELS_SELECT, 'change', geneLabelsChanged);
|
|
19277
|
+
on(SCALE_BAR_CB, 'click', scaleBarCbClicked);
|
|
18410
19278
|
on(SCALE_AXIS_CB, 'click', scaleAxisCbClicked);
|
|
18411
19279
|
on(SCALE_GRID_CB, 'click', scaleGridCbClicked);
|
|
18412
19280
|
|
|
@@ -18671,7 +19539,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
18671
19539
|
// by this, and a version is a value -- it belongs in the About box
|
|
18672
19540
|
// it opens, not in a row that says what the button is
|
|
18673
19541
|
h = h.concat('<button type="button" class="' + PROGNAMELINK + '" aria-label="' + NAME
|
|
18674
|
-
+ '" title="About ' + NAME + '">' + NAME + ' ' + VERSION
|
|
19542
|
+
+ '" title="About ' + NAME + '">' + NAME + ' ' + VERSION
|
|
19543
|
+
+ '<span class="aptx-doctor-dot" hidden></span></button>');
|
|
18675
19544
|
h = h.concat('</div>');
|
|
18676
19545
|
return h;
|
|
18677
19546
|
}
|
|
@@ -18944,6 +19813,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
18944
19813
|
let timeAxis = _timeInfo && _timeInfo.type
|
|
18945
19814
|
? (_timeInfo.type === 'geologic' ? 'the geologic (ICS) time axis' : 'the calendar time axis')
|
|
18946
19815
|
: null;
|
|
19816
|
+
// "Scale", the desktop's label for the same switch (its Settings > Overlays > Scale & Grid)
|
|
19817
|
+
opts.push(makeCheckboxItem('Scale', SCALE_BAR_CB, 'to show/hide the scale bar at the bottom left of a'
|
|
19818
|
+
+ ' phylogram: a round length in the tree\'s branch-length unit (it gives way while the scale axis is on)'));
|
|
18947
19819
|
opts.push(makeCheckboxItem('Scale Axis', SCALE_AXIS_CB, 'to show/hide '
|
|
18948
19820
|
+ (timeAxis ? timeAxis + ' and node-age bars (a distance axis while the branches show divergence)'
|
|
18949
19821
|
: 'a labeled distance axis from the root to the deepest tip')
|
|
@@ -19345,7 +20217,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
19345
20217
|
// 2x, 4x and 8x, and the setting is now the one it starts on
|
|
19346
20218
|
// (Christian, 2026-09-28) -- said, not done silently
|
|
19347
20219
|
if (_pngScale !== _settings.pngExportScale) {
|
|
19348
|
-
|
|
20220
|
+
say('problem', 'warn', WARNING + ': pngExportScale ' + _settings.pngExportScale + ' is not one of '
|
|
19349
20221
|
+ DOWNLOAD_PNG_SCALES.join(', ') + ': the PNG starts at ' + _pngScale + 'x');
|
|
19350
20222
|
}
|
|
19351
20223
|
setRadioButtonValue(_downloadFullSize ? DOWNLOAD_FULL_SIZE_BUTTON : DOWNLOAD_AS_SHOWN_BUTTON, true);
|
|
@@ -19359,6 +20231,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
19359
20231
|
syncHeatmapControls();
|
|
19360
20232
|
setCheckboxValue(SCALE_AXIS_CB, _state.showScaleAxis);
|
|
19361
20233
|
setCheckboxValue(SCALE_GRID_CB, _state.showScaleGrid);
|
|
20234
|
+
setCheckboxValue(SCALE_BAR_CB, _state.showScaleBar);
|
|
19362
20235
|
setCheckboxValue(SHORTEN_NODE_NAME_CB, _state.shortenNodeNames);
|
|
19363
20236
|
populateVisualizationMenus();
|
|
19364
20237
|
initializeSearchOptions();
|
|
@@ -20086,7 +20959,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20086
20959
|
|
|
20087
20960
|
function downloadAsPdf(graphic) {
|
|
20088
20961
|
if (!pdfExportAvailable()) {
|
|
20089
|
-
|
|
20962
|
+
say('note', 'error', ERROR + 'PDF export needs the optional jspdf and svg2pdf.js libraries on the page');
|
|
20090
20963
|
return;
|
|
20091
20964
|
}
|
|
20092
20965
|
let el = new DOMParser().parseFromString(graphic.text, 'image/svg+xml').documentElement;
|
|
@@ -20135,7 +21008,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20135
21008
|
holder.remove();
|
|
20136
21009
|
}, function (err) {
|
|
20137
21010
|
holder.remove();
|
|
20138
|
-
|
|
21011
|
+
say('note', 'error', ERROR + 'PDF export failed: ' + err);
|
|
20139
21012
|
});
|
|
20140
21013
|
}
|
|
20141
21014
|
|
|
@@ -20181,7 +21054,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20181
21054
|
renderPng(graphic).then(function (blob) {
|
|
20182
21055
|
saveAs(blob, downloadFileName(PNG_SUFFIX));
|
|
20183
21056
|
}, function (err) {
|
|
20184
|
-
|
|
21057
|
+
say('note', 'error', ERROR + 'PNG export failed: ' + err);
|
|
20185
21058
|
});
|
|
20186
21059
|
}
|
|
20187
21060
|
|
|
@@ -20199,7 +21072,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20199
21072
|
return Promise.reject(new Error('this browser cannot paint a canvas of that size'));
|
|
20200
21073
|
}
|
|
20201
21074
|
if (target.scale < _pngScale) {
|
|
20202
|
-
|
|
21075
|
+
say('note', 'warn', WARNING + ': PNG exported at ' + target.scale.toFixed(2) + 'x rather than ' + _pngScale
|
|
20203
21076
|
+ 'x: the largest image this browser can paint');
|
|
20204
21077
|
}
|
|
20205
21078
|
let canvas = target.canvas;
|
|
@@ -20260,7 +21133,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20260
21133
|
written.then(function () {
|
|
20261
21134
|
flashButton(btn, 'Copied ✓');
|
|
20262
21135
|
}, function (err) {
|
|
20263
|
-
|
|
21136
|
+
say('note', 'error', ERROR + 'copying the PNG failed: ' + err);
|
|
20264
21137
|
flashButton(btn, 'Copy failed');
|
|
20265
21138
|
});
|
|
20266
21139
|
}
|
|
@@ -20427,7 +21300,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
20427
21300
|
return undefined;
|
|
20428
21301
|
}
|
|
20429
21302
|
|
|
20430
|
-
archaeopteryx.launchArchaeopteryx = function (
|
|
21303
|
+
archaeopteryx.launchArchaeopteryx = function () {
|
|
21304
|
+
try {
|
|
21305
|
+
return parseAndLaunch.apply(null, arguments);
|
|
21306
|
+
} catch (e) {
|
|
21307
|
+
// its own failures -- a fifth argument, a file that does not
|
|
21308
|
+
// parse -- happen before launch() and its shell are reached
|
|
21309
|
+
recordLaunchFailure(e);
|
|
21310
|
+
throw e;
|
|
21311
|
+
}
|
|
21312
|
+
};
|
|
21313
|
+
function parseAndLaunch(container, fileName, data, config) {
|
|
20431
21314
|
if (arguments.length > 4) {
|
|
20432
21315
|
throw new Error(ERROR + 'launchArchaeopteryx() takes exactly (container, fileName,'
|
|
20433
21316
|
+ ' data, config); the old trailing arguments were removed. The separate settings'
|
|
@@ -20455,7 +21338,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20455
21338
|
// added nothing but a prefix, and swallowing them left the caller with
|
|
20456
21339
|
// a blank page and no way to find out why.
|
|
20457
21340
|
return archaeopteryx.launch(container, trees, config);
|
|
20458
|
-
}
|
|
21341
|
+
}
|
|
20459
21342
|
|
|
20460
21343
|
|
|
20461
21344
|
// --------------------------------------------------------------
|