archaeopteryx 3.11.0 → 3.13.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 +92 -11
- package/archaeopteryx.d.ts +6 -0
- package/archaeopteryx.js +257 -83
- package/forester.js +433 -82
- 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.13.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// Archaeopteryx.js is a software tool for the visualization and
|
|
@@ -103,7 +103,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
103
103
|
// IIFE's own function name -- a plain object says what it is.)
|
|
104
104
|
let archaeopteryx = {};
|
|
105
105
|
|
|
106
|
-
const VERSION = '3.
|
|
106
|
+
const VERSION = '3.13.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';
|
|
@@ -363,6 +363,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
363
363
|
const DOWNLOAD_BUTTON = 'dl_b';
|
|
364
364
|
const DYNAHIDE_CB = 'dynahide_cb';
|
|
365
365
|
const MSA_CB = 'msa_cb';
|
|
366
|
+
const MSA_LOGO_CB = 'msa_logo_cb';
|
|
366
367
|
const HEATMAP_CB = 'heatmap_cb';
|
|
367
368
|
const HEATMAP_CONTROLS = 'heatmap_controls';
|
|
368
369
|
const HEATMAP_ORDER_SELECT = 'heatmap_order';
|
|
@@ -422,8 +423,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
422
423
|
const RETURN_TO_SUPERTREE_BUTTON_BY_ONE = 'ret1_b';
|
|
423
424
|
const SEARCH_FIELD_0 = 'sf0';
|
|
424
425
|
const SEARCH_FIELD_1 = 'sf1';
|
|
425
|
-
const SEARCH_B_WRAP = 'search_b_wrap';
|
|
426
|
-
const SEARCH_B_TOGGLE = 'search_b_tgl';
|
|
427
426
|
const SEARCH_NAV_ROW = 'searchnavrow';
|
|
428
427
|
const SEARCH_NAV_PREV = 'searchnavprev';
|
|
429
428
|
const SEARCH_NAV_NEXT = 'searchnavnext';
|
|
@@ -547,7 +546,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
547
546
|
const MSA_MAX_VIEWPORT_FRACTION = 0.6;// nor grows past this share of the display
|
|
548
547
|
const MSA_CONS_BAR_H = 22; // conservation bar band height
|
|
549
548
|
const MSA_CONS_TOP_GAP = 4;
|
|
550
|
-
const
|
|
549
|
+
const MSA_CONS_ROW_H = 13; // the consensus letter row
|
|
550
|
+
const MSA_RULER_H = 17; // the column ruler under it
|
|
551
|
+
const MSA_LOGO_H = 46; // the sequence logo's stack, full height
|
|
552
|
+
const MSA_LOGO_CAP_H = 11; // ... and the caption line above it
|
|
553
|
+
const MSA_LOGO_FONT_PX = 24; // the size logo glyphs are measured at, then scaled
|
|
551
554
|
const MSA_NAV_RESERVE = 46; // ... plus the navigation bar floating at the viewport bottom
|
|
552
555
|
const MSA_MIN_TREE_PX = 220; // the tree itself never shrinks below this
|
|
553
556
|
let _msaColOffset = 0; // first shown alignment column (0-based)
|
|
@@ -4083,8 +4086,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
4083
4086
|
};
|
|
4084
4087
|
|
|
4085
4088
|
|
|
4089
|
+
// A branch length of ZERO is a length and gets a label. Testing the
|
|
4090
|
+
// number for truthiness read 0 as "no branch length", so the one branch
|
|
4091
|
+
// whose length is worth pointing out -- two identical sequences, a
|
|
4092
|
+
// multifurcation written out as a run of zero-length branches -- was the
|
|
4093
|
+
// one drawn bare. Same mistake the phyloXML writer was making until
|
|
4094
|
+
// phyloxml 1.1.1; the desktop cannot make it at all, because it marks an
|
|
4095
|
+
// absent length with a sentinel (-1024) rather than with zero.
|
|
4086
4096
|
let makeBranchLengthLabel = function (phynode) {
|
|
4087
|
-
if (phynode.branch_length) {
|
|
4097
|
+
if (phynode.branch_length !== undefined && phynode.branch_length !== null) {
|
|
4088
4098
|
if (_state.phylogram && _state.minBranchLengthValueToShow && phynode.branch_length < _state.minBranchLengthValueToShow) {
|
|
4089
4099
|
return;
|
|
4090
4100
|
}
|
|
@@ -4112,7 +4122,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
4112
4122
|
if (_state.showConfidenceValues && support.length > 0
|
|
4113
4123
|
&& (!_state.minConfidenceValueToShow || support.some(function (c) { return c.value >= _state.minConfidenceValueToShow; }))) {
|
|
4114
4124
|
support.forEach(function (c) {
|
|
4115
|
-
|
|
4125
|
+
// a support of 0 is a support value, and a telling one: the
|
|
4126
|
+
// MAD branch above already tested it this way
|
|
4127
|
+
if (typeof c.value === 'number' && isFinite(c.value)) {
|
|
4116
4128
|
parts.push(+c.value.toFixed(CONFIDENCE_VALUE_DIGITS_DEFAULT));
|
|
4117
4129
|
}
|
|
4118
4130
|
});
|
|
@@ -4927,6 +4939,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4927
4939
|
const STATE_KEYS = [
|
|
4928
4940
|
'layout',
|
|
4929
4941
|
'showMsa',
|
|
4942
|
+
'showMsaLogo',
|
|
4930
4943
|
'showHeatmap',
|
|
4931
4944
|
'heatmapColumnOrder',
|
|
4932
4945
|
'heatmapManualOrder',
|
|
@@ -5169,6 +5182,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
5169
5182
|
if (_state.showHeatmap === undefined) {
|
|
5170
5183
|
_state.showHeatmap = false;
|
|
5171
5184
|
}
|
|
5185
|
+
// The logo is a summary of the alignment, not a second view of the
|
|
5186
|
+
// tree, so it waits to be asked for: an alignment opens showing its
|
|
5187
|
+
// residues, which is what a reader came for.
|
|
5188
|
+
if (_state.showMsaLogo === undefined) {
|
|
5189
|
+
_state.showMsaLogo = false;
|
|
5190
|
+
}
|
|
5172
5191
|
// The columns' order. Left unset it is resolved from the data on first
|
|
5173
5192
|
// use (heatmapMode / forester.heatmapDefaultOrder): a clustered order,
|
|
5174
5193
|
// because reading block structure is what a heat map beside a tree is
|
|
@@ -6824,6 +6843,64 @@ function (root, d3, forester, phyloXml) {
|
|
|
6824
6843
|
&& _basicTreeProperties.maxMolSeqLength > 0;
|
|
6825
6844
|
}
|
|
6826
6845
|
|
|
6846
|
+
// The logo replaces the conservation bar and the consensus row rather
|
|
6847
|
+
// than joining them: a stack's height IS the column's conservation and its
|
|
6848
|
+
// top letter IS the consensus, so drawing all three would say the same
|
|
6849
|
+
// thing three times and take three times the room.
|
|
6850
|
+
function msaLogoShown() {
|
|
6851
|
+
return _state.showMsaLogo === true && msaShown();
|
|
6852
|
+
}
|
|
6853
|
+
|
|
6854
|
+
// What the strip under the alignment needs. The conservation form is the
|
|
6855
|
+
// 56 px this was a constant for; the logo form is taller and drops the
|
|
6856
|
+
// consensus row it subsumes.
|
|
6857
|
+
function msaBottomReserve() {
|
|
6858
|
+
return MSA_CONS_TOP_GAP
|
|
6859
|
+
+ (msaLogoShown() ? MSA_LOGO_CAP_H + MSA_LOGO_H : MSA_CONS_BAR_H + MSA_CONS_ROW_H)
|
|
6860
|
+
+ MSA_RULER_H;
|
|
6861
|
+
}
|
|
6862
|
+
|
|
6863
|
+
// Logo letters are drawn at a fixed size and then scaled to their cell and
|
|
6864
|
+
// to their share of the stack, so what is needed is how much ink this face
|
|
6865
|
+
// actually puts on the page for THIS letter -- measured, not assumed: a
|
|
6866
|
+
// 0.6 advance and a 0.72 cap height are true of some monospace faces and
|
|
6867
|
+
// not others, and a letter scaled by the wrong constant overflows its
|
|
6868
|
+
// column.
|
|
6869
|
+
//
|
|
6870
|
+
// Per letter, because a logo's letters must each fill their slice exactly:
|
|
6871
|
+
// measured off one glyph, Q and G hung their descenders through the
|
|
6872
|
+
// baseline and across the ruler, and every letter without a descender sat
|
|
6873
|
+
// a little short. What is scaled to the slice is the glyph's INK BOX
|
|
6874
|
+
// (ascent + descent), and the baseline is then placed so that box lands
|
|
6875
|
+
// inside the slice.
|
|
6876
|
+
let _logoGlyphs = {};
|
|
6877
|
+
|
|
6878
|
+
function logoGlyphMetrics(ch) {
|
|
6879
|
+
if (_logoGlyphs[ch]) {
|
|
6880
|
+
return _logoGlyphs[ch];
|
|
6881
|
+
}
|
|
6882
|
+
if (!_legendMeasureCtx) {
|
|
6883
|
+
_legendMeasureCtx = document.createElement('canvas').getContext('2d');
|
|
6884
|
+
}
|
|
6885
|
+
_legendMeasureCtx.font = MSA_LOGO_FONT_PX + 'px monospace';
|
|
6886
|
+
let m = _legendMeasureCtx.measureText(ch);
|
|
6887
|
+
let ascent = m.actualBoundingBoxAscent;
|
|
6888
|
+
let descent = m.actualBoundingBoxDescent;
|
|
6889
|
+
if (!(ascent > 0)) {
|
|
6890
|
+
ascent = MSA_LOGO_FONT_PX * 0.72; // a face that will not say
|
|
6891
|
+
descent = 0;
|
|
6892
|
+
}
|
|
6893
|
+
if (!(descent > 0)) {
|
|
6894
|
+
descent = 0; // no descender, or none reported
|
|
6895
|
+
}
|
|
6896
|
+
_logoGlyphs[ch] = {
|
|
6897
|
+
width: m.width || (MSA_LOGO_FONT_PX * 0.6),
|
|
6898
|
+
ascent: ascent,
|
|
6899
|
+
descent: descent
|
|
6900
|
+
};
|
|
6901
|
+
return _logoGlyphs[ch];
|
|
6902
|
+
}
|
|
6903
|
+
|
|
6827
6904
|
function msaRowSeq(d) {
|
|
6828
6905
|
if (d.sequences && d.sequences.length > 0) {
|
|
6829
6906
|
let s = d.sequences[0];
|
|
@@ -7014,12 +7091,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
7014
7091
|
.attr('stroke', ink).attr('stroke-opacity', 0.7).attr('stroke-width', 1);
|
|
7015
7092
|
}
|
|
7016
7093
|
|
|
7017
|
-
//
|
|
7018
|
-
//
|
|
7019
|
-
//
|
|
7020
|
-
|
|
7094
|
+
// Scored over the visible WINDOW only -- the whole alignment would
|
|
7095
|
+
// cost rows x total per redraw; indices below are window-relative.
|
|
7096
|
+
// The rows are the tips ON SCREEN: enter a clade and the summary is
|
|
7097
|
+
// that clade's, which is the whole point of putting it next to a tree.
|
|
7098
|
+
let windowRows = tips.map(function (t) {
|
|
7021
7099
|
return msaRowSeq(t).slice(offset, offset + visible);
|
|
7022
|
-
})
|
|
7100
|
+
});
|
|
7101
|
+
let logo = msaLogoShown() ? forester.msaLogo(windowRows, visible, isNuc) : null;
|
|
7102
|
+
let cons = logo ? null : forester.msaConservation(windowRows, visible, 'identity', isNuc);
|
|
7023
7103
|
// A faint dashed guide from each tip -- from the end of its label, or
|
|
7024
7104
|
// from the node itself when the label is hidden -- across to its row
|
|
7025
7105
|
// in the track, so a row can be read back to its sequence without
|
|
@@ -7055,34 +7135,91 @@ function (root, d3, forester, phyloXml) {
|
|
|
7055
7135
|
}
|
|
7056
7136
|
|
|
7057
7137
|
// ---- conservation, consensus, ruler: a FLOATING strip ----
|
|
7058
|
-
let
|
|
7138
|
+
let stripH = msaBottomReserve();
|
|
7139
|
+
let strip = floatStripGroup('aptx-msa-strip', _clusterH, stripH);
|
|
7059
7140
|
strip.append('rect').attr('x', Math.round(originX) - 2).attr('y', _clusterH)
|
|
7060
|
-
.attr('width', trackW + 4).attr('height',
|
|
7141
|
+
.attr('width', trackW + 4).attr('height', stripH)
|
|
7061
7142
|
.attr('fill', _state.backgroundColorDefault);
|
|
7062
7143
|
let consTop = _clusterH + MSA_CONS_TOP_GAP;
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
if (score <= 0) {
|
|
7069
|
-
continue;
|
|
7144
|
+
// A caption on the panel's own background, so it stays readable over
|
|
7145
|
+
// whatever the band happens to draw underneath it.
|
|
7146
|
+
let caption = function (txt, y) {
|
|
7147
|
+
if (trackW <= 170) {
|
|
7148
|
+
return;
|
|
7070
7149
|
}
|
|
7071
|
-
let
|
|
7072
|
-
let
|
|
7073
|
-
let
|
|
7074
|
-
strip.append('rect').attr('x',
|
|
7075
|
-
.attr('width',
|
|
7076
|
-
.attr('fill',
|
|
7077
|
-
|
|
7078
|
-
if (trackW > 170) {
|
|
7079
|
-
strip.append('text').attr('x', Math.round(originX) + trackW - 3).attr('y', consTop + 9)
|
|
7150
|
+
let font = '8px ' + FONT_DEFAULTS;
|
|
7151
|
+
let w = legendTextWidth(txt, font) + 6;
|
|
7152
|
+
let xe = Math.round(originX) + trackW - 3;
|
|
7153
|
+
strip.append('rect').attr('x', xe - w).attr('y', y - 8)
|
|
7154
|
+
.attr('width', w).attr('height', 11)
|
|
7155
|
+
.attr('fill', _state.backgroundColorDefault).attr('fill-opacity', 0.85);
|
|
7156
|
+
strip.append('text').attr('x', xe - 3).attr('y', y)
|
|
7080
7157
|
.attr('text-anchor', 'end')
|
|
7081
7158
|
.style('font-size', '8px').style('fill', ink).style('fill-opacity', 0.9)
|
|
7082
|
-
.text(
|
|
7083
|
-
}
|
|
7084
|
-
|
|
7085
|
-
|
|
7159
|
+
.text(txt);
|
|
7160
|
+
};
|
|
7161
|
+
if (logo) {
|
|
7162
|
+
// Each column a stack of letters: total height the column's
|
|
7163
|
+
// information content in bits, each letter's share its frequency.
|
|
7164
|
+
// Least frequent at the BOTTOM, so the eye reads the consensus
|
|
7165
|
+
// off the top of the stack.
|
|
7166
|
+
//
|
|
7167
|
+
// The caption gets a line of its own above the stacks (hence the
|
|
7168
|
+
// CAP_H here). Laid over them it washed out the very letters it
|
|
7169
|
+
// was labelling, which is a poor trade for eleven pixels.
|
|
7170
|
+
let floor = consTop + MSA_LOGO_CAP_H + MSA_LOGO_H;
|
|
7171
|
+
strip.append('line')
|
|
7172
|
+
.attr('x1', Math.round(originX)).attr('x2', Math.round(originX) + trackW)
|
|
7173
|
+
.attr('y1', floor).attr('y2', floor)
|
|
7174
|
+
.attr('stroke', ink).attr('stroke-opacity', 0.5).attr('stroke-width', 1);
|
|
7175
|
+
for (let i = 0; i < visible; ++i) {
|
|
7176
|
+
let col = logo.columns[i];
|
|
7177
|
+
if (!col || !(col.height > 0)) {
|
|
7178
|
+
continue;
|
|
7179
|
+
}
|
|
7180
|
+
let x0 = Math.round(originX + (i * cw));
|
|
7181
|
+
let x1 = Math.round(originX + ((i + 1) * cw));
|
|
7182
|
+
let y = floor;
|
|
7183
|
+
for (let k = col.letters.length - 1; k >= 0; --k) {
|
|
7184
|
+
let letter = col.letters[k];
|
|
7185
|
+
let hpx = (letter.h / logo.maxBits) * MSA_LOGO_H;
|
|
7186
|
+
if (hpx < 0.5) {
|
|
7187
|
+
continue; // under half a pixel of ink: nothing to draw
|
|
7188
|
+
}
|
|
7189
|
+
let glyph = logoGlyphMetrics(letter.ch);
|
|
7190
|
+
let sy = hpx / (glyph.ascent + glyph.descent);
|
|
7191
|
+
let rgb = forester.msaResidueRgb(letter.ch, isNuc);
|
|
7192
|
+
strip.append('text')
|
|
7193
|
+
.attr('transform', 'translate(' + (x0 + ((x1 - x0) / 2)) + ','
|
|
7194
|
+
+ (y - (glyph.descent * sy)) + ')'
|
|
7195
|
+
+ ' scale(' + (Math.max(1, x1 - x0) / glyph.width) + ',' + sy + ')')
|
|
7196
|
+
.attr('text-anchor', 'middle')
|
|
7197
|
+
.style('font-family', 'monospace')
|
|
7198
|
+
.style('font-size', MSA_LOGO_FONT_PX + 'px')
|
|
7199
|
+
.style('fill', rgb === null ? ink : 'rgb(' + rgb.join(',') + ')')
|
|
7200
|
+
.text(letter.ch);
|
|
7201
|
+
y -= hpx;
|
|
7202
|
+
}
|
|
7203
|
+
}
|
|
7204
|
+
caption('Sequence logo \u2014 0 to ' + logo.maxBits.toFixed(1)
|
|
7205
|
+
+ ' bits (n = ' + n + ')', consTop + 8);
|
|
7206
|
+
} else {
|
|
7207
|
+
strip.append('rect').attr('x', Math.round(originX)).attr('y', consTop)
|
|
7208
|
+
.attr('width', trackW).attr('height', MSA_CONS_BAR_H)
|
|
7209
|
+
.attr('fill', ink).attr('fill-opacity', 0.08);
|
|
7210
|
+
for (let i = 0; i < visible; ++i) {
|
|
7211
|
+
let score = cons.scores[i] || 0;
|
|
7212
|
+
if (score <= 0) {
|
|
7213
|
+
continue;
|
|
7214
|
+
}
|
|
7215
|
+
let bh = Math.max(1, Math.round(score * MSA_CONS_BAR_H));
|
|
7216
|
+
let x0 = Math.round(originX + (i * cw));
|
|
7217
|
+
let x1 = Math.round(originX + ((i + 1) * cw));
|
|
7218
|
+
strip.append('rect').attr('x', x0).attr('y', (consTop + MSA_CONS_BAR_H) - bh)
|
|
7219
|
+
.attr('width', Math.max(1, x1 - x0)).attr('height', bh)
|
|
7220
|
+
.attr('fill', ink).attr('fill-opacity', 0.7);
|
|
7221
|
+
}
|
|
7222
|
+
caption('Consensus identity (n = ' + n + ')', consTop + 9);
|
|
7086
7223
|
for (let i = 0; i < visible; ++i) {
|
|
7087
7224
|
let cc = cons.consensus[i];
|
|
7088
7225
|
if (!cc) {
|
|
@@ -7099,7 +7236,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7099
7236
|
|
|
7100
7237
|
// the 1-based column ruler: absolute column numbers at nice steps,
|
|
7101
7238
|
// and always the first and last column when their edge is in view
|
|
7102
|
-
let rulerY = consTop +
|
|
7239
|
+
let rulerY = consTop + (logo ? MSA_LOGO_CAP_H + MSA_LOGO_H : MSA_CONS_BAR_H + MSA_CONS_ROW_H);
|
|
7103
7240
|
strip.append('line').attr('x1', Math.round(originX)).attr('x2', Math.round(originX) + trackW)
|
|
7104
7241
|
.attr('y1', rulerY).attr('y2', rulerY)
|
|
7105
7242
|
.attr('stroke', ink).attr('stroke-opacity', 0.8).attr('stroke-width', 1);
|
|
@@ -7228,8 +7365,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
7228
7365
|
msaScrollTo(Infinity);
|
|
7229
7366
|
});
|
|
7230
7367
|
|
|
7231
|
-
// "column [ 1234 ]
|
|
7232
|
-
// the window and takes a column to jump to; the rest reads back
|
|
7368
|
+
// "column [ 1234 ] 1357 of 30,000": the box is the left edge of
|
|
7369
|
+
// the window and takes a column to jump to; the rest reads back.
|
|
7370
|
+
// No dash between them -- the box's own border already separates
|
|
7371
|
+
// the two numbers, and a dash sitting against it read as a minus
|
|
7372
|
+
// sign on the number after it.
|
|
7233
7373
|
let read = document.createElement('span');
|
|
7234
7374
|
read.className = 'aptx-msa-nav-read';
|
|
7235
7375
|
let lbl = document.createElement('span');
|
|
@@ -7279,7 +7419,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7279
7419
|
_msaNav._jump.value = String(offset + 1);
|
|
7280
7420
|
}
|
|
7281
7421
|
_msaNav._jump.max = String(total);
|
|
7282
|
-
_msaNav._tail.textContent =
|
|
7422
|
+
_msaNav._tail.textContent = (offset + visible).toLocaleString()
|
|
7283
7423
|
+ ' of ' + total.toLocaleString();
|
|
7284
7424
|
let atStart = offset <= 0;
|
|
7285
7425
|
let atEnd = offset >= total - visible;
|
|
@@ -7357,6 +7497,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
7357
7497
|
scheduleUpdate(null, 0);
|
|
7358
7498
|
}
|
|
7359
7499
|
|
|
7500
|
+
function msaLogoCbClicked() {
|
|
7501
|
+
_state.showMsaLogo = getCheckboxValue(MSA_LOGO_CB);
|
|
7502
|
+
scheduleUpdate(null, 0);
|
|
7503
|
+
}
|
|
7504
|
+
|
|
7360
7505
|
// ===================== Heat map =====================
|
|
7361
7506
|
// The desktop's MATRIX annotation columns: one cell per (tip, numeric
|
|
7362
7507
|
// per-tip property) to the right of the labels, every column painted on
|
|
@@ -8930,6 +9075,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8930
9075
|
}
|
|
8931
9076
|
if (_basicTreeProperties.alignedMolSeqs === true && _basicTreeProperties.maxMolSeqLength > 0) {
|
|
8932
9077
|
s.msa = _state.showMsa === true;
|
|
9078
|
+
if (_state.showMsaLogo === true) {
|
|
9079
|
+
s.msaLogo = true;
|
|
9080
|
+
}
|
|
8933
9081
|
}
|
|
8934
9082
|
if (heatmapAvailable()) {
|
|
8935
9083
|
s.heatmap = _state.showHeatmap === true;
|
|
@@ -9058,6 +9206,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
9058
9206
|
if (typeof s.msa === 'boolean') {
|
|
9059
9207
|
_state.showMsa = s.msa;
|
|
9060
9208
|
}
|
|
9209
|
+
if (typeof s.msaLogo === 'boolean') {
|
|
9210
|
+
_state.showMsaLogo = s.msaLogo;
|
|
9211
|
+
}
|
|
9061
9212
|
if (typeof s.heatmap === 'boolean') {
|
|
9062
9213
|
_state.showHeatmap = s.heatmap;
|
|
9063
9214
|
}
|
|
@@ -9150,9 +9301,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
9150
9301
|
if (want.value) {
|
|
9151
9302
|
openPanelSection('Search'); // a view carrying a search shows it
|
|
9152
9303
|
}
|
|
9153
|
-
if (idx === 1 && want.value) {
|
|
9154
|
-
showSearchB();
|
|
9155
|
-
}
|
|
9156
9304
|
}
|
|
9157
9305
|
|
|
9158
9306
|
// The panel's controls from the state, after applyViewState
|
|
@@ -9168,6 +9316,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9168
9316
|
}
|
|
9169
9317
|
});
|
|
9170
9318
|
setCheckboxValue(MSA_CB, _state.showMsa === true);
|
|
9319
|
+
setCheckboxValue(MSA_LOGO_CB, _state.showMsaLogo === true);
|
|
9171
9320
|
setCheckboxValue(HEATMAP_CB, _state.showHeatmap === true);
|
|
9172
9321
|
syncHeatmapControls();
|
|
9173
9322
|
setCheckboxValue(TIME_AXIS_CB, _state.showTimeAxis === true);
|
|
@@ -9241,10 +9390,20 @@ function (root, d3, forester, phyloXml) {
|
|
|
9241
9390
|
// Lists are comma-joined, booleans 1 / 0, the two searches flattened
|
|
9242
9391
|
// to a / af / am / a2 and b / bf / bm / b2. Made for a URL hash, where
|
|
9243
9392
|
// none of these characters needs escaping.
|
|
9244
|
-
|
|
9393
|
+
// Every key getViewState writes has to be here, or a view survives
|
|
9394
|
+
// getViewState/applyViewState (what an embedder uses) and is silently
|
|
9395
|
+
// dropped from the URL hash (what "Copy link to this view" produces).
|
|
9396
|
+
// The heat map's four keys and the alignment logo went in exactly that
|
|
9397
|
+
// way, while the README promised a link reproduced the figure.
|
|
9398
|
+
const VIEW_TEXT_KEYS = ['layout', 'display', 'order', 'root', 'colorBy', 'shapeBy', 'domainLabels', 'combine',
|
|
9399
|
+
'heatmapOrder'];
|
|
9245
9400
|
const VIEW_INT_KEYS = ['tree', 'subtree', 'rotation', 'domainEvalue'];
|
|
9246
9401
|
const VIEW_NUMBER_KEYS = ['font', 'node', 'branch'];
|
|
9247
|
-
const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', '
|
|
9402
|
+
const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', 'msaLogo', 'heatmap', 'domains', 'domainGlow',
|
|
9403
|
+
'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
|
|
9404
|
+
// Lists of strings, comma-joined. A property ref carries ':' but never a
|
|
9405
|
+
// comma, and encodeViewValue leaves both readable in a hash.
|
|
9406
|
+
const VIEW_LIST_KEYS = ['heatmapManual'];
|
|
9248
9407
|
const VIEW_SEARCH_KEYS = [['searchA', 'a'], ['searchB', 'b']];
|
|
9249
9408
|
|
|
9250
9409
|
function encodeViewValue(v) {
|
|
@@ -9278,6 +9437,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
9278
9437
|
if (Array.isArray(state.collapsed) && state.collapsed.length > 0) {
|
|
9279
9438
|
put('collapsed', state.collapsed.join(','));
|
|
9280
9439
|
}
|
|
9440
|
+
VIEW_LIST_KEYS.forEach(function (k) {
|
|
9441
|
+
let v = state[k];
|
|
9442
|
+
if (Array.isArray(v) && v.length > 0) {
|
|
9443
|
+
parts.push(k + '=' + v.map(encodeViewValue).join(','));
|
|
9444
|
+
}
|
|
9445
|
+
});
|
|
9281
9446
|
VIEW_BOOL_KEYS.forEach(function (k) {
|
|
9282
9447
|
if (typeof state[k] === 'boolean') {
|
|
9283
9448
|
put(k, state[k] ? 1 : 0);
|
|
@@ -9344,6 +9509,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
9344
9509
|
return parseInt(x, 10);
|
|
9345
9510
|
});
|
|
9346
9511
|
}
|
|
9512
|
+
VIEW_LIST_KEYS.forEach(function (k) {
|
|
9513
|
+
if (raw[k] !== undefined) {
|
|
9514
|
+
let list = raw[k].split(',').filter(function (x) {
|
|
9515
|
+
return x.length > 0;
|
|
9516
|
+
});
|
|
9517
|
+
if (list.length > 0) {
|
|
9518
|
+
state[k] = list;
|
|
9519
|
+
}
|
|
9520
|
+
}
|
|
9521
|
+
});
|
|
9347
9522
|
VIEW_SEARCH_KEYS.forEach(function (sk) {
|
|
9348
9523
|
let p = sk[1];
|
|
9349
9524
|
if (raw[p]) {
|
|
@@ -10261,30 +10436,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
10261
10436
|
});
|
|
10262
10437
|
}
|
|
10263
10438
|
|
|
10264
|
-
// Search B starts hidden to keep the panel compact; one click (or a
|
|
10265
|
-
// configured initial value) reveals it, and it stays revealed.
|
|
10266
|
-
// Search B is folded away until wanted: the '+ Search B' link (which
|
|
10267
|
-
// focuses it) or a view that carries a second search.
|
|
10268
|
-
function showSearchB() {
|
|
10269
|
-
openPanelSection('Search');
|
|
10270
|
-
let wrap = byId(SEARCH_B_WRAP);
|
|
10271
|
-
if (wrap) {
|
|
10272
|
-
wrap.style.display = '';
|
|
10273
|
-
}
|
|
10274
|
-
let tgl = byId(SEARCH_B_TOGGLE);
|
|
10275
|
-
if (tgl) {
|
|
10276
|
-
tgl.style.display = 'none';
|
|
10277
|
-
}
|
|
10278
|
-
}
|
|
10279
|
-
|
|
10280
|
-
function revealSearchB() {
|
|
10281
|
-
showSearchB();
|
|
10282
|
-
let f = byId(SEARCH_FIELD_1);
|
|
10283
|
-
if (f) {
|
|
10284
|
-
f.focus();
|
|
10285
|
-
}
|
|
10286
|
-
}
|
|
10287
|
-
|
|
10288
10439
|
// ===================== Time axis =====================
|
|
10289
10440
|
// The desktop's time overlays, drawn beneath a rectangular PHYLOGRAM of
|
|
10290
10441
|
// a dated tree: the two-band ICS geologic axis with a "Ma before
|
|
@@ -10324,7 +10475,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10324
10475
|
// own bottom rows (conservation, consensus, ruler) must end above it
|
|
10325
10476
|
// or the bar covers them -- the old bare slider did exactly that.
|
|
10326
10477
|
// The scale bar sits in the same band, under the tree's last row.
|
|
10327
|
-
return Math.max(msaShown() ?
|
|
10478
|
+
return Math.max(msaShown() ? msaBottomReserve() + MSA_NAV_RESERVE : 0, timeAxisBottomReserve(),
|
|
10328
10479
|
heatmapBottomReserve(),
|
|
10329
10480
|
scaleBarShown() && !radialDisplay() ? SCALE_BAR_RESERVE : 0);
|
|
10330
10481
|
}
|
|
@@ -11273,8 +11424,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
11273
11424
|
}
|
|
11274
11425
|
if (_state.searchBinitialValue) {
|
|
11275
11426
|
setValue(SEARCH_FIELD_1, _state.searchBinitialValue);
|
|
11276
|
-
revealSearchB();
|
|
11277
|
-
|
|
11278
11427
|
} else {
|
|
11279
11428
|
setValue(SEARCH_FIELD_1, '');
|
|
11280
11429
|
}
|
|
@@ -11793,6 +11942,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
11793
11942
|
if (msaCb) {
|
|
11794
11943
|
msaCb.disabled = radialDisplay();
|
|
11795
11944
|
}
|
|
11945
|
+
let msaLogoCb = byId(MSA_LOGO_CB);
|
|
11946
|
+
if (msaLogoCb) {
|
|
11947
|
+
// nothing to summarize while the alignment itself is not drawn
|
|
11948
|
+
msaLogoCb.disabled = radialDisplay() || _state.showMsa !== true;
|
|
11949
|
+
}
|
|
11796
11950
|
let heatCb = byId(HEATMAP_CB);
|
|
11797
11951
|
if (heatCb) {
|
|
11798
11952
|
// circular draws the columns as rings; only unrooted cannot
|
|
@@ -12366,7 +12520,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
12366
12520
|
let open = panelSections(panel).filter(function (s) {
|
|
12367
12521
|
return !s.fieldset.classList.contains('aptx-collapsed') && s.name !== justOpened;
|
|
12368
12522
|
});
|
|
12369
|
-
|
|
12523
|
+
// Never the one just opened -- that is the one the user asked
|
|
12524
|
+
// for -- and never the last one standing. Without the second
|
|
12525
|
+
// clause a container dragged short enough empties the panel down
|
|
12526
|
+
// to a stack of legends, taking away the section being worked in.
|
|
12527
|
+
// (justOpened is itself open and excluded from this list, so when
|
|
12528
|
+
// it is set the last one standing is already protected above.)
|
|
12529
|
+
if (open.length === 0 || (!justOpened && open.length === 1)) {
|
|
12370
12530
|
return;
|
|
12371
12531
|
}
|
|
12372
12532
|
open.sort(function (a, b) {
|
|
@@ -13073,8 +13233,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
13073
13233
|
+ '.aptx-panel .aptx-slider-row { display:flex; align-items:center; gap:7px; margin:3px 0; }'
|
|
13074
13234
|
+ '.aptx-panel .aptx-slider-row label { flex:0 0 42px; font-size:10px; color:var(--p-muted); }'
|
|
13075
13235
|
+ '.aptx-panel .aptx-slider-row input[type=range] { flex:1 1 auto; min-width:0; margin:0; }'
|
|
13076
|
-
+ '.aptx-panel .aptx-linkbtn { background:none; border:0; padding:1px 0 2px; margin:0; font:inherit; font-size:10px; color:var(--p-accent); cursor:pointer; display:block; }'
|
|
13077
|
-
+ '.aptx-panel .aptx-linkbtn:hover { text-decoration:underline; }'
|
|
13078
13236
|
// one row, its buttons sharing it evenly, its edges on the zoom rows'
|
|
13079
13237
|
+ '.aptx-panel .aptx-toolrow { margin-top:5px; display:flex; gap:3px; }'
|
|
13080
13238
|
+ '.aptx-panel .aptx-toolrow .aptx-gbtn { flex:1 1 0; min-width:0; padding:0; margin:2px 0; }'
|
|
@@ -13282,6 +13440,20 @@ function (root, d3, forester, phyloXml) {
|
|
|
13282
13440
|
// spare, since the user opened none of them just now.
|
|
13283
13441
|
fitPanelSections(panel, null);
|
|
13284
13442
|
|
|
13443
|
+
// ... and again whenever the room changes. The panel is held to its
|
|
13444
|
+
// container's height, so a window dragged shorter (or a host resizing
|
|
13445
|
+
// the div around us) can push an arrangement that fitted a moment ago
|
|
13446
|
+
// into a scrollbar. Watching the CONTAINER, not the panel: folding a
|
|
13447
|
+
// section changes the panel's height and would re-enter the observer.
|
|
13448
|
+
// One-way on purpose -- growing the window back does NOT reopen what
|
|
13449
|
+
// was folded. What is open is the user's choice; only running out of
|
|
13450
|
+
// room may overrule it, and then only far enough to fit.
|
|
13451
|
+
if (typeof ResizeObserver !== 'undefined' && panel.parentElement) {
|
|
13452
|
+
new ResizeObserver(function () {
|
|
13453
|
+
fitPanelSections(panel, null);
|
|
13454
|
+
}).observe(panel.parentElement);
|
|
13455
|
+
}
|
|
13456
|
+
|
|
13285
13457
|
// Apply the current light/dark choice to this (and every) panel.
|
|
13286
13458
|
applyPanelTheme();
|
|
13287
13459
|
}
|
|
@@ -13426,7 +13598,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
13426
13598
|
if (d.name) {
|
|
13427
13599
|
text += 'Name: ' + d.name + '<br>';
|
|
13428
13600
|
}
|
|
13429
|
-
if (d.branch_length
|
|
13601
|
+
if (d.branch_length !== undefined && d.branch_length !== null
|
|
13602
|
+
&& !(unrooted && d.children)) {
|
|
13430
13603
|
text += (unrooted ? 'Branch length: ' : 'Distance to parent: ') + d.branch_length + '<br>';
|
|
13431
13604
|
}
|
|
13432
13605
|
let date = dateText(d.date);
|
|
@@ -14068,7 +14241,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
14068
14241
|
on(CONFIDENCE_VALUES_CB, 'click', confidenceValuesCbClicked);
|
|
14069
14242
|
on(SUPPORT_DOTS_CB, 'click', supportDotsCbClicked);
|
|
14070
14243
|
on(MAD_VALUES_CB, 'click', madValuesCbClicked);
|
|
14071
|
-
on(SEARCH_B_TOGGLE, 'click', revealSearchB);
|
|
14072
14244
|
on(SEARCH_NAV_PREV, 'click', function () {
|
|
14073
14245
|
stepToFoundNode(-1);
|
|
14074
14246
|
});
|
|
@@ -14093,6 +14265,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14093
14265
|
|
|
14094
14266
|
on(DYNAHIDE_CB, 'click', dynaHideCbClicked);
|
|
14095
14267
|
on(MSA_CB, 'click', msaCbClicked);
|
|
14268
|
+
on(MSA_LOGO_CB, 'click', msaLogoCbClicked);
|
|
14096
14269
|
on(HEATMAP_CB, 'click', heatmapCbClicked);
|
|
14097
14270
|
on(HEATMAP_ORDER_SELECT, 'change', heatmapOrderChanged);
|
|
14098
14271
|
on(HEATMAP_REORDER_BUTTON, 'click', showHeatmapReorderDialog);
|
|
@@ -14637,6 +14810,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14637
14810
|
// labels -- they live here
|
|
14638
14811
|
if (_basicTreeProperties.alignedMolSeqs && _basicTreeProperties.maxMolSeqLength > 0) {
|
|
14639
14812
|
opts.push(makeCheckboxItem('Alignment', MSA_CB, 'to show/hide the sequence alignment beside the tree (rectangular layout only)'));
|
|
14813
|
+
opts.push(makeCheckboxItem('Sequence Logo', MSA_LOGO_CB, 'to summarize the alignment under it as a sequence logo: each column a stack of letters, as tall as that column\'s information content and shared out by residue frequency. Over the tips currently on screen, so entering a clade summarizes that clade'));
|
|
14640
14814
|
}
|
|
14641
14815
|
if (_basicTreeProperties.domainArchitectures) {
|
|
14642
14816
|
opts.push(makeCheckboxItem('Domain Architectures', DOMAINS_CB, 'to show/hide the protein domain architectures beside the tips', true));
|
|
@@ -14810,14 +14984,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
14810
14984
|
h = h.concat('<fieldset>');
|
|
14811
14985
|
h = h.concat('<legend>Search</legend>');
|
|
14812
14986
|
h = h.concat(makeSearchBox('Search A', 0));
|
|
14813
|
-
// Search B
|
|
14814
|
-
//
|
|
14815
|
-
//
|
|
14816
|
-
|
|
14817
|
-
|
|
14818
|
-
|
|
14987
|
+
// Search B used to sit behind a '+ Search B' link, to keep 45px out
|
|
14988
|
+
// of the panel. That link was a second, weaker disclosure mechanism
|
|
14989
|
+
// nested inside the section fold: it was forgotten on every reload
|
|
14990
|
+
// (the fold is remembered), it was one-way -- nothing put B back --
|
|
14991
|
+
// and it defeated the fit rule, since revealing B grew a section
|
|
14992
|
+
// that had just been fitted without it, leaving the panel to
|
|
14993
|
+
// scroll. The fold IS the disclosure. B is simply here.
|
|
14819
14994
|
h = h.concat(makeSearchBox('Search B', 1));
|
|
14820
|
-
h = h.concat('</div>');
|
|
14821
14995
|
h = h.concat('<div class="aptx-searchnav" id="' + SEARCH_NAV_ROW + '" style="display:none">');
|
|
14822
14996
|
h = h.concat('<button type="button" class="aptx-gbtn" id="' + SEARCH_NAV_PREV
|
|
14823
14997
|
+ '" title="center the previous search hit">◀</button>');
|
|
@@ -14971,6 +15145,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14971
15145
|
setCheckboxValue(VIS_CB, _state.showVisualizations);
|
|
14972
15146
|
setCheckboxValue(DYNAHIDE_CB, _state.dynahide);
|
|
14973
15147
|
setCheckboxValue(MSA_CB, _state.showMsa);
|
|
15148
|
+
setCheckboxValue(MSA_LOGO_CB, _state.showMsaLogo === true);
|
|
14974
15149
|
setCheckboxValue(HEATMAP_CB, _state.showHeatmap);
|
|
14975
15150
|
syncDomainControls();
|
|
14976
15151
|
syncHeatmapControls();
|
|
@@ -15031,7 +15206,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
15031
15206
|
}
|
|
15032
15207
|
if (_state.searchBinitialValue) {
|
|
15033
15208
|
setValue(SEARCH_FIELD_1, _state.searchBinitialValue);
|
|
15034
|
-
revealSearchB();
|
|
15035
15209
|
}
|
|
15036
15210
|
}
|
|
15037
15211
|
|