archaeopteryx 3.11.0 → 3.12.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 +51 -4
- package/archaeopteryx.d.ts +6 -0
- package/archaeopteryx.js +210 -34
- package/forester.js +65 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ config key live and shows the exact config JSON to copy into your own
|
|
|
37
37
|
* [Nucleotide alignment (600 columns)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=alignment_nt)
|
|
38
38
|
* [Genome alignment (150 × 30,000 columns)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=genome_alignment)
|
|
39
39
|
* [Sequence alignment](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=alignment)
|
|
40
|
+
* [Sequence logo (the motif changes by clade)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=sequence_logo)
|
|
40
41
|
* [Influenza HA (annotated)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=influenza)
|
|
41
42
|
* [Dinosaur time tree](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=dinosaur)
|
|
42
43
|
* [Ammonite time tree (fossil ranges)](https://cmzmasek.github.io/archaeopteryx-js/demo.html?tree=ammonite)
|
|
@@ -455,7 +456,9 @@ desktop's Unicode entry: it stays with the text field there, as it should.
|
|
|
455
456
|
A view is what you made of a tree with the panel: the layout and display
|
|
456
457
|
type, which labels show, the colour and shape fields, both searches, the
|
|
457
458
|
clade you switched to, the clades you collapsed, the font, node and branch
|
|
458
|
-
sizes, the rotation, the tracks
|
|
459
|
+
sizes, the rotation, and the tracks — which track is shown, the heat map's
|
|
460
|
+
column order (including one you arranged by hand) and whether the alignment
|
|
461
|
+
is summarised as a logo. On the demo pages it rides in the URL's
|
|
459
462
|
`#` hash and follows every change, so the address bar is always a link to
|
|
460
463
|
what is on screen: copy it (**Copy link to this view** in the toolbar) and
|
|
461
464
|
the recipient opens the same tree in the same view. Opening your own file
|
|
@@ -472,7 +475,9 @@ as the page does.
|
|
|
472
475
|
Embedders get the same four pieces: the handle's `getViewState()` and
|
|
473
476
|
`applyViewState(state)`, the config's `view` (open straight into one) and
|
|
474
477
|
`onViewChange(state, encoded)` (called when it changes), and
|
|
475
|
-
`archaeopteryx.encodeViewState()` / `decodeViewState()` for the hash form
|
|
478
|
+
`archaeopteryx.encodeViewState()` / `decodeViewState()` for the hash form
|
|
479
|
+
(every key the state can hold survives the hash — a test reads `getViewState`
|
|
480
|
+
and fails on any key the codec does not know),
|
|
476
481
|
which reads like
|
|
477
482
|
`layout=circular&colorBy=tax:common_name&show=name,external&font=9&collapsed=12,44&a=HUMAN&af=Any+Text&am=contains`.
|
|
478
483
|
Nodes are named by their launch-time preorder index, so a view belongs to
|
|
@@ -530,6 +535,23 @@ checkbox under Display Data toggles the whole track.
|
|
|
530
535
|
To find a motif, pick **Molecular Sequence** in a search box: it matches the
|
|
531
536
|
residues as written, gap characters included, as the desktop does.
|
|
532
537
|
|
|
538
|
+
**Sequence Logo** (the checkbox under **Alignment**) replaces the conservation
|
|
539
|
+
bar with a **logo**: every column a stack of letters, as tall as that column's
|
|
540
|
+
information content in bits and shared out by residue frequency, most frequent
|
|
541
|
+
on top — the display the MEME Suite and WebLogo draw. A conserved column is one
|
|
542
|
+
tall letter, a variable one a short pile, and the caption gives the scale
|
|
543
|
+
(0 to 2 bits for nucleotides, 0 to 4.3 for amino acids).
|
|
544
|
+
|
|
545
|
+
It summarises **the tips currently on screen**, so entering a clade gives that
|
|
546
|
+
clade's motif rather than the file's, and the caption names how many tips that
|
|
547
|
+
is (`n = 12`). Two consequences worth knowing: gaps are not a letter —
|
|
548
|
+
frequencies are taken over the residues present, and the stack is then scaled
|
|
549
|
+
by the column's occupancy, so a column held up by two sequences out of fifty
|
|
550
|
+
draws short rather than perfectly conserved; and there is **no small-sample
|
|
551
|
+
correction**, because entering a three-tip clade is a normal thing to do and
|
|
552
|
+
Schneider's correction would subtract more than the maximum and leave the
|
|
553
|
+
column blank. Read `n` and judge.
|
|
554
|
+
|
|
533
555
|
Alignments arrive with the tree: as phyloXML `<mol_seq is_aligned="true">`
|
|
534
556
|
elements, or in a **Nexus** file whose characters matrix accompanies its tree.
|
|
535
557
|
The **Nexus** entry in the Download menu writes the current tree *and* its
|
|
@@ -1041,6 +1063,7 @@ copy-pastable JSON.
|
|
|
1041
1063
|
| `layout` | `'rectangular'` | The starting layout: `'rectangular'`, `'circular'`, or `'unrooted'`. |
|
|
1042
1064
|
| `ladderizeTree` | `true` | Ladderize the tree on load: at each node, the larger clade first (any number of children, so a polytomy sorts too). |
|
|
1043
1065
|
| `showMsa` | tree-derived | Open with the alignment track shown. Default: on when the tree carries an aligned `mol_seq`, off otherwise — an explicit `true`/`false` overrides that. |
|
|
1066
|
+
| `showMsaLogo` | `false` | Open with the alignment summarised as a sequence logo instead of a conservation bar: each column a stack of letters as tall as its information content, over the tips currently on screen. Only drawn while the alignment track is shown. |
|
|
1044
1067
|
| `showHeatmap` | `false` | Open with the heat map shown. Offered whenever the tree carries two or more numeric per-tip fields, but off unless asked for: almost any annotated tree has such fields, so turning it on by itself would be an opinion about the tree rather than a service. |
|
|
1045
1068
|
| `heatmapColumnOrder` | tree-derived | How the heat map's columns are ordered: `'document'` (as the file lists them), `'clustered'` (Euclidean), `'clustered-presence'` (Bray–Curtis), `'alphabetical'`, `'frequency'`. The clustered modes also draw the dendrogram. Default: a **clustered** order, with the distance chosen from the values — Bray–Curtis where the matrix has zeros to ignore and nothing negative, Euclidean otherwise. An explicit value always wins and is never re-derived. |
|
|
1046
1069
|
| `heatmapManualOrder` | `null` | The heat map's columns in your own order, as an array of property refs (`['meta:recA', 'meta:gyrA', …]`). Only read while `heatmapColumnOrder` is `'manual'`. A ref the tree has not got is ignored, and a column the list does not name follows the ones it does. |
|
|
@@ -1716,8 +1739,32 @@ which clamps and redraws; the tree never moves. A faint dashed guide runs
|
|
|
1716
1739
|
from each tip's label (or its node, when labels are hidden) across to that
|
|
1717
1740
|
tip's row, so a row reads back to its sequence without counting.
|
|
1718
1741
|
|
|
1719
|
-
The
|
|
1720
|
-
|
|
1742
|
+
**The sequence logo** (`showMsaLogo`, `forester.msaLogo`) replaces the
|
|
1743
|
+
conservation bar and the consensus row rather than joining them: a stack's
|
|
1744
|
+
height *is* the column's conservation and its top letter *is* the consensus,
|
|
1745
|
+
so all three would say one thing three times. Per column the model returns
|
|
1746
|
+
`bits` = `log₂K − H` over the non-gap residues, `occupancy` = non-gap / rows,
|
|
1747
|
+
`height` = `bits × occupancy`, and the letters most frequent first (ties
|
|
1748
|
+
alphabetical, so a figure reproduces). Rows are the **displayed** tips over
|
|
1749
|
+
the visible window, which is what makes entering a clade re-read the summary.
|
|
1750
|
+
**No small-sample correction**: Schneider's `e_n = (K−1)/(2 ln2 · n)` is for a
|
|
1751
|
+
motif sampled from many sequences, and at n = 3 for protein it exceeds the
|
|
1752
|
+
4.32-bit maximum, so a perfectly conserved column of a small clade would draw
|
|
1753
|
+
nothing — the caption names `n` instead.
|
|
1754
|
+
|
|
1755
|
+
Each letter is scaled to fill its slice: `sy = hpx / (ascent + descent)` of
|
|
1756
|
+
**that glyph's** ink box, measured once per character off a canvas, with the
|
|
1757
|
+
baseline placed at `y − descent × sy` so the ink lands inside the slice.
|
|
1758
|
+
Measured off one glyph instead, Q and G hung their descenders through the
|
|
1759
|
+
ruler. Note that an SVG `<text>`'s `getBoundingClientRect` is the *font's*
|
|
1760
|
+
layout box, not the ink, so it cannot check this — `test_trees/msa_logo.html`
|
|
1761
|
+
compares letters and order through the DOM and the ink itself is measured from
|
|
1762
|
+
rendered pixels.
|
|
1763
|
+
|
|
1764
|
+
The conservation bar or logo, the consensus row and the column ruler are a
|
|
1765
|
+
**floating strip** (see the time axes below); the residue rows stay with their
|
|
1766
|
+
tips. The strip's height follows what it holds (`msaBottomReserve()`), and the
|
|
1767
|
+
bottom reserve the fit allows for follows that.
|
|
1721
1768
|
|
|
1722
1769
|
### The heat map
|
|
1723
1770
|
|
package/archaeopteryx.d.ts
CHANGED
|
@@ -98,6 +98,10 @@ export interface ArchaeopteryxConfig {
|
|
|
98
98
|
heatmapManualOrder?: string[] | null;
|
|
99
99
|
showHeatmap?: boolean;
|
|
100
100
|
showMsa?: boolean;
|
|
101
|
+
/** Summarize the alignment as a sequence logo instead of a conservation
|
|
102
|
+
* bar: each column a stack of letters as tall as its information content,
|
|
103
|
+
* over the tips currently on screen. Only drawn with the track shown. */
|
|
104
|
+
showMsaLogo?: boolean;
|
|
101
105
|
showSupportDots?: boolean;
|
|
102
106
|
showTimeAxis?: boolean;
|
|
103
107
|
supportDotMinimum?: number;
|
|
@@ -150,6 +154,8 @@ export interface ViewState {
|
|
|
150
154
|
rotation?: number;
|
|
151
155
|
horizontalLabels?: boolean;
|
|
152
156
|
msa?: boolean;
|
|
157
|
+
/** The alignment summarized as a sequence logo. */
|
|
158
|
+
msaLogo?: boolean;
|
|
153
159
|
heatmap?: boolean;
|
|
154
160
|
heatmapOrder?: 'document' | 'clustered' | 'clustered-presence' | 'alphabetical' | 'frequency' | 'manual';
|
|
155
161
|
heatmapManual?: string[];
|
package/archaeopteryx.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.12.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.12.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';
|
|
@@ -547,7 +548,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
547
548
|
const MSA_MAX_VIEWPORT_FRACTION = 0.6;// nor grows past this share of the display
|
|
548
549
|
const MSA_CONS_BAR_H = 22; // conservation bar band height
|
|
549
550
|
const MSA_CONS_TOP_GAP = 4;
|
|
550
|
-
const
|
|
551
|
+
const MSA_CONS_ROW_H = 13; // the consensus letter row
|
|
552
|
+
const MSA_RULER_H = 17; // the column ruler under it
|
|
553
|
+
const MSA_LOGO_H = 46; // the sequence logo's stack, full height
|
|
554
|
+
const MSA_LOGO_CAP_H = 11; // ... and the caption line above it
|
|
555
|
+
const MSA_LOGO_FONT_PX = 24; // the size logo glyphs are measured at, then scaled
|
|
551
556
|
const MSA_NAV_RESERVE = 46; // ... plus the navigation bar floating at the viewport bottom
|
|
552
557
|
const MSA_MIN_TREE_PX = 220; // the tree itself never shrinks below this
|
|
553
558
|
let _msaColOffset = 0; // first shown alignment column (0-based)
|
|
@@ -4927,6 +4932,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
4927
4932
|
const STATE_KEYS = [
|
|
4928
4933
|
'layout',
|
|
4929
4934
|
'showMsa',
|
|
4935
|
+
'showMsaLogo',
|
|
4930
4936
|
'showHeatmap',
|
|
4931
4937
|
'heatmapColumnOrder',
|
|
4932
4938
|
'heatmapManualOrder',
|
|
@@ -5169,6 +5175,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
5169
5175
|
if (_state.showHeatmap === undefined) {
|
|
5170
5176
|
_state.showHeatmap = false;
|
|
5171
5177
|
}
|
|
5178
|
+
// The logo is a summary of the alignment, not a second view of the
|
|
5179
|
+
// tree, so it waits to be asked for: an alignment opens showing its
|
|
5180
|
+
// residues, which is what a reader came for.
|
|
5181
|
+
if (_state.showMsaLogo === undefined) {
|
|
5182
|
+
_state.showMsaLogo = false;
|
|
5183
|
+
}
|
|
5172
5184
|
// The columns' order. Left unset it is resolved from the data on first
|
|
5173
5185
|
// use (heatmapMode / forester.heatmapDefaultOrder): a clustered order,
|
|
5174
5186
|
// because reading block structure is what a heat map beside a tree is
|
|
@@ -6824,6 +6836,64 @@ function (root, d3, forester, phyloXml) {
|
|
|
6824
6836
|
&& _basicTreeProperties.maxMolSeqLength > 0;
|
|
6825
6837
|
}
|
|
6826
6838
|
|
|
6839
|
+
// The logo replaces the conservation bar and the consensus row rather
|
|
6840
|
+
// than joining them: a stack's height IS the column's conservation and its
|
|
6841
|
+
// top letter IS the consensus, so drawing all three would say the same
|
|
6842
|
+
// thing three times and take three times the room.
|
|
6843
|
+
function msaLogoShown() {
|
|
6844
|
+
return _state.showMsaLogo === true && msaShown();
|
|
6845
|
+
}
|
|
6846
|
+
|
|
6847
|
+
// What the strip under the alignment needs. The conservation form is the
|
|
6848
|
+
// 56 px this was a constant for; the logo form is taller and drops the
|
|
6849
|
+
// consensus row it subsumes.
|
|
6850
|
+
function msaBottomReserve() {
|
|
6851
|
+
return MSA_CONS_TOP_GAP
|
|
6852
|
+
+ (msaLogoShown() ? MSA_LOGO_CAP_H + MSA_LOGO_H : MSA_CONS_BAR_H + MSA_CONS_ROW_H)
|
|
6853
|
+
+ MSA_RULER_H;
|
|
6854
|
+
}
|
|
6855
|
+
|
|
6856
|
+
// Logo letters are drawn at a fixed size and then scaled to their cell and
|
|
6857
|
+
// to their share of the stack, so what is needed is how much ink this face
|
|
6858
|
+
// actually puts on the page for THIS letter -- measured, not assumed: a
|
|
6859
|
+
// 0.6 advance and a 0.72 cap height are true of some monospace faces and
|
|
6860
|
+
// not others, and a letter scaled by the wrong constant overflows its
|
|
6861
|
+
// column.
|
|
6862
|
+
//
|
|
6863
|
+
// Per letter, because a logo's letters must each fill their slice exactly:
|
|
6864
|
+
// measured off one glyph, Q and G hung their descenders through the
|
|
6865
|
+
// baseline and across the ruler, and every letter without a descender sat
|
|
6866
|
+
// a little short. What is scaled to the slice is the glyph's INK BOX
|
|
6867
|
+
// (ascent + descent), and the baseline is then placed so that box lands
|
|
6868
|
+
// inside the slice.
|
|
6869
|
+
let _logoGlyphs = {};
|
|
6870
|
+
|
|
6871
|
+
function logoGlyphMetrics(ch) {
|
|
6872
|
+
if (_logoGlyphs[ch]) {
|
|
6873
|
+
return _logoGlyphs[ch];
|
|
6874
|
+
}
|
|
6875
|
+
if (!_legendMeasureCtx) {
|
|
6876
|
+
_legendMeasureCtx = document.createElement('canvas').getContext('2d');
|
|
6877
|
+
}
|
|
6878
|
+
_legendMeasureCtx.font = MSA_LOGO_FONT_PX + 'px monospace';
|
|
6879
|
+
let m = _legendMeasureCtx.measureText(ch);
|
|
6880
|
+
let ascent = m.actualBoundingBoxAscent;
|
|
6881
|
+
let descent = m.actualBoundingBoxDescent;
|
|
6882
|
+
if (!(ascent > 0)) {
|
|
6883
|
+
ascent = MSA_LOGO_FONT_PX * 0.72; // a face that will not say
|
|
6884
|
+
descent = 0;
|
|
6885
|
+
}
|
|
6886
|
+
if (!(descent > 0)) {
|
|
6887
|
+
descent = 0; // no descender, or none reported
|
|
6888
|
+
}
|
|
6889
|
+
_logoGlyphs[ch] = {
|
|
6890
|
+
width: m.width || (MSA_LOGO_FONT_PX * 0.6),
|
|
6891
|
+
ascent: ascent,
|
|
6892
|
+
descent: descent
|
|
6893
|
+
};
|
|
6894
|
+
return _logoGlyphs[ch];
|
|
6895
|
+
}
|
|
6896
|
+
|
|
6827
6897
|
function msaRowSeq(d) {
|
|
6828
6898
|
if (d.sequences && d.sequences.length > 0) {
|
|
6829
6899
|
let s = d.sequences[0];
|
|
@@ -7014,12 +7084,15 @@ function (root, d3, forester, phyloXml) {
|
|
|
7014
7084
|
.attr('stroke', ink).attr('stroke-opacity', 0.7).attr('stroke-width', 1);
|
|
7015
7085
|
}
|
|
7016
7086
|
|
|
7017
|
-
//
|
|
7018
|
-
//
|
|
7019
|
-
//
|
|
7020
|
-
|
|
7087
|
+
// Scored over the visible WINDOW only -- the whole alignment would
|
|
7088
|
+
// cost rows x total per redraw; indices below are window-relative.
|
|
7089
|
+
// The rows are the tips ON SCREEN: enter a clade and the summary is
|
|
7090
|
+
// that clade's, which is the whole point of putting it next to a tree.
|
|
7091
|
+
let windowRows = tips.map(function (t) {
|
|
7021
7092
|
return msaRowSeq(t).slice(offset, offset + visible);
|
|
7022
|
-
})
|
|
7093
|
+
});
|
|
7094
|
+
let logo = msaLogoShown() ? forester.msaLogo(windowRows, visible, isNuc) : null;
|
|
7095
|
+
let cons = logo ? null : forester.msaConservation(windowRows, visible, 'identity', isNuc);
|
|
7023
7096
|
// A faint dashed guide from each tip -- from the end of its label, or
|
|
7024
7097
|
// from the node itself when the label is hidden -- across to its row
|
|
7025
7098
|
// in the track, so a row can be read back to its sequence without
|
|
@@ -7055,34 +7128,91 @@ function (root, d3, forester, phyloXml) {
|
|
|
7055
7128
|
}
|
|
7056
7129
|
|
|
7057
7130
|
// ---- conservation, consensus, ruler: a FLOATING strip ----
|
|
7058
|
-
let
|
|
7131
|
+
let stripH = msaBottomReserve();
|
|
7132
|
+
let strip = floatStripGroup('aptx-msa-strip', _clusterH, stripH);
|
|
7059
7133
|
strip.append('rect').attr('x', Math.round(originX) - 2).attr('y', _clusterH)
|
|
7060
|
-
.attr('width', trackW + 4).attr('height',
|
|
7134
|
+
.attr('width', trackW + 4).attr('height', stripH)
|
|
7061
7135
|
.attr('fill', _state.backgroundColorDefault);
|
|
7062
7136
|
let consTop = _clusterH + MSA_CONS_TOP_GAP;
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
if (score <= 0) {
|
|
7069
|
-
continue;
|
|
7137
|
+
// A caption on the panel's own background, so it stays readable over
|
|
7138
|
+
// whatever the band happens to draw underneath it.
|
|
7139
|
+
let caption = function (txt, y) {
|
|
7140
|
+
if (trackW <= 170) {
|
|
7141
|
+
return;
|
|
7070
7142
|
}
|
|
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)
|
|
7143
|
+
let font = '8px ' + FONT_DEFAULTS;
|
|
7144
|
+
let w = legendTextWidth(txt, font) + 6;
|
|
7145
|
+
let xe = Math.round(originX) + trackW - 3;
|
|
7146
|
+
strip.append('rect').attr('x', xe - w).attr('y', y - 8)
|
|
7147
|
+
.attr('width', w).attr('height', 11)
|
|
7148
|
+
.attr('fill', _state.backgroundColorDefault).attr('fill-opacity', 0.85);
|
|
7149
|
+
strip.append('text').attr('x', xe - 3).attr('y', y)
|
|
7080
7150
|
.attr('text-anchor', 'end')
|
|
7081
7151
|
.style('font-size', '8px').style('fill', ink).style('fill-opacity', 0.9)
|
|
7082
|
-
.text(
|
|
7083
|
-
}
|
|
7084
|
-
|
|
7085
|
-
|
|
7152
|
+
.text(txt);
|
|
7153
|
+
};
|
|
7154
|
+
if (logo) {
|
|
7155
|
+
// Each column a stack of letters: total height the column's
|
|
7156
|
+
// information content in bits, each letter's share its frequency.
|
|
7157
|
+
// Least frequent at the BOTTOM, so the eye reads the consensus
|
|
7158
|
+
// off the top of the stack.
|
|
7159
|
+
//
|
|
7160
|
+
// The caption gets a line of its own above the stacks (hence the
|
|
7161
|
+
// CAP_H here). Laid over them it washed out the very letters it
|
|
7162
|
+
// was labelling, which is a poor trade for eleven pixels.
|
|
7163
|
+
let floor = consTop + MSA_LOGO_CAP_H + MSA_LOGO_H;
|
|
7164
|
+
strip.append('line')
|
|
7165
|
+
.attr('x1', Math.round(originX)).attr('x2', Math.round(originX) + trackW)
|
|
7166
|
+
.attr('y1', floor).attr('y2', floor)
|
|
7167
|
+
.attr('stroke', ink).attr('stroke-opacity', 0.5).attr('stroke-width', 1);
|
|
7168
|
+
for (let i = 0; i < visible; ++i) {
|
|
7169
|
+
let col = logo.columns[i];
|
|
7170
|
+
if (!col || !(col.height > 0)) {
|
|
7171
|
+
continue;
|
|
7172
|
+
}
|
|
7173
|
+
let x0 = Math.round(originX + (i * cw));
|
|
7174
|
+
let x1 = Math.round(originX + ((i + 1) * cw));
|
|
7175
|
+
let y = floor;
|
|
7176
|
+
for (let k = col.letters.length - 1; k >= 0; --k) {
|
|
7177
|
+
let letter = col.letters[k];
|
|
7178
|
+
let hpx = (letter.h / logo.maxBits) * MSA_LOGO_H;
|
|
7179
|
+
if (hpx < 0.5) {
|
|
7180
|
+
continue; // under half a pixel of ink: nothing to draw
|
|
7181
|
+
}
|
|
7182
|
+
let glyph = logoGlyphMetrics(letter.ch);
|
|
7183
|
+
let sy = hpx / (glyph.ascent + glyph.descent);
|
|
7184
|
+
let rgb = forester.msaResidueRgb(letter.ch, isNuc);
|
|
7185
|
+
strip.append('text')
|
|
7186
|
+
.attr('transform', 'translate(' + (x0 + ((x1 - x0) / 2)) + ','
|
|
7187
|
+
+ (y - (glyph.descent * sy)) + ')'
|
|
7188
|
+
+ ' scale(' + (Math.max(1, x1 - x0) / glyph.width) + ',' + sy + ')')
|
|
7189
|
+
.attr('text-anchor', 'middle')
|
|
7190
|
+
.style('font-family', 'monospace')
|
|
7191
|
+
.style('font-size', MSA_LOGO_FONT_PX + 'px')
|
|
7192
|
+
.style('fill', rgb === null ? ink : 'rgb(' + rgb.join(',') + ')')
|
|
7193
|
+
.text(letter.ch);
|
|
7194
|
+
y -= hpx;
|
|
7195
|
+
}
|
|
7196
|
+
}
|
|
7197
|
+
caption('Sequence logo \u2014 0 to ' + logo.maxBits.toFixed(1)
|
|
7198
|
+
+ ' bits (n = ' + n + ')', consTop + 8);
|
|
7199
|
+
} else {
|
|
7200
|
+
strip.append('rect').attr('x', Math.round(originX)).attr('y', consTop)
|
|
7201
|
+
.attr('width', trackW).attr('height', MSA_CONS_BAR_H)
|
|
7202
|
+
.attr('fill', ink).attr('fill-opacity', 0.08);
|
|
7203
|
+
for (let i = 0; i < visible; ++i) {
|
|
7204
|
+
let score = cons.scores[i] || 0;
|
|
7205
|
+
if (score <= 0) {
|
|
7206
|
+
continue;
|
|
7207
|
+
}
|
|
7208
|
+
let bh = Math.max(1, Math.round(score * MSA_CONS_BAR_H));
|
|
7209
|
+
let x0 = Math.round(originX + (i * cw));
|
|
7210
|
+
let x1 = Math.round(originX + ((i + 1) * cw));
|
|
7211
|
+
strip.append('rect').attr('x', x0).attr('y', (consTop + MSA_CONS_BAR_H) - bh)
|
|
7212
|
+
.attr('width', Math.max(1, x1 - x0)).attr('height', bh)
|
|
7213
|
+
.attr('fill', ink).attr('fill-opacity', 0.7);
|
|
7214
|
+
}
|
|
7215
|
+
caption('Consensus identity (n = ' + n + ')', consTop + 9);
|
|
7086
7216
|
for (let i = 0; i < visible; ++i) {
|
|
7087
7217
|
let cc = cons.consensus[i];
|
|
7088
7218
|
if (!cc) {
|
|
@@ -7099,7 +7229,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
7099
7229
|
|
|
7100
7230
|
// the 1-based column ruler: absolute column numbers at nice steps,
|
|
7101
7231
|
// and always the first and last column when their edge is in view
|
|
7102
|
-
let rulerY = consTop +
|
|
7232
|
+
let rulerY = consTop + (logo ? MSA_LOGO_CAP_H + MSA_LOGO_H : MSA_CONS_BAR_H + MSA_CONS_ROW_H);
|
|
7103
7233
|
strip.append('line').attr('x1', Math.round(originX)).attr('x2', Math.round(originX) + trackW)
|
|
7104
7234
|
.attr('y1', rulerY).attr('y2', rulerY)
|
|
7105
7235
|
.attr('stroke', ink).attr('stroke-opacity', 0.8).attr('stroke-width', 1);
|
|
@@ -7357,6 +7487,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
7357
7487
|
scheduleUpdate(null, 0);
|
|
7358
7488
|
}
|
|
7359
7489
|
|
|
7490
|
+
function msaLogoCbClicked() {
|
|
7491
|
+
_state.showMsaLogo = getCheckboxValue(MSA_LOGO_CB);
|
|
7492
|
+
scheduleUpdate(null, 0);
|
|
7493
|
+
}
|
|
7494
|
+
|
|
7360
7495
|
// ===================== Heat map =====================
|
|
7361
7496
|
// The desktop's MATRIX annotation columns: one cell per (tip, numeric
|
|
7362
7497
|
// per-tip property) to the right of the labels, every column painted on
|
|
@@ -8930,6 +9065,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
8930
9065
|
}
|
|
8931
9066
|
if (_basicTreeProperties.alignedMolSeqs === true && _basicTreeProperties.maxMolSeqLength > 0) {
|
|
8932
9067
|
s.msa = _state.showMsa === true;
|
|
9068
|
+
if (_state.showMsaLogo === true) {
|
|
9069
|
+
s.msaLogo = true;
|
|
9070
|
+
}
|
|
8933
9071
|
}
|
|
8934
9072
|
if (heatmapAvailable()) {
|
|
8935
9073
|
s.heatmap = _state.showHeatmap === true;
|
|
@@ -9058,6 +9196,9 @@ function (root, d3, forester, phyloXml) {
|
|
|
9058
9196
|
if (typeof s.msa === 'boolean') {
|
|
9059
9197
|
_state.showMsa = s.msa;
|
|
9060
9198
|
}
|
|
9199
|
+
if (typeof s.msaLogo === 'boolean') {
|
|
9200
|
+
_state.showMsaLogo = s.msaLogo;
|
|
9201
|
+
}
|
|
9061
9202
|
if (typeof s.heatmap === 'boolean') {
|
|
9062
9203
|
_state.showHeatmap = s.heatmap;
|
|
9063
9204
|
}
|
|
@@ -9168,6 +9309,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
9168
9309
|
}
|
|
9169
9310
|
});
|
|
9170
9311
|
setCheckboxValue(MSA_CB, _state.showMsa === true);
|
|
9312
|
+
setCheckboxValue(MSA_LOGO_CB, _state.showMsaLogo === true);
|
|
9171
9313
|
setCheckboxValue(HEATMAP_CB, _state.showHeatmap === true);
|
|
9172
9314
|
syncHeatmapControls();
|
|
9173
9315
|
setCheckboxValue(TIME_AXIS_CB, _state.showTimeAxis === true);
|
|
@@ -9241,10 +9383,20 @@ function (root, d3, forester, phyloXml) {
|
|
|
9241
9383
|
// Lists are comma-joined, booleans 1 / 0, the two searches flattened
|
|
9242
9384
|
// to a / af / am / a2 and b / bf / bm / b2. Made for a URL hash, where
|
|
9243
9385
|
// none of these characters needs escaping.
|
|
9244
|
-
|
|
9386
|
+
// Every key getViewState writes has to be here, or a view survives
|
|
9387
|
+
// getViewState/applyViewState (what an embedder uses) and is silently
|
|
9388
|
+
// dropped from the URL hash (what "Copy link to this view" produces).
|
|
9389
|
+
// The heat map's four keys and the alignment logo went in exactly that
|
|
9390
|
+
// way, while the README promised a link reproduced the figure.
|
|
9391
|
+
const VIEW_TEXT_KEYS = ['layout', 'display', 'order', 'root', 'colorBy', 'shapeBy', 'domainLabels', 'combine',
|
|
9392
|
+
'heatmapOrder'];
|
|
9245
9393
|
const VIEW_INT_KEYS = ['tree', 'subtree', 'rotation', 'domainEvalue'];
|
|
9246
9394
|
const VIEW_NUMBER_KEYS = ['font', 'node', 'branch'];
|
|
9247
|
-
const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', '
|
|
9395
|
+
const VIEW_BOOL_KEYS = ['horizontalLabels', 'msa', 'msaLogo', 'heatmap', 'domains', 'domainGlow',
|
|
9396
|
+
'timeAxis', 'timeGrid', 'matchCase', 'inverse'];
|
|
9397
|
+
// Lists of strings, comma-joined. A property ref carries ':' but never a
|
|
9398
|
+
// comma, and encodeViewValue leaves both readable in a hash.
|
|
9399
|
+
const VIEW_LIST_KEYS = ['heatmapManual'];
|
|
9248
9400
|
const VIEW_SEARCH_KEYS = [['searchA', 'a'], ['searchB', 'b']];
|
|
9249
9401
|
|
|
9250
9402
|
function encodeViewValue(v) {
|
|
@@ -9278,6 +9430,12 @@ function (root, d3, forester, phyloXml) {
|
|
|
9278
9430
|
if (Array.isArray(state.collapsed) && state.collapsed.length > 0) {
|
|
9279
9431
|
put('collapsed', state.collapsed.join(','));
|
|
9280
9432
|
}
|
|
9433
|
+
VIEW_LIST_KEYS.forEach(function (k) {
|
|
9434
|
+
let v = state[k];
|
|
9435
|
+
if (Array.isArray(v) && v.length > 0) {
|
|
9436
|
+
parts.push(k + '=' + v.map(encodeViewValue).join(','));
|
|
9437
|
+
}
|
|
9438
|
+
});
|
|
9281
9439
|
VIEW_BOOL_KEYS.forEach(function (k) {
|
|
9282
9440
|
if (typeof state[k] === 'boolean') {
|
|
9283
9441
|
put(k, state[k] ? 1 : 0);
|
|
@@ -9344,6 +9502,16 @@ function (root, d3, forester, phyloXml) {
|
|
|
9344
9502
|
return parseInt(x, 10);
|
|
9345
9503
|
});
|
|
9346
9504
|
}
|
|
9505
|
+
VIEW_LIST_KEYS.forEach(function (k) {
|
|
9506
|
+
if (raw[k] !== undefined) {
|
|
9507
|
+
let list = raw[k].split(',').filter(function (x) {
|
|
9508
|
+
return x.length > 0;
|
|
9509
|
+
});
|
|
9510
|
+
if (list.length > 0) {
|
|
9511
|
+
state[k] = list;
|
|
9512
|
+
}
|
|
9513
|
+
}
|
|
9514
|
+
});
|
|
9347
9515
|
VIEW_SEARCH_KEYS.forEach(function (sk) {
|
|
9348
9516
|
let p = sk[1];
|
|
9349
9517
|
if (raw[p]) {
|
|
@@ -10324,7 +10492,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
10324
10492
|
// own bottom rows (conservation, consensus, ruler) must end above it
|
|
10325
10493
|
// or the bar covers them -- the old bare slider did exactly that.
|
|
10326
10494
|
// The scale bar sits in the same band, under the tree's last row.
|
|
10327
|
-
return Math.max(msaShown() ?
|
|
10495
|
+
return Math.max(msaShown() ? msaBottomReserve() + MSA_NAV_RESERVE : 0, timeAxisBottomReserve(),
|
|
10328
10496
|
heatmapBottomReserve(),
|
|
10329
10497
|
scaleBarShown() && !radialDisplay() ? SCALE_BAR_RESERVE : 0);
|
|
10330
10498
|
}
|
|
@@ -11793,6 +11961,11 @@ function (root, d3, forester, phyloXml) {
|
|
|
11793
11961
|
if (msaCb) {
|
|
11794
11962
|
msaCb.disabled = radialDisplay();
|
|
11795
11963
|
}
|
|
11964
|
+
let msaLogoCb = byId(MSA_LOGO_CB);
|
|
11965
|
+
if (msaLogoCb) {
|
|
11966
|
+
// nothing to summarize while the alignment itself is not drawn
|
|
11967
|
+
msaLogoCb.disabled = radialDisplay() || _state.showMsa !== true;
|
|
11968
|
+
}
|
|
11796
11969
|
let heatCb = byId(HEATMAP_CB);
|
|
11797
11970
|
if (heatCb) {
|
|
11798
11971
|
// circular draws the columns as rings; only unrooted cannot
|
|
@@ -14093,6 +14266,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14093
14266
|
|
|
14094
14267
|
on(DYNAHIDE_CB, 'click', dynaHideCbClicked);
|
|
14095
14268
|
on(MSA_CB, 'click', msaCbClicked);
|
|
14269
|
+
on(MSA_LOGO_CB, 'click', msaLogoCbClicked);
|
|
14096
14270
|
on(HEATMAP_CB, 'click', heatmapCbClicked);
|
|
14097
14271
|
on(HEATMAP_ORDER_SELECT, 'change', heatmapOrderChanged);
|
|
14098
14272
|
on(HEATMAP_REORDER_BUTTON, 'click', showHeatmapReorderDialog);
|
|
@@ -14637,6 +14811,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14637
14811
|
// labels -- they live here
|
|
14638
14812
|
if (_basicTreeProperties.alignedMolSeqs && _basicTreeProperties.maxMolSeqLength > 0) {
|
|
14639
14813
|
opts.push(makeCheckboxItem('Alignment', MSA_CB, 'to show/hide the sequence alignment beside the tree (rectangular layout only)'));
|
|
14814
|
+
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
14815
|
}
|
|
14641
14816
|
if (_basicTreeProperties.domainArchitectures) {
|
|
14642
14817
|
opts.push(makeCheckboxItem('Domain Architectures', DOMAINS_CB, 'to show/hide the protein domain architectures beside the tips', true));
|
|
@@ -14971,6 +15146,7 @@ function (root, d3, forester, phyloXml) {
|
|
|
14971
15146
|
setCheckboxValue(VIS_CB, _state.showVisualizations);
|
|
14972
15147
|
setCheckboxValue(DYNAHIDE_CB, _state.dynahide);
|
|
14973
15148
|
setCheckboxValue(MSA_CB, _state.showMsa);
|
|
15149
|
+
setCheckboxValue(MSA_LOGO_CB, _state.showMsaLogo === true);
|
|
14974
15150
|
setCheckboxValue(HEATMAP_CB, _state.showHeatmap);
|
|
14975
15151
|
syncDomainControls();
|
|
14976
15152
|
syncHeatmapControls();
|
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.12.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -8440,6 +8440,70 @@
|
|
|
8440
8440
|
return {scores: scores, consensus: consensus};
|
|
8441
8441
|
};
|
|
8442
8442
|
|
|
8443
|
+
// Per-column sequence logo over the given rows -- the stack of letters
|
|
8444
|
+
// whose total height is the column's information content in BITS and whose
|
|
8445
|
+
// shares are the residue frequencies (Schneider & Stephens 1990), which is
|
|
8446
|
+
// the display the MEME Suite and WebLogo draw.
|
|
8447
|
+
//
|
|
8448
|
+
// No small-sample correction. Schneider's e_n = (K-1)/(2 ln2 n) is meant
|
|
8449
|
+
// for a motif sampled from many sequences; here the rows are the tips
|
|
8450
|
+
// currently on screen, so n is routinely 3 or 4 -- and for protein at n=3
|
|
8451
|
+
// the correction (4.57 bits) EXCEEDS the maximum (4.32), so a perfectly
|
|
8452
|
+
// conserved column of a small clade would draw nothing at all, exactly
|
|
8453
|
+
// when the reader has asked about that clade. The caller names n instead.
|
|
8454
|
+
//
|
|
8455
|
+
// Gaps are not a letter: frequencies are taken over the non-gap residues,
|
|
8456
|
+
// and the stack is then scaled by the column's occupancy, so a column held
|
|
8457
|
+
// up by two residues out of fifty is short rather than perfectly conserved.
|
|
8458
|
+
// `bits` and `occupancy` are returned separately so a caller can say which
|
|
8459
|
+
// of the two made a column short.
|
|
8460
|
+
//
|
|
8461
|
+
// Letters come back most frequent FIRST, ties broken alphabetically, so a
|
|
8462
|
+
// figure is reproducible rather than at the mercy of key order.
|
|
8463
|
+
forester.msaLogo = function (rows, length, nucleotide) {
|
|
8464
|
+
let maxBits = Math.log(nucleotide ? 4 : 20) / Math.LN2;
|
|
8465
|
+
if (!(length >= 0) || !isFinite(length)) {
|
|
8466
|
+
return {columns: [], maxBits: maxBits};
|
|
8467
|
+
}
|
|
8468
|
+
length = Math.floor(length);
|
|
8469
|
+
let n = rows.length;
|
|
8470
|
+
let columns = new Array(length);
|
|
8471
|
+
for (let c = 0; c < length; ++c) {
|
|
8472
|
+
let counts = {};
|
|
8473
|
+
let nonGap = 0;
|
|
8474
|
+
for (let r = 0; r < n; ++r) {
|
|
8475
|
+
let row = rows[r];
|
|
8476
|
+
let ch = (row && c < row.length) ? row.charAt(c) : '-';
|
|
8477
|
+
if (forester.isMsaGap(ch)) {
|
|
8478
|
+
continue;
|
|
8479
|
+
}
|
|
8480
|
+
ch = ch.toUpperCase();
|
|
8481
|
+
++nonGap;
|
|
8482
|
+
counts[ch] = (counts[ch] || 0) + 1;
|
|
8483
|
+
}
|
|
8484
|
+
if (n < 1 || nonGap < 1) {
|
|
8485
|
+
columns[c] = {bits: 0, occupancy: 0, height: 0, letters: []};
|
|
8486
|
+
continue;
|
|
8487
|
+
}
|
|
8488
|
+
let H = 0;
|
|
8489
|
+
Object.keys(counts).forEach(function (ch) {
|
|
8490
|
+
let p = counts[ch] / nonGap;
|
|
8491
|
+
H -= p * (Math.log(p) / Math.LN2);
|
|
8492
|
+
});
|
|
8493
|
+
let bits = Math.max(0, maxBits - H);
|
|
8494
|
+
let occupancy = nonGap / n;
|
|
8495
|
+
let height = bits * occupancy;
|
|
8496
|
+
let letters = Object.keys(counts).sort().sort(function (a, b) {
|
|
8497
|
+
return counts[b] - counts[a]; // count first, alphabetical within a tie
|
|
8498
|
+
}).map(function (ch) {
|
|
8499
|
+
let p = counts[ch] / nonGap;
|
|
8500
|
+
return {ch: ch, p: p, h: p * height};
|
|
8501
|
+
});
|
|
8502
|
+
columns[c] = {bits: bits, occupancy: occupancy, height: height, letters: letters};
|
|
8503
|
+
}
|
|
8504
|
+
return {columns: columns, maxBits: maxBits};
|
|
8505
|
+
};
|
|
8506
|
+
|
|
8443
8507
|
// The hover readout's description of one residue: full name, class (amino
|
|
8444
8508
|
// acids), Kyte-Doolittle hydropathy. Returns null for a gap.
|
|
8445
8509
|
forester.msaResidueInfo = function (ch, nucleotide) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archaeopteryx",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0",
|
|
4
4
|
"description": "Archaeopteryx.js is a software tool for the visualization and analysis of highly annotated phylogenetic trees.",
|
|
5
5
|
"main": "archaeopteryx.js",
|
|
6
6
|
"types": "archaeopteryx.d.ts",
|