archaeopteryx 3.26.0 → 3.28.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 +151 -99
- package/archaeopteryx.d.ts +56 -0
- package/archaeopteryx.js +1319 -182
- package/forester.js +8 -1
- 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.28.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.28.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';
|
|
@@ -272,6 +272,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
272
272
|
const PHYLOGRAM_MIN_BRANCH_FRACTION = 0.5;
|
|
273
273
|
const SHORTEN_NAME_MAX_LENGTH = 18;
|
|
274
274
|
const PANEL_STYLE_ID = 'aptx-panel-styles';
|
|
275
|
+
// The tree's own <svg>: named so that the stylesheet can say what a host
|
|
276
|
+
// page's rules for a bare "svg" would otherwise decide (hostProofCss).
|
|
277
|
+
const TREE_SVG_CLASS = 'aptx-tree';
|
|
275
278
|
// How wide a legend row is treated as, for grabbing it with the mouse.
|
|
276
279
|
const PANEL_WIDTH = 214; // fixed control-panel width; shared by the .aptx-panel CSS and leftPanelClearance() so the two can't drift
|
|
277
280
|
const PANEL_WIDTH_COMPACT = 196; // the same, at panelDensity 'compact'; panelWidth() picks between them
|
|
@@ -303,6 +306,52 @@ function (root, d3, forester, phyloXml) {
|
|
|
303
306
|
const MESSAGE = 'ArchaeopteryxJS: ';
|
|
304
307
|
const ERROR = 'ArchaeopteryxJS: ERROR: ';
|
|
305
308
|
|
|
309
|
+
// What this launch had to say, kept as well as printed. The console is
|
|
310
|
+
// where a warning goes and where nobody embedding the viewer looks: a
|
|
311
|
+
// site passed a retired key for a month and a forced visualization for
|
|
312
|
+
// longer (BV-BRC, 2026-10-08), each announced on every page load. The
|
|
313
|
+
// Diagnostics report (archaeopteryx.doctor, further down) reads this
|
|
314
|
+
// list; 'problem' entries are somebody's mistake and light the dot on
|
|
315
|
+
// the program name, 'note' entries are only stated. The same line twice
|
|
316
|
+
// is one entry with a count.
|
|
317
|
+
//
|
|
318
|
+
// The list belongs to the tree on view. It starts afresh whenever the
|
|
319
|
+
// launch proper (launchInto) begins for a tree -- a new launch, or
|
|
320
|
+
// showTree() moving to another tree of the file, whose config warnings
|
|
321
|
+
// are then said again and whose per-tree ones are its own. A launch that
|
|
322
|
+
// is REJECTED while an earlier viewer is still on the page gives that
|
|
323
|
+
// viewer its list back and adds one line saying what failed (the shell
|
|
324
|
+
// round launch()): the page still shows the earlier tree, and the report
|
|
325
|
+
// describes the page.
|
|
326
|
+
const DIAGNOSTICS_MAX = 40;
|
|
327
|
+
let _diagnostics = [];
|
|
328
|
+
// What is on view, noted at the moment a launch takes the page over (it
|
|
329
|
+
// clears the container): the report's "launch" section and the dot read
|
|
330
|
+
// this, never the module's working state, which a launch writes to
|
|
331
|
+
// before the checks that can still reject it.
|
|
332
|
+
let _onView = null;
|
|
333
|
+
let _tookOver = false;
|
|
334
|
+
let _recordedErrors = (typeof WeakSet === 'function') ? new WeakSet() : null;
|
|
335
|
+
function recordDiagnostic(level, line) {
|
|
336
|
+
let text = String(line).replace(/^ArchaeopteryxJS: (WARNING: |ERROR: )?/, '');
|
|
337
|
+
let seen = _diagnostics.find(function (d) {
|
|
338
|
+
return d.level === level && d.text === text;
|
|
339
|
+
});
|
|
340
|
+
if (seen) {
|
|
341
|
+
seen.count += 1;
|
|
342
|
+
} else if (_diagnostics.length < DIAGNOSTICS_MAX) {
|
|
343
|
+
_diagnostics.push({level: level, text: text, count: 1});
|
|
344
|
+
}
|
|
345
|
+
if (level === 'problem') {
|
|
346
|
+
syncDoctorDot();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Print as before (same console method, same line) and keep it.
|
|
350
|
+
function say(level, method, line) {
|
|
351
|
+
console[method](line);
|
|
352
|
+
recordDiagnostic(level, line);
|
|
353
|
+
}
|
|
354
|
+
|
|
306
355
|
// Call-time dependency guards (the UMD header explains why they are not
|
|
307
356
|
// load-time): each failure names exactly what is missing.
|
|
308
357
|
function requireD3() {
|
|
@@ -408,6 +457,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
408
457
|
// archaeopteryx-anime.svg), inlined so the library stays a single file.
|
|
409
458
|
// Gradient ids are prefixed: they were generic enough to collide with an
|
|
410
459
|
// embedding page's own defs.
|
|
460
|
+
// (drawn with attributes, as any svg file is; paintInline moves its paint into styles where it is used)
|
|
411
461
|
const ARCHAEOPTERYX_LOGO_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" role="img" aria-label="Archaeopteryx"> <defs> <linearGradient id="aptxlogo-wg" x1="0" y1="1" x2="1" y2="0"> <stop offset="0" stop-color="#4f46e5"/><stop offset="0.55" stop-color="#38bdf8"/><stop offset="1" stop-color="#a7f3d0"/> </linearGradient> <linearGradient id="aptxlogo-wg2" x1="0" y1="1" x2="1" y2="0"> <stop offset="0" stop-color="#3730a3"/><stop offset="1" stop-color="#4f79e0"/> </linearGradient> <linearGradient id="aptxlogo-tg" x1="0" y1="1" x2="1" y2="0"> <stop offset="0" stop-color="#f43f5e"/><stop offset="0.5" stop-color="#fb923c"/><stop offset="1" stop-color="#fde047"/> </linearGradient> <linearGradient id="aptxlogo-bg" x1="0" y1="0" x2="0" y2="1"> <stop offset="0" stop-color="#fff7e0"/><stop offset="1" stop-color="#fcd34d"/> </linearGradient> </defs> <g transform="translate(28.9,18.0) scale(0.9091)"> <g stroke="#25304f" stroke-width="6" stroke-linejoin="round" stroke-linecap="round"> <path d="M 78.0,152.0 C 49.3,150.6 12.5,173.3 -4.4,194.3 C 25.1,192.0 66.1,175.5 78.0,152.0 Z" fill="url(#aptxlogo-tg)"/> <path d="M 78.0,152.0 C 48.1,143.8 3.5,158.8 -19.7,176.6 C 12.1,181.2 59.5,173.7 78.0,152.0 Z" fill="url(#aptxlogo-tg)"/> <path d="M 78.0,152.0 C 51.8,138.2 6.9,142.5 -18.7,154.0 C 10.0,164.8 56.0,167.9 78.0,152.0 Z" fill="url(#aptxlogo-tg)"/> <path d="M 78.0,152.0 C 58.9,135.3 20.6,130.3 -2.8,135.1 C 18.9,149.7 56.6,161.2 78.0,152.0 Z" fill="url(#aptxlogo-tg)"/> <path d="M 118.0,126.0 C 110.4,106.2 84.4,93.0 65.5,92.1 C 75.9,110.5 99.0,129.6 118.0,126.0 Z" fill="url(#aptxlogo-wg2)"/> <path d="M 118.0,126.0 C 115.6,103.3 92.6,79.7 73.5,71.3 C 79.1,93.8 97.5,122.0 118.0,126.0 Z" fill="url(#aptxlogo-wg2)"/> <path d="M 118.0,126.0 C 123.0,106.1 110.4,79.8 96.5,67.4 C 94.4,87.9 101.6,117.0 118.0,126.0 Z" fill="url(#aptxlogo-wg2)"/> <path d="M 106,172 L 101,202 M 132,172 L 129,202" fill="none" stroke-width="9"/> <path d="M 101,202 L 88,209 M 101,202 L 112,210 M 101,202 L 99,213 M 129,202 L 116,209 M 129,202 L 140,210 M 129,202 L 128,213" fill="none" stroke-width="6"/> <path d="M 96,176 C 74,168 66,146 76,128 C 86,110 110,102 132,106 C 152,110 164,126 162,146 C 160,166 142,180 120,180 C 112,180 103,179 96,176 Z" fill="url(#aptxlogo-bg)"/> <path d="M 100,174 C 86,166 82,150 90,138 C 96,146 108,152 122,152 C 136,152 148,148 156,140 C 160,156 150,172 128,177 C 118,179 108,178 100,174 Z" fill="#fff6dd" stroke="none"/> <path d="M 96,176 C 74,168 66,146 76,128 C 86,110 110,102 132,106 C 152,110 164,126 162,146 C 160,166 142,180 120,180 C 112,180 103,179 96,176 Z" fill="none"/> <path d="M 150.0,60.0 C 159.5,51.2 158.7,35.2 152.9,25.9 C 145.3,35.7 141.5,51.8 150.0,60.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 164.0,56.0 C 174.4,51.0 177.8,37.3 175.2,28.0 C 166.2,34.2 158.9,46.9 164.0,56.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 136.0,66.0 C 141.8,56.5 137.0,44.2 129.6,38.5 C 125.7,48.3 126.7,61.9 136.0,66.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 158,58 C 186,54 208,72 208,96 C 208,120 188,136 164,134 C 140,132 126,114 128,92 C 130,72 142,60 158,58 Z" fill="url(#aptxlogo-bg)"/> <path d="M 203,82 C 216,84 230,92 238,100 C 230,108 216,113 202,112 C 206,104 206,90 203,82 Z" fill="#fb923c"/> <path d="M 204,101 C 214,102 224,103 234,101" fill="none" stroke-width="4"/> <path d="M 128.0,116.0 C 113.1,91.9 75.3,80.3 50.0,83.2 C 68.3,104.8 103.7,124.6 128.0,116.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 128.0,116.0 C 118.0,85.8 79.7,60.2 51.1,54.1 C 65.4,83.1 98.9,116.3 128.0,116.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 128.0,116.0 C 127.0,83.7 97.3,47.3 71.5,32.7 C 76.9,65.0 99.8,107.4 128.0,116.0 Z" fill="url(#aptxlogo-wg)"/> <path d="M 128.0,116.0 C 136.1,88.7 121.1,50.1 103.3,30.9 C 99.0,59.5 106.6,101.2 128.0,116.0 Z" fill="url(#aptxlogo-wg)"/> </g> <g> <ellipse cx="172" cy="94" rx="15" ry="17" fill="#25304f"/> <ellipse cx="172" cy="97" rx="10" ry="11" fill="#38bdf8"/> <circle cx="167" cy="88" r="6" fill="#ffffff"/> <circle cx="178" cy="103" r="3" fill="#ffffff" opacity="0.9"/> <ellipse cx="146" cy="112" rx="9" ry="5" fill="#fb7185" opacity="0.75"/> <path d="M 52,53 C 53.62,59.75 54.25,60.38 61,62 C 54.25,63.62 53.62,64.25 52,71 C 50.38,64.25 49.75,63.62 43,62 C 49.75,60.38 50.38,59.75 52,53 Z" fill="#fde68a"/> <path d="M 222,161 C 223.26,166.25 223.75,166.74 229,168 C 223.75,169.26 223.26,169.75 222,175 C 220.74,169.75 220.25,169.26 215,168 C 220.25,166.74 220.74,166.25 222,161 Z" fill="#fde68a"/> <path d="M 86,208 C 87.08,212.5 87.5,212.92 92,214 C 87.5,215.08 87.08,215.5 86,220 C 84.92,215.5 84.5,215.08 80,214 C 84.5,212.92 84.92,212.5 86,208 Z" fill="#fde68a"/> </g> </g> </svg>';
|
|
412
462
|
const NODE_DATA = 'node_data_dialog';
|
|
413
463
|
const COLOR_PICKER = 'color_picker_dialog';
|
|
@@ -440,6 +490,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
440
490
|
const CLOCK_PLOT_BUTTON = 'clock_plot_b';
|
|
441
491
|
const CLOCK_PLOT_DIALOG = 'aptx_clock_plot';
|
|
442
492
|
const ABOUT_DIALOG = 'aptx_about';
|
|
493
|
+
const DOCTOR_DIALOG = 'aptx_doctor';
|
|
443
494
|
const TREE_PROPERTIES_DIALOG = 'aptx_tree_props';
|
|
444
495
|
const PROG_NAME = 'progname';
|
|
445
496
|
const PROGNAMELINK = 'prognamelink';
|
|
@@ -1491,9 +1542,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
1491
1542
|
let fill = hoverGlowColor(d);
|
|
1492
1543
|
HOVER_GLOW_RADII.forEach(function (radius, i) {
|
|
1493
1544
|
g.append('circle')
|
|
1494
|
-
.
|
|
1495
|
-
.
|
|
1496
|
-
.
|
|
1545
|
+
.style('fill-opacity', HOVER_GLOW_ALPHAS[i])
|
|
1546
|
+
.style('stroke', 'none')
|
|
1547
|
+
.style('fill', fill)
|
|
1497
1548
|
.attr('r', (dia * radius) / 2);
|
|
1498
1549
|
});
|
|
1499
1550
|
}
|
|
@@ -1938,7 +1989,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1938
1989
|
|| (c.label && String(c.label).toLowerCase() === want);
|
|
1939
1990
|
}) || null;
|
|
1940
1991
|
if (!requested) {
|
|
1941
|
-
|
|
1992
|
+
say('problem', 'warn', WARNING + ': initialVisualization "'
|
|
1942
1993
|
+ _settings.initialVisualization
|
|
1943
1994
|
+ '" names no visualization of this tree; using the automatic choice');
|
|
1944
1995
|
}
|
|
@@ -1968,7 +2019,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1968
2019
|
? requested.values.length + ' distinct values on this tree, more than the '
|
|
1969
2020
|
+ LEGEND_MAX_ROWS + ' the legend lists'
|
|
1970
2021
|
: 'a value on ' + requested.coverage + ' of the tree\'s ' + requested.total + ' tips';
|
|
1971
|
-
|
|
2022
|
+
say('problem', 'warn', WARNING + ': initialVisualization "' + _settings.initialVisualization
|
|
1972
2023
|
+ '" names a field with ' + why + '; left to itself the viewer would '
|
|
1973
2024
|
+ (auto ? 'open with "' + auto.label + '" (' + auto.values.length + ' values)'
|
|
1974
2025
|
: 'open uncoloured'));
|
|
@@ -2711,7 +2762,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
2711
2762
|
chips.forEach(function (chip) {
|
|
2712
2763
|
let t = g.append('text')
|
|
2713
2764
|
.attr('x', chipRight).attr('y', baseline)
|
|
2714
|
-
.
|
|
2765
|
+
.style('text-anchor', 'end')
|
|
2715
2766
|
.style('font', rowFont)
|
|
2716
2767
|
.style('fill', ink)
|
|
2717
2768
|
.style('fill-opacity', 0.75)
|
|
@@ -2739,10 +2790,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
2739
2790
|
let frac = max > min ? (mean - min) / (max - min) : 0.5;
|
|
2740
2791
|
let gradId = 'aptx_legend_ramp_' + id;
|
|
2741
2792
|
let grad = g.append('defs').append('linearGradient').attr('id', gradId);
|
|
2742
|
-
grad.append('stop').attr('offset', '0%').
|
|
2793
|
+
grad.append('stop').attr('offset', '0%').style('stop-color', VIS_COLOR_RAMP[0]);
|
|
2743
2794
|
grad.append('stop').attr('offset', (100 * Math.max(0.01, Math.min(0.99, frac))) + '%')
|
|
2744
|
-
.
|
|
2745
|
-
grad.append('stop').attr('offset', '100%').
|
|
2795
|
+
.style('stop-color', VIS_COLOR_RAMP[1]);
|
|
2796
|
+
grad.append('stop').attr('offset', '100%').style('stop-color', VIS_COLOR_RAMP[2]);
|
|
2746
2797
|
let barY = baseline + 6;
|
|
2747
2798
|
let barW = width - 2 * PAD;
|
|
2748
2799
|
g.append('rect')
|
|
@@ -2758,7 +2809,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
2758
2809
|
.text(legendNumberLabel(min));
|
|
2759
2810
|
g.append('text')
|
|
2760
2811
|
.attr('x', x + PAD + barW).attr('y', lblY)
|
|
2761
|
-
.
|
|
2812
|
+
.style('text-anchor', 'end')
|
|
2762
2813
|
.style('font', rowFont).style('fill', ink)
|
|
2763
2814
|
.text(legendNumberLabel(max));
|
|
2764
2815
|
baseline = lblY;
|
|
@@ -2823,7 +2874,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
2823
2874
|
.text(r.text);
|
|
2824
2875
|
g.append('text')
|
|
2825
2876
|
.attr('x', x + width - PAD).attr('y', baseline)
|
|
2826
|
-
.
|
|
2877
|
+
.style('text-anchor', 'end')
|
|
2827
2878
|
.style('font', rowFont)
|
|
2828
2879
|
.style('fill', ink)
|
|
2829
2880
|
.style('fill-opacity', 0.55)
|
|
@@ -3333,7 +3384,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3333
3384
|
.style('paint-order', 'stroke')
|
|
3334
3385
|
// anchor, dy and x all come from nodeLabelPlacement, which the
|
|
3335
3386
|
// occupancy box is built from too
|
|
3336
|
-
.
|
|
3387
|
+
.style('text-anchor', function (d) {
|
|
3337
3388
|
return nodeLabelPlacement(d).anchor;
|
|
3338
3389
|
})
|
|
3339
3390
|
.attr('transform', function (d) {
|
|
@@ -3371,7 +3422,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3371
3422
|
node.select('text.bllabel')
|
|
3372
3423
|
.style('font-size', _state.branchDataFontSize + 'px')
|
|
3373
3424
|
.style('fill', _state.branchColorDefault)
|
|
3374
|
-
.
|
|
3425
|
+
.style('text-anchor', function () {
|
|
3375
3426
|
return radialDisplay() ? 'middle' : null;
|
|
3376
3427
|
})
|
|
3377
3428
|
.attr('transform', function (d) {
|
|
@@ -3538,9 +3589,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
3538
3589
|
if (tipGuides || collapsedGuides.length > 0) {
|
|
3539
3590
|
let ext = _svgGroup.insert('g', 'g').attr('class', 'aptx-align-ext');
|
|
3540
3591
|
let guideStyle = function (sel) {
|
|
3541
|
-
return sel.
|
|
3542
|
-
.
|
|
3543
|
-
.
|
|
3592
|
+
return sel.style('fill', 'none')
|
|
3593
|
+
.style('stroke-width', 1)
|
|
3594
|
+
.style('stroke', _state.branchColorDefault)
|
|
3544
3595
|
.style('stroke-opacity', 0.25);
|
|
3545
3596
|
};
|
|
3546
3597
|
if (tipGuides) {
|
|
@@ -3585,10 +3636,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
3585
3636
|
conn.selectAll('line')
|
|
3586
3637
|
.data(connected)
|
|
3587
3638
|
.enter().append('line')
|
|
3588
|
-
.
|
|
3639
|
+
.style('stroke', _state.branchColorDefault)
|
|
3589
3640
|
// width/opacity match the rectangular aligned extensions; the
|
|
3590
3641
|
// dash alone marks these as connectors (0.5px at 0.3 vanished)
|
|
3591
|
-
.
|
|
3642
|
+
.style('stroke-width', 1)
|
|
3592
3643
|
.style('stroke-opacity', 0.4)
|
|
3593
3644
|
.style('stroke-dasharray', '2,3')
|
|
3594
3645
|
.attr('x1', function (d) { return radialXY(d.x, innerY(d))[0]; })
|
|
@@ -4682,7 +4733,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4682
4733
|
sync(g, 'text.extlabel', d._extLabelText !== '', function (sel) {
|
|
4683
4734
|
sel.append('text')
|
|
4684
4735
|
.attr('class', 'extlabel')
|
|
4685
|
-
.
|
|
4736
|
+
.style('text-anchor', d.children ? 'end' : 'start')
|
|
4686
4737
|
.style('font-family', font)
|
|
4687
4738
|
.style('fill-opacity', 0.5);
|
|
4688
4739
|
});
|
|
@@ -4698,7 +4749,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4698
4749
|
sync(g, 'text.conflabel', d._confText !== '', function (sel) {
|
|
4699
4750
|
sel.append('text')
|
|
4700
4751
|
.attr('class', 'conflabel')
|
|
4701
|
-
.
|
|
4752
|
+
.style('text-anchor', 'middle')
|
|
4702
4753
|
.style('font-family', font);
|
|
4703
4754
|
});
|
|
4704
4755
|
}
|
|
@@ -4706,7 +4757,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4706
4757
|
sync(g, 'text.brancheventlabel', d._eventText !== '', function (sel) {
|
|
4707
4758
|
sel.append('text')
|
|
4708
4759
|
.attr('class', 'brancheventlabel')
|
|
4709
|
-
.
|
|
4760
|
+
.style('text-anchor', 'middle')
|
|
4710
4761
|
.style('font-family', font);
|
|
4711
4762
|
});
|
|
4712
4763
|
}
|
|
@@ -5346,9 +5397,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
5346
5397
|
let els = sizeLayer(_geomLayers.branches, 'path', branchList.length);
|
|
5347
5398
|
branchList.forEach(function (b, k) {
|
|
5348
5399
|
b.el = els[k];
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5400
|
+
// These paths are kept from one drawing to the next, and a style
|
|
5401
|
+
// is not an attribute: a value that is no colour or no width (a
|
|
5402
|
+
// file's <width>-1</width>) is IGNORED, and the path would keep
|
|
5403
|
+
// what another batch gave it the last time. So the old is taken
|
|
5404
|
+
// off first, and such a batch is drawn as the svg has it.
|
|
5405
|
+
b.el.style.setProperty('fill', 'none');
|
|
5406
|
+
b.el.style.removeProperty('stroke');
|
|
5407
|
+
b.el.style.setProperty('stroke', b.color);
|
|
5408
|
+
b.el.style.removeProperty('stroke-width');
|
|
5409
|
+
b.el.style.setProperty('stroke-width', b.width);
|
|
5352
5410
|
});
|
|
5353
5411
|
// `last`: batches painted after the sorted ones, whatever their count
|
|
5354
5412
|
let paintLayer = function (batches, layer, last) {
|
|
@@ -5825,14 +5883,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
5825
5883
|
return;
|
|
5826
5884
|
}
|
|
5827
5885
|
if (dep.to) {
|
|
5828
|
-
|
|
5886
|
+
say('problem', 'warn', WARNING + ': "' + k + '" was replaced by "' + dep.to
|
|
5829
5887
|
+ '": ' + dep.note);
|
|
5830
5888
|
// An explicit new key always wins over the old alias.
|
|
5831
5889
|
if (given[dep.to] === undefined) {
|
|
5832
5890
|
given[dep.to] = dep.map(given[k]);
|
|
5833
5891
|
}
|
|
5834
5892
|
} else {
|
|
5835
|
-
|
|
5893
|
+
say('problem', 'warn', WARNING + ': "' + k + '" is retired and has no effect: ' + dep.note);
|
|
5836
5894
|
}
|
|
5837
5895
|
delete given[k];
|
|
5838
5896
|
});
|
|
@@ -6078,7 +6136,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6078
6136
|
// malformed domains are skipped, never fatal -- said once, here
|
|
6079
6137
|
let ignored = forester.domainArchitectureStats(_treeData).ignored;
|
|
6080
6138
|
if (ignored > 0) {
|
|
6081
|
-
|
|
6139
|
+
say('note', 'warn', MESSAGE + ignored + ' protein domain' + (ignored === 1 ? '' : 's')
|
|
6082
6140
|
+ ' with a missing or impossible from / to / E-value ignored');
|
|
6083
6141
|
}
|
|
6084
6142
|
}
|
|
@@ -6111,11 +6169,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6111
6169
|
// are left out, never fatal -- said once, here
|
|
6112
6170
|
let gs = forester.geneRegionStats(_treeData);
|
|
6113
6171
|
if (gs.ignored > 0) {
|
|
6114
|
-
|
|
6172
|
+
say('note', 'warn', MESSAGE + gs.ignored + ' sequence location' + (gs.ignored === 1 ? '' : 's')
|
|
6115
6173
|
+ ' not in the form contig:start-end(+) ignored');
|
|
6116
6174
|
}
|
|
6117
6175
|
if (gs.otherContigs > 0) {
|
|
6118
|
-
|
|
6176
|
+
say('note', 'warn', MESSAGE + gs.otherContigs + ' gene' + (gs.otherContigs === 1 ? '' : 's')
|
|
6119
6177
|
+ ' on a genome\'s other contigs not drawn: the gene track shows one contig per genome');
|
|
6120
6178
|
}
|
|
6121
6179
|
}
|
|
@@ -6347,11 +6405,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6347
6405
|
_settings.enableDynamicSizing = true;
|
|
6348
6406
|
}
|
|
6349
6407
|
if (_settings.displayWidth && _settings.enableDynamicSizing === true) {
|
|
6350
|
-
|
|
6408
|
+
say('note', 'log', WARNING + ': dynamic sizing is turned on, will ignore displayWidth setting');
|
|
6351
6409
|
_settings.displayWidth = 0;
|
|
6352
6410
|
}
|
|
6353
6411
|
if (_settings.displayHeight && _settings.enableDynamicSizing === true) {
|
|
6354
|
-
|
|
6412
|
+
say('note', 'log', WARNING + ': dynamic sizing is turned on, will ignore displayHeight setting');
|
|
6355
6413
|
_settings.displayHeight = 0;
|
|
6356
6414
|
}
|
|
6357
6415
|
if ((!_settings.displayWidth) && (!_settings.enableDynamicSizing)) {
|
|
@@ -6640,7 +6698,40 @@ function (root, d3, forester, phyloXml) {
|
|
|
6640
6698
|
|
|
6641
6699
|
// launch(container, tree, config) -> a viewer handle {getSelectedNodes,
|
|
6642
6700
|
// destroy}. Exactly three arguments; config is THE one config object.
|
|
6643
|
-
|
|
6701
|
+
// launch() proper is launchChecked below. This shell keeps what a
|
|
6702
|
+
// rejected launch said: the thrown message reaches whoever called, which
|
|
6703
|
+
// on an embedding site is often a catch block nobody reads, and
|
|
6704
|
+
// archaeopteryx.doctor() can still tell. (launchArchaeopteryx has its own
|
|
6705
|
+
// failures -- a file that does not parse -- and reports them through the
|
|
6706
|
+
// same function; an error is recorded once, whoever catches it first.)
|
|
6707
|
+
function recordLaunchFailure(e) {
|
|
6708
|
+
if (_recordedErrors && e && typeof e === 'object') {
|
|
6709
|
+
if (_recordedErrors.has(e)) {
|
|
6710
|
+
return;
|
|
6711
|
+
}
|
|
6712
|
+
_recordedErrors.add(e);
|
|
6713
|
+
}
|
|
6714
|
+
recordDiagnostic('problem', 'launch failed: '
|
|
6715
|
+
+ String(e && e.message ? e.message : e).replace(/^ArchaeopteryxJS: ERROR: /, ''));
|
|
6716
|
+
}
|
|
6717
|
+
archaeopteryx.launch = function () {
|
|
6718
|
+
let before = _diagnostics;
|
|
6719
|
+
let wasOnView = _onView;
|
|
6720
|
+
_tookOver = false;
|
|
6721
|
+
try {
|
|
6722
|
+
return launchChecked.apply(null, arguments);
|
|
6723
|
+
} catch (e) {
|
|
6724
|
+
if (wasOnView && !_tookOver) {
|
|
6725
|
+
// rejected before it touched the page: the earlier viewer is
|
|
6726
|
+
// still there, and so is everything said about it
|
|
6727
|
+
_diagnostics = before;
|
|
6728
|
+
}
|
|
6729
|
+
recordLaunchFailure(e);
|
|
6730
|
+
throw e;
|
|
6731
|
+
}
|
|
6732
|
+
};
|
|
6733
|
+
|
|
6734
|
+
function launchChecked(container, phylo, config) {
|
|
6644
6735
|
|
|
6645
6736
|
// Bad input is the caller's bug, so it is thrown at the caller. It used
|
|
6646
6737
|
// to pop a browser alert and return, which blocks the whole tab and
|
|
@@ -6715,12 +6806,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
6715
6806
|
let view = config && config.view;
|
|
6716
6807
|
let start = (view && Number.isInteger(view.tree) && view.tree > 0 && view.tree < trees.length) ? view.tree : 0;
|
|
6717
6808
|
return launchInto(container, trees, start, config);
|
|
6718
|
-
}
|
|
6809
|
+
}
|
|
6719
6810
|
|
|
6720
6811
|
// The launch proper, for one tree of the list: launch() validates and
|
|
6721
6812
|
// starts at the first, showTree() re-enters here for another.
|
|
6722
6813
|
function launchInto(container, trees, index, config) {
|
|
6723
6814
|
let phylo = trees[index];
|
|
6815
|
+
// what is said from here on is said about THIS tree (see _diagnostics)
|
|
6816
|
+
_diagnostics = [];
|
|
6724
6817
|
let cfg = readConfig(config);
|
|
6725
6818
|
requireForester();
|
|
6726
6819
|
requireD3();
|
|
@@ -6767,6 +6860,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6767
6860
|
checkLayoutValue(cfg.state ? cfg.state.layout : undefined);
|
|
6768
6861
|
initializeSettings(cfg.settings);
|
|
6769
6862
|
|
|
6863
|
+
// From here the page is this launch's: nothing below is expected to
|
|
6864
|
+
// throw, and what a previous one drew goes now.
|
|
6865
|
+
_tookOver = true;
|
|
6866
|
+
_onView = {config: config, container: containerEl, tips: _basicTreeProperties.externalNodesCount,
|
|
6867
|
+
tree: index + 1, trees: trees.length};
|
|
6770
6868
|
// whatever a previous launch left in this container (or in the
|
|
6771
6869
|
// container before it) goes now, so the old tree does not sit under
|
|
6772
6870
|
// the card while the new one is prepared
|
|
@@ -6805,8 +6903,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
6805
6903
|
createGui();
|
|
6806
6904
|
|
|
6807
6905
|
_baseSvg = d3.select(containerEl).append('svg')
|
|
6808
|
-
.attr('
|
|
6809
|
-
.
|
|
6906
|
+
.attr('class', TREE_SVG_CLASS)
|
|
6907
|
+
.call(sizeTreeSvg, _displayWidth, _displayHeight)
|
|
6908
|
+
// The tree's font, for the texts that state only a size (the
|
|
6909
|
+
// alignment strip's ruler and caption, the heat map strip's
|
|
6910
|
+
// numbers and caption): they were drawn in the PAGE's font,
|
|
6911
|
+
// in boxes measured for this one, and in an exported file in
|
|
6912
|
+
// whatever the program that opened it thought best. Inline,
|
|
6913
|
+
// so that the file has it too.
|
|
6914
|
+
.style('font-family', FONT_DEFAULTS)
|
|
6810
6915
|
// an inline svg sits on a text baseline, and the room left
|
|
6811
6916
|
// under it for descenders makes a container sized to the
|
|
6812
6917
|
// svg a few pixels taller than the svg itself -- enough to
|
|
@@ -6829,8 +6934,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6829
6934
|
}
|
|
6830
6935
|
_displayWidth = size.w;
|
|
6831
6936
|
_displayHeight = size.h;
|
|
6832
|
-
_baseSvg.
|
|
6833
|
-
_baseSvg.attr('height', size.h);
|
|
6937
|
+
sizeTreeSvg(_baseSvg, size.w, size.h);
|
|
6834
6938
|
rebuildOverview();
|
|
6835
6939
|
if ((_settings.zoomToFitUponWindowResize === true) && (_zoomed_x_or_y === false) && (Math.abs(currentZoomScale() - 1.0) < 0.001)) {
|
|
6836
6940
|
zoomToFit();
|
|
@@ -6977,6 +7081,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
6977
7081
|
cfg.view = view || viewForTree(_trees[index]);
|
|
6978
7082
|
let handle = launchInto(_container, _trees, index, cfg);
|
|
6979
7083
|
_launchConfig = launchConfig;
|
|
7084
|
+
// the report shows the config the SITE passed, not this copy with
|
|
7085
|
+
// the tree's own view added
|
|
7086
|
+
if (_onView) {
|
|
7087
|
+
_onView.config = launchConfig;
|
|
7088
|
+
}
|
|
6980
7089
|
return handle;
|
|
6981
7090
|
}
|
|
6982
7091
|
|
|
@@ -7897,7 +8006,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7897
8006
|
let x1 = Math.round(originX + (endI * cw));
|
|
7898
8007
|
g.append('rect').attr('x', x0).attr('y', cy)
|
|
7899
8008
|
.attr('width', Math.max(1, x1 - x0)).attr('height', rh)
|
|
7900
|
-
.
|
|
8009
|
+
.style('fill', 'rgb(' + runRgb.join(',') + ')');
|
|
7901
8010
|
runStart = -1;
|
|
7902
8011
|
runRgb = null;
|
|
7903
8012
|
};
|
|
@@ -7912,7 +8021,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7912
8021
|
g.append('line')
|
|
7913
8022
|
.attr('x1', Math.round(originX + (gapStart * cw))).attr('y1', mid)
|
|
7914
8023
|
.attr('x2', Math.round(originX + (endI * cw))).attr('y2', mid)
|
|
7915
|
-
.
|
|
8024
|
+
.style('stroke', ink).style('stroke-opacity', 0.35).style('stroke-width', 1);
|
|
7916
8025
|
gapStart = -1;
|
|
7917
8026
|
};
|
|
7918
8027
|
for (let i = 0; i < visible; ++i) {
|
|
@@ -7936,9 +8045,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
7936
8045
|
let x1 = Math.round(originX + ((i + 1) * cw));
|
|
7937
8046
|
g.append('rect').attr('x', x0).attr('y', cy)
|
|
7938
8047
|
.attr('width', Math.max(1, x1 - x0)).attr('height', rh)
|
|
7939
|
-
.
|
|
8048
|
+
.style('fill', 'rgb(' + rgb.join(',') + ')');
|
|
7940
8049
|
g.append('text').attr('x', x0 + ((x1 - x0) / 2)).attr('y', cy + (rh / 2))
|
|
7941
|
-
.
|
|
8050
|
+
.style('text-anchor', 'middle').attr('dy', '0.35em')
|
|
7942
8051
|
.style('font-family', 'monospace').style('font-size', fontPx + 'px')
|
|
7943
8052
|
.style('fill', forester.msaLetterInk(rgb))
|
|
7944
8053
|
.text(ch.toUpperCase());
|
|
@@ -7962,13 +8071,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
7962
8071
|
if (offset <= 0) {
|
|
7963
8072
|
g.append('line').attr('x1', Math.round(originX)).attr('x2', Math.round(originX))
|
|
7964
8073
|
.attr('y1', rowsTop).attr('y2', rowsBottom)
|
|
7965
|
-
.
|
|
8074
|
+
.style('stroke', ink).style('stroke-opacity', 0.7).style('stroke-width', 1);
|
|
7966
8075
|
}
|
|
7967
8076
|
if ((offset + visible) >= total) {
|
|
7968
8077
|
let xe = Math.round(originX + (visible * cw));
|
|
7969
8078
|
g.append('line').attr('x1', xe).attr('x2', xe)
|
|
7970
8079
|
.attr('y1', rowsTop).attr('y2', rowsBottom)
|
|
7971
|
-
.
|
|
8080
|
+
.style('stroke', ink).style('stroke-opacity', 0.7).style('stroke-width', 1);
|
|
7972
8081
|
}
|
|
7973
8082
|
|
|
7974
8083
|
// Scored over the visible WINDOW only -- the whole alignment would
|
|
@@ -8007,9 +8116,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8007
8116
|
g.append('line')
|
|
8008
8117
|
.attr('x1', from).attr('x2', guideEnd)
|
|
8009
8118
|
.attr('y1', d.x).attr('y2', d.x)
|
|
8010
|
-
.
|
|
8011
|
-
.
|
|
8012
|
-
.
|
|
8119
|
+
.style('stroke', ink)
|
|
8120
|
+
.style('stroke-width', 1)
|
|
8121
|
+
.style('stroke-dasharray', '2,3')
|
|
8013
8122
|
.style('stroke-opacity', 0.35)
|
|
8014
8123
|
.style('pointer-events', 'none');
|
|
8015
8124
|
}
|
|
@@ -8019,7 +8128,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8019
8128
|
let strip = floatStripGroup('aptx-msa-strip', _clusterH, stripH);
|
|
8020
8129
|
strip.append('rect').attr('x', Math.round(originX) - 2).attr('y', _clusterH)
|
|
8021
8130
|
.attr('width', trackW + 4).attr('height', stripH)
|
|
8022
|
-
.
|
|
8131
|
+
.style('fill', _state.backgroundColorDefault);
|
|
8023
8132
|
let consTop = _clusterH + MSA_CONS_TOP_GAP;
|
|
8024
8133
|
// A caption on the panel's own background, so it stays readable over
|
|
8025
8134
|
// whatever the band happens to draw underneath it.
|
|
@@ -8032,9 +8141,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8032
8141
|
let xe = Math.round(originX) + trackW - 3;
|
|
8033
8142
|
strip.append('rect').attr('x', xe - w).attr('y', y - 8)
|
|
8034
8143
|
.attr('width', w).attr('height', 11)
|
|
8035
|
-
.
|
|
8144
|
+
.style('fill', _state.backgroundColorDefault).style('fill-opacity', 0.85);
|
|
8036
8145
|
strip.append('text').attr('x', xe - 3).attr('y', y)
|
|
8037
|
-
.
|
|
8146
|
+
.style('text-anchor', 'end')
|
|
8038
8147
|
.style('font-size', '8px').style('fill', ink).style('fill-opacity', 0.9)
|
|
8039
8148
|
.text(txt);
|
|
8040
8149
|
};
|
|
@@ -8051,7 +8160,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8051
8160
|
strip.append('line')
|
|
8052
8161
|
.attr('x1', Math.round(originX)).attr('x2', Math.round(originX) + trackW)
|
|
8053
8162
|
.attr('y1', floor).attr('y2', floor)
|
|
8054
|
-
.
|
|
8163
|
+
.style('stroke', ink).style('stroke-opacity', 0.5).style('stroke-width', 1);
|
|
8055
8164
|
for (let i = 0; i < visible; ++i) {
|
|
8056
8165
|
let col = logo.columns[i];
|
|
8057
8166
|
if (!col || !(col.height > 0)) {
|
|
@@ -8073,7 +8182,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8073
8182
|
.attr('transform', 'translate(' + (x0 + ((x1 - x0) / 2)) + ','
|
|
8074
8183
|
+ (y - (glyph.descent * sy)) + ')'
|
|
8075
8184
|
+ ' scale(' + (Math.max(1, x1 - x0) / glyph.width) + ',' + sy + ')')
|
|
8076
|
-
.
|
|
8185
|
+
.style('text-anchor', 'middle')
|
|
8077
8186
|
.style('font-family', 'monospace')
|
|
8078
8187
|
.style('font-size', MSA_LOGO_FONT_PX + 'px')
|
|
8079
8188
|
.style('fill', rgb === null ? ink : 'rgb(' + rgb.join(',') + ')')
|
|
@@ -8086,7 +8195,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8086
8195
|
} else {
|
|
8087
8196
|
strip.append('rect').attr('x', Math.round(originX)).attr('y', consTop)
|
|
8088
8197
|
.attr('width', trackW).attr('height', MSA_CONS_BAR_H)
|
|
8089
|
-
.
|
|
8198
|
+
.style('fill', ink).style('fill-opacity', 0.08);
|
|
8090
8199
|
for (let i = 0; i < visible; ++i) {
|
|
8091
8200
|
let score = cons.scores[i] || 0;
|
|
8092
8201
|
if (score <= 0) {
|
|
@@ -8097,7 +8206,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8097
8206
|
let x1 = Math.round(originX + ((i + 1) * cw));
|
|
8098
8207
|
strip.append('rect').attr('x', x0).attr('y', (consTop + MSA_CONS_BAR_H) - bh)
|
|
8099
8208
|
.attr('width', Math.max(1, x1 - x0)).attr('height', bh)
|
|
8100
|
-
.
|
|
8209
|
+
.style('fill', ink).style('fill-opacity', 0.7);
|
|
8101
8210
|
}
|
|
8102
8211
|
caption('Consensus identity (n = ' + n + ')', consTop + 9);
|
|
8103
8212
|
for (let i = 0; i < visible; ++i) {
|
|
@@ -8107,7 +8216,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8107
8216
|
}
|
|
8108
8217
|
strip.append('text').attr('x', Math.round(originX + (i * cw)) + (cw / 2))
|
|
8109
8218
|
.attr('y', consTop + MSA_CONS_BAR_H + 10)
|
|
8110
|
-
.
|
|
8219
|
+
.style('text-anchor', 'middle')
|
|
8111
8220
|
.style('font-family', 'monospace').style('font-size', '8px')
|
|
8112
8221
|
.style('fill', ink)
|
|
8113
8222
|
.text(cc);
|
|
@@ -8119,7 +8228,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8119
8228
|
let rulerY = consTop + (logo ? MSA_LOGO_CAP_H + MSA_LOGO_H : MSA_CONS_BAR_H + MSA_CONS_ROW_H);
|
|
8120
8229
|
strip.append('line').attr('x1', Math.round(originX)).attr('x2', Math.round(originX) + trackW)
|
|
8121
8230
|
.attr('y1', rulerY).attr('y2', rulerY)
|
|
8122
|
-
.
|
|
8231
|
+
.style('stroke', ink).style('stroke-opacity', 0.8).style('stroke-width', 1);
|
|
8123
8232
|
let lastRight = -Infinity;
|
|
8124
8233
|
let drawTick = function (c, force) {
|
|
8125
8234
|
let i = c - 1 - offset;
|
|
@@ -8128,11 +8237,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
8128
8237
|
}
|
|
8129
8238
|
let x = Math.round(originX + (i * cw)) + (cw / 2);
|
|
8130
8239
|
strip.append('line').attr('x1', x).attr('x2', x).attr('y1', rulerY).attr('y2', rulerY + 4)
|
|
8131
|
-
.
|
|
8240
|
+
.style('stroke', ink).style('stroke-opacity', 0.8).style('stroke-width', 1);
|
|
8132
8241
|
let label = String(c);
|
|
8133
8242
|
let half = label.length * 2.8;
|
|
8134
8243
|
if (force || (x - half) >= (lastRight + 4)) {
|
|
8135
|
-
strip.append('text').attr('x', x).attr('y', rulerY + 14).
|
|
8244
|
+
strip.append('text').attr('x', x).attr('y', rulerY + 14).style('text-anchor', 'middle')
|
|
8136
8245
|
.style('font-size', '9px').style('fill', ink)
|
|
8137
8246
|
.text(label);
|
|
8138
8247
|
lastRight = x + half;
|
|
@@ -8149,7 +8258,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8149
8258
|
g.append('rect').attr('class', 'aptx-msa-hover')
|
|
8150
8259
|
.attr('x', Math.round(originX)).attr('y', rowsTop)
|
|
8151
8260
|
.attr('width', trackW).attr('height', Math.max(1, rowsBottom - rowsTop))
|
|
8152
|
-
.
|
|
8261
|
+
.style('fill', 'transparent')
|
|
8153
8262
|
.on('mousemove', msaHoverMove)
|
|
8154
8263
|
.on('mouseout', msaHoverOut)
|
|
8155
8264
|
.on('wheel', function (event) {
|
|
@@ -8651,10 +8760,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
8651
8760
|
// that came and went with the zoom level. Both strokes are a constant
|
|
8652
8761
|
// screen width, so a zoom neither fattens nor loses them.
|
|
8653
8762
|
function paintHeatmapCell(el, fill, bordered) {
|
|
8654
|
-
el.
|
|
8655
|
-
.
|
|
8656
|
-
.
|
|
8657
|
-
|
|
8763
|
+
el.style('fill', fill)
|
|
8764
|
+
.style('stroke', bordered ? heatmapBorderColor(fill) : fill)
|
|
8765
|
+
.style('stroke-width', bordered ? HEATMAP_BORDER_W : HEATMAP_SEAL_W)
|
|
8766
|
+
// ... said twice: as a style for the page, as everything here is,
|
|
8767
|
+
// and as an attribute for canvg, which draws the PNG and reads this
|
|
8768
|
+
// from the attribute alone (as a style only, every border came out
|
|
8769
|
+
// at the export's scale, four times as wide)
|
|
8770
|
+
.style('vector-effect', 'non-scaling-stroke').attr('vector-effect', 'non-scaling-stroke');
|
|
8658
8771
|
return el;
|
|
8659
8772
|
}
|
|
8660
8773
|
|
|
@@ -8784,8 +8897,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
8784
8897
|
let rect = g.append('rect').attr('x', x0).attr('y', cy)
|
|
8785
8898
|
.attr('width', Math.max(1, x1 - x0)).attr('height', rh);
|
|
8786
8899
|
if (runFill === HEATMAP_BLANK) {
|
|
8787
|
-
rect.
|
|
8788
|
-
.
|
|
8900
|
+
rect.style('fill', _state.backgroundColorDefault)
|
|
8901
|
+
.style('stroke', ink).style('stroke-opacity', 0.45).style('stroke-width', 1);
|
|
8789
8902
|
} else {
|
|
8790
8903
|
paintHeatmapCell(rect, runFill, bordered);
|
|
8791
8904
|
}
|
|
@@ -8817,8 +8930,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
8817
8930
|
g.append('rect')
|
|
8818
8931
|
.attr('x', Math.round(originX)).attr('y', rowsTop)
|
|
8819
8932
|
.attr('width', trackW).attr('height', Math.max(1, rowsBottom - rowsTop))
|
|
8820
|
-
.
|
|
8821
|
-
.
|
|
8933
|
+
.style('fill', 'none')
|
|
8934
|
+
.style('stroke', ink).style('stroke-opacity', 0.55).style('stroke-width', 1);
|
|
8822
8935
|
|
|
8823
8936
|
// ---- the clustering behind the column order, above the grid ----
|
|
8824
8937
|
// Drawn with the cells, in the tree's own group, so it zooms, pans and
|
|
@@ -8871,9 +8984,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8871
8984
|
}
|
|
8872
8985
|
g.append('path').attr('class', 'aptx-heatmap-dendro')
|
|
8873
8986
|
.attr('d', path)
|
|
8874
|
-
.
|
|
8875
|
-
.
|
|
8876
|
-
.
|
|
8987
|
+
.style('fill', 'none')
|
|
8988
|
+
.style('stroke', ink)
|
|
8989
|
+
.style('stroke-width', Math.max(1, _state.branchWidthDefault))
|
|
8877
8990
|
.style('pointer-events', 'none');
|
|
8878
8991
|
}
|
|
8879
8992
|
|
|
@@ -8902,9 +9015,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8902
9015
|
g.append('line')
|
|
8903
9016
|
.attr('x1', from).attr('x2', guideEnd)
|
|
8904
9017
|
.attr('y1', d.x).attr('y2', d.x)
|
|
8905
|
-
.
|
|
8906
|
-
.
|
|
8907
|
-
.
|
|
9018
|
+
.style('stroke', ink)
|
|
9019
|
+
.style('stroke-width', 1)
|
|
9020
|
+
.style('stroke-dasharray', '2,3')
|
|
8908
9021
|
.style('stroke-opacity', 0.35)
|
|
8909
9022
|
.style('pointer-events', 'none');
|
|
8910
9023
|
}
|
|
@@ -8916,7 +9029,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8916
9029
|
let strip = floatStripGroup('aptx-heatmap-strip', _clusterH, heatmapBottomReserve());
|
|
8917
9030
|
strip.append('rect').attr('x', Math.round(originX) - 2).attr('y', _clusterH)
|
|
8918
9031
|
.attr('width', trackW + 4).attr('height', heatmapStripHeight())
|
|
8919
|
-
.
|
|
9032
|
+
.style('fill', _state.backgroundColorDefault);
|
|
8920
9033
|
let labelTop = _clusterH + HEATMAP_HEAD_GAP;
|
|
8921
9034
|
let headH = heatmapHeadHeight();
|
|
8922
9035
|
if (headH > 0) {
|
|
@@ -8927,7 +9040,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8927
9040
|
.attr('transform', 'translate(' + (originX + (i * cw) + (cw / 2)) + ','
|
|
8928
9041
|
+ labelTop + ') rotate(-90)')
|
|
8929
9042
|
.attr('dy', '0.32em')
|
|
8930
|
-
.
|
|
9043
|
+
.style('text-anchor', 'end')
|
|
8931
9044
|
.style('font', font)
|
|
8932
9045
|
.style('fill', _state.labelColorDefault)
|
|
8933
9046
|
.text(clipTextToWidth(col.label, font, HEATMAP_LABEL_MAX_PX));
|
|
@@ -8944,20 +9057,20 @@ function (root, d3, forester, phyloXml) {
|
|
|
8944
9057
|
VIS_COLOR_RAMP.forEach(function (c, i) {
|
|
8945
9058
|
grad.append('stop')
|
|
8946
9059
|
.attr('offset', ((i * 100) / (VIS_COLOR_RAMP.length - 1)) + '%')
|
|
8947
|
-
.
|
|
9060
|
+
.style('stop-color', c);
|
|
8948
9061
|
});
|
|
8949
9062
|
strip.append('rect')
|
|
8950
9063
|
.attr('x', Math.round(originX)).attr('y', scaleTop)
|
|
8951
9064
|
.attr('width', barW).attr('height', HEATMAP_SCALE_BAR_H)
|
|
8952
|
-
.
|
|
8953
|
-
.
|
|
9065
|
+
.style('fill', 'url(#' + gradId + ')')
|
|
9066
|
+
.style('stroke', ink).style('stroke-opacity', 0.5).style('stroke-width', 1);
|
|
8954
9067
|
let numY = scaleTop + HEATMAP_SCALE_BAR_H + 10;
|
|
8955
9068
|
strip.append('text').attr('x', Math.round(originX)).attr('y', numY)
|
|
8956
|
-
.
|
|
9069
|
+
.style('text-anchor', 'start')
|
|
8957
9070
|
.style('font-size', '9px').style('fill', ink)
|
|
8958
9071
|
.text(heatmapNum(model.min));
|
|
8959
9072
|
strip.append('text').attr('x', Math.round(originX) + barW).attr('y', numY)
|
|
8960
|
-
.
|
|
9073
|
+
.style('text-anchor', 'end')
|
|
8961
9074
|
.style('font-size', '9px').style('fill', ink)
|
|
8962
9075
|
.text(heatmapNum(model.max));
|
|
8963
9076
|
// the caption: what the matrix is showing, and -- when it is windowed
|
|
@@ -8975,11 +9088,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
8975
9088
|
if ((rightEdge - cursor) >= keyW) {
|
|
8976
9089
|
strip.append('rect').attr('x', cursor).attr('y', scaleTop)
|
|
8977
9090
|
.attr('width', HEATMAP_SCALE_BAR_H).attr('height', HEATMAP_SCALE_BAR_H)
|
|
8978
|
-
.
|
|
8979
|
-
.
|
|
9091
|
+
.style('fill', _state.backgroundColorDefault)
|
|
9092
|
+
.style('stroke', ink).style('stroke-opacity', 0.45).style('stroke-width', 1);
|
|
8980
9093
|
strip.append('text').attr('x', cursor + HEATMAP_SCALE_BAR_H + 4)
|
|
8981
9094
|
.attr('y', scaleTop + HEATMAP_SCALE_BAR_H)
|
|
8982
|
-
.
|
|
9095
|
+
.style('text-anchor', 'start')
|
|
8983
9096
|
.style('font', capFont).style('fill', ink).style('fill-opacity', 0.85)
|
|
8984
9097
|
.text(HEATMAP_NOT_ASSESSED);
|
|
8985
9098
|
cursor += keyW + 10;
|
|
@@ -8987,7 +9100,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8987
9100
|
}
|
|
8988
9101
|
if ((rightEdge - cursor) > 60) {
|
|
8989
9102
|
strip.append('text').attr('x', cursor).attr('y', scaleTop + HEATMAP_SCALE_BAR_H)
|
|
8990
|
-
.
|
|
9103
|
+
.style('text-anchor', 'start')
|
|
8991
9104
|
.style('font', capFont).style('fill', ink).style('fill-opacity', 0.85)
|
|
8992
9105
|
.text(clipTextToWidth(caption, capFont, rightEdge - cursor));
|
|
8993
9106
|
}
|
|
@@ -8996,7 +9109,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
8996
9109
|
g.append('rect').attr('class', 'aptx-heatmap-hover')
|
|
8997
9110
|
.attr('x', Math.round(originX)).attr('y', rowsTop)
|
|
8998
9111
|
.attr('width', trackW).attr('height', Math.max(1, rowsBottom - rowsTop))
|
|
8999
|
-
.
|
|
9112
|
+
.style('fill', 'transparent')
|
|
9000
9113
|
.on('mousemove', heatmapHoverMove)
|
|
9001
9114
|
.on('mouseout', heatmapHoverOut)
|
|
9002
9115
|
.on('wheel', function (event) {
|
|
@@ -9138,12 +9251,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
9138
9251
|
let path = g.append('path')
|
|
9139
9252
|
.attr('d', heatmapSector(edge[runStart], a1, r0, r1 + bleed));
|
|
9140
9253
|
if (runFill === HEATMAP_BLANK) {
|
|
9141
|
-
path.
|
|
9142
|
-
.
|
|
9254
|
+
path.style('fill', _state.backgroundColorDefault)
|
|
9255
|
+
.style('stroke', ink).style('stroke-opacity', 0.45).style('stroke-width', 1);
|
|
9143
9256
|
} else if (bordered) {
|
|
9144
9257
|
paintHeatmapCell(path, runFill, true);
|
|
9145
9258
|
} else {
|
|
9146
|
-
path.
|
|
9259
|
+
path.style('fill', runFill); // the bleed already closes the seams here
|
|
9147
9260
|
}
|
|
9148
9261
|
runStart = -1;
|
|
9149
9262
|
runFill = null;
|
|
@@ -9169,7 +9282,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9169
9282
|
[start, start + (columns.length * ringW)].forEach(function (r) {
|
|
9170
9283
|
g.append('path')
|
|
9171
9284
|
.attr('d', heatmapSector(edge[0], edge[n], r - 0.5, r + 0.5))
|
|
9172
|
-
.
|
|
9285
|
+
.style('fill', ink).style('fill-opacity', 0.5);
|
|
9173
9286
|
});
|
|
9174
9287
|
|
|
9175
9288
|
// Each ring's name, laid along the ring at the fan's seam -- the gap
|
|
@@ -9193,7 +9306,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9193
9306
|
g.append('text')
|
|
9194
9307
|
.attr('transform', 'translate(' + xy[0].toFixed(1) + ',' + xy[1].toFixed(1) + ') '
|
|
9195
9308
|
+ 'rotate(' + (deg + (flip ? 180 : 0)).toFixed(1) + ')')
|
|
9196
|
-
.
|
|
9309
|
+
.style('text-anchor', 'middle')
|
|
9197
9310
|
.attr('dy', '0.32em')
|
|
9198
9311
|
.style('font', font)
|
|
9199
9312
|
.style('fill', _state.labelColorDefault)
|
|
@@ -9205,7 +9318,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9205
9318
|
g.append('path')
|
|
9206
9319
|
.attr('class', 'aptx-heatmap-hover')
|
|
9207
9320
|
.attr('d', heatmapSector(edge[0], edge[n], start, start + (columns.length * ringW)))
|
|
9208
|
-
.
|
|
9321
|
+
.style('fill', 'transparent')
|
|
9209
9322
|
.on('mousemove', heatmapHoverMove)
|
|
9210
9323
|
.on('mouseout', heatmapHoverOut);
|
|
9211
9324
|
|
|
@@ -9259,31 +9372,31 @@ function (root, d3, forester, phyloXml) {
|
|
|
9259
9372
|
VIS_COLOR_RAMP.forEach(function (c, i) {
|
|
9260
9373
|
grad.append('stop')
|
|
9261
9374
|
.attr('offset', ((i * 100) / (VIS_COLOR_RAMP.length - 1)) + '%')
|
|
9262
|
-
.
|
|
9375
|
+
.style('stop-color', c);
|
|
9263
9376
|
});
|
|
9264
9377
|
let barY = y + PAD;
|
|
9265
9378
|
g.append('rect').attr('x', x + PAD).attr('y', barY)
|
|
9266
9379
|
.attr('width', BAR_W).attr('height', HEATMAP_SCALE_BAR_H)
|
|
9267
|
-
.
|
|
9268
|
-
.
|
|
9380
|
+
.style('fill', 'url(#' + gradId + ')')
|
|
9381
|
+
.style('stroke', frame).style('stroke-opacity', 0.5).style('stroke-width', 1);
|
|
9269
9382
|
let line = barY + HEATMAP_SCALE_BAR_H + FS + 2;
|
|
9270
|
-
g.append('text').attr('x', x + PAD).attr('y', line).
|
|
9383
|
+
g.append('text').attr('x', x + PAD).attr('y', line).style('text-anchor', 'start')
|
|
9271
9384
|
.style('font', capFont).style('fill', ink).text(heatmapNum(m.min));
|
|
9272
|
-
g.append('text').attr('x', x + PAD + BAR_W).attr('y', line).
|
|
9385
|
+
g.append('text').attr('x', x + PAD + BAR_W).attr('y', line).style('text-anchor', 'end')
|
|
9273
9386
|
.style('font', capFont).style('fill', ink).text(heatmapNum(m.max));
|
|
9274
9387
|
if (blanks) {
|
|
9275
9388
|
line += FS + 4;
|
|
9276
9389
|
g.append('rect').attr('x', x + PAD).attr('y', line - HEATMAP_SCALE_BAR_H)
|
|
9277
9390
|
.attr('width', HEATMAP_SCALE_BAR_H).attr('height', HEATMAP_SCALE_BAR_H)
|
|
9278
|
-
.
|
|
9279
|
-
.
|
|
9391
|
+
.style('fill', _state.backgroundColorDefault)
|
|
9392
|
+
.style('stroke', frame).style('stroke-opacity', 0.45).style('stroke-width', 1);
|
|
9280
9393
|
g.append('text').attr('x', x + PAD + HEATMAP_SCALE_BAR_H + 4).attr('y', line)
|
|
9281
|
-
.
|
|
9394
|
+
.style('text-anchor', 'start')
|
|
9282
9395
|
.style('font', capFont).style('fill', ink).style('fill-opacity', 0.85)
|
|
9283
9396
|
.text(HEATMAP_NOT_ASSESSED);
|
|
9284
9397
|
}
|
|
9285
9398
|
line += FS + 4;
|
|
9286
|
-
g.append('text').attr('x', x + PAD).attr('y', line).
|
|
9399
|
+
g.append('text').attr('x', x + PAD).attr('y', line).style('text-anchor', 'start')
|
|
9287
9400
|
.style('font', capFont).style('fill', ink).style('fill-opacity', 0.85)
|
|
9288
9401
|
.text(caption);
|
|
9289
9402
|
}
|
|
@@ -9878,7 +9991,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9878
9991
|
let gap = _state.nodeLabelGap;
|
|
9879
9992
|
let flip = radialDisplay() && labelFlip(d);
|
|
9880
9993
|
if (_state.circularDisplay && _radial) {
|
|
9881
|
-
t.
|
|
9994
|
+
t.style('text-anchor', flip ? 'end' : 'start')
|
|
9882
9995
|
.attr('x', flip ? -gap : gap).attr('dy', '0.32em');
|
|
9883
9996
|
if (_radialLabelsHorizontal) {
|
|
9884
9997
|
let p = radialXY(d.x, d.y);
|
|
@@ -9891,12 +10004,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
9891
10004
|
} else if (radialDisplay()) {
|
|
9892
10005
|
let r = radialRadius(far) - radialRadius(d.y) + gap;
|
|
9893
10006
|
t.attr('transform', 'rotate(' + labelAngleDeg(d) + ') translate(' + r + ',0)' + (flip ? ' rotate(180)' : ''))
|
|
9894
|
-
.
|
|
10007
|
+
.style('text-anchor', flip ? 'end' : 'start')
|
|
9895
10008
|
.attr('x', 0).attr('dy', '0.32em');
|
|
9896
10009
|
} else {
|
|
9897
10010
|
let column = (_state.phylogram && _state.alignPhylogram) ? _w : far;
|
|
9898
10011
|
t.attr('transform', null)
|
|
9899
|
-
.
|
|
10012
|
+
.style('text-anchor', 'start')
|
|
9900
10013
|
.attr('x', column - d.y + gap)
|
|
9901
10014
|
.attr('dy', (0.3 * fs) + 'px');
|
|
9902
10015
|
}
|
|
@@ -10370,7 +10483,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10370
10483
|
try {
|
|
10371
10484
|
_settings.onViewChange(state, encoded);
|
|
10372
10485
|
} catch (e) {
|
|
10373
|
-
|
|
10486
|
+
say('problem', 'error', ERROR + 'onViewChange threw: ' + (e && e.message ? e.message : e));
|
|
10374
10487
|
}
|
|
10375
10488
|
}
|
|
10376
10489
|
|
|
@@ -11163,6 +11276,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
11163
11276
|
let svg = clockSvg(body, 'svg', 'aptx-clock-svg');
|
|
11164
11277
|
svg.setAttribute('width', CLOCK_PLOT_W);
|
|
11165
11278
|
svg.setAttribute('height', CLOCK_PLOT_H);
|
|
11279
|
+
// inline as well: an attribute gives way to a page's rule for a bare "svg" (sizeTreeSvg)
|
|
11280
|
+
svg.style.width = CLOCK_PLOT_W + 'px';
|
|
11281
|
+
svg.style.height = CLOCK_PLOT_H + 'px';
|
|
11166
11282
|
svg.setAttribute('viewBox', '0 0 ' + CLOCK_PLOT_W + ' ' + CLOCK_PLOT_H);
|
|
11167
11283
|
svg.setAttribute('role', 'img');
|
|
11168
11284
|
svg.setAttribute('aria-label', 'Clock plot: each tip\'s date against its divergence from the root');
|
|
@@ -11489,7 +11605,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
11489
11605
|
let t = clockSvg(cp.axes, 'text', cls);
|
|
11490
11606
|
t.setAttribute('x', tx);
|
|
11491
11607
|
t.setAttribute('y', ty);
|
|
11492
|
-
t.
|
|
11608
|
+
t.style.setProperty('text-anchor', anchor);
|
|
11493
11609
|
t.textContent = s;
|
|
11494
11610
|
return t;
|
|
11495
11611
|
};
|
|
@@ -12282,8 +12398,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
12282
12398
|
made[id] = true;
|
|
12283
12399
|
let grad = d3.select(defs).append('linearGradient').attr('id', id)
|
|
12284
12400
|
.attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1);
|
|
12285
|
-
grad.append('stop').attr('offset', '0%').
|
|
12286
|
-
grad.append('stop').attr('offset', '100%').
|
|
12401
|
+
grad.append('stop').attr('offset', '0%').style('stop-color', forester.domainLighten(base, 0.12));
|
|
12402
|
+
grad.append('stop').attr('offset', '100%').style('stop-color', forester.domainDarken(base, 0.10));
|
|
12287
12403
|
}
|
|
12288
12404
|
return 'url(#' + id + ')';
|
|
12289
12405
|
}
|
|
@@ -12464,7 +12580,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
12464
12580
|
el.setAttribute('x', t.x);
|
|
12465
12581
|
el.setAttribute('y', t.y);
|
|
12466
12582
|
el.setAttribute('dy', '0.35em');
|
|
12467
|
-
el.
|
|
12583
|
+
el.style.setProperty('text-anchor', 'middle');
|
|
12468
12584
|
el.style.font = t.font;
|
|
12469
12585
|
el.style.fill = t.fill;
|
|
12470
12586
|
el.textContent = t.text;
|
|
@@ -13177,7 +13293,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13177
13293
|
el.setAttribute('x', t.x);
|
|
13178
13294
|
el.setAttribute('y', t.y);
|
|
13179
13295
|
el.setAttribute('dy', '0.35em');
|
|
13180
|
-
el.
|
|
13296
|
+
el.style.setProperty('text-anchor', 'middle');
|
|
13181
13297
|
el.style.font = t.font;
|
|
13182
13298
|
el.style.fill = t.fill;
|
|
13183
13299
|
el.textContent = t.text;
|
|
@@ -13592,7 +13708,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13592
13708
|
});
|
|
13593
13709
|
g.append('text')
|
|
13594
13710
|
.attr('x', x + (bar.px / 2)).attr('y', y - 6)
|
|
13595
|
-
.
|
|
13711
|
+
.style('text-anchor', 'middle')
|
|
13596
13712
|
.style('font', fs + 'px ' + FONT_DEFAULTS)
|
|
13597
13713
|
.style('fill', _state.labelColorDefault)
|
|
13598
13714
|
.text(bar.label);
|
|
@@ -13671,8 +13787,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
13671
13787
|
function gridLineAt(x) {
|
|
13672
13788
|
grid.append('line').attr('x1', x).attr('x2', x)
|
|
13673
13789
|
.attr('y1', 0).attr('y2', axisTop)
|
|
13674
|
-
.
|
|
13675
|
-
.
|
|
13790
|
+
.style('stroke', ink).style('stroke-opacity', 0.18)
|
|
13791
|
+
.style('stroke-width', 1);
|
|
13676
13792
|
}
|
|
13677
13793
|
|
|
13678
13794
|
let sc = info.type === 'calendar' ? -corr : corr;
|
|
@@ -13692,11 +13808,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
13692
13808
|
let p1 = polarXY(a, radialRadius(x1));
|
|
13693
13809
|
el = g.append('line').attr('x1', p0[0]).attr('y1', p0[1])
|
|
13694
13810
|
.attr('x2', p1[0]).attr('y2', p1[1])
|
|
13695
|
-
.
|
|
13811
|
+
.style('stroke', fill).style('stroke-width', thick);
|
|
13696
13812
|
} else {
|
|
13697
13813
|
el = g.append('rect').attr('x', Math.min(x0, x1)).attr('y', d.x - (thick / 2))
|
|
13698
13814
|
.attr('width', Math.max(1, Math.abs(x1 - x0))).attr('height', thick)
|
|
13699
|
-
.
|
|
13815
|
+
.style('fill', fill);
|
|
13700
13816
|
}
|
|
13701
13817
|
el.datum(d).attr('class', cls);
|
|
13702
13818
|
}
|
|
@@ -13715,7 +13831,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13715
13831
|
q = [x, d.x + 4];
|
|
13716
13832
|
}
|
|
13717
13833
|
g.append('line').attr('x1', p[0]).attr('y1', p[1]).attr('x2', q[0]).attr('y2', q[1])
|
|
13718
|
-
.
|
|
13834
|
+
.style('stroke', FOSSIL_BAR_COLOR).style('stroke-width', 1)
|
|
13719
13835
|
.datum(d).attr('class', 'aptx-fossil-cap');
|
|
13720
13836
|
}
|
|
13721
13837
|
|
|
@@ -13787,7 +13903,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13787
13903
|
let left = Math.min(x0, x1);
|
|
13788
13904
|
ax.append('rect').attr('x', left - 2).attr('y', axisTop - 4)
|
|
13789
13905
|
.attr('width', Math.abs(x1 - x0) + 4).attr('height', scaleAxisBottomReserve() + 4)
|
|
13790
|
-
.
|
|
13906
|
+
.style('fill', _state.backgroundColorDefault);
|
|
13791
13907
|
}
|
|
13792
13908
|
if (info.type === 'geologic') {
|
|
13793
13909
|
let rootAge = info.rootAge;
|
|
@@ -13828,11 +13944,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
13828
13944
|
}
|
|
13829
13945
|
ax.append('rect').attr('x', left).attr('y', rowY)
|
|
13830
13946
|
.attr('width', w).attr('height', TIME_BAND_ROW_H)
|
|
13831
|
-
.
|
|
13832
|
-
.
|
|
13947
|
+
.style('fill', iv.color)
|
|
13948
|
+
.style('stroke', ink).style('stroke-opacity', 0.5).style('stroke-width', 0.5);
|
|
13833
13949
|
if ((iv.name.length * 5.5) + 4 <= w) {
|
|
13834
13950
|
ax.append('text').attr('x', left + (w / 2)).attr('y', rowY + TIME_BAND_ROW_H - 3.5)
|
|
13835
|
-
.
|
|
13951
|
+
.style('text-anchor', 'middle')
|
|
13836
13952
|
.style('font-size', '9px')
|
|
13837
13953
|
.style('fill', forester.msaLetterInk(hexToRgbTriple(iv.color)))
|
|
13838
13954
|
.text(iv.name);
|
|
@@ -13843,7 +13959,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13843
13959
|
let rulerY = axisTop + (2 * TIME_BAND_ROW_H) + 4;
|
|
13844
13960
|
ax.append('line').attr('x1', xOfAge(rootAge)).attr('x2', xOfAge(youngBound))
|
|
13845
13961
|
.attr('y1', rulerY).attr('y2', rulerY)
|
|
13846
|
-
.
|
|
13962
|
+
.style('stroke', ink).style('stroke-width', 1);
|
|
13847
13963
|
let labels = [];
|
|
13848
13964
|
forester.maAxisTickValues(rootAge).forEach(function (v) {
|
|
13849
13965
|
if (v >= youngBound - 1e-9 && v <= rootAge + 1e-9) {
|
|
@@ -13863,7 +13979,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13863
13979
|
let x = xOfAge(l.age);
|
|
13864
13980
|
ax.append('line').attr('x1', x).attr('x2', x)
|
|
13865
13981
|
.attr('y1', rulerY).attr('y2', rulerY + 4)
|
|
13866
|
-
.
|
|
13982
|
+
.style('stroke', ink).style('stroke-width', 1);
|
|
13867
13983
|
let text = String(l.age);
|
|
13868
13984
|
let half = text.length * 2.8;
|
|
13869
13985
|
let ok = true;
|
|
@@ -13875,14 +13991,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
13875
13991
|
}
|
|
13876
13992
|
if (ok || l.priority > 0) {
|
|
13877
13993
|
ax.append('text').attr('x', x).attr('y', rulerY + 14)
|
|
13878
|
-
.
|
|
13994
|
+
.style('text-anchor', 'middle')
|
|
13879
13995
|
.style('font-size', '9px').style('fill', ink)
|
|
13880
13996
|
.text(text);
|
|
13881
13997
|
placed.push(x);
|
|
13882
13998
|
}
|
|
13883
13999
|
});
|
|
13884
14000
|
ax.append('text').attr('x', xOfAge(youngBound) + 8).attr('y', rulerY + 14)
|
|
13885
|
-
.
|
|
14001
|
+
.style('text-anchor', 'start')
|
|
13886
14002
|
.style('font-size', '9px').style('fill', ink)
|
|
13887
14003
|
.text('Ma');
|
|
13888
14004
|
} else {
|
|
@@ -13911,18 +14027,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
13911
14027
|
let rulerY = axisTop + 4;
|
|
13912
14028
|
ax.append('line').attr('x1', xOfYear(rootYear)).attr('x2', xOfYear(present))
|
|
13913
14029
|
.attr('y1', rulerY).attr('y2', rulerY)
|
|
13914
|
-
.
|
|
14030
|
+
.style('stroke', ink).style('stroke-width', 1);
|
|
13915
14031
|
let lastRight = -Infinity;
|
|
13916
14032
|
forester.calendarTickYears(rootYear, present).forEach(function (yv) {
|
|
13917
14033
|
let x = xOfYear(yv);
|
|
13918
14034
|
ax.append('line').attr('x1', x).attr('x2', x)
|
|
13919
14035
|
.attr('y1', rulerY).attr('y2', rulerY + 4)
|
|
13920
|
-
.
|
|
14036
|
+
.style('stroke', ink).style('stroke-width', 1);
|
|
13921
14037
|
let text = String(Math.round(yv));
|
|
13922
14038
|
let half = text.length * 2.8;
|
|
13923
14039
|
if ((x - half) >= lastRight + 4) {
|
|
13924
14040
|
ax.append('text').attr('x', x).attr('y', rulerY + 14)
|
|
13925
|
-
.
|
|
14041
|
+
.style('text-anchor', 'middle')
|
|
13926
14042
|
.style('font-size', '9px').style('fill', ink)
|
|
13927
14043
|
.text(text);
|
|
13928
14044
|
lastRight = x + half;
|
|
@@ -14016,15 +14132,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
14016
14132
|
return;
|
|
14017
14133
|
}
|
|
14018
14134
|
behind.append('path').attr('d', ring.innerRadius(a).outerRadius(b)())
|
|
14019
|
-
.
|
|
14135
|
+
.style('fill', iv.color).style('fill-opacity', 0.35);
|
|
14020
14136
|
named.push({r: (a + b) / 2, span: b - a, text: iv.name, color: iv.color});
|
|
14021
14137
|
});
|
|
14022
14138
|
if (grid) {
|
|
14023
14139
|
ticks.forEach(function (t) {
|
|
14024
14140
|
let r = radiusOf(t.value);
|
|
14025
14141
|
if (r > r0 + 0.5 && r < r1 - 0.5) {
|
|
14026
|
-
grid.append('circle').attr('r', r).
|
|
14027
|
-
.
|
|
14142
|
+
grid.append('circle').attr('r', r).style('fill', 'none')
|
|
14143
|
+
.style('stroke', ink).style('stroke-opacity', 0.18).style('stroke-width', 1);
|
|
14028
14144
|
}
|
|
14029
14145
|
});
|
|
14030
14146
|
}
|
|
@@ -14043,7 +14159,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14043
14159
|
let e0 = at(r0, 0);
|
|
14044
14160
|
let e1 = at(r1, 0);
|
|
14045
14161
|
ax.append('line').attr('x1', e0[0]).attr('y1', e0[1]).attr('x2', e1[0]).attr('y2', e1[1])
|
|
14046
|
-
.
|
|
14162
|
+
.style('stroke', ink).style('stroke-width', 1);
|
|
14047
14163
|
// numbers on one side of the ruler, interval names on the other; the
|
|
14048
14164
|
// headline value first, then the round ticks outward, none overlapping
|
|
14049
14165
|
function label(r, off, text, fill, backdrop) {
|
|
@@ -14053,9 +14169,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
14053
14169
|
let w = text.length * 5.5;
|
|
14054
14170
|
if (backdrop) {
|
|
14055
14171
|
t.append('rect').attr('x', -(w / 2) - 2).attr('y', -6).attr('width', w + 4).attr('height', 12)
|
|
14056
|
-
.attr('rx', 2).
|
|
14172
|
+
.attr('rx', 2).style('fill', _state.backgroundColorDefault).style('fill-opacity', 0.85);
|
|
14057
14173
|
}
|
|
14058
|
-
t.append('text').
|
|
14174
|
+
t.append('text').style('text-anchor', 'middle').style('dominant-baseline', 'central')
|
|
14059
14175
|
.style('font-size', '9px').style('fill', fill).text(text);
|
|
14060
14176
|
}
|
|
14061
14177
|
let side = flip ? -1 : 1;
|
|
@@ -14068,7 +14184,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14068
14184
|
let c0 = at(r, -4);
|
|
14069
14185
|
let c1 = at(r, 4);
|
|
14070
14186
|
ax.append('line').attr('x1', c0[0]).attr('y1', c0[1]).attr('x2', c1[0]).attr('y2', c1[1])
|
|
14071
|
-
.
|
|
14187
|
+
.style('stroke', ink).style('stroke-width', 1);
|
|
14072
14188
|
let half = t.text.length * 2.8;
|
|
14073
14189
|
for (let i = 0; i < placed.length; ++i) {
|
|
14074
14190
|
if (Math.abs(placed[i] - r) < (half * 2) + 6) {
|
|
@@ -14142,8 +14258,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
14142
14258
|
if (v > 0 && v < maxDist - 1e-9) {
|
|
14143
14259
|
grid.append('line').attr('x1', xOf(v)).attr('x2', xOf(v))
|
|
14144
14260
|
.attr('y1', 0).attr('y2', axisTop)
|
|
14145
|
-
.
|
|
14146
|
-
.
|
|
14261
|
+
.style('stroke', ink).style('stroke-opacity', 0.18)
|
|
14262
|
+
.style('stroke-width', 1);
|
|
14147
14263
|
}
|
|
14148
14264
|
});
|
|
14149
14265
|
}
|
|
@@ -14155,23 +14271,23 @@ function (root, d3, forester, phyloXml) {
|
|
|
14155
14271
|
// opaque, as the time axis's, so tips panned under it do not show
|
|
14156
14272
|
ax.append('rect').attr('x', x0 - 2).attr('y', axisTop - 4)
|
|
14157
14273
|
.attr('width', (x1 - x0) + 4).attr('height', reserve + 4)
|
|
14158
|
-
.
|
|
14274
|
+
.style('fill', _state.backgroundColorDefault);
|
|
14159
14275
|
let rulerY = axisTop + 4;
|
|
14160
14276
|
ax.append('line').attr('x1', x0).attr('x2', x1)
|
|
14161
14277
|
.attr('y1', rulerY).attr('y2', rulerY)
|
|
14162
|
-
.
|
|
14278
|
+
.style('stroke', ink).style('stroke-width', 1);
|
|
14163
14279
|
// every tick drawn, its number only where it clears the last one
|
|
14164
14280
|
let lastRight = -Infinity;
|
|
14165
14281
|
ticks.forEach(function (v) {
|
|
14166
14282
|
let x = xOf(v);
|
|
14167
14283
|
ax.append('line').attr('x1', x).attr('x2', x)
|
|
14168
14284
|
.attr('y1', rulerY).attr('y2', rulerY + 4)
|
|
14169
|
-
.
|
|
14285
|
+
.style('stroke', ink).style('stroke-width', 1);
|
|
14170
14286
|
let text = String(v);
|
|
14171
14287
|
let half = text.length * 2.8;
|
|
14172
14288
|
if ((x - half) >= lastRight + 4) {
|
|
14173
14289
|
ax.append('text').attr('x', x).attr('y', rulerY + 14)
|
|
14174
|
-
.
|
|
14290
|
+
.style('text-anchor', 'middle')
|
|
14175
14291
|
.style('font-size', '9px').style('fill', ink)
|
|
14176
14292
|
.text(text);
|
|
14177
14293
|
lastRight = x + half;
|
|
@@ -14181,7 +14297,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14181
14297
|
// past the ruler's end, and past the last number, which is
|
|
14182
14298
|
// centred on that end
|
|
14183
14299
|
ax.append('text').attr('x', Math.max(x1 + 8, lastRight + 6)).attr('y', rulerY + 14)
|
|
14184
|
-
.
|
|
14300
|
+
.style('text-anchor', 'start')
|
|
14185
14301
|
.style('font-size', '9px').style('fill', ink)
|
|
14186
14302
|
.text(unit);
|
|
14187
14303
|
}
|
|
@@ -14763,8 +14879,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14763
14879
|
_displayHeight = size.h;
|
|
14764
14880
|
// the canvas has to follow, or the layout is computed for one
|
|
14765
14881
|
// size and drawn on another
|
|
14766
|
-
_baseSvg.
|
|
14767
|
-
_baseSvg.attr('height', size.h);
|
|
14882
|
+
sizeTreeSvg(_baseSvg, size.w, size.h);
|
|
14768
14883
|
}
|
|
14769
14884
|
}
|
|
14770
14885
|
// Where the user dragged the legend to is their choice; a resize is no
|
|
@@ -15904,14 +16019,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
15904
16019
|
// The async clipboard API where the page allows it (a secure context);
|
|
15905
16020
|
// otherwise the old select-and-copy, so a page served over plain http
|
|
15906
16021
|
// still copies. Resolves to whether the text made it.
|
|
15907
|
-
|
|
16022
|
+
//
|
|
16023
|
+
// `host` is the open modal dialog the copy is made from, if any: under a
|
|
16024
|
+
// modal everything outside it is inert, a textarea on the body included.
|
|
16025
|
+
function copyText(text, host) {
|
|
15908
16026
|
let legacy = function () {
|
|
15909
16027
|
let ta = document.createElement('textarea');
|
|
15910
16028
|
ta.value = text;
|
|
15911
16029
|
ta.setAttribute('readonly', '');
|
|
15912
16030
|
ta.style.position = 'fixed';
|
|
15913
16031
|
ta.style.left = '-10000px';
|
|
15914
|
-
document.body.appendChild(ta);
|
|
16032
|
+
(host || document.body).appendChild(ta);
|
|
15915
16033
|
ta.select();
|
|
15916
16034
|
let ok = false;
|
|
15917
16035
|
try {
|
|
@@ -16650,9 +16768,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
16650
16768
|
case 'moon': body = glyphMoon(); break;
|
|
16651
16769
|
default: throw new Error('unknown control-panel glyph: ' + kind);
|
|
16652
16770
|
}
|
|
16653
|
-
|
|
16771
|
+
// written as an svg file would be, its paint in attributes; paintInline
|
|
16772
|
+
// makes inline styles of it, which no rule of a page outranks
|
|
16773
|
+
return paintInline('<svg class="aptx-glyph" viewBox="0 0 ' + w + ' 100" aria-hidden="true" focusable="false"'
|
|
16654
16774
|
+ ' fill="none" stroke="currentColor" stroke-width="' + sw + '"'
|
|
16655
|
-
+ ' stroke-linecap="' + cap + '" stroke-linejoin="' + join + '">' + body + '</svg>';
|
|
16775
|
+
+ ' stroke-linecap="' + cap + '" stroke-linejoin="' + join + '">' + body + '</svg>');
|
|
16656
16776
|
}
|
|
16657
16777
|
|
|
16658
16778
|
// The desktop's UNCOLLAPSE_ALL glyph (ControlButtonIcon.paintUncollapse),
|
|
@@ -16669,6 +16789,214 @@ function (root, d3, forester, phyloXml) {
|
|
|
16669
16789
|
+ '<path d="M56,20 H94 M56,50 H94 M56,80 H94"/>';
|
|
16670
16790
|
}
|
|
16671
16791
|
|
|
16792
|
+
// ===================== A host page's stylesheet =====================
|
|
16793
|
+
// The viewer is drawn inside somebody else's page, under that page's
|
|
16794
|
+
// stylesheet, and a rule of the host's wins wherever this sheet says
|
|
16795
|
+
// nothing. BV-BRC, 2026-10-09: ".Phylogeny svg { background:#FFFFFF }",
|
|
16796
|
+
// written when the only svg in that widget was the tree, put a white box
|
|
16797
|
+
// behind every icon of the panel. Measured after that, on pages nobody
|
|
16798
|
+
// had tried: a page set in capitals, or centred, set the dialogs and the
|
|
16799
|
+
// tooltip that way too; and under "* { box-sizing: border-box }", which
|
|
16800
|
+
// most pages have, the tooltip came out 22 px narrower.
|
|
16801
|
+
//
|
|
16802
|
+
// So the sheet opens by saying what it used to leave unsaid. These rules
|
|
16803
|
+
// come FIRST, and the general ones carry as little weight as will do:
|
|
16804
|
+
// every rule after them still has the last word. What they cannot do is
|
|
16805
|
+
// outrank a rule of the host's that is heavier still: one scoped by an
|
|
16806
|
+
// id, or by more classes than the rule here has, or marked !important.
|
|
16807
|
+
//
|
|
16808
|
+
// test/browser/host_css.html launches the viewer under real host sheets
|
|
16809
|
+
// and fails on any difference from a clean page. Something new that the
|
|
16810
|
+
// viewer hangs on the page goes into HOST_PROOF_ROOTS, and through it.
|
|
16811
|
+
const HOST_PROOF_ROOTS = ['.aptx-panel', '.aptx-dialog', '.aptx-node-menu', '.aptx-suggest', '.aptx-tip',
|
|
16812
|
+
'.aptx-busy', '.aptx-msa-nav'];
|
|
16813
|
+
// What paints a shape or sets a text, what shows or hides it, and what
|
|
16814
|
+
// would animate it: the properties a page's rule for a bare "path", "rect"
|
|
16815
|
+
// or "text" is likely to set. An svg ATTRIBUTE for one of these gives way
|
|
16816
|
+
// to any such rule, so the viewer states them as inline styles, and its
|
|
16817
|
+
// sheet says, for every shape of its svgs, "as a page that says nothing
|
|
16818
|
+
// has it" -- which for most of them is what the shape's parent has, and
|
|
16819
|
+
// so, in the end, what the svg itself has: the value beside each name,
|
|
16820
|
+
// where a parent hands it down. (Without one: not handed down; or stated
|
|
16821
|
+
// by the svg's own rule; or left to the page on purpose, as the cursor
|
|
16822
|
+
// and whether the thing is shown at all.)
|
|
16823
|
+
//
|
|
16824
|
+
// It also means that an ATTRIBUTE for one of these, on a shape of the
|
|
16825
|
+
// viewer's, no longer does anything, in any page: test/
|
|
16826
|
+
// visualization_test.js holds the drawing code to that, and test/browser/
|
|
16827
|
+
// host_css.html the page (with these rules taken out, nothing changes).
|
|
16828
|
+
// Markup with its paint in attributes goes through paintInline.
|
|
16829
|
+
//
|
|
16830
|
+
// Not here, and still attributes: where a shape is and how large (x, y,
|
|
16831
|
+
// d, r, transform), clip-path and mask.
|
|
16832
|
+
const SHAPE_PAINT = {
|
|
16833
|
+
'fill': '#000', 'fill-opacity': '1', 'fill-rule': 'nonzero',
|
|
16834
|
+
'stroke': 'none', 'stroke-width': '1', 'stroke-opacity': '1', 'stroke-dasharray': 'none',
|
|
16835
|
+
'stroke-dashoffset': '0', 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter', 'stroke-miterlimit': '4',
|
|
16836
|
+
'paint-order': 'normal', 'text-anchor': 'start', 'dominant-baseline': 'auto', 'shape-rendering': 'auto',
|
|
16837
|
+
'vector-effect': null, 'stop-color': null, 'stop-opacity': null, 'opacity': null, 'filter': null,
|
|
16838
|
+
'mix-blend-mode': null, 'display': null, 'visibility': null, 'cursor': null, 'pointer-events': null,
|
|
16839
|
+
'color': null, 'font': null, 'letter-spacing': null, 'word-spacing': null, 'text-transform': null,
|
|
16840
|
+
'text-decoration': null, 'text-shadow': null, 'text-rendering': null, 'white-space': null,
|
|
16841
|
+
'transition': null, 'animation': null
|
|
16842
|
+
};
|
|
16843
|
+
// What of that paint a FILE states as attributes again (paintAsAttributes,
|
|
16844
|
+
// for the SVG, PNG and PDF downloads). A file has no page to hold its
|
|
16845
|
+
// ground against, and not every program that opens one reads a style as
|
|
16846
|
+
// well as it reads an attribute: MuPDF takes a line's colour from a style
|
|
16847
|
+
// and not its opacity, and drew the faint lines of a time axis at full
|
|
16848
|
+
// strength. These are the paint properties SVG 1.1 has as attributes, and
|
|
16849
|
+
// vector-effect, which every release wrote as one. (paint-order is SVG
|
|
16850
|
+
// 2's: it stays the style it always was. So do the fonts.)
|
|
16851
|
+
const FILE_PAINT = ['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-width', 'stroke-opacity',
|
|
16852
|
+
'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit',
|
|
16853
|
+
'text-anchor', 'dominant-baseline', 'shape-rendering', 'vector-effect', 'stop-color', 'stop-opacity',
|
|
16854
|
+
'opacity'];
|
|
16855
|
+
// The paints that are a colour, each with the attribute a colour's alpha
|
|
16856
|
+
// goes to in a file. SVG 1.1 has no colour with an alpha and no
|
|
16857
|
+
// "transparent", and a program that knows neither draws black: MuPDF put
|
|
16858
|
+
// a black slab over a heat map (the surface it listens to the pointer
|
|
16859
|
+
// with) and over every row of a legend, and a solid bar where a node's
|
|
16860
|
+
// age interval is a translucent one.
|
|
16861
|
+
const FILE_ALPHA = {'fill': 'fill-opacity', 'stroke': 'stroke-opacity', 'stop-color': 'stop-opacity'};
|
|
16862
|
+
// every kind of element an svg of the viewer's is made of: the rule names
|
|
16863
|
+
// them one by one, so that a browser never tries it on anything else
|
|
16864
|
+
const SHAPE_ELEMENTS = ['g', 'path', 'line', 'rect', 'circle', 'ellipse', 'polygon', 'polyline', 'text', 'tspan',
|
|
16865
|
+
'textPath', 'use', 'image', 'a', 'defs', 'mask', 'clipPath', 'linearGradient', 'radialGradient', 'stop',
|
|
16866
|
+
'symbol', 'marker', 'pattern', 'title', 'desc'];
|
|
16867
|
+
// Weight without rank. A selector made of element names alone weighs less
|
|
16868
|
+
// than one class however many names it has, so eight names that are no
|
|
16869
|
+
// element's, inside a ":not", make a rule outweigh every rule a page
|
|
16870
|
+
// writes with element names only ("label", "fieldset > legend", "*"),
|
|
16871
|
+
// whichever sheet came first, and still give way to any rule for a class.
|
|
16872
|
+
const ELEMENT_WEIGHT = ':not(' + new Array(8).fill('aptx-none').join(' ') + ')';
|
|
16873
|
+
// Where the browser's own focus ring is what shows the focus (the menus
|
|
16874
|
+
// light the entry instead), and the controls there that draw their own.
|
|
16875
|
+
const FOCUS_RING_ROOTS = ['.aptx-panel', '.aptx-dialog', '.aptx-msa-nav'];
|
|
16876
|
+
// (Every rule of this sheet that sets an outline has its control here: the
|
|
16877
|
+
// rule that gives the ring back outweighs them all, so one left out gets
|
|
16878
|
+
// the browser's ring on top of its own. A colour swatch draws an outline
|
|
16879
|
+
// under the pointer, and keeps the browser's ring otherwise.)
|
|
16880
|
+
const OWN_FOCUS = ['.aptx-cp-hex', '.aptx-reps-input', '.aptx-reorder-row', '.aptx-reps-button',
|
|
16881
|
+
'.aptx-msa-nav-jump', '.aptx-clampable', '.aptx-cp-sw:hover', 'input[type=text]', 'select'];
|
|
16882
|
+
|
|
16883
|
+
// Svg markup as a file has it, its paint in attributes, with that paint
|
|
16884
|
+
// moved into inline styles: an attribute gives way to any rule of the page
|
|
16885
|
+
// the markup is put in, and to the viewer's own "as a page that says
|
|
16886
|
+
// nothing has it" (SHAPE_PAINT). The icons and the logo go through here.
|
|
16887
|
+
// An attribute may say what a style may not: a font given piece by piece,
|
|
16888
|
+
// a size without its unit.
|
|
16889
|
+
function paintInline(markup) {
|
|
16890
|
+
let names = Object.keys(SHAPE_PAINT).concat(['font-family', 'font-size', 'font-style', 'font-weight']);
|
|
16891
|
+
let paint = new RegExp('\\s(' + names.join('|') + ')="([^"]*)"', 'g');
|
|
16892
|
+
return markup.replace(/<([a-zA-Z]+)(\s[^<>]*?)(\/?)>/g, function (all, tag, attributes, closed) {
|
|
16893
|
+
let styles = [];
|
|
16894
|
+
let rest = attributes.replace(paint, function (attribute, name, value) {
|
|
16895
|
+
let unitless = (name === 'font-size' || name === 'letter-spacing' || name === 'word-spacing')
|
|
16896
|
+
&& /^-?[0-9.]+$/.test(value);
|
|
16897
|
+
styles.push(name + ':' + value + (unitless ? 'px' : ''));
|
|
16898
|
+
return '';
|
|
16899
|
+
});
|
|
16900
|
+
if (styles.length === 0) {
|
|
16901
|
+
return all;
|
|
16902
|
+
}
|
|
16903
|
+
// a style the element already has is said after, and so still has the last word
|
|
16904
|
+
let own = '';
|
|
16905
|
+
rest = rest.replace(/\sstyle="([^"]*)"/, function (attribute, value) {
|
|
16906
|
+
own = ';' + value;
|
|
16907
|
+
return '';
|
|
16908
|
+
});
|
|
16909
|
+
return '<' + tag + rest + ' style="' + styles.join(';') + own + '"' + closed + '>';
|
|
16910
|
+
});
|
|
16911
|
+
}
|
|
16912
|
+
|
|
16913
|
+
function hostProofCss() {
|
|
16914
|
+
let roots = HOST_PROOF_ROOTS.join(', ');
|
|
16915
|
+
// (the weight: BV-BRC's rule is ".Phylogeny svg", exactly as heavy as "svg.aptx-tree")
|
|
16916
|
+
let tree = 'svg.' + TREE_SVG_CLASS + ELEMENT_WEIGHT;
|
|
16917
|
+
// Every root and all that is in it (":where" adds no weight), svgs
|
|
16918
|
+
// apart: their shapes are drawn with attributes, which a rule saying
|
|
16919
|
+
// "revert" would undo. The part about the element itself stands last,
|
|
16920
|
+
// where a browser tries it first: a shape of the tree is turned away
|
|
16921
|
+
// at "svg *". (Measured, a whole page's styles worked out again: 8 ms
|
|
16922
|
+
// became 10 on a page of 20,000 elements around the viewer.)
|
|
16923
|
+
let everything = ':where(' + roots + ')' + ELEMENT_WEIGHT
|
|
16924
|
+
+ ', :where(' + roots + ') :not(svg, svg *)' + ELEMENT_WEIGHT;
|
|
16925
|
+
// Every shape of every svg of the viewer's: the tree's, the icons',
|
|
16926
|
+
// the logo's, the clock plot's. With the weight of a class, as the
|
|
16927
|
+
// svgs themselves: "rect:hover", "text:first-child" and ".chart text"
|
|
16928
|
+
// all weigh one class and an element. So a rule of this sheet for a
|
|
16929
|
+
// shape names two classes (".aptx-dialog .aptx-clock-grid"), or it
|
|
16930
|
+
// loses to this one.
|
|
16931
|
+
let svgs = ':is(svg.' + TREE_SVG_CLASS + ', ' + HOST_PROOF_ROOTS.map(function (r) {
|
|
16932
|
+
return r + ' svg';
|
|
16933
|
+
}).join(', ') + ')';
|
|
16934
|
+
let shapes = SHAPE_ELEMENTS.map(function (element) {
|
|
16935
|
+
return svgs + ' ' + element + ELEMENT_WEIGHT;
|
|
16936
|
+
}).join(', ');
|
|
16937
|
+
let properties = Object.keys(SHAPE_PAINT);
|
|
16938
|
+
// where a shape's paint starts from, for what it does not state: said on the svg
|
|
16939
|
+
let start = properties.filter(function (name) {
|
|
16940
|
+
return SHAPE_PAINT[name] !== null;
|
|
16941
|
+
}).map(function (name) {
|
|
16942
|
+
return name + ':' + SHAPE_PAINT[name] + ';';
|
|
16943
|
+
}).join(' ');
|
|
16944
|
+
return ''
|
|
16945
|
+
// 1. Everything starts from the browser's own defaults, as in a
|
|
16946
|
+
// page that says nothing: a reset's "* { box-sizing:border-box }"
|
|
16947
|
+
// or "button { font: inherit }", a framework's "label { font-
|
|
16948
|
+
// weight: 700 }". In a page that does say nothing this rule
|
|
16949
|
+
// changes nothing; and every rule of this sheet for a class,
|
|
16950
|
+
// here or below, still wins (ELEMENT_WEIGHT).
|
|
16951
|
+
+ everything + ' { all:revert; }'
|
|
16952
|
+
// An svg's own element likewise (its shapes are left alone),
|
|
16953
|
+
// and with the weight of a class: BV-BRC's rule was a class of
|
|
16954
|
+
// the page's and "svg". So an svg of the viewer's states its
|
|
16955
|
+
// size in the sheet or inline, never as attributes alone, and
|
|
16956
|
+
// its own rule names two classes (or it loses to this one).
|
|
16957
|
+
+ ':is(' + roots + ') svg' + ELEMENT_WEIGHT + ' { all:revert; ' + start + ' }'
|
|
16958
|
+
// And a shape of an svg, in what paints it (SHAPE_PAINT): it
|
|
16959
|
+
// states that inline, so what a page says of a "path" or a
|
|
16960
|
+
// "text" is set aside for what the browser has, which for most
|
|
16961
|
+
// of these is what the shape's parent has.
|
|
16962
|
+
+ shapes + ' { ' + properties.join(':revert; ') + ':revert; }'
|
|
16963
|
+
// Bootstrap 3 names tick boxes by their type, which outweighs
|
|
16964
|
+
// the rule above: the one tick box here that states no margin
|
|
16965
|
+
// of its own keeps the browser's, and a disabled one its cursor.
|
|
16966
|
+
+ '.aptx-dialog .aptx-reorder-check { margin:revert; }'
|
|
16967
|
+
+ '.aptx-dialog input[type=checkbox]:disabled, .aptx-dialog input[type=radio]:disabled { cursor:default; }'
|
|
16968
|
+
// 2. What a page hands down by inheritance stops at each root. The
|
|
16969
|
+
// values are the ones a page that says nothing has; a root's
|
|
16970
|
+
// own rule, further down, states its font, size and colour.
|
|
16971
|
+
// (Left to the page on purpose: direction, cursor, visibility,
|
|
16972
|
+
// pointer-events.)
|
|
16973
|
+
+ roots + ', ' + tree + ' {'
|
|
16974
|
+
+ ' color-scheme:light; accent-color:auto; caret-color:auto;'
|
|
16975
|
+
+ ' font:400 11px system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;'
|
|
16976
|
+
+ ' letter-spacing:normal; word-spacing:normal; text-align:start; text-align-last:auto; text-indent:0;'
|
|
16977
|
+
+ ' text-transform:none; text-shadow:none; text-rendering:auto;'
|
|
16978
|
+
+ ' -webkit-text-size-adjust:auto; text-size-adjust:auto;'
|
|
16979
|
+
+ ' white-space:normal; word-break:normal; overflow-wrap:normal; hyphens:manual; tab-size:8;'
|
|
16980
|
+
+ ' quotes:auto; list-style:disc outside none; }'
|
|
16981
|
+
// 3. The tree's own svg. Its shapes carry their paint inline; what
|
|
16982
|
+
// they inherit, and what a rule for a bare "svg" would set, is
|
|
16983
|
+
// said here. (Its width, height and font are inline: they are
|
|
16984
|
+
// the file's as well as the page's, see sizeTreeSvg and where
|
|
16985
|
+
// the svg is made. The background is the viewer's own rect.)
|
|
16986
|
+
// border-box, so that a border a page gives it on purpose fits
|
|
16987
|
+
// inside the size stated, in a page with a reset and without.
|
|
16988
|
+
// Nor does it move as a page's "* { transition: all .3s }" would
|
|
16989
|
+
// have it: the tree is redrawn frame by frame.
|
|
16990
|
+
+ tree + ' { color:#000; background:none; max-width:none; box-sizing:border-box;'
|
|
16991
|
+
+ ' vertical-align:baseline; transition:none; animation:none; ' + start + ' }'
|
|
16992
|
+
// "node" is every d3 example's class as well: ".node { font: 10px sans-serif; cursor: pointer }"
|
|
16993
|
+
+ tree + ' g.node { font:inherit; cursor:inherit; }'
|
|
16994
|
+
// 4. The browser's own focus ring, back on whatever does not draw
|
|
16995
|
+
// one of its own: ":focus { outline:0 }" is in many a sheet.
|
|
16996
|
+
+ ':is(' + FOCUS_RING_ROOTS.join(', ') + ') :focus-visible:not(' + OWN_FOCUS.join(', ') + ')'
|
|
16997
|
+
+ ' { outline:revert; outline-offset:revert; }';
|
|
16998
|
+
}
|
|
16999
|
+
|
|
16672
17000
|
function injectPanelStyles() {
|
|
16673
17001
|
loadPanelTheme();
|
|
16674
17002
|
if (document.getElementById(PANEL_STYLE_ID)) {
|
|
@@ -16680,7 +17008,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
16680
17008
|
+ ' --p-line:#27343f; --p-line-strong:#35434f; --p-surface2:#202d38;'
|
|
16681
17009
|
+ ' --p-accent:#57a6ff; --p-accent-ink:#9cc7ff; --p-accent-weak:rgba(87,166,255,0.18);'
|
|
16682
17010
|
+ ' --p-shadow-sm:0 1px 2px rgba(0,0,0,0.4);';
|
|
16683
|
-
let css =
|
|
17011
|
+
let css = hostProofCss()
|
|
16684
17012
|
+ '.aptx-panel {'
|
|
16685
17013
|
+ ' --p-bg: rgba(255,255,255,0.86); --p-ink:#1e2a35; --p-muted:#6b7a89; --p-faint:#93a3b2; --p-warn:#b15a0b;'
|
|
16686
17014
|
+ ' --p-line:#e3e9f0; --p-line-strong:#cad6e1; --p-surface2:#f3f6fa;'
|
|
@@ -16817,6 +17145,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
16817
17145
|
+ '.aptx-dialog-close:hover { background:var(--p-accent-weak); color:var(--p-accent-ink);'
|
|
16818
17146
|
+ ' border-color:var(--p-accent); }'
|
|
16819
17147
|
+ '.aptx-dialog-body { padding:10px 13px 12px; overflow:auto; }'
|
|
17148
|
+
// a dialog that scrolls gets the panel's own scrollbar: the
|
|
17149
|
+
// browser's default track is white in the dark theme too
|
|
17150
|
+
+ '.aptx-dialog-body::-webkit-scrollbar { width:9px; height:9px; }'
|
|
17151
|
+
+ '.aptx-dialog-body::-webkit-scrollbar-thumb { background:var(--p-line-strong); border-radius:9px; border:2px solid var(--p-bg); }'
|
|
17152
|
+
+ '.aptx-dialog-body::-webkit-scrollbar-track, .aptx-dialog-body::-webkit-scrollbar-corner { background:transparent; }'
|
|
16820
17153
|
+ '.aptx-cp-sv { position:relative; width:100%; box-sizing:border-box; height:104px; border-radius:7px;'
|
|
16821
17154
|
+ ' border:1px solid var(--p-line-strong); cursor:crosshair; touch-action:none; }'
|
|
16822
17155
|
+ '.aptx-cp-hue { position:relative; width:100%; box-sizing:border-box; height:13px; margin-top:9px;'
|
|
@@ -16871,7 +17204,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
16871
17204
|
+ '.aptx-reps { line-height:1.35; }'
|
|
16872
17205
|
+ '.aptx-reps-lead { margin:0 0 4px; }'
|
|
16873
17206
|
+ '.aptx-reps-choice { display:flex; align-items:center; gap:6px; padding:2px 0; cursor:pointer; }'
|
|
16874
|
-
|
|
17207
|
+
// (".aptx-dialog": Bootstrap 3's rule for a tick box weighs as much as ".aptx-reps-choice input")
|
|
17208
|
+
+ '.aptx-dialog .aptx-reps-choice input { accent-color:var(--p-accent); width:13px; height:13px; margin:0; flex:none; }'
|
|
16875
17209
|
+ '.aptx-reps-disabled { color:var(--p-faint); cursor:default; }'
|
|
16876
17210
|
+ '.aptx-reps-input { display:block; box-sizing:border-box; width:100%; font:inherit; color:var(--p-ink);'
|
|
16877
17211
|
+ ' background:var(--p-surface2); border:1px solid var(--p-line-strong); border-radius:6px; padding:4px 7px; }'
|
|
@@ -16920,18 +17254,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
16920
17254
|
// node's in the tree and is set on the point itself
|
|
16921
17255
|
+ '.aptx-dialog-drag { cursor:move; touch-action:none; user-select:none; -webkit-user-select:none; }'
|
|
16922
17256
|
+ '.aptx-clock.aptx-dialog-body { overflow:visible; }'
|
|
16923
|
-
+ '.aptx-clock-svg { display:block; touch-action:none; user-select:none; -webkit-user-select:none; }'
|
|
16924
|
-
+ '.aptx-clock-svg text { font-size:10px; fill:var(--p-muted); font-variant-numeric:tabular-nums; }'
|
|
16925
|
-
+ '.aptx-clock-svg text.aptx-clock-title { font-size:10.5px; fill:var(--p-ink); }'
|
|
16926
|
-
+ '.aptx-clock-ground { fill:var(--p-surface2); }'
|
|
16927
|
-
+ '.aptx-clock-grid { stroke:var(--p-line); stroke-width:1; shape-rendering:crispEdges; }'
|
|
16928
|
-
+ '.aptx-clock-frame { stroke:var(--p-line-strong); stroke-width:1; shape-rendering:crispEdges; }'
|
|
16929
|
-
+ '.aptx-clock-line { stroke:var(--p-accent); stroke-width:1.6; }'
|
|
16930
|
-
+ '.aptx-clock-internal circle { fill:var(--p-faint); fill-opacity:0.55; }'
|
|
16931
|
-
+ '.aptx-clock-tips circle { fill:var(--p-ink); fill-opacity:0.8; }'
|
|
16932
|
-
+ '.aptx-clock-svg circle.aptx-clock-hit { fill-opacity:1; stroke-width:1.2; }'
|
|
16933
|
-
+ '.aptx-clock-ring { fill:none; stroke:var(--p-accent); stroke-width:1.8; pointer-events:none; }'
|
|
16934
|
-
+ '.aptx-clock-box { fill:var(--p-accent-weak); stroke:var(--p-accent); stroke-width:1;'
|
|
17257
|
+
+ '.aptx-dialog .aptx-clock-svg { display:block; touch-action:none; user-select:none; -webkit-user-select:none; }'
|
|
17258
|
+
+ '.aptx-dialog .aptx-clock-svg text { font-size:10px; fill:var(--p-muted); font-variant-numeric:tabular-nums; }'
|
|
17259
|
+
+ '.aptx-dialog .aptx-clock-svg text.aptx-clock-title { font-size:10.5px; fill:var(--p-ink); }'
|
|
17260
|
+
+ '.aptx-dialog .aptx-clock-ground { fill:var(--p-surface2); }'
|
|
17261
|
+
+ '.aptx-dialog .aptx-clock-grid { stroke:var(--p-line); stroke-width:1; shape-rendering:crispEdges; }'
|
|
17262
|
+
+ '.aptx-dialog .aptx-clock-frame { stroke:var(--p-line-strong); stroke-width:1; shape-rendering:crispEdges; }'
|
|
17263
|
+
+ '.aptx-dialog .aptx-clock-line { stroke:var(--p-accent); stroke-width:1.6; }'
|
|
17264
|
+
+ '.aptx-dialog .aptx-clock-internal circle { fill:var(--p-faint); fill-opacity:0.55; }'
|
|
17265
|
+
+ '.aptx-dialog .aptx-clock-tips circle { fill:var(--p-ink); fill-opacity:0.8; }'
|
|
17266
|
+
+ '.aptx-dialog .aptx-clock-svg circle.aptx-clock-hit { fill-opacity:1; stroke-width:1.2; }'
|
|
17267
|
+
+ '.aptx-dialog .aptx-clock-ring { fill:none; stroke:var(--p-accent); stroke-width:1.8; pointer-events:none; }'
|
|
17268
|
+
+ '.aptx-dialog .aptx-clock-box { fill:var(--p-accent-weak); stroke:var(--p-accent); stroke-width:1;'
|
|
16935
17269
|
+ ' shape-rendering:crispEdges; pointer-events:none; }'
|
|
16936
17270
|
+ '.aptx-clock-options { display:flex; align-items:center; gap:14px; margin:6px 0 8px; }'
|
|
16937
17271
|
+ '.aptx-clock-options .aptx-clock-deselect { margin-left:auto; height:22px; padding:0 9px; }'
|
|
@@ -16974,13 +17308,30 @@ function (root, d3, forester, phyloXml) {
|
|
|
16974
17308
|
+ '.aptx-panel .' + PROGNAMELINK + ' { background:none; padding:0; margin:0; cursor:pointer;'
|
|
16975
17309
|
+ ' font-family:inherit; font-weight:inherit; letter-spacing:inherit; }'
|
|
16976
17310
|
+ '.aptx-panel .' + PROGNAMELINK + ':hover { text-decoration:underline; }'
|
|
17311
|
+
+ '.aptx-doctor-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:var(--p-muted); vertical-align:middle; }'
|
|
17312
|
+
+ '.aptx-doctor-dot[hidden] { display:none; }'
|
|
17313
|
+
+ '.aptx-panel .' + PROGNAMELINK + ' .aptx-doctor-dot { margin-left:6px; }'
|
|
17314
|
+
+ '.aptx-about .aptx-doctor-dot { margin-right:6px; }'
|
|
17315
|
+
+ '.aptx-dialog-body.aptx-doctor { max-height:min(70vh,560px); }'
|
|
17316
|
+
+ '.aptx-doctor .aptx-dialog-key { flex-basis:34%; }'
|
|
17317
|
+
+ '.aptx-doctor-head { display:flex; align-items:center; gap:10px; margin-bottom:6px; }'
|
|
17318
|
+
+ '.aptx-doctor-status { flex:1 1 auto; min-width:0; font-weight:650; }'
|
|
17319
|
+
+ '.aptx-doctor-status.aptx-doctor-bad { box-shadow:inset 3px 0 0 var(--p-warn); padding-left:9px; }'
|
|
17320
|
+
+ '.aptx-doctor-item { position:relative; padding:2px 0 2px 14px; line-height:1.35; overflow-wrap:anywhere; }'
|
|
17321
|
+
+ '.aptx-doctor-item::before { content:""; position:absolute; left:2px; top:0.62em; width:6px; height:6px; border-radius:50%; background:var(--p-faint); }'
|
|
17322
|
+
+ '.aptx-doctor-item.aptx-doctor-bad::before { background:var(--p-warn); }'
|
|
17323
|
+
+ '.aptx-doctor-item.aptx-doctor-note { color:var(--p-muted); }'
|
|
17324
|
+
+ '.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); }'
|
|
17325
|
+
+ '.aptx-doctor-detail { display:block; color:var(--p-muted); line-height:1.35; }'
|
|
17326
|
+
+ '.aptx-doctor .aptx-dialog-line.aptx-doctor-bad .aptx-doctor-value { color:var(--p-warn); font-weight:600; }'
|
|
17327
|
+
+ '.aptx-doctor-copy { flex:none; }'
|
|
16977
17328
|
+ '.aptx-dialog-body.aptx-about { padding-top:13px; }'
|
|
16978
17329
|
// the label column is narrower here than for node data: these values
|
|
16979
17330
|
// are URLs, and they should not have to wrap mid-word
|
|
16980
17331
|
+ '.aptx-about .aptx-dialog-key { flex-basis:31%; }'
|
|
16981
17332
|
+ '.aptx-about-head { display:flex; align-items:center; gap:12px; margin-bottom:10px; }'
|
|
16982
17333
|
+ '.aptx-about-logo { flex:none; width:56px; height:56px; }'
|
|
16983
|
-
+ '.aptx-about-logo svg { width:100%; height:100%; display:block; }'
|
|
17334
|
+
+ '.aptx-dialog .aptx-about-logo svg { width:100%; height:100%; display:block; }'
|
|
16984
17335
|
+ '.aptx-about-name { font-size:15px; font-weight:650; letter-spacing:-0.01em; }'
|
|
16985
17336
|
+ '.aptx-about-version { color:var(--p-muted); }'
|
|
16986
17337
|
+ '.aptx-about-blurb { margin:0 0 11px; color:var(--p-muted); }'
|
|
@@ -17006,6 +17357,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
17006
17357
|
+ '.aptx-panel legend { float:none; width:auto; padding:0; margin:0 0 5px; font-size:9px; font-weight:700; letter-spacing:0.09em; text-transform:uppercase; color:var(--p-faint); }'
|
|
17007
17358
|
+ '.aptx-panel label { cursor:pointer; }'
|
|
17008
17359
|
+ '.aptx-panel input[type=checkbox],.aptx-panel input[type=radio] { accent-color:var(--p-accent); width:13px; height:13px; vertical-align:-2px; margin:0 4px 0 0; }'
|
|
17360
|
+
+ '.aptx-panel input[type=checkbox]:disabled,.aptx-panel input[type=radio]:disabled { cursor:default; }'
|
|
17009
17361
|
// checkbox/radio + label as one item (used by the Display Data grid and the inline P/A/C and search-option rows)
|
|
17010
17362
|
// every item carries an invisible box, so the one that lights up
|
|
17011
17363
|
// (Auto-hide Labels while it is hiding) does not shift its neighbours
|
|
@@ -17093,9 +17445,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
17093
17445
|
+ '.aptx-panel .aptx-searchnav { align-items:center; gap:4px; margin:2px 0 4px; }'
|
|
17094
17446
|
+ '.aptx-panel .aptx-searchnav span { flex:1 1 auto; text-align:center; font-weight:600; font-size:11px; color:var(--p-ink); }'
|
|
17095
17447
|
+ '.aptx-panel .aptx-searchnav .aptx-gbtn:last-child { margin-right:0; }'
|
|
17096
|
-
+ '.aptx-found-halo { opacity:' + HALO_REST_OPACITY + '; transform-box:fill-box; transform-origin:center; animation:aptx-halo-pulse 1.3s ease-in-out infinite; }'
|
|
17448
|
+
+ 'svg.' + TREE_SVG_CLASS + ' .aptx-found-halo { opacity:' + HALO_REST_OPACITY + '; transform-box:fill-box; transform-origin:center; animation:aptx-halo-pulse 1.3s ease-in-out infinite; }'
|
|
17097
17449
|
+ '@keyframes aptx-halo-pulse { 0%,100% { transform:scale(1); opacity:' + HALO_REST_OPACITY + '; } 50% { transform:scale(2.5); opacity:0.12; } }'
|
|
17098
|
-
+ '@media (prefers-reduced-motion: reduce) { .aptx-found-halo { animation:none; } }'
|
|
17450
|
+
+ '@media (prefers-reduced-motion: reduce) { svg.' + TREE_SVG_CLASS + ' .aptx-found-halo { animation:none; } }'
|
|
17099
17451
|
+ '.aptx-panel .aptx-zoomgrid { display:flex; flex-direction:column; align-items:stretch; }'
|
|
17100
17452
|
+ '.aptx-panel .aptx-zoomgrid > input[type=button] { width:100%; margin-right:0; }'
|
|
17101
17453
|
+ '.aptx-panel .aptx-zoomrow { display:flex; }'
|
|
@@ -17255,6 +17607,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
17255
17607
|
e.stopPropagation();
|
|
17256
17608
|
showAboutDialog();
|
|
17257
17609
|
});
|
|
17610
|
+
// config warnings are recorded before this header exists
|
|
17611
|
+
syncDoctorDot();
|
|
17612
|
+
// ... and a library can arrive after it (see syncDoctorDot)
|
|
17613
|
+
let panel = header.closest('.aptx-panel');
|
|
17614
|
+
if (panel) {
|
|
17615
|
+
panel.addEventListener('pointerenter', syncDoctorDot);
|
|
17616
|
+
}
|
|
17617
|
+
if (document.readyState !== 'complete') {
|
|
17618
|
+
window.addEventListener('load', syncDoctorDot, {once: true});
|
|
17619
|
+
}
|
|
17258
17620
|
|
|
17259
17621
|
let themeBtn = document.createElement('button');
|
|
17260
17622
|
themeBtn.type = 'button';
|
|
@@ -18088,10 +18450,693 @@ function (root, d3, forester, phyloXml) {
|
|
|
18088
18450
|
shell.dialog.showModal();
|
|
18089
18451
|
}
|
|
18090
18452
|
|
|
18453
|
+
// ===================== Diagnostics =====================
|
|
18454
|
+
// "Dependencies are checked when used, never at load time" makes the
|
|
18455
|
+
// viewer hard to break and easy to run half-equipped without knowing: on
|
|
18456
|
+
// BV-BRC it ran for weeks with no PDF or PNG export, a retired config key
|
|
18457
|
+
// and a forced visualization, and every one of those was found by reading
|
|
18458
|
+
// the site's source (Christian, 2026-10-09: a "check engine light" and a
|
|
18459
|
+
// doctor behind it). This is the one place that says what is loaded, what
|
|
18460
|
+
// that switches off, and what the launch complained about.
|
|
18461
|
+
//
|
|
18462
|
+
// Two rules decide what is a PROBLEM (lights the dot) and what is only
|
|
18463
|
+
// stated:
|
|
18464
|
+
// * an optional library that is simply absent is a site's choice, and
|
|
18465
|
+
// is stated ("PDF export: off"), never flagged;
|
|
18466
|
+
// * something nobody chooses is a problem: half of a pair (jspdf
|
|
18467
|
+
// without svg2pdf.js), a library from the 2.x days under its old
|
|
18468
|
+
// global, a d3 that is not 7, forester.js from another release, a
|
|
18469
|
+
// phyloxml that no longer writes what this release expects, and every
|
|
18470
|
+
// warning or error the launch printed about its config.
|
|
18471
|
+
// Only d3, jsPDF and forester state a version, so everything else is
|
|
18472
|
+
// judged by what it can do.
|
|
18473
|
+
//
|
|
18474
|
+
// Nothing in here may throw: every check runs in its own try, and a check
|
|
18475
|
+
// that fails says so in its own row.
|
|
18476
|
+
|
|
18477
|
+
// phyloxml states no version, so a small tree goes through it and what
|
|
18478
|
+
// comes back is read for what an OLDER release is known to do. Each of
|
|
18479
|
+
// these is a named fault, and each published release before 1.1.7 shows
|
|
18480
|
+
// at least one (measured on 1.0.2, 1.1.3, 1.1.4, 1.1.5 and 1.1.6):
|
|
18481
|
+
// * it drops the domain architecture, or a <phylogeny>'s own property
|
|
18482
|
+
// (before 1.1.5 / 1.1.3): a saved tree LOSES data;
|
|
18483
|
+
// * it writes a style colour as it came, or the style property before
|
|
18484
|
+
// the node's other properties (before 1.1.7): the saved file differs
|
|
18485
|
+
// from the desktop program's.
|
|
18486
|
+
// Whether the output is byte for byte what 1.1.7 writes is looked at too,
|
|
18487
|
+
// but a difference there alone is only a NOTE. package.json admits any
|
|
18488
|
+
// 1.1.x from 1.1.7 up, a release of this viewer cannot be corrected
|
|
18489
|
+
// after it is published, and a newer phyloxml that lays a file out
|
|
18490
|
+
// differently must not light a dot on every page that has it. (A first
|
|
18491
|
+
// version demanded the bytes and called every difference "an older
|
|
18492
|
+
// copy".)
|
|
18493
|
+
const PHYLOXML_PROBE_IN = '<phyloxml xmlns="http://www.phyloxml.org"><phylogeny rooted="true"><clade>'
|
|
18494
|
+
+ '<clade><name>a</name><branch_length>1</branch_length>'
|
|
18495
|
+
+ '<sequence><domain_architecture length="9"><domain from="1" to="5" confidence="0.1">D</domain>'
|
|
18496
|
+
+ '</domain_architecture></sequence>'
|
|
18497
|
+
+ '<property ref="style:font_color" datatype="xsd:token" applies_to="node">#CE1616</property>'
|
|
18498
|
+
+ '<property ref="x:y" datatype="xsd:string" applies_to="node">v</property></clade>'
|
|
18499
|
+
+ '<clade><name>b</name><branch_length>2</branch_length></clade></clade>'
|
|
18500
|
+
+ '<property ref="p:q" datatype="xsd:string" applies_to="phylogeny">w</property></phylogeny></phyloxml>';
|
|
18501
|
+
const PHYLOXML_PROBE_OUT = ['<?xml version="1.0" encoding="UTF-8"?>',
|
|
18502
|
+
'<phyloxml xmlns="http://www.phyloxml.org">',
|
|
18503
|
+
'<phylogeny rooted="true" rerootable="true">',
|
|
18504
|
+
' <clade>',
|
|
18505
|
+
' <clade>',
|
|
18506
|
+
' <name>a</name>',
|
|
18507
|
+
' <branch_length>1</branch_length>',
|
|
18508
|
+
' <sequence>',
|
|
18509
|
+
' <domain_architecture length="9">',
|
|
18510
|
+
' <domain from="1" to="5" confidence="0.1">D</domain>',
|
|
18511
|
+
' </domain_architecture>',
|
|
18512
|
+
' </sequence>',
|
|
18513
|
+
' <property ref="x:y" datatype="xsd:string" applies_to="node">v</property>',
|
|
18514
|
+
' <property ref="style:font_color" datatype="xsd:token" applies_to="node">#ce1616</property>',
|
|
18515
|
+
' </clade>',
|
|
18516
|
+
' <clade>',
|
|
18517
|
+
' <name>b</name>',
|
|
18518
|
+
' <branch_length>2</branch_length>',
|
|
18519
|
+
' </clade>',
|
|
18520
|
+
' </clade>',
|
|
18521
|
+
' <property ref="p:q" datatype="xsd:string" applies_to="phylogeny">w</property>',
|
|
18522
|
+
'</phylogeny>',
|
|
18523
|
+
'</phyloxml>'].join('\n');
|
|
18524
|
+
const PHYLOXML_EXPECTED = '1.1.7';
|
|
18525
|
+
const D3_TESTED_MINOR = 9; // package.json: d3 ^7.9.0
|
|
18526
|
+
const JSPDF_TESTED_MAJOR = 4; // package.json: jspdf ^4.2.1
|
|
18527
|
+
|
|
18528
|
+
// The probe's answer for the library object it was asked about: the
|
|
18529
|
+
// dot asks often, and the answer cannot change.
|
|
18530
|
+
// threw / stage reading ('read') or writing ('write') failed, with the message
|
|
18531
|
+
// lost what a saved tree no longer has
|
|
18532
|
+
// old what is written as releases before 1.1.7 wrote it
|
|
18533
|
+
// same the output is byte for byte 1.1.7's
|
|
18534
|
+
let _phyloXmlProbe = null;
|
|
18535
|
+
function probePhyloXml(px) {
|
|
18536
|
+
if (_phyloXmlProbe && _phyloXmlProbe.lib === px) {
|
|
18537
|
+
return _phyloXmlProbe.result;
|
|
18538
|
+
}
|
|
18539
|
+
let result = {threw: null, stage: null, lost: [], old: [], same: false};
|
|
18540
|
+
let message = function (e) {
|
|
18541
|
+
return String(e && e.message ? e.message : e);
|
|
18542
|
+
};
|
|
18543
|
+
let tree = null;
|
|
18544
|
+
try {
|
|
18545
|
+
tree = px.parse(PHYLOXML_PROBE_IN, {trim: true, normalize: true})[0];
|
|
18546
|
+
} catch (e) {
|
|
18547
|
+
result.threw = message(e);
|
|
18548
|
+
result.stage = 'read';
|
|
18549
|
+
}
|
|
18550
|
+
let written = null;
|
|
18551
|
+
if (!result.threw) {
|
|
18552
|
+
try {
|
|
18553
|
+
written = String(px.toPhyloXML(tree, 9));
|
|
18554
|
+
} catch (e) {
|
|
18555
|
+
result.threw = message(e);
|
|
18556
|
+
result.stage = 'write';
|
|
18557
|
+
}
|
|
18558
|
+
}
|
|
18559
|
+
if (written !== null) {
|
|
18560
|
+
result.same = written === PHYLOXML_PROBE_OUT;
|
|
18561
|
+
if (!/<domain from="1" to="5"[^>]*>D<\/domain>/.test(written)) {
|
|
18562
|
+
result.lost.push('protein domains');
|
|
18563
|
+
}
|
|
18564
|
+
if (written.indexOf('ref="p:q"') < 0) {
|
|
18565
|
+
result.lost.push('a tree\'s own properties');
|
|
18566
|
+
}
|
|
18567
|
+
if (written.indexOf('ref="x:y"') < 0) {
|
|
18568
|
+
result.lost.push('node properties');
|
|
18569
|
+
}
|
|
18570
|
+
let style = written.indexOf('ref="style:font_color"');
|
|
18571
|
+
if (style < 0) {
|
|
18572
|
+
result.lost.push('node styles');
|
|
18573
|
+
} else {
|
|
18574
|
+
if (written.indexOf('>#ce1616<') < 0) {
|
|
18575
|
+
result.old.push('a style colour as it came, not in lower case');
|
|
18576
|
+
}
|
|
18577
|
+
if (written.indexOf('ref="x:y"') > style) {
|
|
18578
|
+
result.old.push('a node\'s style before its other properties');
|
|
18579
|
+
}
|
|
18580
|
+
}
|
|
18581
|
+
}
|
|
18582
|
+
_phyloXmlProbe = {lib: px, result: result};
|
|
18583
|
+
return result;
|
|
18584
|
+
}
|
|
18585
|
+
|
|
18586
|
+
// One library's row. `judge` returns {loaded, version, status, detail};
|
|
18587
|
+
// status is 'ok', 'absent' (optional and not there: stated), 'note' or
|
|
18588
|
+
// 'problem'.
|
|
18589
|
+
function doctorLibrary(name, role, judge) {
|
|
18590
|
+
let row = {name: name, role: role, loaded: false, version: null, status: 'problem', detail: ''};
|
|
18591
|
+
try {
|
|
18592
|
+
let j = judge();
|
|
18593
|
+
row.loaded = !!j.loaded;
|
|
18594
|
+
row.version = j.version || null;
|
|
18595
|
+
row.status = j.status;
|
|
18596
|
+
row.detail = j.detail || '';
|
|
18597
|
+
} catch (e) {
|
|
18598
|
+
row.detail = 'this check failed: ' + (e && e.message ? e.message : e);
|
|
18599
|
+
}
|
|
18600
|
+
return row;
|
|
18601
|
+
}
|
|
18602
|
+
|
|
18603
|
+
function doctorLibraries() {
|
|
18604
|
+
let g = root || {};
|
|
18605
|
+
// without a document there is nothing to launch into: the parsers
|
|
18606
|
+
// work with no d3 at all, so its absence there is not a fault
|
|
18607
|
+
let inBrowser = typeof g.document !== 'undefined';
|
|
18608
|
+
let libs = [];
|
|
18609
|
+
|
|
18610
|
+
libs.push(doctorLibrary('forester.js', 'required', function () {
|
|
18611
|
+
let f = forester || g.forester;
|
|
18612
|
+
if (!f) {
|
|
18613
|
+
return {loaded: false, status: 'problem', detail: 'not loaded: nothing can be read or drawn'};
|
|
18614
|
+
}
|
|
18615
|
+
if (f.VERSION === VERSION) {
|
|
18616
|
+
return {loaded: true, version: f.VERSION, status: 'ok'};
|
|
18617
|
+
}
|
|
18618
|
+
return {loaded: true, version: f.VERSION || null, status: 'problem',
|
|
18619
|
+
detail: (f.VERSION ? 'forester.js is ' + f.VERSION : 'forester.js states no version, so it is an older file')
|
|
18620
|
+
+ ', archaeopteryx.js is ' + VERSION + ': the two must come from the same release'};
|
|
18621
|
+
}));
|
|
18622
|
+
|
|
18623
|
+
libs.push(doctorLibrary('d3', 'required', function () {
|
|
18624
|
+
// THE d3 THE VIEWER USES, chosen as requireD3 chooses it: the one
|
|
18625
|
+
// it was handed at load, and the page's global only where that
|
|
18626
|
+
// one is missing or unusable. (A first version looked at the
|
|
18627
|
+
// global first and could report on a d3 nothing was drawn with.)
|
|
18628
|
+
let d = (g.d3 && (!d3 || typeof d3.zoom !== 'function')) ? g.d3 : d3;
|
|
18629
|
+
if (!d) {
|
|
18630
|
+
return inBrowser
|
|
18631
|
+
? {loaded: false, status: 'problem', detail: 'not loaded: launch() fails without d3 version 7'}
|
|
18632
|
+
: {loaded: false, status: 'absent', detail: 'not loaded: only launch() needs it'};
|
|
18633
|
+
}
|
|
18634
|
+
let usable = typeof d.zoom === 'function' && typeof d.cluster === 'function';
|
|
18635
|
+
let version = typeof d.version === 'string' ? d.version : null;
|
|
18636
|
+
if (version === null) {
|
|
18637
|
+
// d3's ES-module build exports no `version`: a bundler's
|
|
18638
|
+
// `import * as d3`, jsdelivr's +esm, Node's require('d3'). It
|
|
18639
|
+
// is judged by what only d3 7 has -- InternMap arrived with
|
|
18640
|
+
// d3-array 3, which is d3 7.0.0 -- and never called outdated
|
|
18641
|
+
// for not saying its number.
|
|
18642
|
+
if (usable && typeof d.InternMap === 'function') {
|
|
18643
|
+
return {loaded: true, status: 'ok', detail: 'states no version (an ES-module build); has what d3 7 has'};
|
|
18644
|
+
}
|
|
18645
|
+
return {loaded: true, status: inBrowser ? 'problem' : 'absent',
|
|
18646
|
+
detail: inBrowser ? 'states no version and lacks what d3 7 has: Archaeopteryx.js needs d3 version 7'
|
|
18647
|
+
: 'not usable here as d3 version 7: only launch() needs it'};
|
|
18648
|
+
}
|
|
18649
|
+
let parts = version.split('.').map(Number);
|
|
18650
|
+
if (parts[0] !== 7 || !usable) {
|
|
18651
|
+
return {loaded: true, version: version, status: inBrowser ? 'problem' : 'absent',
|
|
18652
|
+
detail: inBrowser ? 'Archaeopteryx.js needs d3 version 7'
|
|
18653
|
+
: 'not usable here as d3 version 7: only launch() needs it'};
|
|
18654
|
+
}
|
|
18655
|
+
if (parts[1] < D3_TESTED_MINOR) {
|
|
18656
|
+
return {loaded: true, version: version, status: 'note',
|
|
18657
|
+
detail: 'older than the 7.' + D3_TESTED_MINOR + ' this release is tested with'};
|
|
18658
|
+
}
|
|
18659
|
+
return {loaded: true, version: version, status: 'ok'};
|
|
18660
|
+
}));
|
|
18661
|
+
|
|
18662
|
+
let px = null;
|
|
18663
|
+
let probe = null;
|
|
18664
|
+
libs.push(doctorLibrary('phyloxml', 'required', function () {
|
|
18665
|
+
px = phyloXml || g.phyloXml;
|
|
18666
|
+
probe = px ? probePhyloXml(px) : null;
|
|
18667
|
+
if (!px) {
|
|
18668
|
+
return {loaded: false, status: 'problem',
|
|
18669
|
+
detail: 'not loaded: phyloXML cannot be read, and the phyloXML download fails'};
|
|
18670
|
+
}
|
|
18671
|
+
if (probe.threw) {
|
|
18672
|
+
// on a page phyloxml takes sax from the global, and a missing
|
|
18673
|
+
// one is the usual reason reading fails; anywhere else, and
|
|
18674
|
+
// for any other failure, the message itself is the finding
|
|
18675
|
+
return {loaded: true, status: 'problem', detail: (probe.stage === 'read' && inBrowser && !g.sax)
|
|
18676
|
+
? 'cannot read phyloXML: sax.js is not loaded'
|
|
18677
|
+
: (probe.stage === 'read' ? 'reading' : 'writing') + ' a small tree failed: ' + probe.threw};
|
|
18678
|
+
}
|
|
18679
|
+
if (probe.lost.length > 0) {
|
|
18680
|
+
return {loaded: true, status: 'problem', detail: 'an older copy: saving a tree as phyloXML drops '
|
|
18681
|
+
+ probe.lost.join(', ') + ' (phyloxml ' + PHYLOXML_EXPECTED + ' keeps them)'};
|
|
18682
|
+
}
|
|
18683
|
+
if (probe.old.length > 0) {
|
|
18684
|
+
return {loaded: true, status: 'problem', detail: 'an older copy: it writes ' + probe.old.join(', and ')
|
|
18685
|
+
+ ', so a saved tree differs from the desktop program\'s file (phyloxml ' + PHYLOXML_EXPECTED
|
|
18686
|
+
+ ' does not)'};
|
|
18687
|
+
}
|
|
18688
|
+
if (!probe.same) {
|
|
18689
|
+
return {loaded: true, status: 'note', detail: 'writes a small tree differently from phyloxml '
|
|
18690
|
+
+ PHYLOXML_EXPECTED + ', losing nothing: a newer release, or an altered copy'};
|
|
18691
|
+
}
|
|
18692
|
+
return {loaded: true, status: 'ok', detail: 'writes phyloXML as ' + PHYLOXML_EXPECTED + ' does'};
|
|
18693
|
+
}));
|
|
18694
|
+
libs.push(doctorLibrary('sax', 'required', function () {
|
|
18695
|
+
// A page hands sax over as a global. Anywhere else (Node, a
|
|
18696
|
+
// bundler) phyloxml gets it by require() when it loads, so a
|
|
18697
|
+
// phyloxml that loaded has it, and a tree it read proves it.
|
|
18698
|
+
if (g.sax || (probe && probe.stage !== 'read')) {
|
|
18699
|
+
return {loaded: true, status: 'ok'};
|
|
18700
|
+
}
|
|
18701
|
+
if (!inBrowser && px) {
|
|
18702
|
+
return {loaded: true, status: 'ok', detail: 'taken by phyloxml when it loaded'};
|
|
18703
|
+
}
|
|
18704
|
+
return {loaded: false, status: 'problem', detail: 'not loaded: phyloxml needs it to read phyloXML'};
|
|
18705
|
+
}));
|
|
18706
|
+
|
|
18707
|
+
libs.push(doctorLibrary('canvg', 'optional', function () {
|
|
18708
|
+
if (g.Canvg && typeof g.Canvg.fromString === 'function') {
|
|
18709
|
+
return {loaded: true, status: 'ok'};
|
|
18710
|
+
}
|
|
18711
|
+
if (g.Canvg) {
|
|
18712
|
+
return {loaded: true, status: 'problem',
|
|
18713
|
+
detail: 'window.Canvg has no fromString: not canvg 4, so PNG export is off'};
|
|
18714
|
+
}
|
|
18715
|
+
if (g.canvg) {
|
|
18716
|
+
return {loaded: true, status: 'problem', detail: 'loaded under its old global "canvg" (canvg 1 to 3);'
|
|
18717
|
+
+ ' PNG export needs canvg 4 as window.Canvg, so it is off'};
|
|
18718
|
+
}
|
|
18719
|
+
return {loaded: false, status: 'absent', detail: 'not loaded: PNG export and Copy PNG are off'};
|
|
18720
|
+
}));
|
|
18721
|
+
|
|
18722
|
+
// The pair is read inside each row's own guard: whatever a page has
|
|
18723
|
+
// put under these names, a lookup that throws costs its row and no
|
|
18724
|
+
// other.
|
|
18725
|
+
let pdfPair = function () {
|
|
18726
|
+
let J = g.jspdf && g.jspdf.jsPDF;
|
|
18727
|
+
return {J: J, attached: !!(J && J.API && J.API.svg), svg2pdf: !!g.svg2pdf};
|
|
18728
|
+
};
|
|
18729
|
+
libs.push(doctorLibrary('jspdf', 'optional', function () {
|
|
18730
|
+
let pair = pdfPair();
|
|
18731
|
+
let J = pair.J;
|
|
18732
|
+
let attached = pair.attached;
|
|
18733
|
+
if (J) {
|
|
18734
|
+
let version = typeof J.version === 'string' ? J.version : null;
|
|
18735
|
+
if (!attached && !g.svg2pdf) {
|
|
18736
|
+
return {loaded: true, version: version, status: 'problem',
|
|
18737
|
+
detail: 'loaded without svg2pdf.js: PDF export needs both, so it is off'};
|
|
18738
|
+
}
|
|
18739
|
+
if (version && Number(version.split('.')[0]) !== JSPDF_TESTED_MAJOR) {
|
|
18740
|
+
return {loaded: true, version: version, status: 'note',
|
|
18741
|
+
detail: 'this release is tested with jspdf ' + JSPDF_TESTED_MAJOR};
|
|
18742
|
+
}
|
|
18743
|
+
return {loaded: true, version: version, status: 'ok'};
|
|
18744
|
+
}
|
|
18745
|
+
if (g.jsPDF) {
|
|
18746
|
+
return {loaded: true, status: 'problem', detail: 'loaded under its old global "jsPDF" (jsPDF 1);'
|
|
18747
|
+
+ ' PDF export needs jspdf ' + JSPDF_TESTED_MAJOR + ' as window.jspdf, so it is off'};
|
|
18748
|
+
}
|
|
18749
|
+
return {loaded: false, status: 'absent', detail: 'not loaded: PDF export is off'};
|
|
18750
|
+
}));
|
|
18751
|
+
libs.push(doctorLibrary('svg2pdf.js', 'optional', function () {
|
|
18752
|
+
let pair = pdfPair();
|
|
18753
|
+
let J = pair.J;
|
|
18754
|
+
let attached = pair.attached;
|
|
18755
|
+
if (attached) {
|
|
18756
|
+
return {loaded: true, status: 'ok'};
|
|
18757
|
+
}
|
|
18758
|
+
if (g.svg2pdf && J) {
|
|
18759
|
+
return {loaded: true, status: 'problem', detail: 'loaded, but not attached to jspdf (jsPDF.API.svg'
|
|
18760
|
+
+ ' is missing): load jspdf first. PDF export is off'};
|
|
18761
|
+
}
|
|
18762
|
+
if (g.svg2pdf) {
|
|
18763
|
+
return {loaded: true, status: 'problem',
|
|
18764
|
+
detail: 'loaded without jspdf: PDF export needs both, so it is off'};
|
|
18765
|
+
}
|
|
18766
|
+
return {loaded: false, status: 'absent',
|
|
18767
|
+
detail: J ? 'not loaded' : 'not loaded: PDF export is off'};
|
|
18768
|
+
}));
|
|
18769
|
+
return libs;
|
|
18770
|
+
}
|
|
18771
|
+
|
|
18772
|
+
// What the user gets or does not get, each with the reason.
|
|
18773
|
+
function doctorFunctions(libs) {
|
|
18774
|
+
let lib = function (name) {
|
|
18775
|
+
return libs.find(function (l) {
|
|
18776
|
+
return l.name === name;
|
|
18777
|
+
});
|
|
18778
|
+
};
|
|
18779
|
+
let inBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
18780
|
+
let fn = function (name, test, whyNot) {
|
|
18781
|
+
let available = false;
|
|
18782
|
+
let detail = '';
|
|
18783
|
+
try {
|
|
18784
|
+
available = inBrowser && !!test();
|
|
18785
|
+
detail = available ? '' : (inBrowser ? whyNot() : 'needs a browser');
|
|
18786
|
+
} catch (e) {
|
|
18787
|
+
detail = 'this check failed: ' + (e && e.message ? e.message : e);
|
|
18788
|
+
}
|
|
18789
|
+
return {name: name, available: available, detail: detail};
|
|
18790
|
+
};
|
|
18791
|
+
// why a function is off, in the library's name: its row's reason
|
|
18792
|
+
// reads "not loaded: ..." under its own name and needs the subject here
|
|
18793
|
+
let why = function (l) {
|
|
18794
|
+
return l.status === 'absent' ? l.name + ' is not loaded' : l.name + ': ' + l.detail;
|
|
18795
|
+
};
|
|
18796
|
+
let out = [];
|
|
18797
|
+
try {
|
|
18798
|
+
// ON whenever phyloxml reads and writes at all -- which is what
|
|
18799
|
+
// the Download menu's phyloXML entry does, and it is always
|
|
18800
|
+
// offered. An older copy still works: its row above says what a
|
|
18801
|
+
// saved file loses, and this row repeats it without turning off.
|
|
18802
|
+
let px = lib('phyloxml');
|
|
18803
|
+
let works = px.loaded && !/(cannot read phyloXML|a small tree failed|this check failed)/.test(px.detail);
|
|
18804
|
+
out.push({name: 'Read and save phyloXML', available: works,
|
|
18805
|
+
detail: px.status === 'ok' ? '' : (works ? px.detail : why(px))});
|
|
18806
|
+
} catch (e) {
|
|
18807
|
+
out.push({name: 'Read and save phyloXML', available: false,
|
|
18808
|
+
detail: 'this check failed: ' + (e && e.message ? e.message : e)});
|
|
18809
|
+
}
|
|
18810
|
+
out.push(fn('PNG export', pngExportAvailable, function () {
|
|
18811
|
+
return why(lib('canvg'));
|
|
18812
|
+
}));
|
|
18813
|
+
out.push(fn('Copy PNG', pngCopyAvailable, function () {
|
|
18814
|
+
if (!pngExportAvailable()) {
|
|
18815
|
+
return why(lib('canvg'));
|
|
18816
|
+
}
|
|
18817
|
+
return window.isSecureContext ? 'this browser cannot write an image to the clipboard'
|
|
18818
|
+
: 'needs a secure page (https, or localhost)';
|
|
18819
|
+
}));
|
|
18820
|
+
out.push(fn('PDF export', pdfExportAvailable, function () {
|
|
18821
|
+
let pair = [lib('jspdf'), lib('svg2pdf.js')];
|
|
18822
|
+
let bad = pair.find(function (l) {
|
|
18823
|
+
return l.status === 'problem';
|
|
18824
|
+
});
|
|
18825
|
+
if (bad) {
|
|
18826
|
+
return why(bad);
|
|
18827
|
+
}
|
|
18828
|
+
return pair.every(function (l) {
|
|
18829
|
+
return l.status === 'absent';
|
|
18830
|
+
}) ? 'jspdf and svg2pdf.js are not loaded' : why(pair.find(function (l) {
|
|
18831
|
+
return l.status !== 'ok';
|
|
18832
|
+
}) || pair[0]);
|
|
18833
|
+
}));
|
|
18834
|
+
return out;
|
|
18835
|
+
}
|
|
18836
|
+
|
|
18837
|
+
// A config value as one short word or number: enough to see what a site
|
|
18838
|
+
// passed, never the value itself where that could be a tree or a callback.
|
|
18839
|
+
function doctorConfigValue(v) {
|
|
18840
|
+
if (v === null || typeof v === 'boolean' || typeof v === 'number') {
|
|
18841
|
+
return v;
|
|
18842
|
+
}
|
|
18843
|
+
if (typeof v === 'string') {
|
|
18844
|
+
return v.length > 80 ? v.substring(0, 77) + '...' : v;
|
|
18845
|
+
}
|
|
18846
|
+
if (typeof v === 'function') {
|
|
18847
|
+
return '(function)';
|
|
18848
|
+
}
|
|
18849
|
+
if (Array.isArray(v)) {
|
|
18850
|
+
return '(list of ' + v.length + ')';
|
|
18851
|
+
}
|
|
18852
|
+
return '(' + typeof v + ')';
|
|
18853
|
+
}
|
|
18854
|
+
|
|
18855
|
+
// The tree on view, as noted when its launch took the page over
|
|
18856
|
+
// (_onView): null until one has, so a first launch that was rejected
|
|
18857
|
+
// describes no tree. The tip count is the one the launch computed.
|
|
18858
|
+
function doctorLaunch() {
|
|
18859
|
+
if (!_onView) {
|
|
18860
|
+
return null;
|
|
18861
|
+
}
|
|
18862
|
+
let launch = {config: {}, tips: null, tree: _onView.tree, trees: _onView.trees, container: null};
|
|
18863
|
+
try {
|
|
18864
|
+
Object.keys(_onView.config || {}).sort().forEach(function (k) {
|
|
18865
|
+
launch.config[k] = doctorConfigValue(_onView.config[k]);
|
|
18866
|
+
});
|
|
18867
|
+
} catch {
|
|
18868
|
+
launch.config = {};
|
|
18869
|
+
}
|
|
18870
|
+
launch.tips = (typeof _onView.tips === 'number') ? _onView.tips : null;
|
|
18871
|
+
try {
|
|
18872
|
+
launch.container = {width: _onView.container.clientWidth, height: _onView.container.clientHeight};
|
|
18873
|
+
} catch {
|
|
18874
|
+
launch.container = null;
|
|
18875
|
+
}
|
|
18876
|
+
return launch;
|
|
18877
|
+
}
|
|
18878
|
+
|
|
18879
|
+
function doctorText(rep) {
|
|
18880
|
+
let pad = function (text, n) {
|
|
18881
|
+
text = String(text);
|
|
18882
|
+
return text + ' '.repeat(Math.max(1, n - text.length));
|
|
18883
|
+
};
|
|
18884
|
+
let lines = [NAME + ' ' + rep.version + ' diagnostics', ''];
|
|
18885
|
+
lines.push('Status: ' + rep.summary);
|
|
18886
|
+
rep.problems.forEach(function (p) {
|
|
18887
|
+
lines.push(' ! ' + p);
|
|
18888
|
+
});
|
|
18889
|
+
lines.push('', 'Libraries');
|
|
18890
|
+
rep.libraries.forEach(function (l) {
|
|
18891
|
+
lines.push(' ' + pad(l.name, 13) + pad(l.version || (l.loaded ? 'loaded' : 'not loaded'), 12)
|
|
18892
|
+
+ pad(l.status === 'absent' ? l.role : l.status, 9) + l.detail);
|
|
18893
|
+
});
|
|
18894
|
+
lines.push('', 'Functions');
|
|
18895
|
+
rep.functions.forEach(function (f) {
|
|
18896
|
+
lines.push(' ' + pad(f.name, 24) + (f.available ? 'on' : 'off') + (f.detail ? ' ' + f.detail : ''));
|
|
18897
|
+
});
|
|
18898
|
+
if (rep.notes.length > 0) {
|
|
18899
|
+
lines.push('', 'Notes');
|
|
18900
|
+
rep.notes.forEach(function (n) {
|
|
18901
|
+
lines.push(' - ' + n);
|
|
18902
|
+
});
|
|
18903
|
+
}
|
|
18904
|
+
lines.push('', 'Launch');
|
|
18905
|
+
if (rep.launch) {
|
|
18906
|
+
lines.push(' config: ' + JSON.stringify(rep.launch.config));
|
|
18907
|
+
if (rep.launch.tips !== null) {
|
|
18908
|
+
lines.push(' tree: ' + rep.launch.tips + ' tips' + (rep.launch.trees > 1
|
|
18909
|
+
? ' (tree ' + rep.launch.tree + ' of ' + rep.launch.trees + ')' : ''));
|
|
18910
|
+
}
|
|
18911
|
+
if (rep.launch.container) {
|
|
18912
|
+
lines.push(' container: ' + rep.launch.container.width + ' x ' + rep.launch.container.height + ' px');
|
|
18913
|
+
}
|
|
18914
|
+
} else {
|
|
18915
|
+
lines.push(' no tree is on view');
|
|
18916
|
+
}
|
|
18917
|
+
lines.push('', 'Environment');
|
|
18918
|
+
lines.push(' ' + (rep.environment.userAgent || 'no browser'));
|
|
18919
|
+
if (rep.environment.secureContext !== null) {
|
|
18920
|
+
lines.push(' secure page: ' + (rep.environment.secureContext ? 'yes' : 'no'));
|
|
18921
|
+
}
|
|
18922
|
+
return lines.join('\n');
|
|
18923
|
+
}
|
|
18924
|
+
|
|
18925
|
+
// The problems and the notes: the libraries' and what the launch said.
|
|
18926
|
+
// This is all the dot needs, so it is kept apart from the report, which
|
|
18927
|
+
// also works out the functions, the launch section and the text.
|
|
18928
|
+
function doctorFindings() {
|
|
18929
|
+
let libs;
|
|
18930
|
+
try {
|
|
18931
|
+
libs = doctorLibraries();
|
|
18932
|
+
} catch (e) {
|
|
18933
|
+
libs = [{name: 'libraries', role: 'required', loaded: false, version: null, status: 'problem',
|
|
18934
|
+
detail: 'the checks failed: ' + (e && e.message ? e.message : e)}];
|
|
18935
|
+
}
|
|
18936
|
+
let problems = [];
|
|
18937
|
+
let notes = [];
|
|
18938
|
+
libs.forEach(function (l) {
|
|
18939
|
+
if (l.status === 'problem') {
|
|
18940
|
+
problems.push(l.name + ': ' + l.detail);
|
|
18941
|
+
} else if (l.status === 'note') {
|
|
18942
|
+
notes.push(l.name + (l.version ? ' ' + l.version : '') + ': ' + l.detail);
|
|
18943
|
+
}
|
|
18944
|
+
});
|
|
18945
|
+
_diagnostics.forEach(function (d) {
|
|
18946
|
+
(d.level === 'problem' ? problems : notes).push(d.text + (d.count > 1 ? ' (' + d.count + ' times)' : ''));
|
|
18947
|
+
});
|
|
18948
|
+
return {libraries: libs, problems: problems, notes: notes,
|
|
18949
|
+
summary: problems.length === 0 ? 'nothing to report'
|
|
18950
|
+
: problems.length + (problems.length === 1 ? ' thing' : ' things') + ' to check'};
|
|
18951
|
+
}
|
|
18952
|
+
|
|
18953
|
+
function doctorReport() {
|
|
18954
|
+
let found = doctorFindings();
|
|
18955
|
+
let libs = found.libraries;
|
|
18956
|
+
let problems = found.problems;
|
|
18957
|
+
let notes = found.notes;
|
|
18958
|
+
let functions;
|
|
18959
|
+
try {
|
|
18960
|
+
functions = doctorFunctions(libs);
|
|
18961
|
+
} catch {
|
|
18962
|
+
functions = [];
|
|
18963
|
+
}
|
|
18964
|
+
let launch = null;
|
|
18965
|
+
try {
|
|
18966
|
+
launch = doctorLaunch();
|
|
18967
|
+
} catch {
|
|
18968
|
+
launch = null;
|
|
18969
|
+
}
|
|
18970
|
+
let nav = (typeof navigator !== 'undefined') ? navigator : null;
|
|
18971
|
+
let rep = {
|
|
18972
|
+
program: NAME,
|
|
18973
|
+
version: VERSION,
|
|
18974
|
+
ok: problems.length === 0,
|
|
18975
|
+
summary: found.summary,
|
|
18976
|
+
problems: problems,
|
|
18977
|
+
notes: notes,
|
|
18978
|
+
libraries: libs,
|
|
18979
|
+
functions: functions,
|
|
18980
|
+
launch: launch,
|
|
18981
|
+
environment: {
|
|
18982
|
+
userAgent: nav && nav.userAgent ? nav.userAgent : null,
|
|
18983
|
+
secureContext: (typeof window !== 'undefined' && typeof window.isSecureContext === 'boolean')
|
|
18984
|
+
? window.isSecureContext : null
|
|
18985
|
+
}
|
|
18986
|
+
};
|
|
18987
|
+
try {
|
|
18988
|
+
rep.text = doctorText(rep);
|
|
18989
|
+
} catch (e) {
|
|
18990
|
+
rep.text = NAME + ' ' + VERSION + ' diagnostics: the report could not be written ('
|
|
18991
|
+
+ (e && e.message ? e.message : e) + ')';
|
|
18992
|
+
}
|
|
18993
|
+
return rep;
|
|
18994
|
+
}
|
|
18995
|
+
|
|
18996
|
+
/**
|
|
18997
|
+
* The state of the program on this page: which libraries are loaded (and
|
|
18998
|
+
* their versions, where a library states one), which functions that
|
|
18999
|
+
* switches off, and what the last launch() warned about. Works before a
|
|
19000
|
+
* launch and after a failed one, and in Node. Never throws.
|
|
19001
|
+
*
|
|
19002
|
+
* @returns {{program: string, version: string, ok: boolean, summary: string,
|
|
19003
|
+
* problems: string[], notes: string[], libraries: Object[], functions: Object[],
|
|
19004
|
+
* launch: Object|null, environment: Object, text: string}} `ok` is false when
|
|
19005
|
+
* something is wrong that nobody chooses (see README "Diagnostics");
|
|
19006
|
+
* `text` is the whole report as plain text, for pasting into an issue.
|
|
19007
|
+
*/
|
|
19008
|
+
archaeopteryx.doctor = function () {
|
|
19009
|
+
let rep = doctorReport();
|
|
19010
|
+
syncDoctorDot(); // whoever asks sees a dot that agrees with the answer
|
|
19011
|
+
return rep;
|
|
19012
|
+
};
|
|
19013
|
+
|
|
19014
|
+
// The "check engine light": a small dot after the program name in the
|
|
19015
|
+
// panel header, there only while the report has a problem. Neutral, not
|
|
19016
|
+
// a warning colour: the people who see it are mostly the site's users,
|
|
19017
|
+
// who can do nothing about it, and it is for the one who can.
|
|
19018
|
+
//
|
|
19019
|
+
// What a page has loaded can change under a viewer that is already up (a
|
|
19020
|
+
// library arriving late, a script replacing a global) and nothing
|
|
19021
|
+
// announces it, so the dot is brought up to date whenever it could be
|
|
19022
|
+
// looked at or asked about: when a problem is recorded, when the header
|
|
19023
|
+
// is built, when the page finishes loading, when the pointer enters the
|
|
19024
|
+
// panel, when the About box opens and when doctor() is called.
|
|
19025
|
+
function syncDoctorDot() {
|
|
19026
|
+
try {
|
|
19027
|
+
let host = _onView && _onView.container;
|
|
19028
|
+
if (!host || typeof host.querySelector !== 'function') {
|
|
19029
|
+
return;
|
|
19030
|
+
}
|
|
19031
|
+
let button = host.querySelector('.aptx-panel .' + PROGNAMELINK);
|
|
19032
|
+
let dot = button && button.querySelector('.aptx-doctor-dot');
|
|
19033
|
+
if (!dot) {
|
|
19034
|
+
return;
|
|
19035
|
+
}
|
|
19036
|
+
let found = doctorFindings();
|
|
19037
|
+
let ok = found.problems.length === 0;
|
|
19038
|
+
dot.hidden = ok;
|
|
19039
|
+
button.title = 'About ' + NAME + (ok ? '' : ': ' + found.summary);
|
|
19040
|
+
} catch {
|
|
19041
|
+
// a light that cannot be lit must not take the viewer with it
|
|
19042
|
+
}
|
|
19043
|
+
}
|
|
19044
|
+
|
|
19045
|
+
function showDoctorDialog() {
|
|
19046
|
+
let rep = doctorReport();
|
|
19047
|
+
let shell = makeDialogShell(DOCTOR_DIALOG, 'Diagnostics', 500);
|
|
19048
|
+
shell.body.classList.add('aptx-doctor');
|
|
19049
|
+
|
|
19050
|
+
// The summary and Copy report share the first line: the report is
|
|
19051
|
+
// longer than a small window, and the one button must not be the
|
|
19052
|
+
// thing scrolled out of sight.
|
|
19053
|
+
let head = document.createElement('div');
|
|
19054
|
+
head.className = 'aptx-doctor-head';
|
|
19055
|
+
let status = document.createElement('div');
|
|
19056
|
+
status.className = 'aptx-doctor-status' + (rep.ok ? '' : ' aptx-doctor-bad');
|
|
19057
|
+
status.textContent = NAME + ' ' + rep.version + ': ' + rep.summary;
|
|
19058
|
+
let copy = document.createElement('button');
|
|
19059
|
+
copy.type = 'button';
|
|
19060
|
+
copy.className = 'aptx-reps-button aptx-doctor-copy';
|
|
19061
|
+
copy.textContent = 'Copy report';
|
|
19062
|
+
copy.title = 'copy this report as text, with the launch configuration and the browser added,'
|
|
19063
|
+
+ ' to paste into an issue or an e-mail';
|
|
19064
|
+
copy.addEventListener('click', function () {
|
|
19065
|
+
copyText(rep.text, shell.dialog).then(function (ok) {
|
|
19066
|
+
flashButton(copy, ok ? 'Copied \u2713' : 'Copy failed');
|
|
19067
|
+
});
|
|
19068
|
+
});
|
|
19069
|
+
head.appendChild(status);
|
|
19070
|
+
head.appendChild(copy);
|
|
19071
|
+
shell.body.appendChild(head);
|
|
19072
|
+
|
|
19073
|
+
let list = function (items, cls) {
|
|
19074
|
+
items.forEach(function (text) {
|
|
19075
|
+
let item = document.createElement('div');
|
|
19076
|
+
item.className = 'aptx-doctor-item ' + cls;
|
|
19077
|
+
item.textContent = text; // may quote a config value: never markup
|
|
19078
|
+
shell.body.appendChild(item);
|
|
19079
|
+
});
|
|
19080
|
+
};
|
|
19081
|
+
let section = function (title) {
|
|
19082
|
+
let h = document.createElement('div');
|
|
19083
|
+
h.className = 'aptx-doctor-section';
|
|
19084
|
+
h.textContent = title;
|
|
19085
|
+
shell.body.appendChild(h);
|
|
19086
|
+
};
|
|
19087
|
+
let row = function (key, value, detail, bad) {
|
|
19088
|
+
let line = document.createElement('div');
|
|
19089
|
+
line.className = 'aptx-dialog-line' + (bad ? ' aptx-doctor-bad' : '');
|
|
19090
|
+
let k = document.createElement('span');
|
|
19091
|
+
k.className = 'aptx-dialog-key';
|
|
19092
|
+
k.textContent = key;
|
|
19093
|
+
let v = document.createElement('span');
|
|
19094
|
+
v.className = 'aptx-dialog-val';
|
|
19095
|
+
let main = document.createElement('span');
|
|
19096
|
+
main.className = 'aptx-doctor-value';
|
|
19097
|
+
main.textContent = value;
|
|
19098
|
+
v.appendChild(main);
|
|
19099
|
+
if (detail) {
|
|
19100
|
+
let d = document.createElement('span');
|
|
19101
|
+
d.className = 'aptx-doctor-detail';
|
|
19102
|
+
d.textContent = detail;
|
|
19103
|
+
v.appendChild(d);
|
|
19104
|
+
}
|
|
19105
|
+
line.appendChild(k);
|
|
19106
|
+
line.appendChild(v);
|
|
19107
|
+
shell.body.appendChild(line);
|
|
19108
|
+
};
|
|
19109
|
+
list(rep.problems, 'aptx-doctor-bad');
|
|
19110
|
+
|
|
19111
|
+
section('Libraries');
|
|
19112
|
+
rep.libraries.forEach(function (l) {
|
|
19113
|
+
// "not loaded" is the value: a reason that opens with the same
|
|
19114
|
+
// two words and a colon does not say them again. (Only that
|
|
19115
|
+
// exact opening: "loaded, but not attached..." keeps its start.)
|
|
19116
|
+
let value = l.version || (l.loaded ? 'loaded' : 'not loaded');
|
|
19117
|
+
let detail = l.detail === value ? ''
|
|
19118
|
+
: (l.detail.indexOf('not loaded: ') === 0 && value === 'not loaded' ? l.detail.substring(12) : l.detail);
|
|
19119
|
+
row(l.name, value, detail, l.status === 'problem');
|
|
19120
|
+
});
|
|
19121
|
+
section('Functions');
|
|
19122
|
+
rep.functions.forEach(function (f) {
|
|
19123
|
+
row(f.name, f.available ? 'on' : 'off', f.detail, false);
|
|
19124
|
+
});
|
|
19125
|
+
if (rep.notes.length > 0) {
|
|
19126
|
+
section('Notes');
|
|
19127
|
+
list(rep.notes, 'aptx-doctor-note');
|
|
19128
|
+
}
|
|
19129
|
+
|
|
19130
|
+
shell.dialog.showModal();
|
|
19131
|
+
}
|
|
19132
|
+
|
|
18091
19133
|
// The About box behind the panel's title. Everything here is fixed text and
|
|
18092
19134
|
// fixed URLs -- no tree data reaches it -- so the links are built as real
|
|
18093
|
-
// elements rather than markup.
|
|
19135
|
+
// elements rather than markup. (The Diagnostics row's text is the doctor's
|
|
19136
|
+
// own summary: a count, never a value.)
|
|
18094
19137
|
function showAboutDialog() {
|
|
19138
|
+
let rep = doctorReport();
|
|
19139
|
+
syncDoctorDot();
|
|
18095
19140
|
let shell = makeDialogShell(ABOUT_DIALOG, 'About', 380);
|
|
18096
19141
|
shell.body.classList.add('aptx-about');
|
|
18097
19142
|
|
|
@@ -18099,7 +19144,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
18099
19144
|
head.className = 'aptx-about-head';
|
|
18100
19145
|
let logo = document.createElement('div');
|
|
18101
19146
|
logo.className = 'aptx-about-logo';
|
|
18102
|
-
logo.innerHTML = ARCHAEOPTERYX_LOGO_SVG;
|
|
19147
|
+
logo.innerHTML = paintInline(ARCHAEOPTERYX_LOGO_SVG);
|
|
18103
19148
|
head.appendChild(logo);
|
|
18104
19149
|
|
|
18105
19150
|
let titles = document.createElement('div');
|
|
@@ -18123,8 +19168,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
18123
19168
|
['Desktop version', DESKTOP_WEBSITE, 'cmzmasek.github.io/archaeopteryx'],
|
|
18124
19169
|
['Source code', SOURCE_WEBSITE, 'github.com/cmzmasek/archaeopteryx-js'],
|
|
18125
19170
|
['License', LICENSE_WEBSITE, LICENSE_NAME],
|
|
18126
|
-
['Keyboard', null, 'shortcuts (' + (IS_MAC ? '\u2318 /' : 'Ctrl+/') + ')'],
|
|
18127
|
-
['Control panel', null, 'what each control does']
|
|
19171
|
+
['Keyboard', null, 'shortcuts (' + (IS_MAC ? '\u2318 /' : 'Ctrl+/') + ')', showShortcutsDialog],
|
|
19172
|
+
['Control panel', null, 'what each control does', showCheatSheetDialog],
|
|
19173
|
+
['Diagnostics', null, rep.summary, showDoctorDialog]].forEach(function (row) {
|
|
18128
19174
|
let line = document.createElement('div');
|
|
18129
19175
|
line.className = 'aptx-dialog-line';
|
|
18130
19176
|
let key = document.createElement('span');
|
|
@@ -18138,9 +19184,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
18138
19184
|
a.target = '_blank';
|
|
18139
19185
|
a.rel = 'noopener noreferrer';
|
|
18140
19186
|
} else {
|
|
18141
|
-
// the
|
|
18142
|
-
//
|
|
18143
|
-
let opener = row[
|
|
19187
|
+
// the rows with no URL open another dialog in place of this
|
|
19188
|
+
// one: the key list, the control-panel sheet, the diagnostics
|
|
19189
|
+
let opener = row[3];
|
|
18144
19190
|
a.href = '#';
|
|
18145
19191
|
a.addEventListener('click', function (e) {
|
|
18146
19192
|
e.preventDefault();
|
|
@@ -18149,6 +19195,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
18149
19195
|
});
|
|
18150
19196
|
}
|
|
18151
19197
|
a.textContent = row[2];
|
|
19198
|
+
if (row[0] === 'Diagnostics') {
|
|
19199
|
+
a.className = 'aptx-doctor-link';
|
|
19200
|
+
if (!rep.ok) {
|
|
19201
|
+
let dot = document.createElement('span');
|
|
19202
|
+
dot.className = 'aptx-doctor-dot';
|
|
19203
|
+
val.appendChild(dot);
|
|
19204
|
+
}
|
|
19205
|
+
}
|
|
18152
19206
|
val.appendChild(a);
|
|
18153
19207
|
line.appendChild(key);
|
|
18154
19208
|
line.appendChild(val);
|
|
@@ -18720,7 +19774,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
18720
19774
|
// by this, and a version is a value -- it belongs in the About box
|
|
18721
19775
|
// it opens, not in a row that says what the button is
|
|
18722
19776
|
h = h.concat('<button type="button" class="' + PROGNAMELINK + '" aria-label="' + NAME
|
|
18723
|
-
+ '" title="About ' + NAME + '">' + NAME + ' ' + VERSION
|
|
19777
|
+
+ '" title="About ' + NAME + '">' + NAME + ' ' + VERSION
|
|
19778
|
+
+ '<span class="aptx-doctor-dot" hidden></span></button>');
|
|
18724
19779
|
h = h.concat('</div>');
|
|
18725
19780
|
return h;
|
|
18726
19781
|
}
|
|
@@ -19397,7 +20452,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
19397
20452
|
// 2x, 4x and 8x, and the setting is now the one it starts on
|
|
19398
20453
|
// (Christian, 2026-09-28) -- said, not done silently
|
|
19399
20454
|
if (_pngScale !== _settings.pngExportScale) {
|
|
19400
|
-
|
|
20455
|
+
say('problem', 'warn', WARNING + ': pngExportScale ' + _settings.pngExportScale + ' is not one of '
|
|
19401
20456
|
+ DOWNLOAD_PNG_SCALES.join(', ') + ': the PNG starts at ' + _pngScale + 'x');
|
|
19402
20457
|
}
|
|
19403
20458
|
setRadioButtonValue(_downloadFullSize ? DOWNLOAD_FULL_SIZE_BUTTON : DOWNLOAD_AS_SHOWN_BUTTON, true);
|
|
@@ -19424,7 +20479,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
19424
20479
|
.attr('height', '100%')
|
|
19425
20480
|
.style('opacity', 1)
|
|
19426
20481
|
.attr('class', BASE_BACKGROUND)
|
|
19427
|
-
.
|
|
20482
|
+
.style('fill', _state.backgroundColorDefault);
|
|
19428
20483
|
}
|
|
19429
20484
|
|
|
19430
20485
|
|
|
@@ -19630,6 +20685,19 @@ function (root, d3, forester, phyloXml) {
|
|
|
19630
20685
|
return _baseSvg ? _baseSvg.node() : null;
|
|
19631
20686
|
}
|
|
19632
20687
|
|
|
20688
|
+
// The tree svg's size, said twice. The attributes are what an export and
|
|
20689
|
+
// the layout read; but an attribute gives way to any rule of the page the
|
|
20690
|
+
// viewer is drawn in: under "svg { width: 24px; height: 24px }", a page
|
|
20691
|
+
// whose only svgs were icons, the tree was 24 px square. (A rule that says
|
|
20692
|
+
// "height: auto" changes nothing: the attributes still give the svg its
|
|
20693
|
+
// own size.) The inline style does not give way.
|
|
20694
|
+
function sizeTreeSvg(svg, w, h) {
|
|
20695
|
+
svg.attr('width', w)
|
|
20696
|
+
.attr('height', h)
|
|
20697
|
+
.style('width', w + 'px')
|
|
20698
|
+
.style('height', h + 'px');
|
|
20699
|
+
}
|
|
20700
|
+
|
|
19633
20701
|
// An export is always light, whatever the screen is set to: a dark PNG or
|
|
19634
20702
|
// SVG is wrong on paper, in a slide and in a paper figure, and it is the
|
|
19635
20703
|
// file that outlives the session. Only the four theme colours are swapped,
|
|
@@ -19673,6 +20741,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
19673
20741
|
}
|
|
19674
20742
|
|
|
19675
20743
|
function cleanExportCopy(copy) {
|
|
20744
|
+
// What the live svg carries only for the page it is drawn in (its
|
|
20745
|
+
// class, its size said a second time): a file has no page, and an
|
|
20746
|
+
// inline size would hold a full-size drawing to the window's.
|
|
20747
|
+
copy.classList.remove(TREE_SVG_CLASS);
|
|
20748
|
+
if (!copy.getAttribute('class')) {
|
|
20749
|
+
copy.removeAttribute('class');
|
|
20750
|
+
}
|
|
20751
|
+
copy.style.removeProperty('width');
|
|
20752
|
+
copy.style.removeProperty('height');
|
|
19676
20753
|
// Serialize a COPY with the overview taken out of it: the overview is
|
|
19677
20754
|
// an on-screen navigation aid, not part of the tree, and working on a
|
|
19678
20755
|
// copy leaves the live display untouched.
|
|
@@ -19689,7 +20766,57 @@ function (root, d3, forester, phyloXml) {
|
|
|
19689
20766
|
if (glow) {
|
|
19690
20767
|
glow.remove();
|
|
19691
20768
|
}
|
|
19692
|
-
return copy;
|
|
20769
|
+
return paintAsAttributes(copy);
|
|
20770
|
+
}
|
|
20771
|
+
|
|
20772
|
+
// The opposite of paintInline, for a file: a shape's paint, inline styles
|
|
20773
|
+
// on the page, is attributes again (FILE_PAINT). Where an element says a
|
|
20774
|
+
// thing both ways, the style is what painted it and has the last word.
|
|
20775
|
+
function paintAsAttributes(svg) {
|
|
20776
|
+
[svg].concat(Array.prototype.slice.call(svg.querySelectorAll('[style]'))).forEach(function (el) {
|
|
20777
|
+
let style = el.style;
|
|
20778
|
+
if (!style) {
|
|
20779
|
+
return;
|
|
20780
|
+
}
|
|
20781
|
+
FILE_PAINT.forEach(function (name) {
|
|
20782
|
+
let value = style.getPropertyValue(name);
|
|
20783
|
+
if (value !== '') {
|
|
20784
|
+
// (the browser writes url("#id"); an attribute has no quotes there)
|
|
20785
|
+
el.setAttribute(name, value.replace(/^url\("([^"]*)"\)$/, 'url($1)'));
|
|
20786
|
+
style.removeProperty(name);
|
|
20787
|
+
}
|
|
20788
|
+
});
|
|
20789
|
+
if (style.length === 0) {
|
|
20790
|
+
el.removeAttribute('style');
|
|
20791
|
+
}
|
|
20792
|
+
});
|
|
20793
|
+
// A colour with an alpha is the colour and, in the paint's own
|
|
20794
|
+
// opacity, the alpha times what the shape has of it already, stated
|
|
20795
|
+
// or handed down; a paint that is no paint at all is "none". Only on
|
|
20796
|
+
// a shape: an opacity stated on a group would reach the shapes in it
|
|
20797
|
+
// that have a colour of their own.
|
|
20798
|
+
Array.prototype.slice.call(svg.querySelectorAll('[fill], [stroke], [stop-color]')).forEach(function (el) {
|
|
20799
|
+
let holdsShapes = Array.prototype.some.call(el.children, function (child) {
|
|
20800
|
+
return child.localName !== 'title' && child.localName !== 'desc';
|
|
20801
|
+
});
|
|
20802
|
+
Object.keys(FILE_ALPHA).forEach(function (paint) {
|
|
20803
|
+
let c = el.hasAttribute(paint) ? d3.color(el.getAttribute(paint)) : null;
|
|
20804
|
+
if (!c || !(c.opacity < 1) || holdsShapes) {
|
|
20805
|
+
return;
|
|
20806
|
+
}
|
|
20807
|
+
let opacity = FILE_ALPHA[paint];
|
|
20808
|
+
if (c.opacity === 0 && paint !== 'stop-color') {
|
|
20809
|
+
el.setAttribute(paint, 'none');
|
|
20810
|
+
return;
|
|
20811
|
+
}
|
|
20812
|
+
// (a gradient's stop hands nothing down, and is handed nothing)
|
|
20813
|
+
let from = paint === 'stop-color' ? (el.hasAttribute(opacity) ? el : null) : el.closest('[' + opacity + ']');
|
|
20814
|
+
let has = from ? parseFloat(from.getAttribute(opacity)) : 1;
|
|
20815
|
+
el.setAttribute(paint, c.copy({opacity: 1}).formatRgb());
|
|
20816
|
+
el.setAttribute(opacity, String(+(c.opacity * (isNaN(has) ? 1 : has)).toFixed(4)));
|
|
20817
|
+
});
|
|
20818
|
+
});
|
|
20819
|
+
return svg;
|
|
19693
20820
|
}
|
|
19694
20821
|
|
|
19695
20822
|
// The same element of the copy as `live` is of the svg: children are
|
|
@@ -20139,7 +21266,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20139
21266
|
|
|
20140
21267
|
function downloadAsPdf(graphic) {
|
|
20141
21268
|
if (!pdfExportAvailable()) {
|
|
20142
|
-
|
|
21269
|
+
say('note', 'error', ERROR + 'PDF export needs the optional jspdf and svg2pdf.js libraries on the page');
|
|
20143
21270
|
return;
|
|
20144
21271
|
}
|
|
20145
21272
|
let el = new DOMParser().parseFromString(graphic.text, 'image/svg+xml').documentElement;
|
|
@@ -20188,7 +21315,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20188
21315
|
holder.remove();
|
|
20189
21316
|
}, function (err) {
|
|
20190
21317
|
holder.remove();
|
|
20191
|
-
|
|
21318
|
+
say('note', 'error', ERROR + 'PDF export failed: ' + err);
|
|
20192
21319
|
});
|
|
20193
21320
|
}
|
|
20194
21321
|
|
|
@@ -20234,7 +21361,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20234
21361
|
renderPng(graphic).then(function (blob) {
|
|
20235
21362
|
saveAs(blob, downloadFileName(PNG_SUFFIX));
|
|
20236
21363
|
}, function (err) {
|
|
20237
|
-
|
|
21364
|
+
say('note', 'error', ERROR + 'PNG export failed: ' + err);
|
|
20238
21365
|
});
|
|
20239
21366
|
}
|
|
20240
21367
|
|
|
@@ -20252,7 +21379,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20252
21379
|
return Promise.reject(new Error('this browser cannot paint a canvas of that size'));
|
|
20253
21380
|
}
|
|
20254
21381
|
if (target.scale < _pngScale) {
|
|
20255
|
-
|
|
21382
|
+
say('note', 'warn', WARNING + ': PNG exported at ' + target.scale.toFixed(2) + 'x rather than ' + _pngScale
|
|
20256
21383
|
+ 'x: the largest image this browser can paint');
|
|
20257
21384
|
}
|
|
20258
21385
|
let canvas = target.canvas;
|
|
@@ -20313,7 +21440,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20313
21440
|
written.then(function () {
|
|
20314
21441
|
flashButton(btn, 'Copied ✓');
|
|
20315
21442
|
}, function (err) {
|
|
20316
|
-
|
|
21443
|
+
say('note', 'error', ERROR + 'copying the PNG failed: ' + err);
|
|
20317
21444
|
flashButton(btn, 'Copy failed');
|
|
20318
21445
|
});
|
|
20319
21446
|
}
|
|
@@ -20480,7 +21607,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
20480
21607
|
return undefined;
|
|
20481
21608
|
}
|
|
20482
21609
|
|
|
20483
|
-
archaeopteryx.launchArchaeopteryx = function (
|
|
21610
|
+
archaeopteryx.launchArchaeopteryx = function () {
|
|
21611
|
+
try {
|
|
21612
|
+
return parseAndLaunch.apply(null, arguments);
|
|
21613
|
+
} catch (e) {
|
|
21614
|
+
// its own failures -- a fifth argument, a file that does not
|
|
21615
|
+
// parse -- happen before launch() and its shell are reached
|
|
21616
|
+
recordLaunchFailure(e);
|
|
21617
|
+
throw e;
|
|
21618
|
+
}
|
|
21619
|
+
};
|
|
21620
|
+
function parseAndLaunch(container, fileName, data, config) {
|
|
20484
21621
|
if (arguments.length > 4) {
|
|
20485
21622
|
throw new Error(ERROR + 'launchArchaeopteryx() takes exactly (container, fileName,'
|
|
20486
21623
|
+ ' data, config); the old trailing arguments were removed. The separate settings'
|
|
@@ -20508,7 +21645,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
20508
21645
|
// added nothing but a prefix, and swallowing them left the caller with
|
|
20509
21646
|
// a blank page and no way to find out why.
|
|
20510
21647
|
return archaeopteryx.launch(container, trees, config);
|
|
20511
|
-
}
|
|
21648
|
+
}
|
|
20512
21649
|
|
|
20513
21650
|
|
|
20514
21651
|
// --------------------------------------------------------------
|