archaeopteryx 3.0.0 → 3.1.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 +116 -21
- package/archaeopteryx.js +1651 -578
- package/forester.js +297 -11
- package/package.json +1 -1
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.1.0
|
|
24
24
|
// 2026-09-04
|
|
25
25
|
//
|
|
26
26
|
// Archaeopteryx.js is a software tool for the visualization and
|
|
@@ -54,10 +54,7 @@
|
|
|
54
54
|
// DOM elements, so jQuery UI (and its CSS) is no longer required.
|
|
55
55
|
//
|
|
56
56
|
//
|
|
57
|
-
//
|
|
58
|
-
// https://docs.google.com/document/d/1COVe0iYbKtcBQxGTP4_zuimpk2FH9iusOVOgd5xCJ3A
|
|
59
|
-
//
|
|
60
|
-
// User documentation:
|
|
57
|
+
// Documentation and live demos:
|
|
61
58
|
// https://cmzmasek.github.io/archaeopteryx-js/
|
|
62
59
|
|
|
63
60
|
// UMD: one file that loads everywhere an embedder might put it --
|
|
@@ -106,7 +103,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
106
103
|
// IIFE's own function name -- a plain object says what it is.)
|
|
107
104
|
let archaeopteryx = {};
|
|
108
105
|
|
|
109
|
-
const VERSION = '3.
|
|
106
|
+
const VERSION = '3.1.0';
|
|
110
107
|
const WEBSITE = 'https://cmzmasek.github.io/archaeopteryx-js/';
|
|
111
108
|
const DESKTOP_WEBSITE = 'https://cmzmasek.github.io/archaeopteryx/';
|
|
112
109
|
const SOURCE_WEBSITE = 'https://github.com/cmzmasek/archaeopteryx-js';
|
|
@@ -177,6 +174,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
177
174
|
'Helvetica Neue', 'Arial', 'sans-serif'];
|
|
178
175
|
// Okabe-Ito color-blind-safe palette for search / selection highlights.
|
|
179
176
|
const FOUND0_COLOR_DEFAULT = '#0072B2'; // Search A — blue
|
|
177
|
+
// Search A in the dark theme: the same Okabe-Ito palette's sky blue. The
|
|
178
|
+
// light theme's blue is right on white and sinks into a dark ground; sky
|
|
179
|
+
// blue keeps the hue and stays distinct from B's vermillion and A&B's
|
|
180
|
+
// yellow. The other two already read on dark.
|
|
181
|
+
const FOUND0_COLOR_DARK = '#56B4E9';
|
|
180
182
|
const FOUND1_COLOR_DEFAULT = '#D55E00'; // Search B — vermillion
|
|
181
183
|
const FOUND0AND1_COLOR_DEFAULT = '#F0E442'; // A and B — yellow
|
|
182
184
|
const SELECTED_COLOR_DEFAULT = '#009E73'; // Selected — bluish green
|
|
@@ -280,6 +282,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
280
282
|
const SVG_EXPORT_FORMAT = 'SVG';
|
|
281
283
|
const TOP_AND_BOTTOM_BORDER_HEIGHT = 10;
|
|
282
284
|
const TRANSITION_DURATION_DEFAULT = 750;
|
|
285
|
+
// Above this many nodes the first draw is deferred by one frame behind a
|
|
286
|
+
// "working" card, so the page shows it is busy instead of appearing frozen.
|
|
287
|
+
// Below it everything stays synchronous exactly as before -- the deferral
|
|
288
|
+
// buys nothing on a tree that draws in a few hundred milliseconds.
|
|
289
|
+
const BIG_TREE_NODES = 2000;
|
|
283
290
|
const WARNING = 'ArchaeopteryxJS: WARNING';
|
|
284
291
|
const MESSAGE = 'ArchaeopteryxJS: ';
|
|
285
292
|
const ERROR = 'ArchaeopteryxJS: ERROR: ';
|
|
@@ -338,7 +345,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
338
345
|
const CONFIDENCE_VALUES_CB = 'conf_cb';
|
|
339
346
|
const SUPPORT_DOTS_CB = 'suppdots_cb';
|
|
340
347
|
const DOWNLOAD_BUTTON = 'dl_b';
|
|
341
|
-
const SUBMIT_SELECTED_NODES_BUTTON = 'submit_sel_nodes_b';
|
|
342
348
|
const DYNAHIDE_CB = 'dynahide_cb';
|
|
343
349
|
const MSA_CB = 'msa_cb';
|
|
344
350
|
const TIME_AXIS_CB = 'timeaxis_cb';
|
|
@@ -356,6 +362,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
356
362
|
// embedding page's own defs.
|
|
357
363
|
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>';
|
|
358
364
|
const NODE_DATA = 'node_data_dialog';
|
|
365
|
+
const COLOR_PICKER = 'color_picker_dialog';
|
|
359
366
|
const NODE_EVENTS_CB = 'nevts_cb';
|
|
360
367
|
const NODE_NAME_CB = 'nn_cb';
|
|
361
368
|
const NODE_SHAPE_SELECT_MENU = 'nshapes_menu';
|
|
@@ -469,6 +476,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
469
476
|
let _searchBox1Empty = true;
|
|
470
477
|
let _settings = null; // fixed for the life of the launch; never written after init
|
|
471
478
|
let _svgGroup = null;
|
|
479
|
+
// The floating layer: a sibling of _svgGroup that is NOT zoomed or
|
|
480
|
+
// panned. The bottom strips -- the time axis, the alignment's
|
|
481
|
+
// conservation/consensus/ruler -- are drawn into it in tree coordinates
|
|
482
|
+
// and placed by placeFloatingOverlays() from the current zoom transform,
|
|
483
|
+
// so they ride with the tree until their bottom would leave the viewport
|
|
484
|
+
// and then stick there. See floatStripGroup().
|
|
485
|
+
let _floatGroup = null;
|
|
486
|
+
let _floatStrips = {}; // class -> {top, height} in tree coordinates
|
|
472
487
|
let _treeData = null;
|
|
473
488
|
let _treeFn = null;
|
|
474
489
|
let _vis = null; // the automatic visualizations: candidates, scales, choices
|
|
@@ -485,10 +500,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
485
500
|
const MSA_CONS_BAR_H = 22; // conservation bar band height
|
|
486
501
|
const MSA_CONS_TOP_GAP = 4;
|
|
487
502
|
const MSA_BOTTOM_RESERVE = 56; // vertical room under the rows (conservation + ruler)
|
|
503
|
+
const MSA_NAV_RESERVE = 46; // ... plus the navigation bar floating at the viewport bottom
|
|
488
504
|
const MSA_MIN_TREE_PX = 220; // the tree itself never shrinks below this
|
|
489
505
|
let _msaColOffset = 0; // first shown alignment column (0-based)
|
|
490
506
|
let _msaReserve = 0; // horizontal px reserved for the track, set with _w
|
|
491
507
|
let _msaScroller = null; // the fixed HTML range input, created lazily
|
|
508
|
+
let _msaNav = null; // the bar around it: paging, jump-to-column, readout
|
|
492
509
|
let _msaGeom = null; // window geometry of the last draw, for the hover readout
|
|
493
510
|
// ------ time axis (the desktop's geologic / calendar overlays) ------
|
|
494
511
|
const TIME_GEO_RESERVE = 52; // two ICS band rows + the Ma ruler
|
|
@@ -509,6 +526,29 @@ function (root, d3, forester, phyloXml) {
|
|
|
509
526
|
const RADIAL_MAX_EXTENT = 24000;
|
|
510
527
|
let _radialLabelsHorizontal = false; // circular layout: external labels upright at the ring instead of riding their spokes
|
|
511
528
|
let _panelTheme = null; // null = follow OS; 'light' / 'dark' = header switch choice
|
|
529
|
+
// Resolves once the current launch has drawn. Immediately for a small
|
|
530
|
+
// tree; after the deferred first draw for a big one. Exposed as
|
|
531
|
+
// viewer.ready so an embedder can wait for the DOM to exist.
|
|
532
|
+
let _readyPromise = Promise.resolve();
|
|
533
|
+
// Bumped by every launch() and destroy(), so a first draw deferred by a
|
|
534
|
+
// launch that was since torn down or replaced knows not to run.
|
|
535
|
+
let _launchSeq = 0;
|
|
536
|
+
// A redraw waiting for its frame (big trees only): the arguments of every
|
|
537
|
+
// scheduleUpdate() call made since, merged, so that one update() serves
|
|
538
|
+
// them all.
|
|
539
|
+
let _pendingUpdate = null;
|
|
540
|
+
// Work to run once the pending redraw has happened (big trees only):
|
|
541
|
+
// the viewport re-centring a zoom needs the new layout for.
|
|
542
|
+
let _afterUpdate = [];
|
|
543
|
+
// The viewport centre captured by the FIRST zoom step of a burst; the
|
|
544
|
+
// re-centring after the coalesced redraw maps that one point forward.
|
|
545
|
+
let _pendingCentre = null;
|
|
546
|
+
// True while initialize() runs. A redraw scheduled from inside it -- the
|
|
547
|
+
// searches it runs end in one -- is dropped: the draw in progress renders
|
|
548
|
+
// that same state, so the extra pass was always redundant, and on a big
|
|
549
|
+
// tree it would run after the card had gone and merge later interactions
|
|
550
|
+
// into an invisible redraw.
|
|
551
|
+
let _initializing = false;
|
|
512
552
|
let _searchFields = []; // available search-field descriptors, rebuilt per tree
|
|
513
553
|
let _zoomListener = null;
|
|
514
554
|
let _zoomed_x_or_y = false;
|
|
@@ -569,8 +609,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
569
609
|
// at. It appears and disappears on its own -- there is nothing to switch on.
|
|
570
610
|
// Clicking or dragging inside it moves the main view there.
|
|
571
611
|
|
|
572
|
-
const OVERVIEW_WIDTH =
|
|
573
|
-
const OVERVIEW_HEIGHT =
|
|
612
|
+
const OVERVIEW_WIDTH = 142;
|
|
613
|
+
const OVERVIEW_HEIGHT = 111;
|
|
574
614
|
const OVERVIEW_MARGIN = 12;
|
|
575
615
|
const OVERVIEW_PAD = 5;
|
|
576
616
|
|
|
@@ -991,9 +1031,44 @@ function (root, d3, forester, phyloXml) {
|
|
|
991
1031
|
|
|
992
1032
|
function zoom(event) {
|
|
993
1033
|
_svgGroup.attr('transform', event.transform);
|
|
1034
|
+
placeFloatingOverlays();
|
|
994
1035
|
updateOverviewViewport();
|
|
995
1036
|
}
|
|
996
1037
|
|
|
1038
|
+
// A strip in the floating layer. Drawn in TREE coordinates like everything
|
|
1039
|
+
// else (its content is x-aligned with the tree), it is then placed by a
|
|
1040
|
+
// transform of its own rather than the tree's -- the same x and scale, but
|
|
1041
|
+
// a y that is STICKY: the strip rides at the tree's bottom edge until that
|
|
1042
|
+
// edge would leave the viewport, and holds at the viewport bottom from
|
|
1043
|
+
// there. Nothing changes while the whole tree fits; nothing disappears
|
|
1044
|
+
// when it does not. `top` and `height` are the strip's extent in tree
|
|
1045
|
+
// coordinates, which is all the placement needs.
|
|
1046
|
+
function floatStripGroup(cls, top, height) {
|
|
1047
|
+
_floatGroup.selectAll('g.' + cls).remove();
|
|
1048
|
+
_floatStrips[cls] = {top: top, height: height};
|
|
1049
|
+
return _floatGroup.append('g').attr('class', cls);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
const FLOAT_STRIP_MARGIN = 6; // px between a stuck strip and the viewport edge
|
|
1053
|
+
|
|
1054
|
+
function placeFloatingOverlays() {
|
|
1055
|
+
if (!_floatGroup || !_baseSvg) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
let t = d3.zoomTransform(_baseSvg.node());
|
|
1059
|
+
let size = svgSize();
|
|
1060
|
+
if (!size) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
Object.keys(_floatStrips).forEach(function (cls) {
|
|
1064
|
+
let s = _floatStrips[cls];
|
|
1065
|
+
let stuckY = size.h - FLOAT_STRIP_MARGIN - (t.k * (s.top + s.height));
|
|
1066
|
+
let ty = Math.min(t.y, stuckY);
|
|
1067
|
+
_floatGroup.select('g.' + cls)
|
|
1068
|
+
.attr('transform', 'translate(' + t.x + ',' + ty + ') scale(' + t.k + ')');
|
|
1069
|
+
});
|
|
1070
|
+
}
|
|
1071
|
+
|
|
997
1072
|
function centerNode(source, x, y) {
|
|
998
1073
|
let scale = currentZoomScale();
|
|
999
1074
|
if (!x) {
|
|
@@ -1028,6 +1103,53 @@ function (root, d3, forester, phyloXml) {
|
|
|
1028
1103
|
const HOVER_GLOW_ALPHAS = [34 / 255, 44 / 255, 58 / 255];
|
|
1029
1104
|
const HOVER_GLOW_MIN_DIA = 18;
|
|
1030
1105
|
const HOVER_GLOW_ACCENT = 'rgb(38,117,191)';
|
|
1106
|
+
// Normalisation floors for a node-hued glow (see hoverGlowColor).
|
|
1107
|
+
const HOVER_GLOW_MIN_SAT = 0.15; // below this the colour has no hue to take
|
|
1108
|
+
const HOVER_GLOW_SAT_FLOOR = 0.55;
|
|
1109
|
+
const HOVER_GLOW_VAL_FLOOR = 0.80;
|
|
1110
|
+
|
|
1111
|
+
// The glow takes the hue of the node it marks, when the node has one --
|
|
1112
|
+
// a node coloured by Host Group glows in its host colour -- and the UI
|
|
1113
|
+
// accent otherwise. As on the desktop (TreePanel.hoverGlowColor). The
|
|
1114
|
+
// precedence is the one the node itself is painted by: the Color-by
|
|
1115
|
+
// value, then a per-node visual style, then an event colour, then a
|
|
1116
|
+
// colourised clade's branch colour. NEVER the found/search colour: a
|
|
1117
|
+
// focus ring must not read as a selection state.
|
|
1118
|
+
//
|
|
1119
|
+
// The glow is a 13-23% alpha wash, so a dark or a pale colour would
|
|
1120
|
+
// vanish in it. The hue is kept and saturation and value are floored:
|
|
1121
|
+
// Tableau-style oranges and reds pass unchanged, a muted blue gets a
|
|
1122
|
+
// modest lift, and a grey (no hue to speak of) falls back to the accent.
|
|
1123
|
+
function hoverGlowColor(d) {
|
|
1124
|
+
let c = null;
|
|
1125
|
+
let vis = makeVisNodeFillColor(d);
|
|
1126
|
+
if (vis && vis !== _state.backgroundColorDefault) {
|
|
1127
|
+
c = vis;
|
|
1128
|
+
}
|
|
1129
|
+
if (!c) {
|
|
1130
|
+
let style = nodeStyle(d);
|
|
1131
|
+
if (style && (style.nodeColor || style.fontColor)) {
|
|
1132
|
+
c = style.nodeColor || style.fontColor;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (!c && _state.showNodeEvents && d.events && d.children) {
|
|
1136
|
+
c = makeNodeEventsDependentColor(d.events);
|
|
1137
|
+
}
|
|
1138
|
+
if (!c && _state.useVisualStyles && d.color) {
|
|
1139
|
+
let bc = 'rgb(' + d.color.red + ',' + d.color.green + ',' + d.color.blue + ')';
|
|
1140
|
+
if (bc !== _state.branchColorDefault) {
|
|
1141
|
+
c = bc;
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
if (!c) {
|
|
1145
|
+
return HOVER_GLOW_ACCENT;
|
|
1146
|
+
}
|
|
1147
|
+
let hsv = rgbToHsv(c);
|
|
1148
|
+
if (hsv.s < HOVER_GLOW_MIN_SAT) {
|
|
1149
|
+
return HOVER_GLOW_ACCENT;
|
|
1150
|
+
}
|
|
1151
|
+
return hsvToHex({h: hsv.h, s: Math.max(hsv.s, HOVER_GLOW_SAT_FLOOR), v: Math.max(hsv.v, HOVER_GLOW_VAL_FLOOR)});
|
|
1152
|
+
}
|
|
1031
1153
|
|
|
1032
1154
|
function showHoverGlow(d) {
|
|
1033
1155
|
if (!_svgGroup || !d || d.x === undefined) {
|
|
@@ -1039,19 +1161,21 @@ function (root, d3, forester, phyloXml) {
|
|
|
1039
1161
|
.style('pointer-events', 'none');
|
|
1040
1162
|
HOVER_GLOW_RADII.forEach(function (unused, i) {
|
|
1041
1163
|
g.append('circle')
|
|
1042
|
-
.attr('fill', HOVER_GLOW_ACCENT)
|
|
1043
1164
|
.attr('fill-opacity', HOVER_GLOW_ALPHAS[i])
|
|
1044
1165
|
.attr('stroke', 'none');
|
|
1045
1166
|
});
|
|
1046
1167
|
}
|
|
1047
1168
|
let dia = Math.max(HOVER_GLOW_MIN_DIA, (_state.nodeSizeDefault || 0) * 3);
|
|
1048
1169
|
let p = layoutPointXY(d);
|
|
1170
|
+
let fill = hoverGlowColor(d);
|
|
1049
1171
|
g.attr('transform', 'translate(' + p[0] + ',' + p[1] + ')')
|
|
1050
1172
|
.style('display', null)
|
|
1051
1173
|
.raise();
|
|
1052
|
-
g.selectAll('circle')
|
|
1053
|
-
|
|
1054
|
-
|
|
1174
|
+
g.selectAll('circle')
|
|
1175
|
+
.attr('fill', fill)
|
|
1176
|
+
.attr('r', function (unused, i) {
|
|
1177
|
+
return (dia * HOVER_GLOW_RADII[i]) / 2;
|
|
1178
|
+
});
|
|
1055
1179
|
}
|
|
1056
1180
|
|
|
1057
1181
|
function hideHoverGlow() {
|
|
@@ -1074,147 +1198,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1074
1198
|
|
|
1075
1199
|
function mousemove(event, d) {
|
|
1076
1200
|
|
|
1077
|
-
let mo_text =
|
|
1078
|
-
if (d.name) {
|
|
1079
|
-
mo_text += 'Name: ' + d.name + '<br>';
|
|
1080
|
-
}
|
|
1081
|
-
if (d.branch_length) {
|
|
1082
|
-
mo_text += 'Distance to Parent: ' + d.branch_length + '<br>';
|
|
1083
|
-
}
|
|
1084
|
-
if (d.date && (typeof d.date.value === 'number'
|
|
1085
|
-
|| (typeof d.date.minimum === 'number' && typeof d.date.maximum === 'number'))) {
|
|
1086
|
-
let dv = typeof d.date.value === 'number' ? String(d.date.value) : '';
|
|
1087
|
-
if (typeof d.date.minimum === 'number' && typeof d.date.maximum === 'number') {
|
|
1088
|
-
dv += ' [' + d.date.minimum + ' - ' + d.date.maximum + ']';
|
|
1089
|
-
}
|
|
1090
|
-
if (d.date.unit) {
|
|
1091
|
-
dv += ' ' + d.date.unit;
|
|
1092
|
-
}
|
|
1093
|
-
mo_text += 'Date: ' + dv.trim() + '<br>';
|
|
1094
|
-
}
|
|
1095
|
-
mo_text += 'Depth: ' + forester.calcDepth(d) + '<br>';
|
|
1096
|
-
let i = 0;
|
|
1097
|
-
if (d.confidences) {
|
|
1098
|
-
for (i = 0; i < d.confidences.length; ++i) {
|
|
1099
|
-
let c = d.confidences[i];
|
|
1100
|
-
if (c.type) {
|
|
1101
|
-
mo_text += 'Confidence [' + c.type + ']: ' + c.value + '<br>';
|
|
1102
|
-
} else {
|
|
1103
|
-
mo_text += 'Confidence: ' + c.value + '<br>';
|
|
1104
|
-
}
|
|
1105
|
-
if (c.stddev) {
|
|
1106
|
-
mo_text += '- stdev: ' + c.stddev + '<br>';
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
if (d.taxonomies) {
|
|
1111
|
-
for (i = 0; i < d.taxonomies.length; ++i) {
|
|
1112
|
-
mo_text += 'Taxonomy<br>';
|
|
1113
|
-
let t = d.taxonomies[i];
|
|
1114
|
-
if (t.id) {
|
|
1115
|
-
if (t.id.provider) {
|
|
1116
|
-
mo_text += '- Id [' + t.id.provider + ']: ' + t.id.value + '<br>';
|
|
1117
|
-
} else {
|
|
1118
|
-
mo_text += '- Id: ' + t.id.value + '<br>';
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
if (t.code) {
|
|
1122
|
-
mo_text += '- Code: ' + t.code + '<br>';
|
|
1123
|
-
}
|
|
1124
|
-
if (t.scientific_name) {
|
|
1125
|
-
mo_text += '- Scientific name: ' + t.scientific_name + '<br>';
|
|
1126
|
-
}
|
|
1127
|
-
if (t.common_name) {
|
|
1128
|
-
mo_text += '- Common name: ' + t.common_name + '<br>';
|
|
1129
|
-
}
|
|
1130
|
-
if (t.rank) {
|
|
1131
|
-
mo_text += '- Rank: ' + t.rank + '<br>';
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
if (d.sequences) {
|
|
1136
|
-
for (i = 0; i < d.sequences.length; ++i) {
|
|
1137
|
-
mo_text += 'Sequence<br>';
|
|
1138
|
-
let s = d.sequences[i];
|
|
1139
|
-
if (s.accession) {
|
|
1140
|
-
if (s.accession.source) {
|
|
1141
|
-
mo_text += '- Accession [' + s.accession.source + ']: ' + s.accession.value + '<br>';
|
|
1142
|
-
} else {
|
|
1143
|
-
mo_text += '- Accession: ' + s.accession.value + '<br>';
|
|
1144
|
-
}
|
|
1145
|
-
if (s.accession.comment) {
|
|
1146
|
-
mo_text += '-- comment: ' + s.accession.comment + '<br>';
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
if (s.symbol) {
|
|
1150
|
-
mo_text += '- Symbol: ' + s.symbol + '<br>';
|
|
1151
|
-
}
|
|
1152
|
-
if (s.name) {
|
|
1153
|
-
mo_text += '- Name: ' + s.name + '<br>';
|
|
1154
|
-
}
|
|
1155
|
-
if (s.gene_name) {
|
|
1156
|
-
mo_text += '- Gene name: ' + s.gene_name + '<br>';
|
|
1157
|
-
}
|
|
1158
|
-
if (s.location) {
|
|
1159
|
-
mo_text += '- Location: ' + s.location + '<br>';
|
|
1160
|
-
}
|
|
1161
|
-
if (s.type) {
|
|
1162
|
-
mo_text += '- Type: ' + s.type + '<br>';
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
if (d.distributions) {
|
|
1167
|
-
let distributions = d.distributions;
|
|
1168
|
-
for (i = 0; i < distributions.length; ++i) {
|
|
1169
|
-
mo_text += 'Distribution: ';
|
|
1170
|
-
if (distributions[i].desc) {
|
|
1171
|
-
mo_text += distributions[i].desc + '<br>';
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
if (d.date) {
|
|
1176
|
-
mo_text += 'Date: ';
|
|
1177
|
-
let date = d.date;
|
|
1178
|
-
if (date.desc) {
|
|
1179
|
-
mo_text += date.desc + '<br>';
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
if (d.events) {
|
|
1183
|
-
mo_text += 'Events<br>';
|
|
1184
|
-
let ev = d.events;
|
|
1185
|
-
if (ev.type && ev.type.length > 0) {
|
|
1186
|
-
mo_text += '- Type: ' + ev.type + '<br>';
|
|
1187
|
-
}
|
|
1188
|
-
if (ev.duplications && ev.duplications > 0) {
|
|
1189
|
-
mo_text += '- Duplications: ' + ev.duplications + '<br>';
|
|
1190
|
-
}
|
|
1191
|
-
if (ev.speciations && ev.speciations > 0) {
|
|
1192
|
-
mo_text += '- Speciations: ' + ev.speciations + '<br>';
|
|
1193
|
-
}
|
|
1194
|
-
if (ev.losses && ev.losses > 0) {
|
|
1195
|
-
mo_text += '- Losses: ' + ev.losses + '<br>';
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
if (d.properties && d.properties.length > 0) {
|
|
1199
|
-
let propertiesLength = d.properties.length;
|
|
1200
|
-
for (i = 0; i < propertiesLength; ++i) {
|
|
1201
|
-
let property = d.properties[i];
|
|
1202
|
-
if (property.ref && property.value) {
|
|
1203
|
-
let prop_ref = property.ref
|
|
1204
|
-
if (prop_ref.indexOf(':') > 0) {
|
|
1205
|
-
prop_ref = prop_ref.substring(prop_ref.indexOf(':') + 1)
|
|
1206
|
-
}
|
|
1207
|
-
if (property.unit) {
|
|
1208
|
-
mo_text += prop_ref + ': ' + property.value + property.unit + '<br>';
|
|
1209
|
-
} else {
|
|
1210
|
-
mo_text += prop_ref + ': ' + property.value + '<br>';
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
if (d.children) {
|
|
1216
|
-
mo_text += 'Sum of Subtree Tips: ' + forester.calcSumOfAllExternalDescendants(d) + '<br>';
|
|
1217
|
-
}
|
|
1201
|
+
let mo_text = nodeDataText(d);
|
|
1218
1202
|
|
|
1219
1203
|
// Same label/value layout as the node-data dialog, so the two read alike.
|
|
1220
1204
|
let tip = _node_mouseover_div.node();
|
|
@@ -1359,7 +1343,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1359
1343
|
_vis = {candidates: [], byId: {}, colorId: null, shapeId: null, autoColorId: null,
|
|
1360
1344
|
labelRef: null, labelPrefix: null,
|
|
1361
1345
|
legendSortById: {}, colorModeById: {}, legendExpandedById: {},
|
|
1362
|
-
colorMemory: {}, colorNext: {}, shapeMemory: {}, shapeNext: {},
|
|
1346
|
+
colorMemory: {}, colorNext: {}, colorOverrides: {}, shapeMemory: {}, shapeNext: {},
|
|
1363
1347
|
hasStyles: false};
|
|
1364
1348
|
// The readable-name inference stands on its own: it applies even when
|
|
1365
1349
|
// the Color / Shape menus are disabled.
|
|
@@ -1440,6 +1424,325 @@ function (root, d3, forester, phyloXml) {
|
|
|
1440
1424
|
// values first met in a smaller view extend the memory with the next free
|
|
1441
1425
|
// palette slots. (On the launch view this assigns the sorted domain
|
|
1442
1426
|
// 0,1,2,... -- exactly the old fixed-palette behaviour.)
|
|
1427
|
+
// Clicking a legend colour opens a colour picker and repaints that value
|
|
1428
|
+
// everywhere -- the legend swatch and every node carrying it -- because
|
|
1429
|
+
// both read the same categoryScale.
|
|
1430
|
+
//
|
|
1431
|
+
// Built in-page rather than using <input type="color">, which was the first
|
|
1432
|
+
// version and was wrong on three counts a user noticed immediately: an OS
|
|
1433
|
+
// widget cannot follow the panel's light/dark choice, cannot carry a close
|
|
1434
|
+
// button of its own, and only reports its value on dismissal, so the tree
|
|
1435
|
+
// did not move while you were choosing. This one is a non-modal dialog --
|
|
1436
|
+
// NOT showModal(), whose backdrop would dim the very tree being previewed.
|
|
1437
|
+
function openColorPicker(current, valueLabel, anchor, onChange) {
|
|
1438
|
+
let anchorClass = anchor && anchor.getAttribute ? anchor.getAttribute('class') : null;
|
|
1439
|
+
let hsv = rgbToHsv(current);
|
|
1440
|
+
let shell = makeDialogShell(COLOR_PICKER, valueLabel, 212);
|
|
1441
|
+
let dialog = shell.dialog;
|
|
1442
|
+
|
|
1443
|
+
let sv = document.createElement('div');
|
|
1444
|
+
sv.className = 'aptx-cp-sv';
|
|
1445
|
+
let svKnob = document.createElement('div');
|
|
1446
|
+
svKnob.className = 'aptx-cp-knob';
|
|
1447
|
+
sv.appendChild(svKnob);
|
|
1448
|
+
|
|
1449
|
+
let hue = document.createElement('div');
|
|
1450
|
+
hue.className = 'aptx-cp-hue';
|
|
1451
|
+
let hueKnob = document.createElement('div');
|
|
1452
|
+
hueKnob.className = 'aptx-cp-knob';
|
|
1453
|
+
hueKnob.style.top = '6px';
|
|
1454
|
+
hue.appendChild(hueKnob);
|
|
1455
|
+
|
|
1456
|
+
let row = document.createElement('div');
|
|
1457
|
+
row.className = 'aptx-cp-row';
|
|
1458
|
+
let preview = document.createElement('div');
|
|
1459
|
+
preview.className = 'aptx-cp-prev';
|
|
1460
|
+
let hex = document.createElement('input');
|
|
1461
|
+
hex.className = 'aptx-cp-hex';
|
|
1462
|
+
hex.type = 'text';
|
|
1463
|
+
hex.spellcheck = false;
|
|
1464
|
+
hex.setAttribute('aria-label', 'hex colour');
|
|
1465
|
+
row.appendChild(preview);
|
|
1466
|
+
row.appendChild(hex);
|
|
1467
|
+
|
|
1468
|
+
// The palette the tree hands out, so "put it back to something that
|
|
1469
|
+
// belongs" is one click rather than a hunt in the gradient.
|
|
1470
|
+
let grid = document.createElement('div');
|
|
1471
|
+
grid.className = 'aptx-cp-grid';
|
|
1472
|
+
VIS_COLOR_PALETTE.forEach(function (c) {
|
|
1473
|
+
let b = document.createElement('button');
|
|
1474
|
+
b.type = 'button';
|
|
1475
|
+
b.className = 'aptx-cp-sw';
|
|
1476
|
+
b.style.background = c;
|
|
1477
|
+
b.title = c;
|
|
1478
|
+
b.addEventListener('click', function () {
|
|
1479
|
+
hsv = rgbToHsv(c);
|
|
1480
|
+
render(true);
|
|
1481
|
+
});
|
|
1482
|
+
grid.appendChild(b);
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
let note = document.createElement('div');
|
|
1486
|
+
note.className = 'aptx-cp-note';
|
|
1487
|
+
note.textContent = 'The tree updates as you pick.';
|
|
1488
|
+
|
|
1489
|
+
shell.body.appendChild(sv);
|
|
1490
|
+
shell.body.appendChild(hue);
|
|
1491
|
+
shell.body.appendChild(row);
|
|
1492
|
+
shell.body.appendChild(grid);
|
|
1493
|
+
shell.body.appendChild(note);
|
|
1494
|
+
|
|
1495
|
+
// Repainting a big tree on every pointer move would fall behind the
|
|
1496
|
+
// pointer, so the tree redraw is coalesced to one per frame while the
|
|
1497
|
+
// picker's own chrome updates immediately.
|
|
1498
|
+
let pendingColor = null;
|
|
1499
|
+
let frame = 0;
|
|
1500
|
+
|
|
1501
|
+
function render(applyNow) {
|
|
1502
|
+
let color = hsvToHex(hsv);
|
|
1503
|
+
sv.style.background = 'linear-gradient(to top,#000,rgba(0,0,0,0)),'
|
|
1504
|
+
+ 'linear-gradient(to right,#fff,rgba(255,255,255,0)),'
|
|
1505
|
+
+ 'hsl(' + hsv.h + ',100%,50%)';
|
|
1506
|
+
svKnob.style.left = (hsv.s * 100) + '%';
|
|
1507
|
+
svKnob.style.top = ((1 - hsv.v) * 100) + '%';
|
|
1508
|
+
svKnob.style.background = color;
|
|
1509
|
+
hueKnob.style.left = ((hsv.h / 360) * 100) + '%';
|
|
1510
|
+
hueKnob.style.background = 'hsl(' + hsv.h + ',100%,50%)';
|
|
1511
|
+
preview.style.background = color;
|
|
1512
|
+
if (document.activeElement !== hex) {
|
|
1513
|
+
hex.value = color;
|
|
1514
|
+
}
|
|
1515
|
+
if (applyNow) {
|
|
1516
|
+
pendingColor = color;
|
|
1517
|
+
if (!frame) {
|
|
1518
|
+
frame = requestAnimationFrame(function () {
|
|
1519
|
+
frame = 0;
|
|
1520
|
+
onChange(pendingColor);
|
|
1521
|
+
place(); // the legend may have just changed width
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function track(el, move) {
|
|
1528
|
+
el.addEventListener('pointerdown', function (event) {
|
|
1529
|
+
el.setPointerCapture(event.pointerId);
|
|
1530
|
+
move(event);
|
|
1531
|
+
event.preventDefault();
|
|
1532
|
+
});
|
|
1533
|
+
el.addEventListener('pointermove', function (event) {
|
|
1534
|
+
if (el.hasPointerCapture(event.pointerId)) {
|
|
1535
|
+
move(event);
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
track(sv, function (event) {
|
|
1540
|
+
let r = sv.getBoundingClientRect();
|
|
1541
|
+
hsv.s = Math.max(0, Math.min(1, (event.clientX - r.left) / r.width));
|
|
1542
|
+
hsv.v = 1 - Math.max(0, Math.min(1, (event.clientY - r.top) / r.height));
|
|
1543
|
+
render(true);
|
|
1544
|
+
});
|
|
1545
|
+
track(hue, function (event) {
|
|
1546
|
+
let r = hue.getBoundingClientRect();
|
|
1547
|
+
hsv.h = Math.max(0, Math.min(1, (event.clientX - r.left) / r.width)) * 360;
|
|
1548
|
+
render(true);
|
|
1549
|
+
});
|
|
1550
|
+
hex.addEventListener('input', function () {
|
|
1551
|
+
if (/^#[0-9a-f]{6}$/i.test(hex.value.trim())) {
|
|
1552
|
+
hsv = rgbToHsv(hex.value.trim());
|
|
1553
|
+
render(true);
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
let closed = false;
|
|
1558
|
+
function close() {
|
|
1559
|
+
document.removeEventListener('pointerdown', onOutside, true);
|
|
1560
|
+
document.removeEventListener('keydown', onKey, true);
|
|
1561
|
+
if (frame) {
|
|
1562
|
+
cancelAnimationFrame(frame);
|
|
1563
|
+
frame = 0;
|
|
1564
|
+
}
|
|
1565
|
+
if (closed) {
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
closed = true;
|
|
1569
|
+
if (dialog.open) {
|
|
1570
|
+
dialog.close();
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
// A non-modal dialog gets neither Escape nor a light-dismiss for free.
|
|
1574
|
+
function onKey(event) {
|
|
1575
|
+
if (event.key === 'Escape') {
|
|
1576
|
+
close();
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
function onOutside(event) {
|
|
1580
|
+
if (!dialog.contains(event.target)) {
|
|
1581
|
+
close();
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// The shell's own close button calls dialog.close() directly, which
|
|
1586
|
+
// would leave these page-level listeners behind on every dismissal
|
|
1587
|
+
// that is not Escape or an outside click. Both routes now run the same
|
|
1588
|
+
// teardown, and close() is idempotent.
|
|
1589
|
+
let shellClose = dialog.querySelector('.aptx-dialog-close');
|
|
1590
|
+
if (shellClose) {
|
|
1591
|
+
shellClose.addEventListener('click', close);
|
|
1592
|
+
}
|
|
1593
|
+
dialog.addEventListener('close', close);
|
|
1594
|
+
|
|
1595
|
+
render(false);
|
|
1596
|
+
dialog.show();
|
|
1597
|
+
place();
|
|
1598
|
+
|
|
1599
|
+
// Beside the legend it edits, never on top of it -- a picker that hides
|
|
1600
|
+
// the swatch it is changing defeats the live preview.
|
|
1601
|
+
//
|
|
1602
|
+
// Re-measured rather than remembered, for two reasons: every redraw
|
|
1603
|
+
// DESTROYS and rebuilds the legend group, so a held reference goes
|
|
1604
|
+
// stale and measures zero; and the card grows when the [reset colors]
|
|
1605
|
+
// chip appears on the first pick, which would leave a
|
|
1606
|
+
// positioned-once picker sitting over the counts. It only MOVES when
|
|
1607
|
+
// it would otherwise overlap, so the dialog does not chase the legend
|
|
1608
|
+
// around while the user drags.
|
|
1609
|
+
function place() {
|
|
1610
|
+
let el = anchorClass ? document.querySelector('g.' + anchorClass) : null;
|
|
1611
|
+
let box = el ? el.getBoundingClientRect() : null;
|
|
1612
|
+
if (!box || !box.width) {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
let now = dialog.getBoundingClientRect();
|
|
1616
|
+
let overlaps = now.width > 0
|
|
1617
|
+
&& !(now.left >= box.right || now.right <= box.left
|
|
1618
|
+
|| now.top >= box.bottom || now.bottom <= box.top);
|
|
1619
|
+
if (now.width > 0 && !overlaps) {
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
let w = dialog.offsetWidth;
|
|
1623
|
+
let h = dialog.offsetHeight;
|
|
1624
|
+
// to the right of the card, or to its left when that would leave
|
|
1625
|
+
// the viewport
|
|
1626
|
+
let left = box.right + 10;
|
|
1627
|
+
if (left + w > window.innerWidth - 6) {
|
|
1628
|
+
left = box.left - w - 10;
|
|
1629
|
+
}
|
|
1630
|
+
setStyles(dialog, {
|
|
1631
|
+
'position': 'fixed',
|
|
1632
|
+
'margin': '0',
|
|
1633
|
+
'left': Math.max(6, Math.min(window.innerWidth - w - 6, left)) + 'px',
|
|
1634
|
+
'top': Math.max(6, Math.min(window.innerHeight - h - 6, box.top)) + 'px'
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
setTimeout(function () {
|
|
1638
|
+
document.addEventListener('pointerdown', onOutside, true);
|
|
1639
|
+
document.addEventListener('keydown', onKey, true);
|
|
1640
|
+
}, 0);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// HSV, not d3's HSL: a saturation/value square is what a colour picker
|
|
1644
|
+
// shows, and round-tripping through HSL would drift the knob.
|
|
1645
|
+
function rgbToHsv(c) {
|
|
1646
|
+
let col = null;
|
|
1647
|
+
try {
|
|
1648
|
+
col = d3.color(c);
|
|
1649
|
+
} catch {
|
|
1650
|
+
col = null;
|
|
1651
|
+
}
|
|
1652
|
+
col = col ? col.rgb() : {r: 0, g: 0, b: 0};
|
|
1653
|
+
let r = col.r / 255, g = col.g / 255, b = col.b / 255;
|
|
1654
|
+
let max = Math.max(r, g, b), min = Math.min(r, g, b), d = max - min;
|
|
1655
|
+
let h = 0;
|
|
1656
|
+
if (d > 0) {
|
|
1657
|
+
if (max === r) {
|
|
1658
|
+
h = 60 * (((g - b) / d) % 6);
|
|
1659
|
+
} else if (max === g) {
|
|
1660
|
+
h = 60 * (((b - r) / d) + 2);
|
|
1661
|
+
} else {
|
|
1662
|
+
h = 60 * (((r - g) / d) + 4);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return {h: (h + 360) % 360, s: max === 0 ? 0 : d / max, v: max};
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
function hsvToHex(hsv) {
|
|
1669
|
+
let c = hsv.v * hsv.s;
|
|
1670
|
+
let x = c * (1 - Math.abs(((hsv.h / 60) % 2) - 1));
|
|
1671
|
+
let m = hsv.v - c;
|
|
1672
|
+
let i = Math.floor(hsv.h / 60) % 6;
|
|
1673
|
+
let rgb = [[c, x, 0], [x, c, 0], [0, c, x], [0, x, c], [x, 0, c], [c, 0, x]][i];
|
|
1674
|
+
return '#' + rgb.map(function (v) {
|
|
1675
|
+
return Math.round((v + m) * 255).toString(16).padStart(2, '0');
|
|
1676
|
+
}).join('');
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// The key a value is remembered under. Property values fold to lower case
|
|
1680
|
+
// (the same fold the value grouping uses), so a legend override applies to
|
|
1681
|
+
// every case variant of that value -- which is what the legend shows as
|
|
1682
|
+
// one row anyway. ONE definition: the candidate builder and the legend's
|
|
1683
|
+
// colour picker must agree, or an override would be written under a key
|
|
1684
|
+
// the scale never reads.
|
|
1685
|
+
function visMemoryKey(vis, v) {
|
|
1686
|
+
return vis.kind === 'property' ? String(v).toLowerCase() : v;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Colours live in a per-candidate MEMORY map rather than being computed
|
|
1690
|
+
// from the value's position, so they survive a re-render, a Color-by
|
|
1691
|
+
// switch away and back, and a legend re-sort. That is also what makes a
|
|
1692
|
+
// user override possible at all: writing one entry and rebuilding is
|
|
1693
|
+
// enough, and every consumer (the legend swatch AND the node colour) reads
|
|
1694
|
+
// through the same scale.
|
|
1695
|
+
function buildCategoryScale(vis) {
|
|
1696
|
+
let mem = _vis.colorMemory[vis.id] || (_vis.colorMemory[vis.id] = {});
|
|
1697
|
+
let next = _vis.colorNext[vis.id] || 0;
|
|
1698
|
+
vis.values.forEach(function (v) {
|
|
1699
|
+
let k = visMemoryKey(vis, v);
|
|
1700
|
+
if (!(k in mem)) {
|
|
1701
|
+
mem[k] = extendedPaletteColor(next++);
|
|
1702
|
+
}
|
|
1703
|
+
});
|
|
1704
|
+
_vis.colorNext[vis.id] = next;
|
|
1705
|
+
vis.categoryScale = d3.scaleOrdinal()
|
|
1706
|
+
.domain(vis.values)
|
|
1707
|
+
.range(vis.values.map(function (v) {
|
|
1708
|
+
return mem[visMemoryKey(vis, v)];
|
|
1709
|
+
}));
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// What the PALETTE had assigned before the user overrode it, keyed the
|
|
1713
|
+
// same way as colorMemory. Storing the displaced colour rather than the
|
|
1714
|
+
// chosen one is what makes reset exact: deleting the memory entry instead
|
|
1715
|
+
// and re-running buildCategoryScale would hand the value the next UNUSED
|
|
1716
|
+
// palette colour, since colorNext has moved on -- so "reset" would restore
|
|
1717
|
+
// a palette colour, just not the one that was there.
|
|
1718
|
+
function hasColorOverrides(vis) {
|
|
1719
|
+
let o = _vis && _vis.colorOverrides && _vis.colorOverrides[vis.id];
|
|
1720
|
+
return !!(o && Object.keys(o).length > 0);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function setColorOverride(vis, value, color) {
|
|
1724
|
+
let o = _vis.colorOverrides[vis.id] || (_vis.colorOverrides[vis.id] = {});
|
|
1725
|
+
let k = visMemoryKey(vis, value);
|
|
1726
|
+
if (!(k in o)) {
|
|
1727
|
+
// first override of this value: remember what it displaced
|
|
1728
|
+
o[k] = _vis.colorMemory[vis.id][k];
|
|
1729
|
+
}
|
|
1730
|
+
_vis.colorMemory[vis.id][k] = color;
|
|
1731
|
+
buildCategoryScale(vis);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function clearColorOverrides(vis) {
|
|
1735
|
+
let o = _vis.colorOverrides[vis.id];
|
|
1736
|
+
if (!o) {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
Object.keys(o).forEach(function (k) {
|
|
1740
|
+
_vis.colorMemory[vis.id][k] = o[k];
|
|
1741
|
+
});
|
|
1742
|
+
delete _vis.colorOverrides[vis.id];
|
|
1743
|
+
buildCategoryScale(vis);
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1443
1746
|
function computeVisualizationCandidates(viewRoot) {
|
|
1444
1747
|
_vis.candidates = [];
|
|
1445
1748
|
_vis.byId = {};
|
|
@@ -1456,24 +1759,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
1456
1759
|
.domain([nums[0], mean, nums[nums.length - 1]]);
|
|
1457
1760
|
}
|
|
1458
1761
|
function memoryKey(v) {
|
|
1459
|
-
return c
|
|
1762
|
+
return visMemoryKey(c, v);
|
|
1460
1763
|
}
|
|
1461
1764
|
// a switchable candidate needs both scales standing by
|
|
1462
1765
|
if (c.colorMode === 'category' || c.switchable) {
|
|
1463
|
-
|
|
1464
|
-
let next = _vis.colorNext[c.id] || 0;
|
|
1465
|
-
c.values.forEach(function (v) {
|
|
1466
|
-
let k = memoryKey(v);
|
|
1467
|
-
if (!(k in mem)) {
|
|
1468
|
-
mem[k] = extendedPaletteColor(next++);
|
|
1469
|
-
}
|
|
1470
|
-
});
|
|
1471
|
-
_vis.colorNext[c.id] = next;
|
|
1472
|
-
c.categoryScale = d3.scaleOrdinal()
|
|
1473
|
-
.domain(c.values)
|
|
1474
|
-
.range(c.values.map(function (v) {
|
|
1475
|
-
return mem[memoryKey(v)];
|
|
1476
|
-
}));
|
|
1766
|
+
buildCategoryScale(c);
|
|
1477
1767
|
}
|
|
1478
1768
|
if (c.shape) {
|
|
1479
1769
|
let mem = _vis.shapeMemory[c.id] || (_vis.shapeMemory[c.id] = {});
|
|
@@ -1669,6 +1959,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
1669
1959
|
}
|
|
1670
1960
|
});
|
|
1671
1961
|
}
|
|
1962
|
+
if (kind === 'color' && !isRange && hasColorOverrides(vis)) {
|
|
1963
|
+
chips.push({
|
|
1964
|
+
text: '[reset colors]',
|
|
1965
|
+
tip: 'put the palette colors back',
|
|
1966
|
+
act: function () {
|
|
1967
|
+
clearColorOverrides(vis);
|
|
1968
|
+
update(null, 0);
|
|
1969
|
+
}
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1672
1972
|
if (!isRange && (hidden > 0 || (_vis.legendExpandedById[vis.id] && vis.values.length > LEGEND_MAX_ROWS))) {
|
|
1673
1973
|
chips.push({
|
|
1674
1974
|
text: hidden > 0 ? '[+' + hidden + ' more]' : '[fewer]',
|
|
@@ -1797,11 +2097,38 @@ function (root, d3, forester, phyloXml) {
|
|
|
1797
2097
|
.style('fill', 'none')
|
|
1798
2098
|
.style('stroke', frame);
|
|
1799
2099
|
} else {
|
|
1800
|
-
g.append('rect')
|
|
2100
|
+
let sw = g.append('rect')
|
|
1801
2101
|
.attr('x', x + PAD).attr('y', swY)
|
|
1802
2102
|
.attr('width', SWATCH).attr('height', SWATCH)
|
|
1803
2103
|
.attr('rx', 2)
|
|
1804
2104
|
.style('fill', vis.categoryScale(r.value));
|
|
2105
|
+
if (kind === 'color') {
|
|
2106
|
+
// A 9px square is a poor target, so the hit area is the
|
|
2107
|
+
// swatch plus its label -- and it is a separate transparent
|
|
2108
|
+
// rect so the visible swatch keeps its exact geometry.
|
|
2109
|
+
let value = r.value;
|
|
2110
|
+
g.append('rect')
|
|
2111
|
+
.attr('x', x + PAD - 2).attr('y', swY - 2)
|
|
2112
|
+
.attr('width', SWATCH + GAP + legendTextWidth(r.text, rowFont) + 4)
|
|
2113
|
+
.attr('height', SWATCH + 4)
|
|
2114
|
+
.style('fill', 'transparent')
|
|
2115
|
+
.style('cursor', 'pointer')
|
|
2116
|
+
.on('mousedown', function (event) {
|
|
2117
|
+
event.stopPropagation(); // picking a colour is not a drag
|
|
2118
|
+
})
|
|
2119
|
+
.on('click', function (event) {
|
|
2120
|
+
event.stopPropagation();
|
|
2121
|
+
// the legend CARD, not the swatch: a picker that
|
|
2122
|
+
// covers the legend hides the very swatch it changes
|
|
2123
|
+
openColorPicker(vis.categoryScale(value), r.text, this.parentNode, function (picked) {
|
|
2124
|
+
setColorOverride(vis, value, picked);
|
|
2125
|
+
update(null, 0);
|
|
2126
|
+
});
|
|
2127
|
+
})
|
|
2128
|
+
.append('title')
|
|
2129
|
+
.text('click to change the color for "' + r.text + '"');
|
|
2130
|
+
sw.style('cursor', 'pointer');
|
|
2131
|
+
}
|
|
1805
2132
|
}
|
|
1806
2133
|
g.append('text')
|
|
1807
2134
|
.attr('x', x + PAD + SWATCH + GAP).attr('y', baseline)
|
|
@@ -1880,6 +2207,82 @@ function (root, d3, forester, phyloXml) {
|
|
|
1880
2207
|
|
|
1881
2208
|
// --------------------------------------------------------------
|
|
1882
2209
|
|
|
2210
|
+
// Redraw -- at once on a small tree, coalesced and deferred on a big one.
|
|
2211
|
+
//
|
|
2212
|
+
// Two things this buys on a big tree, both measured on the 18,512-node
|
|
2213
|
+
// BV-BRC tree where one update() costs over a second:
|
|
2214
|
+
//
|
|
2215
|
+
// - Coalescing. Several handlers redraw more than once per click:
|
|
2216
|
+
// nine of them run search0(), search1() and update() -- three full
|
|
2217
|
+
// redraws, since runSearches() ends in its own update() -- and the
|
|
2218
|
+
// Visualizations and Auto-hide toggles call update() twice back to
|
|
2219
|
+
// back. A slider drag fires an update per input event. Every call
|
|
2220
|
+
// made in the same tick now collapses into ONE update().
|
|
2221
|
+
// - The yield. The redraw runs behind the "working" card on the next
|
|
2222
|
+
// frame, so a click is answered by a visible card instead of a page
|
|
2223
|
+
// that has stopped responding.
|
|
2224
|
+
//
|
|
2225
|
+
// Only for callers that read nothing back afterwards. zoomToFit() and
|
|
2226
|
+
// the layout switches update() and then read the fresh layout in the
|
|
2227
|
+
// same tick, so they keep calling update() directly -- deferring those
|
|
2228
|
+
// would have them measure a tree that has not been drawn yet.
|
|
2229
|
+
//
|
|
2230
|
+
// Arguments merge conservatively: any caller that wants the width
|
|
2231
|
+
// recalculated gets it, the shortest transition wins, and a named source
|
|
2232
|
+
// survives a null one.
|
|
2233
|
+
function scheduleUpdate(source, transitionDuration, doNotRecalculateWidth) {
|
|
2234
|
+
if (_initializing) {
|
|
2235
|
+
runAfterUpdate(); // nothing pending; anything queued can run now
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
if (!_basicTreeProperties || _basicTreeProperties.nodeCount < BIG_TREE_NODES) {
|
|
2239
|
+
update(source, transitionDuration, doNotRecalculateWidth);
|
|
2240
|
+
runAfterUpdate();
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
let dur = (transitionDuration === undefined) ? TRANSITION_DURATION_DEFAULT : transitionDuration;
|
|
2244
|
+
if (_pendingUpdate) {
|
|
2245
|
+
_pendingUpdate.source = _pendingUpdate.source || source;
|
|
2246
|
+
_pendingUpdate.duration = Math.min(_pendingUpdate.duration, dur);
|
|
2247
|
+
_pendingUpdate.noWidth = _pendingUpdate.noWidth && (doNotRecalculateWidth === true);
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
2250
|
+
_pendingUpdate = {source: source, duration: dur, noWidth: doNotRecalculateWidth === true};
|
|
2251
|
+
let seq = _launchSeq;
|
|
2252
|
+
showBusy(_container, 'Redrawing ' + _basicTreeProperties.nodeCount.toLocaleString() + ' nodes');
|
|
2253
|
+
afterPaint(function () {
|
|
2254
|
+
let p = _pendingUpdate;
|
|
2255
|
+
_pendingUpdate = null;
|
|
2256
|
+
if (seq !== _launchSeq || !p) {
|
|
2257
|
+
return; // the view was torn down or replaced meanwhile
|
|
2258
|
+
}
|
|
2259
|
+
try {
|
|
2260
|
+
update(p.source, p.duration, p.noWidth);
|
|
2261
|
+
runAfterUpdate();
|
|
2262
|
+
} finally {
|
|
2263
|
+
hideBusy();
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// Run fn once the redraw has happened: now, if none is pending (a small
|
|
2269
|
+
// tree, or nothing scheduled), else after the deferred update() runs.
|
|
2270
|
+
function afterUpdate(fn) {
|
|
2271
|
+
if (_pendingUpdate) {
|
|
2272
|
+
_afterUpdate.push(fn);
|
|
2273
|
+
} else {
|
|
2274
|
+
fn();
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function runAfterUpdate() {
|
|
2279
|
+
let fns = _afterUpdate;
|
|
2280
|
+
_afterUpdate = [];
|
|
2281
|
+
fns.forEach(function (fn) {
|
|
2282
|
+
fn();
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
|
|
1883
2286
|
function update(source, transitionDuration, doNotRecalculateWidth) {
|
|
1884
2287
|
|
|
1885
2288
|
if (!source) {
|
|
@@ -2060,14 +2463,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
2060
2463
|
});
|
|
2061
2464
|
|
|
2062
2465
|
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2065
|
-
//
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2466
|
+
// Only the three elements EVERY node needs are created here. The other
|
|
2467
|
+
// six -- the found halo, the four text labels and the support dot --
|
|
2468
|
+
// are created on demand by syncOptionalNodeChildren() below.
|
|
2469
|
+
//
|
|
2470
|
+
// They used to be appended unconditionally, nine elements per node
|
|
2471
|
+
// whether or not they would ever show anything, and on a real BV-BRC
|
|
2472
|
+
// tree (18,512 nodes) that was 222,197 SVG elements of which HALF were
|
|
2473
|
+
// inert: 74,052 of 74,064 <text> elements empty, 43,413 circles at
|
|
2474
|
+
// r=0. The cost is not creating them -- allocating that many elements
|
|
2475
|
+
// takes about two seconds -- it is that every later redraw walks them
|
|
2476
|
+
// all, setting attributes and running an accessor per element. Dropping
|
|
2477
|
+
// the inert half measured 2.7x faster redraws.
|
|
2071
2478
|
nodeEnter.append('path')
|
|
2072
2479
|
.attr('d', 'M0,0');
|
|
2073
2480
|
|
|
@@ -2091,33 +2498,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
2091
2498
|
return 0;
|
|
2092
2499
|
});
|
|
2093
2500
|
|
|
2094
|
-
nodeEnter.append('text')
|
|
2095
|
-
.attr('class', 'extlabel')
|
|
2096
|
-
.attr('text-anchor', function (d) {
|
|
2097
|
-
return d.children ? "end" : "start";
|
|
2098
|
-
})
|
|
2099
|
-
.style('font-family', _state.defaultFont)
|
|
2100
|
-
.style('fill-opacity', 0.5);
|
|
2101
|
-
|
|
2102
|
-
nodeEnter.append('text')
|
|
2103
|
-
.attr('class', 'bllabel')
|
|
2104
|
-
.style('font-family', _state.defaultFont)
|
|
2105
|
-
.style('fill-opacity', 0.5);
|
|
2106
|
-
|
|
2107
|
-
nodeEnter.append('text')
|
|
2108
|
-
.attr('class', 'conflabel')
|
|
2109
|
-
.attr('text-anchor', 'middle')
|
|
2110
|
-
.style('font-family', _state.defaultFont);
|
|
2111
|
-
|
|
2112
|
-
nodeEnter.append('circle')
|
|
2113
|
-
.attr('class', 'suppdot')
|
|
2114
|
-
.style('pointer-events', 'none');
|
|
2115
|
-
|
|
2116
|
-
nodeEnter.append('text')
|
|
2117
|
-
.attr('class', 'brancheventlabel')
|
|
2118
|
-
.attr('text-anchor', 'middle')
|
|
2119
|
-
.style('font-family', _state.defaultFont);
|
|
2120
|
-
|
|
2121
2501
|
// Grab the exit selection BEFORE merging. In d3 v3 the exit selection
|
|
2122
2502
|
// survived on the merged result, but in v4+ merge() returns a NEW
|
|
2123
2503
|
// selection with no exit, so calling .exit() after this line silently
|
|
@@ -2130,6 +2510,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
2130
2510
|
// merge them before the shared styling/positioning below.
|
|
2131
2511
|
node = nodeEnter.merge(node);
|
|
2132
2512
|
|
|
2513
|
+
// Create the optional children only where they will show something,
|
|
2514
|
+
// and drop them where they will not. Must run BEFORE node.transition()
|
|
2515
|
+
// below, so a child created now still gets its attributes set by the
|
|
2516
|
+
// .select() calls that follow.
|
|
2517
|
+
syncOptionalNodeChildren(node);
|
|
2518
|
+
|
|
2133
2519
|
node.select("text.extlabel")
|
|
2134
2520
|
.style('font-size', function (d) {
|
|
2135
2521
|
let style = nodeStyle(d);
|
|
@@ -2303,6 +2689,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
2303
2689
|
}
|
|
2304
2690
|
});
|
|
2305
2691
|
|
|
2692
|
+
if (anySearchHits()) {
|
|
2306
2693
|
node.select('circle.foundHalo')
|
|
2307
2694
|
.attr('class', function (d) {
|
|
2308
2695
|
// the animated class ONLY on hits: hundreds of idle infinite
|
|
@@ -2319,6 +2706,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
2319
2706
|
.style('fill', function (d) {
|
|
2320
2707
|
return isNodeFound(d) ? getFoundColor(d) : 'none';
|
|
2321
2708
|
});
|
|
2709
|
+
}
|
|
2322
2710
|
|
|
2323
2711
|
node.select('circle.nodeCircle')
|
|
2324
2712
|
.attr('r', function (d) {
|
|
@@ -2355,19 +2743,21 @@ function (root, d3, forester, phyloXml) {
|
|
|
2355
2743
|
|
|
2356
2744
|
nodeUpdate.select('text.extlabel')
|
|
2357
2745
|
.text(function (d) {
|
|
2358
|
-
|
|
2359
|
-
return makeNodeLabel(d);
|
|
2360
|
-
}
|
|
2746
|
+
return d._extLabelText; // computed in syncOptionalNodeChildren
|
|
2361
2747
|
});
|
|
2362
2748
|
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
.text
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
.text
|
|
2749
|
+
// Guarded, not just passed a null accessor: syncOptionalNodeChildren
|
|
2750
|
+
// has already removed these elements when the switch is off, and a
|
|
2751
|
+
// .select() that finds nothing still walks every node to find out.
|
|
2752
|
+
if (_state.showBranchLengthValues) {
|
|
2753
|
+
nodeUpdate.select('text.bllabel').text(makeBranchLengthLabel);
|
|
2754
|
+
}
|
|
2755
|
+
if (_state.showConfidenceValues) {
|
|
2756
|
+
nodeUpdate.select('text.conflabel').text(makeConfidenceValuesLabel);
|
|
2757
|
+
}
|
|
2758
|
+
if (_state.showBranchEvents) {
|
|
2759
|
+
nodeUpdate.select('text.brancheventlabel').text(makeBranchEventsLabel);
|
|
2760
|
+
}
|
|
2371
2761
|
|
|
2372
2762
|
let drawShapes = _state.showVisualizations || stylesActive();
|
|
2373
2763
|
nodeUpdate.select('path')
|
|
@@ -2376,16 +2766,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
2376
2766
|
.style('fill', drawShapes ? makeNodeFillColor : null)
|
|
2377
2767
|
.attr('d', drawShapes ? makeNodeVisShape : null);
|
|
2378
2768
|
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2769
|
+
// Collapse the vis shape on internal nodes that no longer have one.
|
|
2770
|
+
// This used to be a node.each() that built a fresh d3 selection AND a
|
|
2771
|
+
// fresh transition per internal node -- 5,265 of them on a big tree,
|
|
2772
|
+
// and a full 18k-node walk even when drawShapes made it a no-op. One
|
|
2773
|
+
// filtered selection with one transition does the same thing.
|
|
2774
|
+
if (!drawShapes) {
|
|
2775
|
+
node.filter(function (d) {
|
|
2776
|
+
return d.children && makeNodeVisShape(d) === null;
|
|
2777
|
+
}).select('path').transition().duration(transitionDuration)
|
|
2778
|
+
.attr('d', 'M0,0');
|
|
2779
|
+
}
|
|
2389
2780
|
|
|
2390
2781
|
// Departing nodes are removed OUTRIGHT rather than on the end of a
|
|
2391
2782
|
// transition. Going to a subtree (or deleting one) fires several updates
|
|
@@ -2397,9 +2788,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
2397
2788
|
// way; nodes now match.
|
|
2398
2789
|
nodeExitSelection.remove();
|
|
2399
2790
|
|
|
2791
|
+
// No .attr('d', elbow) before the join: it ran elbow() for all 18k+
|
|
2792
|
+
// links against the PRE-JOIN data and the transition below overwrote
|
|
2793
|
+
// the result, so every redraw computed the same geometry twice.
|
|
2794
|
+
// stroke-width used to ride along here and is applied on the merged
|
|
2795
|
+
// selection instead -- it is the only place existing links get it.
|
|
2400
2796
|
let link = _svgGroup.selectAll('path.link')
|
|
2401
|
-
.attr('d', elbow)
|
|
2402
|
-
.attr('stroke-width', makeBranchWidth)
|
|
2403
2797
|
.data(links, function (d) {
|
|
2404
2798
|
return d.target.id;
|
|
2405
2799
|
});
|
|
@@ -2423,6 +2817,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
2423
2817
|
|
|
2424
2818
|
link = linkEnter.merge(link);
|
|
2425
2819
|
|
|
2820
|
+
link.attr('stroke-width', makeBranchWidth);
|
|
2821
|
+
|
|
2426
2822
|
link.transition()
|
|
2427
2823
|
.duration(transitionDuration)
|
|
2428
2824
|
.attr('stroke', makeBranchColor)
|
|
@@ -2877,6 +3273,121 @@ function (root, d3, forester, phyloXml) {
|
|
|
2877
3273
|
return false;
|
|
2878
3274
|
}
|
|
2879
3275
|
|
|
3276
|
+
// The six per-node children that are usually not needed. Whether one is
|
|
3277
|
+
// wanted is either a GLOBAL switch (three of the four labels, the support
|
|
3278
|
+
// dot) -- in which case the whole class can be dropped in a single
|
|
3279
|
+
// selectAll().remove() rather than a per-node test -- or a per-node
|
|
3280
|
+
// question (the external label's text, and whether a search hit this node).
|
|
3281
|
+
//
|
|
3282
|
+
// The external label's text is computed HERE and stashed, because it is the
|
|
3283
|
+
// one predicate that costs something and the update below would otherwise
|
|
3284
|
+
// compute it a second time.
|
|
3285
|
+
function anySearchHits() {
|
|
3286
|
+
return (_foundNodes0 && _foundNodes0.size > 0)
|
|
3287
|
+
|| (_foundNodes1 && _foundNodes1.size > 0);
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
function syncOptionalNodeChildren(node) {
|
|
3291
|
+
let wantBl = _state.showBranchLengthValues === true;
|
|
3292
|
+
let wantConf = _state.showConfidenceValues === true;
|
|
3293
|
+
let wantEvent = _state.showBranchEvents === true;
|
|
3294
|
+
let wantDot = _state.showSupportDots === true;
|
|
3295
|
+
|
|
3296
|
+
// whole-class removals first: one pass, no per-node callback
|
|
3297
|
+
if (!wantBl) {
|
|
3298
|
+
node.selectAll('text.bllabel').remove();
|
|
3299
|
+
}
|
|
3300
|
+
if (!wantConf) {
|
|
3301
|
+
node.selectAll('text.conflabel').remove();
|
|
3302
|
+
}
|
|
3303
|
+
if (!wantEvent) {
|
|
3304
|
+
node.selectAll('text.brancheventlabel').remove();
|
|
3305
|
+
}
|
|
3306
|
+
if (!wantDot) {
|
|
3307
|
+
node.selectAll('circle.suppdot').remove();
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
// No search running means no halo anywhere -- one pass instead of
|
|
3311
|
+
// 18,512 set lookups plus 18,512 DOM queries.
|
|
3312
|
+
let anyFound = anySearchHits();
|
|
3313
|
+
if (!anyFound) {
|
|
3314
|
+
node.selectAll('circle.foundHalo').remove();
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
// Raw DOM inside the loop, not d3 selections: this runs once per node
|
|
3318
|
+
// per redraw, and wrapping each node in a d3 selection just to ask
|
|
3319
|
+
// whether a child exists costs more than the question is worth.
|
|
3320
|
+
let font = _state.defaultFont;
|
|
3321
|
+
node.each(function (d) {
|
|
3322
|
+
let g = this;
|
|
3323
|
+
|
|
3324
|
+
// The label the update is about to draw. Empty is the common case
|
|
3325
|
+
// on a big tree: Auto-hide Labels blanks them exactly when there
|
|
3326
|
+
// are most nodes to blank.
|
|
3327
|
+
d._extLabelText = (!_state.dynahide || !d.hide) ? (makeNodeLabel(d) || '') : '';
|
|
3328
|
+
let ext = g.querySelector('text.extlabel');
|
|
3329
|
+
if (d._extLabelText !== '') {
|
|
3330
|
+
if (!ext) {
|
|
3331
|
+
d3.select(g).append('text')
|
|
3332
|
+
.attr('class', 'extlabel')
|
|
3333
|
+
.attr('text-anchor', d.children ? 'end' : 'start')
|
|
3334
|
+
.style('font-family', font)
|
|
3335
|
+
.style('fill-opacity', 0.5);
|
|
3336
|
+
}
|
|
3337
|
+
} else if (ext) {
|
|
3338
|
+
ext.remove();
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
if (anyFound) {
|
|
3342
|
+
// The pulse behind a search hit. It must be FIRST in the group
|
|
3343
|
+
// so everything else draws over it -- appending would put it
|
|
3344
|
+
// on top.
|
|
3345
|
+
let halo = g.querySelector('circle.foundHalo');
|
|
3346
|
+
if (isNodeFound(d)) {
|
|
3347
|
+
if (!halo) {
|
|
3348
|
+
d3.select(g).insert('circle', ':first-child')
|
|
3349
|
+
.attr('class', 'foundHalo aptx-found-halo')
|
|
3350
|
+
.attr('r', 0)
|
|
3351
|
+
.style('pointer-events', 'none');
|
|
3352
|
+
}
|
|
3353
|
+
} else if (halo) {
|
|
3354
|
+
halo.remove();
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3358
|
+
if (wantBl && !g.querySelector('text.bllabel')) {
|
|
3359
|
+
d3.select(g).append('text')
|
|
3360
|
+
.attr('class', 'bllabel')
|
|
3361
|
+
.style('font-family', font)
|
|
3362
|
+
.style('fill-opacity', 0.5);
|
|
3363
|
+
}
|
|
3364
|
+
if (wantConf && !g.querySelector('text.conflabel')) {
|
|
3365
|
+
d3.select(g).append('text')
|
|
3366
|
+
.attr('class', 'conflabel')
|
|
3367
|
+
.attr('text-anchor', 'middle')
|
|
3368
|
+
.style('font-family', font);
|
|
3369
|
+
}
|
|
3370
|
+
if (wantEvent && !g.querySelector('text.brancheventlabel')) {
|
|
3371
|
+
d3.select(g).append('text')
|
|
3372
|
+
.attr('class', 'brancheventlabel')
|
|
3373
|
+
.attr('text-anchor', 'middle')
|
|
3374
|
+
.style('font-family', font);
|
|
3375
|
+
}
|
|
3376
|
+
if (wantDot) {
|
|
3377
|
+
let dot = g.querySelector('circle.suppdot');
|
|
3378
|
+
if (showSupportDot(d)) {
|
|
3379
|
+
if (!dot) {
|
|
3380
|
+
d3.select(g).append('circle')
|
|
3381
|
+
.attr('class', 'suppdot')
|
|
3382
|
+
.style('pointer-events', 'none');
|
|
3383
|
+
}
|
|
3384
|
+
} else if (dot) {
|
|
3385
|
+
dot.remove();
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
|
|
2880
3391
|
let makeNodeLabel = function (phynode) {
|
|
2881
3392
|
if (!_state.showExternalLabels && !(phynode.children)) {
|
|
2882
3393
|
return null;
|
|
@@ -3253,6 +3764,49 @@ function (root, d3, forester, phyloXml) {
|
|
|
3253
3764
|
// One list, because there is one config object. Checking each of the two old
|
|
3254
3765
|
// bags only against its own list meant the right name in the wrong bag was
|
|
3255
3766
|
// silently ignored -- the very failure this list exists to prevent.
|
|
3767
|
+
// THE one source for the parse-time defaults. initializeSettings runs from
|
|
3768
|
+
// launch(), which is AFTER launchArchaeopteryx has already parsed, and
|
|
3769
|
+
// parseTree is a public entry point that docs/open.html calls directly --
|
|
3770
|
+
// so a second copy of these literals in either place is a default that
|
|
3771
|
+
// cannot take effect. (The desktop's command-line launch drifted from its
|
|
3772
|
+
// File>Open exactly this way, on a constant that had been hardcoded false
|
|
3773
|
+
// since its config parser was removed.) Two equal literals is the bug;
|
|
3774
|
+
// one object read by both is the fix.
|
|
3775
|
+
const PARSE_DEFAULTS = {
|
|
3776
|
+
internalNumericLabels: 'auto'
|
|
3777
|
+
};
|
|
3778
|
+
|
|
3779
|
+
// Keys that still WORK but have moved, or have been retired outright.
|
|
3780
|
+
// Unlike REMOVED_CONFIG these do not throw: they are translated (or
|
|
3781
|
+
// ignored) with a warning, so an upgrade does not break a working embed
|
|
3782
|
+
// over a rename. An entry without a "to" has no replacement -- it is
|
|
3783
|
+
// accepted and dropped.
|
|
3784
|
+
const DEPRECATED_CONFIG = {
|
|
3785
|
+
nhConfidenceValuesInBrackets: {
|
|
3786
|
+
// Retired in 3.1.0. It gated whether "[95]" after a node is read
|
|
3787
|
+
// as a confidence, and setting it false did not reinterpret the
|
|
3788
|
+
// bracket -- it DISCARDED it, so the flag's only power was to
|
|
3789
|
+
// throw support values away. Non-numeric brackets (plain Newick
|
|
3790
|
+
// comments) are ignored either way, and NHX / BEAST blobs go
|
|
3791
|
+
// through a different path entirely, so there is no tree for
|
|
3792
|
+
// which false was the right answer. Its one real purpose was
|
|
3793
|
+
// historical: it used to be mutually exclusive with
|
|
3794
|
+
// nhConfidenceValuesAsInternalNames, and that guard is gone.
|
|
3795
|
+
note: 'bracketed values such as "[95]" are always read as confidences now; the option had no other effect'
|
|
3796
|
+
},
|
|
3797
|
+
nhConfidenceValuesAsInternalNames: {
|
|
3798
|
+
to: 'internalNumericLabels',
|
|
3799
|
+
// true meant "read every numeric internal label as a confidence",
|
|
3800
|
+
// which is exactly 'confidence'. It is NOT 'auto': auto is
|
|
3801
|
+
// all-or-nothing and promotes nothing in a mixed tree, so a caller
|
|
3802
|
+
// silently moved to auto would lose promotions they had.
|
|
3803
|
+
map: function (v) {
|
|
3804
|
+
return v === true ? 'confidence' : PARSE_DEFAULTS.internalNumericLabels;
|
|
3805
|
+
},
|
|
3806
|
+
note: 'use \'confidence\' for what this flag did (\'auto\' is the new default, and does not promote mixed trees)'
|
|
3807
|
+
}
|
|
3808
|
+
};
|
|
3809
|
+
|
|
3256
3810
|
const REMOVED_CONFIG = {
|
|
3257
3811
|
circular: 'renamed to "layout": use layout: "circular"',
|
|
3258
3812
|
circularDisplay: 'replaced by "layout": use layout: "circular" (or "rectangular" / "unrooted")',
|
|
@@ -3428,9 +3982,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
3428
3982
|
'enableSubtreeDeletion',
|
|
3429
3983
|
'enableVisualizations',
|
|
3430
3984
|
'initialVisualization',
|
|
3985
|
+
'internalNumericLabels',
|
|
3431
3986
|
'ladderizeTree',
|
|
3432
|
-
'nhConfidenceValuesAsInternalNames',
|
|
3433
|
-
'nhConfidenceValuesInBrackets',
|
|
3434
3987
|
'nhExportWriteConfidences',
|
|
3435
3988
|
'nodeLabels',
|
|
3436
3989
|
'pngExportScale',
|
|
@@ -3453,6 +4006,24 @@ function (root, d3, forester, phyloXml) {
|
|
|
3453
4006
|
});
|
|
3454
4007
|
}
|
|
3455
4008
|
|
|
4009
|
+
Object.keys(given).forEach(function (k) {
|
|
4010
|
+
let dep = DEPRECATED_CONFIG[k];
|
|
4011
|
+
if (dep === undefined) {
|
|
4012
|
+
return;
|
|
4013
|
+
}
|
|
4014
|
+
if (dep.to) {
|
|
4015
|
+
console.warn(WARNING + ': "' + k + '" was replaced by "' + dep.to
|
|
4016
|
+
+ '": ' + dep.note);
|
|
4017
|
+
// An explicit new key always wins over the old alias.
|
|
4018
|
+
if (given[dep.to] === undefined) {
|
|
4019
|
+
given[dep.to] = dep.map(given[k]);
|
|
4020
|
+
}
|
|
4021
|
+
} else {
|
|
4022
|
+
console.warn(WARNING + ': "' + k + '" is retired and has no effect: ' + dep.note);
|
|
4023
|
+
}
|
|
4024
|
+
delete given[k];
|
|
4025
|
+
});
|
|
4026
|
+
|
|
3456
4027
|
let removed = Object.keys(given).filter(function (k) {
|
|
3457
4028
|
return REMOVED_CONFIG[k] !== undefined;
|
|
3458
4029
|
});
|
|
@@ -3493,6 +4064,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
3493
4064
|
return CONTROLS_0_LEFT_DEFAULT + PANEL_WIDTH + ROOT_CLEARANCE;
|
|
3494
4065
|
}
|
|
3495
4066
|
|
|
4067
|
+
// The only validation initializeState() performs. Hoisted so launch() can
|
|
4068
|
+
// run it synchronously and defer the rest of initializeState behind the
|
|
4069
|
+
// working card on a big tree; the check inside initializeState stays as
|
|
4070
|
+
// a backstop and can no longer fire from launch().
|
|
4071
|
+
function checkLayoutValue(layout) {
|
|
4072
|
+
if (layout !== undefined && layout !== 'rectangular' && layout !== 'circular' && layout !== 'unrooted') {
|
|
4073
|
+
throw new Error(ERROR + '"layout" must be "rectangular", "circular", or "unrooted"');
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
|
|
3496
4077
|
function initializeState(state) {
|
|
3497
4078
|
_state = state;
|
|
3498
4079
|
|
|
@@ -3507,12 +4088,38 @@ function (root, d3, forester, phyloXml) {
|
|
|
3507
4088
|
});
|
|
3508
4089
|
}
|
|
3509
4090
|
|
|
3510
|
-
// Branch lengths are worth drawing to scale only when MOST
|
|
3511
|
-
//
|
|
3512
|
-
//
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
4091
|
+
// Branch lengths are worth drawing to scale only when MOST of the
|
|
4092
|
+
// branches that CARRY THE SCALE have one. Two corrections to the
|
|
4093
|
+
// obvious "count every branch" version, both from real BV-BRC trees:
|
|
4094
|
+
//
|
|
4095
|
+
// 1. Judge INTERNAL branches, not all of them. Exporters routinely
|
|
4096
|
+
// omit branch_length on tips where it would be zero, so a tree can
|
|
4097
|
+
// be fully measured internally and still have most of its branches
|
|
4098
|
+
// bare -- segment_3_2025_12_09.xml is 100% measured across its 5265
|
|
4099
|
+
// internal branches and only 16% across its 13246 tips, and used to
|
|
4100
|
+
// open as a cladogram on 0.40 of all branches. A missing TIP length
|
|
4101
|
+
// still draws correctly (the tip sits at its parent); a missing
|
|
4102
|
+
// INTERNAL length is what destroys the scale. A star tree has no
|
|
4103
|
+
// internal branches at all, so there the tips are all there is.
|
|
4104
|
+
// 2. Ask whether a branch HAS a length, not whether it is positive.
|
|
4105
|
+
// An explicit zero is a real measurement -- a tree writing its
|
|
4106
|
+
// polytomies as zero-length internal branches is a phylogram.
|
|
4107
|
+
//
|
|
4108
|
+
// The root is excluded from every count: a branch length is a property
|
|
4109
|
+
// of the branch ABOVE a node, and the root has none.
|
|
4110
|
+
let scaleBearing = _basicTreeProperties.internalBranchCount > 0;
|
|
4111
|
+
let measured = scaleBearing
|
|
4112
|
+
? _basicTreeProperties.internalBranchesWithLength
|
|
4113
|
+
: _basicTreeProperties.branchesWithLength;
|
|
4114
|
+
let branchCount = scaleBearing
|
|
4115
|
+
? _basicTreeProperties.internalBranchCount
|
|
4116
|
+
: _basicTreeProperties.branchCount;
|
|
4117
|
+
// branchLengths gates the phylogram BUTTON too (a tree whose every
|
|
4118
|
+
// length is zero cannot be drawn to scale), so the default must agree
|
|
4119
|
+
// with it or the view would contradict its own control.
|
|
4120
|
+
_state.phylogram = _basicTreeProperties.branchLengths === true
|
|
4121
|
+
&& branchCount > 0
|
|
4122
|
+
&& (measured / branchCount) > PHYLOGRAM_MIN_BRANCH_FRACTION;
|
|
3516
4123
|
_state.alignPhylogram = false;
|
|
3517
4124
|
// "layout" is launch-time only: a caller picks the starting layout by
|
|
3518
4125
|
// name, and from here on _state.circularDisplay/_state.unrootedDisplay
|
|
@@ -3554,7 +4161,22 @@ function (root, d3, forester, phyloXml) {
|
|
|
3554
4161
|
// The Confidence checkbox APPEARS whenever the tree has support values
|
|
3555
4162
|
// (that is driven by _basicTreeProperties.confidences where the panel
|
|
3556
4163
|
// is built) but starts unchecked: confidence values clutter the tree.
|
|
3557
|
-
|
|
4164
|
+
//
|
|
4165
|
+
// The ONE exception is a tree whose confidences were just promoted from
|
|
4166
|
+
// bare internal labels: those numbers were ON SCREEN a moment ago, as
|
|
4167
|
+
// node names. Promotion MOVES the label, so leaving the display off
|
|
4168
|
+
// would delete them from the view -- turning "my support values show as
|
|
4169
|
+
// names" into "my support values are gone", which is worse than the bug
|
|
4170
|
+
// being fixed. Promotion must be visually neutral: the same numbers
|
|
4171
|
+
// stay, they merely become confidences.
|
|
4172
|
+
//
|
|
4173
|
+
// Deliberately NOT "any tree that has confidences" (which is what the
|
|
4174
|
+
// desktop does): a phyloXML tree carrying real confidences never had
|
|
4175
|
+
// them drawn, so there is nothing to preserve and today's uncluttered
|
|
4176
|
+
// default is right. Only promotion destroys visible information. That
|
|
4177
|
+
// difference between the two programs is a NAMED divergence, agreed
|
|
4178
|
+
// 2026-09-08 -- do not "fix" it into either shape.
|
|
4179
|
+
_state.showConfidenceValues = !!(_treeData && _treeData.confidencesFromInternalLabels > 0);
|
|
3558
4180
|
// A tree carrying an aligned mol_seq per tip shows its alignment
|
|
3559
4181
|
// track from the start, as the desktop auto-enables it on load --
|
|
3560
4182
|
// unless the caller already said so explicitly (an explicit value
|
|
@@ -3786,13 +4408,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
3786
4408
|
if (_settings.nhExportWriteConfidences === undefined) {
|
|
3787
4409
|
_settings.nhExportWriteConfidences = true;
|
|
3788
4410
|
}
|
|
3789
|
-
// the
|
|
3790
|
-
// parseTree; they were positional arguments before the one-config API)
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
4411
|
+
// the New Hampshire parse options (used by launchArchaeopteryx /
|
|
4412
|
+
// parseTree; they were positional arguments before the one-config API).
|
|
4413
|
+
// Both read PARSE_DEFAULTS -- see the comment there for why a second
|
|
4414
|
+
// copy of these literals would be a default that cannot take effect.
|
|
4415
|
+
if (_settings.internalNumericLabels === undefined) {
|
|
4416
|
+
_settings.internalNumericLabels = PARSE_DEFAULTS.internalNumericLabels;
|
|
4417
|
+
} else if (['auto', 'confidence', 'label'].indexOf(_settings.internalNumericLabels) < 0) {
|
|
4418
|
+
throw new Error(ERROR + 'internalNumericLabels must be "auto", "confidence" or "label"');
|
|
3796
4419
|
}
|
|
3797
4420
|
// custom label-field checkboxes (was launch()'s sixth positional arg)
|
|
3798
4421
|
if (_settings.nodeLabels === undefined) {
|
|
@@ -3887,6 +4510,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
3887
4510
|
populateSearchMenus();
|
|
3888
4511
|
|
|
3889
4512
|
_svgGroup = _baseSvg.append('g');
|
|
4513
|
+
// the floating strips paint over the tree and under the overview
|
|
4514
|
+
_floatGroup = _baseSvg.append('g').attr('class', 'aptx-float')
|
|
4515
|
+
.style('pointer-events', 'none');
|
|
4516
|
+
_floatStrips = {};
|
|
3890
4517
|
makeOverview(); // appended after the tree group, so it paints on top of it
|
|
3891
4518
|
|
|
3892
4519
|
if (_settings.ladderizeTree) {
|
|
@@ -3919,6 +4546,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
3919
4546
|
getSelectedNodes: function () {
|
|
3920
4547
|
return archaeopteryx.getSelectedNodes();
|
|
3921
4548
|
},
|
|
4549
|
+
// resolves once the tree is drawn: at once for a small tree,
|
|
4550
|
+
// after the deferred first draw for a big one
|
|
4551
|
+
ready: _readyPromise,
|
|
3922
4552
|
destroy: destroyViewer
|
|
3923
4553
|
};
|
|
3924
4554
|
}
|
|
@@ -3930,6 +4560,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
3930
4560
|
// outlived the view. A later launch() into any container works normally
|
|
3931
4561
|
// (the page-level handlers rebind).
|
|
3932
4562
|
function destroyViewer() {
|
|
4563
|
+
++_launchSeq; // a first draw still waiting for its frame must not run
|
|
4564
|
+
hideBusy();
|
|
3933
4565
|
if (_container) {
|
|
3934
4566
|
d3.select(_container).selectAll('svg').remove();
|
|
3935
4567
|
_container.querySelectorAll('.aptx-panel').forEach(function (p) {
|
|
@@ -3940,8 +4572,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
3940
4572
|
document.querySelectorAll('dialog.aptx-dialog').forEach(function (dlg) {
|
|
3941
4573
|
dlg.remove();
|
|
3942
4574
|
});
|
|
3943
|
-
if (
|
|
3944
|
-
|
|
4575
|
+
if (_msaNav) {
|
|
4576
|
+
_msaNav.remove();
|
|
4577
|
+
_msaNav = null;
|
|
3945
4578
|
_msaScroller = null;
|
|
3946
4579
|
}
|
|
3947
4580
|
if (_node_mouseover_div) {
|
|
@@ -4044,27 +4677,25 @@ function (root, d3, forester, phyloXml) {
|
|
|
4044
4677
|
_radialRotation = 0;
|
|
4045
4678
|
_radialLabelsHorizontal = false;
|
|
4046
4679
|
_msaColOffset = 0;
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
initializeState
|
|
4680
|
+
// ---- synchronous: everything that can throw, and the cheap setup --
|
|
4681
|
+
//
|
|
4682
|
+
// initializeSettings validates values and throws on a bad one, so it
|
|
4683
|
+
// stays here. It reads nothing initializeState produces, so it can
|
|
4684
|
+
// run first -- which lets initializeState, the visualization
|
|
4685
|
+
// candidates and the panel move BEHIND the working card on a big tree:
|
|
4686
|
+
// together they were ~2.7 s of synchronous label intelligence and
|
|
4687
|
+
// candidate ranking over 13k tips before the card could even paint.
|
|
4688
|
+
checkLayoutValue(cfg.state ? cfg.state.layout : undefined);
|
|
4051
4689
|
initializeSettings(cfg.settings);
|
|
4052
4690
|
_nodeLabels = _settings.nodeLabels || null;
|
|
4053
4691
|
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
// launch() may run again on the same container (tree switchers do
|
|
4058
|
-
// exactly that): tear the previous viewer's DOM down first, or the
|
|
4059
|
-
// duplicated element ids leave the NEW panel's controls wired to the
|
|
4060
|
-
// OLD, invisible one.
|
|
4692
|
+
// whatever a previous launch left in this container (or in the
|
|
4693
|
+
// container before it) goes now, so the old tree does not sit under
|
|
4694
|
+
// the card while the new one is prepared
|
|
4061
4695
|
d3.select(containerEl).selectAll('svg').remove();
|
|
4062
4696
|
containerEl.querySelectorAll('.aptx-panel').forEach(function (p) {
|
|
4063
4697
|
p.remove();
|
|
4064
4698
|
});
|
|
4065
|
-
// ...and a previous viewer in a DIFFERENT container: one viewer per
|
|
4066
|
-
// page is the contract, and "replaced" must never mean "abandoned on
|
|
4067
|
-
// screen with its controls cross-wired to the new tree"
|
|
4068
4699
|
if (previousContainer && previousContainer !== containerEl) {
|
|
4069
4700
|
d3.select(previousContainer).selectAll('svg').remove();
|
|
4070
4701
|
previousContainer.querySelectorAll('.aptx-panel').forEach(function (p) {
|
|
@@ -4072,10 +4703,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
4072
4703
|
});
|
|
4073
4704
|
}
|
|
4074
4705
|
_selectedNodes = new Set();
|
|
4075
|
-
// and null the old tree: the initialization sequence runs update()
|
|
4076
|
-
// once mid-way (applyTreeTheme), which on the first launch is a no-op
|
|
4077
|
-
// because _root is null -- a relaunch must start from the same state,
|
|
4078
|
-
// not render the OLD tree against half-reset globals
|
|
4079
4706
|
_root = null;
|
|
4080
4707
|
_root_const = null;
|
|
4081
4708
|
_in_subtree = false;
|
|
@@ -4085,50 +4712,86 @@ function (root, d3, forester, phyloXml) {
|
|
|
4085
4712
|
_overviewContent = null;
|
|
4086
4713
|
_overviewViewport = null;
|
|
4087
4714
|
_overviewMap = null;
|
|
4715
|
+
_vis = null;
|
|
4088
4716
|
|
|
4089
|
-
|
|
4717
|
+
// ---- the rest: deferred one frame on a big tree, immediate otherwise
|
|
4718
|
+
let seq = ++_launchSeq;
|
|
4719
|
+
function setupAndDraw() {
|
|
4720
|
+
initializeState(cfg.state);
|
|
4721
|
+
initializeVisualizations();
|
|
4722
|
+
createGui();
|
|
4090
4723
|
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4724
|
+
_baseSvg = d3.select(containerEl).append('svg')
|
|
4725
|
+
.attr('width', _displayWidth)
|
|
4726
|
+
.attr('height', _displayHeight)
|
|
4727
|
+
.call(_zoomListener);
|
|
4095
4728
|
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4729
|
+
if (_settings.enableDynamicSizing) {
|
|
4730
|
+
d3.select(window)
|
|
4731
|
+
.on('resize.archaeopteryx', function () {
|
|
4732
|
+
let size = displaySizeFromContainer();
|
|
4733
|
+
if (!size) {
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
_displayWidth = size.w;
|
|
4737
|
+
_displayHeight = size.h;
|
|
4738
|
+
_baseSvg.attr('width', size.w);
|
|
4739
|
+
_baseSvg.attr('height', size.h);
|
|
4740
|
+
rebuildOverview();
|
|
4741
|
+
if ((_settings.zoomToFitUponWindowResize === true) && (_zoomed_x_or_y === false) && (Math.abs(currentZoomScale() - 1.0) < 0.001)) {
|
|
4742
|
+
zoomToFit();
|
|
4743
|
+
}
|
|
4744
|
+
});
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
_treeFn = d3.cluster()
|
|
4748
|
+
.size([_displayHeight, _displayWidth]);
|
|
4749
|
+
|
|
4750
|
+
_treeFn.clickEvent = getClickEventListenerNode(phylo);
|
|
4751
|
+
|
|
4752
|
+
_root = phylo;
|
|
4753
|
+
_root_const = _root;
|
|
4754
|
+
|
|
4755
|
+
calcMaxExtLabel();
|
|
4756
|
+
_root.x0 = _displayHeight / 2;
|
|
4757
|
+
_root.y0 = 0;
|
|
4758
|
+
_initializing = true;
|
|
4759
|
+
try {
|
|
4760
|
+
initialize();
|
|
4761
|
+
} finally {
|
|
4762
|
+
_initializing = false;
|
|
4763
|
+
}
|
|
4764
|
+
}
|
|
4765
|
+
|
|
4766
|
+
// Everything that can THROW has already happened above -- bad
|
|
4767
|
+
// arguments, unknown config keys, invalid setting values, an
|
|
4768
|
+
// unresolvable container -- so deferring the rest keeps launch()'s
|
|
4769
|
+
// synchronous-error contract intact. The handle closes over module
|
|
4770
|
+
// state, so it is valid before the draw; viewer.ready says when.
|
|
4771
|
+
let nodeCount = _basicTreeProperties ? _basicTreeProperties.nodeCount : 0;
|
|
4772
|
+
if (nodeCount >= BIG_TREE_NODES) {
|
|
4773
|
+
showBusy(containerEl, 'Drawing ' + nodeCount.toLocaleString() + ' nodes',
|
|
4774
|
+
'this can take a moment on a tree this size');
|
|
4775
|
+
_readyPromise = new Promise(function (resolve) {
|
|
4776
|
+
afterPaint(function () {
|
|
4777
|
+
// torn down or replaced while we waited: nothing to draw
|
|
4778
|
+
if (seq !== _launchSeq) {
|
|
4779
|
+
resolve();
|
|
4104
4780
|
return;
|
|
4105
4781
|
}
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
rebuildOverview();
|
|
4112
|
-
if ((_settings.zoomToFitUponWindowResize === true) && (_zoomed_x_or_y === false) && (Math.abs(currentZoomScale() - 1.0) < 0.001)) {
|
|
4113
|
-
zoomToFit();
|
|
4782
|
+
try {
|
|
4783
|
+
setupAndDraw();
|
|
4784
|
+
} finally {
|
|
4785
|
+
hideBusy();
|
|
4786
|
+
resolve();
|
|
4114
4787
|
}
|
|
4115
4788
|
});
|
|
4789
|
+
});
|
|
4790
|
+
} else {
|
|
4791
|
+
setupAndDraw();
|
|
4792
|
+
_readyPromise = Promise.resolve();
|
|
4116
4793
|
}
|
|
4117
4794
|
|
|
4118
|
-
_treeFn = d3.cluster()
|
|
4119
|
-
.size([_displayHeight, _displayWidth]);
|
|
4120
|
-
|
|
4121
|
-
_treeFn.clickEvent = getClickEventListenerNode(phylo);
|
|
4122
|
-
|
|
4123
|
-
_root = phylo;
|
|
4124
|
-
_root_const = _root;
|
|
4125
|
-
|
|
4126
|
-
calcMaxExtLabel();
|
|
4127
|
-
|
|
4128
|
-
_root.x0 = _displayHeight / 2;
|
|
4129
|
-
_root.y0 = 0;
|
|
4130
|
-
initialize();
|
|
4131
|
-
|
|
4132
4795
|
return makeViewerHandle();
|
|
4133
4796
|
|
|
4134
4797
|
//////////////////////////////////////////////////////////////////////
|
|
@@ -4143,9 +4806,43 @@ function (root, d3, forester, phyloXml) {
|
|
|
4143
4806
|
return phy;
|
|
4144
4807
|
};
|
|
4145
4808
|
|
|
4146
|
-
|
|
4809
|
+
// Bare numeric internal labels become confidence values here, in the two
|
|
4810
|
+
// thin wrappers rather than in launchArchaeopteryx -- because parseTree
|
|
4811
|
+
// dispatches by calling THESE two by name, so every JS path picks it up at
|
|
4812
|
+
// once: parseTree, launchArchaeopteryx (which calls parseTree), and
|
|
4813
|
+
// docs/open.html, which calls parseTree DIRECTLY. Hooking the launch path
|
|
4814
|
+
// instead would have shipped the fix while missing the page the bug was
|
|
4815
|
+
// reported from. (Calling forester.parseNewHampshire directly still
|
|
4816
|
+
// bypasses it -- deliberately: forester is a library that also serves
|
|
4817
|
+
// callers who want the file exactly as written.)
|
|
4818
|
+
//
|
|
4819
|
+
// Nexus needs this as much as Newick: a Nexus tree statement IS Newick, so
|
|
4820
|
+
// ")100:" has always had the same problem there. phyloXML and Auspice do
|
|
4821
|
+
// not -- both carry real confidences, and promoting would corrupt them.
|
|
4822
|
+
//
|
|
4823
|
+
// The count rides on the phylogeny so the VIEWER can react without the
|
|
4824
|
+
// promotion knowing anything about display state: initialize() switches
|
|
4825
|
+
// the confidence display on when anything was promoted, so support values
|
|
4826
|
+
// APPEAR rather than names silently vanishing.
|
|
4827
|
+
function promoteInternalLabels(phy, mode) {
|
|
4828
|
+
let promoted = forester.promoteInternalLabelsToConfidence(phy,
|
|
4829
|
+
mode === undefined ? PARSE_DEFAULTS.internalNumericLabels : mode);
|
|
4830
|
+
if (promoted > 0) {
|
|
4831
|
+
phy.confidencesFromInternalLabels = promoted;
|
|
4832
|
+
}
|
|
4833
|
+
return phy;
|
|
4834
|
+
}
|
|
4835
|
+
|
|
4836
|
+
// The retired brackets flag used to be the second argument and is still
|
|
4837
|
+
// accepted there, ignored, with the mode following it. forester keeps its
|
|
4838
|
+
// own flag -- that parameter is a shared contract with forester-Java, and
|
|
4839
|
+
// a caller reaching straight past this wrapper can still set it -- but
|
|
4840
|
+
// nothing here ever passes anything but true, because false only ever
|
|
4841
|
+
// discarded the bracketed value.
|
|
4842
|
+
archaeopteryx.parseNewHampshire = function (data, mode, legacyMode) {
|
|
4147
4843
|
requireForester();
|
|
4148
|
-
|
|
4844
|
+
let m = (typeof mode === 'boolean' || mode === undefined) ? legacyMode : mode;
|
|
4845
|
+
return promoteInternalLabels(forester.parseNewHampshire(data, true, false), m);
|
|
4149
4846
|
};
|
|
4150
4847
|
|
|
4151
4848
|
// An Auspice / Nextstrain v2 dataset.json; the tree opens on the time
|
|
@@ -4158,13 +4855,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
4158
4855
|
|
|
4159
4856
|
// A Nexus file can hold several trees; the FIRST one is displayed (any
|
|
4160
4857
|
// alignment from the file's characters matrix rides along on its tips).
|
|
4161
|
-
archaeopteryx.parseNexus = function (data,
|
|
4858
|
+
archaeopteryx.parseNexus = function (data, mode, legacyMode) {
|
|
4162
4859
|
requireForester();
|
|
4163
|
-
let
|
|
4860
|
+
let m = (typeof mode === 'boolean' || mode === undefined) ? legacyMode : mode;
|
|
4861
|
+
let trees = forester.parseNexus(data, true, false);
|
|
4164
4862
|
if (trees.length === 0) {
|
|
4165
4863
|
throw new Error('no tree found in the Nexus data');
|
|
4166
4864
|
}
|
|
4167
|
-
return trees[0];
|
|
4865
|
+
return promoteInternalLabels(trees[0], m);
|
|
4168
4866
|
};
|
|
4169
4867
|
|
|
4170
4868
|
function calcMaxExtLabel() {
|
|
@@ -4245,132 +4943,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4245
4943
|
|
|
4246
4944
|
function displayNodeData(n) {
|
|
4247
4945
|
let title = 'Node Data';
|
|
4248
|
-
let text =
|
|
4249
|
-
if (n.name) {
|
|
4250
|
-
text += 'Name: ' + n.name + '<br>';
|
|
4251
|
-
}
|
|
4252
|
-
if (n.branch_length) {
|
|
4253
|
-
text += 'Distance to Parent: ' + n.branch_length + '<br>';
|
|
4254
|
-
}
|
|
4255
|
-
text += 'Depth: ' + forester.calcDepth(n) + '<br>';
|
|
4256
|
-
let i = 0;
|
|
4257
|
-
if (n.confidences) {
|
|
4258
|
-
for (i = 0; i < n.confidences.length; ++i) {
|
|
4259
|
-
let c = n.confidences[i];
|
|
4260
|
-
if (c.type) {
|
|
4261
|
-
text += 'Confidence [' + c.type + ']: ' + c.value + '<br>';
|
|
4262
|
-
} else {
|
|
4263
|
-
text += 'Confidence: ' + c.value + '<br>';
|
|
4264
|
-
}
|
|
4265
|
-
if (c.stddev) {
|
|
4266
|
-
text += '- stdev: ' + c.stddev + '<br>';
|
|
4267
|
-
}
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
4270
|
-
if (n.taxonomies) {
|
|
4271
|
-
for (i = 0; i < n.taxonomies.length; ++i) {
|
|
4272
|
-
text += 'Taxonomy<br>';
|
|
4273
|
-
let t = n.taxonomies[i];
|
|
4274
|
-
if (t.id) {
|
|
4275
|
-
if (t.id.provider) {
|
|
4276
|
-
text += '- Id [' + t.id.provider + ']: ' + t.id.value + '<br>';
|
|
4277
|
-
} else {
|
|
4278
|
-
text += '- Id: ' + t.id.value + '<br>';
|
|
4279
|
-
}
|
|
4280
|
-
}
|
|
4281
|
-
if (t.code) {
|
|
4282
|
-
text += '- Code: ' + t.code + '<br>';
|
|
4283
|
-
}
|
|
4284
|
-
if (t.scientific_name) {
|
|
4285
|
-
text += '- Scientific name: ' + t.scientific_name + '<br>';
|
|
4286
|
-
}
|
|
4287
|
-
if (t.common_name) {
|
|
4288
|
-
text += '- Common name: ' + t.common_name + '<br>';
|
|
4289
|
-
}
|
|
4290
|
-
if (t.rank) {
|
|
4291
|
-
text += '- Rank: ' + t.rank + '<br>';
|
|
4292
|
-
}
|
|
4293
|
-
}
|
|
4294
|
-
}
|
|
4295
|
-
if (n.sequences) {
|
|
4296
|
-
for (i = 0; i < n.sequences.length; ++i) {
|
|
4297
|
-
text += 'Sequence<br>';
|
|
4298
|
-
let s = n.sequences[i];
|
|
4299
|
-
if (s.accession) {
|
|
4300
|
-
if (s.accession.source) {
|
|
4301
|
-
text += '- Accession [' + s.accession.source + ']: ' + s.accession.value + '<br>';
|
|
4302
|
-
} else {
|
|
4303
|
-
text += '- Accession: ' + s.accession.value + '<br>';
|
|
4304
|
-
}
|
|
4305
|
-
if (s.accession.comment) {
|
|
4306
|
-
text += '-- comment: ' + s.accession.comment + '<br>';
|
|
4307
|
-
}
|
|
4308
|
-
}
|
|
4309
|
-
if (s.symbol) {
|
|
4310
|
-
text += '- Symbol: ' + s.symbol + '<br>';
|
|
4311
|
-
}
|
|
4312
|
-
if (s.name) {
|
|
4313
|
-
text += '- Name: ' + s.name + '<br>';
|
|
4314
|
-
}
|
|
4315
|
-
if (s.gene_name) {
|
|
4316
|
-
text += '- Gene name: ' + s.gene_name + '<br>';
|
|
4317
|
-
}
|
|
4318
|
-
if (s.location) {
|
|
4319
|
-
text += '- Location: ' + s.location + '<br>';
|
|
4320
|
-
}
|
|
4321
|
-
if (s.type) {
|
|
4322
|
-
text += '- Type: ' + s.type + '<br>';
|
|
4323
|
-
}
|
|
4324
|
-
}
|
|
4325
|
-
}
|
|
4326
|
-
if (n.distributions) {
|
|
4327
|
-
let distributions = n.distributions;
|
|
4328
|
-
for (i = 0; i < distributions.length; ++i) {
|
|
4329
|
-
text += 'Distribution: ';
|
|
4330
|
-
if (distributions[i].desc) {
|
|
4331
|
-
text += distributions[i].desc + '<br>';
|
|
4332
|
-
}
|
|
4333
|
-
}
|
|
4334
|
-
}
|
|
4335
|
-
if (n.date) {
|
|
4336
|
-
text += 'Date: ';
|
|
4337
|
-
let date = n.date;
|
|
4338
|
-
if (date.desc) {
|
|
4339
|
-
text += date.desc + '<br>';
|
|
4340
|
-
}
|
|
4341
|
-
}
|
|
4342
|
-
if (n.events) {
|
|
4343
|
-
text += 'Events<br>';
|
|
4344
|
-
let ev = n.events;
|
|
4345
|
-
if (ev.type && ev.type.length > 0) {
|
|
4346
|
-
text += '- Type: ' + ev.type + '<br>';
|
|
4347
|
-
}
|
|
4348
|
-
if (ev.duplications && ev.duplications > 0) {
|
|
4349
|
-
text += '- Duplications: ' + ev.duplications + '<br>';
|
|
4350
|
-
}
|
|
4351
|
-
if (ev.speciations && ev.speciations > 0) {
|
|
4352
|
-
text += '- Speciations: ' + ev.speciations + '<br>';
|
|
4353
|
-
}
|
|
4354
|
-
if (ev.losses && ev.losses > 0) {
|
|
4355
|
-
text += '- Losses: ' + ev.losses + '<br>';
|
|
4356
|
-
}
|
|
4357
|
-
}
|
|
4358
|
-
if (n.properties && n.properties.length > 0) {
|
|
4359
|
-
let propertiesLength = n.properties.length;
|
|
4360
|
-
for (i = 0; i < propertiesLength; ++i) {
|
|
4361
|
-
let property = n.properties[i];
|
|
4362
|
-
if (property.ref && property.value) {
|
|
4363
|
-
if (property.unit) {
|
|
4364
|
-
text += property.ref + ': ' + property.value + property.unit + '<br>';
|
|
4365
|
-
} else {
|
|
4366
|
-
text += property.ref + ': ' + property.value + '<br>';
|
|
4367
|
-
}
|
|
4368
|
-
}
|
|
4369
|
-
}
|
|
4370
|
-
}
|
|
4371
|
-
if (n.children) {
|
|
4372
|
-
text += 'Sum of Subtree Tips: ' + forester.calcSumOfAllExternalDescendants(n) + '<br>';
|
|
4373
|
-
}
|
|
4946
|
+
let text = nodeDataText(n);
|
|
4374
4947
|
|
|
4375
4948
|
showNodeDataDialog(title, text, false, 260, 300);
|
|
4376
4949
|
|
|
@@ -4832,39 +5405,44 @@ function (root, d3, forester, phyloXml) {
|
|
|
4832
5405
|
applyZoom();
|
|
4833
5406
|
return;
|
|
4834
5407
|
}
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
//
|
|
4838
|
-
|
|
4839
|
-
|
|
5408
|
+
// The point under the viewport centre, captured ONCE per burst: with
|
|
5409
|
+
// a big tree the zoom steps of a wheel flick coalesce into one
|
|
5410
|
+
// deferred redraw, and the re-centring after it maps this one point
|
|
5411
|
+
// forward to the final layout. Capturing again on each step would
|
|
5412
|
+
// measure a transform that has not been applied yet.
|
|
5413
|
+
if (!_pendingCentre) {
|
|
5414
|
+
let t = d3.zoomTransform(_baseSvg.node());
|
|
5415
|
+
_pendingCentre = {
|
|
5416
|
+
size: size,
|
|
5417
|
+
t: t,
|
|
5418
|
+
before: layoutSpans(),
|
|
5419
|
+
cx: ((size.w / 2) - t.x) / t.k,
|
|
5420
|
+
cy: ((size.h / 2) - t.y) / t.k
|
|
5421
|
+
};
|
|
5422
|
+
}
|
|
4840
5423
|
|
|
4841
5424
|
applyZoom();
|
|
4842
5425
|
|
|
5426
|
+
afterUpdate(recentreViewport);
|
|
5427
|
+
}
|
|
5428
|
+
|
|
5429
|
+
function recentreViewport() {
|
|
5430
|
+
let p = _pendingCentre;
|
|
5431
|
+
_pendingCentre = null;
|
|
5432
|
+
if (!p || !_baseSvg) {
|
|
5433
|
+
return;
|
|
5434
|
+
}
|
|
4843
5435
|
let after = layoutSpans();
|
|
4844
|
-
//
|
|
4845
|
-
//
|
|
4846
|
-
//
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
// SCALE with it, but a viewport centre can also sit over the label
|
|
4852
|
-
// zone beyond it (with long labels, most of the picture) -- label text
|
|
4853
|
-
// keeps its length and only TRANSLATES with its anchor node, so that
|
|
4854
|
-
// part of the distance is carried over unscaled. Splitting the anchor
|
|
4855
|
-
// this way is exact for labels anchored at the far edge and a close
|
|
4856
|
-
// approximation for the rest; scaling the whole distance made the
|
|
4857
|
-
// display race off horizontally on long-labelled trees.
|
|
4858
|
-
// ... and the same at the OTHER end: a centre panned before the root
|
|
4859
|
-
// (negative) or past the span is empty margin too, and scaling it
|
|
4860
|
-
// flings the tree the way the label overshoot once did.
|
|
4861
|
-
let bx = Math.max(0, Math.min(cx, before.horizontal));
|
|
4862
|
-
let nx = (bx * (after.horizontal / before.horizontal)) + (cx - bx);
|
|
4863
|
-
let by = Math.max(0, Math.min(cy, before.vertical));
|
|
4864
|
-
let ny = (by * (after.vertical / before.vertical)) + (cy - by);
|
|
5436
|
+
// Map the captured point through the change in layout extent: a
|
|
5437
|
+
// point inside the tree scales with it, a point in the margin keeps
|
|
5438
|
+
// its offset from the tree's edge.
|
|
5439
|
+
let bx = Math.max(0, Math.min(p.cx, p.before.horizontal));
|
|
5440
|
+
let nx = (bx * (after.horizontal / p.before.horizontal)) + (p.cx - bx);
|
|
5441
|
+
let by = Math.max(0, Math.min(p.cy, p.before.vertical));
|
|
5442
|
+
let ny = (by * (after.vertical / p.before.vertical)) + (p.cy - by);
|
|
4865
5443
|
_baseSvg.call(_zoomListener.transform, d3.zoomIdentity
|
|
4866
|
-
.translate((size.w / 2) - (nx * t.k), (size.h / 2) - (ny * t.k))
|
|
4867
|
-
.scale(t.k));
|
|
5444
|
+
.translate((p.size.w / 2) - (nx * p.t.k), (p.size.h / 2) - (ny * p.t.k))
|
|
5445
|
+
.scale(p.t.k));
|
|
4868
5446
|
}
|
|
4869
5447
|
|
|
4870
5448
|
// The desktop's radial rotation: each press turns the circular layout by
|
|
@@ -4873,7 +5451,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4873
5451
|
// so horizontal zoom would be redundant there.
|
|
4874
5452
|
function rotateRadial(clockwise) {
|
|
4875
5453
|
_radialRotation = (_radialRotation + (clockwise ? 1 : -1) * Math.PI / 32) % (2 * Math.PI);
|
|
4876
|
-
|
|
5454
|
+
scheduleUpdate(null, 0, true);
|
|
4877
5455
|
}
|
|
4878
5456
|
|
|
4879
5457
|
// The desktop's fit-width button: fit the tree to the window width,
|
|
@@ -4912,7 +5490,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4912
5490
|
} else {
|
|
4913
5491
|
_displayWidth = _displayWidth * BUTTON_ZOOM_IN_FACTOR;
|
|
4914
5492
|
}
|
|
4915
|
-
|
|
5493
|
+
scheduleUpdate(null, 0);
|
|
4916
5494
|
});
|
|
4917
5495
|
}
|
|
4918
5496
|
|
|
@@ -4933,7 +5511,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4933
5511
|
} else {
|
|
4934
5512
|
_displayHeight = _displayHeight * f;
|
|
4935
5513
|
}
|
|
4936
|
-
|
|
5514
|
+
scheduleUpdate(null, 0);
|
|
4937
5515
|
});
|
|
4938
5516
|
}
|
|
4939
5517
|
|
|
@@ -4952,7 +5530,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4952
5530
|
}
|
|
4953
5531
|
if ((newDisplayWidth - calcMaxTreeLengthForDisplay()) >= 1) {
|
|
4954
5532
|
_displayWidth = newDisplayWidth;
|
|
4955
|
-
|
|
5533
|
+
scheduleUpdate(null, 0);
|
|
4956
5534
|
}
|
|
4957
5535
|
});
|
|
4958
5536
|
}
|
|
@@ -4973,7 +5551,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4973
5551
|
_displayHeight = min;
|
|
4974
5552
|
}
|
|
4975
5553
|
}
|
|
4976
|
-
|
|
5554
|
+
scheduleUpdate(null, 0);
|
|
4977
5555
|
});
|
|
4978
5556
|
}
|
|
4979
5557
|
|
|
@@ -5039,6 +5617,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
5039
5617
|
return;
|
|
5040
5618
|
}
|
|
5041
5619
|
_svgGroup.selectAll('g.aptx-msa').remove();
|
|
5620
|
+
if (_floatGroup) {
|
|
5621
|
+
_floatGroup.selectAll('g.aptx-msa-strip').remove();
|
|
5622
|
+
delete _floatStrips['aptx-msa-strip'];
|
|
5623
|
+
}
|
|
5042
5624
|
if (!msaShown() || !_root) {
|
|
5043
5625
|
_msaGeom = null;
|
|
5044
5626
|
updateMsaScrollbar(0, 1, 0);
|
|
@@ -5202,8 +5784,47 @@ function (root, d3, forester, phyloXml) {
|
|
|
5202
5784
|
let cons = forester.msaConservation(tips.map(function (t) {
|
|
5203
5785
|
return msaRowSeq(t).slice(offset, offset + visible);
|
|
5204
5786
|
}), visible, 'identity', isNuc);
|
|
5787
|
+
// A faint dashed guide from each tip -- from the end of its label, or
|
|
5788
|
+
// from the node itself when the label is hidden -- across to its row
|
|
5789
|
+
// in the track, so a row can be read back to its sequence without
|
|
5790
|
+
// counting. Same idea as the aligned-phylogram extension; dashed so it
|
|
5791
|
+
// cannot be mistaken for a branch. Skipped where a label already
|
|
5792
|
+
// reaches the track.
|
|
5793
|
+
let guideFont = _state.externalNodeFontSize + 'px ' + _state.defaultFont;
|
|
5794
|
+
let guideGap = _state.nodeLabelGap;
|
|
5795
|
+
let guideEnd = originX - 3;
|
|
5796
|
+
for (let r = 0; r < n; ++r) {
|
|
5797
|
+
let d = tips[r];
|
|
5798
|
+
let label = d._extLabelText || '';
|
|
5799
|
+
let from;
|
|
5800
|
+
if (label) {
|
|
5801
|
+
let labelX = (_state.phylogram && _state.alignPhylogram && _yScale)
|
|
5802
|
+
? d.y - _yScale(d.distToRoot) + _w + guideGap
|
|
5803
|
+
: d.y + guideGap;
|
|
5804
|
+
from = labelX + legendTextWidth(label, guideFont) + 5;
|
|
5805
|
+
} else {
|
|
5806
|
+
from = d.y + makeNodeSize(d) + 4;
|
|
5807
|
+
}
|
|
5808
|
+
if (guideEnd - from < 6) {
|
|
5809
|
+
continue;
|
|
5810
|
+
}
|
|
5811
|
+
g.append('line')
|
|
5812
|
+
.attr('x1', from).attr('x2', guideEnd)
|
|
5813
|
+
.attr('y1', d.x).attr('y2', d.x)
|
|
5814
|
+
.attr('stroke', ink)
|
|
5815
|
+
.attr('stroke-width', 1)
|
|
5816
|
+
.attr('stroke-dasharray', '2,3')
|
|
5817
|
+
.style('stroke-opacity', 0.35)
|
|
5818
|
+
.style('pointer-events', 'none');
|
|
5819
|
+
}
|
|
5820
|
+
|
|
5821
|
+
// ---- conservation, consensus, ruler: a FLOATING strip ----
|
|
5822
|
+
let strip = floatStripGroup('aptx-msa-strip', _clusterH, MSA_BOTTOM_RESERVE);
|
|
5823
|
+
strip.append('rect').attr('x', Math.round(originX) - 2).attr('y', _clusterH)
|
|
5824
|
+
.attr('width', trackW + 4).attr('height', MSA_BOTTOM_RESERVE)
|
|
5825
|
+
.attr('fill', _state.backgroundColorDefault);
|
|
5205
5826
|
let consTop = _clusterH + MSA_CONS_TOP_GAP;
|
|
5206
|
-
|
|
5827
|
+
strip.append('rect').attr('x', Math.round(originX)).attr('y', consTop)
|
|
5207
5828
|
.attr('width', trackW).attr('height', MSA_CONS_BAR_H)
|
|
5208
5829
|
.attr('fill', ink).attr('fill-opacity', 0.08);
|
|
5209
5830
|
for (let i = 0; i < visible; ++i) {
|
|
@@ -5214,12 +5835,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
5214
5835
|
let bh = Math.max(1, Math.round(score * MSA_CONS_BAR_H));
|
|
5215
5836
|
let x0 = Math.round(originX + (i * cw));
|
|
5216
5837
|
let x1 = Math.round(originX + ((i + 1) * cw));
|
|
5217
|
-
|
|
5838
|
+
strip.append('rect').attr('x', x0).attr('y', (consTop + MSA_CONS_BAR_H) - bh)
|
|
5218
5839
|
.attr('width', Math.max(1, x1 - x0)).attr('height', bh)
|
|
5219
5840
|
.attr('fill', ink).attr('fill-opacity', 0.7);
|
|
5220
5841
|
}
|
|
5221
5842
|
if (trackW > 170) {
|
|
5222
|
-
|
|
5843
|
+
strip.append('text').attr('x', Math.round(originX) + trackW - 3).attr('y', consTop + 9)
|
|
5223
5844
|
.attr('text-anchor', 'end')
|
|
5224
5845
|
.style('font-size', '8px').style('fill', ink).style('fill-opacity', 0.9)
|
|
5225
5846
|
.text('Consensus identity (n = ' + n + ')');
|
|
@@ -5231,7 +5852,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5231
5852
|
if (!cc) {
|
|
5232
5853
|
continue;
|
|
5233
5854
|
}
|
|
5234
|
-
|
|
5855
|
+
strip.append('text').attr('x', Math.round(originX + (i * cw)) + (cw / 2))
|
|
5235
5856
|
.attr('y', consTop + MSA_CONS_BAR_H + 10)
|
|
5236
5857
|
.attr('text-anchor', 'middle')
|
|
5237
5858
|
.style('font-family', 'monospace').style('font-size', '8px')
|
|
@@ -5243,7 +5864,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5243
5864
|
// the 1-based column ruler: absolute column numbers at nice steps,
|
|
5244
5865
|
// and always the first and last column when their edge is in view
|
|
5245
5866
|
let rulerY = consTop + MSA_CONS_BAR_H + (consensusRow ? 13 : 3);
|
|
5246
|
-
|
|
5867
|
+
strip.append('line').attr('x1', Math.round(originX)).attr('x2', Math.round(originX) + trackW)
|
|
5247
5868
|
.attr('y1', rulerY).attr('y2', rulerY)
|
|
5248
5869
|
.attr('stroke', ink).attr('stroke-opacity', 0.8).attr('stroke-width', 1);
|
|
5249
5870
|
let lastRight = -Infinity;
|
|
@@ -5253,12 +5874,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
5253
5874
|
return;
|
|
5254
5875
|
}
|
|
5255
5876
|
let x = Math.round(originX + (i * cw)) + (cw / 2);
|
|
5256
|
-
|
|
5877
|
+
strip.append('line').attr('x1', x).attr('x2', x).attr('y1', rulerY).attr('y2', rulerY + 4)
|
|
5257
5878
|
.attr('stroke', ink).attr('stroke-opacity', 0.8).attr('stroke-width', 1);
|
|
5258
5879
|
let label = String(c);
|
|
5259
5880
|
let half = label.length * 2.8;
|
|
5260
5881
|
if (force || (x - half) >= (lastRight + 4)) {
|
|
5261
|
-
|
|
5882
|
+
strip.append('text').attr('x', x).attr('y', rulerY + 14).attr('text-anchor', 'middle')
|
|
5262
5883
|
.style('font-size', '9px').style('fill', ink)
|
|
5263
5884
|
.text(label);
|
|
5264
5885
|
lastRight = x + half;
|
|
@@ -5285,49 +5906,152 @@ function (root, d3, forester, phyloXml) {
|
|
|
5285
5906
|
if (delta === 0) {
|
|
5286
5907
|
return;
|
|
5287
5908
|
}
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5909
|
+
// a tenth of a screen per notch (never under three columns):
|
|
5910
|
+
// fine enough to place a column, and a long alignment is not
|
|
5911
|
+
// ten thousand notches end to end
|
|
5912
|
+
let step = Math.max(3, Math.round(visible / 10));
|
|
5913
|
+
msaScrollTo(_msaColOffset + (delta > 0 ? step : -step));
|
|
5293
5914
|
});
|
|
5294
5915
|
_msaGeom = {
|
|
5295
5916
|
originX: originX, cw: cw, offset: offset, visible: visible, total: total,
|
|
5296
5917
|
bounds: bounds, tips: tips, isNuc: isNuc
|
|
5297
5918
|
};
|
|
5298
5919
|
updateMsaScrollbar(offset, visible, total);
|
|
5920
|
+
placeFloatingOverlays();
|
|
5299
5921
|
}
|
|
5300
5922
|
|
|
5301
5923
|
// The dedicated column scroller: a fixed HTML range input at the window
|
|
5302
5924
|
// bottom, shown only while the alignment is wider than its window. Lives
|
|
5303
5925
|
// outside the svg, so exports never include it.
|
|
5926
|
+
// Scroll the alignment window to start at column `offset` (0-based),
|
|
5927
|
+
// clamped, and redraw. Every way of moving -- slider, wheel, paging
|
|
5928
|
+
// buttons, the jump box -- lands here.
|
|
5929
|
+
function msaScrollTo(offset) {
|
|
5930
|
+
if (!_msaGeom) {
|
|
5931
|
+
return;
|
|
5932
|
+
}
|
|
5933
|
+
let max = Math.max(0, _msaGeom.total - _msaGeom.visible);
|
|
5934
|
+
let next = Math.max(0, Math.min(max, Math.round(offset)));
|
|
5935
|
+
if (next !== _msaColOffset) {
|
|
5936
|
+
_msaColOffset = next;
|
|
5937
|
+
drawMsaTrack();
|
|
5938
|
+
}
|
|
5939
|
+
}
|
|
5940
|
+
|
|
5941
|
+
// The navigation bar under a scrollable alignment. A bare range slider
|
|
5942
|
+
// used to be all there was, and on a 30,000-column alignment 260 px of
|
|
5943
|
+
// slider is ~115 columns per pixel and the wheel moved three columns a
|
|
5944
|
+
// notch -- nothing could be FOUND, only scrolled past. Now: first / page
|
|
5945
|
+
// back / slider / page forward / last, a jump-to-column box, and a
|
|
5946
|
+
// readout of the columns on screen. A page is one screenful of columns.
|
|
5304
5947
|
function updateMsaScrollbar(offset, visible, total) {
|
|
5305
5948
|
let scrollable = msaShown() && total > visible;
|
|
5306
|
-
if (!
|
|
5307
|
-
return; // never park
|
|
5308
|
-
}
|
|
5309
|
-
if (!
|
|
5949
|
+
if (!_msaNav && !scrollable) {
|
|
5950
|
+
return; // never park page-level controls under a tree that has no use for them
|
|
5951
|
+
}
|
|
5952
|
+
if (!_msaNav) {
|
|
5953
|
+
let nav = document.createElement('div');
|
|
5954
|
+
nav.className = 'aptx-msa-nav';
|
|
5955
|
+
if (_panelTheme) {
|
|
5956
|
+
nav.classList.add('aptx-' + _panelTheme);
|
|
5957
|
+
}
|
|
5958
|
+
function button(text, title, act) {
|
|
5959
|
+
let b = document.createElement('button');
|
|
5960
|
+
b.type = 'button';
|
|
5961
|
+
b.className = 'aptx-msa-nav-btn';
|
|
5962
|
+
b.textContent = text;
|
|
5963
|
+
b.title = title;
|
|
5964
|
+
b.addEventListener('click', act);
|
|
5965
|
+
nav.appendChild(b);
|
|
5966
|
+
return b;
|
|
5967
|
+
}
|
|
5968
|
+
button('\u23EE', 'first column', function () {
|
|
5969
|
+
msaScrollTo(0);
|
|
5970
|
+
});
|
|
5971
|
+
button('\u25C0', 'back one screen of columns', function () {
|
|
5972
|
+
msaScrollTo(_msaColOffset - (_msaGeom ? _msaGeom.visible : 0));
|
|
5973
|
+
});
|
|
5310
5974
|
let el = document.createElement('input');
|
|
5311
5975
|
el.type = 'range';
|
|
5312
5976
|
el.id = MSA_SCROLL_ID;
|
|
5313
5977
|
el.className = 'aptx-msa-scroll';
|
|
5314
5978
|
el.min = '0';
|
|
5315
5979
|
el.step = '1';
|
|
5316
|
-
el.title = 'scroll the alignment
|
|
5980
|
+
el.title = 'scroll the alignment (the wheel over the alignment works too; PageUp/PageDown jump)';
|
|
5317
5981
|
el.addEventListener('input', function () {
|
|
5318
5982
|
let v = parseInt(el.value, 10);
|
|
5319
|
-
if (!isNaN(v)
|
|
5320
|
-
|
|
5321
|
-
|
|
5983
|
+
if (!isNaN(v)) {
|
|
5984
|
+
msaScrollTo(v);
|
|
5985
|
+
}
|
|
5986
|
+
});
|
|
5987
|
+
nav.appendChild(el);
|
|
5988
|
+
button('\u25B6', 'forward one screen of columns', function () {
|
|
5989
|
+
msaScrollTo(_msaColOffset + (_msaGeom ? _msaGeom.visible : 0));
|
|
5990
|
+
});
|
|
5991
|
+
button('\u23ED', 'last column', function () {
|
|
5992
|
+
msaScrollTo(Infinity);
|
|
5993
|
+
});
|
|
5994
|
+
|
|
5995
|
+
// "column [ 1234 ] - 1357 of 30,000": the box is the left edge of
|
|
5996
|
+
// the window and takes a column to jump to; the rest reads back
|
|
5997
|
+
let read = document.createElement('span');
|
|
5998
|
+
read.className = 'aptx-msa-nav-read';
|
|
5999
|
+
let lbl = document.createElement('span');
|
|
6000
|
+
lbl.textContent = 'column';
|
|
6001
|
+
let jump = document.createElement('input');
|
|
6002
|
+
jump.type = 'number';
|
|
6003
|
+
jump.min = '1';
|
|
6004
|
+
jump.step = '1';
|
|
6005
|
+
jump.className = 'aptx-msa-nav-jump';
|
|
6006
|
+
jump.title = 'type a column number and press Enter to scroll there';
|
|
6007
|
+
jump.setAttribute('aria-label', 'first visible column; type a column to jump to');
|
|
6008
|
+
function jumpNow() {
|
|
6009
|
+
let v = parseInt(jump.value, 10);
|
|
6010
|
+
if (!isNaN(v)) {
|
|
6011
|
+
msaScrollTo(v - 1);
|
|
6012
|
+
}
|
|
6013
|
+
jump.blur();
|
|
6014
|
+
}
|
|
6015
|
+
jump.addEventListener('keydown', function (event) {
|
|
6016
|
+
if (event.key === 'Enter') {
|
|
6017
|
+
event.preventDefault();
|
|
6018
|
+
jumpNow();
|
|
6019
|
+
} else if (event.key === 'Escape') {
|
|
6020
|
+
jump.value = String(_msaColOffset + 1);
|
|
6021
|
+
jump.blur();
|
|
5322
6022
|
}
|
|
5323
6023
|
});
|
|
5324
|
-
|
|
6024
|
+
jump.addEventListener('change', jumpNow);
|
|
6025
|
+
let tail = document.createElement('span');
|
|
6026
|
+
tail.className = 'aptx-msa-nav-tail';
|
|
6027
|
+
read.appendChild(lbl);
|
|
6028
|
+
read.appendChild(jump);
|
|
6029
|
+
read.appendChild(tail);
|
|
6030
|
+
nav.appendChild(read);
|
|
6031
|
+
|
|
6032
|
+
document.body.appendChild(nav);
|
|
6033
|
+
_msaNav = nav;
|
|
5325
6034
|
_msaScroller = el;
|
|
6035
|
+
_msaNav._jump = jump;
|
|
6036
|
+
_msaNav._tail = tail;
|
|
5326
6037
|
}
|
|
5327
|
-
|
|
6038
|
+
_msaNav.style.display = scrollable ? 'flex' : 'none';
|
|
5328
6039
|
if (scrollable) {
|
|
5329
6040
|
_msaScroller.max = String(total - visible);
|
|
5330
6041
|
_msaScroller.value = String(offset);
|
|
6042
|
+
if (document.activeElement !== _msaNav._jump) {
|
|
6043
|
+
_msaNav._jump.value = String(offset + 1);
|
|
6044
|
+
}
|
|
6045
|
+
_msaNav._jump.max = String(total);
|
|
6046
|
+
_msaNav._tail.textContent = '\u2013 ' + (offset + visible).toLocaleString()
|
|
6047
|
+
+ ' of ' + total.toLocaleString();
|
|
6048
|
+
let atStart = offset <= 0;
|
|
6049
|
+
let atEnd = offset >= total - visible;
|
|
6050
|
+
let btns = _msaNav.querySelectorAll('.aptx-msa-nav-btn');
|
|
6051
|
+
btns[0].disabled = atStart;
|
|
6052
|
+
btns[1].disabled = atStart;
|
|
6053
|
+
btns[2].disabled = atEnd;
|
|
6054
|
+
btns[3].disabled = atEnd;
|
|
5331
6055
|
}
|
|
5332
6056
|
}
|
|
5333
6057
|
|
|
@@ -5399,7 +6123,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5399
6123
|
|
|
5400
6124
|
function msaCbClicked() {
|
|
5401
6125
|
_state.showMsa = getCheckboxValue(MSA_CB);
|
|
5402
|
-
|
|
6126
|
+
scheduleUpdate(null, 0);
|
|
5403
6127
|
}
|
|
5404
6128
|
|
|
5405
6129
|
// Search B starts hidden to keep the panel compact; one click (or a
|
|
@@ -5446,7 +6170,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
5446
6170
|
// under the tree (left) -- horizontally disjoint, so the two reserves
|
|
5447
6171
|
// share the same strip rather than stacking.
|
|
5448
6172
|
function bottomOverlayReserve() {
|
|
5449
|
-
|
|
6173
|
+
// The navigation bar is fixed at the viewport bottom, so the track's
|
|
6174
|
+
// own bottom rows (conservation, consensus, ruler) must end above it
|
|
6175
|
+
// or the bar covers them -- the old bare slider did exactly that.
|
|
6176
|
+
return Math.max(msaShown() ? MSA_BOTTOM_RESERVE + MSA_NAV_RESERVE : 0, timeAxisBottomReserve());
|
|
5450
6177
|
}
|
|
5451
6178
|
|
|
5452
6179
|
function hexToRgbTriple(hex) {
|
|
@@ -5460,6 +6187,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
5460
6187
|
return;
|
|
5461
6188
|
}
|
|
5462
6189
|
_svgGroup.selectAll('g.aptx-time').remove();
|
|
6190
|
+
if (_floatGroup) {
|
|
6191
|
+
_floatGroup.selectAll('g.aptx-time-axis').remove();
|
|
6192
|
+
delete _floatStrips['aptx-time-axis'];
|
|
6193
|
+
}
|
|
5463
6194
|
_svgGroup.selectAll('g.aptx-timegrid').remove();
|
|
5464
6195
|
if (!timeAxisShown() || !_root || !_yScale) {
|
|
5465
6196
|
return;
|
|
@@ -5551,6 +6282,19 @@ function (root, d3, forester, phyloXml) {
|
|
|
5551
6282
|
}
|
|
5552
6283
|
});
|
|
5553
6284
|
|
|
6285
|
+
// ---- the axis itself: a FLOATING strip (see floatStripGroup) ----
|
|
6286
|
+
let ax = floatStripGroup('aptx-time-axis', axisTop - 4, timeAxisBottomReserve() + 4);
|
|
6287
|
+
function axisBackdrop(x0, x1) {
|
|
6288
|
+
// opaque, in the background colour, so tips panned under the
|
|
6289
|
+
// strip do not show through between the bands and the ruler
|
|
6290
|
+
let left = Math.min(x0, x1);
|
|
6291
|
+
ax.append('rect').attr('x', left - 2).attr('y', axisTop - 4)
|
|
6292
|
+
.attr('width', Math.abs(x1 - x0) + 4).attr('height', timeAxisBottomReserve() + 4)
|
|
6293
|
+
.attr('fill', _state.backgroundColorDefault);
|
|
6294
|
+
ax.append('line').attr('x1', left - 2).attr('x2', left - 2 + Math.abs(x1 - x0) + 4)
|
|
6295
|
+
.attr('y1', axisTop - 4).attr('y2', axisTop - 4)
|
|
6296
|
+
.attr('stroke', ink).attr('stroke-opacity', 0.25).attr('stroke-width', 1);
|
|
6297
|
+
}
|
|
5554
6298
|
if (info.type === 'geologic') {
|
|
5555
6299
|
let rootAge = info.rootAge;
|
|
5556
6300
|
if (!(rootAge > 0)) {
|
|
@@ -5562,7 +6306,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
5562
6306
|
return anchorX + ((anchorAge - age) * corr);
|
|
5563
6307
|
};
|
|
5564
6308
|
let youngBound = Math.max(0, anchorAge - ((maxTipX - anchorX) / corr));
|
|
5565
|
-
|
|
6309
|
+
// both bounds: a fossil-only clade spans [youngest tip, root],
|
|
6310
|
+
// and a narrow window bands Series over Stage
|
|
6311
|
+
axisBackdrop(xOfAge(rootAge), xOfAge(youngBound));
|
|
6312
|
+
let ranks = forester.geoBandRanks(youngBound, rootAge);
|
|
5566
6313
|
if (grid) {
|
|
5567
6314
|
// the FINE rank's old-side boundaries, root and tip edges
|
|
5568
6315
|
// excluded (they would just retrace the tree's outline)
|
|
@@ -5585,12 +6332,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
5585
6332
|
if (w <= 0) {
|
|
5586
6333
|
continue;
|
|
5587
6334
|
}
|
|
5588
|
-
|
|
6335
|
+
ax.append('rect').attr('x', left).attr('y', rowY)
|
|
5589
6336
|
.attr('width', w).attr('height', TIME_BAND_ROW_H)
|
|
5590
6337
|
.attr('fill', iv.color)
|
|
5591
6338
|
.attr('stroke', ink).attr('stroke-opacity', 0.5).attr('stroke-width', 0.5);
|
|
5592
6339
|
if ((iv.name.length * 5.5) + 4 <= w) {
|
|
5593
|
-
|
|
6340
|
+
ax.append('text').attr('x', left + (w / 2)).attr('y', rowY + TIME_BAND_ROW_H - 3.5)
|
|
5594
6341
|
.attr('text-anchor', 'middle')
|
|
5595
6342
|
.style('font-size', '9px')
|
|
5596
6343
|
.style('fill', forester.msaLetterInk(hexToRgbTriple(iv.color)))
|
|
@@ -5600,7 +6347,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5600
6347
|
}
|
|
5601
6348
|
// the "Ma before present" ruler
|
|
5602
6349
|
let rulerY = axisTop + (2 * TIME_BAND_ROW_H) + 4;
|
|
5603
|
-
|
|
6350
|
+
ax.append('line').attr('x1', xOfAge(rootAge)).attr('x2', xOfAge(youngBound))
|
|
5604
6351
|
.attr('y1', rulerY).attr('y2', rulerY)
|
|
5605
6352
|
.attr('stroke', ink).attr('stroke-width', 1);
|
|
5606
6353
|
let labels = [];
|
|
@@ -5620,7 +6367,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5620
6367
|
let placed = [];
|
|
5621
6368
|
labels.forEach(function (l) {
|
|
5622
6369
|
let x = xOfAge(l.age);
|
|
5623
|
-
|
|
6370
|
+
ax.append('line').attr('x1', x).attr('x2', x)
|
|
5624
6371
|
.attr('y1', rulerY).attr('y2', rulerY + 4)
|
|
5625
6372
|
.attr('stroke', ink).attr('stroke-width', 1);
|
|
5626
6373
|
let text = String(l.age);
|
|
@@ -5633,14 +6380,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
5633
6380
|
}
|
|
5634
6381
|
}
|
|
5635
6382
|
if (ok || l.priority > 0) {
|
|
5636
|
-
|
|
6383
|
+
ax.append('text').attr('x', x).attr('y', rulerY + 14)
|
|
5637
6384
|
.attr('text-anchor', 'middle')
|
|
5638
6385
|
.style('font-size', '9px').style('fill', ink)
|
|
5639
6386
|
.text(text);
|
|
5640
6387
|
placed.push(x);
|
|
5641
6388
|
}
|
|
5642
6389
|
});
|
|
5643
|
-
|
|
6390
|
+
ax.append('text').attr('x', xOfAge(youngBound) + 8).attr('y', rulerY + 14)
|
|
5644
6391
|
.attr('text-anchor', 'start')
|
|
5645
6392
|
.style('font-size', '9px').style('fill', ink)
|
|
5646
6393
|
.text('Ma');
|
|
@@ -5666,20 +6413,21 @@ function (root, d3, forester, phyloXml) {
|
|
|
5666
6413
|
}
|
|
5667
6414
|
});
|
|
5668
6415
|
}
|
|
6416
|
+
axisBackdrop(xOfYear(rootYear), xOfYear(present));
|
|
5669
6417
|
let rulerY = axisTop + 4;
|
|
5670
|
-
|
|
6418
|
+
ax.append('line').attr('x1', xOfYear(rootYear)).attr('x2', xOfYear(present))
|
|
5671
6419
|
.attr('y1', rulerY).attr('y2', rulerY)
|
|
5672
6420
|
.attr('stroke', ink).attr('stroke-width', 1);
|
|
5673
6421
|
let lastRight = -Infinity;
|
|
5674
6422
|
forester.calendarTickYears(rootYear, present).forEach(function (yv) {
|
|
5675
6423
|
let x = xOfYear(yv);
|
|
5676
|
-
|
|
6424
|
+
ax.append('line').attr('x1', x).attr('x2', x)
|
|
5677
6425
|
.attr('y1', rulerY).attr('y2', rulerY + 4)
|
|
5678
6426
|
.attr('stroke', ink).attr('stroke-width', 1);
|
|
5679
6427
|
let text = String(Math.round(yv));
|
|
5680
6428
|
let half = text.length * 2.8;
|
|
5681
6429
|
if ((x - half) >= lastRight + 4) {
|
|
5682
|
-
|
|
6430
|
+
ax.append('text').attr('x', x).attr('y', rulerY + 14)
|
|
5683
6431
|
.attr('text-anchor', 'middle')
|
|
5684
6432
|
.style('font-size', '9px').style('fill', ink)
|
|
5685
6433
|
.text(text);
|
|
@@ -5687,16 +6435,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
5687
6435
|
}
|
|
5688
6436
|
});
|
|
5689
6437
|
}
|
|
6438
|
+
placeFloatingOverlays();
|
|
5690
6439
|
}
|
|
5691
6440
|
|
|
5692
6441
|
function timeAxisCbClicked() {
|
|
5693
6442
|
_state.showTimeAxis = getCheckboxValue(TIME_AXIS_CB);
|
|
5694
|
-
|
|
6443
|
+
scheduleUpdate(null, 0);
|
|
5695
6444
|
}
|
|
5696
6445
|
|
|
5697
6446
|
function timeGridCbClicked() {
|
|
5698
6447
|
_state.timeAxisGrid = getCheckboxValue(TIME_GRID_CB);
|
|
5699
|
-
|
|
6448
|
+
scheduleUpdate(null, 0);
|
|
5700
6449
|
}
|
|
5701
6450
|
|
|
5702
6451
|
// Fit the circular tree into the viewport, centred. The root is at the
|
|
@@ -5910,7 +6659,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
5910
6659
|
setValue(SEARCH_FIELD_1, '');
|
|
5911
6660
|
}
|
|
5912
6661
|
|
|
5913
|
-
|
|
6662
|
+
// the searches each end in a redraw of their own; on a big tree all
|
|
6663
|
+
// three collapse into one
|
|
6664
|
+
scheduleUpdate(null, 0);
|
|
5914
6665
|
search0();
|
|
5915
6666
|
search1();
|
|
5916
6667
|
|
|
@@ -5957,7 +6708,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5957
6708
|
if (aActive) _foundNodes0 = forester.searchWithSpec(_root, specA);
|
|
5958
6709
|
if (bActive) _foundNodes1 = forester.searchWithSpec(_root, specB);
|
|
5959
6710
|
}
|
|
5960
|
-
|
|
6711
|
+
scheduleUpdate(null, 0, true);
|
|
5961
6712
|
}
|
|
5962
6713
|
|
|
5963
6714
|
// The Combine control only makes sense when both boxes hold a query.
|
|
@@ -6127,21 +6878,21 @@ function (root, d3, forester, phyloXml) {
|
|
|
6127
6878
|
_state.phylogram = true;
|
|
6128
6879
|
_state.alignPhylogram = false;
|
|
6129
6880
|
setDisplayTypeButtons();
|
|
6130
|
-
|
|
6881
|
+
scheduleUpdate(null, 0);
|
|
6131
6882
|
}
|
|
6132
6883
|
|
|
6133
6884
|
function toAlignedPhylogram() {
|
|
6134
6885
|
_state.phylogram = true;
|
|
6135
6886
|
_state.alignPhylogram = true;
|
|
6136
6887
|
setDisplayTypeButtons();
|
|
6137
|
-
|
|
6888
|
+
scheduleUpdate(null, 0);
|
|
6138
6889
|
}
|
|
6139
6890
|
|
|
6140
6891
|
function toCladegram() {
|
|
6141
6892
|
_state.phylogram = false;
|
|
6142
6893
|
_state.alignPhylogram = false;
|
|
6143
6894
|
setDisplayTypeButtons();
|
|
6144
|
-
|
|
6895
|
+
scheduleUpdate(null, 0);
|
|
6145
6896
|
}
|
|
6146
6897
|
|
|
6147
6898
|
function layoutButtonClicked() {
|
|
@@ -6248,7 +6999,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6248
6999
|
}
|
|
6249
7000
|
search0();
|
|
6250
7001
|
search1();
|
|
6251
|
-
|
|
7002
|
+
scheduleUpdate();
|
|
6252
7003
|
}
|
|
6253
7004
|
|
|
6254
7005
|
|
|
@@ -6276,7 +7027,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6276
7027
|
}
|
|
6277
7028
|
search0();
|
|
6278
7029
|
search1();
|
|
6279
|
-
|
|
7030
|
+
scheduleUpdate();
|
|
6280
7031
|
}
|
|
6281
7032
|
|
|
6282
7033
|
function sequenceCbClicked() {
|
|
@@ -6287,65 +7038,64 @@ function (root, d3, forester, phyloXml) {
|
|
|
6287
7038
|
}
|
|
6288
7039
|
search0();
|
|
6289
7040
|
search1();
|
|
6290
|
-
|
|
7041
|
+
scheduleUpdate();
|
|
6291
7042
|
}
|
|
6292
7043
|
|
|
6293
7044
|
function confidenceValuesCbClicked() {
|
|
6294
7045
|
_state.showConfidenceValues = getCheckboxValue(CONFIDENCE_VALUES_CB);
|
|
6295
7046
|
search0();
|
|
6296
7047
|
search1();
|
|
6297
|
-
|
|
7048
|
+
scheduleUpdate();
|
|
6298
7049
|
}
|
|
6299
7050
|
|
|
6300
7051
|
function supportDotsCbClicked() {
|
|
6301
7052
|
_state.showSupportDots = getCheckboxValue(SUPPORT_DOTS_CB);
|
|
6302
|
-
|
|
7053
|
+
scheduleUpdate();
|
|
6303
7054
|
}
|
|
6304
7055
|
|
|
6305
7056
|
function branchLengthsCbClicked() {
|
|
6306
7057
|
_state.showBranchLengthValues = getCheckboxValue(BRANCH_LENGTH_VALUES_CB);
|
|
6307
|
-
|
|
7058
|
+
scheduleUpdate();
|
|
6308
7059
|
}
|
|
6309
7060
|
|
|
6310
7061
|
function nodeEventsCbClicked() {
|
|
6311
7062
|
_state.showNodeEvents = getCheckboxValue(NODE_EVENTS_CB);
|
|
6312
7063
|
search0();
|
|
6313
7064
|
search1();
|
|
6314
|
-
|
|
7065
|
+
scheduleUpdate();
|
|
6315
7066
|
}
|
|
6316
7067
|
|
|
6317
7068
|
function branchEventsCbClicked() {
|
|
6318
7069
|
_state.showBranchEvents = getCheckboxValue(BRANCH_EVENTS_CB);
|
|
6319
7070
|
search0();
|
|
6320
7071
|
search1();
|
|
6321
|
-
|
|
7072
|
+
scheduleUpdate();
|
|
6322
7073
|
}
|
|
6323
7074
|
|
|
6324
7075
|
function internalLabelsCbClicked() {
|
|
6325
7076
|
_state.showInternalLabels = getCheckboxValue(INTERNAL_LABEL_CB);
|
|
6326
7077
|
search0();
|
|
6327
7078
|
search1();
|
|
6328
|
-
|
|
7079
|
+
scheduleUpdate();
|
|
6329
7080
|
}
|
|
6330
7081
|
|
|
6331
7082
|
function externalLabelsCbClicked() {
|
|
6332
7083
|
_state.showExternalLabels = getCheckboxValue(EXTERNAL_LABEL_CB);
|
|
6333
7084
|
search0();
|
|
6334
7085
|
search1();
|
|
6335
|
-
|
|
7086
|
+
scheduleUpdate();
|
|
6336
7087
|
}
|
|
6337
7088
|
|
|
6338
7089
|
function visCbClicked() {
|
|
6339
7090
|
_state.showVisualizations = getCheckboxValue(VIS_CB);
|
|
6340
7091
|
resetVis();
|
|
6341
|
-
|
|
6342
|
-
update(null, 0);
|
|
7092
|
+
scheduleUpdate(null, 0);
|
|
6343
7093
|
}
|
|
6344
7094
|
|
|
6345
7095
|
function visualStylesCbClicked() {
|
|
6346
7096
|
_state.useVisualStyles = getCheckboxValue(VISUAL_STYLES_CB);
|
|
6347
7097
|
resetVis();
|
|
6348
|
-
|
|
7098
|
+
scheduleUpdate(null, 0);
|
|
6349
7099
|
}
|
|
6350
7100
|
|
|
6351
7101
|
function dynaHideCbClicked() {
|
|
@@ -6353,8 +7103,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6353
7103
|
resetVis();
|
|
6354
7104
|
search0();
|
|
6355
7105
|
search1();
|
|
6356
|
-
|
|
6357
|
-
update(null, 0);
|
|
7106
|
+
scheduleUpdate(null, 0);
|
|
6358
7107
|
}
|
|
6359
7108
|
|
|
6360
7109
|
function shortenCbClicked() {
|
|
@@ -6362,7 +7111,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6362
7111
|
resetVis();
|
|
6363
7112
|
search0();
|
|
6364
7113
|
search1();
|
|
6365
|
-
|
|
7114
|
+
scheduleUpdate(null, 0);
|
|
6366
7115
|
}
|
|
6367
7116
|
|
|
6368
7117
|
function downloadButtonPressed() {
|
|
@@ -6373,11 +7122,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
6373
7122
|
}
|
|
6374
7123
|
}
|
|
6375
7124
|
|
|
6376
|
-
function submitSelectedPressed() {
|
|
6377
|
-
const event = new Event('submit_selected_nodes_event');
|
|
6378
|
-
document.dispatchEvent(event);
|
|
6379
|
-
}
|
|
6380
|
-
|
|
6381
7125
|
function changeBaseBackgoundColor(color) {
|
|
6382
7126
|
setStylesAll('.' + BASE_BACKGROUND, {
|
|
6383
7127
|
'fill': color
|
|
@@ -6386,12 +7130,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
6386
7130
|
|
|
6387
7131
|
function changeBranchWidth(e) {
|
|
6388
7132
|
_state.branchWidthDefault = getSliderValue(e);
|
|
6389
|
-
|
|
7133
|
+
scheduleUpdate(null, 0, true);
|
|
6390
7134
|
}
|
|
6391
7135
|
|
|
6392
7136
|
function changeNodeSize(e) {
|
|
6393
7137
|
_state.nodeSizeDefault = getSliderValue(e);
|
|
6394
|
-
|
|
7138
|
+
scheduleUpdate(null, 0, true);
|
|
6395
7139
|
}
|
|
6396
7140
|
|
|
6397
7141
|
|
|
@@ -6399,7 +7143,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6399
7143
|
// data alike, as the desktop does it.
|
|
6400
7144
|
function changeFontSize(e) {
|
|
6401
7145
|
setFontSizes(getSliderValue(e));
|
|
6402
|
-
|
|
7146
|
+
scheduleUpdate(null, 0, true);
|
|
6403
7147
|
}
|
|
6404
7148
|
|
|
6405
7149
|
function clampFontSize(v) {
|
|
@@ -6568,6 +7312,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6568
7312
|
_state.backgroundColorDefault = dark ? BACKGROUND_COLOR_DARK : BACKGROUND_COLOR_DEFAULT;
|
|
6569
7313
|
_state.branchColorDefault = dark ? BRANCH_COLOR_DARK : BRANCH_COLOR_DEFAULT;
|
|
6570
7314
|
_state.labelColorDefault = dark ? LABEL_COLOR_DARK : LABEL_COLOR_DEFAULT;
|
|
7315
|
+
_state.found0ColorDefault = dark ? FOUND0_COLOR_DARK : FOUND0_COLOR_DEFAULT;
|
|
6571
7316
|
// Always white, in both themes: getTreeAsSvg() rewrites the dark label
|
|
6572
7317
|
// and branch colours to their light counterparts on the way out, so an
|
|
6573
7318
|
// export can no longer end up as white text on a white ground.
|
|
@@ -6577,7 +7322,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6577
7322
|
}
|
|
6578
7323
|
changeBaseBackgoundColor(_state.backgroundColorDefault);
|
|
6579
7324
|
applyOverviewTheme();
|
|
6580
|
-
|
|
7325
|
+
scheduleUpdate(null, 0);
|
|
6581
7326
|
}
|
|
6582
7327
|
|
|
6583
7328
|
// Apply the current theme choice to every panel and refresh the switch icons.
|
|
@@ -6988,13 +7733,30 @@ function (root, d3, forester, phyloXml) {
|
|
|
6988
7733
|
+ ' background:#e5484d; color:#fff; }'
|
|
6989
7734
|
+ '.aptx-node-menu hr { border:0; border-top:1px solid var(--p-line); margin:3px 4px; }'
|
|
6990
7735
|
// The node-data dialog, on the same palette as the panel and the menu.
|
|
6991
|
-
+ '.aptx-dialog {'
|
|
7736
|
+
+ '.aptx-dialog, .aptx-busy, .aptx-msa-nav {'
|
|
6992
7737
|
+ ' --p-bg: rgba(255,255,255,0.98); --p-ink:#1e2a35; --p-muted:#6b7a89; --p-faint:#93a3b2;'
|
|
6993
7738
|
+ ' --p-line:#e3e9f0; --p-line-strong:#cad6e1; --p-surface2:#f3f6fa;'
|
|
6994
7739
|
+ ' --p-accent:#2f83f2; --p-accent-ink:#1c5fbf; --p-accent-weak:rgba(47,131,242,0.12);'
|
|
6995
7740
|
+ '}'
|
|
6996
|
-
+ '@media (prefers-color-scheme:dark){ .aptx-dialog:not(.aptx-light):not(.aptx-dark)
|
|
6997
|
-
+ '.aptx-
|
|
7741
|
+
+ '@media (prefers-color-scheme:dark){ .aptx-dialog:not(.aptx-light):not(.aptx-dark),'
|
|
7742
|
+
+ ' .aptx-busy:not(.aptx-light):not(.aptx-dark), .aptx-msa-nav:not(.aptx-light):not(.aptx-dark) {' + dark + '} }'
|
|
7743
|
+
+ '.aptx-dialog.aptx-dark, .aptx-busy.aptx-dark, .aptx-msa-nav.aptx-dark {' + dark + '}'
|
|
7744
|
+
// The "working" card shown over the tree area while a big tree is
|
|
7745
|
+
// drawn. It covers the whole container so nothing underneath --
|
|
7746
|
+
// panel included -- can be clicked into a half-built view.
|
|
7747
|
+
+ '.aptx-busy { position:absolute; inset:0; z-index:1000; display:grid; place-items:center;'
|
|
7748
|
+
+ ' font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;'
|
|
7749
|
+
+ ' font-size:12px; color:var(--p-ink); cursor:progress; }'
|
|
7750
|
+
+ '.aptx-busy-card { display:flex; align-items:center; gap:10px; padding:10px 16px 10px 13px;'
|
|
7751
|
+
+ ' border:1px solid var(--p-line-strong); border-radius:12px; background:var(--p-bg);'
|
|
7752
|
+
+ ' box-shadow:0 24px 48px -16px rgba(23,34,46,0.45),0 4px 12px -4px rgba(23,34,46,0.22); }'
|
|
7753
|
+
+ '.aptx-busy-dot { width:10px; height:10px; border-radius:50%; flex:none;'
|
|
7754
|
+
+ ' border:2px solid var(--p-accent-weak); border-top-color:var(--p-accent);'
|
|
7755
|
+
+ ' animation:aptx-busy-spin 0.8s linear infinite; }'
|
|
7756
|
+
+ '@keyframes aptx-busy-spin { to { transform:rotate(360deg); } }'
|
|
7757
|
+
+ '@media (prefers-reduced-motion:reduce){ .aptx-busy-dot { animation:none;'
|
|
7758
|
+
+ ' border-color:var(--p-accent); } }'
|
|
7759
|
+
+ '.aptx-busy-sub { color:var(--p-muted); }'
|
|
6998
7760
|
+ '.aptx-dialog { padding:0; border:1px solid var(--p-line-strong); border-radius:12px;'
|
|
6999
7761
|
+ ' background:var(--p-bg); color:var(--p-ink); max-width:92vw;'
|
|
7000
7762
|
+ ' box-shadow:0 24px 48px -16px rgba(23,34,46,0.45),0 4px 12px -4px rgba(23,34,46,0.22);'
|
|
@@ -7010,6 +7772,28 @@ function (root, d3, forester, phyloXml) {
|
|
|
7010
7772
|
+ '.aptx-dialog-close:hover { background:var(--p-accent-weak); color:var(--p-accent-ink);'
|
|
7011
7773
|
+ ' border-color:var(--p-accent); }'
|
|
7012
7774
|
+ '.aptx-dialog-body { padding:10px 13px 12px; overflow:auto; }'
|
|
7775
|
+
+ '.aptx-cp-sv { position:relative; width:100%; box-sizing:border-box; height:104px; border-radius:7px;'
|
|
7776
|
+
+ ' border:1px solid var(--p-line-strong); cursor:crosshair; touch-action:none; }'
|
|
7777
|
+
+ '.aptx-cp-hue { position:relative; width:100%; box-sizing:border-box; height:13px; margin-top:9px;'
|
|
7778
|
+
+ ' border-radius:7px; border:1px solid var(--p-line-strong); cursor:ew-resize;'
|
|
7779
|
+
+ ' touch-action:none; background:linear-gradient(to right,#f00 0%,#ff0 17%,#0f0 33%,'
|
|
7780
|
+
+ ' #0ff 50%,#00f 67%,#f0f 83%,#f00 100%); }'
|
|
7781
|
+
+ '.aptx-cp-knob { position:absolute; width:12px; height:12px; margin:-7px 0 0 -7px;'
|
|
7782
|
+
+ ' border-radius:50%; border:2px solid #fff; pointer-events:none;'
|
|
7783
|
+
+ ' box-shadow:0 0 0 1px rgba(0,0,0,0.45),0 1px 3px rgba(0,0,0,0.4); }'
|
|
7784
|
+
+ '.aptx-cp-row { display:flex; align-items:center; gap:7px; margin-top:9px; }'
|
|
7785
|
+
+ '.aptx-cp-prev { flex:none; width:26px; height:20px; border-radius:5px;'
|
|
7786
|
+
+ ' border:1px solid var(--p-line-strong); }'
|
|
7787
|
+
+ '.aptx-cp-hex { flex:1; min-width:0; padding:3px 6px; border-radius:5px;'
|
|
7788
|
+
+ ' border:1px solid var(--p-line-strong); background:var(--p-surface2);'
|
|
7789
|
+
+ ' color:var(--p-ink); font:inherit; font-variant-numeric:tabular-nums; }'
|
|
7790
|
+
+ '.aptx-cp-hex:focus { outline:none; border-color:var(--p-accent); }'
|
|
7791
|
+
+ '.aptx-cp-grid { display:grid; grid-template-columns:repeat(10,1fr); gap:3px; margin-top:9px; }'
|
|
7792
|
+
+ '.aptx-cp-sw { width:100%; aspect-ratio:1; padding:0; border-radius:4px; cursor:pointer;'
|
|
7793
|
+
+ ' border:1px solid rgba(128,128,128,0.45); }'
|
|
7794
|
+
+ '.aptx-cp-sw:hover { outline:2px solid var(--p-accent); outline-offset:1px; }'
|
|
7795
|
+
+ '.aptx-cp-note { margin-top:8px; color:var(--p-faint); }'
|
|
7796
|
+
+ '#color_picker_dialog .aptx-dialog-body { overflow:visible; }'
|
|
7013
7797
|
+ '.aptx-dialog-head { margin:10px 0 3px; font-size:9px; font-weight:700; letter-spacing:0.07em;'
|
|
7014
7798
|
+ ' text-transform:uppercase; color:var(--p-faint); }'
|
|
7015
7799
|
+ '.aptx-dialog-body > .aptx-dialog-head:first-child { margin-top:0; }'
|
|
@@ -7120,7 +7904,21 @@ function (root, d3, forester, phyloXml) {
|
|
|
7120
7904
|
// drawn glyph. The glyph inherits the button's colour (currentColor)
|
|
7121
7905
|
// and a disabled button fades the whole svg with the button chrome.
|
|
7122
7906
|
+ '.aptx-panel .aptx-gbtn { display:inline-flex; align-items:center; justify-content:center; min-width:32px; padding:0 7px; vertical-align:middle; }'
|
|
7123
|
-
+ '.aptx-msa-
|
|
7907
|
+
+ '.aptx-msa-nav { position:fixed; bottom:10px; left:55%; transform:translateX(-50%); z-index:20;'
|
|
7908
|
+
+ ' display:none; align-items:center; gap:6px; padding:6px 10px; border-radius:10px;'
|
|
7909
|
+
+ ' border:1px solid var(--p-line-strong); background:var(--p-bg); color:var(--p-ink);'
|
|
7910
|
+
+ ' box-shadow:0 8px 24px -10px rgba(23,34,46,0.35); font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;'
|
|
7911
|
+
+ ' font-size:11.5px; line-height:1; }'
|
|
7912
|
+
+ '.aptx-msa-scroll { width:260px; margin:0 2px; }'
|
|
7913
|
+
+ '.aptx-msa-nav-btn { width:24px; height:22px; padding:0; border-radius:6px; border:1px solid var(--p-line-strong);'
|
|
7914
|
+
+ ' background:var(--p-surface2); color:var(--p-ink); font-size:10px; line-height:1; cursor:pointer; }'
|
|
7915
|
+
+ '.aptx-msa-nav-btn:hover:not(:disabled) { border-color:var(--p-accent); color:var(--p-accent-ink); }'
|
|
7916
|
+
+ '.aptx-msa-nav-btn:disabled { opacity:0.35; cursor:default; }'
|
|
7917
|
+
+ '.aptx-msa-nav-read { display:inline-flex; align-items:center; gap:5px; margin-left:6px; color:var(--p-muted);'
|
|
7918
|
+
+ ' font-variant-numeric:tabular-nums; white-space:nowrap; }'
|
|
7919
|
+
+ '.aptx-msa-nav-jump { width:64px; padding:2px 5px; border-radius:5px; border:1px solid var(--p-line-strong);'
|
|
7920
|
+
+ ' background:var(--p-surface2); color:var(--p-ink); font:inherit; font-variant-numeric:tabular-nums; text-align:right; }'
|
|
7921
|
+
+ '.aptx-msa-nav-jump:focus { outline:none; border-color:var(--p-accent); }'
|
|
7124
7922
|
+ '.aptx-panel .aptx-seg:has(input:disabled) { opacity:0.4; }'
|
|
7125
7923
|
+ '.aptx-panel .aptx-seg:has(input:disabled) { cursor:default; }'
|
|
7126
7924
|
+ '.aptx-panel .aptx-slider-row { display:flex; align-items:center; gap:7px; margin:3px 0; }'
|
|
@@ -7368,6 +8166,161 @@ function (root, d3, forester, phyloXml) {
|
|
|
7368
8166
|
// Content arrives as "Label: value" lines separated by <br>. Setting the
|
|
7369
8167
|
// label part apart makes a wall of such lines scannable. Lines without a
|
|
7370
8168
|
// label (a heading like "Taxonomy", or a FASTA sequence) are left alone.
|
|
8169
|
+
// The node's data as the hover tooltip and the "Display Node Data" dialog
|
|
8170
|
+
// both show it -- ONE builder, because they used to be two near-copies
|
|
8171
|
+
// and carried the same two bugs twice (a bare "Date: " line whenever a
|
|
8172
|
+
// date had no description, and "Distribution: " printed before its
|
|
8173
|
+
// description was checked).
|
|
8174
|
+
//
|
|
8175
|
+
// Order, as the desktop lays it out: first the node itself -- Name,
|
|
8176
|
+
// Distance to parent, Date, Distribution, Depth, Tips below -- then its
|
|
8177
|
+
// confidences, then the headed sections. Every row is either in that
|
|
8178
|
+
// leading block or under a heading, so nothing reads as a stray tail of
|
|
8179
|
+
// the section above it. markUpDataLabels() renders the result: a line
|
|
8180
|
+
// shaped "Key: value" is a row, anything else a heading, and a leading
|
|
8181
|
+
// "- " marks a row as a section's sub-entry.
|
|
8182
|
+
function nodeDataText(d) {
|
|
8183
|
+
let text = '';
|
|
8184
|
+
if (d.name) {
|
|
8185
|
+
text += 'Name: ' + d.name + '<br>';
|
|
8186
|
+
}
|
|
8187
|
+
if (d.branch_length) {
|
|
8188
|
+
text += 'Distance to parent: ' + d.branch_length + '<br>';
|
|
8189
|
+
}
|
|
8190
|
+
let date = dateText(d.date);
|
|
8191
|
+
if (date) {
|
|
8192
|
+
text += 'Date: ' + date + '<br>';
|
|
8193
|
+
}
|
|
8194
|
+
if (d.distributions) {
|
|
8195
|
+
for (let i = 0; i < d.distributions.length; ++i) {
|
|
8196
|
+
// only a distribution that has something to say gets a row
|
|
8197
|
+
if (d.distributions[i].desc) {
|
|
8198
|
+
text += 'Distribution: ' + d.distributions[i].desc + '<br>';
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
8201
|
+
}
|
|
8202
|
+
text += 'Depth: ' + forester.calcDepth(d) + '<br>';
|
|
8203
|
+
if (d.children) {
|
|
8204
|
+
text += 'Tips below: ' + forester.calcSumOfAllExternalDescendants(d) + '<br>';
|
|
8205
|
+
}
|
|
8206
|
+
if (d.confidences) {
|
|
8207
|
+
for (let i = 0; i < d.confidences.length; ++i) {
|
|
8208
|
+
let c = d.confidences[i];
|
|
8209
|
+
text += (c.type ? 'Confidence [' + c.type + ']: ' : 'Confidence: ') + c.value + '<br>';
|
|
8210
|
+
if (c.stddev) {
|
|
8211
|
+
text += '- stdev: ' + c.stddev + '<br>';
|
|
8212
|
+
}
|
|
8213
|
+
}
|
|
8214
|
+
}
|
|
8215
|
+
if (d.taxonomies) {
|
|
8216
|
+
for (let i = 0; i < d.taxonomies.length; ++i) {
|
|
8217
|
+
let tx = d.taxonomies[i];
|
|
8218
|
+
text += 'Taxonomy<br>';
|
|
8219
|
+
if (tx.id) {
|
|
8220
|
+
text += (tx.id.provider ? '- Id [' + tx.id.provider + ']: ' : '- Id: ') + tx.id.value + '<br>';
|
|
8221
|
+
}
|
|
8222
|
+
if (tx.code) {
|
|
8223
|
+
text += '- Code: ' + tx.code + '<br>';
|
|
8224
|
+
}
|
|
8225
|
+
if (tx.scientific_name) {
|
|
8226
|
+
text += '- Scientific name: ' + tx.scientific_name + '<br>';
|
|
8227
|
+
}
|
|
8228
|
+
if (tx.common_name) {
|
|
8229
|
+
text += '- Common name: ' + tx.common_name + '<br>';
|
|
8230
|
+
}
|
|
8231
|
+
if (tx.rank) {
|
|
8232
|
+
text += '- Rank: ' + tx.rank + '<br>';
|
|
8233
|
+
}
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
if (d.sequences) {
|
|
8237
|
+
for (let i = 0; i < d.sequences.length; ++i) {
|
|
8238
|
+
let s = d.sequences[i];
|
|
8239
|
+
text += 'Sequence<br>';
|
|
8240
|
+
if (s.accession) {
|
|
8241
|
+
text += (s.accession.source ? '- Accession [' + s.accession.source + ']: ' : '- Accession: ')
|
|
8242
|
+
+ s.accession.value + '<br>';
|
|
8243
|
+
if (s.accession.comment) {
|
|
8244
|
+
text += '-- comment: ' + s.accession.comment + '<br>';
|
|
8245
|
+
}
|
|
8246
|
+
}
|
|
8247
|
+
if (s.symbol) {
|
|
8248
|
+
text += '- Symbol: ' + s.symbol + '<br>';
|
|
8249
|
+
}
|
|
8250
|
+
if (s.name) {
|
|
8251
|
+
text += '- Name: ' + s.name + '<br>';
|
|
8252
|
+
}
|
|
8253
|
+
if (s.gene_name) {
|
|
8254
|
+
text += '- Gene name: ' + s.gene_name + '<br>';
|
|
8255
|
+
}
|
|
8256
|
+
if (s.location) {
|
|
8257
|
+
text += '- Location: ' + s.location + '<br>';
|
|
8258
|
+
}
|
|
8259
|
+
if (s.type) {
|
|
8260
|
+
text += '- Type: ' + s.type + '<br>';
|
|
8261
|
+
}
|
|
8262
|
+
}
|
|
8263
|
+
}
|
|
8264
|
+
if (d.events) {
|
|
8265
|
+
let ev = d.events;
|
|
8266
|
+
text += 'Events<br>';
|
|
8267
|
+
if (ev.type && ev.type.length > 0) {
|
|
8268
|
+
text += '- Type: ' + ev.type + '<br>';
|
|
8269
|
+
}
|
|
8270
|
+
if (ev.duplications && ev.duplications > 0) {
|
|
8271
|
+
text += '- Duplications: ' + ev.duplications + '<br>';
|
|
8272
|
+
}
|
|
8273
|
+
if (ev.speciations && ev.speciations > 0) {
|
|
8274
|
+
text += '- Speciations: ' + ev.speciations + '<br>';
|
|
8275
|
+
}
|
|
8276
|
+
if (ev.losses && ev.losses > 0) {
|
|
8277
|
+
text += '- Losses: ' + ev.losses + '<br>';
|
|
8278
|
+
}
|
|
8279
|
+
}
|
|
8280
|
+
if (d.properties && d.properties.length > 0) {
|
|
8281
|
+
// Named as the Color-by menu names them ("Host Group", not
|
|
8282
|
+
// "BVBRC:host_group"). style:* refs are the desktop's visual-style
|
|
8283
|
+
// namespace -- how a node is drawn, not what it is -- and are
|
|
8284
|
+
// skipped, as the Color-by menu skips them.
|
|
8285
|
+
let rows = '';
|
|
8286
|
+
for (let i = 0; i < d.properties.length; ++i) {
|
|
8287
|
+
let pr = d.properties[i];
|
|
8288
|
+
if (pr.ref && pr.value && pr.ref.indexOf('style:') !== 0) {
|
|
8289
|
+
rows += '- ' + forester.propertyDisplayName(pr.ref) + ': '
|
|
8290
|
+
+ pr.value + (pr.unit ? ' ' + pr.unit : '') + '<br>';
|
|
8291
|
+
}
|
|
8292
|
+
}
|
|
8293
|
+
if (rows) {
|
|
8294
|
+
text += 'Properties<br>' + rows;
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
return text;
|
|
8298
|
+
}
|
|
8299
|
+
|
|
8300
|
+
// One line for a node's date, as the desktop writes it: the value, its
|
|
8301
|
+
// range, its unit and its description folded together -- "6.5 [5 - 8]
|
|
8302
|
+
// mya (split)" -- or just the description when that is all there is.
|
|
8303
|
+
// Null when there is nothing to say, so no bare "Date: " ever appears.
|
|
8304
|
+
function dateText(date) {
|
|
8305
|
+
if (!date) {
|
|
8306
|
+
return null;
|
|
8307
|
+
}
|
|
8308
|
+
let hasValue = typeof date.value === 'number';
|
|
8309
|
+
let hasRange = typeof date.minimum === 'number' && typeof date.maximum === 'number';
|
|
8310
|
+
let s = hasValue ? String(date.value) : '';
|
|
8311
|
+
if (hasRange) {
|
|
8312
|
+
s += ' [' + date.minimum + ' - ' + date.maximum + ']';
|
|
8313
|
+
}
|
|
8314
|
+
if ((hasValue || hasRange) && date.unit) {
|
|
8315
|
+
s += ' ' + date.unit;
|
|
8316
|
+
}
|
|
8317
|
+
s = s.trim();
|
|
8318
|
+
if (date.desc) {
|
|
8319
|
+
s = s ? s + ' (' + date.desc + ')' : date.desc;
|
|
8320
|
+
}
|
|
8321
|
+
return s || null;
|
|
8322
|
+
}
|
|
8323
|
+
|
|
7371
8324
|
function markUpDataLabels(escaped) {
|
|
7372
8325
|
return escaped.split('<br>').map(function (line) {
|
|
7373
8326
|
if (!line.trim()) {
|
|
@@ -7389,6 +8342,68 @@ function (root, d3, forester, phyloXml) {
|
|
|
7389
8342
|
// The shell every modal shares: title bar with a close button, a body, the
|
|
7390
8343
|
// panel's light/dark choice, and removal on close. Returns the body for the
|
|
7391
8344
|
// caller to fill. Only one dialog of a given id exists at a time.
|
|
8345
|
+
// A "working" card over the tree area. Shown while a big tree's first draw
|
|
8346
|
+
// is deferred, so the user sees the program is busy rather than a page that
|
|
8347
|
+
// has stopped responding -- which is what a 10-second synchronous draw
|
|
8348
|
+
// looks like from the outside.
|
|
8349
|
+
function showBusy(container, text, sub) {
|
|
8350
|
+
container.querySelectorAll(':scope > .aptx-busy').forEach(function (b) {
|
|
8351
|
+
b.remove();
|
|
8352
|
+
});
|
|
8353
|
+
let busy = document.createElement('div');
|
|
8354
|
+
busy.className = 'aptx-busy';
|
|
8355
|
+
if (_panelTheme) {
|
|
8356
|
+
busy.classList.add('aptx-' + _panelTheme);
|
|
8357
|
+
}
|
|
8358
|
+
busy.setAttribute('role', 'status');
|
|
8359
|
+
busy.setAttribute('aria-live', 'polite');
|
|
8360
|
+
let card = document.createElement('div');
|
|
8361
|
+
card.className = 'aptx-busy-card';
|
|
8362
|
+
let dot = document.createElement('span');
|
|
8363
|
+
dot.className = 'aptx-busy-dot';
|
|
8364
|
+
let label = document.createElement('span');
|
|
8365
|
+
label.textContent = text;
|
|
8366
|
+
card.appendChild(dot);
|
|
8367
|
+
card.appendChild(label);
|
|
8368
|
+
if (sub) {
|
|
8369
|
+
let s = document.createElement('span');
|
|
8370
|
+
s.className = 'aptx-busy-sub';
|
|
8371
|
+
s.textContent = sub;
|
|
8372
|
+
card.appendChild(s);
|
|
8373
|
+
}
|
|
8374
|
+
busy.appendChild(card);
|
|
8375
|
+
container.appendChild(busy);
|
|
8376
|
+
return busy;
|
|
8377
|
+
}
|
|
8378
|
+
|
|
8379
|
+
function hideBusy() {
|
|
8380
|
+
if (_container) {
|
|
8381
|
+
_container.querySelectorAll('.aptx-busy').forEach(function (b) {
|
|
8382
|
+
b.remove();
|
|
8383
|
+
});
|
|
8384
|
+
}
|
|
8385
|
+
}
|
|
8386
|
+
|
|
8387
|
+
// Run fn after the browser has had a chance to PAINT. A plain setTimeout
|
|
8388
|
+
// does not promise that; rAF fires just before a paint, so a timeout queued
|
|
8389
|
+
// from inside it lands just after one. A hidden tab may never fire rAF at
|
|
8390
|
+
// all, so a fallback timer guarantees the work still happens.
|
|
8391
|
+
function afterPaint(fn) {
|
|
8392
|
+
let done = false;
|
|
8393
|
+
function go() {
|
|
8394
|
+
if (!done) {
|
|
8395
|
+
done = true;
|
|
8396
|
+
fn();
|
|
8397
|
+
}
|
|
8398
|
+
}
|
|
8399
|
+
if (typeof requestAnimationFrame === 'function') {
|
|
8400
|
+
requestAnimationFrame(function () {
|
|
8401
|
+
setTimeout(go, 0);
|
|
8402
|
+
});
|
|
8403
|
+
}
|
|
8404
|
+
setTimeout(go, 150);
|
|
8405
|
+
}
|
|
8406
|
+
|
|
7392
8407
|
function makeDialogShell(id, title, width) {
|
|
7393
8408
|
let existing = document.getElementById(id);
|
|
7394
8409
|
if (existing) {
|
|
@@ -7568,7 +8583,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7568
8583
|
}
|
|
7569
8584
|
setFontSizes(base);
|
|
7570
8585
|
setSliderValue(FONT_SIZE_SLIDER, _state.externalNodeFontSize);
|
|
7571
|
-
|
|
8586
|
+
scheduleUpdate(null, 0, true);
|
|
7572
8587
|
}
|
|
7573
8588
|
|
|
7574
8589
|
|
|
@@ -7660,10 +8675,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
7660
8675
|
|
|
7661
8676
|
c0.insertAdjacentHTML('beforeend',makeSearchBoxes());
|
|
7662
8677
|
|
|
7663
|
-
if (_settings.enableManualNodeSelection) {
|
|
7664
|
-
//c0.append(makeSubmitSection()); //~~~
|
|
7665
|
-
}
|
|
7666
|
-
|
|
7667
8678
|
if (_settings.enableDownloads) {
|
|
7668
8679
|
c0.insertAdjacentHTML('beforeend',makeDownloadSection());
|
|
7669
8680
|
}
|
|
@@ -7706,14 +8717,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
7706
8717
|
});
|
|
7707
8718
|
}
|
|
7708
8719
|
|
|
7709
|
-
const submitSelectedButton = byId(SUBMIT_SELECTED_NODES_BUTTON);
|
|
7710
|
-
|
|
7711
|
-
if (submitSelectedButton) {
|
|
7712
|
-
setStyles(submitSelectedButton, {
|
|
7713
|
-
'width': '80px', 'margin-bottom': '3px'
|
|
7714
|
-
});
|
|
7715
|
-
}
|
|
7716
|
-
|
|
7717
8720
|
|
|
7718
8721
|
on(SEARCH_FIELD_0, 'keyup', search0);
|
|
7719
8722
|
|
|
@@ -7904,10 +8907,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
7904
8907
|
downloadButton.addEventListener('click', downloadButtonPressed);
|
|
7905
8908
|
}
|
|
7906
8909
|
|
|
7907
|
-
if (submitSelectedButton) {
|
|
7908
|
-
submitSelectedButton.addEventListener('click', submitSelectedPressed);
|
|
7909
|
-
}
|
|
7910
|
-
|
|
7911
8910
|
setStyles(byId(EXPORT_FORMAT_SELECT), {
|
|
7912
8911
|
'font': 'inherit', 'color': 'inherit'
|
|
7913
8912
|
});
|
|
@@ -8320,16 +9319,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
8320
9319
|
return h;
|
|
8321
9320
|
}
|
|
8322
9321
|
|
|
8323
|
-
function makeSubmitSection() {
|
|
8324
|
-
let h = "";
|
|
8325
|
-
h = h.concat('<fieldset>');
|
|
8326
|
-
h = h.concat('<div class="submit_nodes">');
|
|
8327
|
-
h = h.concat('<input type="button" value="Submit Selected" name="' + SUBMIT_SELECTED_NODES_BUTTON + '" title="to submit the selected nodes" id="' + SUBMIT_SELECTED_NODES_BUTTON + '">');
|
|
8328
|
-
h = h.concat('</div>');
|
|
8329
|
-
h = h.concat('</fieldset>');
|
|
8330
|
-
return h;
|
|
8331
|
-
}
|
|
8332
|
-
|
|
8333
9322
|
function makeSliders() {
|
|
8334
9323
|
let h = "";
|
|
8335
9324
|
h = h.concat('<fieldset>');
|
|
@@ -8627,7 +9616,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8627
9616
|
b.style.color = '';
|
|
8628
9617
|
} else {
|
|
8629
9618
|
b.style.background = _state.found0ColorDefault;
|
|
8630
|
-
|
|
9619
|
+
// black or white by luminance: the dark theme's sky blue
|
|
9620
|
+
// is too light for white text
|
|
9621
|
+
b.style.color = forester.msaLetterInk(hexToRgbTriple(_state.found0ColorDefault));
|
|
8631
9622
|
}
|
|
8632
9623
|
let nd0 = _foundNodes0.size === 1 ? 'node' : 'nodes';
|
|
8633
9624
|
b.title = 'found ' + _foundNodes0.size + ' ' + nd0 + ' [click to ' + RESET_SEARCH_A_BTN_TOOLTIP + ']';
|
|
@@ -8759,6 +9750,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
8759
9750
|
if (glow) {
|
|
8760
9751
|
glow.remove();
|
|
8761
9752
|
}
|
|
9753
|
+
// The floating strips re-anchor to the TREE for export: on screen
|
|
9754
|
+
// a strip may be stuck at the viewport bottom, but a figure must
|
|
9755
|
+
// not carry an artefact of where the user happened to be scrolled
|
|
9756
|
+
// -- the axis belongs at the bottom of the tree. (As the desktop
|
|
9757
|
+
// does for PDF and graphics export.)
|
|
9758
|
+
let t = d3.zoomTransform(svg);
|
|
9759
|
+
copy.querySelectorAll('.aptx-float > g').forEach(function (strip) {
|
|
9760
|
+
strip.setAttribute('transform', 'translate(' + t.x + ',' + t.y + ') scale(' + t.k + ')');
|
|
9761
|
+
});
|
|
8762
9762
|
svgTree = toLightExport((new XMLSerializer()).serializeToString(copy));
|
|
8763
9763
|
} else if (typeof svg.xml !== 'undefined') {
|
|
8764
9764
|
svgTree = svg.xml;
|
|
@@ -8968,18 +9968,31 @@ function (root, d3, forester, phyloXml) {
|
|
|
8968
9968
|
/**
|
|
8969
9969
|
* Convenience method for loading tree on HTML page
|
|
8970
9970
|
*
|
|
8971
|
-
* @param location
|
|
8972
|
-
*
|
|
8973
|
-
* @param
|
|
8974
|
-
* @param
|
|
9971
|
+
* @param location - file name (only its extension is used; the content is
|
|
9972
|
+
* sniffed too, so a pasted tree with no name works)
|
|
9973
|
+
* @param data - the file's content
|
|
9974
|
+
* @param mode - 'auto' (default) | 'confidence' | 'label', how bare
|
|
9975
|
+
* numeric internal labels are read. The retired
|
|
9976
|
+
* nhConfidenceValuesInBrackets flag used to sit in this
|
|
9977
|
+
* position and is still accepted (and ignored) there,
|
|
9978
|
+
* with the mode following it.
|
|
9979
|
+
* @param legacyMode - the mode, when the retired flag occupies `mode`
|
|
8975
9980
|
* @returns {*}
|
|
8976
9981
|
*/
|
|
8977
|
-
archaeopteryx.parseTree = function (location, data,
|
|
8978
|
-
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
9982
|
+
archaeopteryx.parseTree = function (location, data, mode, legacyMode) {
|
|
9983
|
+
// The retired brackets flag used to be the third argument, so a
|
|
9984
|
+
// boolean there means the old four-argument shape and the mode is the
|
|
9985
|
+
// fourth. A string is the mode itself.
|
|
9986
|
+
let internalNumericLabels = (typeof mode === 'boolean' || mode === undefined) ? legacyMode : mode;
|
|
9987
|
+
// PARSE_DEFAULTS, not literals -- initializeSettings applies the same
|
|
9988
|
+
// object, and it runs at launch(), AFTER this has already parsed.
|
|
9989
|
+
if (typeof internalNumericLabels === 'boolean') {
|
|
9990
|
+
// the retired positional flag: true meant "every numeric internal
|
|
9991
|
+
// label is a confidence", which is 'confidence'.
|
|
9992
|
+
internalNumericLabels = internalNumericLabels
|
|
9993
|
+
? 'confidence' : PARSE_DEFAULTS.internalNumericLabels;
|
|
9994
|
+
} else if (internalNumericLabels === undefined) {
|
|
9995
|
+
internalNumericLabels = PARSE_DEFAULTS.internalNumericLabels;
|
|
8983
9996
|
}
|
|
8984
9997
|
let tree;
|
|
8985
9998
|
let loc = location ? String(location).toLowerCase() : '';
|
|
@@ -8987,34 +10000,94 @@ function (root, d3, forester, phyloXml) {
|
|
|
8987
10000
|
// content decides; the filename alone is enough too.
|
|
8988
10001
|
if ((forester.isString(data) && /^\s*#nexus\b/i.test(data))
|
|
8989
10002
|
|| /\.(nex|nexus)$/.test(loc)) {
|
|
8990
|
-
tree = archaeopteryx.parseNexus(data,
|
|
10003
|
+
tree = archaeopteryx.parseNexus(data, internalNumericLabels);
|
|
8991
10004
|
} else if ((forester.isString(data) && /^\s*\{/.test(data)) || /\.json$/.test(loc)) {
|
|
8992
10005
|
tree = archaeopteryx.parseAuspiceJson(data);
|
|
8993
|
-
} else if (
|
|
10006
|
+
} else if ((forester.isString(data) && /^\s*</.test(data))
|
|
10007
|
+
|| loc.substr(-3, 3) === 'xml') {
|
|
10008
|
+
// No other supported format can start with '<', so content alone
|
|
10009
|
+
// is enough -- pasted phyloXML has no filename to go by.
|
|
8994
10010
|
tree = archaeopteryx.parsePhyloXML(data);
|
|
8995
10011
|
} else {
|
|
8996
|
-
tree = archaeopteryx.parseNewHampshire(data,
|
|
10012
|
+
tree = archaeopteryx.parseNewHampshire(data, internalNumericLabels);
|
|
8997
10013
|
}
|
|
8998
10014
|
return tree;
|
|
8999
10015
|
};
|
|
9000
10016
|
|
|
10017
|
+
// Show the viewer's own "working" card in a container. For the part of a
|
|
10018
|
+
// load the library cannot defer for you -- your own parse of a big file
|
|
10019
|
+
// before launch(), or a fetch -- so the page shows the same card the
|
|
10020
|
+
// viewer shows while it draws, rather than something foreign or nothing.
|
|
10021
|
+
//
|
|
10022
|
+
// With `work`: the card is painted, work() runs on the next frame, and
|
|
10023
|
+
// the card is removed when it returns -- the browser gets a frame in
|
|
10024
|
+
// between, which is the whole point, and a hidden tab still runs the
|
|
10025
|
+
// work on a fallback timer. Without `work`: the card stays until the
|
|
10026
|
+
// returned function is called; then it is on you to yield.
|
|
10027
|
+
archaeopteryx.busy = function (container, text, sub, work) {
|
|
10028
|
+
let el = (typeof container === 'string') ? document.querySelector(container) : container;
|
|
10029
|
+
if (!el) {
|
|
10030
|
+
throw new Error(ERROR + 'busy(): container not found');
|
|
10031
|
+
}
|
|
10032
|
+
injectPanelStyles();
|
|
10033
|
+
let busy;
|
|
10034
|
+
if (el === document.body) {
|
|
10035
|
+
// whole-page: fixed, and never touch the body's own positioning
|
|
10036
|
+
busy = showBusy(el, text, sub);
|
|
10037
|
+
busy.style.position = 'fixed';
|
|
10038
|
+
} else {
|
|
10039
|
+
if (getComputedStyle(el).position === 'static') {
|
|
10040
|
+
el.style.position = 'relative';
|
|
10041
|
+
}
|
|
10042
|
+
busy = showBusy(el, text, sub);
|
|
10043
|
+
}
|
|
10044
|
+
let remove = function () {
|
|
10045
|
+
busy.remove();
|
|
10046
|
+
};
|
|
10047
|
+
if (typeof work === 'function') {
|
|
10048
|
+
afterPaint(function () {
|
|
10049
|
+
try {
|
|
10050
|
+
work();
|
|
10051
|
+
} finally {
|
|
10052
|
+
remove();
|
|
10053
|
+
}
|
|
10054
|
+
});
|
|
10055
|
+
}
|
|
10056
|
+
return remove;
|
|
10057
|
+
};
|
|
10058
|
+
|
|
9001
10059
|
// Parse-and-launch in one step: fetch the file's content yourself (the
|
|
9002
10060
|
// library does no networking), hand the text here. fileName is used only
|
|
9003
10061
|
// to pick the parser (its extension; the content is sniffed too).
|
|
9004
10062
|
// Returns the same viewer handle launch() returns.
|
|
10063
|
+
// launchArchaeopteryx parses BEFORE launch() runs readConfig, so the
|
|
10064
|
+
// deprecated alias has to be resolved HERE as well -- translating it later
|
|
10065
|
+
// would be translating it after the tree was already built. readConfig
|
|
10066
|
+
// emits the warning; this only resolves the value, so it is not doubled.
|
|
10067
|
+
function effectiveInternalLabelsMode(c) {
|
|
10068
|
+
if (c.internalNumericLabels !== undefined) {
|
|
10069
|
+
return c.internalNumericLabels;
|
|
10070
|
+
}
|
|
10071
|
+
if (c.nhConfidenceValuesAsInternalNames !== undefined) {
|
|
10072
|
+
return DEPRECATED_CONFIG.nhConfidenceValuesAsInternalNames
|
|
10073
|
+
.map(c.nhConfidenceValuesAsInternalNames);
|
|
10074
|
+
}
|
|
10075
|
+
return undefined;
|
|
10076
|
+
}
|
|
10077
|
+
|
|
9005
10078
|
archaeopteryx.launchArchaeopteryx = function (container, fileName, data, config) {
|
|
9006
10079
|
if (arguments.length > 4) {
|
|
9007
10080
|
throw new Error(ERROR + 'launchArchaeopteryx() takes exactly (container, fileName,'
|
|
9008
10081
|
+ ' data, config); the old trailing arguments were removed. The separate settings'
|
|
9009
10082
|
+ ' bag and "nodeVisualizations" are gone (see launch()), and the two New Hampshire'
|
|
9010
|
-
+ ' parse
|
|
9011
|
-
+ ' "
|
|
10083
|
+
+ ' parse option moved into the config as'
|
|
10084
|
+
+ ' "internalNumericLabels".');
|
|
9012
10085
|
}
|
|
9013
10086
|
let c = config || {};
|
|
9014
10087
|
let tree;
|
|
9015
10088
|
try {
|
|
9016
10089
|
tree = archaeopteryx.parseTree(fileName, data,
|
|
9017
|
-
c
|
|
10090
|
+
effectiveInternalLabelsMode(c));
|
|
9018
10091
|
} catch (e) {
|
|
9019
10092
|
// Worth catching only to say that it was the parse, not the launch,
|
|
9020
10093
|
// that failed -- a malformed tree file rather than a bug in here.
|