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/archaeopteryx.js CHANGED
@@ -20,7 +20,7 @@
20
20
  *
21
21
  */
22
22
 
23
- // v 3.25.0
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.25.0';
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, then an event colour, then a
1451
- // colourised clade's branch colour. NEVER the found/search colour: a
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
- let style = nodeStyle(d);
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
- console.warn(WARNING + ': initialVisualization "'
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
- console.warn(WARNING + ': initialVisualization "' + _settings.initialVisualization
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
- let style = nodeStyle(phynode);
3680
- if (style && (style.nodeColor || style.fontColor)) {
3681
- return style.nodeColor || style.fontColor;
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 style = nodeStyle(phynode);
3717
- if (style && (style.nodeColor || style.fontColor)) {
3718
- return style.nodeColor || style.fontColor;
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
- if (_state.showVisualizations && currentColorVis()) {
3733
- let color = makeVisLabelColor(phynode);
3734
- if (color) {
3735
- return color;
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
- // an active Color visualization outranks style:font_color, as on the
3739
- // desktop; clear the Color menu to see the tree as its file styled it
3740
- let style = nodeStyle(phynode);
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
- console.warn(WARNING + ': "' + k + '" was replaced by "' + dep.to
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
- console.warn(WARNING + ': "' + k + '" is retired and has no effect: ' + dep.note);
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
- console.warn(MESSAGE + ignored + ' protein domain' + (ignored === 1 ? '' : 's')
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
- console.warn(MESSAGE + gs.ignored + ' sequence location' + (gs.ignored === 1 ? '' : 's')
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
- console.warn(MESSAGE + gs.otherContigs + ' gene' + (gs.otherContigs === 1 ? '' : 's')
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
- console.log(WARNING + ': dynamic sizing is turned on, will ignore displayWidth setting');
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
- console.log(WARNING + ': dynamic sizing is turned on, will ignore displayHeight setting');
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
- archaeopteryx.launch = function (container, phylo, config) {
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
- console.error(ERROR + 'onViewChange threw: ' + (e && e.message ? e.message : e));
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 style = nodeStyle(node);
11354
- if (style && (style.nodeColor || style.fontColor)) {
11355
- return style.nodeColor || style.fontColor;
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
- let bar = forester.scaleBarLength(branchLengthPixelsPerUnit(), SCALE_BAR_TARGET_PX);
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
- function copyText(text) {
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']].forEach(function (row) {
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 two rows with no URL open another dialog in place of
18094
- // this one: the key list, and the control-panel sheet
18095
- let opener = row[0] === 'Control panel' ? showCheatSheetDialog : showShortcutsDialog;
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 + '</button>');
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
- console.warn(WARNING + ': pngExportScale ' + _settings.pngExportScale + ' is not one of '
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
- console.error(ERROR + 'PDF export needs the optional jspdf and svg2pdf.js libraries on the page');
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
- console.error(ERROR + 'PDF export failed: ' + err);
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
- console.error(ERROR + 'PNG export failed: ' + err);
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
- console.warn(WARNING + ': PNG exported at ' + target.scale.toFixed(2) + 'x rather than ' + _pngScale
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
- console.error(ERROR + 'copying the PNG failed: ' + err);
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 (container, fileName, data, config) {
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
  // --------------------------------------------------------------