archaeopteryx 3.20.0 → 3.22.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 +165 -18
- package/archaeopteryx.d.ts +24 -0
- package/archaeopteryx.js +971 -91
- package/forester.js +541 -2
- package/package.json +2 -2
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.22.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.22.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';
|
|
@@ -248,6 +248,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
248
248
|
const LEGEND_LABEL_COLOR = 'legendLabelColor';
|
|
249
249
|
const LEGEND_NODE_SHAPE = 'legendNodeShape';
|
|
250
250
|
const LEGEND_DOMAINS = 'legendDomains';
|
|
251
|
+
const LEGEND_GENES = 'legendGenes';
|
|
251
252
|
const NH_EXPORT_FORMAT = 'Newick';
|
|
252
253
|
const NEXUS_EXPORT_FORMAT = 'Nexus';
|
|
253
254
|
const NODE_SIZE_MAX = 9;
|
|
@@ -380,6 +381,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
380
381
|
const DOMAIN_EVALUE_READOUT = 'domain_evalue';
|
|
381
382
|
const DOMAIN_LABELS_SELECT = 'domain_labels';
|
|
382
383
|
const DOMAIN_GLOW_CB = 'domain_glow_cb';
|
|
384
|
+
const GENES_CB = 'genes_cb';
|
|
385
|
+
const GENE_CONTROLS = 'gene_controls';
|
|
386
|
+
const GENE_WIDTH_DEC = 'gene_width_dec';
|
|
387
|
+
const GENE_WIDTH_INC = 'gene_width_inc';
|
|
388
|
+
const GENE_ANCHOR_SELECT = 'gene_anchor';
|
|
389
|
+
const GENE_FAMILY_SELECT = 'gene_family';
|
|
390
|
+
const GENE_LABELS_SELECT = 'gene_labels';
|
|
383
391
|
const SCALE_AXIS_CB = 'scaleaxis_cb';
|
|
384
392
|
const SCALE_GRID_CB = 'scalegrid_cb';
|
|
385
393
|
const MSA_SCROLL_ID = 'aptxmsascroll';
|
|
@@ -605,6 +613,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
605
613
|
const DOMAIN_LABEL_MODES = ['none', 'domains', 'legend'];
|
|
606
614
|
let _domain = null; // {width, palette, next, legendFrac}, set per launch
|
|
607
615
|
let _domainReserve = 0; // horizontal px reserved for the tracks, set with _w
|
|
616
|
+
// The gene track (genome regions) shares the domain track's widths, box
|
|
617
|
+
// heights, backbone colour and legend geometry: the two are the same
|
|
618
|
+
// kind of thing, a backbone with boxes beside each tip.
|
|
619
|
+
const GENE_LABEL_MODES = ['none', 'genes', 'legend'];
|
|
620
|
+
const GENE_KB_BAR_TARGET_PX = 80; // the track's own scale bar aims at this length
|
|
621
|
+
const GENE_CAP_W = 1.5; // px: the bar that marks a contig end
|
|
622
|
+
const GENE_LANE_MIN_H = 3; // px: a lane of two is never thinner
|
|
623
|
+
let _gene = null; // {width, radialWidth, palette, legendFrac, cache}, set per launch
|
|
624
|
+
let _geneReserve = 0; // horizontal px reserved for the gene track, set with _w
|
|
608
625
|
// ------ heat map (the desktop's MATRIX annotation columns) ------
|
|
609
626
|
const HEATMAP_TRACK_GAP = 8; // px between what is left of the tree and the matrix
|
|
610
627
|
const HEATMAP_PREF_COL_W = 14; // a column never grows past this
|
|
@@ -1184,7 +1201,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
1184
1201
|
// the viewport: measured box=[0,13 1497x755] against
|
|
1185
1202
|
// view=[-254,-10 1497x800] with rootOffset=254, so "does it fit" said
|
|
1186
1203
|
// no by precisely 254px and the overview appeared after every Fit.
|
|
1187
|
-
maxX += calcMaxTreeLengthForDisplay() - _settings.rootOffset + _domainReserve;
|
|
1204
|
+
maxX += calcMaxTreeLengthForDisplay() - _settings.rootOffset + _domainReserve + _geneReserve;
|
|
1188
1205
|
minY -= heatmapTopReserve(); // the dendrogram band stands above the first row
|
|
1189
1206
|
if (scaleBarShown()) {
|
|
1190
1207
|
maxY += SCALE_BAR_RESERVE; // the bar sits under the last row
|
|
@@ -2900,8 +2917,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
2900
2917
|
// the alignment track reserves its window on the right, so the
|
|
2901
2918
|
// tree and labels compress to make room rather than overlapping
|
|
2902
2919
|
_msaReserve = 0;
|
|
2903
|
-
// and the domain tracks reserve their column past the labels
|
|
2920
|
+
// and the domain tracks reserve their column past the labels,
|
|
2921
|
+
// the gene track its own past theirs
|
|
2904
2922
|
_domainReserve = domainReserve();
|
|
2923
|
+
_geneReserve = geneReserve();
|
|
2905
2924
|
// The heat map is budgeted FIRST of the two right-hand tracks: it
|
|
2906
2925
|
// wants a fixed, finite width (its columns at their preferred size
|
|
2907
2926
|
// and no more), while the alignment's band is a WINDOW that scrolls
|
|
@@ -2913,7 +2932,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
2913
2932
|
let vp0 = svgSize();
|
|
2914
2933
|
let vw0 = Math.min(_displayWidth, (vp0 && vp0.w) ? vp0.w : _displayWidth);
|
|
2915
2934
|
let hband = Math.max(HEATMAP_MIN_BAND_PX, Math.round(vw0 * HEATMAP_MAX_VIEWPORT_FRACTION));
|
|
2916
|
-
let hmax = _displayWidth - calcMaxTreeLengthForDisplay() - _domainReserve
|
|
2935
|
+
let hmax = _displayWidth - calcMaxTreeLengthForDisplay() - _domainReserve - _geneReserve
|
|
2917
2936
|
- HEATMAP_TRACK_GAP - HEATMAP_MIN_TREE_PX;
|
|
2918
2937
|
hband = Math.max(HEATMAP_MIN_BAND_PX, Math.min(hband, hmax));
|
|
2919
2938
|
_heatmapReserve = HEATMAP_TRACK_GAP + Math.min(cols * HEATMAP_PREF_COL_W, hband);
|
|
@@ -2928,12 +2947,13 @@ function (root, d3, forester, phyloXml) {
|
|
|
2928
2947
|
let band = Math.max(MSA_MIN_BAND_PX, Math.round(vw * MSA_MAX_VIEWPORT_FRACTION));
|
|
2929
2948
|
// a wide alignment must not squeeze the tree itself away: the
|
|
2930
2949
|
// band yields until the tree keeps its minimum share
|
|
2931
|
-
let maxBand = _displayWidth - calcMaxTreeLengthForDisplay() - _domainReserve
|
|
2950
|
+
let maxBand = _displayWidth - calcMaxTreeLengthForDisplay() - _domainReserve - _geneReserve
|
|
2932
2951
|
- _heatmapReserve - MSA_TRACK_GAP - MSA_MIN_TREE_PX;
|
|
2933
2952
|
band = Math.max(MSA_MIN_BAND_PX, Math.min(band, maxBand));
|
|
2934
2953
|
_msaReserve = MSA_TRACK_GAP + Math.min(fullPx, band);
|
|
2935
2954
|
}
|
|
2936
|
-
_w = _displayWidth - calcMaxTreeLengthForDisplay() - _msaReserve - _domainReserve -
|
|
2955
|
+
_w = _displayWidth - calcMaxTreeLengthForDisplay() - _msaReserve - _domainReserve - _geneReserve
|
|
2956
|
+
- _heatmapReserve;
|
|
2937
2957
|
if (_w < 1) {
|
|
2938
2958
|
_w = 1;
|
|
2939
2959
|
}
|
|
@@ -3487,6 +3507,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
3487
3507
|
drawScaleBar();
|
|
3488
3508
|
drawDomainArchitectures();
|
|
3489
3509
|
drawDomainLegend();
|
|
3510
|
+
drawGeneTrack();
|
|
3511
|
+
drawGeneLegend();
|
|
3490
3512
|
drawMsaTrack();
|
|
3491
3513
|
drawHeatmapTrack();
|
|
3492
3514
|
drawTimeOverlays();
|
|
@@ -5651,6 +5673,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
5651
5673
|
'domainLabels',
|
|
5652
5674
|
'domainGlow',
|
|
5653
5675
|
'domainEvalueExponent',
|
|
5676
|
+
'showGenes',
|
|
5677
|
+
'geneAnchor',
|
|
5678
|
+
'geneFamily',
|
|
5679
|
+
'geneLabels',
|
|
5654
5680
|
'showScaleAxis',
|
|
5655
5681
|
'showScaleGrid',
|
|
5656
5682
|
'showSupportDots',
|
|
@@ -5961,6 +5987,43 @@ function (root, d3, forester, phyloXml) {
|
|
|
5961
5987
|
+ ' with a missing or impossible from / to / E-value ignored');
|
|
5962
5988
|
}
|
|
5963
5989
|
}
|
|
5990
|
+
// Likewise a tree whose tips carry genome regions -- genes as located
|
|
5991
|
+
// sequences -- draws its gene track from the start, unless the caller
|
|
5992
|
+
// decided. The anchor family and the family namespace are chosen from
|
|
5993
|
+
// the tree ('auto') unless named; the label mode is the domain
|
|
5994
|
+
// track's.
|
|
5995
|
+
if (_state.showGenes === undefined) {
|
|
5996
|
+
_state.showGenes = _basicTreeProperties.geneRegions === true;
|
|
5997
|
+
}
|
|
5998
|
+
if (_state.geneLabels === undefined) {
|
|
5999
|
+
_state.geneLabels = 'genes';
|
|
6000
|
+
} else if (GENE_LABEL_MODES.indexOf(_state.geneLabels) < 0) {
|
|
6001
|
+
throw new Error(ERROR + '"geneLabels" must be "none", "genes" or "legend"');
|
|
6002
|
+
}
|
|
6003
|
+
if (_state.geneAnchor === undefined) {
|
|
6004
|
+
_state.geneAnchor = 'auto';
|
|
6005
|
+
} else if (typeof _state.geneAnchor !== 'string' || _state.geneAnchor.length === 0) {
|
|
6006
|
+
throw new Error(ERROR + '"geneAnchor" must be a family ref such as "pgfam:PGF_00019355", "auto" or "none"');
|
|
6007
|
+
}
|
|
6008
|
+
if (_state.geneFamily === undefined) {
|
|
6009
|
+
_state.geneFamily = 'auto';
|
|
6010
|
+
} else if (typeof _state.geneFamily !== 'string' || _state.geneFamily.length === 0) {
|
|
6011
|
+
throw new Error(ERROR + '"geneFamily" must be an annotation namespace such as "pgfam", or "auto"');
|
|
6012
|
+
}
|
|
6013
|
+
_gene = {width: null, radialWidth: null, palette: null, legendFrac: null, cache: new WeakMap()};
|
|
6014
|
+
if (_basicTreeProperties.geneRegions) {
|
|
6015
|
+
// a location outside the grammar and a gene on a second contig
|
|
6016
|
+
// are left out, never fatal -- said once, here
|
|
6017
|
+
let gs = forester.geneRegionStats(_treeData);
|
|
6018
|
+
if (gs.ignored > 0) {
|
|
6019
|
+
console.warn(MESSAGE + gs.ignored + ' sequence location' + (gs.ignored === 1 ? '' : 's')
|
|
6020
|
+
+ ' not in the form contig:start-end(+) ignored');
|
|
6021
|
+
}
|
|
6022
|
+
if (gs.otherContigs > 0) {
|
|
6023
|
+
console.warn(MESSAGE + gs.otherContigs + ' gene' + (gs.otherContigs === 1 ? '' : 's')
|
|
6024
|
+
+ ' on a genome\'s other contigs not drawn: the gene track shows one contig per genome');
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
5964
6027
|
// Likewise a dated tree draws its scale axis from the start -- a
|
|
5965
6028
|
// time axis of geologic ICS bands or calendar years, decided from the
|
|
5966
6029
|
// <date> elements by forester.timeAxisInfo -- again unless the caller
|
|
@@ -6059,6 +6122,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
6059
6122
|
return null;
|
|
6060
6123
|
}
|
|
6061
6124
|
let s = n.sequences[0];
|
|
6125
|
+
// On a tree of genomes a tip's located sequences -- its
|
|
6126
|
+
// contig record and its genes -- describe the region drawn
|
|
6127
|
+
// beside it, not the tip: "S01 chromosome" is no name for
|
|
6128
|
+
// S01, and a gene's product even less so. They are no
|
|
6129
|
+
// label, so the node name stands.
|
|
6130
|
+
if (_basicTreeProperties.geneRegions && forester.parseSequenceLocation(s.location)) {
|
|
6131
|
+
return null;
|
|
6132
|
+
}
|
|
6062
6133
|
let l = '';
|
|
6063
6134
|
if (_state.showSequenceSymbol) {
|
|
6064
6135
|
l = joinFrag(l, s.symbol);
|
|
@@ -7374,7 +7445,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7374
7445
|
// once flung the whole tree off-screen after three X+ presses).
|
|
7375
7446
|
return {
|
|
7376
7447
|
horizontal: Math.max(1, _displayWidth - calcMaxTreeLengthForDisplay() - _msaReserve
|
|
7377
|
-
- _domainReserve - _heatmapReserve),
|
|
7448
|
+
- _domainReserve - _geneReserve - _heatmapReserve),
|
|
7378
7449
|
vertical: Math.max(40, _displayHeight - (2 * TOP_AND_BOTTOM_BORDER_HEIGHT)
|
|
7379
7450
|
- bottomOverlayReserve() - topOverlayReserve())
|
|
7380
7451
|
};
|
|
@@ -8722,9 +8793,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8722
8793
|
}
|
|
8723
8794
|
|
|
8724
8795
|
// A faint dashed guide from each tip across to its row, as the
|
|
8725
|
-
// alignment track draws -- skipped when the domain tracks are
|
|
8726
|
-
// between, where it would run straight through their boxes.
|
|
8727
|
-
if (_domainReserve === 0) {
|
|
8796
|
+
// alignment track draws -- skipped when the domain or gene tracks are
|
|
8797
|
+
// in between, where it would run straight through their boxes.
|
|
8798
|
+
if (_domainReserve === 0 && _geneReserve === 0) {
|
|
8728
8799
|
let guideFont = _state.externalNodeFontSize + 'px ' + _state.defaultFont;
|
|
8729
8800
|
let guideGap = _state.nodeLabelGap;
|
|
8730
8801
|
let guideEnd = originX - 3;
|
|
@@ -9227,6 +9298,63 @@ function (root, d3, forester, phyloXml) {
|
|
|
9227
9298
|
// The same rule as the desktop's resolveEdited, minus its "did the order
|
|
9228
9299
|
// actually change" test, which it needs only because its dialog does other
|
|
9229
9300
|
// things too.
|
|
9301
|
+
// Drag a row of a reorder list -- the heat map's columns, the label's
|
|
9302
|
+
// metadata fields -- to a new place. The LIVE node is moved rather than
|
|
9303
|
+
// the list redrawn (a redraw would replace the element the drag is
|
|
9304
|
+
// following), its position read back from the DOM each time, and
|
|
9305
|
+
// `working` moved with it; `draw` runs once the pointer is released. A
|
|
9306
|
+
// press on a control in the row (an arrow, a checkbox) is not a drag.
|
|
9307
|
+
// Shared, so that both dialogs drag the same way: the rows' cursor
|
|
9308
|
+
// promised a drag the fields chooser did not have (Christian, 2026-10-01).
|
|
9309
|
+
function attachReorderDrag(list, row, working, draw) {
|
|
9310
|
+
function rowAt(y) {
|
|
9311
|
+
let rows = Array.prototype.slice.call(list.children);
|
|
9312
|
+
for (let i = 0; i < rows.length; ++i) {
|
|
9313
|
+
let b = rows[i].getBoundingClientRect();
|
|
9314
|
+
if (y < b.top + (b.height / 2)) {
|
|
9315
|
+
return i;
|
|
9316
|
+
}
|
|
9317
|
+
}
|
|
9318
|
+
return rows.length;
|
|
9319
|
+
}
|
|
9320
|
+
row.addEventListener('pointerdown', function (event) {
|
|
9321
|
+
if (event.target.closest('button, input')) {
|
|
9322
|
+
return;
|
|
9323
|
+
}
|
|
9324
|
+
event.preventDefault();
|
|
9325
|
+
row.classList.add('aptx-reorder-dragging');
|
|
9326
|
+
let onMove = function (ev) {
|
|
9327
|
+
let kids = Array.prototype.slice.call(list.children);
|
|
9328
|
+
let from = kids.indexOf(row);
|
|
9329
|
+
let to = rowAt(ev.clientY);
|
|
9330
|
+
if (to > from) {
|
|
9331
|
+
to -= 1; // taking the row out shifts everything after it up one
|
|
9332
|
+
}
|
|
9333
|
+
if (to === from || to < 0 || to >= kids.length) {
|
|
9334
|
+
return;
|
|
9335
|
+
}
|
|
9336
|
+
working.splice(to, 0, working.splice(from, 1)[0]);
|
|
9337
|
+
let others = kids.filter(function (k) {
|
|
9338
|
+
return k !== row;
|
|
9339
|
+
});
|
|
9340
|
+
list.insertBefore(row, others[to] || null);
|
|
9341
|
+
Array.prototype.slice.call(list.children).forEach(function (r, k) {
|
|
9342
|
+
let btns = r.querySelectorAll('.aptx-reorder-move');
|
|
9343
|
+
btns[0].disabled = (k === 0);
|
|
9344
|
+
btns[1].disabled = (k === list.children.length - 1);
|
|
9345
|
+
});
|
|
9346
|
+
};
|
|
9347
|
+
let onUp = function () {
|
|
9348
|
+
row.classList.remove('aptx-reorder-dragging');
|
|
9349
|
+
window.removeEventListener('pointermove', onMove);
|
|
9350
|
+
window.removeEventListener('pointerup', onUp);
|
|
9351
|
+
draw();
|
|
9352
|
+
};
|
|
9353
|
+
window.addEventListener('pointermove', onMove);
|
|
9354
|
+
window.addEventListener('pointerup', onUp);
|
|
9355
|
+
});
|
|
9356
|
+
}
|
|
9357
|
+
|
|
9230
9358
|
function showHeatmapReorderDialog() {
|
|
9231
9359
|
let ordered = heatmapOrdered();
|
|
9232
9360
|
if (!ordered || ordered.columns.length < 2) {
|
|
@@ -9239,17 +9367,6 @@ function (root, d3, forester, phyloXml) {
|
|
|
9239
9367
|
let list = repsElement(body, 'div', 'aptx-reorder');
|
|
9240
9368
|
let working = ordered.columns.slice();
|
|
9241
9369
|
|
|
9242
|
-
function rowAt(y) {
|
|
9243
|
-
let rows = Array.prototype.slice.call(list.children);
|
|
9244
|
-
for (let i = 0; i < rows.length; ++i) {
|
|
9245
|
-
let b = rows[i].getBoundingClientRect();
|
|
9246
|
-
if (y < b.top + (b.height / 2)) {
|
|
9247
|
-
return i;
|
|
9248
|
-
}
|
|
9249
|
-
}
|
|
9250
|
-
return rows.length;
|
|
9251
|
-
}
|
|
9252
|
-
|
|
9253
9370
|
function move(from, to) {
|
|
9254
9371
|
if (to < 0 || to >= working.length || from === to) {
|
|
9255
9372
|
return false;
|
|
@@ -9296,49 +9413,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9296
9413
|
list.children[i + d].focus();
|
|
9297
9414
|
}
|
|
9298
9415
|
});
|
|
9299
|
-
|
|
9300
|
-
if (event.target.classList.contains('aptx-reorder-move')) {
|
|
9301
|
-
return; // the arrows are not a drag handle
|
|
9302
|
-
}
|
|
9303
|
-
event.preventDefault();
|
|
9304
|
-
row.classList.add('aptx-reorder-dragging');
|
|
9305
|
-
// The LIVE node is moved rather than the list redrawn: a
|
|
9306
|
-
// redraw would replace the element this drag is following.
|
|
9307
|
-
// Its position is read back from the DOM each time instead
|
|
9308
|
-
// of being tracked in a variable -- the first cut kept an
|
|
9309
|
-
// index, updated it, and then used it to work out where to
|
|
9310
|
-
// insert, by which point it no longer meant what the
|
|
9311
|
-
// calculation assumed.
|
|
9312
|
-
let onMove = function (ev) {
|
|
9313
|
-
let kids = Array.prototype.slice.call(list.children);
|
|
9314
|
-
let from = kids.indexOf(row);
|
|
9315
|
-
let to = rowAt(ev.clientY);
|
|
9316
|
-
if (to > from) {
|
|
9317
|
-
to -= 1; // taking the row out shifts everything after it up one
|
|
9318
|
-
}
|
|
9319
|
-
if (to === from || to < 0 || to >= kids.length) {
|
|
9320
|
-
return;
|
|
9321
|
-
}
|
|
9322
|
-
working.splice(to, 0, working.splice(from, 1)[0]);
|
|
9323
|
-
let others = kids.filter(function (k) {
|
|
9324
|
-
return k !== row;
|
|
9325
|
-
});
|
|
9326
|
-
list.insertBefore(row, others[to] || null);
|
|
9327
|
-
Array.prototype.slice.call(list.children).forEach(function (r, k) {
|
|
9328
|
-
let btns = r.querySelectorAll('.aptx-reorder-move');
|
|
9329
|
-
btns[0].disabled = (k === 0);
|
|
9330
|
-
btns[1].disabled = (k === list.children.length - 1);
|
|
9331
|
-
});
|
|
9332
|
-
};
|
|
9333
|
-
let onUp = function () {
|
|
9334
|
-
row.classList.remove('aptx-reorder-dragging');
|
|
9335
|
-
window.removeEventListener('pointermove', onMove);
|
|
9336
|
-
window.removeEventListener('pointerup', onUp);
|
|
9337
|
-
draw();
|
|
9338
|
-
};
|
|
9339
|
-
window.addEventListener('pointermove', onMove);
|
|
9340
|
-
window.addEventListener('pointerup', onUp);
|
|
9341
|
-
});
|
|
9416
|
+
attachReorderDrag(list, row, working, draw);
|
|
9342
9417
|
});
|
|
9343
9418
|
}
|
|
9344
9419
|
draw();
|
|
@@ -9895,6 +9970,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
9895
9970
|
s.domainGlow = _state.domainGlow === true;
|
|
9896
9971
|
s.domainEvalue = _state.domainEvalueExponent;
|
|
9897
9972
|
}
|
|
9973
|
+
if (_basicTreeProperties.geneRegions === true) {
|
|
9974
|
+
s.genes = _state.showGenes === true;
|
|
9975
|
+
s.geneLabels = _state.geneLabels;
|
|
9976
|
+
// the EFFECTIVE choices, so that a link reproduces the figure
|
|
9977
|
+
// even where an automatic choice would have moved
|
|
9978
|
+
s.geneAnchor = geneAnchor() || 'none';
|
|
9979
|
+
s.geneFamily = geneFamilyNamespace() || 'auto';
|
|
9980
|
+
}
|
|
9898
9981
|
if (_basicTreeProperties.branchLengths === true) {
|
|
9899
9982
|
s.scaleAxis = _state.showScaleAxis === true;
|
|
9900
9983
|
s.scaleGrid = _state.showScaleGrid === true;
|
|
@@ -10057,6 +10140,21 @@ function (root, d3, forester, phyloXml) {
|
|
|
10057
10140
|
_domain.palette = null; // the drawn set changed: the palette is dealt again
|
|
10058
10141
|
}
|
|
10059
10142
|
}
|
|
10143
|
+
if (typeof s.genes === 'boolean') {
|
|
10144
|
+
_state.showGenes = s.genes;
|
|
10145
|
+
}
|
|
10146
|
+
if (GENE_LABEL_MODES.indexOf(s.geneLabels) >= 0) {
|
|
10147
|
+
_state.geneLabels = s.geneLabels;
|
|
10148
|
+
}
|
|
10149
|
+
if (typeof s.geneFamily === 'string' && s.geneFamily.length > 0 && s.geneFamily !== _state.geneFamily) {
|
|
10150
|
+
_state.geneFamily = s.geneFamily;
|
|
10151
|
+
if (_gene) {
|
|
10152
|
+
_gene.palette = null; // the families change with the namespace
|
|
10153
|
+
}
|
|
10154
|
+
}
|
|
10155
|
+
if (typeof s.geneAnchor === 'string' && s.geneAnchor.length > 0) {
|
|
10156
|
+
_state.geneAnchor = s.geneAnchor;
|
|
10157
|
+
}
|
|
10060
10158
|
// timeAxis / timeGrid: the names before the axis also measured
|
|
10061
10159
|
// distance, still read so that older links keep their axis
|
|
10062
10160
|
let axis = typeof s.scaleAxis === 'boolean' ? s.scaleAxis : s.timeAxis;
|
|
@@ -10069,8 +10167,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
10069
10167
|
}
|
|
10070
10168
|
if (!radialDisplay()) {
|
|
10071
10169
|
_radialLabelsHorizontal = false;
|
|
10072
|
-
} else if (_state.showDomainArchitectures && _basicTreeProperties.domainArchitectures)
|
|
10073
|
-
|
|
10170
|
+
} else if ((_state.showDomainArchitectures && _basicTreeProperties.domainArchitectures)
|
|
10171
|
+
|| (_state.showGenes && _basicTreeProperties.geneRegions)) {
|
|
10172
|
+
_radialLabelsHorizontal = false; // the domain boxes and gene arrows ride the spokes, so the labels must too
|
|
10074
10173
|
}
|
|
10075
10174
|
if (_vis) {
|
|
10076
10175
|
if (s.colorBy === 'none') {
|
|
@@ -10150,6 +10249,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10150
10249
|
setCheckboxValue(DOMAIN_GLOW_CB, _state.domainGlow === true);
|
|
10151
10250
|
setValue(DOMAIN_LABELS_SELECT, _state.domainLabels);
|
|
10152
10251
|
syncDomainControls();
|
|
10252
|
+
syncGeneControls();
|
|
10153
10253
|
setSliderValue(FONT_SIZE_SLIDER, _state.externalNodeFontSize);
|
|
10154
10254
|
setSliderValue(NODE_SIZE_SLIDER, _state.nodeSizeDefault);
|
|
10155
10255
|
setSliderValue(BRANCH_WIDTH_SLIDER, _state.branchWidthDefault);
|
|
@@ -10222,10 +10322,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
10222
10322
|
// The heat map's four keys and the alignment logo went in exactly that
|
|
10223
10323
|
// way, while the README promised a link reproduced the figure.
|
|
10224
10324
|
const VIEW_TEXT_KEYS = ['layout', 'display', 'order', 'root', 'colorBy', 'shapeBy', 'domainLabels', 'combine',
|
|
10225
|
-
'heatmapOrder', 'scale'];
|
|
10325
|
+
'heatmapOrder', 'scale', 'geneAnchor', 'geneFamily', 'geneLabels'];
|
|
10226
10326
|
const VIEW_INT_KEYS = ['tree', 'subtree', 'rotation', 'domainEvalue'];
|
|
10227
10327
|
const VIEW_NUMBER_KEYS = ['font', 'node', 'branch'];
|
|
10228
|
-
const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', 'msaLogo', 'heatmap', 'domains', 'domainGlow',
|
|
10328
|
+
const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', 'msaLogo', 'heatmap', 'domains', 'domainGlow', 'genes',
|
|
10229
10329
|
'scaleAxis', 'scaleGrid', 'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
|
|
10230
10330
|
// Lists of strings, comma-joined. A property ref carries ':' but never a
|
|
10231
10331
|
// comma, and encodeViewValue leaves both readable in a hash.
|
|
@@ -11738,6 +11838,710 @@ function (root, d3, forester, phyloXml) {
|
|
|
11738
11838
|
});
|
|
11739
11839
|
}
|
|
11740
11840
|
|
|
11841
|
+
// ===================== Genome regions (the gene track) =====================
|
|
11842
|
+
// A tip that is a genome can carry a region of it: its genes as located
|
|
11843
|
+
// sequences with their families, one per gene, plus the contig's record
|
|
11844
|
+
// (forester's gene model, test/gene_test.js). Drawn as the domain track
|
|
11845
|
+
// is drawn -- a backbone per tip in one aligned column past the labels in
|
|
11846
|
+
// the rectangular layout, riding the tip's spoke in the radial ones --
|
|
11847
|
+
// but each gene is an ARROW pointing by strand, coloured by family, and
|
|
11848
|
+
// the rows are ANCHORED: the anchor family's gene sits at the same x in
|
|
11849
|
+
// every row, pointing right, the neighbours following, so gene order
|
|
11850
|
+
// compares across genomes (Christian, 2026-10-01). One scale for the
|
|
11851
|
+
// whole tree -- f px per base pair, from the track width and the extent
|
|
11852
|
+
// the rows share -- so distances compare too. A bar at the end of a
|
|
11853
|
+
// backbone is a contig end; the track's own kb bar sits under the rows.
|
|
11854
|
+
|
|
11855
|
+
function genesShown() {
|
|
11856
|
+
return _gene !== null
|
|
11857
|
+
&& _state.showGenes === true
|
|
11858
|
+
&& _basicTreeProperties.geneRegions === true
|
|
11859
|
+
&& _state.showExternalLabels
|
|
11860
|
+
// in a radial layout the arrows ride the spokes, so only radial
|
|
11861
|
+
// labels go with them
|
|
11862
|
+
&& (!radialDisplay() || !_radialLabelsHorizontal);
|
|
11863
|
+
}
|
|
11864
|
+
|
|
11865
|
+
// The region a tip carries, computed once per node: its sequences never
|
|
11866
|
+
// change after load, and a subtree view shows the same nodes.
|
|
11867
|
+
function geneRegion(d) {
|
|
11868
|
+
if (_gene.cache.has(d)) {
|
|
11869
|
+
return _gene.cache.get(d);
|
|
11870
|
+
}
|
|
11871
|
+
let r = forester.geneRegionOf(d);
|
|
11872
|
+
_gene.cache.set(d, r);
|
|
11873
|
+
return r;
|
|
11874
|
+
}
|
|
11875
|
+
|
|
11876
|
+
// Every region of the WHOLE tree, in its order. The anchor, the family
|
|
11877
|
+
// namespace and the palette are decided on the tree, never on the view,
|
|
11878
|
+
// so that entering a subtree moves nothing under the reader.
|
|
11879
|
+
function geneTreeRegions() {
|
|
11880
|
+
let out = [];
|
|
11881
|
+
if (!_treeData) {
|
|
11882
|
+
return out;
|
|
11883
|
+
}
|
|
11884
|
+
forester.preOrderTraversalAll(forester.getTreeRoot(_treeData), function (n) {
|
|
11885
|
+
if (!n.children) {
|
|
11886
|
+
let r = geneRegion(n);
|
|
11887
|
+
if (r) {
|
|
11888
|
+
out.push(r);
|
|
11889
|
+
}
|
|
11890
|
+
}
|
|
11891
|
+
});
|
|
11892
|
+
return out;
|
|
11893
|
+
}
|
|
11894
|
+
|
|
11895
|
+
// The namespace the colours and the anchor follow: the one named in the
|
|
11896
|
+
// config or the view, else the one most of the genes carry.
|
|
11897
|
+
function geneFamilyNamespace() {
|
|
11898
|
+
if (_state.geneFamily && _state.geneFamily !== 'auto') {
|
|
11899
|
+
return _state.geneFamily;
|
|
11900
|
+
}
|
|
11901
|
+
let all = forester.geneFamilyNamespaces(geneTreeRegions());
|
|
11902
|
+
return all.length > 0 ? all[0].ns : null;
|
|
11903
|
+
}
|
|
11904
|
+
|
|
11905
|
+
// The anchor family: 'none' is none, 'auto' the tree's best (single-copy
|
|
11906
|
+
// in the most genomes, at least half of them), a ref is itself.
|
|
11907
|
+
function geneAnchor() {
|
|
11908
|
+
let a = _state.geneAnchor;
|
|
11909
|
+
if (a === 'none') {
|
|
11910
|
+
return null;
|
|
11911
|
+
}
|
|
11912
|
+
if (!a || a === 'auto') {
|
|
11913
|
+
return forester.geneDefaultAnchor(geneTreeRegions(), geneFamilyNamespace());
|
|
11914
|
+
}
|
|
11915
|
+
return a;
|
|
11916
|
+
}
|
|
11917
|
+
|
|
11918
|
+
// The track width: the domain track's rules (a quarter of the viewport
|
|
11919
|
+
// at first use in the rectangular layouts, a width of the radial
|
|
11920
|
+
// layouts' own, the same step buttons), kept apart from the domain
|
|
11921
|
+
// track's so each is sized for what it draws.
|
|
11922
|
+
function geneRectangularWidth() {
|
|
11923
|
+
if (!_gene.width) {
|
|
11924
|
+
let vp = svgSize();
|
|
11925
|
+
let vw = Math.min(_displayWidth, (vp && vp.w) ? vp.w : _displayWidth);
|
|
11926
|
+
_gene.width = Math.max(DOMAIN_WIDTH_MIN, Math.round(vw * DOMAIN_WIDTH_VIEWPORT_FRACTION));
|
|
11927
|
+
}
|
|
11928
|
+
return _gene.width;
|
|
11929
|
+
}
|
|
11930
|
+
|
|
11931
|
+
function geneTrackWidth() {
|
|
11932
|
+
if (!radialDisplay()) {
|
|
11933
|
+
return geneRectangularWidth();
|
|
11934
|
+
}
|
|
11935
|
+
if (!_gene.radialWidth) {
|
|
11936
|
+
let r = _state.circularDisplay ? (_radial ? _radial.maxRad : 0) : (_unroot ? _unroot.maxRad : 0);
|
|
11937
|
+
if (!(r > 0)) {
|
|
11938
|
+
return geneRectangularWidth(); // no radius yet (before the first radial draw)
|
|
11939
|
+
}
|
|
11940
|
+
_gene.radialWidth = Math.max(DOMAIN_WIDTH_MIN, Math.min(geneRectangularWidth(), DOMAIN_RADIAL_WIDTH_FRACTION * r));
|
|
11941
|
+
}
|
|
11942
|
+
return _gene.radialWidth;
|
|
11943
|
+
}
|
|
11944
|
+
|
|
11945
|
+
function setGeneTrackWidth(w) {
|
|
11946
|
+
if (radialDisplay()) {
|
|
11947
|
+
_gene.radialWidth = w;
|
|
11948
|
+
} else {
|
|
11949
|
+
_gene.width = w;
|
|
11950
|
+
}
|
|
11951
|
+
}
|
|
11952
|
+
|
|
11953
|
+
// The rectangular layout reserves the track's column past the domain
|
|
11954
|
+
// tracks' (both counted wherever _w is); the radial layouts fit it into
|
|
11955
|
+
// the ring (fitRadialExtent).
|
|
11956
|
+
function geneReserve() {
|
|
11957
|
+
if (!genesShown() || radialDisplay()) {
|
|
11958
|
+
return 0;
|
|
11959
|
+
}
|
|
11960
|
+
return DOMAIN_TRACK_START_GAP + geneTrackWidth() + DOMAIN_TRACK_END_GAP;
|
|
11961
|
+
}
|
|
11962
|
+
|
|
11963
|
+
function geneRadialExtent() {
|
|
11964
|
+
if (!genesShown() || !radialDisplay()) {
|
|
11965
|
+
return 0;
|
|
11966
|
+
}
|
|
11967
|
+
return DOMAIN_RADIAL_GAP + geneTrackWidth() + DOMAIN_TRACK_END_GAP;
|
|
11968
|
+
}
|
|
11969
|
+
|
|
11970
|
+
// Colours: the families SHARED over the whole tree (in two genomes or
|
|
11971
|
+
// more), sorted by code unit, take the palette in order -- dealt at load
|
|
11972
|
+
// and when the namespace changes. A family met in one genome only is
|
|
11973
|
+
// grey, so conserved order stands out; a gene with no family is lighter
|
|
11974
|
+
// grey.
|
|
11975
|
+
function genePalette() {
|
|
11976
|
+
if (!_gene.palette) {
|
|
11977
|
+
let names = forester.geneFamilySummary(geneTreeRegions(), geneFamilyNamespace()).names;
|
|
11978
|
+
_gene.palette = Object.create(null);
|
|
11979
|
+
names.forEach(function (name, i) {
|
|
11980
|
+
_gene.palette[name] = i;
|
|
11981
|
+
});
|
|
11982
|
+
}
|
|
11983
|
+
return _gene.palette;
|
|
11984
|
+
}
|
|
11985
|
+
|
|
11986
|
+
function geneColor(family) {
|
|
11987
|
+
if (!family) {
|
|
11988
|
+
return forester.GENE_NO_FAMILY_COLOR;
|
|
11989
|
+
}
|
|
11990
|
+
let palette = genePalette();
|
|
11991
|
+
if (palette[family] === undefined) {
|
|
11992
|
+
return forester.GENE_SINGLETON_COLOR;
|
|
11993
|
+
}
|
|
11994
|
+
return forester.domainQualitativeColor(palette[family]);
|
|
11995
|
+
}
|
|
11996
|
+
|
|
11997
|
+
// What a gene is called where there is room for one word: its symbol,
|
|
11998
|
+
// else its gene name.
|
|
11999
|
+
function geneLabel(g) {
|
|
12000
|
+
return g.symbol || g.geneName || '';
|
|
12001
|
+
}
|
|
12002
|
+
|
|
12003
|
+
// ---- hover and click: the readout the domains, heat map and alignment use
|
|
12004
|
+
function geneInfoAt(event) {
|
|
12005
|
+
let t = event.target;
|
|
12006
|
+
return (t && t.__aptxGene) ? t.__aptxGene : null;
|
|
12007
|
+
}
|
|
12008
|
+
|
|
12009
|
+
let _geneHovered = null;
|
|
12010
|
+
|
|
12011
|
+
function geneHover(event) {
|
|
12012
|
+
let info = geneInfoAt(event);
|
|
12013
|
+
if (!info) {
|
|
12014
|
+
geneHoverOut();
|
|
12015
|
+
return;
|
|
12016
|
+
}
|
|
12017
|
+
let tip_el = _node_mouseover_div.node();
|
|
12018
|
+
if (info === _geneHovered) {
|
|
12019
|
+
placeHoverReadout(tip_el, event); // follows the pointer; the text is unchanged
|
|
12020
|
+
return;
|
|
12021
|
+
}
|
|
12022
|
+
_geneHovered = info;
|
|
12023
|
+
let g = info.gene;
|
|
12024
|
+
let title = geneLabel(g) || g.product || '(unnamed)';
|
|
12025
|
+
let txt = 'Gene: ' + title + '<br>';
|
|
12026
|
+
if (g.product && g.product !== title) {
|
|
12027
|
+
txt += 'Product: ' + g.product + '<br>';
|
|
12028
|
+
}
|
|
12029
|
+
if (info.family) {
|
|
12030
|
+
txt += 'Family: ' + info.family + '<br>';
|
|
12031
|
+
}
|
|
12032
|
+
txt += 'Location: ' + forester.geneLocationText(g) + (info.flipped ? ' (the row is mirrored)' : '') + '<br>';
|
|
12033
|
+
txt += 'Length: ' + countNumber(g.length) + ' bp<br>';
|
|
12034
|
+
if (info.tip) {
|
|
12035
|
+
txt += 'Tip: ' + info.tip + '<br>';
|
|
12036
|
+
}
|
|
12037
|
+
if (g.accession) {
|
|
12038
|
+
txt += 'Accession' + (g.accession.source ? ' [' + g.accession.source + ']' : '') + ': ' + g.accession.value + '<br>';
|
|
12039
|
+
}
|
|
12040
|
+
let ref = forester.geneReference(g);
|
|
12041
|
+
if (ref) {
|
|
12042
|
+
// the readout follows the pointer, so the ARROW is the link and
|
|
12043
|
+
// the readout says so (a "label: value" row, as the domains do it)
|
|
12044
|
+
txt += 'Click: the ' + ref.site + ' entry';
|
|
12045
|
+
}
|
|
12046
|
+
tip_el.classList.remove('aptx-light', 'aptx-dark');
|
|
12047
|
+
if (_panelTheme) {
|
|
12048
|
+
tip_el.classList.add('aptx-' + _panelTheme);
|
|
12049
|
+
}
|
|
12050
|
+
_node_mouseover_div.html(markUpDataLabels(escapeHtmlKeepBreaks(txt)));
|
|
12051
|
+
placeHoverReadout(tip_el, event);
|
|
12052
|
+
_node_mouseover_div.transition().duration(100).style('opacity', 0.95);
|
|
12053
|
+
}
|
|
12054
|
+
|
|
12055
|
+
function geneHoverOut() {
|
|
12056
|
+
_geneHovered = null;
|
|
12057
|
+
_node_mouseover_div.transition().duration(300).style('opacity', 1e-6);
|
|
12058
|
+
}
|
|
12059
|
+
|
|
12060
|
+
let _geneOpenedAt = 0;
|
|
12061
|
+
|
|
12062
|
+
function geneClicked(event) {
|
|
12063
|
+
let info = geneInfoAt(event);
|
|
12064
|
+
if (!info) {
|
|
12065
|
+
return;
|
|
12066
|
+
}
|
|
12067
|
+
event.stopPropagation(); // the arrow's click, linked or not: never the node menu behind it
|
|
12068
|
+
let ref = forester.geneReference(info.gene);
|
|
12069
|
+
if (!ref) {
|
|
12070
|
+
return;
|
|
12071
|
+
}
|
|
12072
|
+
let now = Date.now();
|
|
12073
|
+
if ((now - _geneOpenedAt) < 500) { // a double click arrives as two clicks
|
|
12074
|
+
return;
|
|
12075
|
+
}
|
|
12076
|
+
_geneOpenedAt = now;
|
|
12077
|
+
openExternal(ref.url);
|
|
12078
|
+
}
|
|
12079
|
+
|
|
12080
|
+
function geneDblClicked(event) {
|
|
12081
|
+
if (geneInfoAt(event)) {
|
|
12082
|
+
event.stopPropagation();
|
|
12083
|
+
event.preventDefault();
|
|
12084
|
+
}
|
|
12085
|
+
}
|
|
12086
|
+
|
|
12087
|
+
// ---- the track
|
|
12088
|
+
function drawGeneTrack() {
|
|
12089
|
+
if (!_svgGroup) {
|
|
12090
|
+
return;
|
|
12091
|
+
}
|
|
12092
|
+
let existing = _svgGroup.select('g.aptx-genes');
|
|
12093
|
+
// A region goes WITH ITS NAME, and is left out when the crowding rule
|
|
12094
|
+
// took the name (the domain track's rule, for the same reason)
|
|
12095
|
+
let tips = (genesShown() && _root) ? displayedTips().filter(function (d) {
|
|
12096
|
+
if (d._labelDropped) {
|
|
12097
|
+
return false;
|
|
12098
|
+
}
|
|
12099
|
+
return _state.unrootedDisplay ? (d.ux !== undefined) : (d.x !== undefined);
|
|
12100
|
+
}) : [];
|
|
12101
|
+
if (tips.length > 0 && !radialDisplay()) {
|
|
12102
|
+
tips.sort(function (p, q) {
|
|
12103
|
+
return p.x - q.x;
|
|
12104
|
+
});
|
|
12105
|
+
}
|
|
12106
|
+
let regions = tips.map(geneRegion);
|
|
12107
|
+
let ns = tips.length > 0 ? geneFamilyNamespace() : null;
|
|
12108
|
+
let anchor = tips.length > 0 ? geneAnchor() : null;
|
|
12109
|
+
let laid = tips.length > 0 ? forester.geneTrackRows(regions, anchor, ns) : null;
|
|
12110
|
+
let W = tips.length > 0 ? geneTrackWidth() : 0;
|
|
12111
|
+
let f = (laid && laid.extent && laid.extent.max > laid.extent.min)
|
|
12112
|
+
? (W / (laid.extent.max - laid.extent.min)) * DOMAIN_SCALE_HEADROOM : 0;
|
|
12113
|
+
if (!laid || !(f > 0) || !isFinite(f)) {
|
|
12114
|
+
existing.remove();
|
|
12115
|
+
geneHoverOut(); // the arrow under the pointer just stopped existing
|
|
12116
|
+
return;
|
|
12117
|
+
}
|
|
12118
|
+
// [the backbones and caps, the arrows, the symbols, the kb bar], kept
|
|
12119
|
+
// between redraws and moved to the end of the tree group
|
|
12120
|
+
let g = existing.node();
|
|
12121
|
+
if (!g) {
|
|
12122
|
+
g = document.createElementNS(d3.namespaces.svg, 'g');
|
|
12123
|
+
g.setAttribute('class', 'aptx-genes');
|
|
12124
|
+
g.style.pointerEvents = 'none';
|
|
12125
|
+
['g', 'g', 'g', 'g'].forEach(function (tag) {
|
|
12126
|
+
g.appendChild(document.createElementNS(d3.namespaces.svg, tag));
|
|
12127
|
+
});
|
|
12128
|
+
}
|
|
12129
|
+
_svgGroup.node().appendChild(g);
|
|
12130
|
+
let underLayer = g.children[0];
|
|
12131
|
+
let bodyLayer = g.children[1];
|
|
12132
|
+
let labelLayer = g.children[2];
|
|
12133
|
+
let rulerLayer = g.children[3];
|
|
12134
|
+
// the group ignores the mouse so the tree underneath stays clickable;
|
|
12135
|
+
// the ARROWS alone take it back
|
|
12136
|
+
bodyLayer.style.pointerEvents = 'auto';
|
|
12137
|
+
if (!bodyLayer.__aptxWired) {
|
|
12138
|
+
bodyLayer.__aptxWired = true;
|
|
12139
|
+
bodyLayer.addEventListener('mousemove', geneHover);
|
|
12140
|
+
bodyLayer.addEventListener('mouseout', geneHoverOut);
|
|
12141
|
+
bodyLayer.addEventListener('click', geneClicked);
|
|
12142
|
+
bodyLayer.addEventListener('dblclick', geneDblClicked);
|
|
12143
|
+
}
|
|
12144
|
+
|
|
12145
|
+
// Where each tip's region goes: the start of the track along the
|
|
12146
|
+
// row, the top and height of its arrows, and in the radial layouts
|
|
12147
|
+
// the transform that turns it onto its spoke. Past the domain tracks
|
|
12148
|
+
// when those are shown.
|
|
12149
|
+
let labelSpace = tipLabelSpace();
|
|
12150
|
+
let rows = [];
|
|
12151
|
+
let columnStart = null;
|
|
12152
|
+
if (_state.circularDisplay) {
|
|
12153
|
+
let r0 = _radial.maxRad + labelSpace + domainRadialExtent() + DOMAIN_RADIAL_GAP;
|
|
12154
|
+
let h = domainBoxHeight((r0 * 2 * Math.PI) / tips.length);
|
|
12155
|
+
tips.forEach(function (d) {
|
|
12156
|
+
rows.push({d: d, start: r0, y1: -h / 2, h: h,
|
|
12157
|
+
transform: 'rotate(' + labelAngleDeg(d) + ')', labelsOn: false});
|
|
12158
|
+
});
|
|
12159
|
+
} else if (_state.unrootedDisplay) {
|
|
12160
|
+
let h = domainBoxHeight((Math.PI * 2 * _unroot.maxRad) / tips.length);
|
|
12161
|
+
let start = labelSpace + domainRadialExtent() + DOMAIN_RADIAL_GAP;
|
|
12162
|
+
tips.forEach(function (d) {
|
|
12163
|
+
rows.push({d: d, start: start, y1: -h / 2, h: h,
|
|
12164
|
+
transform: 'translate(' + d.ux + ',' + d.uy + ') rotate(' + labelAngleDeg(d) + ')', labelsOn: false});
|
|
12165
|
+
});
|
|
12166
|
+
} else {
|
|
12167
|
+
let n = tips.length;
|
|
12168
|
+
let pitch = n > 1 ? (tips[n - 1].x - tips[0].x) / (n - 1) : _state.externalNodeFontSize;
|
|
12169
|
+
let h = domainBoxHeight(pitch / 2);
|
|
12170
|
+
columnStart = _w + _state.nodeLabelGap + labelSpace + _domainReserve + DOMAIN_TRACK_START_GAP;
|
|
12171
|
+
let labelsOn = _state.geneLabels === 'genes';
|
|
12172
|
+
tips.forEach(function (d) {
|
|
12173
|
+
rows.push({d: d, start: columnStart, y1: d.x - (h / 2), h: h, transform: null, labelsOn: labelsOn});
|
|
12174
|
+
});
|
|
12175
|
+
}
|
|
12176
|
+
|
|
12177
|
+
// ---- collect: the backbones and caps as path data (one path for the
|
|
12178
|
+
// whole track in the rectangular layout, one per row in the radial
|
|
12179
|
+
// ones, each in its row's turned frame -- the domain track's lesson
|
|
12180
|
+
// about turned coordinates and PDF size), one path element per
|
|
12181
|
+
// arrow (it is what the mouse finds), the symbols that fit
|
|
12182
|
+
let under = []; // {d, transform}
|
|
12183
|
+
let trackParts = [];
|
|
12184
|
+
let bodies = []; // {d, fill, stroke, transform, info, link}
|
|
12185
|
+
let names = [];
|
|
12186
|
+
let lowest = -Infinity;
|
|
12187
|
+
rows.forEach(function (row, i) {
|
|
12188
|
+
let lr = laid.rows[i];
|
|
12189
|
+
if (!lr) {
|
|
12190
|
+
return;
|
|
12191
|
+
}
|
|
12192
|
+
let geo = forester.geneRowGeometry(lr, row.start, f, laid.extent.min);
|
|
12193
|
+
let h = row.h;
|
|
12194
|
+
let y1 = row.y1;
|
|
12195
|
+
let parts = row.transform ? [] : trackParts;
|
|
12196
|
+
parts.push(roundRectPath(geo.backbone.x, y1 + (h / 2) - 0.5, geo.backbone.w, 1, 0));
|
|
12197
|
+
if (geo.capLeft) {
|
|
12198
|
+
parts.push(roundRectPath(geo.backbone.x - (GENE_CAP_W / 2), y1, GENE_CAP_W, h, 0));
|
|
12199
|
+
}
|
|
12200
|
+
if (geo.capRight) {
|
|
12201
|
+
parts.push(roundRectPath(geo.backbone.x + geo.backbone.w - (GENE_CAP_W / 2), y1, GENE_CAP_W, h, 0));
|
|
12202
|
+
}
|
|
12203
|
+
if (row.transform) {
|
|
12204
|
+
under.push({d: parts.join(''), transform: row.transform});
|
|
12205
|
+
}
|
|
12206
|
+
if (y1 + h > lowest) {
|
|
12207
|
+
lowest = y1 + h;
|
|
12208
|
+
}
|
|
12209
|
+
// once per ROW: displayNodeName walks the node's properties when
|
|
12210
|
+
// a label-ref visualization is on
|
|
12211
|
+
let rowTip = displayNodeName(row.d) || '';
|
|
12212
|
+
// two lanes share the height where genes overlap
|
|
12213
|
+
let laneH = lr.lanes > 1 ? Math.max(GENE_LANE_MIN_H, h / 2) : h;
|
|
12214
|
+
let fs = Math.min(_state.externalNodeFontSize, laneH - 2);
|
|
12215
|
+
let font = fs + 'px ' + FONT_DEFAULTS;
|
|
12216
|
+
geo.genes.forEach(function (b) {
|
|
12217
|
+
let fam = forester.geneFamilyOf(b.gene, ns);
|
|
12218
|
+
let base = geneColor(fam);
|
|
12219
|
+
let y = (lr.lanes > 1 && b.lane === 1) ? (y1 + h - laneH) : y1;
|
|
12220
|
+
let d = forester.geneArrowPath(b.x, y, b.w, laneH, b.dir);
|
|
12221
|
+
if (d === '') {
|
|
12222
|
+
return;
|
|
12223
|
+
}
|
|
12224
|
+
let info = {gene: b.gene, family: fam, tip: rowTip, flipped: lr.flip};
|
|
12225
|
+
bodies.push({d: d, fill: base, stroke: forester.domainDarken(base, 0.3), transform: row.transform,
|
|
12226
|
+
info: info, link: !!forester.geneReference(b.gene)});
|
|
12227
|
+
let label = geneLabel(b.gene);
|
|
12228
|
+
let head = Math.min(laneH / 2, b.w);
|
|
12229
|
+
if (row.labelsOn && label && fs > 4 && legendTextWidth(label, font) <= b.w - head - 4) {
|
|
12230
|
+
// centred on the body, which the head is not part of
|
|
12231
|
+
names.push({x: b.x + ((b.dir > 0 ? 0 : head) + ((b.w - head) / 2)), y: y + (laneH / 2), font: font,
|
|
12232
|
+
fill: forester.domainLabelInk(base), text: label});
|
|
12233
|
+
}
|
|
12234
|
+
});
|
|
12235
|
+
});
|
|
12236
|
+
if (trackParts.length > 0) {
|
|
12237
|
+
under.push({d: trackParts.join(''), transform: null});
|
|
12238
|
+
}
|
|
12239
|
+
|
|
12240
|
+
// ---- draw: bottom to top, reusing the elements already there
|
|
12241
|
+
let paths = sizeLayer(underLayer, 'path', under.length);
|
|
12242
|
+
under.forEach(function (u, k) {
|
|
12243
|
+
paths[k].setAttribute('d', u.d);
|
|
12244
|
+
if (u.transform) {
|
|
12245
|
+
paths[k].setAttribute('transform', u.transform);
|
|
12246
|
+
} else {
|
|
12247
|
+
paths[k].removeAttribute('transform');
|
|
12248
|
+
}
|
|
12249
|
+
paths[k].style.fill = DOMAIN_BACKBONE_COLOR;
|
|
12250
|
+
});
|
|
12251
|
+
geneHoverOut(); // a redraw can take the hovered arrow away without a mouseout
|
|
12252
|
+
let arrows = sizeLayer(bodyLayer, 'path', bodies.length);
|
|
12253
|
+
bodies.forEach(function (b, k) {
|
|
12254
|
+
let el = arrows[k];
|
|
12255
|
+
el.setAttribute('d', b.d);
|
|
12256
|
+
if (b.transform) {
|
|
12257
|
+
el.setAttribute('transform', b.transform);
|
|
12258
|
+
} else {
|
|
12259
|
+
el.removeAttribute('transform');
|
|
12260
|
+
}
|
|
12261
|
+
el.style.fill = b.fill;
|
|
12262
|
+
el.style.stroke = b.stroke;
|
|
12263
|
+
el.style.strokeWidth = 1;
|
|
12264
|
+
el.style.strokeLinejoin = 'round';
|
|
12265
|
+
el.__aptxGene = b.info;
|
|
12266
|
+
el.style.cursor = b.link ? 'pointer' : 'default'; // a hand only where the click goes somewhere
|
|
12267
|
+
});
|
|
12268
|
+
let texts = sizeLayer(labelLayer, 'text', names.length);
|
|
12269
|
+
names.forEach(function (t, k) {
|
|
12270
|
+
let el = texts[k];
|
|
12271
|
+
el.setAttribute('x', t.x);
|
|
12272
|
+
el.setAttribute('y', t.y);
|
|
12273
|
+
el.setAttribute('dy', '0.35em');
|
|
12274
|
+
el.setAttribute('text-anchor', 'middle');
|
|
12275
|
+
el.style.font = t.font;
|
|
12276
|
+
el.style.fill = t.fill;
|
|
12277
|
+
el.textContent = t.text;
|
|
12278
|
+
});
|
|
12279
|
+
|
|
12280
|
+
// the kb bar under the rows, rectangular only: a round number of base
|
|
12281
|
+
// pairs at this scale, with end ticks, the label after it
|
|
12282
|
+
let ruler = d3.select(rulerLayer);
|
|
12283
|
+
ruler.selectAll('*').remove();
|
|
12284
|
+
let bar = (columnStart !== null && isFinite(lowest)) ? forester.geneScaleBar(f, GENE_KB_BAR_TARGET_PX) : null;
|
|
12285
|
+
if (bar) {
|
|
12286
|
+
let ink = _state.labelColorDefault;
|
|
12287
|
+
let y = lowest + 12;
|
|
12288
|
+
let x0 = columnStart;
|
|
12289
|
+
let x1 = columnStart + bar.px;
|
|
12290
|
+
let fs = Math.max(9, Math.min(_state.externalNodeFontSize, 12));
|
|
12291
|
+
ruler.append('path')
|
|
12292
|
+
.attr('d', 'M' + x0 + ',' + (y - 3) + 'v6M' + x0 + ',' + y + 'H' + x1 + 'M' + x1 + ',' + (y - 3) + 'v6')
|
|
12293
|
+
.style('fill', 'none').style('stroke', ink).style('stroke-width', 1);
|
|
12294
|
+
ruler.append('text')
|
|
12295
|
+
.attr('x', x1 + 5).attr('y', y).attr('dy', '0.35em')
|
|
12296
|
+
.style('font', fs + 'px ' + FONT_DEFAULTS).style('fill', ink)
|
|
12297
|
+
.text(bar.label);
|
|
12298
|
+
}
|
|
12299
|
+
}
|
|
12300
|
+
|
|
12301
|
+
// The gene legend, in "Legend" mode only: the shared families drawn in
|
|
12302
|
+
// the view, in first-appearance order, each with its commonest product
|
|
12303
|
+
// and the number of genomes carrying it; then, when there are any, the
|
|
12304
|
+
// grey rows for the families unique to one genome and the genes without
|
|
12305
|
+
// a family. Its own card with the domain legend's manners: home
|
|
12306
|
+
// bottom-right, a drag keeps its place, a double-click sends it home.
|
|
12307
|
+
function drawGeneLegend() {
|
|
12308
|
+
if (!_baseSvg) {
|
|
12309
|
+
return;
|
|
12310
|
+
}
|
|
12311
|
+
_baseSvg.selectAll('g.' + LEGEND_GENES).remove();
|
|
12312
|
+
if (!_root || !genesShown() || _state.geneLabels !== 'legend') {
|
|
12313
|
+
return;
|
|
12314
|
+
}
|
|
12315
|
+
let tips = displayedTips().filter(function (d) {
|
|
12316
|
+
return d.x !== undefined;
|
|
12317
|
+
}).sort(function (p, q) {
|
|
12318
|
+
return p.x - q.x;
|
|
12319
|
+
});
|
|
12320
|
+
let ns = geneFamilyNamespace();
|
|
12321
|
+
let summary = forester.geneFamilySummary(tips.map(geneRegion), ns, new Set(Object.keys(genePalette())));
|
|
12322
|
+
let size = svgSize();
|
|
12323
|
+
if ((summary.legend.length === 0 && summary.singletons === 0 && summary.unfamilied === 0) || !size) {
|
|
12324
|
+
return;
|
|
12325
|
+
}
|
|
12326
|
+
const FS = Math.max(11, _state.externalNodeFontSize || 11);
|
|
12327
|
+
const PAD = 9;
|
|
12328
|
+
const ROW = FS + 6;
|
|
12329
|
+
const SWATCH = 10;
|
|
12330
|
+
const GAP = 6;
|
|
12331
|
+
const rowFont = FS + 'px ' + FONT_DEFAULTS;
|
|
12332
|
+
const titleFont = '600 ' + (FS + 1) + 'px ' + FONT_DEFAULTS;
|
|
12333
|
+
const ink = _state.labelColorDefault;
|
|
12334
|
+
const frame = _state.branchColorDefault;
|
|
12335
|
+
let title = 'Gene families' + (ns ? ' (' + ns + ')' : '');
|
|
12336
|
+
let rows = summary.legend.map(function (r) {
|
|
12337
|
+
let what = r.product || r.symbol || r.family;
|
|
12338
|
+
return {
|
|
12339
|
+
text: clipTextToWidth(what + ' (' + r.tips + ')', rowFont, DOMAIN_LEGEND_ROW_MAX_PX),
|
|
12340
|
+
color: geneColor(r.family)
|
|
12341
|
+
};
|
|
12342
|
+
});
|
|
12343
|
+
if (summary.singletons > 0) {
|
|
12344
|
+
rows.push({text: 'unique to one genome (' + summary.singletons + ')', color: forester.GENE_SINGLETON_COLOR});
|
|
12345
|
+
}
|
|
12346
|
+
if (summary.unfamilied > 0) {
|
|
12347
|
+
rows.push({text: 'no family (' + summary.unfamilied + ')', color: forester.GENE_NO_FAMILY_COLOR});
|
|
12348
|
+
}
|
|
12349
|
+
let width = legendTextWidth(title, titleFont);
|
|
12350
|
+
rows.forEach(function (r) {
|
|
12351
|
+
width = Math.max(width, SWATCH + GAP + legendTextWidth(r.text, rowFont));
|
|
12352
|
+
});
|
|
12353
|
+
width += 2 * PAD;
|
|
12354
|
+
let height = PAD + ROW + (rows.length * ROW) + PAD - 2;
|
|
12355
|
+
let x, y;
|
|
12356
|
+
if (_gene.legendFrac) {
|
|
12357
|
+
x = _gene.legendFrac.fx * size.w;
|
|
12358
|
+
y = _gene.legendFrac.fy * size.h;
|
|
12359
|
+
} else {
|
|
12360
|
+
x = size.w - width - DOMAIN_LEGEND_INSET;
|
|
12361
|
+
y = size.h - height - DOMAIN_LEGEND_INSET;
|
|
12362
|
+
}
|
|
12363
|
+
x = Math.max(0, Math.min(size.w - 20, x));
|
|
12364
|
+
y = Math.max(0, Math.min(size.h - 20, y));
|
|
12365
|
+
|
|
12366
|
+
let g = _baseSvg.append('g').attr('class', LEGEND_GENES)
|
|
12367
|
+
.style('cursor', 'move')
|
|
12368
|
+
.call(d3.drag()
|
|
12369
|
+
.on('start', function (event) {
|
|
12370
|
+
if (event.sourceEvent) {
|
|
12371
|
+
event.sourceEvent.stopPropagation(); // not a pan of the tree
|
|
12372
|
+
}
|
|
12373
|
+
})
|
|
12374
|
+
.on('drag', function (event) {
|
|
12375
|
+
x += event.dx;
|
|
12376
|
+
y += event.dy;
|
|
12377
|
+
_gene.legendFrac = {fx: x / size.w, fy: y / size.h};
|
|
12378
|
+
drawGeneLegend();
|
|
12379
|
+
}))
|
|
12380
|
+
.on('dblclick', function (event) {
|
|
12381
|
+
event.stopPropagation();
|
|
12382
|
+
_gene.legendFrac = null;
|
|
12383
|
+
drawGeneLegend();
|
|
12384
|
+
});
|
|
12385
|
+
g.append('title').text('drag to move; double-click to send it back to the corner');
|
|
12386
|
+
g.append('rect')
|
|
12387
|
+
.attr('x', x).attr('y', y)
|
|
12388
|
+
.attr('width', width).attr('height', height)
|
|
12389
|
+
.attr('rx', 5)
|
|
12390
|
+
.style('fill', _state.backgroundColorDefault)
|
|
12391
|
+
.style('fill-opacity', 0.92)
|
|
12392
|
+
.style('stroke', frame)
|
|
12393
|
+
.style('stroke-opacity', 0.5);
|
|
12394
|
+
let baseline = y + PAD + FS;
|
|
12395
|
+
g.append('text')
|
|
12396
|
+
.attr('x', x + PAD).attr('y', baseline)
|
|
12397
|
+
.style('font', titleFont)
|
|
12398
|
+
.style('fill', ink)
|
|
12399
|
+
.text(title);
|
|
12400
|
+
rows.forEach(function (r) {
|
|
12401
|
+
baseline += ROW;
|
|
12402
|
+
g.append('rect')
|
|
12403
|
+
.attr('x', x + PAD).attr('y', baseline - SWATCH + 1)
|
|
12404
|
+
.attr('width', SWATCH).attr('height', SWATCH)
|
|
12405
|
+
.attr('rx', 2)
|
|
12406
|
+
.style('fill', r.color);
|
|
12407
|
+
g.append('text')
|
|
12408
|
+
.attr('x', x + PAD + SWATCH + GAP).attr('y', baseline)
|
|
12409
|
+
.style('font', rowFont)
|
|
12410
|
+
.style('fill', ink)
|
|
12411
|
+
.text(r.text);
|
|
12412
|
+
});
|
|
12413
|
+
}
|
|
12414
|
+
|
|
12415
|
+
// ---- the controls follow the toggle: hidden without it; while shown the
|
|
12416
|
+
// step buttons, the two selects and the label mode say where things stand
|
|
12417
|
+
function fillSelect(sel, options, value) {
|
|
12418
|
+
sel.textContent = '';
|
|
12419
|
+
options.forEach(function (o) {
|
|
12420
|
+
let opt = document.createElement('option');
|
|
12421
|
+
opt.value = o.value;
|
|
12422
|
+
opt.textContent = o.text;
|
|
12423
|
+
if (o.title) {
|
|
12424
|
+
opt.title = o.title;
|
|
12425
|
+
}
|
|
12426
|
+
sel.appendChild(opt);
|
|
12427
|
+
});
|
|
12428
|
+
sel.value = value;
|
|
12429
|
+
}
|
|
12430
|
+
|
|
12431
|
+
function geneFamilyShortName(c) {
|
|
12432
|
+
return c.symbol || c.product || c.family;
|
|
12433
|
+
}
|
|
12434
|
+
|
|
12435
|
+
function syncGeneControls() {
|
|
12436
|
+
let fs = byId(GENE_CONTROLS);
|
|
12437
|
+
if (!fs) {
|
|
12438
|
+
return;
|
|
12439
|
+
}
|
|
12440
|
+
let on = _state.showGenes === true && _basicTreeProperties.geneRegions === true && _gene !== null;
|
|
12441
|
+
setCheckboxValue(GENES_CB, _state.showGenes === true);
|
|
12442
|
+
fs.style.display = on ? '' : 'none';
|
|
12443
|
+
if (!on) {
|
|
12444
|
+
return;
|
|
12445
|
+
}
|
|
12446
|
+
let w = geneTrackWidth();
|
|
12447
|
+
(w > DOMAIN_WIDTH_MIN ? enableButton : disableButton)(byId(GENE_WIDTH_DEC));
|
|
12448
|
+
(w < DOMAIN_WIDTH_MAX ? enableButton : disableButton)(byId(GENE_WIDTH_INC));
|
|
12449
|
+
let regions = geneTreeRegions();
|
|
12450
|
+
let ns = geneFamilyNamespace();
|
|
12451
|
+
// the anchor: Auto (naming its pick), every family that can anchor
|
|
12452
|
+
// two genomes or more -- most first -- and None
|
|
12453
|
+
let sel = byId(GENE_ANCHOR_SELECT);
|
|
12454
|
+
if (sel) {
|
|
12455
|
+
let auto = forester.geneDefaultAnchor(regions, ns);
|
|
12456
|
+
let cands = forester.geneAnchorCandidates(regions, ns);
|
|
12457
|
+
let byFamily = Object.create(null);
|
|
12458
|
+
cands.forEach(function (c) {
|
|
12459
|
+
byFamily[c.family] = c;
|
|
12460
|
+
});
|
|
12461
|
+
let options = [{value: 'auto', text: 'Auto' + (auto ? ' (' + geneFamilyShortName(byFamily[auto]) + ')' : ' (none)'),
|
|
12462
|
+
title: auto ? auto : 'no family is single-copy in half the genomes'}];
|
|
12463
|
+
cands.forEach(function (c) {
|
|
12464
|
+
if (c.single >= 2 || c.family === _state.geneAnchor) {
|
|
12465
|
+
options.push({value: c.family, text: geneFamilyShortName(c) + ' (' + c.single + '/' + regions.length + ')',
|
|
12466
|
+
title: c.family + (c.product && c.symbol ? ' — ' + c.product : '')});
|
|
12467
|
+
}
|
|
12468
|
+
});
|
|
12469
|
+
if (_state.geneAnchor !== 'auto' && _state.geneAnchor !== 'none' && !byFamily[_state.geneAnchor]) {
|
|
12470
|
+
options.push({value: _state.geneAnchor, text: _state.geneAnchor + ' (not in this tree)', title: _state.geneAnchor});
|
|
12471
|
+
}
|
|
12472
|
+
options.push({value: 'none', text: 'None (left-aligned)', title: 'no anchor: every row starts at its left end'});
|
|
12473
|
+
fillSelect(sel, options, _state.geneAnchor);
|
|
12474
|
+
}
|
|
12475
|
+
// the namespace: offered when the genes carry more than one
|
|
12476
|
+
let nss = forester.geneFamilyNamespaces(regions);
|
|
12477
|
+
let row = byId(GENE_FAMILY_SELECT + '_row');
|
|
12478
|
+
let fsel = byId(GENE_FAMILY_SELECT);
|
|
12479
|
+
if (fsel && row) {
|
|
12480
|
+
let options = nss.map(function (x) {
|
|
12481
|
+
return {value: x.ns, text: x.ns + ' (' + countNumber(x.genes) + ' genes)', title: 'annotations with the prefix ' + x.ns + ':'};
|
|
12482
|
+
});
|
|
12483
|
+
if (ns && !nss.some(function (x) {
|
|
12484
|
+
return x.ns === ns;
|
|
12485
|
+
})) {
|
|
12486
|
+
options.push({value: ns, text: ns + ' (not in this tree)', title: ns});
|
|
12487
|
+
}
|
|
12488
|
+
fillSelect(fsel, options, ns || '');
|
|
12489
|
+
row.style.display = options.length > 1 ? '' : 'none';
|
|
12490
|
+
}
|
|
12491
|
+
setValue(GENE_LABELS_SELECT, _state.geneLabels);
|
|
12492
|
+
}
|
|
12493
|
+
|
|
12494
|
+
function genesCbClicked() {
|
|
12495
|
+
_state.showGenes = getCheckboxValue(GENES_CB);
|
|
12496
|
+
if (_state.showGenes && radialDisplay()) {
|
|
12497
|
+
_radialLabelsHorizontal = false; // the arrows ride the spokes, so the labels must too
|
|
12498
|
+
syncZoomRowButtons();
|
|
12499
|
+
}
|
|
12500
|
+
syncGeneControls();
|
|
12501
|
+
scheduleUpdate(null, 0);
|
|
12502
|
+
}
|
|
12503
|
+
|
|
12504
|
+
function geneWidthStep(grow) {
|
|
12505
|
+
let w = geneTrackWidth();
|
|
12506
|
+
if (grow ? w >= DOMAIN_WIDTH_MAX : w <= DOMAIN_WIDTH_MIN) {
|
|
12507
|
+
return;
|
|
12508
|
+
}
|
|
12509
|
+
setGeneTrackWidth(w * (grow ? DOMAIN_WIDTH_GROW : DOMAIN_WIDTH_SHRINK));
|
|
12510
|
+
syncGeneControls();
|
|
12511
|
+
if (radialDisplay()) {
|
|
12512
|
+
scheduleUpdate(null, 0, true);
|
|
12513
|
+
afterUpdate(zoomToFit);
|
|
12514
|
+
} else {
|
|
12515
|
+
scheduleUpdate(null, 0);
|
|
12516
|
+
}
|
|
12517
|
+
}
|
|
12518
|
+
|
|
12519
|
+
function geneAnchorChanged() {
|
|
12520
|
+
let v = getValue(GENE_ANCHOR_SELECT);
|
|
12521
|
+
if (!v || v === _state.geneAnchor) {
|
|
12522
|
+
return;
|
|
12523
|
+
}
|
|
12524
|
+
_state.geneAnchor = v;
|
|
12525
|
+
scheduleUpdate(null, 0, true);
|
|
12526
|
+
}
|
|
12527
|
+
|
|
12528
|
+
function geneFamilyChanged() {
|
|
12529
|
+
let v = getValue(GENE_FAMILY_SELECT);
|
|
12530
|
+
if (!v || v === geneFamilyNamespace()) {
|
|
12531
|
+
return;
|
|
12532
|
+
}
|
|
12533
|
+
_state.geneFamily = v;
|
|
12534
|
+
_state.geneAnchor = 'auto'; // the old anchor was a family of the old namespace
|
|
12535
|
+
_gene.palette = null; // the families change with the namespace
|
|
12536
|
+
syncGeneControls();
|
|
12537
|
+
scheduleUpdate(null, 0, true);
|
|
12538
|
+
}
|
|
12539
|
+
|
|
12540
|
+
function geneLabelsChanged() {
|
|
12541
|
+
_state.geneLabels = getValue(GENE_LABELS_SELECT);
|
|
12542
|
+
scheduleUpdate(null, 0, true);
|
|
12543
|
+
}
|
|
12544
|
+
|
|
11741
12545
|
// ===================== Time axis =====================
|
|
11742
12546
|
// The desktop's time overlays, drawn beneath a rectangular PHYLOGRAM of
|
|
11743
12547
|
// a dated tree: the two-band ICS geologic axis with a "Ma before
|
|
@@ -12502,7 +13306,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
12502
13306
|
return;
|
|
12503
13307
|
}
|
|
12504
13308
|
let labelSpace = (_maxLabelLength * _state.externalNodeFontSize * LABEL_SIZE_CALC_FACTOR) + LABEL_SIZE_CALC_ADDITION;
|
|
12505
|
-
let outer = maxRad + labelSpace + domainRadialExtent() + heatmapRadialExtent();
|
|
13309
|
+
let outer = maxRad + labelSpace + domainRadialExtent() + geneRadialExtent() + heatmapRadialExtent();
|
|
12506
13310
|
let W = +_baseSvg.attr('width'), H = +_baseSvg.attr('height');
|
|
12507
13311
|
let scale = 0.9 * (Math.min(W, H) / (2 * outer));
|
|
12508
13312
|
if (!isFinite(scale) || scale <= 0) {
|
|
@@ -13561,12 +14365,14 @@ function (root, d3, forester, phyloXml) {
|
|
|
13561
14365
|
search0();
|
|
13562
14366
|
search1();
|
|
13563
14367
|
}
|
|
13564
|
-
if (radialDisplay() && _state.showDomainArchitectures && _basicTreeProperties.domainArchitectures)
|
|
13565
|
-
|
|
14368
|
+
if (radialDisplay() && ((_state.showDomainArchitectures && _basicTreeProperties.domainArchitectures)
|
|
14369
|
+
|| (_state.showGenes && _basicTreeProperties.geneRegions))) {
|
|
14370
|
+
// the domain boxes and gene arrows ride the tips' spokes, so the labels must too
|
|
13566
14371
|
_radialLabelsHorizontal = false;
|
|
13567
14372
|
}
|
|
13568
14373
|
syncZoomRowButtons();
|
|
13569
14374
|
afterUpdate(syncDomainControls); // the track width buttons follow the layout's own width
|
|
14375
|
+
afterUpdate(syncGeneControls);
|
|
13570
14376
|
zoomToFit();
|
|
13571
14377
|
}
|
|
13572
14378
|
|
|
@@ -13777,7 +14583,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13777
14583
|
}
|
|
13778
14584
|
|
|
13779
14585
|
// Which properties the label shows, and in what order: each field ticked
|
|
13780
|
-
// or not, and moved with the arrows
|
|
14586
|
+
// or not, and moved by dragging, with the arrows, or with the arrow keys. Apply keeps the ticked
|
|
13781
14587
|
// ones in the order shown; picking a field that was not shown turns the
|
|
13782
14588
|
// Properties label on, as the desktop does, since otherwise choosing it
|
|
13783
14589
|
// would visibly do nothing. "All" goes back to every field.
|
|
@@ -13810,10 +14616,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
13810
14616
|
}).map(function (w) {
|
|
13811
14617
|
return w.ref;
|
|
13812
14618
|
});
|
|
13813
|
-
let shell = makeDialogShell(LABEL_FIELDS_DIALOG, '
|
|
14619
|
+
let shell = makeDialogShell(LABEL_FIELDS_DIALOG, 'Metadata fields', 340);
|
|
13814
14620
|
let body = shell.body;
|
|
13815
14621
|
repsElement(body, 'div', 'aptx-reps-lead',
|
|
13816
|
-
'Tick the
|
|
14622
|
+
'Tick the metadata fields the labels show; drag a field, or use the arrows, to set their order.');
|
|
13817
14623
|
let list = repsElement(body, 'div', 'aptx-reorder');
|
|
13818
14624
|
|
|
13819
14625
|
function move(from, to) {
|
|
@@ -13830,6 +14636,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
13830
14636
|
working.forEach(function (w, i) {
|
|
13831
14637
|
let row = repsElement(list, 'div', 'aptx-reorder-row');
|
|
13832
14638
|
row.tabIndex = 0;
|
|
14639
|
+
repsElement(row, 'span', 'aptx-reorder-grip', '≡');
|
|
13833
14640
|
let box = repsElement(row, 'input', 'aptx-reorder-check');
|
|
13834
14641
|
box.type = 'checkbox';
|
|
13835
14642
|
box.checked = w.on;
|
|
@@ -13871,15 +14678,26 @@ function (root, d3, forester, phyloXml) {
|
|
|
13871
14678
|
list.children[i + d].focus();
|
|
13872
14679
|
}
|
|
13873
14680
|
});
|
|
14681
|
+
attachReorderDrag(list, row, working, draw);
|
|
13874
14682
|
});
|
|
13875
14683
|
}
|
|
13876
14684
|
draw();
|
|
13877
14685
|
|
|
13878
14686
|
let actions = repsElement(body, 'div', 'aptx-reps-actions');
|
|
14687
|
+
// All and None tick the boxes and leave the dialog open for Apply --
|
|
14688
|
+
// All used to mean "back to the default" and close at once, which
|
|
14689
|
+
// read as the dialog vanishing (Christian, 2026-10-01)
|
|
14690
|
+
let tickAll = function (on) {
|
|
14691
|
+
working.forEach(function (w) {
|
|
14692
|
+
w.on = on;
|
|
14693
|
+
});
|
|
14694
|
+
draw();
|
|
14695
|
+
};
|
|
13879
14696
|
repsButton(actions, 'All', false, function () {
|
|
13880
|
-
|
|
13881
|
-
|
|
13882
|
-
|
|
14697
|
+
tickAll(true);
|
|
14698
|
+
});
|
|
14699
|
+
repsButton(actions, 'None', false, function () {
|
|
14700
|
+
tickAll(false);
|
|
13883
14701
|
});
|
|
13884
14702
|
repsButton(actions, 'Cancel', false, function () {
|
|
13885
14703
|
shell.dialog.close();
|
|
@@ -14100,7 +14918,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14100
14918
|
}
|
|
14101
14919
|
let part = subtreeExtentNote();
|
|
14102
14920
|
items.push({heading: part ? 'Tree: ' + part : 'Tree'});
|
|
14103
|
-
items.push({label: 'phyloXML', detail: 'keeps everything the tree carries: taxonomy, sequences, dates,
|
|
14921
|
+
items.push({label: 'phyloXML', detail: 'keeps everything the tree carries: taxonomy, sequences, dates, metadata',
|
|
14104
14922
|
action: entry(PHYLOXML_EXPORT_FORMAT)});
|
|
14105
14923
|
items.push({label: 'Newick', detail: 'names, branch lengths and support values only',
|
|
14106
14924
|
action: entry(NH_EXPORT_FORMAT)});
|
|
@@ -14111,7 +14929,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14111
14929
|
// Fasta entries) -- on any other tree the download would be empty
|
|
14112
14930
|
items.push({heading: part ? 'Tip data: ' + part : 'Tip data'});
|
|
14113
14931
|
items.push({label: 'TSV', detail: 'a table, one row per tip: names, taxonomy, sequence names, '
|
|
14114
|
-
+ 'branch lengths and every
|
|
14932
|
+
+ 'branch lengths and every metadata field', action: entry(TSV_EXPORT_FORMAT)});
|
|
14115
14933
|
if (_basicTreeProperties && _basicTreeProperties.maxMolSeqLength > 0) {
|
|
14116
14934
|
items.push({label: 'FASTA', detail: 'every molecular sequence the tips carry',
|
|
14117
14935
|
action: entry(FASTA_EXPORT_FORMAT)});
|
|
@@ -14486,7 +15304,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14486
15304
|
const PANEL_SECTIONS_KEY = 'aptx-panel-sections';
|
|
14487
15305
|
// Download too (Christian, 2026-09-28): four rows, and not something a
|
|
14488
15306
|
// session uses often.
|
|
14489
|
-
const PANEL_SECTIONS_CLOSED_BY_DEFAULT = ['View & Tools', 'Sizes', 'Domain Architectures', 'Download'];
|
|
15307
|
+
const PANEL_SECTIONS_CLOSED_BY_DEFAULT = ['View & Tools', 'Sizes', 'Domain Architectures', 'Genome regions', 'Download'];
|
|
14490
15308
|
|
|
14491
15309
|
function loadPanelSections() {
|
|
14492
15310
|
if (_panelSections) {
|
|
@@ -15816,6 +16634,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
15816
16634
|
if (s.type) {
|
|
15817
16635
|
text += '- Type: ' + s.type + '<br>';
|
|
15818
16636
|
}
|
|
16637
|
+
// The annotations, which this dialog never listed and which a
|
|
16638
|
+
// gene's family lives in (<annotation ref="pgfam:...">): the
|
|
16639
|
+
// ref, with the description when there is one.
|
|
16640
|
+
(s.annotations || []).forEach(function (an) {
|
|
16641
|
+
if (!an || (!an.ref && !an.desc)) {
|
|
16642
|
+
return;
|
|
16643
|
+
}
|
|
16644
|
+
text += '- Annotation: ' + (an.ref ? an.ref + (an.desc ? ' (' + an.desc + ')' : '') : an.desc) + '<br>';
|
|
16645
|
+
});
|
|
15819
16646
|
// The domain architecture, which phyloXML has always carried
|
|
15820
16647
|
// and this dialog has never shown: a reader could find a node
|
|
15821
16648
|
// by searching for one of its domains -- forester indexes them
|
|
@@ -15874,7 +16701,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
15874
16701
|
}
|
|
15875
16702
|
}
|
|
15876
16703
|
if (rows) {
|
|
15877
|
-
text += '
|
|
16704
|
+
text += 'Metadata<br>' + rows;
|
|
15878
16705
|
}
|
|
15879
16706
|
}
|
|
15880
16707
|
return text;
|
|
@@ -16271,6 +17098,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
16271
17098
|
statOf(st.tipsWithDomains, st.tips, 'tips')
|
|
16272
17099
|
+ (bp.maxDomainArchitectureLength > 0
|
|
16273
17100
|
? (', up to ' + countNumber(bp.maxDomainArchitectureLength) + ' residues') : ''));
|
|
17101
|
+
carries('Genome regions', st.tipsWithGenes > 0,
|
|
17102
|
+
statOf(st.tipsWithGenes, st.tips, 'tips')
|
|
17103
|
+
+ (bp.maxGeneRegionLength > 0
|
|
17104
|
+
? (', up to ' + countNumber(bp.maxGeneRegionLength) + ' bp') : ''));
|
|
16274
17105
|
carries('Dates', st.nodesWithDate > 0, statOf(st.nodesWithDate, st.nodes, 'nodes'));
|
|
16275
17106
|
carries('Distributions', st.tipsWithDistribution > 0,
|
|
16276
17107
|
statOf(st.tipsWithDistribution, st.tips, 'tips'));
|
|
@@ -16290,7 +17121,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
16290
17121
|
carries('Events', true, parts.join(', '));
|
|
16291
17122
|
}
|
|
16292
17123
|
st.propertyRefs.forEach(function (r) {
|
|
16293
|
-
carries('
|
|
17124
|
+
carries('Metadata ' + r.ref, true, statOf(r.nodes, st.nodes, 'nodes'));
|
|
16294
17125
|
});
|
|
16295
17126
|
if (carried === 0) {
|
|
16296
17127
|
treePropRow(box, 'Annotations', 'none, names and topology only');
|
|
@@ -16543,6 +17374,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
16543
17374
|
|
|
16544
17375
|
c0.insertAdjacentHTML('beforeend',makeDomainControls());
|
|
16545
17376
|
|
|
17377
|
+
c0.insertAdjacentHTML('beforeend',makeGeneControls());
|
|
17378
|
+
|
|
16546
17379
|
c0.insertAdjacentHTML('beforeend',makeHeatmapControls());
|
|
16547
17380
|
|
|
16548
17381
|
c0.insertAdjacentHTML('beforeend',makeZoomControl());
|
|
@@ -16672,6 +17505,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
16672
17505
|
on(DOMAIN_EVALUE_INC, 'click', function () { domainEvalueStep(1); });
|
|
16673
17506
|
on(DOMAIN_LABELS_SELECT, 'change', domainLabelsChanged);
|
|
16674
17507
|
on(DOMAIN_GLOW_CB, 'click', domainGlowCbClicked);
|
|
17508
|
+
on(GENES_CB, 'click', genesCbClicked);
|
|
17509
|
+
onHoldRepeat(GENE_WIDTH_DEC, function () { geneWidthStep(false); });
|
|
17510
|
+
onHoldRepeat(GENE_WIDTH_INC, function () { geneWidthStep(true); });
|
|
17511
|
+
on(GENE_ANCHOR_SELECT, 'change', geneAnchorChanged);
|
|
17512
|
+
on(GENE_FAMILY_SELECT, 'change', geneFamilyChanged);
|
|
17513
|
+
on(GENE_LABELS_SELECT, 'change', geneLabelsChanged);
|
|
16675
17514
|
on(SCALE_AXIS_CB, 'click', scaleAxisCbClicked);
|
|
16676
17515
|
on(SCALE_GRID_CB, 'click', scaleGridCbClicked);
|
|
16677
17516
|
|
|
@@ -17152,8 +17991,10 @@ function (root, d3, forester, phyloXml) {
|
|
|
17152
17991
|
labels.push(makeCheckboxItem('Sequence', SEQUENCE_CB, 'to show/hide node sequence information'));
|
|
17153
17992
|
}
|
|
17154
17993
|
if (labelFields().length > 0) {
|
|
17155
|
-
|
|
17156
|
-
|
|
17994
|
+
// "Metadata", not phyloXML's "properties" (Christian, 2026-10-01):
|
|
17995
|
+
// the word the users' own tools use for these fields
|
|
17996
|
+
labels.push(makeCheckboxItem('Metadata', PROPERTIES_CB, 'to show/hide the nodes\' metadata (phyloXML'
|
|
17997
|
+
+ ' properties: species, host, …) in their labels; Metadata fields… chooses which, and in what order'));
|
|
17157
17998
|
}
|
|
17158
17999
|
if (_nodeLabels) {
|
|
17159
18000
|
for (const [key, value] of Object.entries(_nodeLabels)) {
|
|
@@ -17219,6 +18060,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
17219
18060
|
if (_basicTreeProperties.domainArchitectures) {
|
|
17220
18061
|
opts.push(makeCheckboxItem('Domain Architectures', DOMAINS_CB, 'to show/hide the protein domain architectures beside the tips', true));
|
|
17221
18062
|
}
|
|
18063
|
+
if (_basicTreeProperties.geneRegions) {
|
|
18064
|
+
opts.push(makeCheckboxItem('Genes', GENES_CB, 'to show/hide the genome regions beside the tips: each gene an arrow on a backbone, coloured by family, the rows lined up on one family'));
|
|
18065
|
+
}
|
|
17222
18066
|
if (heatmapAvailable()) {
|
|
17223
18067
|
opts.push(makeCheckboxItem('Heat Map', HEATMAP_CB, 'to show/hide a cell per tip and numeric field beside the tree, every column on one color scale; concentric rings in the circular layout (not in the unrooted one, where a column has no ring to be)'));
|
|
17224
18068
|
}
|
|
@@ -17238,9 +18082,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
17238
18082
|
h = h.concat(makeCheckboxGroup('Labels', labels));
|
|
17239
18083
|
if (labelFields().length > 0) {
|
|
17240
18084
|
h = h.concat('<div class="aptx-domrow"><span class="aptx-domlabel"></span>'
|
|
17241
|
-
+ '<input type="button" class="aptx-widebtn" value="
|
|
17242
|
-
+ '" id="' + LABEL_FIELDS_BUTTON + '" title="choose which
|
|
17243
|
-
+ ' order"></div>');
|
|
18085
|
+
+ '<input type="button" class="aptx-widebtn" value="Metadata fields…" name="' + LABEL_FIELDS_BUTTON
|
|
18086
|
+
+ '" id="' + LABEL_FIELDS_BUTTON + '" title="choose which metadata fields the labels show, and in'
|
|
18087
|
+
+ ' what order"></div>');
|
|
17244
18088
|
}
|
|
17245
18089
|
h = h.concat(makeCheckboxGroup('Nodes', nodes));
|
|
17246
18090
|
h = h.concat(makeCheckboxGroup('Options', opts));
|
|
@@ -17357,6 +18201,40 @@ function (root, d3, forester, phyloXml) {
|
|
|
17357
18201
|
return h;
|
|
17358
18202
|
}
|
|
17359
18203
|
|
|
18204
|
+
// The gene track's controls, shown only while the Genes toggle is on
|
|
18205
|
+
// (syncGeneControls): the track width, the anchor family, the family
|
|
18206
|
+
// namespace the colours follow (shown when the genes carry more than
|
|
18207
|
+
// one), and the label mode. The two selects are filled per tree by
|
|
18208
|
+
// syncGeneControls, since their options come from the genes.
|
|
18209
|
+
function makeGeneControls() {
|
|
18210
|
+
let h = '<fieldset id="' + GENE_CONTROLS + '" style="display:none">';
|
|
18211
|
+
h = h.concat('<legend>Genome regions</legend>');
|
|
18212
|
+
h = h.concat('<div class="aptx-domrow"><span class="aptx-domlabel">Track width</span>');
|
|
18213
|
+
h = h.concat(makeButton('−', GENE_WIDTH_DEC, 'narrow the gene track (hold to repeat)'));
|
|
18214
|
+
h = h.concat(makeButton('+', GENE_WIDTH_INC, 'widen the gene track (hold to repeat)'));
|
|
18215
|
+
h = h.concat('</div>');
|
|
18216
|
+
h = h.concat('<div class="aptx-domrow"><label class="aptx-domlabel" for="' + GENE_ANCHOR_SELECT + '">Anchor</label>');
|
|
18217
|
+
h = h.concat('<select name="' + GENE_ANCHOR_SELECT + '" id="' + GENE_ANCHOR_SELECT
|
|
18218
|
+
+ '" title="the family the rows line up on: its gene sits at the same place in every genome, pointing'
|
|
18219
|
+
+ ' right, and the neighbours follow (a genome with it on the other strand is mirrored). Auto picks the'
|
|
18220
|
+
+ ' family single-copy in the most genomes; None left-aligns the rows"></select></div>');
|
|
18221
|
+
h = h.concat('<div class="aptx-domrow" id="' + GENE_FAMILY_SELECT + '_row"><label class="aptx-domlabel" for="'
|
|
18222
|
+
+ GENE_FAMILY_SELECT + '">Family</label>');
|
|
18223
|
+
h = h.concat('<select name="' + GENE_FAMILY_SELECT + '" id="' + GENE_FAMILY_SELECT
|
|
18224
|
+
+ '" title="which family annotation colours the genes and anchors the rows (a BV-BRC export carries'
|
|
18225
|
+
+ ' both PGfams and PLfams)"></select></div>');
|
|
18226
|
+
h = h.concat('<div class="aptx-domrow"><label class="aptx-domlabel" for="' + GENE_LABELS_SELECT + '">Labels</label>');
|
|
18227
|
+
h = h.concat('<select name="' + GENE_LABELS_SELECT + '" id="' + GENE_LABELS_SELECT
|
|
18228
|
+
+ '" title="where the gene symbols go: on the arrows (rectangular layout only), in a legend of the'
|
|
18229
|
+
+ ' families, or nowhere">');
|
|
18230
|
+
h = h.concat('<option value="genes">On genes</option>');
|
|
18231
|
+
h = h.concat('<option value="legend">Legend</option>');
|
|
18232
|
+
h = h.concat('<option value="none">None</option>');
|
|
18233
|
+
h = h.concat('</select></div>');
|
|
18234
|
+
h = h.concat('</fieldset>');
|
|
18235
|
+
return h;
|
|
18236
|
+
}
|
|
18237
|
+
|
|
17360
18238
|
// Shown only while the Heat Map toggle is on (syncHeatmapControls).
|
|
17361
18239
|
// The desktop's View > Order Matrix Columns, minus its Manual mode --
|
|
17362
18240
|
// that one means "the order you dragged the rows into", and there is
|
|
@@ -17500,12 +18378,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
17500
18378
|
h = h.concat('<fieldset>');
|
|
17501
18379
|
h = h.concat('<legend>Visualizations</legend>');
|
|
17502
18380
|
if (hasColorVisualizations()) {
|
|
17503
|
-
h = h.concat(makeSelectMenu('Color by:', '<br>', LABEL_COLOR_SELECT_MENU, 'colorize the node label and the node itself
|
|
18381
|
+
h = h.concat(makeSelectMenu('Color by:', '<br>', LABEL_COLOR_SELECT_MENU, 'colorize the node label and the node itself by a field the tree carries'));
|
|
17504
18382
|
h = h.concat('<br>');
|
|
17505
18383
|
h = h.concat('<br>');
|
|
17506
18384
|
}
|
|
17507
18385
|
if (hasShapeVisualizations()) {
|
|
17508
|
-
h = h.concat(makeSelectMenu('Shape:', '<br>', NODE_SHAPE_SELECT_MENU, 'change the node shape
|
|
18386
|
+
h = h.concat(makeSelectMenu('Shape:', '<br>', NODE_SHAPE_SELECT_MENU, 'change the node shape by a field the tree carries'));
|
|
17509
18387
|
h = h.concat('<br>');
|
|
17510
18388
|
}
|
|
17511
18389
|
h = h.concat('</fieldset>');
|
|
@@ -17603,6 +18481,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
17603
18481
|
setCheckboxValue(MSA_LOGO_CB, _state.showMsaLogo === true);
|
|
17604
18482
|
setCheckboxValue(HEATMAP_CB, _state.showHeatmap);
|
|
17605
18483
|
syncDomainControls();
|
|
18484
|
+
syncGeneControls();
|
|
17606
18485
|
syncHeatmapControls();
|
|
17607
18486
|
setCheckboxValue(SCALE_AXIS_CB, _state.showScaleAxis);
|
|
17608
18487
|
setCheckboxValue(SCALE_GRID_CB, _state.showScaleGrid);
|
|
@@ -18065,7 +18944,8 @@ function (root, d3, forester, phyloXml) {
|
|
|
18065
18944
|
// colour and shape legends, the domain legend, the heat-map ring's scale.
|
|
18066
18945
|
// On screen they sit where the window has room; in a full-size figure the
|
|
18067
18946
|
// window is gone, so they are stacked beside the tree instead.
|
|
18068
|
-
const WINDOW_LEGEND_CLASSES = [LEGEND_LABEL_COLOR, LEGEND_NODE_SHAPE, LEGEND_DOMAINS,
|
|
18947
|
+
const WINDOW_LEGEND_CLASSES = [LEGEND_LABEL_COLOR, LEGEND_NODE_SHAPE, LEGEND_DOMAINS, LEGEND_GENES,
|
|
18948
|
+
'aptx-heatmap-ringlegend'];
|
|
18069
18949
|
const FULL_SIZE_LEGEND_STACK_GAP = 12;
|
|
18070
18950
|
|
|
18071
18951
|
// The enlarged drawing, cropped to what is actually drawn: the tree
|