archaeopteryx 3.2.0 → 3.3.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 +96 -8
- package/archaeopteryx.js +1106 -183
- package/forester.js +895 -437
- package/package.json +3 -3
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.3.0
|
|
24
24
|
// 2026-09-10
|
|
25
25
|
//
|
|
26
26
|
// Archaeopteryx.js is a software tool for the visualization and
|
|
@@ -103,7 +103,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
103
103
|
// IIFE's own function name -- a plain object says what it is.)
|
|
104
104
|
let archaeopteryx = {};
|
|
105
105
|
|
|
106
|
-
const VERSION = '3.
|
|
106
|
+
const VERSION = '3.3.0';
|
|
107
107
|
const WEBSITE = 'https://cmzmasek.github.io/archaeopteryx-js/';
|
|
108
108
|
const DESKTOP_WEBSITE = 'https://cmzmasek.github.io/archaeopteryx/';
|
|
109
109
|
const SOURCE_WEBSITE = 'https://github.com/cmzmasek/archaeopteryx-js';
|
|
@@ -251,6 +251,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
251
251
|
const LABEL_SIZE_CALC_FACTOR = 0.5;
|
|
252
252
|
const LEGEND_LABEL_COLOR = 'legendLabelColor';
|
|
253
253
|
const LEGEND_NODE_SHAPE = 'legendNodeShape';
|
|
254
|
+
const LEGEND_DOMAINS = 'legendDomains';
|
|
254
255
|
const NH_EXPORT_FORMAT = 'Newick';
|
|
255
256
|
const NEXUS_EXPORT_FORMAT = 'Nexus';
|
|
256
257
|
const NODE_SIZE_MAX = 9;
|
|
@@ -283,10 +284,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
283
284
|
const TOP_AND_BOTTOM_BORDER_HEIGHT = 10;
|
|
284
285
|
const TRANSITION_DURATION_DEFAULT = 750;
|
|
285
286
|
// 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
|
-
//
|
|
288
|
-
//
|
|
289
|
-
|
|
287
|
+
// "working" card, so the page shows it is busy instead of appearing frozen,
|
|
288
|
+
// and later redraws coalesce instead of running synchronously. Below it
|
|
289
|
+
// everything stays synchronous exactly as before -- the deferral buys
|
|
290
|
+
// nothing on a tree that draws in a few hundred milliseconds.
|
|
291
|
+
//
|
|
292
|
+
// Raised from 2000 to 3000 on 2026-09-11: a redraw of the 18,512-node tree
|
|
293
|
+
// went from ~4.2 s to ~1.9 s that day, so the size at which a draw is slow
|
|
294
|
+
// enough to need announcing moved up with it.
|
|
295
|
+
const BIG_TREE_NODES = 3000;
|
|
290
296
|
const WARNING = 'ArchaeopteryxJS: WARNING';
|
|
291
297
|
const MESSAGE = 'ArchaeopteryxJS: ';
|
|
292
298
|
const ERROR = 'ArchaeopteryxJS: ERROR: ';
|
|
@@ -347,6 +353,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
347
353
|
const DOWNLOAD_BUTTON = 'dl_b';
|
|
348
354
|
const DYNAHIDE_CB = 'dynahide_cb';
|
|
349
355
|
const MSA_CB = 'msa_cb';
|
|
356
|
+
const DOMAINS_CB = 'domains_cb';
|
|
357
|
+
const DOMAIN_CONTROLS = 'domain_controls';
|
|
358
|
+
const DOMAIN_WIDTH_DEC = 'domain_width_dec';
|
|
359
|
+
const DOMAIN_WIDTH_INC = 'domain_width_inc';
|
|
360
|
+
const DOMAIN_EVALUE_DEC = 'domain_evalue_dec';
|
|
361
|
+
const DOMAIN_EVALUE_INC = 'domain_evalue_inc';
|
|
362
|
+
const DOMAIN_EVALUE_READOUT = 'domain_evalue';
|
|
363
|
+
const DOMAIN_LABELS_SELECT = 'domain_labels';
|
|
364
|
+
const DOMAIN_GLOW_CB = 'domain_glow_cb';
|
|
350
365
|
const TIME_AXIS_CB = 'timeaxis_cb';
|
|
351
366
|
const TIME_GRID_CB = 'timegrid_cb';
|
|
352
367
|
const MSA_SCROLL_ID = 'aptxmsascroll';
|
|
@@ -507,6 +522,28 @@ function (root, d3, forester, phyloXml) {
|
|
|
507
522
|
let _msaScroller = null; // the fixed HTML range input, created lazily
|
|
508
523
|
let _msaNav = null; // the bar around it: paging, jump-to-column, readout
|
|
509
524
|
let _msaGeom = null; // window geometry of the last draw, for the hover readout
|
|
525
|
+
// ------ protein domain architectures (the desktop's domain display) ------
|
|
526
|
+
const DOMAIN_TRACK_START_GAP = 20; // px from the aligned label column to the backbone's start
|
|
527
|
+
const DOMAIN_TRACK_END_GAP = 10; // px kept free past the widest track
|
|
528
|
+
const DOMAIN_RADIAL_GAP = 4; // px from the label ring / the tip's label to the backbone, radial layouts
|
|
529
|
+
const DOMAIN_WIDTH_MIN = 20; // the track width stops growing / shrinking at these
|
|
530
|
+
const DOMAIN_WIDTH_MAX = 2000;
|
|
531
|
+
const DOMAIN_WIDTH_GROW = 1.2; // one d+ press
|
|
532
|
+
const DOMAIN_WIDTH_SHRINK = 0.8; // one d- press
|
|
533
|
+
const DOMAIN_WIDTH_VIEWPORT_FRACTION = 0.25; // the initial track width
|
|
534
|
+
const DOMAIN_RADIAL_WIDTH_FRACTION = 0.2; // ... capped at this share of the radius in radial layouts
|
|
535
|
+
const DOMAIN_SCALE_HEADROOM = 0.9; // drawn length over reserved length
|
|
536
|
+
const DOMAIN_BOX_MIN_H = 6;
|
|
537
|
+
const DOMAIN_BOX_MAX_H = 16;
|
|
538
|
+
const DOMAIN_BACKBONE_COLOR = 'rgb(100,100,100)'; // the same in both themes and in exports
|
|
539
|
+
const DOMAIN_SHADOW_COLOR = 'rgb(8,18,21)';
|
|
540
|
+
const DOMAIN_SHADOWS = [[0.4, 0.7, 40], [0.9, 1.5, 28], [1.6, 2.5, 17]]; // dx, dy, alpha of 255, in drawing order
|
|
541
|
+
const DOMAIN_GLOWS = [[3.2, 20], [1.6, 34]]; // grown by, alpha of 255
|
|
542
|
+
const DOMAIN_LEGEND_INSET = 10; // px from the bottom-right corner, the legend's home
|
|
543
|
+
const DOMAIN_LEGEND_ROW_MAX_PX = 240; // a legend row is clipped to this
|
|
544
|
+
const DOMAIN_LABEL_MODES = ['none', 'domains', 'legend'];
|
|
545
|
+
let _domain = null; // {width, palette, next, legendFrac}, set per launch
|
|
546
|
+
let _domainReserve = 0; // horizontal px reserved for the tracks, set with _w
|
|
510
547
|
// ------ time axis (the desktop's geologic / calendar overlays) ------
|
|
511
548
|
const TIME_GEO_RESERVE = 52; // two ICS band rows + the Ma ruler
|
|
512
549
|
const TIME_CAL_RESERVE = 26; // the calendar year ruler
|
|
@@ -613,6 +650,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
613
650
|
const OVERVIEW_HEIGHT = 111;
|
|
614
651
|
const OVERVIEW_MARGIN = 12;
|
|
615
652
|
const OVERVIEW_PAD = 5;
|
|
653
|
+
// Smallest the "you are here" rectangle is allowed to get, in overview
|
|
654
|
+
// pixels. Zoom vertically into a 13,000-tip tree and the visible slice is
|
|
655
|
+
// a fraction of one percent of it: the rectangle is then accurate,
|
|
656
|
+
// sub-pixel and useless. Below this it stops being a measurement and
|
|
657
|
+
// becomes a marker -- it still says WHERE you are, it just stops claiming
|
|
658
|
+
// to say exactly how much you can see. The inner frame is 132 x 101, so
|
|
659
|
+
// this is small enough never to mislead about position.
|
|
660
|
+
const OVERVIEW_VIEWPORT_MIN = 7;
|
|
616
661
|
|
|
617
662
|
let _overviewGroup = null; // the whole overview, appended above the tree
|
|
618
663
|
let _overviewContent = null; // the scaled miniature inside it
|
|
@@ -913,17 +958,90 @@ function (root, d3, forester, phyloXml) {
|
|
|
913
958
|
// Rebuild the miniature. Called when the tree itself changes: the branch
|
|
914
959
|
// paths are copied straight from what was just rendered, so the overview
|
|
915
960
|
// matches the real display in either layout without redoing any geometry.
|
|
961
|
+
// The links of the CURRENT layout. The overview is built from these --
|
|
962
|
+
// from the MODEL -- never by reading back what was drawn.
|
|
963
|
+
//
|
|
964
|
+
// Reading the drawn tree is what broke it: update() animates the links
|
|
965
|
+
// with a transition, and the overview was rebuilt from inside update(),
|
|
966
|
+
// so every attribute it read was the frame BEFORE the one being drawn.
|
|
967
|
+
// Logging from inside the function showed all 215 links degenerate on
|
|
968
|
+
// EVERY rebuild, in every display mode -- the tree had not been drawn
|
|
969
|
+
// yet. The desktop has never had this bug because its miniature is
|
|
970
|
+
// likewise computed from the model.
|
|
971
|
+
let _overviewLinks = null;
|
|
972
|
+
|
|
973
|
+
// The tree's extent for the overview, in un-zoomed tree coordinates:
|
|
974
|
+
// branches as the layout placed them, plus the space the layout reserved
|
|
975
|
+
// for external labels.
|
|
976
|
+
//
|
|
977
|
+
// ONE extent, used for BOTH the miniature's placement and the "is the
|
|
978
|
+
// overview needed at all" test. That is the property that matters, and
|
|
979
|
+
// getting it wrong is what made the first attempt at this worse than the
|
|
980
|
+
// bug: the miniature was fitted to the branches while the visibility test
|
|
981
|
+
// measured branches PLUS labels, so the "you are here" rectangle was
|
|
982
|
+
// drawn against a different extent than the picture under it. The two may
|
|
983
|
+
// live in different spaces -- the desktop deliberately keeps the
|
|
984
|
+
// rectangle in frame space -- but they must derive from one extent.
|
|
985
|
+
//
|
|
986
|
+
// Labels count, by Christian's decision and as the desktop does it: the
|
|
987
|
+
// question is whether what the user can SEE fits, and a cut-off label is
|
|
988
|
+
// not visible.
|
|
989
|
+
function overviewExtent() {
|
|
990
|
+
if (_state.circularDisplay || _state.unrootedDisplay) {
|
|
991
|
+
// maxRad already reaches the outer label ring -- fitCircular()
|
|
992
|
+
// fits the display to exactly this.
|
|
993
|
+
let r = _state.circularDisplay
|
|
994
|
+
? (_radial ? _radial.maxRad : null)
|
|
995
|
+
: (_unroot ? _unroot.maxRad : null);
|
|
996
|
+
if (!r || r <= 0) {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
return {x: -r, y: -r, width: 2 * r, height: 2 * r};
|
|
1000
|
+
}
|
|
1001
|
+
if (!_overviewLinks || _overviewLinks.length === 0) {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
// elbow() writes 'M source.y,source.x V target.x H target.y', so the
|
|
1005
|
+
// horizontal axis is .y and the vertical axis is .x.
|
|
1006
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
|
|
1007
|
+
for (let i = 0, len = _overviewLinks.length; i !== len; ++i) {
|
|
1008
|
+
let l = _overviewLinks[i];
|
|
1009
|
+
let sx = l.source.y, tx = l.target.y;
|
|
1010
|
+
let sy = l.source.x, ty = l.target.x;
|
|
1011
|
+
if (sx < minX) { minX = sx; }
|
|
1012
|
+
if (tx < minX) { minX = tx; }
|
|
1013
|
+
if (sx > maxX) { maxX = sx; }
|
|
1014
|
+
if (tx > maxX) { maxX = tx; }
|
|
1015
|
+
if (sy < minY) { minY = sy; }
|
|
1016
|
+
if (ty < minY) { minY = ty; }
|
|
1017
|
+
if (sy > maxY) { maxY = sy; }
|
|
1018
|
+
if (ty > maxY) { maxY = ty; }
|
|
1019
|
+
}
|
|
1020
|
+
if (!isFinite(minX) || !isFinite(minY) || maxX <= minX) {
|
|
1021
|
+
return null;
|
|
1022
|
+
}
|
|
1023
|
+
// The label reservation the layout was sized against, MINUS the root
|
|
1024
|
+
// offset. calcMaxTreeLengthForDisplay() bundles both: the space kept
|
|
1025
|
+
// to the LEFT of the root and the space kept to the RIGHT for
|
|
1026
|
+
// external labels. Only the label half belongs on this edge.
|
|
1027
|
+
//
|
|
1028
|
+
// Including the root offset made the box exactly rootOffset too wide,
|
|
1029
|
+
// and since centerNode() shifts the view left by that same amount to
|
|
1030
|
+
// seat the root, the right edge of the box always fell just outside
|
|
1031
|
+
// the viewport: measured box=[0,13 1497x755] against
|
|
1032
|
+
// view=[-254,-10 1497x800] with rootOffset=254, so "does it fit" said
|
|
1033
|
+
// no by precisely 254px and the overview appeared after every Fit.
|
|
1034
|
+
maxX += calcMaxTreeLengthForDisplay() - _settings.rootOffset + _domainReserve;
|
|
1035
|
+
let h = maxY - minY;
|
|
1036
|
+
return {x: minX, y: minY, width: maxX - minX, height: h > 0 ? h : 1};
|
|
1037
|
+
}
|
|
1038
|
+
|
|
916
1039
|
function rebuildOverview() {
|
|
917
1040
|
if (!_overviewGroup) {
|
|
918
1041
|
return;
|
|
919
1042
|
}
|
|
920
1043
|
let size = svgSize();
|
|
921
|
-
let box =
|
|
922
|
-
try {
|
|
923
|
-
box = _svgGroup.node().getBBox(); // in un-zoomed tree coordinates
|
|
924
|
-
} catch {
|
|
925
|
-
box = null;
|
|
926
|
-
}
|
|
1044
|
+
let box = overviewExtent(); // from the MODEL: see overviewExtent()
|
|
927
1045
|
if (!size || !box || box.width <= 0 || box.height <= 0) {
|
|
928
1046
|
_overviewGroup.style('display', 'none');
|
|
929
1047
|
_overviewMap = null;
|
|
@@ -942,15 +1060,32 @@ function (root, d3, forester, phyloXml) {
|
|
|
942
1060
|
_overviewContent.attr('transform', 'translate(' + _overviewMap.tx + ',' + _overviewMap.ty + ') scale(' + scale + ')');
|
|
943
1061
|
|
|
944
1062
|
let paths = [];
|
|
945
|
-
|
|
946
|
-
let
|
|
947
|
-
|
|
948
|
-
|
|
1063
|
+
if (_overviewLinks) {
|
|
1064
|
+
for (let i = 0, len = _overviewLinks.length; i !== len; ++i) {
|
|
1065
|
+
let d = elbow(_overviewLinks[i]); // the SAME routine the tree is drawn with
|
|
1066
|
+
if (d) {
|
|
1067
|
+
paths.push(d);
|
|
1068
|
+
}
|
|
949
1069
|
}
|
|
950
|
-
}
|
|
951
|
-
|
|
1070
|
+
}
|
|
1071
|
+
// ONE path for the whole miniature, not one per link. Every elbow()
|
|
1072
|
+
// string starts with an absolute moveto, so concatenating them yields
|
|
1073
|
+
// a single path of many subpaths that renders identically -- they
|
|
1074
|
+
// already share one stroke, width and vector-effect, so nothing
|
|
1075
|
+
// per-link was being expressed by separate elements.
|
|
1076
|
+
//
|
|
1077
|
+
// It is worth doing because the alternative keeps a SECOND copy of
|
|
1078
|
+
// every link in the document: 18,512 extra elements on the big demo
|
|
1079
|
+
// tree, rewritten on every redraw. Measured on that tree, same data:
|
|
1080
|
+
// writing 18,512 paths 222 ms, writing the one concatenated path
|
|
1081
|
+
// 9 ms. Verified equivalent before adopting -- node ordering, the
|
|
1082
|
+
// index of the first node group, and the datum order of the inserted
|
|
1083
|
+
// paths all match, and the union bounding box is unchanged.
|
|
1084
|
+
let sel = _overviewContent.selectAll('path.aptx-overview-links')
|
|
1085
|
+
.data(paths.length > 0 ? [paths.join(' ')] : []);
|
|
952
1086
|
sel.exit().remove();
|
|
953
1087
|
sel.enter().append('path')
|
|
1088
|
+
.attr('class', 'aptx-overview-links')
|
|
954
1089
|
.merge(sel)
|
|
955
1090
|
.attr('d', function (d) {
|
|
956
1091
|
return d;
|
|
@@ -1023,10 +1158,32 @@ function (root, d3, forester, phyloXml) {
|
|
|
1023
1158
|
let y0 = Math.max(OVERVIEW_PAD, _overviewMap.ty + (visY * _overviewMap.scale));
|
|
1024
1159
|
let x1 = Math.min(OVERVIEW_WIDTH - OVERVIEW_PAD, _overviewMap.tx + ((visX + visW) * _overviewMap.scale));
|
|
1025
1160
|
let y1 = Math.min(OVERVIEW_HEIGHT - OVERVIEW_PAD, _overviewMap.ty + ((visY + visH) * _overviewMap.scale));
|
|
1161
|
+
// Keep the rectangle visible when the visible slice is tiny. Grown
|
|
1162
|
+
// about its own centre, so the marker stays over the part of the tree
|
|
1163
|
+
// you are actually looking at, then pushed back inside the frame if
|
|
1164
|
+
// that growth would take it out.
|
|
1165
|
+
let floorSpan = function (a, b, lo, hi) {
|
|
1166
|
+
if ((b - a) >= OVERVIEW_VIEWPORT_MIN || (hi - lo) <= OVERVIEW_VIEWPORT_MIN) {
|
|
1167
|
+
return [a, b];
|
|
1168
|
+
}
|
|
1169
|
+
let mid = (a + b) / 2;
|
|
1170
|
+
let start = mid - (OVERVIEW_VIEWPORT_MIN / 2);
|
|
1171
|
+
let end = start + OVERVIEW_VIEWPORT_MIN;
|
|
1172
|
+
if (start < lo) {
|
|
1173
|
+
start = lo;
|
|
1174
|
+
end = lo + OVERVIEW_VIEWPORT_MIN;
|
|
1175
|
+
} else if (end > hi) {
|
|
1176
|
+
end = hi;
|
|
1177
|
+
start = hi - OVERVIEW_VIEWPORT_MIN;
|
|
1178
|
+
}
|
|
1179
|
+
return [start, end];
|
|
1180
|
+
};
|
|
1181
|
+
let xs = floorSpan(x0, x1, OVERVIEW_PAD, OVERVIEW_WIDTH - OVERVIEW_PAD);
|
|
1182
|
+
let ys = floorSpan(y0, y1, OVERVIEW_PAD, OVERVIEW_HEIGHT - OVERVIEW_PAD);
|
|
1026
1183
|
_overviewViewport
|
|
1027
|
-
.attr('x',
|
|
1028
|
-
.attr('width', Math.max(0,
|
|
1029
|
-
.attr('height', Math.max(0,
|
|
1184
|
+
.attr('x', xs[0]).attr('y', ys[0])
|
|
1185
|
+
.attr('width', Math.max(0, xs[1] - xs[0]))
|
|
1186
|
+
.attr('height', Math.max(0, ys[1] - ys[0]));
|
|
1030
1187
|
}
|
|
1031
1188
|
|
|
1032
1189
|
function zoom(event) {
|
|
@@ -1311,6 +1468,79 @@ function (root, d3, forester, phyloXml) {
|
|
|
1311
1468
|
document.addEventListener('keydown', onKey);
|
|
1312
1469
|
}
|
|
1313
1470
|
|
|
1471
|
+
// One set of listeners on the tree group instead of five on every node.
|
|
1472
|
+
//
|
|
1473
|
+
// These used to be attached in the node enter: click and contextmenu on
|
|
1474
|
+
// each g.node, and mouseover/mousemove/mouseout on each
|
|
1475
|
+
// circle.nodeCircleOptions. On an 18,512-node tree that is 92,560
|
|
1476
|
+
// registrations on the first draw, and it measured 300-450 ms of one.
|
|
1477
|
+
// Mouse events bubble, so one listener per type on the group does the same
|
|
1478
|
+
// work -- find which node the event came from, and hand the handler its
|
|
1479
|
+
// datum. The handlers never used `this`, which is what makes this a
|
|
1480
|
+
// straight substitution rather than a rewrite.
|
|
1481
|
+
//
|
|
1482
|
+
// Note it is mouseover/mouseout, NOT mouseenter/mouseleave: only the
|
|
1483
|
+
// former pair bubbles, so only the former pair can be delegated.
|
|
1484
|
+
function attachNodeEventDelegation(group) {
|
|
1485
|
+
|
|
1486
|
+
// d3 stores a node's datum on the element as __data__, which is all
|
|
1487
|
+
// selection.datum() reads. Going straight to it avoids allocating a
|
|
1488
|
+
// selection on every mousemove.
|
|
1489
|
+
function datumOf(el) {
|
|
1490
|
+
return el ? el.__data__ : null;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// The hover target stays the invisible 5px circle rather than the whole
|
|
1494
|
+
// node, or the tooltip would follow the labels too.
|
|
1495
|
+
function hoverDatum(event) {
|
|
1496
|
+
let t = event.target;
|
|
1497
|
+
if (!t || !t.classList || !t.classList.contains('nodeCircleOptions')) {
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
return datumOf(t.parentNode);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Click and the context menu take the whole node group, so the node's
|
|
1504
|
+
// LABEL works as a target too -- a much easier thing to aim at than a
|
|
1505
|
+
// 5px circle.
|
|
1506
|
+
function nodeDatum(event) {
|
|
1507
|
+
let t = event.target;
|
|
1508
|
+
return datumOf(t && t.closest ? t.closest('g.node') : null);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
group
|
|
1512
|
+
.on('click', function (event) {
|
|
1513
|
+
let d = nodeDatum(event);
|
|
1514
|
+
if (d && _treeFn.clickEvent) {
|
|
1515
|
+
_treeFn.clickEvent(event, d);
|
|
1516
|
+
}
|
|
1517
|
+
})
|
|
1518
|
+
.on('contextmenu', function (event) {
|
|
1519
|
+
let d = nodeDatum(event);
|
|
1520
|
+
if (d && _treeFn.clickEvent) {
|
|
1521
|
+
event.preventDefault(); // ours instead of the browser's menu
|
|
1522
|
+
_treeFn.clickEvent(event, d);
|
|
1523
|
+
}
|
|
1524
|
+
})
|
|
1525
|
+
.on('mouseover', function (event) {
|
|
1526
|
+
let d = hoverDatum(event);
|
|
1527
|
+
if (d) {
|
|
1528
|
+
mouseover(event, d);
|
|
1529
|
+
}
|
|
1530
|
+
})
|
|
1531
|
+
.on('mousemove', function (event) {
|
|
1532
|
+
let d = hoverDatum(event);
|
|
1533
|
+
if (d) {
|
|
1534
|
+
mousemove(event, d);
|
|
1535
|
+
}
|
|
1536
|
+
})
|
|
1537
|
+
.on('mouseout', function (event) {
|
|
1538
|
+
if (hoverDatum(event)) {
|
|
1539
|
+
mouseout();
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1314
1544
|
function mouseout() {
|
|
1315
1545
|
hideHoverGlow();
|
|
1316
1546
|
// Fade only. Emptying the tooltip here collapsed it to nothing but its
|
|
@@ -1330,9 +1560,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
1330
1560
|
// the next launch).
|
|
1331
1561
|
//
|
|
1332
1562
|
// The candidates come from forester.visualizationCandidates: the tree is
|
|
1333
|
-
// the only input
|
|
1334
|
-
//
|
|
1335
|
-
//
|
|
1563
|
+
// the only input, and candidacy is decided on the TREE -- at launch, and
|
|
1564
|
+
// again only after a permanent edit (a deletion). A VIEW -- the subtree
|
|
1565
|
+
// the user switched into -- never changes what is offered or chosen; it
|
|
1566
|
+
// only re-summarizes each candidate over the tips on screen (legend rows
|
|
1567
|
+
// and counts, the "no value" row, a numeric field's band and gradient
|
|
1568
|
+
// range) through forester.visualizationSummary. Colour and shape
|
|
1569
|
+
// identities live in the memory maps below and so survive every view.
|
|
1570
|
+
// Until 2026-09-12 the classifier was re-run per view and its refusals
|
|
1571
|
+
// applied to the chosen field, which dropped the colouring in 61% of the
|
|
1572
|
+
// corpus's clades and did not bring it back on return.
|
|
1336
1573
|
// The tree as displayed right now: the subtree the user switched into,
|
|
1337
1574
|
// otherwise the (possibly edited) full tree.
|
|
1338
1575
|
function displayedRoot() {
|
|
@@ -1379,7 +1616,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
1379
1616
|
if (!_settings.enableVisualizations) {
|
|
1380
1617
|
return;
|
|
1381
1618
|
}
|
|
1382
|
-
|
|
1619
|
+
installCandidates(forester.visualizationCandidates(_treeData));
|
|
1620
|
+
summarizeView(_treeData);
|
|
1383
1621
|
// The embedder may name the visualization to open with (the
|
|
1384
1622
|
// initialVisualization setting, matched case-insensitively against
|
|
1385
1623
|
// the Color-menu label or the internal id). A name this tree cannot
|
|
@@ -1406,14 +1644,20 @@ function (root, d3, forester, phyloXml) {
|
|
|
1406
1644
|
_vis.autoColorId = requested.id;
|
|
1407
1645
|
_vis.colorId = requested.id;
|
|
1408
1646
|
_state.showVisualizations = true;
|
|
1409
|
-
} else
|
|
1410
|
-
// Auto-apply the best candidate: the classifier
|
|
1411
|
-
// first, so a tree opens already coloured by
|
|
1412
|
-
// field instead of grey with a menu to
|
|
1413
|
-
// (21+ values, legend capped) is offered
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1647
|
+
} else {
|
|
1648
|
+
// Auto-apply the best candidate that is not WIDE: the classifier
|
|
1649
|
+
// returns them best first, so a tree opens already coloured by
|
|
1650
|
+
// its most informative field instead of grey with a menu to
|
|
1651
|
+
// discover. A wide field (21+ values, legend capped) is offered
|
|
1652
|
+
// but never imposed -- and it does not block what sits below it
|
|
1653
|
+
// either: a tree carrying a wide field and an In-Group opens by
|
|
1654
|
+
// the In-Group, not uncoloured.
|
|
1655
|
+
let first = forester.openingVisualization(_vis.candidates);
|
|
1656
|
+
if (first) {
|
|
1657
|
+
_vis.autoColorId = first.id;
|
|
1658
|
+
_vis.colorId = _vis.autoColorId;
|
|
1659
|
+
_state.showVisualizations = true;
|
|
1660
|
+
}
|
|
1417
1661
|
}
|
|
1418
1662
|
}
|
|
1419
1663
|
|
|
@@ -1743,49 +1987,72 @@ function (root, d3, forester, phyloXml) {
|
|
|
1743
1987
|
buildCategoryScale(vis);
|
|
1744
1988
|
}
|
|
1745
1989
|
|
|
1746
|
-
|
|
1747
|
-
|
|
1990
|
+
// The tree's candidates become THE candidates: the menu, the tiers, the
|
|
1991
|
+
// ids the memory maps are keyed by. Their scales come from summarizeView.
|
|
1992
|
+
function installCandidates(list) {
|
|
1993
|
+
_vis.candidates = list;
|
|
1748
1994
|
_vis.byId = {};
|
|
1749
|
-
|
|
1750
|
-
if (c.numeric) {
|
|
1751
|
-
// ramps are POSITION in the view's range, not identity: a
|
|
1752
|
-
// subtree spanning six years gets a full-width gradient
|
|
1753
|
-
let nums = c.values.map(Number);
|
|
1754
|
-
let mean = nums.reduce(function (a, b) {
|
|
1755
|
-
return a + b;
|
|
1756
|
-
}, 0) / nums.length;
|
|
1757
|
-
c.rangeScale = d3.scaleLinear()
|
|
1758
|
-
.range(VIS_COLOR_RAMP)
|
|
1759
|
-
.domain([nums[0], mean, nums[nums.length - 1]]);
|
|
1760
|
-
}
|
|
1761
|
-
function memoryKey(v) {
|
|
1762
|
-
return visMemoryKey(c, v);
|
|
1763
|
-
}
|
|
1764
|
-
// a switchable candidate needs both scales standing by
|
|
1765
|
-
if (c.colorMode === 'category' || c.switchable) {
|
|
1766
|
-
buildCategoryScale(c);
|
|
1767
|
-
}
|
|
1768
|
-
if (c.shape) {
|
|
1769
|
-
let mem = _vis.shapeMemory[c.id] || (_vis.shapeMemory[c.id] = {});
|
|
1770
|
-
let next = _vis.shapeNext[c.id] || 0;
|
|
1771
|
-
c.values.forEach(function (v) {
|
|
1772
|
-
let k = memoryKey(v);
|
|
1773
|
-
if (!(k in mem)) {
|
|
1774
|
-
mem[k] = VIS_SHAPES[next++ % VIS_SHAPES.length];
|
|
1775
|
-
}
|
|
1776
|
-
});
|
|
1777
|
-
_vis.shapeNext[c.id] = next;
|
|
1778
|
-
c.shapeScale = d3.scaleOrdinal()
|
|
1779
|
-
.domain(c.values)
|
|
1780
|
-
.range(c.values.map(function (v) {
|
|
1781
|
-
return mem[memoryKey(v)];
|
|
1782
|
-
}));
|
|
1783
|
-
}
|
|
1784
|
-
_vis.candidates.push(c);
|
|
1995
|
+
list.forEach(function (c) {
|
|
1785
1996
|
_vis.byId[c.id] = c;
|
|
1786
1997
|
});
|
|
1787
1998
|
}
|
|
1788
1999
|
|
|
2000
|
+
// Re-summarizes every candidate over the tips under `root` and rebuilds
|
|
2001
|
+
// its scales from that: the whole tree at launch and after an edit, the
|
|
2002
|
+
// displayed subtree on every view change.
|
|
2003
|
+
function summarizeView(root) {
|
|
2004
|
+
_vis.candidates.forEach(function (c) {
|
|
2005
|
+
let s = forester.visualizationSummary(c, root);
|
|
2006
|
+
c.values = s.values;
|
|
2007
|
+
c.counts = s.counts;
|
|
2008
|
+
c.coverage = s.coverage;
|
|
2009
|
+
c.total = s.total;
|
|
2010
|
+
if (c.numeric) {
|
|
2011
|
+
c.colorMode = s.colorMode;
|
|
2012
|
+
c.switchable = s.switchable;
|
|
2013
|
+
}
|
|
2014
|
+
buildScales(c);
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function buildScales(c) {
|
|
2019
|
+
c.rangeScale = null;
|
|
2020
|
+
if (c.numeric && c.values.length > 0) {
|
|
2021
|
+
// ramps are POSITION in the view's range, not identity: a
|
|
2022
|
+
// subtree spanning six years gets a full-width gradient
|
|
2023
|
+
let nums = c.values.map(Number);
|
|
2024
|
+
let mean = nums.reduce(function (a, b) {
|
|
2025
|
+
return a + b;
|
|
2026
|
+
}, 0) / nums.length;
|
|
2027
|
+
c.rangeScale = d3.scaleLinear()
|
|
2028
|
+
.range(VIS_COLOR_RAMP)
|
|
2029
|
+
.domain([nums[0], mean, nums[nums.length - 1]]);
|
|
2030
|
+
}
|
|
2031
|
+
function memoryKey(v) {
|
|
2032
|
+
return visMemoryKey(c, v);
|
|
2033
|
+
}
|
|
2034
|
+
// a switchable candidate needs both scales standing by
|
|
2035
|
+
if (c.colorMode === 'category' || c.switchable) {
|
|
2036
|
+
buildCategoryScale(c);
|
|
2037
|
+
}
|
|
2038
|
+
if (c.shape) {
|
|
2039
|
+
let mem = _vis.shapeMemory[c.id] || (_vis.shapeMemory[c.id] = {});
|
|
2040
|
+
let next = _vis.shapeNext[c.id] || 0;
|
|
2041
|
+
c.values.forEach(function (v) {
|
|
2042
|
+
let k = memoryKey(v);
|
|
2043
|
+
if (!(k in mem)) {
|
|
2044
|
+
mem[k] = VIS_SHAPES[next++ % VIS_SHAPES.length];
|
|
2045
|
+
}
|
|
2046
|
+
});
|
|
2047
|
+
_vis.shapeNext[c.id] = next;
|
|
2048
|
+
c.shapeScale = d3.scaleOrdinal()
|
|
2049
|
+
.domain(c.values)
|
|
2050
|
+
.range(c.values.map(function (v) {
|
|
2051
|
+
return mem[memoryKey(v)];
|
|
2052
|
+
}));
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
1789
2056
|
function currentColorVis() {
|
|
1790
2057
|
return ((_vis && _vis.colorId) ? _vis.byId[_vis.colorId] : null) || null;
|
|
1791
2058
|
}
|
|
@@ -1797,7 +2064,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
1797
2064
|
// A candidate's colour mode, honouring the legend's [colors] / [gradient]
|
|
1798
2065
|
// switch, and the scale that mode wants.
|
|
1799
2066
|
function colorModeOf(vis) {
|
|
1800
|
-
|
|
2067
|
+
// the legend's [colors]/[gradient] choice holds only while the field
|
|
2068
|
+
// is switchable in THIS view (as on the desktop): above 20 distinct
|
|
2069
|
+
// values it is a gradient, whatever was chosen inside a smaller clade
|
|
2070
|
+
return (vis.switchable && _vis.colorModeById[vis.id]) || vis.colorMode;
|
|
1801
2071
|
}
|
|
1802
2072
|
|
|
1803
2073
|
// Legend row order: numbers list in numeric order by default, words by
|
|
@@ -1847,9 +2117,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
1847
2117
|
// a titled, bordered panel; value rows with counts; a [by count] / [A-Z]
|
|
1848
2118
|
// sort toggle in the title row (count-first is the desktop's default);
|
|
1849
2119
|
// and for numeric ranges a horizontal gradient bar with the min and max
|
|
1850
|
-
// beneath it.
|
|
1851
|
-
//
|
|
1852
|
-
//
|
|
2120
|
+
// beneath it. A long legend shows its 20 most frequent rows and a
|
|
2121
|
+
// [+N more] chip, like the desktop's. Rows, counts and the "no value"
|
|
2122
|
+
// row describe the tips on SCREEN (the view's summary); the colours are
|
|
2123
|
+
// launch-lifetime identities from the memory maps.
|
|
1853
2124
|
//
|
|
1854
2125
|
// The cards live in the tree's own svg, so they ride along into the PNG
|
|
1855
2126
|
// and SVG exports, and their colours are the four theme colours the
|
|
@@ -1915,7 +2186,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
1915
2186
|
const titleFont = '600 ' + (FS + 1) + 'px ' + FONT_DEFAULTS;
|
|
1916
2187
|
const ink = _state.labelColorDefault;
|
|
1917
2188
|
const frame = _state.branchColorDefault;
|
|
1918
|
-
|
|
2189
|
+
// a field with no value in this view has no range to draw: the card
|
|
2190
|
+
// is then just its "no value" row
|
|
2191
|
+
const isRange = kind === 'color' && colorModeOf(vis) === 'range' && vis.values.length > 0;
|
|
1919
2192
|
|
|
1920
2193
|
const LEGEND_MAX_ROWS = 20;
|
|
1921
2194
|
let rows = [];
|
|
@@ -2283,6 +2556,28 @@ function (root, d3, forester, phyloXml) {
|
|
|
2283
2556
|
});
|
|
2284
2557
|
}
|
|
2285
2558
|
|
|
2559
|
+
// Animate when there is something to animate; otherwise set the values
|
|
2560
|
+
// straight onto the selection.
|
|
2561
|
+
//
|
|
2562
|
+
// d3 builds a schedule for every element a transition touches, whether or
|
|
2563
|
+
// not that transition will animate anything -- and a duration of 0 animates
|
|
2564
|
+
// nothing. Nearly every redraw here asks for 0: all 39 call sites that pass
|
|
2565
|
+
// a duration pass 0, and only the 15 that omit the argument animate. So the
|
|
2566
|
+
// common path was paying for machinery it never used. On the 18,512-node
|
|
2567
|
+
// tree, ONE `transition.select('path')` measured ~190-235 ms against ~15 ms
|
|
2568
|
+
// for the same select on a plain selection -- and the node update does six
|
|
2569
|
+
// such selects, plus the node and link roots.
|
|
2570
|
+
//
|
|
2571
|
+
// interrupt() first: a transition still running from an earlier redraw would
|
|
2572
|
+
// otherwise finish afterwards and overwrite what we just set. The transition
|
|
2573
|
+
// this replaces cancelled that animation implicitly, by taking over the same
|
|
2574
|
+
// schedule; a plain selection has to say so.
|
|
2575
|
+
function animateOrSet(selection, duration) {
|
|
2576
|
+
return duration > 0
|
|
2577
|
+
? selection.transition().duration(duration)
|
|
2578
|
+
: selection.interrupt();
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2286
2581
|
function update(source, transitionDuration, doNotRecalculateWidth) {
|
|
2287
2582
|
|
|
2288
2583
|
if (!source) {
|
|
@@ -2300,6 +2595,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
2300
2595
|
// the alignment track reserves its window on the right, so the
|
|
2301
2596
|
// tree and labels compress to make room rather than overlapping
|
|
2302
2597
|
_msaReserve = 0;
|
|
2598
|
+
// and the domain tracks reserve their column past the labels
|
|
2599
|
+
_domainReserve = domainReserve();
|
|
2303
2600
|
if (msaShown()) {
|
|
2304
2601
|
let fullPx = _basicTreeProperties.maxMolSeqLength * MSA_COL_WIDTH;
|
|
2305
2602
|
// budgeted from the VIEWPORT: sizing it from the zoomed layout
|
|
@@ -2310,11 +2607,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
2310
2607
|
let band = Math.max(MSA_MIN_BAND_PX, Math.round(vw * MSA_MAX_VIEWPORT_FRACTION));
|
|
2311
2608
|
// a wide alignment must not squeeze the tree itself away: the
|
|
2312
2609
|
// band yields until the tree keeps its minimum share
|
|
2313
|
-
let maxBand = _displayWidth - calcMaxTreeLengthForDisplay() - MSA_TRACK_GAP - MSA_MIN_TREE_PX;
|
|
2610
|
+
let maxBand = _displayWidth - calcMaxTreeLengthForDisplay() - _domainReserve - MSA_TRACK_GAP - MSA_MIN_TREE_PX;
|
|
2314
2611
|
band = Math.max(MSA_MIN_BAND_PX, Math.min(band, maxBand));
|
|
2315
2612
|
_msaReserve = MSA_TRACK_GAP + Math.min(fullPx, band);
|
|
2316
2613
|
}
|
|
2317
|
-
_w = _displayWidth - calcMaxTreeLengthForDisplay() - _msaReserve;
|
|
2614
|
+
_w = _displayWidth - calcMaxTreeLengthForDisplay() - _msaReserve - _domainReserve;
|
|
2318
2615
|
if (_w < 1) {
|
|
2319
2616
|
_w = 1;
|
|
2320
2617
|
}
|
|
@@ -2365,6 +2662,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
2365
2662
|
let links = hierarchy.links().map(function (link) {
|
|
2366
2663
|
return {source: link.source.data, target: link.target.data};
|
|
2367
2664
|
});
|
|
2665
|
+
_overviewLinks = links; // the overview is built from these, not from the DOM
|
|
2368
2666
|
let gap = _state.nodeLabelGap;
|
|
2369
2667
|
|
|
2370
2668
|
if (_state.phylogram === true) {
|
|
@@ -2446,21 +2744,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
2446
2744
|
return d.id || (d.id = ++_i);
|
|
2447
2745
|
});
|
|
2448
2746
|
|
|
2747
|
+
// No listeners here: click, contextmenu and the hover trio are
|
|
2748
|
+
// delegated once to the tree group -- see attachNodeEventDelegation().
|
|
2449
2749
|
let nodeEnter = node.enter().append('g')
|
|
2450
2750
|
.attr('class', 'node')
|
|
2451
2751
|
.attr('transform', function () {
|
|
2452
2752
|
return 'translate(' + source.y0 + ',' + source.x0 + ')';
|
|
2453
2753
|
})
|
|
2454
|
-
.style('cursor', 'default')
|
|
2455
|
-
.on('click', _treeFn.clickEvent)
|
|
2456
|
-
// Right-click opens the same menu. The left-click target is an
|
|
2457
|
-
// invisible 5px circle on the node itself, which is hard to hit; this
|
|
2458
|
-
// handler sits on the whole node group, so the node's LABEL works as
|
|
2459
|
-
// the target too -- a much easier thing to aim at.
|
|
2460
|
-
.on('contextmenu', function (event, d) {
|
|
2461
|
-
event.preventDefault(); // ours instead of the browser's menu
|
|
2462
|
-
_treeFn.clickEvent.call(this, event, d);
|
|
2463
|
-
});
|
|
2754
|
+
.style('cursor', 'default');
|
|
2464
2755
|
|
|
2465
2756
|
|
|
2466
2757
|
// Only the three elements EVERY node needs are created here. The other
|
|
@@ -2483,11 +2774,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
2483
2774
|
.attr('r', 0);
|
|
2484
2775
|
|
|
2485
2776
|
nodeEnter.append('circle')
|
|
2486
|
-
.on("mouseover", mouseover)
|
|
2487
|
-
.on("mousemove", function (event, d) {
|
|
2488
|
-
mousemove(event, d);
|
|
2489
|
-
})
|
|
2490
|
-
.on("mouseout", mouseout)
|
|
2491
2777
|
.style('cursor', 'pointer')
|
|
2492
2778
|
.style('opacity', '0')
|
|
2493
2779
|
.attr('class', 'nodeCircleOptions')
|
|
@@ -2708,20 +2994,29 @@ function (root, d3, forester, phyloXml) {
|
|
|
2708
2994
|
});
|
|
2709
2995
|
}
|
|
2710
2996
|
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2997
|
+
// One pass, and the fill colour computed once.
|
|
2998
|
+
//
|
|
2999
|
+
// This was four chained .attr/.style calls, so d3 walked all 18.5k
|
|
3000
|
+
// circles four times; and makeNodeFillColor() was called TWICE per
|
|
3001
|
+
// node -- once to decide whether the dot collapses to r=0, once for
|
|
3002
|
+
// the fill itself -- which is not a cheap accessor: it checks the
|
|
3003
|
+
// search hits, the node events, the visualization colour and the
|
|
3004
|
+
// node's own style.
|
|
3005
|
+
//
|
|
3006
|
+
// The union of the two conditions that need the colour is just the
|
|
3007
|
+
// fill condition, since (showVisualizations && !showNodeEvents)
|
|
3008
|
+
// implies showVisualizations. So it is computed once when needed and
|
|
3009
|
+
// reused, and left null when neither branch wants it.
|
|
3010
|
+
node.select('circle.nodeCircle').each(function (d) {
|
|
3011
|
+
let fill = (_state.showVisualizations || _state.showNodeEvents
|
|
3012
|
+
|| isNodeFound(d) || isNodeSelected(d)) ? makeNodeFillColor(d) : null;
|
|
3013
|
+
let r = ((_state.showVisualizations && !_state.showNodeEvents)
|
|
3014
|
+
&& (fill === _state.backgroundColorDefault)) ? 0 : makeNodeSize(d);
|
|
3015
|
+
this.setAttribute('r', r);
|
|
3016
|
+
this.style.stroke = makeNodeStrokeColor(d);
|
|
3017
|
+
this.style.strokeWidth = _state.branchWidthDefault;
|
|
3018
|
+
this.style.fill = (fill === null) ? _state.backgroundColorDefault : fill;
|
|
3019
|
+
});
|
|
2725
3020
|
|
|
2726
3021
|
|
|
2727
3022
|
// Dim Non-Matches: one opacity on the node GROUP dims its labels, dot,
|
|
@@ -2732,16 +3027,24 @@ function (root, d3, forester, phyloXml) {
|
|
|
2732
3027
|
return (_dimNonMatches && !getFoundColor(d)) ? DIM_NON_MATCH_OPACITY : null;
|
|
2733
3028
|
});
|
|
2734
3029
|
|
|
2735
|
-
let nodeUpdate = node
|
|
2736
|
-
.duration(transitionDuration)
|
|
3030
|
+
let nodeUpdate = animateOrSet(node, transitionDuration)
|
|
2737
3031
|
.attr('transform', function (d) {
|
|
2738
3032
|
return nodeTransform(d);
|
|
2739
3033
|
});
|
|
2740
3034
|
|
|
2741
|
-
|
|
3035
|
+
// The same rule for a node's CHILD elements: derive from the transition
|
|
3036
|
+
// when animating, else select straight off the node and interrupt any
|
|
3037
|
+
// animation still on that child.
|
|
3038
|
+
function nodeChild(selector) {
|
|
3039
|
+
return transitionDuration > 0
|
|
3040
|
+
? nodeUpdate.select(selector)
|
|
3041
|
+
: node.select(selector).interrupt();
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3044
|
+
nodeChild('text')
|
|
2742
3045
|
.style('fill-opacity', 1);
|
|
2743
3046
|
|
|
2744
|
-
|
|
3047
|
+
nodeChild('text.extlabel')
|
|
2745
3048
|
.text(function (d) {
|
|
2746
3049
|
return d._extLabelText; // computed in syncOptionalNodeChildren
|
|
2747
3050
|
});
|
|
@@ -2750,17 +3053,17 @@ function (root, d3, forester, phyloXml) {
|
|
|
2750
3053
|
// has already removed these elements when the switch is off, and a
|
|
2751
3054
|
// .select() that finds nothing still walks every node to find out.
|
|
2752
3055
|
if (_state.showBranchLengthValues) {
|
|
2753
|
-
|
|
3056
|
+
nodeChild('text.bllabel').text(makeBranchLengthLabel);
|
|
2754
3057
|
}
|
|
2755
3058
|
if (_state.showConfidenceValues) {
|
|
2756
|
-
|
|
3059
|
+
nodeChild('text.conflabel').text(makeConfidenceValuesLabel);
|
|
2757
3060
|
}
|
|
2758
3061
|
if (_state.showBranchEvents) {
|
|
2759
|
-
|
|
3062
|
+
nodeChild('text.brancheventlabel').text(makeBranchEventsLabel);
|
|
2760
3063
|
}
|
|
2761
3064
|
|
|
2762
3065
|
let drawShapes = _state.showVisualizations || stylesActive();
|
|
2763
|
-
|
|
3066
|
+
nodeChild('path')
|
|
2764
3067
|
.style('stroke', drawShapes ? makeNodeStrokeColor : null)
|
|
2765
3068
|
.style('stroke-width', _state.branchWidthDefault)
|
|
2766
3069
|
.style('fill', drawShapes ? makeNodeFillColor : null)
|
|
@@ -2772,9 +3075,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
2772
3075
|
// and a full 18k-node walk even when drawShapes made it a no-op. One
|
|
2773
3076
|
// filtered selection with one transition does the same thing.
|
|
2774
3077
|
if (!drawShapes) {
|
|
2775
|
-
node.filter(function (d) {
|
|
3078
|
+
animateOrSet(node.filter(function (d) {
|
|
2776
3079
|
return d.children && makeNodeVisShape(d) === null;
|
|
2777
|
-
}).select('path')
|
|
3080
|
+
}).select('path'), transitionDuration)
|
|
2778
3081
|
.attr('d', 'M0,0');
|
|
2779
3082
|
}
|
|
2780
3083
|
|
|
@@ -2798,7 +3101,26 @@ function (root, d3, forester, phyloXml) {
|
|
|
2798
3101
|
return d.target.id;
|
|
2799
3102
|
});
|
|
2800
3103
|
|
|
2801
|
-
|
|
3104
|
+
// The reference node for the insert below, resolved ONCE.
|
|
3105
|
+
//
|
|
3106
|
+
// d3's insert(name, before) takes `before` as a SELECTOR STRING and
|
|
3107
|
+
// then runs parent.querySelector(before) for EVERY element it
|
|
3108
|
+
// inserts. With 18.5k links going into a group that already holds
|
|
3109
|
+
// 18.5k node <g>s, that is 18.5k full-subtree queries and it
|
|
3110
|
+
// dominated the first draw: measured 1,883 ms against 66 ms for the
|
|
3111
|
+
// same inserts with the lookup hoisted -- 28x, and about a third of
|
|
3112
|
+
// the time to open the biggest demo tree.
|
|
3113
|
+
//
|
|
3114
|
+
// Passing a function instead makes d3 use its return value as-is. The
|
|
3115
|
+
// result is identical: every link still lands before the first node
|
|
3116
|
+
// group, in the same order, so links stay painted behind nodes. It
|
|
3117
|
+
// must be re-resolved each update because the node join above may
|
|
3118
|
+
// have just created or removed those groups.
|
|
3119
|
+
let linkBefore = _svgGroup.node().querySelector('g');
|
|
3120
|
+
|
|
3121
|
+
let linkEnter = link.enter().insert('path', function () {
|
|
3122
|
+
return linkBefore;
|
|
3123
|
+
})
|
|
2802
3124
|
.attr('class', 'link')
|
|
2803
3125
|
.attr('fill', 'none')
|
|
2804
3126
|
.attr('stroke-width', makeBranchWidth)
|
|
@@ -2819,8 +3141,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
2819
3141
|
|
|
2820
3142
|
link.attr('stroke-width', makeBranchWidth);
|
|
2821
3143
|
|
|
2822
|
-
link
|
|
2823
|
-
.duration(transitionDuration)
|
|
3144
|
+
animateOrSet(link, transitionDuration)
|
|
2824
3145
|
.attr('stroke', makeBranchColor)
|
|
2825
3146
|
.attr('d', elbow);
|
|
2826
3147
|
|
|
@@ -2897,6 +3218,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
2897
3218
|
}
|
|
2898
3219
|
|
|
2899
3220
|
hideHoverGlow(); // the hovered node may have moved; the next mouseover re-shows it
|
|
3221
|
+
drawDomainArchitectures();
|
|
3222
|
+
drawDomainLegend();
|
|
2900
3223
|
drawMsaTrack();
|
|
2901
3224
|
drawTimeOverlays();
|
|
2902
3225
|
rebuildOverview(); // measured AFTER the overlays, so the bbox is this frame's
|
|
@@ -3090,7 +3413,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
3090
3413
|
if (value === null) {
|
|
3091
3414
|
return null;
|
|
3092
3415
|
}
|
|
3093
|
-
|
|
3416
|
+
if (colorModeOf(vis) === 'range') {
|
|
3417
|
+
return vis.rangeScale ? vis.rangeScale(Number(value)) : null;
|
|
3418
|
+
}
|
|
3419
|
+
return vis.categoryScale(value);
|
|
3094
3420
|
}
|
|
3095
3421
|
|
|
3096
3422
|
let makeVisNodeFillColor = function (node) {
|
|
@@ -3211,7 +3537,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3211
3537
|
if (_vis && _vis.labelRef && !phynode.children && phynode.properties) {
|
|
3212
3538
|
for (let i = 0; i < phynode.properties.length; ++i) {
|
|
3213
3539
|
let p = phynode.properties[i];
|
|
3214
|
-
if (p.ref === _vis.labelRef && p
|
|
3540
|
+
if (p.ref === _vis.labelRef && forester.isNodeScopedProperty(p) && p.value) {
|
|
3215
3541
|
return String(p.value);
|
|
3216
3542
|
}
|
|
3217
3543
|
}
|
|
@@ -3293,25 +3619,38 @@ function (root, d3, forester, phyloXml) {
|
|
|
3293
3619
|
let wantEvent = _state.showBranchEvents === true;
|
|
3294
3620
|
let wantDot = _state.showSupportDots === true;
|
|
3295
3621
|
|
|
3296
|
-
//
|
|
3622
|
+
// Whole-class removals first: ONE query over the tree group, not one
|
|
3623
|
+
// per node. `node.selectAll(...)` runs querySelectorAll on every one of
|
|
3624
|
+
// the 18,512 node groups, and five of those is 92,560 queries made
|
|
3625
|
+
// largely to discover there is nothing to remove -- measured at 82-92 ms
|
|
3626
|
+
// per redraw on the big tree, against 40-50 ms for the whole per-node
|
|
3627
|
+
// loop below it. Each of these classes is created only in this function,
|
|
3628
|
+
// on a g.node inside _svgGroup, so one query over the group finds
|
|
3629
|
+
// exactly the same elements. (The overview's miniature is out of reach
|
|
3630
|
+
// either way: it hangs off _baseSvg, not _svgGroup.)
|
|
3631
|
+
let dropAll = function (selector) {
|
|
3632
|
+
if (_svgGroup) {
|
|
3633
|
+
_svgGroup.selectAll(selector).remove();
|
|
3634
|
+
}
|
|
3635
|
+
};
|
|
3297
3636
|
if (!wantBl) {
|
|
3298
|
-
|
|
3637
|
+
dropAll('text.bllabel');
|
|
3299
3638
|
}
|
|
3300
3639
|
if (!wantConf) {
|
|
3301
|
-
|
|
3640
|
+
dropAll('text.conflabel');
|
|
3302
3641
|
}
|
|
3303
3642
|
if (!wantEvent) {
|
|
3304
|
-
|
|
3643
|
+
dropAll('text.brancheventlabel');
|
|
3305
3644
|
}
|
|
3306
3645
|
if (!wantDot) {
|
|
3307
|
-
|
|
3646
|
+
dropAll('circle.suppdot');
|
|
3308
3647
|
}
|
|
3309
3648
|
|
|
3310
3649
|
// No search running means no halo anywhere -- one pass instead of
|
|
3311
3650
|
// 18,512 set lookups plus 18,512 DOM queries.
|
|
3312
3651
|
let anyFound = anySearchHits();
|
|
3313
3652
|
if (!anyFound) {
|
|
3314
|
-
|
|
3653
|
+
dropAll('circle.foundHalo');
|
|
3315
3654
|
}
|
|
3316
3655
|
|
|
3317
3656
|
// Raw DOM inside the loop, not d3 selections: this runs once per node
|
|
@@ -3471,7 +3810,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
3471
3810
|
if (value.selected === true && value.propertyRef) {
|
|
3472
3811
|
let prop_text = '';
|
|
3473
3812
|
for (let pm = 0; pm < props_length; ++pm) {
|
|
3474
|
-
if (phynode.properties[pm].ref === value.propertyRef && phynode.properties[pm].datatype === 'xsd:string' && phynode.properties[pm]
|
|
3813
|
+
if (phynode.properties[pm].ref === value.propertyRef && phynode.properties[pm].datatype === 'xsd:string' && forester.isNodeScopedProperty(phynode.properties[pm])) {
|
|
3475
3814
|
if (prop_text.length > 0) {
|
|
3476
3815
|
prop_text += ', '
|
|
3477
3816
|
}
|
|
@@ -3962,6 +4301,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
3962
4301
|
const STATE_KEYS = [
|
|
3963
4302
|
'layout',
|
|
3964
4303
|
'showMsa',
|
|
4304
|
+
'showDomainArchitectures',
|
|
4305
|
+
'domainLabels',
|
|
4306
|
+
'domainGlow',
|
|
4307
|
+
'domainEvalueExponent',
|
|
3965
4308
|
'showTimeAxis',
|
|
3966
4309
|
'timeAxisGrid',
|
|
3967
4310
|
'showSupportDots',
|
|
@@ -4081,12 +4424,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
4081
4424
|
// explicitly is derived from what the loaded tree actually contains
|
|
4082
4425
|
// (an explicit caller option always wins). Field presence comes from
|
|
4083
4426
|
// the same per-tree discovery that drives the search Field menu.
|
|
4084
|
-
let presentFields = new Set();
|
|
4085
|
-
|
|
4086
|
-
forester.availableSearchFields(_treeData).forEach(function (f) {
|
|
4087
|
-
presentFields.add(f.key);
|
|
4088
|
-
});
|
|
4089
|
-
}
|
|
4427
|
+
let presentFields = new Set(_treeData ? forester.availableSearchFields(_treeData) : []);
|
|
4428
|
+
let F = forester.searchFields;
|
|
4090
4429
|
|
|
4091
4430
|
// Branch lengths are worth drawing to scale only when MOST of the
|
|
4092
4431
|
// branches that CARRY THE SCALE have one. Two corrections to the
|
|
@@ -4185,6 +4524,39 @@ function (root, d3, forester, phyloXml) {
|
|
|
4185
4524
|
_state.showMsa = _basicTreeProperties.alignedMolSeqs === true
|
|
4186
4525
|
&& _basicTreeProperties.maxMolSeqLength > 0;
|
|
4187
4526
|
}
|
|
4527
|
+
// A tree with protein domain architectures on its tips draws them
|
|
4528
|
+
// from the start (the desktop switches its toggle on at load), again
|
|
4529
|
+
// unless the caller decided. The track width is set at the first
|
|
4530
|
+
// draw from the viewport; threshold, label mode and glow come from
|
|
4531
|
+
// the config or the desktop's defaults.
|
|
4532
|
+
if (_state.showDomainArchitectures === undefined) {
|
|
4533
|
+
_state.showDomainArchitectures = _basicTreeProperties.domainArchitectures === true;
|
|
4534
|
+
}
|
|
4535
|
+
if (_state.domainLabels === undefined) {
|
|
4536
|
+
_state.domainLabels = 'domains';
|
|
4537
|
+
} else if (DOMAIN_LABEL_MODES.indexOf(_state.domainLabels) < 0) {
|
|
4538
|
+
throw new Error(ERROR + '"domainLabels" must be "none", "domains" or "legend"');
|
|
4539
|
+
}
|
|
4540
|
+
if (_state.domainGlow === undefined) {
|
|
4541
|
+
_state.domainGlow = false;
|
|
4542
|
+
}
|
|
4543
|
+
if (_state.domainEvalueExponent === undefined) {
|
|
4544
|
+
_state.domainEvalueExponent = forester.DOMAIN_EVALUE_EXPONENT_DEFAULT;
|
|
4545
|
+
} else if (!Number.isInteger(_state.domainEvalueExponent)
|
|
4546
|
+
|| _state.domainEvalueExponent < forester.DOMAIN_EVALUE_EXPONENT_MIN
|
|
4547
|
+
|| _state.domainEvalueExponent > forester.DOMAIN_EVALUE_EXPONENT_MAX) {
|
|
4548
|
+
throw new Error(ERROR + '"domainEvalueExponent" must be an integer from '
|
|
4549
|
+
+ forester.DOMAIN_EVALUE_EXPONENT_MIN + ' to ' + forester.DOMAIN_EVALUE_EXPONENT_MAX);
|
|
4550
|
+
}
|
|
4551
|
+
_domain = {width: null, palette: null, next: 0, legendFrac: null};
|
|
4552
|
+
if (_basicTreeProperties.domainArchitectures) {
|
|
4553
|
+
// malformed domains are skipped, never fatal -- said once, here
|
|
4554
|
+
let ignored = forester.domainArchitectureStats(_treeData).ignored;
|
|
4555
|
+
if (ignored > 0) {
|
|
4556
|
+
console.warn(MESSAGE + ignored + ' protein domain' + (ignored === 1 ? '' : 's')
|
|
4557
|
+
+ ' with a missing or impossible from / to / E-value ignored');
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4188
4560
|
// Likewise a dated tree draws its time axis from the start --
|
|
4189
4561
|
// geologic ICS bands or calendar years, decided from the <date>
|
|
4190
4562
|
// elements by forester.timeAxisInfo -- again unless the caller set
|
|
@@ -4209,18 +4581,18 @@ function (root, d3, forester, phyloXml) {
|
|
|
4209
4581
|
// Which taxonomy fields to label with is decided from the tree, not
|
|
4210
4582
|
// configured: show the scientific name and code when present, and fall
|
|
4211
4583
|
// back to the common name only when there is no scientific name.
|
|
4212
|
-
_state.showTaxonomyCode = presentFields.has(
|
|
4213
|
-
_state.showTaxonomyScientificName = presentFields.has(
|
|
4214
|
-
_state.showTaxonomyCommonName = presentFields.has(
|
|
4584
|
+
_state.showTaxonomyCode = presentFields.has(F.taxonomyCode);
|
|
4585
|
+
_state.showTaxonomyScientificName = presentFields.has(F.taxonomyScientificName);
|
|
4586
|
+
_state.showTaxonomyCommonName = presentFields.has(F.taxonomyCommonName) && !presentFields.has(F.taxonomyScientificName);
|
|
4215
4587
|
_state.showTaxonomyRank = false;
|
|
4216
4588
|
_state.showTaxonomySynonyms = false;
|
|
4217
4589
|
// Likewise ONE good sequence identifier rather than all of them, in
|
|
4218
4590
|
// order of preference: sequence name, gene name, symbol, accession.
|
|
4219
|
-
_state.showSequenceName = presentFields.has(
|
|
4220
|
-
_state.showSequenceGeneSymbol = presentFields.has(
|
|
4221
|
-
_state.showSequenceSymbol = presentFields.has(
|
|
4222
|
-
_state.showSequenceAccession = presentFields.has(
|
|
4223
|
-
&& !presentFields.has(
|
|
4591
|
+
_state.showSequenceName = presentFields.has(F.sequenceName);
|
|
4592
|
+
_state.showSequenceGeneSymbol = presentFields.has(F.geneName) && !presentFields.has(F.sequenceName);
|
|
4593
|
+
_state.showSequenceSymbol = presentFields.has(F.sequenceSymbol) && !presentFields.has(F.sequenceName) && !presentFields.has(F.geneName);
|
|
4594
|
+
_state.showSequenceAccession = presentFields.has(F.sequenceAccession) && !presentFields.has(F.sequenceName)
|
|
4595
|
+
&& !presentFields.has(F.geneName) && !presentFields.has(F.sequenceSymbol);
|
|
4224
4596
|
|
|
4225
4597
|
// Which of the three groups start CHECKED is then decided from the
|
|
4226
4598
|
// label text itself (forester.suggestLabelFields): a field whose text
|
|
@@ -4243,7 +4615,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4243
4615
|
if (labelRef && n.properties) {
|
|
4244
4616
|
for (let pi = 0; pi < n.properties.length; ++pi) {
|
|
4245
4617
|
let p = n.properties[pi];
|
|
4246
|
-
if (p.ref === labelRef && p
|
|
4618
|
+
if (p.ref === labelRef && forester.isNodeScopedProperty(p) && p.value) {
|
|
4247
4619
|
return String(p.value);
|
|
4248
4620
|
}
|
|
4249
4621
|
}
|
|
@@ -4510,6 +4882,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4510
4882
|
populateSearchMenus();
|
|
4511
4883
|
|
|
4512
4884
|
_svgGroup = _baseSvg.append('g');
|
|
4885
|
+
attachNodeEventDelegation(_svgGroup);
|
|
4513
4886
|
// the floating strips paint over the tree and under the overview
|
|
4514
4887
|
_floatGroup = _baseSvg.append('g').attr('class', 'aptx-float')
|
|
4515
4888
|
.style('pointer-events', 'none');
|
|
@@ -4581,6 +4954,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
4581
4954
|
_node_mouseover_div.remove(); // the body-level tooltip div (d3 selection)
|
|
4582
4955
|
_node_mouseover_div = null;
|
|
4583
4956
|
}
|
|
4957
|
+
// A redraw still waiting for its frame belongs to the viewer being torn
|
|
4958
|
+
// down. Left set, its callback would read the NEXT viewer's pending
|
|
4959
|
+
// update, null it, then find its own sequence number stale and return
|
|
4960
|
+
// -- having already discarded work that was not its own. I could not
|
|
4961
|
+
// construct that ordering from the UI, and one line means nobody has to
|
|
4962
|
+
// reason about it again.
|
|
4963
|
+
_pendingUpdate = null;
|
|
4584
4964
|
clearInterval(_intervalId); // a zoom button held down at destroy time
|
|
4585
4965
|
d3.select(window).on('resize.archaeopteryx', null);
|
|
4586
4966
|
_docListeners.forEach(function (l) {
|
|
@@ -4598,6 +4978,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4598
4978
|
_baseSvg = null;
|
|
4599
4979
|
_svgGroup = null;
|
|
4600
4980
|
_overviewGroup = null;
|
|
4981
|
+
_overviewLinks = null;
|
|
4601
4982
|
_overviewContent = null;
|
|
4602
4983
|
_overviewViewport = null;
|
|
4603
4984
|
_overviewMap = null;
|
|
@@ -4724,7 +5105,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
4724
5105
|
_baseSvg = d3.select(containerEl).append('svg')
|
|
4725
5106
|
.attr('width', _displayWidth)
|
|
4726
5107
|
.attr('height', _displayHeight)
|
|
4727
|
-
.call(_zoomListener)
|
|
5108
|
+
.call(_zoomListener)
|
|
5109
|
+
// d3.zoom binds its own double-click-to-zoom, which scales the
|
|
5110
|
+
// picture -- fonts and all -- and so behaves like the wheel
|
|
5111
|
+
// zoom this program deliberately replaced with a re-layout.
|
|
5112
|
+
// It is also easy to hit by accident: a double click that
|
|
5113
|
+
// misses a node is a miss, not a request to magnify the tree.
|
|
5114
|
+
.on('dblclick.zoom', null);
|
|
4728
5115
|
|
|
4729
5116
|
if (_settings.enableDynamicSizing) {
|
|
4730
5117
|
d3.select(window)
|
|
@@ -4989,7 +5376,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4989
5376
|
let properties_text = '';
|
|
4990
5377
|
let prev_property_ref = null;
|
|
4991
5378
|
for (let pl = 0; pl < l; ++pl) {
|
|
4992
|
-
if (sorted_properties[pl]
|
|
5379
|
+
if (forester.isNodeScopedProperty(sorted_properties[pl])) {
|
|
4993
5380
|
if (sorted_properties[pl].ref === prev_property_ref) {
|
|
4994
5381
|
properties_text = addSepSame(properties_text);
|
|
4995
5382
|
} else {
|
|
@@ -5222,7 +5609,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5222
5609
|
fakeNode.y0 = 0;
|
|
5223
5610
|
_root = fakeNode;
|
|
5224
5611
|
_basicTreeProperties = forester.collectBasicTreeProperties(_root);
|
|
5225
|
-
refreshVisualizations();
|
|
5612
|
+
refreshVisualizations(false);
|
|
5226
5613
|
search0();
|
|
5227
5614
|
search1();
|
|
5228
5615
|
zoomToFit();
|
|
@@ -5335,7 +5722,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5335
5722
|
forester.deleteSubtree(tree, d);
|
|
5336
5723
|
_treeData = tree;
|
|
5337
5724
|
_basicTreeProperties = forester.collectBasicTreeProperties(_treeData);
|
|
5338
|
-
refreshVisualizations();
|
|
5725
|
+
refreshVisualizations(true);
|
|
5339
5726
|
search0();
|
|
5340
5727
|
search1();
|
|
5341
5728
|
zoomToFit();
|
|
@@ -5350,27 +5737,39 @@ function (root, d3, forester, phyloXml) {
|
|
|
5350
5737
|
}
|
|
5351
5738
|
|
|
5352
5739
|
|
|
5353
|
-
//
|
|
5354
|
-
//
|
|
5355
|
-
//
|
|
5356
|
-
//
|
|
5357
|
-
//
|
|
5358
|
-
//
|
|
5359
|
-
//
|
|
5360
|
-
//
|
|
5361
|
-
//
|
|
5362
|
-
//
|
|
5363
|
-
//
|
|
5364
|
-
|
|
5740
|
+
// The view changed (into or out of a subtree), or the tree was EDITED.
|
|
5741
|
+
//
|
|
5742
|
+
// A view never changes what is offered or chosen: the menu stands, the
|
|
5743
|
+
// colour and shape stand, and only the summaries move -- legend rows and
|
|
5744
|
+
// counts, the "no value" row, a numeric band -- to describe the tips on
|
|
5745
|
+
// screen. Diving into a one-value clade coloured by Genus shows a
|
|
5746
|
+
// one-row legend, not a grey tree.
|
|
5747
|
+
//
|
|
5748
|
+
// An edit re-derives the candidates from what is left, because it is a
|
|
5749
|
+
// different tree now. The chosen fields are kept as long as they still
|
|
5750
|
+
// carry a value on any remaining tip (visualizationCandidatesKeeping):
|
|
5751
|
+
// the refusal rules decide what is OFFERED, never what is already
|
|
5752
|
+
// chosen. Only a field with no value left anywhere falls back to default.
|
|
5753
|
+
// No checkbox moves either way, because a view change is not a user choice.
|
|
5754
|
+
function refreshVisualizations(edited) {
|
|
5755
|
+
if (edited && _domain) {
|
|
5756
|
+
_domain.palette = null; // the tree changed: the domain names are dealt their colours again
|
|
5757
|
+
}
|
|
5365
5758
|
if (!_vis) {
|
|
5366
5759
|
return;
|
|
5367
5760
|
}
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5761
|
+
if (edited) {
|
|
5762
|
+
let colorId = _vis.colorId;
|
|
5763
|
+
let shapeId = _vis.shapeId;
|
|
5764
|
+
installCandidates(forester.visualizationCandidatesKeeping(_treeData,
|
|
5765
|
+
[currentColorVis(), currentShapeVis()]));
|
|
5766
|
+
_vis.colorId = (colorId && _vis.byId[colorId]) ? colorId : null;
|
|
5767
|
+
_vis.shapeId = (shapeId && _vis.byId[shapeId] && _vis.byId[shapeId].shape) ? shapeId : null;
|
|
5768
|
+
}
|
|
5769
|
+
summarizeView(displayedRoot());
|
|
5770
|
+
if (edited) {
|
|
5771
|
+
populateVisualizationMenus();
|
|
5772
|
+
}
|
|
5374
5773
|
removeColorLegend(LEGEND_LABEL_COLOR);
|
|
5375
5774
|
removeShapeLegend(LEGEND_NODE_SHAPE);
|
|
5376
5775
|
}
|
|
@@ -5394,7 +5793,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
5394
5793
|
// part of the real spans (an anchor ratio taken from anything else
|
|
5395
5794
|
// once flung the whole tree off-screen after three X+ presses).
|
|
5396
5795
|
return {
|
|
5397
|
-
horizontal: Math.max(1, _displayWidth - calcMaxTreeLengthForDisplay() - _msaReserve),
|
|
5796
|
+
horizontal: Math.max(1, _displayWidth - calcMaxTreeLengthForDisplay() - _msaReserve - _domainReserve),
|
|
5398
5797
|
vertical: Math.max(40, _displayHeight - (2 * TOP_AND_BOTTOM_BORDER_HEIGHT) - bottomOverlayReserve())
|
|
5399
5798
|
};
|
|
5400
5799
|
}
|
|
@@ -6126,6 +6525,448 @@ function (root, d3, forester, phyloXml) {
|
|
|
6126
6525
|
scheduleUpdate(null, 0);
|
|
6127
6526
|
}
|
|
6128
6527
|
|
|
6528
|
+
// ===================== Protein domain architectures =====================
|
|
6529
|
+
// The desktop's domain display, ported from its RenderableDomainArchitecture
|
|
6530
|
+
// and TreePanel (the spec is section D1 of the repo's TODO.md). Each tip's
|
|
6531
|
+
// architecture is a grey backbone of L residues with a rounded box per
|
|
6532
|
+
// domain the E-value threshold admits: in one aligned column past the
|
|
6533
|
+
// labels in the rectangular layout, riding the tip's spoke in the radial
|
|
6534
|
+
// ones. One scale for the whole tree -- f px per residue, from the track
|
|
6535
|
+
// width and the longest architecture -- so lengths compare across tips.
|
|
6536
|
+
// Which domains, where, which colour and which legend rows are forester's
|
|
6537
|
+
// (tested against the desktop's numbers); this is the SVG, the legend
|
|
6538
|
+
// card and the controls. The boxes take no pointer events: nothing to
|
|
6539
|
+
// hover or click, as on the desktop.
|
|
6540
|
+
|
|
6541
|
+
function domainsShown() {
|
|
6542
|
+
return _state.showDomainArchitectures === true
|
|
6543
|
+
&& _basicTreeProperties.domainArchitectures === true
|
|
6544
|
+
&& _basicTreeProperties.maxDomainArchitectureLength > 0
|
|
6545
|
+
&& _state.showExternalLabels
|
|
6546
|
+
// in a radial layout the boxes ride the spokes, so only radial
|
|
6547
|
+
// labels go with them
|
|
6548
|
+
&& (!radialDisplay() || !_radialLabelsHorizontal);
|
|
6549
|
+
}
|
|
6550
|
+
|
|
6551
|
+
// The track width W: a quarter of the viewport at first use (the
|
|
6552
|
+
// layout's width, if smaller); d+ / d- scale it by 1.2 / 0.8 and stop
|
|
6553
|
+
// at 2000 / 20.
|
|
6554
|
+
function domainTrackWidth() {
|
|
6555
|
+
if (!_domain.width) {
|
|
6556
|
+
let vp = svgSize();
|
|
6557
|
+
let vw = Math.min(_displayWidth, (vp && vp.w) ? vp.w : _displayWidth);
|
|
6558
|
+
_domain.width = Math.max(DOMAIN_WIDTH_MIN, Math.round(vw * DOMAIN_WIDTH_VIEWPORT_FRACTION));
|
|
6559
|
+
}
|
|
6560
|
+
return _domain.width;
|
|
6561
|
+
}
|
|
6562
|
+
|
|
6563
|
+
// In a radial layout the track is capped at a share of the radius, or a
|
|
6564
|
+
// long architecture would run off the circle.
|
|
6565
|
+
function domainEffectiveWidth() {
|
|
6566
|
+
let w = domainTrackWidth();
|
|
6567
|
+
if (radialDisplay()) {
|
|
6568
|
+
let r = _state.circularDisplay ? (_radial ? _radial.maxRad : 0) : (_unroot ? _unroot.maxRad : 0);
|
|
6569
|
+
if (r > 0) {
|
|
6570
|
+
w = Math.min(w, DOMAIN_RADIAL_WIDTH_FRACTION * r);
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
return w;
|
|
6574
|
+
}
|
|
6575
|
+
|
|
6576
|
+
// px per residue, one factor for every architecture on the tree; Lmax
|
|
6577
|
+
// counts every domain whatever its E-value, so the threshold never
|
|
6578
|
+
// rescales the tracks
|
|
6579
|
+
function domainScale() {
|
|
6580
|
+
return (domainEffectiveWidth() / _basicTreeProperties.maxDomainArchitectureLength) * DOMAIN_SCALE_HEADROOM;
|
|
6581
|
+
}
|
|
6582
|
+
|
|
6583
|
+
// The room the longest tip label takes: the same estimate the layout is
|
|
6584
|
+
// sized against (there are no font metrics in the model).
|
|
6585
|
+
function domainLabelSpace() {
|
|
6586
|
+
return (_maxLabelLength * _state.externalNodeFontSize * LABEL_SIZE_CALC_FACTOR) + LABEL_SIZE_CALC_ADDITION;
|
|
6587
|
+
}
|
|
6588
|
+
|
|
6589
|
+
// The rectangular layout reserves the tracks' column so the tree and
|
|
6590
|
+
// labels compress to make room, as the alignment track does; the radial
|
|
6591
|
+
// layouts fit the tracks into the ring instead (fitRadialExtent).
|
|
6592
|
+
function domainReserve() {
|
|
6593
|
+
if (!domainsShown() || radialDisplay()) {
|
|
6594
|
+
return 0;
|
|
6595
|
+
}
|
|
6596
|
+
return DOMAIN_TRACK_START_GAP + domainTrackWidth() + DOMAIN_TRACK_END_GAP;
|
|
6597
|
+
}
|
|
6598
|
+
|
|
6599
|
+
function domainRadialExtent() {
|
|
6600
|
+
if (!domainsShown() || !radialDisplay()) {
|
|
6601
|
+
return 0;
|
|
6602
|
+
}
|
|
6603
|
+
return DOMAIN_RADIAL_GAP + domainEffectiveWidth() + DOMAIN_TRACK_END_GAP;
|
|
6604
|
+
}
|
|
6605
|
+
|
|
6606
|
+
// Colours: the names the threshold admits over the WHOLE tree, sorted by
|
|
6607
|
+
// code unit, take the palette in order. Dealt at load, after an edit and
|
|
6608
|
+
// whenever the threshold changes (the drawn set changes then, so colours
|
|
6609
|
+
// can shift); a name met later takes the next unused colour and keeps
|
|
6610
|
+
// it. Never derived from the name's characters -- that let SH2 and SH3
|
|
6611
|
+
// collide on the desktop.
|
|
6612
|
+
function domainPalette() {
|
|
6613
|
+
if (!_domain.palette) {
|
|
6614
|
+
let names = forester.domainSummary(forester.getTreeRoot(_treeData), _state.domainEvalueExponent).names;
|
|
6615
|
+
_domain.palette = Object.create(null);
|
|
6616
|
+
names.forEach(function (name, i) {
|
|
6617
|
+
_domain.palette[name] = i;
|
|
6618
|
+
});
|
|
6619
|
+
_domain.next = names.length;
|
|
6620
|
+
}
|
|
6621
|
+
return _domain.palette;
|
|
6622
|
+
}
|
|
6623
|
+
|
|
6624
|
+
function domainColor(name) {
|
|
6625
|
+
if (!name) {
|
|
6626
|
+
return forester.DOMAIN_UNNAMED_COLOR;
|
|
6627
|
+
}
|
|
6628
|
+
let palette = domainPalette();
|
|
6629
|
+
if (palette[name] === undefined) {
|
|
6630
|
+
palette[name] = _domain.next++;
|
|
6631
|
+
}
|
|
6632
|
+
return forester.domainQualitativeColor(palette[name]);
|
|
6633
|
+
}
|
|
6634
|
+
|
|
6635
|
+
function domainBoxHeight(pitch) {
|
|
6636
|
+
return Math.max(DOMAIN_BOX_MIN_H, Math.min(DOMAIN_BOX_MAX_H, Math.round(pitch)));
|
|
6637
|
+
}
|
|
6638
|
+
|
|
6639
|
+
function drawDomainArchitectures() {
|
|
6640
|
+
if (!_svgGroup) {
|
|
6641
|
+
return;
|
|
6642
|
+
}
|
|
6643
|
+
_svgGroup.selectAll('g.aptx-domains').remove();
|
|
6644
|
+
if (!domainsShown() || !_root) {
|
|
6645
|
+
return;
|
|
6646
|
+
}
|
|
6647
|
+
let tips = forester.getAllExternalNodes(_root).filter(function (d) {
|
|
6648
|
+
return _state.unrootedDisplay ? (d.ux !== undefined) : (d.x !== undefined);
|
|
6649
|
+
});
|
|
6650
|
+
let f = domainScale();
|
|
6651
|
+
if (tips.length === 0 || !(f > 0) || !isFinite(f)) {
|
|
6652
|
+
return;
|
|
6653
|
+
}
|
|
6654
|
+
let g = _svgGroup.append('g').attr('class', 'aptx-domains').style('pointer-events', 'none');
|
|
6655
|
+
// one vertical gradient per base colour, lighter at the top; the
|
|
6656
|
+
// defs ride into the SVG / PDF / PNG exports with the boxes
|
|
6657
|
+
let defs = g.append('defs');
|
|
6658
|
+
let made = Object.create(null);
|
|
6659
|
+
function gradientFor(base) {
|
|
6660
|
+
let id = 'aptx-dom-' + base.substring(1);
|
|
6661
|
+
if (!made[id]) {
|
|
6662
|
+
made[id] = true;
|
|
6663
|
+
let grad = defs.append('linearGradient').attr('id', id)
|
|
6664
|
+
.attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1);
|
|
6665
|
+
grad.append('stop').attr('offset', '0%').attr('stop-color', forester.domainLighten(base, 0.12));
|
|
6666
|
+
grad.append('stop').attr('offset', '100%').attr('stop-color', forester.domainDarken(base, 0.10));
|
|
6667
|
+
}
|
|
6668
|
+
return 'url(#' + id + ')';
|
|
6669
|
+
}
|
|
6670
|
+
let labelSpace = domainLabelSpace();
|
|
6671
|
+
if (_state.circularDisplay) {
|
|
6672
|
+
// every bar starts on one ring past the labels and rides its
|
|
6673
|
+
// tip's spoke outward
|
|
6674
|
+
let r0 = _radial.maxRad + labelSpace + DOMAIN_RADIAL_GAP;
|
|
6675
|
+
let h = domainBoxHeight((r0 * 2 * Math.PI) / tips.length);
|
|
6676
|
+
tips.forEach(function (d) {
|
|
6677
|
+
let t = g.append('g').attr('transform', 'rotate(' + labelAngleDeg(d) + ')');
|
|
6678
|
+
drawOneArchitecture(t, forester.domainArchitectureOf(d), r0, -h / 2, h, f, false, gradientFor);
|
|
6679
|
+
});
|
|
6680
|
+
} else if (_state.unrootedDisplay) {
|
|
6681
|
+
// no common ring: each bar starts past its own tip's label
|
|
6682
|
+
let h = domainBoxHeight((Math.PI * 2 * _unroot.maxRad) / tips.length);
|
|
6683
|
+
let start = labelSpace + DOMAIN_RADIAL_GAP;
|
|
6684
|
+
tips.forEach(function (d) {
|
|
6685
|
+
let t = g.append('g').attr('transform', 'translate(' + d.ux + ',' + d.uy + ') rotate(' + labelAngleDeg(d) + ')');
|
|
6686
|
+
drawOneArchitecture(t, forester.domainArchitectureOf(d), start, -h / 2, h, f, false, gradientFor);
|
|
6687
|
+
});
|
|
6688
|
+
} else {
|
|
6689
|
+
// one aligned column: past the deepest tip and the longest label.
|
|
6690
|
+
// The box height follows the tip pitch (half of it), 6 to 16 px.
|
|
6691
|
+
tips.sort(function (p, q) {
|
|
6692
|
+
return p.x - q.x;
|
|
6693
|
+
});
|
|
6694
|
+
let n = tips.length;
|
|
6695
|
+
let pitch = n > 1 ? (tips[n - 1].x - tips[0].x) / (n - 1) : _state.externalNodeFontSize;
|
|
6696
|
+
let h = domainBoxHeight(pitch / 2);
|
|
6697
|
+
let start = _w + _state.nodeLabelGap + labelSpace + DOMAIN_TRACK_START_GAP;
|
|
6698
|
+
let labelsOn = _state.domainLabels === 'domains';
|
|
6699
|
+
tips.forEach(function (d) {
|
|
6700
|
+
drawOneArchitecture(g, forester.domainArchitectureOf(d), start, d.x - (h / 2), h, f, labelsOn, gradientFor);
|
|
6701
|
+
});
|
|
6702
|
+
}
|
|
6703
|
+
}
|
|
6704
|
+
|
|
6705
|
+
function domainRect(g, x, y, w, h, r) {
|
|
6706
|
+
return g.append('rect')
|
|
6707
|
+
.attr('x', x).attr('y', y).attr('width', w).attr('height', h)
|
|
6708
|
+
.attr('rx', r).attr('ry', r);
|
|
6709
|
+
}
|
|
6710
|
+
|
|
6711
|
+
// One architecture, in the desktop's order: the backbone, then per
|
|
6712
|
+
// admitted domain its stepped drop shadow, the optional glow, the
|
|
6713
|
+
// gradient body with its border, and -- rectangular only -- the name,
|
|
6714
|
+
// when it fits the box.
|
|
6715
|
+
function drawOneArchitecture(g, da, start, y1, h, f, labelsOn, gradientFor) {
|
|
6716
|
+
if (!da) {
|
|
6717
|
+
return;
|
|
6718
|
+
}
|
|
6719
|
+
let geo = forester.domainBoxes(da, start, f, _state.domainEvalueExponent);
|
|
6720
|
+
g.append('rect')
|
|
6721
|
+
.attr('x', geo.backbone.x).attr('y', y1 + (h / 2) - 0.5)
|
|
6722
|
+
.attr('width', geo.backbone.w).attr('height', 1)
|
|
6723
|
+
.style('fill', DOMAIN_BACKBONE_COLOR);
|
|
6724
|
+
let fs = Math.min(_state.externalNodeFontSize, h - 2);
|
|
6725
|
+
let font = fs + 'px ' + FONT_DEFAULTS;
|
|
6726
|
+
geo.boxes.forEach(function (b) {
|
|
6727
|
+
let base = domainColor(b.name);
|
|
6728
|
+
let r = Math.min(2, Math.min(b.w, h) / 2);
|
|
6729
|
+
DOMAIN_SHADOWS.forEach(function (s) {
|
|
6730
|
+
domainRect(g, b.x + s[0], y1 + s[1], b.w, h, r)
|
|
6731
|
+
.style('fill', DOMAIN_SHADOW_COLOR)
|
|
6732
|
+
.style('fill-opacity', s[2] / 255);
|
|
6733
|
+
});
|
|
6734
|
+
if (_state.domainGlow) {
|
|
6735
|
+
DOMAIN_GLOWS.forEach(function (gl) {
|
|
6736
|
+
let o = gl[0];
|
|
6737
|
+
domainRect(g, b.x - o, y1 - o, b.w + (2 * o), h + (2 * o), r + o)
|
|
6738
|
+
.style('fill', base)
|
|
6739
|
+
.style('fill-opacity', gl[1] / 255);
|
|
6740
|
+
});
|
|
6741
|
+
}
|
|
6742
|
+
domainRect(g, b.x, y1, b.w, h, r)
|
|
6743
|
+
.style('fill', gradientFor(base))
|
|
6744
|
+
.style('stroke', forester.domainDarken(base, 0.24))
|
|
6745
|
+
.style('stroke-width', 1);
|
|
6746
|
+
if (labelsOn && b.name && fs > 4 && legendTextWidth(b.name, font) <= b.w - 4) {
|
|
6747
|
+
g.append('text')
|
|
6748
|
+
.attr('x', b.x + (b.w / 2)).attr('y', y1 + (h / 2))
|
|
6749
|
+
.attr('dy', '0.35em')
|
|
6750
|
+
.attr('text-anchor', 'middle')
|
|
6751
|
+
.style('font', font)
|
|
6752
|
+
.style('fill', forester.domainLabelInk(base))
|
|
6753
|
+
.text(b.name);
|
|
6754
|
+
}
|
|
6755
|
+
});
|
|
6756
|
+
}
|
|
6757
|
+
|
|
6758
|
+
function clipTextToWidth(text, font, maxPx) {
|
|
6759
|
+
if (legendTextWidth(text, font) <= maxPx) {
|
|
6760
|
+
return text;
|
|
6761
|
+
}
|
|
6762
|
+
let s = text;
|
|
6763
|
+
while (s.length > 1 && legendTextWidth(s + '…', font) > maxPx) {
|
|
6764
|
+
s = s.substring(0, s.length - 1);
|
|
6765
|
+
}
|
|
6766
|
+
return s + '…';
|
|
6767
|
+
}
|
|
6768
|
+
|
|
6769
|
+
// The domain legend, in "Legend" mode only: the names drawn in the view,
|
|
6770
|
+
// in first-appearance order, each with its box count; a flat swatch per
|
|
6771
|
+
// row. Its home is the bottom-right corner; a drag keeps its place as a
|
|
6772
|
+
// fraction of the view, a double-click sends it home. Its own card, so
|
|
6773
|
+
// it neither follows nor crowds the Color-by legend.
|
|
6774
|
+
function drawDomainLegend() {
|
|
6775
|
+
if (!_baseSvg) {
|
|
6776
|
+
return;
|
|
6777
|
+
}
|
|
6778
|
+
_baseSvg.selectAll('g.' + LEGEND_DOMAINS).remove();
|
|
6779
|
+
if (!_root || !domainsShown() || _state.domainLabels !== 'legend') {
|
|
6780
|
+
return;
|
|
6781
|
+
}
|
|
6782
|
+
// rows in the order the tips are on screen -- top to bottom, or
|
|
6783
|
+
// clockwise -- not the data's order, which a ladderized display
|
|
6784
|
+
// does not follow
|
|
6785
|
+
let tips = forester.getAllExternalNodes(_root).filter(function (d) {
|
|
6786
|
+
return d.x !== undefined;
|
|
6787
|
+
}).sort(function (p, q) {
|
|
6788
|
+
return p.x - q.x;
|
|
6789
|
+
});
|
|
6790
|
+
let summary = forester.domainSummary(tips, _state.domainEvalueExponent);
|
|
6791
|
+
let size = svgSize();
|
|
6792
|
+
if (summary.legend.length === 0 || !size) {
|
|
6793
|
+
return;
|
|
6794
|
+
}
|
|
6795
|
+
const FS = Math.max(11, _state.externalNodeFontSize || 11);
|
|
6796
|
+
const PAD = 9;
|
|
6797
|
+
const ROW = FS + 6;
|
|
6798
|
+
const SWATCH = 10;
|
|
6799
|
+
const GAP = 6;
|
|
6800
|
+
const rowFont = FS + 'px ' + FONT_DEFAULTS;
|
|
6801
|
+
const titleFont = '600 ' + (FS + 1) + 'px ' + FONT_DEFAULTS;
|
|
6802
|
+
const ink = _state.labelColorDefault;
|
|
6803
|
+
const frame = _state.branchColorDefault;
|
|
6804
|
+
let title = 'Protein domains (E ≤ 1e' + _state.domainEvalueExponent + ')';
|
|
6805
|
+
let rows = summary.legend.map(function (r) {
|
|
6806
|
+
return {
|
|
6807
|
+
text: clipTextToWidth(r.name + ' (' + r.count + ')', rowFont, DOMAIN_LEGEND_ROW_MAX_PX),
|
|
6808
|
+
color: domainColor(r.name)
|
|
6809
|
+
};
|
|
6810
|
+
});
|
|
6811
|
+
let width = legendTextWidth(title, titleFont);
|
|
6812
|
+
rows.forEach(function (r) {
|
|
6813
|
+
width = Math.max(width, SWATCH + GAP + legendTextWidth(r.text, rowFont));
|
|
6814
|
+
});
|
|
6815
|
+
width += 2 * PAD;
|
|
6816
|
+
let height = PAD + ROW + (rows.length * ROW) + PAD - 2;
|
|
6817
|
+
let x, y;
|
|
6818
|
+
if (_domain.legendFrac) {
|
|
6819
|
+
x = _domain.legendFrac.fx * size.w;
|
|
6820
|
+
y = _domain.legendFrac.fy * size.h;
|
|
6821
|
+
} else {
|
|
6822
|
+
x = size.w - width - DOMAIN_LEGEND_INSET;
|
|
6823
|
+
y = size.h - height - DOMAIN_LEGEND_INSET;
|
|
6824
|
+
}
|
|
6825
|
+
x = Math.max(0, Math.min(size.w - 20, x));
|
|
6826
|
+
y = Math.max(0, Math.min(size.h - 20, y));
|
|
6827
|
+
|
|
6828
|
+
let g = _baseSvg.append('g').attr('class', LEGEND_DOMAINS)
|
|
6829
|
+
.style('cursor', 'move')
|
|
6830
|
+
.call(d3.drag()
|
|
6831
|
+
.on('start', function (event) {
|
|
6832
|
+
if (event.sourceEvent) {
|
|
6833
|
+
event.sourceEvent.stopPropagation(); // not a pan of the tree
|
|
6834
|
+
}
|
|
6835
|
+
})
|
|
6836
|
+
.on('drag', function (event) {
|
|
6837
|
+
x += event.dx;
|
|
6838
|
+
y += event.dy;
|
|
6839
|
+
_domain.legendFrac = {fx: x / size.w, fy: y / size.h};
|
|
6840
|
+
drawDomainLegend();
|
|
6841
|
+
}))
|
|
6842
|
+
.on('dblclick', function (event) {
|
|
6843
|
+
event.stopPropagation();
|
|
6844
|
+
_domain.legendFrac = null;
|
|
6845
|
+
drawDomainLegend();
|
|
6846
|
+
});
|
|
6847
|
+
g.append('title').text('drag to move; double-click to send it back to the corner');
|
|
6848
|
+
g.append('rect')
|
|
6849
|
+
.attr('x', x).attr('y', y)
|
|
6850
|
+
.attr('width', width).attr('height', height)
|
|
6851
|
+
.attr('rx', 5)
|
|
6852
|
+
.style('fill', _state.backgroundColorDefault)
|
|
6853
|
+
.style('fill-opacity', 0.92)
|
|
6854
|
+
.style('stroke', frame)
|
|
6855
|
+
.style('stroke-opacity', 0.5);
|
|
6856
|
+
let baseline = y + PAD + FS;
|
|
6857
|
+
g.append('text')
|
|
6858
|
+
.attr('x', x + PAD).attr('y', baseline)
|
|
6859
|
+
.style('font', titleFont)
|
|
6860
|
+
.style('fill', ink)
|
|
6861
|
+
.text(title);
|
|
6862
|
+
rows.forEach(function (r) {
|
|
6863
|
+
baseline += ROW;
|
|
6864
|
+
g.append('rect')
|
|
6865
|
+
.attr('x', x + PAD).attr('y', baseline - SWATCH + 1)
|
|
6866
|
+
.attr('width', SWATCH).attr('height', SWATCH)
|
|
6867
|
+
.attr('rx', 2)
|
|
6868
|
+
.style('fill', r.color);
|
|
6869
|
+
g.append('text')
|
|
6870
|
+
.attr('x', x + PAD + SWATCH + GAP).attr('y', baseline)
|
|
6871
|
+
.style('font', rowFont)
|
|
6872
|
+
.style('fill', ink)
|
|
6873
|
+
.text(r.text);
|
|
6874
|
+
});
|
|
6875
|
+
}
|
|
6876
|
+
|
|
6877
|
+
// The controls follow the toggle: hidden without it, and while shown
|
|
6878
|
+
// the readout, the select, the glow box and the four step buttons say
|
|
6879
|
+
// where things stand (a button at its limit is disabled, not a no-op).
|
|
6880
|
+
function syncDomainControls() {
|
|
6881
|
+
let fs = byId(DOMAIN_CONTROLS);
|
|
6882
|
+
if (!fs) {
|
|
6883
|
+
return;
|
|
6884
|
+
}
|
|
6885
|
+
let on = _state.showDomainArchitectures === true && _basicTreeProperties.domainArchitectures === true;
|
|
6886
|
+
setCheckboxValue(DOMAINS_CB, _state.showDomainArchitectures === true);
|
|
6887
|
+
fs.style.display = on ? '' : 'none';
|
|
6888
|
+
if (!on) {
|
|
6889
|
+
return;
|
|
6890
|
+
}
|
|
6891
|
+
let readout = byId(DOMAIN_EVALUE_READOUT);
|
|
6892
|
+
if (readout) {
|
|
6893
|
+
readout.textContent = forester.domainEvalueLabel(_state.domainEvalueExponent);
|
|
6894
|
+
}
|
|
6895
|
+
let w = domainTrackWidth();
|
|
6896
|
+
(w > DOMAIN_WIDTH_MIN ? enableButton : disableButton)(byId(DOMAIN_WIDTH_DEC));
|
|
6897
|
+
(w < DOMAIN_WIDTH_MAX ? enableButton : disableButton)(byId(DOMAIN_WIDTH_INC));
|
|
6898
|
+
(_state.domainEvalueExponent > forester.DOMAIN_EVALUE_EXPONENT_MIN ? enableButton : disableButton)(byId(DOMAIN_EVALUE_DEC));
|
|
6899
|
+
(_state.domainEvalueExponent < forester.DOMAIN_EVALUE_EXPONENT_MAX ? enableButton : disableButton)(byId(DOMAIN_EVALUE_INC));
|
|
6900
|
+
setValue(DOMAIN_LABELS_SELECT, _state.domainLabels);
|
|
6901
|
+
setCheckboxValue(DOMAIN_GLOW_CB, _state.domainGlow === true);
|
|
6902
|
+
}
|
|
6903
|
+
|
|
6904
|
+
function domainsCbClicked() {
|
|
6905
|
+
_state.showDomainArchitectures = getCheckboxValue(DOMAINS_CB);
|
|
6906
|
+
if (_state.showDomainArchitectures && radialDisplay()) {
|
|
6907
|
+
_radialLabelsHorizontal = false; // the boxes ride the spokes, so the labels must too
|
|
6908
|
+
syncZoomRowButtons();
|
|
6909
|
+
}
|
|
6910
|
+
syncDomainControls();
|
|
6911
|
+
scheduleUpdate(null, 0);
|
|
6912
|
+
}
|
|
6913
|
+
|
|
6914
|
+
// d+ / d-: the width grows by 1.2 while under 2000, shrinks by 0.8 while
|
|
6915
|
+
// over 20 (so one press can carry it past a limit; the next is refused)
|
|
6916
|
+
function domainWidthStep(grow) {
|
|
6917
|
+
let w = domainTrackWidth();
|
|
6918
|
+
if (grow ? w >= DOMAIN_WIDTH_MAX : w <= DOMAIN_WIDTH_MIN) {
|
|
6919
|
+
return;
|
|
6920
|
+
}
|
|
6921
|
+
_domain.width = w * (grow ? DOMAIN_WIDTH_GROW : DOMAIN_WIDTH_SHRINK);
|
|
6922
|
+
syncDomainControls();
|
|
6923
|
+
scheduleUpdate(null, 0);
|
|
6924
|
+
}
|
|
6925
|
+
|
|
6926
|
+
function domainEvalueStep(delta) {
|
|
6927
|
+
let e = _state.domainEvalueExponent + delta;
|
|
6928
|
+
if (e < forester.DOMAIN_EVALUE_EXPONENT_MIN || e > forester.DOMAIN_EVALUE_EXPONENT_MAX) {
|
|
6929
|
+
return;
|
|
6930
|
+
}
|
|
6931
|
+
_state.domainEvalueExponent = e;
|
|
6932
|
+
_domain.palette = null; // the drawn set changed: the palette is dealt again
|
|
6933
|
+
syncDomainControls();
|
|
6934
|
+
scheduleUpdate(null, 0, true);
|
|
6935
|
+
}
|
|
6936
|
+
|
|
6937
|
+
function domainLabelsChanged() {
|
|
6938
|
+
_state.domainLabels = getValue(DOMAIN_LABELS_SELECT);
|
|
6939
|
+
scheduleUpdate(null, 0, true);
|
|
6940
|
+
}
|
|
6941
|
+
|
|
6942
|
+
function domainGlowCbClicked() {
|
|
6943
|
+
_state.domainGlow = getCheckboxValue(DOMAIN_GLOW_CB);
|
|
6944
|
+
scheduleUpdate(null, 0, true);
|
|
6945
|
+
}
|
|
6946
|
+
|
|
6947
|
+
// A step button that repeats while held: one step on press, then after
|
|
6948
|
+
// a pause one every 90 ms until release (the desktop's auto-repeat on
|
|
6949
|
+
// d- / d+). A keyboard press steps once.
|
|
6950
|
+
function onHoldRepeat(id, step) {
|
|
6951
|
+
let delay = null;
|
|
6952
|
+
let repeat = null;
|
|
6953
|
+
onHold(id, function () {
|
|
6954
|
+
step();
|
|
6955
|
+
delay = setTimeout(function () {
|
|
6956
|
+
repeat = setInterval(step, 90);
|
|
6957
|
+
}, 400);
|
|
6958
|
+
}, function () {
|
|
6959
|
+
if (delay) {
|
|
6960
|
+
clearTimeout(delay);
|
|
6961
|
+
delay = null;
|
|
6962
|
+
}
|
|
6963
|
+
if (repeat) {
|
|
6964
|
+
clearInterval(repeat);
|
|
6965
|
+
repeat = null;
|
|
6966
|
+
}
|
|
6967
|
+
});
|
|
6968
|
+
}
|
|
6969
|
+
|
|
6129
6970
|
// Search B starts hidden to keep the panel compact; one click (or a
|
|
6130
6971
|
// configured initial value) reveals it, and it stays revealed.
|
|
6131
6972
|
function revealSearchB() {
|
|
@@ -6465,7 +7306,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6465
7306
|
return;
|
|
6466
7307
|
}
|
|
6467
7308
|
let labelSpace = (_maxLabelLength * _state.externalNodeFontSize * LABEL_SIZE_CALC_FACTOR) + LABEL_SIZE_CALC_ADDITION;
|
|
6468
|
-
let outer = maxRad + labelSpace;
|
|
7309
|
+
let outer = maxRad + labelSpace + domainRadialExtent();
|
|
6469
7310
|
let W = +_baseSvg.attr('width'), H = +_baseSvg.attr('height');
|
|
6470
7311
|
let scale = 0.9 * (Math.min(W, H) / (2 * outer));
|
|
6471
7312
|
if (!isFinite(scale) || scale <= 0) {
|
|
@@ -6496,7 +7337,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6496
7337
|
_root = _root_const;
|
|
6497
7338
|
_in_subtree = false;
|
|
6498
7339
|
_basicTreeProperties = forester.collectBasicTreeProperties(_root);
|
|
6499
|
-
refreshVisualizations();
|
|
7340
|
+
refreshVisualizations(false);
|
|
6500
7341
|
search0();
|
|
6501
7342
|
search1();
|
|
6502
7343
|
zoomToFit();
|
|
@@ -6535,7 +7376,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6535
7376
|
}
|
|
6536
7377
|
|
|
6537
7378
|
_basicTreeProperties = forester.collectBasicTreeProperties(_root);
|
|
6538
|
-
refreshVisualizations();
|
|
7379
|
+
refreshVisualizations(false);
|
|
6539
7380
|
search0();
|
|
6540
7381
|
search1();
|
|
6541
7382
|
zoomToFit();
|
|
@@ -6608,7 +7449,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6608
7449
|
_radialLabelsHorizontal = false;
|
|
6609
7450
|
_msaColOffset = 0;
|
|
6610
7451
|
syncZoomRowButtons();
|
|
6611
|
-
refreshVisualizations();
|
|
7452
|
+
refreshVisualizations(false);
|
|
6612
7453
|
// Esc resets to the launch state -- the auto-applied colour, when its
|
|
6613
7454
|
// field still exists in what remains of the tree.
|
|
6614
7455
|
if (_vis) {
|
|
@@ -6763,12 +7604,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
6763
7604
|
|
|
6764
7605
|
// Read a search box's current field / mode / value(s) into a spec.
|
|
6765
7606
|
function currentSearchSpec(idx) {
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
}
|
|
6771
|
-
if (!field) field = _searchFields[0] || { key: 'ANY', label: 'Any Text', numeric: false };
|
|
7607
|
+
// the field menu's option values index _searchFields; before a tree
|
|
7608
|
+
// is loaded there is only Any Text
|
|
7609
|
+
let field = _searchFields[Number(getValue(idx === 0 ? SEARCH_FIELD_SELECT_0 : SEARCH_FIELD_SELECT_1))]
|
|
7610
|
+
|| _searchFields[0] || forester.availableSearchFields(null)[0];
|
|
6772
7611
|
let mode = getValue(idx === 0 ? SEARCH_MODE_SELECT_0 : SEARCH_MODE_SELECT_1);
|
|
6773
7612
|
if (!mode) mode = field.numeric ? 'eq' : 'contains';
|
|
6774
7613
|
return {
|
|
@@ -6793,20 +7632,23 @@ function (root, d3, forester, phyloXml) {
|
|
|
6793
7632
|
// Fill both search-field dropdowns from the loaded tree's available fields,
|
|
6794
7633
|
// then set up each box's mode menu. Called when a tree is (re)loaded.
|
|
6795
7634
|
function populateSearchMenus() {
|
|
7635
|
+
let before = _searchFields;
|
|
6796
7636
|
_searchFields = forester.availableSearchFields(_root);
|
|
6797
7637
|
[SEARCH_FIELD_SELECT_0, SEARCH_FIELD_SELECT_1].forEach(function (selId) {
|
|
6798
7638
|
let sel = byId(selId);
|
|
6799
7639
|
if (!sel) return;
|
|
6800
|
-
|
|
7640
|
+
// a box keeps its field when the new tree still offers it (labels
|
|
7641
|
+
// are unique: a property's label is its ref, which has a colon)
|
|
7642
|
+
let prev = before[Number(sel.value)];
|
|
7643
|
+
let keep = prev ? _searchFields.findIndex(f => f.label === prev.label) : -1;
|
|
6801
7644
|
sel.innerHTML = '';
|
|
6802
7645
|
for (let i = 0; i < _searchFields.length; ++i) {
|
|
6803
7646
|
let opt = document.createElement('option');
|
|
6804
|
-
opt.value =
|
|
7647
|
+
opt.value = String(i);
|
|
6805
7648
|
opt.textContent = _searchFields[i].label;
|
|
6806
7649
|
sel.appendChild(opt);
|
|
6807
7650
|
}
|
|
6808
|
-
|
|
6809
|
-
else sel.value = 'ANY';
|
|
7651
|
+
sel.value = String(keep >= 0 ? keep : 0);
|
|
6810
7652
|
});
|
|
6811
7653
|
populateSearchModeMenu(0);
|
|
6812
7654
|
populateSearchModeMenu(1);
|
|
@@ -6861,7 +7703,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
6861
7703
|
let input = byId(idx === 0 ? SEARCH_FIELD_0 : SEARCH_FIELD_1);
|
|
6862
7704
|
if (!dl || !input) return;
|
|
6863
7705
|
let spec = currentSearchSpec(idx);
|
|
6864
|
-
let enable = !spec.field.numeric && spec.field.
|
|
7706
|
+
let enable = !spec.field.numeric && spec.field.suggest !== false && spec.mode !== 'regex';
|
|
6865
7707
|
dl.innerHTML = '';
|
|
6866
7708
|
if (!enable) { input.removeAttribute('list'); return; }
|
|
6867
7709
|
let vals = forester.distinctSearchValues(_root, spec.field, SEARCH_AUTOCOMPLETE_CAP);
|
|
@@ -6898,6 +7740,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
6898
7740
|
function layoutButtonClicked() {
|
|
6899
7741
|
_state.circularDisplay = getCheckboxValue(LAYOUT_CIRC_BUTTON);
|
|
6900
7742
|
_state.unrootedDisplay = getCheckboxValue(LAYOUT_UNROOTED_BUTTON);
|
|
7743
|
+
if (radialDisplay() && _state.showDomainArchitectures && _basicTreeProperties.domainArchitectures) {
|
|
7744
|
+
// the domain boxes ride the tips' spokes, so the labels must too
|
|
7745
|
+
_radialLabelsHorizontal = false;
|
|
7746
|
+
}
|
|
6901
7747
|
syncZoomRowButtons();
|
|
6902
7748
|
zoomToFit();
|
|
6903
7749
|
}
|
|
@@ -7744,9 +8590,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
7744
8590
|
// The "working" card shown over the tree area while a big tree is
|
|
7745
8591
|
// drawn. It covers the whole container so nothing underneath --
|
|
7746
8592
|
// panel included -- can be clicked into a half-built view.
|
|
8593
|
+
// cursor stays default on purpose: `progress` draws the macOS
|
|
8594
|
+
// beach ball next to the pointer, which reads as "the program has
|
|
8595
|
+
// hung" when the card is already saying exactly what is happening
|
|
8596
|
+
// and for how many nodes. One clear signal, not two.
|
|
7747
8597
|
+ '.aptx-busy { position:absolute; inset:0; z-index:1000; display:grid; place-items:center;'
|
|
7748
8598
|
+ ' font-family:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;'
|
|
7749
|
-
+ ' font-size:12px; color:var(--p-ink); cursor:
|
|
8599
|
+
+ ' font-size:12px; color:var(--p-ink); cursor:default; }'
|
|
7750
8600
|
+ '.aptx-busy-card { display:flex; align-items:center; gap:10px; padding:10px 16px 10px 13px;'
|
|
7751
8601
|
+ ' border:1px solid var(--p-line-strong); border-radius:12px; background:var(--p-bg);'
|
|
7752
8602
|
+ ' box-shadow:0 24px 48px -16px rgba(23,34,46,0.45),0 4px 12px -4px rgba(23,34,46,0.22); }'
|
|
@@ -7938,6 +8788,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
7938
8788
|
+ '.aptx-panel .aptx-zoomrow { display:flex; }'
|
|
7939
8789
|
+ '.aptx-panel .aptx-zoomrow .aptx-gbtn { flex:1 1 0; padding:0; }'
|
|
7940
8790
|
+ '.aptx-panel .aptx-zoomrow .aptx-gbtn:last-child { margin-right:0; }'
|
|
8791
|
+
+ '.aptx-panel .aptx-domrow { display:flex; align-items:center; gap:4px; margin:3px 0; }'
|
|
8792
|
+
+ '.aptx-panel .aptx-domrow .aptx-domlabel { flex:0 0 64px; font-size:10px; color:var(--p-muted); }'
|
|
8793
|
+
+ '.aptx-panel .aptx-domrow input[type=button] { width:30px; padding:0; margin:0; }'
|
|
8794
|
+
+ '.aptx-panel .aptx-domrow select { flex:1 1 auto; min-width:0; }'
|
|
8795
|
+
+ '.aptx-panel .aptx-domreadout { flex:1 1 auto; text-align:center; font-size:12px; font-variant-numeric:tabular-nums; color:var(--p-ink); }'
|
|
7941
8796
|
+ '.aptx-panel .aptx-glyph { height:14px; width:auto; display:block; overflow:visible; }'
|
|
7942
8797
|
+ '.aptx-panel .aptx-seg .aptx-glyph { height:13px; }'
|
|
7943
8798
|
+ '.aptx-panel input[type=text],.aptx-panel select { font-family:inherit; font-size:11px; color:var(--p-ink); background:var(--p-surface2); border:1px solid var(--p-line-strong); border-radius:6px; max-width:100%; padding:3px 6px; }'
|
|
@@ -8259,6 +9114,32 @@ function (root, d3, forester, phyloXml) {
|
|
|
8259
9114
|
if (s.type) {
|
|
8260
9115
|
text += '- Type: ' + s.type + '<br>';
|
|
8261
9116
|
}
|
|
9117
|
+
// The domain architecture, which phyloXML has always carried
|
|
9118
|
+
// and this dialog has never shown: a reader could find a node
|
|
9119
|
+
// by searching for one of its domains -- forester indexes them
|
|
9120
|
+
// as the "Domain" search field -- and then see nothing about it
|
|
9121
|
+
// here. Listed in POSITION order rather than file order, since
|
|
9122
|
+
// an architecture is a line along the protein and reading it
|
|
9123
|
+
// out of order tells you nothing.
|
|
9124
|
+
let da = s.domain_architecture;
|
|
9125
|
+
if (da && da.domains && da.domains.length > 0) {
|
|
9126
|
+
text += '- Domain architecture: '
|
|
9127
|
+
+ (da.length ? da.length + ' aa, ' : '')
|
|
9128
|
+
+ da.domains.length + (da.domains.length === 1 ? ' domain' : ' domains')
|
|
9129
|
+
+ '<br>';
|
|
9130
|
+
da.domains.slice().sort(function (a, b) {
|
|
9131
|
+
return (a.from - b.from) || (a.to - b.to);
|
|
9132
|
+
}).forEach(function (dom) {
|
|
9133
|
+
let name = dom.name || '(unnamed)';
|
|
9134
|
+
let span = (dom.from !== undefined && dom.to !== undefined)
|
|
9135
|
+
? dom.from + '-' + dom.to : '';
|
|
9136
|
+
// the confidence on a domain is its e-value; an
|
|
9137
|
+
// explicit 0 is a real score, so test for undefined
|
|
9138
|
+
let ev = (dom.confidence !== undefined && dom.confidence !== null)
|
|
9139
|
+
? ' (E=' + dom.confidence + ')' : '';
|
|
9140
|
+
text += '-- ' + name + ': ' + span + ev + '<br>';
|
|
9141
|
+
});
|
|
9142
|
+
}
|
|
8262
9143
|
}
|
|
8263
9144
|
}
|
|
8264
9145
|
if (d.events) {
|
|
@@ -8668,6 +9549,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
8668
9549
|
|
|
8669
9550
|
c0.insertAdjacentHTML('beforeend',makeDisplayControl());
|
|
8670
9551
|
|
|
9552
|
+
c0.insertAdjacentHTML('beforeend',makeDomainControls());
|
|
9553
|
+
|
|
8671
9554
|
c0.insertAdjacentHTML('beforeend',makeZoomControl());
|
|
8672
9555
|
|
|
8673
9556
|
|
|
@@ -8775,6 +9658,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
8775
9658
|
|
|
8776
9659
|
on(DYNAHIDE_CB, 'click', dynaHideCbClicked);
|
|
8777
9660
|
on(MSA_CB, 'click', msaCbClicked);
|
|
9661
|
+
on(DOMAINS_CB, 'click', domainsCbClicked);
|
|
9662
|
+
onHoldRepeat(DOMAIN_WIDTH_DEC, function () { domainWidthStep(false); });
|
|
9663
|
+
onHoldRepeat(DOMAIN_WIDTH_INC, function () { domainWidthStep(true); });
|
|
9664
|
+
on(DOMAIN_EVALUE_DEC, 'click', function () { domainEvalueStep(-1); });
|
|
9665
|
+
on(DOMAIN_EVALUE_INC, 'click', function () { domainEvalueStep(1); });
|
|
9666
|
+
on(DOMAIN_LABELS_SELECT, 'change', domainLabelsChanged);
|
|
9667
|
+
on(DOMAIN_GLOW_CB, 'click', domainGlowCbClicked);
|
|
8778
9668
|
on(TIME_AXIS_CB, 'click', timeAxisCbClicked);
|
|
8779
9669
|
on(TIME_GRID_CB, 'click', timeGridCbClicked);
|
|
8780
9670
|
|
|
@@ -9242,6 +10132,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
9242
10132
|
if (_basicTreeProperties.alignedMolSeqs && _basicTreeProperties.maxMolSeqLength > 0) {
|
|
9243
10133
|
opts.push(makeCheckboxItem('Alignment', MSA_CB, 'to show/hide the sequence alignment beside the tree (rectangular layout only)'));
|
|
9244
10134
|
}
|
|
10135
|
+
if (_basicTreeProperties.domainArchitectures) {
|
|
10136
|
+
opts.push(makeCheckboxItem('Domain Architectures', DOMAINS_CB, 'to show/hide the protein domain architectures beside the tips', true));
|
|
10137
|
+
}
|
|
9245
10138
|
if (_timeInfo && _timeInfo.type) {
|
|
9246
10139
|
opts.push(makeCheckboxItem('Time Axis', TIME_AXIS_CB, 'to show/hide the '
|
|
9247
10140
|
+ (_timeInfo.type === 'geologic' ? 'geologic (ICS) time axis' : 'calendar time axis')
|
|
@@ -9319,6 +10212,35 @@ function (root, d3, forester, phyloXml) {
|
|
|
9319
10212
|
return h;
|
|
9320
10213
|
}
|
|
9321
10214
|
|
|
10215
|
+
// The desktop's domain controls, shown only while the Domain
|
|
10216
|
+
// Architectures toggle is on (syncDomainControls): the track width,
|
|
10217
|
+
// the E-value threshold with its readout, the label mode, the glow.
|
|
10218
|
+
function makeDomainControls() {
|
|
10219
|
+
let h = '<fieldset id="' + DOMAIN_CONTROLS + '" style="display:none">';
|
|
10220
|
+
h = h.concat('<legend>Domain Architectures</legend>');
|
|
10221
|
+
h = h.concat('<div class="aptx-domrow"><span class="aptx-domlabel">Track width</span>');
|
|
10222
|
+
h = h.concat(makeButton('−', DOMAIN_WIDTH_DEC, 'narrow the domain tracks (hold to repeat)'));
|
|
10223
|
+
h = h.concat(makeButton('+', DOMAIN_WIDTH_INC, 'widen the domain tracks (hold to repeat)'));
|
|
10224
|
+
h = h.concat('</div>');
|
|
10225
|
+
h = h.concat('<div class="aptx-domrow"><span class="aptx-domlabel">E-value ≤</span>');
|
|
10226
|
+
h = h.concat(makeButton('−', DOMAIN_EVALUE_DEC, 'Decrease the E-value threshold by a factor of 10'));
|
|
10227
|
+
h = h.concat('<span class="aptx-domreadout" id="' + DOMAIN_EVALUE_READOUT
|
|
10228
|
+
+ '" title="domains with an E-value up to this are drawn"></span>');
|
|
10229
|
+
h = h.concat(makeButton('+', DOMAIN_EVALUE_INC, 'Increase the E-value threshold by a factor of 10'));
|
|
10230
|
+
h = h.concat('</div>');
|
|
10231
|
+
h = h.concat('<div class="aptx-domrow"><label class="aptx-domlabel" for="' + DOMAIN_LABELS_SELECT + '">Labels</label>');
|
|
10232
|
+
h = h.concat('<select name="' + DOMAIN_LABELS_SELECT + '" id="' + DOMAIN_LABELS_SELECT
|
|
10233
|
+
+ '" title="where the domain names go: on the boxes (rectangular layout only), in a legend, or nowhere">');
|
|
10234
|
+
h = h.concat('<option value="domains">On domains</option>');
|
|
10235
|
+
h = h.concat('<option value="legend">Legend</option>');
|
|
10236
|
+
h = h.concat('<option value="none">None</option>');
|
|
10237
|
+
h = h.concat('</select></div>');
|
|
10238
|
+
h = h.concat('<div class="aptx-checkgrid">'
|
|
10239
|
+
+ makeCheckboxItem('Glow', DOMAIN_GLOW_CB, 'a soft glow in each domain\'s own colour around its box') + '</div>');
|
|
10240
|
+
h = h.concat('</fieldset>');
|
|
10241
|
+
return h;
|
|
10242
|
+
}
|
|
10243
|
+
|
|
9322
10244
|
function makeSliders() {
|
|
9323
10245
|
let h = "";
|
|
9324
10246
|
h = h.concat('<fieldset>');
|
|
@@ -9500,6 +10422,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9500
10422
|
setCheckboxValue(VIS_CB, _state.showVisualizations);
|
|
9501
10423
|
setCheckboxValue(DYNAHIDE_CB, _state.dynahide);
|
|
9502
10424
|
setCheckboxValue(MSA_CB, _state.showMsa);
|
|
10425
|
+
syncDomainControls();
|
|
9503
10426
|
setCheckboxValue(TIME_AXIS_CB, _state.showTimeAxis);
|
|
9504
10427
|
setCheckboxValue(TIME_GRID_CB, _state.timeAxisGrid);
|
|
9505
10428
|
setCheckboxValue(SHORTEN_NODE_NAME_CB, _state.shortenNodeNames);
|