Spaces:
Running
Add the sparse-feature window browser
Browse filesTwenty-one precomputed 8,191 bp windows across four annotated genomes, read out
base by base from the report's sparse autoencoder. Every base takes the colour of
the feature that fires hardest on it; the annotation rows are a separate register
and never an input to a number on the page.
Each of the eight windows where a feature marks a splice site the annotation does
not carry states a usage verdict from registers independent of that annotation:
AtRTD3's long-read transcripts, the 2026 TAIR12 reannotation, and PastDB's usage
measurements over 202 RNA-seq samples. Used, untestable and untested are three
different claims and the page keeps them apart. chr1:21,735,155, which no
transcript model carries, is an alternative 3' splice site PastDB measures at up
to 7.5%; chr5:5,427,495 sits in a gene with a median expression of 0.00 cRPKM and
cannot be tested either way.
Interaction help and the lane-selection method sit behind the (?) beside the lane
toggle. Each window's case — kind, headline, reason and usage verdict — is one
island inside the figure.
- README.md +25 -0
- assets/sae-browser.js +1959 -0
- assets/sae-sources.css +182 -0
- assets/sae-tables.js +211 -0
- assets/sae.css +745 -0
- causal-variants.html +1 -1
- corpus.html +1 -1
- data/sae/AT1G65170.json +0 -0
- data/sae/B00001890.json +0 -0
- data/sae/B00002447.json +0 -0
- data/sae/B00003566.json +0 -0
- data/sae/B00004408.json +0 -0
- data/sae/B00005282.json +0 -0
- data/sae/B00005450.json +0 -0
- data/sae/B00006020.json +0 -0
- data/sae/B00011941.json +0 -0
- data/sae/B00012077.json +0 -0
- data/sae/B00013816.json +0 -0
- data/sae/B00014101.json +0 -0
- data/sae/B00014342.json +0 -0
- data/sae/B00019012.json +0 -0
- data/sae/B00020610.json +0 -0
- data/sae/B00020628.json +0 -0
- data/sae/B00021698.json +0 -0
- data/sae/B00022102.json +0 -0
- data/sae/B00026784.json +0 -0
- data/sae/B00027277.json +0 -0
- data/sae/B00030192.json +0 -0
- data/sae/annotation-changes.json +1 -0
- data/sae/index.json +0 -0
- factory.html +1 -1
- index.html +1 -1
- leaderboard.html +1 -1
- sae-features.html +204 -0
- use-case.html +1 -1
|
@@ -65,6 +65,18 @@ Static landing page for the Botanic1 models (Living Models), linking to the mode
|
|
| 65 |
grid on mobile; `assets/use-case.css` follows the site's light and dark palettes.
|
| 66 |
- `corpus.html`: the 320-species pre-training corpus as a treemap, with the genome
|
| 67 |
length weighting and deduplication passages.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
## Identity
|
| 70 |
|
|
@@ -100,6 +112,19 @@ refer to the full locus, including when the view is zoomed. The file also contai
|
|
| 100 |
annotation URL, study reference, and source scoring-file SHA-256. Gene annotations
|
| 101 |
are displayed for biological context and are not inputs to the model score.
|
| 102 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
Figure PNGs under `assets/figures/` are the report's committed renders.
|
| 104 |
`data_pipeline.svg` is a `pdftocairo -svg` conversion of the report's data-pipeline PDF.
|
| 105 |
`agentic_orchestration.svg` retains Figure S1's layout, nodes, queues, connections and
|
|
|
|
| 65 |
grid on mobile; `assets/use-case.css` follows the site's light and dark palettes.
|
| 66 |
- `corpus.html`: the 320-species pre-training corpus as a treemap, with the genome
|
| 67 |
length weighting and deduplication passages.
|
| 68 |
+
- `sae-features.html`: the sparse-feature window browser. Twenty 8,191 bp windows,
|
| 69 |
+
five in each of the four genomes whose annotations the features were validated
|
| 70 |
+
against (*A. thaliana*, *O. sativa*, *Z. mays*, *S. lycopersicum*) and one for each
|
| 71 |
+
of the five positions the sampler anchored on, plus the *AT1G65170* locus the
|
| 72 |
+
report's annotation-correction result is drawn from. Every base carries the colour
|
| 73 |
+
of the feature that fires hardest on it, over the shown lanes or over the whole
|
| 74 |
+
8,192-feature dictionary. `assets/sae-browser.js` draws the tracks and handles
|
| 75 |
+
hover, pin, drag-pan, zoom and keyboard stepping; `assets/sae-tables.js` renders
|
| 76 |
+
the named-feature table, the reproduction check and the annotation sources;
|
| 77 |
+
`assets/sae.css` carries the ten-hue feature palette and the paler annotation
|
| 78 |
+
register. Data is `data/sae/index.json` plus one file per window, fetched on
|
| 79 |
+
demand.
|
| 80 |
|
| 81 |
## Identity
|
| 82 |
|
|
|
|
| 112 |
annotation URL, study reference, and source scoring-file SHA-256. Gene annotations
|
| 113 |
are displayed for biological context and are not inputs to the model score.
|
| 114 |
|
| 115 |
+
`data/sae/` holds the precomputed feature read-out. Activations come from the sparse
|
| 116 |
+
autoencoder of the report's interpretability section (`full_650m_L035_x8_k64` —
|
| 117 |
+
Botanic1-M, layer 35, x8 expansion, k = 64) applied at the model's native 8,191 bp
|
| 118 |
+
context, which is the context the report's published activations were produced at:
|
| 119 |
+
re-encoding the *AT1G65170* window reproduces the figure's track at Pearson r
|
| 120 |
+
0.999994, giving 14.80 against a published 14.79 at the donor TAIR12 moved to, and
|
| 121 |
+
0.00 at the donor Araport11 annotates. Each feature's non-zero runs are stored as
|
| 122 |
+
255 levels of that feature's own peak in that window; the peak itself is exact and
|
| 123 |
+
zero stays exactly zero. Per-base annotations and gene extents come from the
|
| 124 |
+
interpretability corpus's own label tracks; each window records the annotation
|
| 125 |
+
release and file digest it was built from, and `index.json` carries the annotation
|
| 126 |
+
sources for every species.
|
| 127 |
+
|
| 128 |
Figure PNGs under `assets/figures/` are the report's committed renders.
|
| 129 |
`data_pipeline.svg` is a `pdftocairo -svg` conversion of the report's data-pipeline PDF.
|
| 130 |
`agentic_orchestration.svg` retains Figure S1's layout, nodes, queues, connections and
|
|
@@ -0,0 +1,1959 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Sparse-feature window browser.
|
| 2 |
+
|
| 3 |
+
Activations are stride-1 measurements read out of one SAE over one window.
|
| 4 |
+
Three rules shape the drawing:
|
| 5 |
+
- zero means the feature did not fire, so an unfired base is left blank and
|
| 6 |
+
the lane is labelled silent rather than given a short bar;
|
| 7 |
+
- several bases under one pixel column collapse to their MAXIMUM, never to a
|
| 8 |
+
mean, and never by interpolating between measured bases;
|
| 9 |
+
- annotations live inside a recessed panel, in pale washes and mid-tone
|
| 10 |
+
ticks, so nothing in the annotation register can be read as a measurement.
|
| 11 |
+
|
| 12 |
+
The dashed anchor outline marks where the window sampler looked. It is an
|
| 13 |
+
outline, never a fill, and it is labelled as its own thing so it cannot pass
|
| 14 |
+
for an annotated site. Every colour is a CSS custom property; the only value
|
| 15 |
+
computed in JS is the ink of a base letter, which is chosen from the luminance
|
| 16 |
+
of the cell it sits on. */
|
| 17 |
+
(async function () {
|
| 18 |
+
const host = document.getElementById('feat-plot');
|
| 19 |
+
if (!host) return;
|
| 20 |
+
const readout = document.getElementById('feat-readout');
|
| 21 |
+
const region = document.getElementById('feat-region');
|
| 22 |
+
const picker = document.getElementById('win-picker');
|
| 23 |
+
const list = document.getElementById('feat-list');
|
| 24 |
+
const title = document.getElementById('feat-title');
|
| 25 |
+
const sub = document.getElementById('feat-sub');
|
| 26 |
+
const caseEl = document.getElementById('feat-case');
|
| 27 |
+
const modeBtns = Array.from(document.querySelectorAll('#feat-mode button'));
|
| 28 |
+
const tools = ['feat-in', 'feat-out', 'feat-full'].map(id => document.getElementById(id));
|
| 29 |
+
tools.forEach(b => { if (b) b.disabled = true; });
|
| 30 |
+
wireHelp(); /* explains the browser, so it works before a window is chosen */
|
| 31 |
+
|
| 32 |
+
/* Window files carry free-form fields whose shapes differ between kinds, so a
|
| 33 |
+
value is only treated as a list when it actually is one. */
|
| 34 |
+
const asList = v => Array.isArray(v) ? v.filter(Boolean) : [];
|
| 35 |
+
const esc = value => String(value ?? '').replace(/[&<>"']/g, ch =>
|
| 36 |
+
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
|
| 37 |
+
const fmt = n => Math.round(n).toLocaleString('en-US');
|
| 38 |
+
const num = (n, d = 2) => n.toFixed(d);
|
| 39 |
+
const path = (d, cls, extra = '') => `<path class="${cls}" d="${d}"${extra}/>`;
|
| 40 |
+
const text = (px, py, s, cls = '', anchor = 'start', extra = '') =>
|
| 41 |
+
`<text x="${(+px).toFixed(2)}" y="${(+py).toFixed(2)}" class="${cls}" text-anchor="${anchor}"${extra}>${esc(s)}</text>`;
|
| 42 |
+
const rect = (x, y, w, h, fill, cls = '', extra = '') =>
|
| 43 |
+
`<rect x="${x.toFixed(2)}" y="${(+y).toFixed(2)}" width="${Math.max(0.4, w).toFixed(2)}" height="${
|
| 44 |
+
(+h).toFixed(2)}"${fill ? ` fill="${fill}"` : ''}${cls ? ` class="${cls}"` : ''}${extra}/>`;
|
| 45 |
+
|
| 46 |
+
const PALETTE = 12; // one hue per lane, so no two lanes share a colour
|
| 47 |
+
const GLOBAL_KEYS = 10; // how many of the 8,192 the whole-dictionary strip names
|
| 48 |
+
const featColour = slot => slot < 0 ? 'var(--feat-none)' : `var(--feat-${slot % PALETTE + 1})`;
|
| 49 |
+
|
| 50 |
+
/* ---- vocabulary -------------------------------------------------------- */
|
| 51 |
+
const ANCHOR_ORDER = ['tss', 'tis', 'donor', 'acceptor', 'background'];
|
| 52 |
+
const ANCHOR_COL = {
|
| 53 |
+
tss: 'TSS', tis: 'Start codon', donor: 'Donor', acceptor: 'Acceptor', background: 'Background'
|
| 54 |
+
};
|
| 55 |
+
const ANCHOR_TITLE = {
|
| 56 |
+
tss: 'Transcription-start', tis: 'Translation-start', donor: 'Splice-donor',
|
| 57 |
+
acceptor: 'Splice-acceptor', background: 'Unanchored background'
|
| 58 |
+
};
|
| 59 |
+
const SITE_LABEL = { tss: 'TSS', tis: 'start codon', donor: 'donor', acceptor: 'acceptor', stop: 'stop', tts: 'TTS' };
|
| 60 |
+
const SITE_SHORT = { tss: 'TSS', tis: 'start', donor: 'donor', acceptor: 'accept', stop: 'stop', tts: 'TTS' };
|
| 61 |
+
const REGION_LABEL = {
|
| 62 |
+
intergenic: 'intergenic', cds: 'CDS', utr5: '5′ UTR', utr3: '3′ UTR',
|
| 63 |
+
intron: 'intron', ncrna: 'non-coding'
|
| 64 |
+
};
|
| 65 |
+
const KIND_GROUP = {
|
| 66 |
+
site: 'Point sites', range: 'Regions', motif: 'Sequence motifs', pattern: 'Repeat patterns'
|
| 67 |
+
};
|
| 68 |
+
const KIND_ORDER = ['site', 'range', 'motif', 'pattern'];
|
| 69 |
+
/* Why a window earns its place. `agreement` is the quiet case; the other three
|
| 70 |
+
are the interesting ones and share one accent, but they keep separate names
|
| 71 |
+
because an alternative isoform, an unannotated site and a moved annotation
|
| 72 |
+
are three different claims. An unrecognised kind is treated as flagged
|
| 73 |
+
rather than quietly filed under agreement. */
|
| 74 |
+
const SEL_KIND = {
|
| 75 |
+
agreement: { tag: 'Agreement', mark: 'agreement', flag: false },
|
| 76 |
+
alternative: { tag: 'Alternative isoform', mark: 'alternative site', flag: true },
|
| 77 |
+
unannotated: { tag: 'Unannotated site', mark: 'unannotated site', flag: true },
|
| 78 |
+
changed: { tag: 'Annotation moved', mark: 'annotation moved', flag: true }
|
| 79 |
+
};
|
| 80 |
+
const selKind = sel => SEL_KIND[sel && sel.kind]
|
| 81 |
+
|| { tag: String((sel && sel.kind) || 'selected'), mark: 'finding', flag: true };
|
| 82 |
+
|
| 83 |
+
/* A `selection` says what the annotation carries. A `usage_check` says what
|
| 84 |
+
independent registers found, which is a different question and is kept as a
|
| 85 |
+
separate verdict: a feature firing where nothing is annotated is not a
|
| 86 |
+
discovery until a register says the site is used, and `untested` is not a
|
| 87 |
+
weaker `used` — it is the absence of a check. An unrecognised verdict falls
|
| 88 |
+
through to `untested` so a new one can never read as confirmation. */
|
| 89 |
+
const USAGE_VERDICT = {
|
| 90 |
+
used: { tag: 'Used', gloss: 'an independent register records the site in use' },
|
| 91 |
+
untestable: { tag: 'Untestable', gloss: 'the registers cover the locus but no measurement is possible there' },
|
| 92 |
+
untested: { tag: 'Untested', gloss: 'no usage register was consulted on this assembly' }
|
| 93 |
+
};
|
| 94 |
+
const usageVerdict = u => USAGE_VERDICT[u && u.verdict] || USAGE_VERDICT.untested;
|
| 95 |
+
/* what each register row reported, in the register's own terms */
|
| 96 |
+
const USAGE_RESULT = {
|
| 97 |
+
used: 'records it', absent: 'does not carry it',
|
| 98 |
+
'no data': 'has no data', 'not consulted': 'not consulted'
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
/* Evidence is free-form and differs by kind, so only fields actually present
|
| 102 |
+
are read, and `score` is only given a name for `agreement`, where its scale
|
| 103 |
+
is documented. */
|
| 104 |
+
function selMetric(sel) {
|
| 105 |
+
const e = (sel && sel.evidence) || {};
|
| 106 |
+
const bits = [];
|
| 107 |
+
if (sel.kind === 'agreement') {
|
| 108 |
+
if (typeof sel.score === 'number') bits.push(`combined score ${sel.score.toFixed(3)}`);
|
| 109 |
+
return bits.join(' · ');
|
| 110 |
+
}
|
| 111 |
+
if (e.dinuc) bits.push(String(e.dinuc));
|
| 112 |
+
const moved = e.old_coord != null || e.new_coord != null;
|
| 113 |
+
if (typeof e.shift_bp === 'number') {
|
| 114 |
+
bits.push(moved ? `moved ${fmt(e.shift_bp)} bp between releases`
|
| 115 |
+
: `${fmt(e.shift_bp)} bp from the canonical site`);
|
| 116 |
+
}
|
| 117 |
+
if (typeof e.nearest_full_bp === 'number') {
|
| 118 |
+
bits.push(`nearest annotated site ${fmt(e.nearest_full_bp)} bp away`);
|
| 119 |
+
}
|
| 120 |
+
if (typeof e.exons_old === 'number' && e.exons_old === e.exons_new) {
|
| 121 |
+
bits.push(`exon count unchanged at ${fmt(e.exons_old)}`);
|
| 122 |
+
}
|
| 123 |
+
if (typeof e.n_cases_in_window === 'number' && e.n_cases_in_window > 1) {
|
| 124 |
+
bits.push(`${fmt(e.n_cases_in_window)} cases in this window`);
|
| 125 |
+
}
|
| 126 |
+
return bits.join(' · ');
|
| 127 |
+
}
|
| 128 |
+
/* the base pair a window was chosen for, and for a moved annotation the pair */
|
| 129 |
+
function findingMarks(sel) {
|
| 130 |
+
if (!sel || sel.position == null) return [];
|
| 131 |
+
const e = sel.evidence || {};
|
| 132 |
+
const marks = [{ base: sel.position, filled: true, tag: selKind(sel).mark }];
|
| 133 |
+
if (typeof e.window_index_new === 'number' && e.window_index_new !== sel.position) {
|
| 134 |
+
marks[0].tag = 'was';
|
| 135 |
+
marks.push({ base: e.window_index_new, filled: false, tag: 'now' });
|
| 136 |
+
}
|
| 137 |
+
return marks;
|
| 138 |
+
}
|
| 139 |
+
const MAX_LANES = 12;
|
| 140 |
+
const LIST_UNNAMED = 40;
|
| 141 |
+
|
| 142 |
+
/* ---- fonts, read out of the tokens so measuring matches the drawing ---- */
|
| 143 |
+
const cssVar = name => getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
| 144 |
+
let FONTS = {};
|
| 145 |
+
const gauge = document.createElement('canvas').getContext('2d');
|
| 146 |
+
function readFonts() {
|
| 147 |
+
const sans = cssVar('--sans') || 'sans-serif';
|
| 148 |
+
const mono = cssVar('--mono') || 'monospace';
|
| 149 |
+
FONTS = {
|
| 150 |
+
name: `600 11px ${sans}`,
|
| 151 |
+
nameId: `600 11px ${mono}`,
|
| 152 |
+
meta: `500 10px ${mono}`,
|
| 153 |
+
strand: `500 10px ${sans}`,
|
| 154 |
+
reg: `500 9px ${mono}`,
|
| 155 |
+
site: `9px ${sans}`,
|
| 156 |
+
band: `500 8.5px ${sans}`,
|
| 157 |
+
tick: `10px ${mono}`
|
| 158 |
+
};
|
| 159 |
+
}
|
| 160 |
+
const measure = (s, font) => { gauge.font = font; return gauge.measureText(String(s)).width; };
|
| 161 |
+
/* mono at 9px carries a 0.13em tracking in CSS, which measureText cannot see */
|
| 162 |
+
const measureTracked = (s, font, em) => measure(s, font) + String(s).length * em;
|
| 163 |
+
|
| 164 |
+
/* The lane colours are design tokens, so they are read back out of the
|
| 165 |
+
stylesheet once per draw; a base letter is then inked near-black or white by
|
| 166 |
+
the luminance of the cell it sits on. */
|
| 167 |
+
/* Relative luminance at which white and black ink have equal contrast:
|
| 168 |
+
(L + 0.05)^2 = 1.05 * 0.05. Above it, ink black; below it, ink white. The
|
| 169 |
+
base glyphs generalise the same test: each base has a light and a dark
|
| 170 |
+
variant of its own hue and takes whichever has more contrast on the cell. */
|
| 171 |
+
const INK_FLIP = 0.1791;
|
| 172 |
+
const BASE_KEYS = ['a', 'c', 'g', 't', 'n'];
|
| 173 |
+
const contrastOf = (a, b) => (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
|
| 174 |
+
const luminance = value => {
|
| 175 |
+
const m = String(value).trim().match(/^#?([0-9a-f]{6})$/i);
|
| 176 |
+
if (!m) return 0.5;
|
| 177 |
+
const n = parseInt(m[1], 16);
|
| 178 |
+
const ch = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map(v => {
|
| 179 |
+
const f = v / 255;
|
| 180 |
+
return f <= 0.03928 ? f / 12.92 : Math.pow((f + 0.055) / 1.055, 2.4);
|
| 181 |
+
});
|
| 182 |
+
return 0.2126 * ch[0] + 0.7152 * ch[1] + 0.0722 * ch[2];
|
| 183 |
+
};
|
| 184 |
+
let inks = new Map(); // slot -> plain contrast ink
|
| 185 |
+
let baseInk = new Map(); // `slot:base` -> that base's hue, in the readable variant
|
| 186 |
+
function readPalette() {
|
| 187 |
+
const cs = getComputedStyle(document.documentElement);
|
| 188 |
+
inks = new Map(); baseInk = new Map();
|
| 189 |
+
const cellLum = new Map();
|
| 190 |
+
for (let i = 1; i <= PALETTE; i++) cellLum.set(i - 1, luminance(cs.getPropertyValue(`--feat-${i}`)));
|
| 191 |
+
cellLum.set(-1, luminance(cs.getPropertyValue('--feat-none')));
|
| 192 |
+
const variants = new Map();
|
| 193 |
+
for (const k of BASE_KEYS) {
|
| 194 |
+
const lit = (cs.getPropertyValue(`--base-${k}-lit`) || '').trim() || '#ffffff';
|
| 195 |
+
const dim = (cs.getPropertyValue(`--base-${k}-dim`) || '').trim() || '#0c110e';
|
| 196 |
+
variants.set(k, [lit, luminance(lit), dim, luminance(dim)]);
|
| 197 |
+
}
|
| 198 |
+
for (const [slot, cl] of cellLum) {
|
| 199 |
+
inks.set(slot, cl > INK_FLIP ? '#0c110e' : '#ffffff');
|
| 200 |
+
for (const k of BASE_KEYS) {
|
| 201 |
+
const [lit, ll, dim, dl] = variants.get(k);
|
| 202 |
+
baseInk.set(`${slot}:${k}`, contrastOf(cl, ll) >= contrastOf(cl, dl) ? lit : dim);
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
}
|
| 206 |
+
const inkFor = slot => inks.get(slot) || '#0c110e';
|
| 207 |
+
const baseInkFor = (slot, letter) => {
|
| 208 |
+
const c = String(letter || 'n').toLowerCase();
|
| 209 |
+
return baseInk.get(`${slot}:${BASE_KEYS.includes(c) ? c : 'n'}`) || inkFor(slot);
|
| 210 |
+
};
|
| 211 |
+
|
| 212 |
+
const B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
| 213 |
+
const LUT = new Uint8Array(256);
|
| 214 |
+
for (let i = 0; i < B64.length; i++) LUT[B64.charCodeAt(i)] = i;
|
| 215 |
+
|
| 216 |
+
/* uint8 run -> activation, undoing the 255-level quantisation of the feature's
|
| 217 |
+
own peak in this window. */
|
| 218 |
+
function expand(runs, peak, n) {
|
| 219 |
+
const out = new Float32Array(n);
|
| 220 |
+
for (const [start, b64] of runs) {
|
| 221 |
+
const clean = b64.replace(/=+$/, '');
|
| 222 |
+
let byte = 0, bits = 0, at = start;
|
| 223 |
+
for (let i = 0; i < clean.length; i++) {
|
| 224 |
+
byte = (byte << 6) | LUT[clean.charCodeAt(i)];
|
| 225 |
+
bits += 6;
|
| 226 |
+
if (bits >= 8) {
|
| 227 |
+
bits -= 8;
|
| 228 |
+
out[at++] = ((byte >> bits) & 0xff) / 255 * peak;
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
return out;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/* ---- state ------------------------------------------------------------- */
|
| 236 |
+
let index = null, doc = null, cache = new Map();
|
| 237 |
+
let shown = []; // feature ids with a lane, in slot order
|
| 238 |
+
let start = 0, end = 0; // view, in window bases
|
| 239 |
+
let cursor = 0, pinned = null, mode = 'selected';
|
| 240 |
+
let W = 0, L = 120, R = 14, H = 400, compact = false;
|
| 241 |
+
let G = null; // geometry of the current draw
|
| 242 |
+
let svg = null, drag = null, pinch = null, vectors = new Map();
|
| 243 |
+
let restOpen = null; // whether the unnamed group is unfolded
|
| 244 |
+
const pointers = new Map();
|
| 245 |
+
|
| 246 |
+
let featureIndex = new Map();
|
| 247 |
+
const featureById = id => featureIndex.get(id);
|
| 248 |
+
/* This SAE is BatchTopK with k active features, so at any base the features
|
| 249 |
+
outside that set read exactly zero, not small. A window therefore only has
|
| 250 |
+
to store a stride-1 track for the features that reach a base's top ranks,
|
| 251 |
+
and every lane is a complete track. A feature with no stored track never
|
| 252 |
+
fires hard enough to reach them anywhere in the window. */
|
| 253 |
+
let tracks = new Map();
|
| 254 |
+
function trackOf(id) {
|
| 255 |
+
if (tracks.has(id)) return tracks.get(id);
|
| 256 |
+
const f = featureById(id);
|
| 257 |
+
const out = f
|
| 258 |
+
? { kind: 'full', v: vectorOf(id), peak: f.peak, label: f.label || null }
|
| 259 |
+
: null;
|
| 260 |
+
tracks.set(id, out);
|
| 261 |
+
return out;
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
/* Reading one base out of every stored track — thousands of them — must not
|
| 265 |
+
expand every track, so a single quantised byte is decoded in place. */
|
| 266 |
+
const runIdx = new Map();
|
| 267 |
+
function runsOf(f) {
|
| 268 |
+
if (runIdx.has(f.id)) return runIdx.get(f.id);
|
| 269 |
+
const list = asList(f.runs).map(([at, b64]) => {
|
| 270 |
+
const clean = String(b64).replace(/=+$/, '');
|
| 271 |
+
return { at, b64: clean, len: Math.floor(clean.length * 6 / 8) };
|
| 272 |
+
});
|
| 273 |
+
runIdx.set(f.id, list);
|
| 274 |
+
return list;
|
| 275 |
+
}
|
| 276 |
+
function byteAt(b64, i) {
|
| 277 |
+
const bit = i * 8;
|
| 278 |
+
let at = Math.floor(bit / 6);
|
| 279 |
+
const skip = bit - at * 6;
|
| 280 |
+
let acc = 0, bits = 0;
|
| 281 |
+
while (bits < skip + 8 && at < b64.length) {
|
| 282 |
+
acc = (acc << 6) | LUT[b64.charCodeAt(at++)];
|
| 283 |
+
bits += 6;
|
| 284 |
+
}
|
| 285 |
+
return (acc >> (bits - skip - 8)) & 0xff;
|
| 286 |
+
}
|
| 287 |
+
function valueAt(f, base) {
|
| 288 |
+
if (vectors.has(f.id)) return vectors.get(f.id)[base];
|
| 289 |
+
for (const r of runsOf(f)) {
|
| 290 |
+
if (base >= r.at && base < r.at + r.len) return byteAt(r.b64, base - r.at) / 255 * f.peak;
|
| 291 |
+
}
|
| 292 |
+
return 0;
|
| 293 |
+
}
|
| 294 |
+
const slotOf = id => shown.indexOf(id);
|
| 295 |
+
const vectorOf = id => {
|
| 296 |
+
if (!vectors.has(id)) {
|
| 297 |
+
const f = featureById(id);
|
| 298 |
+
vectors.set(id, f ? expand(f.runs, f.peak, doc.n_bases) : new Float32Array(doc.n_bases));
|
| 299 |
+
}
|
| 300 |
+
return vectors.get(id);
|
| 301 |
+
};
|
| 302 |
+
/* Genomic coordinate of a window base. A minus-strand window's seq is already
|
| 303 |
+
reverse-complemented, so display order runs against the coordinate. */
|
| 304 |
+
/* The window's n_bases occupy the 1-based inclusive interval [start+1, end]
|
| 305 |
+
(verified by refetching the region and matching seq_sha256), so `start` is a
|
| 306 |
+
half-open left edge and end - start === n_bases. A minus-strand window's seq
|
| 307 |
+
is already reverse-complemented, so display order runs against the
|
| 308 |
+
coordinate: base 0 sits at `end` and the last base at `start + 1`. */
|
| 309 |
+
const coordOf = base => doc.strand < 0 ? doc.end - base : doc.start + 1 + base;
|
| 310 |
+
const baseOf = coord => doc.strand < 0 ? doc.end - coord : coord - doc.start - 1;
|
| 311 |
+
const x = base => L + (base - start) / (end - start) * (W - L - R);
|
| 312 |
+
const nBasesShown = () => end - start;
|
| 313 |
+
/* the view's end is exclusive too, so the last base on screen is end - 1 */
|
| 314 |
+
const coordMin = () => Math.min(coordOf(start), coordOf(end - 1));
|
| 315 |
+
const coordMax = () => Math.max(coordOf(start), coordOf(end - 1));
|
| 316 |
+
|
| 317 |
+
/* ---- annotation lookups ------------------------------------------------ */
|
| 318 |
+
const strandsCovered = () => !doc.annotations ? []
|
| 319 |
+
: ['sense', 'antisense'].filter(s => doc.annotations[s] && doc.annotations[s].covered);
|
| 320 |
+
const regionAt = (strand, base) => {
|
| 321 |
+
const s = doc.annotations && doc.annotations[strand];
|
| 322 |
+
if (!s || !s.covered || !s.regions) return null;
|
| 323 |
+
for (const r of s.regions) if (base >= r.from && base <= r.to) return r.class;
|
| 324 |
+
return null;
|
| 325 |
+
};
|
| 326 |
+
const siteAt = (strand, base) => {
|
| 327 |
+
const s = doc.annotations && doc.annotations[strand];
|
| 328 |
+
if (!s || !s.covered || !s.sites) return null;
|
| 329 |
+
const hit = s.sites.find(t => t.base === base);
|
| 330 |
+
return hit ? hit.class : null;
|
| 331 |
+
};
|
| 332 |
+
const genesAt = (strand, base) => {
|
| 333 |
+
const s = doc.annotations && doc.annotations[strand];
|
| 334 |
+
if (!s || !s.covered || !s.genes) return [];
|
| 335 |
+
return s.genes.filter(g => base >= g.from && base <= g.to);
|
| 336 |
+
};
|
| 337 |
+
|
| 338 |
+
/* Per-base winner among the lanes on screen, or the whole dictionary's rank-0
|
| 339 |
+
feature when the reader asks for it. */
|
| 340 |
+
let argmaxLut = null;
|
| 341 |
+
function buildArgmaxLut() {
|
| 342 |
+
const lut = new Int32Array(doc.n_bases).fill(-1);
|
| 343 |
+
for (const [s, len, id] of doc.argmax.runs) {
|
| 344 |
+
if (id < 0) continue;
|
| 345 |
+
for (let i = s; i < s + len; i++) lut[i] = id;
|
| 346 |
+
}
|
| 347 |
+
argmaxLut = lut;
|
| 348 |
+
}
|
| 349 |
+
const topGlobal = base => argmaxLut ? argmaxLut[base] : -1;
|
| 350 |
+
|
| 351 |
+
function topSelected(base) {
|
| 352 |
+
let best = -1, bestV = 0;
|
| 353 |
+
for (const id of shown) {
|
| 354 |
+
const t = trackOf(id);
|
| 355 |
+
if (!t) continue;
|
| 356 |
+
const v = t.v[base];
|
| 357 |
+
if (v > bestV) { bestV = v; best = id; }
|
| 358 |
+
}
|
| 359 |
+
return { id: best, value: bestV };
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
function globalSlotMap() {
|
| 363 |
+
const ranked = doc.features.slice().sort((a, b) => b.argmax_bases - a.argmax_bases)
|
| 364 |
+
.filter(f => f.argmax_bases > 0).slice(0, GLOBAL_KEYS);
|
| 365 |
+
const map = new Map(), used = new Set();
|
| 366 |
+
for (const f of ranked) {
|
| 367 |
+
if (!shown.includes(f.id)) continue;
|
| 368 |
+
const slot = slotOf(f.id) % PALETTE;
|
| 369 |
+
map.set(f.id, slot); used.add(slot);
|
| 370 |
+
}
|
| 371 |
+
let next = 0;
|
| 372 |
+
for (const f of ranked) {
|
| 373 |
+
if (map.has(f.id)) continue;
|
| 374 |
+
while (next < PALETTE && used.has(next)) next++;
|
| 375 |
+
const slot = Math.min(next, PALETTE - 1);
|
| 376 |
+
map.set(f.id, slot); used.add(slot);
|
| 377 |
+
}
|
| 378 |
+
return map;
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
/* ---- geometry ---------------------------------------------------------- */
|
| 382 |
+
/* One pass over the tracks, so every y coordinate the renderer uses is decided
|
| 383 |
+
in one place and the layout never shifts while panning. */
|
| 384 |
+
function layout() {
|
| 385 |
+
W = Math.max(240, host.clientWidth || 900);
|
| 386 |
+
readFonts();
|
| 387 |
+
|
| 388 |
+
/* The gutter has to hold whichever is widest: a lane name, a lane's
|
| 389 |
+
id-and-max line, a strand name, or a register key. Below the point where
|
| 390 |
+
that leaves too little room for the plot, the labels move onto the plot
|
| 391 |
+
and the gutter disappears. */
|
| 392 |
+
let need = 0;
|
| 393 |
+
for (const id of shown) {
|
| 394 |
+
const t = trackOf(id);
|
| 395 |
+
if (!t) continue;
|
| 396 |
+
const pre = t.label ? `f${id} · ` : '';
|
| 397 |
+
need = Math.max(need,
|
| 398 |
+
measure(t.label || `f${id}`, t.label ? FONTS.name : FONTS.nameId),
|
| 399 |
+
measure(pre + 'max 00.00', FONTS.meta));
|
| 400 |
+
}
|
| 401 |
+
need = Math.max(need,
|
| 402 |
+
measure('top lane here', FONTS.name), measure('top of 8,192', FONTS.name),
|
| 403 |
+
measure('max in column', FONTS.meta), measure('most bases in column', FONTS.meta),
|
| 404 |
+
measure('antisense', FONTS.strand), measure('gene model', FONTS.strand),
|
| 405 |
+
measureTracked('ANNOTATION', FONTS.reg, 1.17));
|
| 406 |
+
const gutter = Math.min(212, Math.round(need) + 12);
|
| 407 |
+
compact = (W - gutter - 14) < 430;
|
| 408 |
+
L = compact ? 3 : gutter;
|
| 409 |
+
R = compact ? 3 : 14;
|
| 410 |
+
|
| 411 |
+
const g = { plotL: L, plotR: W - R, plotW: W - L - R, compact };
|
| 412 |
+
let y = 2;
|
| 413 |
+
g.anchorBase = y + 9; g.anchorTop = y; y += 13;
|
| 414 |
+
g.tickBase = y + 10; g.axisY = y + 15; y += 15;
|
| 415 |
+
g.findTop = null; g.findBase = null;
|
| 416 |
+
if (doc.selection && doc.selection.position != null) {
|
| 417 |
+
g.findTop = y + 1; g.findBase = y + 9; y += 13;
|
| 418 |
+
}
|
| 419 |
+
if (compact) { g.stripLabBase = y + 10; y += 13; }
|
| 420 |
+
g.stripY = y; g.stripH = compact ? 20 : 26; y += g.stripH;
|
| 421 |
+
if (mode === 'global') { g.gkeyY = y + 5; y += 19; } else g.gkeyY = null;
|
| 422 |
+
y += compact ? 12 : 15;
|
| 423 |
+
|
| 424 |
+
/* annotation panel */
|
| 425 |
+
g.annTop = y;
|
| 426 |
+
let py = y + (compact ? 6 : 7);
|
| 427 |
+
g.annRegBase = py + 7; py += compact ? 13 : 13;
|
| 428 |
+
g.blocks = [];
|
| 429 |
+
const blocks = strandsCovered().map(s => ({ kind: 'strand', strand: s }));
|
| 430 |
+
if (!blocks.length && doc.gene_report) blocks.push({ kind: 'model' });
|
| 431 |
+
for (const b of blocks) {
|
| 432 |
+
if (compact) { b.labBase = py + 8; py += 12; }
|
| 433 |
+
if (!compact) { b.siteLabBase = py + 8; py += 11; }
|
| 434 |
+
b.siteY = py; b.siteH = 8; py += 8;
|
| 435 |
+
if (b.kind === 'strand') { b.bandY = py; b.bandH = compact ? 13 : 14; py += b.bandH; }
|
| 436 |
+
b.geneTop = py; b.geneH = b.kind === 'model' ? 22 : 15;
|
| 437 |
+
b.geneLabBase = py + 8; b.geneLineY = py + (b.kind === 'model' ? 16 : 12);
|
| 438 |
+
py += b.geneH;
|
| 439 |
+
b.centre = (b.siteY + py) / 2;
|
| 440 |
+
py += compact ? 6 : 8;
|
| 441 |
+
g.blocks.push(b);
|
| 442 |
+
}
|
| 443 |
+
py += compact ? 3 : 4;
|
| 444 |
+
g.annH = Math.max(20, py - g.annTop);
|
| 445 |
+
y = g.annTop + g.annH;
|
| 446 |
+
y += compact ? 15 : 20;
|
| 447 |
+
|
| 448 |
+
/* lanes */
|
| 449 |
+
g.laneRegBase = y + 7; g.laneRuleY = y - 3; y += 13;
|
| 450 |
+
g.laneTop = y;
|
| 451 |
+
g.laneH = compact ? 30 : 44;
|
| 452 |
+
g.laneLab = compact ? 13 : 0;
|
| 453 |
+
g.laneGap = compact ? 9 : 11;
|
| 454 |
+
g.lanePitch = g.laneH + g.laneGap + g.laneLab;
|
| 455 |
+
y += Math.max(g.lanePitch, shown.length * g.lanePitch);
|
| 456 |
+
g.gridTop = g.annTop;
|
| 457 |
+
g.gridBottom = y - g.laneGap;
|
| 458 |
+
H = Math.round(y + 6);
|
| 459 |
+
G = g;
|
| 460 |
+
return g;
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
/* ---- drawing ----------------------------------------------------------- */
|
| 464 |
+
function render() {
|
| 465 |
+
if (!doc || !svg) return;
|
| 466 |
+
readPalette();
|
| 467 |
+
layout();
|
| 468 |
+
svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
|
| 469 |
+
svg.setAttribute('aria-label', ariaLabel());
|
| 470 |
+
const pxPerBase = G.plotW / nBasesShown();
|
| 471 |
+
const cols = Math.max(1, Math.round(G.plotW));
|
| 472 |
+
|
| 473 |
+
let out = `<title>${esc(windowTitleText())} — ${esc(doc.chrom)}:${fmt(coordMin())}–${
|
| 474 |
+
fmt(coordMax())}, ${shown.length} feature lane${shown.length === 1 ? '' : 's'}</title>`;
|
| 475 |
+
out += `<defs><clipPath id="feat-clip"><rect x="${L}" y="0" width="${
|
| 476 |
+
G.plotW}" height="${H}"/></clipPath></defs>`;
|
| 477 |
+
|
| 478 |
+
out += drawRuler();
|
| 479 |
+
out += drawScale(pxPerBase);
|
| 480 |
+
out += drawStrip(cols, pxPerBase);
|
| 481 |
+
if (mode === 'global') out += drawGlobalKey();
|
| 482 |
+
out += drawFinding(pxPerBase);
|
| 483 |
+
out += drawAnnotations(pxPerBase);
|
| 484 |
+
out += drawLanes(cols, pxPerBase);
|
| 485 |
+
out += drawAnchor(pxPerBase);
|
| 486 |
+
out += '<g id="feat-focus"></g>';
|
| 487 |
+
svg.innerHTML = out;
|
| 488 |
+
focus(pinned === null ? cursor : pinned, false);
|
| 489 |
+
paintRegion(pxPerBase);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
function ticks() {
|
| 493 |
+
const target = nBasesShown() / Math.max(1, Math.floor(G.plotW / (compact ? 84 : 112)));
|
| 494 |
+
const step = [1, 2, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000].find(s => s >= target) || 5000;
|
| 495 |
+
const first = Math.ceil(coordMin() / step) * step;
|
| 496 |
+
const out = [];
|
| 497 |
+
for (let c = first; c <= coordMax(); c += step) {
|
| 498 |
+
const base = baseOf(c);
|
| 499 |
+
if (base < start || base > end) continue;
|
| 500 |
+
out.push({ coord: c, base, px: x(base) });
|
| 501 |
+
}
|
| 502 |
+
return out;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
/* A conventional scale bar: a bracket exactly one round number of bases wide,
|
| 506 |
+
snapped to 1/2/5 x 10^n, relabelled whenever the view changes. It lives in
|
| 507 |
+
the top row, right-aligned, clear of the ruler and of the gutter. */
|
| 508 |
+
function drawScale(pxPerBase) {
|
| 509 |
+
G.scaleLeft = W;
|
| 510 |
+
const room = Math.min(compact ? 118 : 176, G.plotW * 0.32);
|
| 511 |
+
let step = 1;
|
| 512 |
+
for (let e = 0; e <= 6; e++) {
|
| 513 |
+
for (const m of [1, 2, 5]) {
|
| 514 |
+
const v = m * Math.pow(10, e);
|
| 515 |
+
if (v * pxPerBase <= room) step = v;
|
| 516 |
+
}
|
| 517 |
+
}
|
| 518 |
+
const barPx = step * pxPerBase;
|
| 519 |
+
if (barPx < 16) return '';
|
| 520 |
+
const label = step >= 1000 ? `${step / 1000} kb` : `${step} bp`;
|
| 521 |
+
const y = G.anchorTop + 7;
|
| 522 |
+
const right = W - R, left = right - barPx;
|
| 523 |
+
let out = path(`M${left.toFixed(2)} ${y - 3.5}v7M${left.toFixed(2)} ${y}H${right}M${right} ${y - 3.5}v7`, 'scalebar');
|
| 524 |
+
const lw = measure(label, FONTS.reg) + 12;
|
| 525 |
+
const px = left - 6 - lw;
|
| 526 |
+
if (px > L + 8) {
|
| 527 |
+
out += rect(px, y - 6.5, lw, 13, '', 'scale-pill', ' rx="6.5"');
|
| 528 |
+
out += text(px + lw / 2, y + 3.2, label, 'scale-lab', 'middle');
|
| 529 |
+
G.scaleLeft = px;
|
| 530 |
+
} else {
|
| 531 |
+
G.scaleLeft = left;
|
| 532 |
+
}
|
| 533 |
+
return out;
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
function drawRuler() {
|
| 537 |
+
let out = path(`M${L} ${G.axisY}H${W - R}`, 'axis');
|
| 538 |
+
let grid = '';
|
| 539 |
+
for (const t of ticks()) {
|
| 540 |
+
const anchor = t.px < L + 30 ? 'start' : t.px > W - R - 30 ? 'end' : 'middle';
|
| 541 |
+
out += path(`M${t.px.toFixed(2)} ${G.axisY}V${G.axisY + 4}`, 'axis');
|
| 542 |
+
out += text(t.px, G.tickBase, fmt(t.coord), 'ruler-tick', anchor);
|
| 543 |
+
grid += `M${t.px.toFixed(2)} ${G.gridTop}V${G.gridBottom}`;
|
| 544 |
+
}
|
| 545 |
+
out += `<g clip-path="url(#feat-clip)">${path(grid, 'grid')}</g>`;
|
| 546 |
+
return out;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
/* The headline device: one mark per pixel column, coloured by whichever
|
| 550 |
+
feature fires hardest anywhere under that column. Maximum, never a mean. */
|
| 551 |
+
function drawStrip(cols, pxPerBase) {
|
| 552 |
+
const globalSlots = mode === 'global' ? globalSlotMap() : null;
|
| 553 |
+
const colSlot = new Int32Array(cols).fill(-1);
|
| 554 |
+
let cells = '';
|
| 555 |
+
for (let c = 0; c < cols; c++) {
|
| 556 |
+
const from = Math.floor(start + c / pxPerBase);
|
| 557 |
+
const to = Math.max(from + 1, Math.floor(start + (c + 1) / pxPerBase));
|
| 558 |
+
let winner = -1;
|
| 559 |
+
if (mode === 'selected') {
|
| 560 |
+
let best = 0;
|
| 561 |
+
for (let b = from; b < to && b < doc.n_bases; b++) {
|
| 562 |
+
const t = topSelected(b);
|
| 563 |
+
if (t.value > best) { best = t.value; winner = t.id; }
|
| 564 |
+
}
|
| 565 |
+
} else {
|
| 566 |
+
/* The stored argmax track carries the rank-0 feature per base, not its
|
| 567 |
+
value, so a column can only report which feature holds the most bases
|
| 568 |
+
in it. Named as such in the label rather than passed off as a max. */
|
| 569 |
+
const tally = new Map();
|
| 570 |
+
let best = 0;
|
| 571 |
+
for (let b = from; b < to && b < doc.n_bases; b++) {
|
| 572 |
+
const id = topGlobal(b);
|
| 573 |
+
if (id < 0) continue;
|
| 574 |
+
const n = (tally.get(id) || 0) + 1;
|
| 575 |
+
tally.set(id, n);
|
| 576 |
+
if (n > best) { best = n; winner = id; }
|
| 577 |
+
}
|
| 578 |
+
}
|
| 579 |
+
const slot = winner < 0 ? -1
|
| 580 |
+
: (mode === 'selected' ? slotOf(winner) : (globalSlots.get(winner) ?? -1));
|
| 581 |
+
colSlot[c] = slot;
|
| 582 |
+
cells += rect(L + c, G.stripY, 1.02, G.stripH,
|
| 583 |
+
winner < 0 ? 'var(--feat-none)' : featColour(slot), winner < 0 ? 'nofire' : '');
|
| 584 |
+
}
|
| 585 |
+
let out = `<g clip-path="url(#feat-clip)">${cells}`;
|
| 586 |
+
/* letters once there is room for them, inked by base identity in whichever
|
| 587 |
+
variant of that base's hue has more contrast against the cell */
|
| 588 |
+
if (pxPerBase >= 7) {
|
| 589 |
+
for (let b = Math.floor(start); b < Math.ceil(end) && b < doc.n_bases; b++) {
|
| 590 |
+
if (b < 0) continue;
|
| 591 |
+
const centre = x(b) + pxPerBase / 2;
|
| 592 |
+
if (centre - 4 < L || centre + 4 > W - R) continue;
|
| 593 |
+
const slot = colSlot[Math.max(0, Math.min(cols - 1, Math.round(centre - L)))];
|
| 594 |
+
/* an inline style, because #feat-plot text { fill } would beat a
|
| 595 |
+
presentation attribute and every letter would come out ink-2 */
|
| 596 |
+
out += `<text x="${centre.toFixed(2)}" y="${(G.stripY + G.stripH / 2 + 3.6).toFixed(2)}" class="seqletter" text-anchor="middle" style="fill:${
|
| 597 |
+
baseInkFor(slot, doc.seq[b])}">${doc.seq[b]}</text>`;
|
| 598 |
+
}
|
| 599 |
+
}
|
| 600 |
+
out += '</g>';
|
| 601 |
+
out += rect(L, G.stripY, G.plotW, G.stripH, '', 'strip-frame');
|
| 602 |
+
|
| 603 |
+
const primary = mode === 'selected' ? 'top lane here' : 'top of 8,192';
|
| 604 |
+
const secondary = mode === 'selected' ? 'max in column' : 'most bases in column';
|
| 605 |
+
if (compact) {
|
| 606 |
+
out += text(L + 2, G.stripLabBase, primary, 'lane-name', 'start');
|
| 607 |
+
out += text(W - R, G.stripLabBase, secondary, 'lane-max', 'end');
|
| 608 |
+
} else {
|
| 609 |
+
out += text(L - 11, G.stripY + 12, primary, 'lane-name', 'end');
|
| 610 |
+
out += text(L - 11, G.stripY + 24, secondary, 'lane-max', 'end');
|
| 611 |
+
}
|
| 612 |
+
return out;
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
/* In global mode the strip reads all 8,192 features, so the ten colours mean
|
| 616 |
+
something other than the lanes below. Name them. */
|
| 617 |
+
function drawGlobalKey() {
|
| 618 |
+
const ranked = Array.from(globalSlotMap().entries());
|
| 619 |
+
let out = '', cx = L + 1;
|
| 620 |
+
out += text(cx, G.gkeyY + 8, 'strip keys:', 'lane-max', 'start');
|
| 621 |
+
cx += measure('strip keys:', FONTS.meta) + 9;
|
| 622 |
+
for (const [id, slot] of ranked) {
|
| 623 |
+
const label = `f${id}`;
|
| 624 |
+
const w = measure(label, FONTS.meta);
|
| 625 |
+
if (cx + w + 12 > W - R) break;
|
| 626 |
+
out += rect(cx, G.gkeyY + 1, 8, 8, featColour(slot), '', ' rx="2"');
|
| 627 |
+
out += text(cx + 11, G.gkeyY + 8, label, 'gkey-id', 'start');
|
| 628 |
+
cx += w + 20;
|
| 629 |
+
}
|
| 630 |
+
return out;
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
/* The annotation register. A recessed plate, pale washes, mid-tone ticks: the
|
| 634 |
+
point is that no part of this block can be mistaken for a measurement. */
|
| 635 |
+
function drawAnnotations(pxPerBase) {
|
| 636 |
+
let out = rect(L, G.annTop, G.plotW, G.annH, '', 'ann-panel', ' rx="4"');
|
| 637 |
+
out += compact
|
| 638 |
+
? text(L + 6, G.annRegBase, 'ANNOTATION', 'reg-lab halo-panel', 'start')
|
| 639 |
+
: text(L - 11, G.annRegBase, 'ANNOTATION', 'reg-lab', 'end');
|
| 640 |
+
|
| 641 |
+
let body = '';
|
| 642 |
+
for (const b of G.blocks) {
|
| 643 |
+
const name = b.kind === 'model' ? 'gene model' : b.strand;
|
| 644 |
+
if (compact) body += text(L + 6, b.labBase, name, 'strand-name halo-panel', 'start');
|
| 645 |
+
else out += text(L - 11, b.centre + 3, name, 'strand-name', 'end');
|
| 646 |
+
body += b.kind === 'model' ? drawGeneModel(b, pxPerBase) : drawStrand(b, pxPerBase);
|
| 647 |
+
}
|
| 648 |
+
return out + `<g clip-path="url(#feat-clip)">${body}</g>`;
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
function drawStrand(b, pxPerBase) {
|
| 652 |
+
const s = doc.annotations[b.strand];
|
| 653 |
+
let out = '';
|
| 654 |
+
/* regions partition every base, so they are drawn as abutting bands */
|
| 655 |
+
let bandLabels = '';
|
| 656 |
+
for (const r of s.regions) {
|
| 657 |
+
if (r.to < start || r.from > end) continue;
|
| 658 |
+
const a = x(Math.max(r.from, start)), z = x(Math.min(r.to + 1, end));
|
| 659 |
+
out += rect(a, b.bandY, z - a, b.bandH, `var(--ann-${r.class})`, 'ann-band');
|
| 660 |
+
/* The washes are pale on purpose, which makes them close in luminance and
|
| 661 |
+
useless under greyscale or dichromacy, so each band says what it is
|
| 662 |
+
wherever there is room for the word. */
|
| 663 |
+
const label = REGION_LABEL[r.class] || r.class;
|
| 664 |
+
const lw = measure(label, FONTS.band);
|
| 665 |
+
if (z - a > lw + 10) {
|
| 666 |
+
bandLabels += text((a + z) / 2, b.bandY + b.bandH / 2 + 3, label, 'ann-bandlab', 'middle');
|
| 667 |
+
}
|
| 668 |
+
}
|
| 669 |
+
out += bandLabels;
|
| 670 |
+
out += path(`M${L} ${b.bandY}H${W - R}M${L} ${b.bandY + b.bandH}H${W - R}`, 'ann-band-rule');
|
| 671 |
+
|
| 672 |
+
/* gene extents, with a strand chevron and end caps */
|
| 673 |
+
for (const g of s.genes) {
|
| 674 |
+
if (g.to < start || g.from > end) continue;
|
| 675 |
+
const a = Math.max(x(g.from), L), z = Math.min(x(g.to + 1), W - R);
|
| 676 |
+
out += path(`M${a.toFixed(2)} ${b.geneLineY}H${z.toFixed(2)}` +
|
| 677 |
+
`M${a.toFixed(2)} ${b.geneLineY - 3}v6M${z.toFixed(2)} ${b.geneLineY - 3}v6`, 'gene-line');
|
| 678 |
+
const label = `${b.strand === 'sense' ? '▶' : '◀'} ${g.gene_name || g.gene_id}`;
|
| 679 |
+
if (z - a > measure(label, FONTS.site) + 10) {
|
| 680 |
+
out += text(b.strand === 'sense' ? a + 4 : z - 4, b.geneLabBase, label, 'gene-label',
|
| 681 |
+
b.strand === 'sense' ? 'start' : 'end');
|
| 682 |
+
}
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
/* sites are points, never runs: a tick with a small cap, so the idiom does
|
| 686 |
+
not resemble an activation stem rising from a baseline */
|
| 687 |
+
const marks = s.sites.filter(t => t.base >= start && t.base <= end);
|
| 688 |
+
const tickW = Math.max(1.4, Math.min(pxPerBase, 4));
|
| 689 |
+
for (const t of marks) {
|
| 690 |
+
const px = x(t.base);
|
| 691 |
+
out += rect(px, b.siteY, tickW, b.siteH + (b.bandH || 0), `var(--ann-${t.class})`);
|
| 692 |
+
out += rect(px - 2 + tickW / 2, b.siteY, 4, 2.5, `var(--ann-${t.class})`);
|
| 693 |
+
}
|
| 694 |
+
if (!compact) out += siteLabels(marks.map(t => ({ px: x(t.base), cls: t.class })), b.siteLabBase);
|
| 695 |
+
return out;
|
| 696 |
+
}
|
| 697 |
+
|
| 698 |
+
/* Site class names, drawn only where they fit without touching each other. */
|
| 699 |
+
function siteLabels(marks, base) {
|
| 700 |
+
let out = '', lastRight = -1e9;
|
| 701 |
+
for (const m of marks.slice().sort((a, b) => a.px - b.px)) {
|
| 702 |
+
const long = m.label || SITE_LABEL[m.cls] || m.cls;
|
| 703 |
+
const short = m.label ? m.label.split(' ')[0] : (SITE_SHORT[m.cls] || m.cls);
|
| 704 |
+
for (const label of [long, short]) {
|
| 705 |
+
const w = measure(label, FONTS.site);
|
| 706 |
+
if (m.px - w / 2 < lastRight + 5) continue;
|
| 707 |
+
if (m.px - w / 2 < L + 1 || m.px + w / 2 > W - R - 1) continue;
|
| 708 |
+
out += text(m.px, base, label, 'ann-sitelab', 'middle');
|
| 709 |
+
lastRight = m.px + w / 2;
|
| 710 |
+
break;
|
| 711 |
+
}
|
| 712 |
+
}
|
| 713 |
+
return out;
|
| 714 |
+
}
|
| 715 |
+
|
| 716 |
+
/* The featured locus carries the report's own gene model instead of a label
|
| 717 |
+
track, plus the two donor positions the annotation releases disagree on. */
|
| 718 |
+
function drawGeneModel(b, pxPerBase) {
|
| 719 |
+
const g = doc.gene_report;
|
| 720 |
+
let out = '';
|
| 721 |
+
const a = x(baseOf(g.start)), z = x(baseOf(g.end) + 1);
|
| 722 |
+
out += path(`M${a.toFixed(2)} ${b.geneLineY}H${z.toFixed(2)}`, 'gene-line');
|
| 723 |
+
for (const [s0, s1] of g.exons) {
|
| 724 |
+
const ex = x(baseOf(s0)), ew = x(baseOf(s1) + 1) - ex;
|
| 725 |
+
out += rect(ex, b.geneLineY - 5, ew, 10, '', 'gene-exon');
|
| 726 |
+
}
|
| 727 |
+
out += text(a + 4, b.geneLabBase, `▶ ${g.gene_id}`, 'gene-label', 'start');
|
| 728 |
+
|
| 729 |
+
const c = doc.contested;
|
| 730 |
+
if (c) {
|
| 731 |
+
const marks = [];
|
| 732 |
+
if (c.araport11_donor != null) marks.push({ base: c.araport11_donor, cls: 'donor', label: 'Araport11 donor' });
|
| 733 |
+
if (c.tair12_donor != null) marks.push({ base: c.tair12_donor, cls: 'donor', label: 'TAIR12 donor' });
|
| 734 |
+
if (c.acceptor != null) marks.push({ base: c.acceptor, cls: 'acceptor', label: 'acceptor' });
|
| 735 |
+
const tickW = Math.max(1.4, Math.min(pxPerBase, 4));
|
| 736 |
+
for (const m of marks) {
|
| 737 |
+
if (m.base < start || m.base > end) continue;
|
| 738 |
+
const px = x(m.base);
|
| 739 |
+
out += rect(px, b.siteY, tickW, b.siteH + 5, `var(--ann-${m.cls})`);
|
| 740 |
+
out += rect(px - 2 + tickW / 2, b.siteY, 4, 2.5, `var(--ann-${m.cls})`);
|
| 741 |
+
}
|
| 742 |
+
if (!compact) {
|
| 743 |
+
out += siteLabels(marks.filter(m => m.base >= start && m.base <= end)
|
| 744 |
+
.map(m => ({ px: x(m.base), cls: m.cls, label: m.label })), b.siteLabBase, true);
|
| 745 |
+
}
|
| 746 |
+
}
|
| 747 |
+
return out;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
+
/* One lane per selected feature. A bar per pixel column, height = the maximum
|
| 751 |
+
activation measured under that column, scaled to the feature's peak over the
|
| 752 |
+
whole window so lanes stay comparable as the reader zooms. Columns with no
|
| 753 |
+
measurement above zero get no bar at all. */
|
| 754 |
+
/* A lane's published name is a claim the report measured. A lane chosen because it
|
| 755 |
+
matches an annotated category best in THIS window is a different kind of statement,
|
| 756 |
+
so it is labelled with the category and drawn in the softer style. */
|
| 757 |
+
let laneMarks = new Map();
|
| 758 |
+
function buildLaneMarks() {
|
| 759 |
+
laneMarks = new Map();
|
| 760 |
+
for (const r of asList(doc && doc.lane_rationale)) {
|
| 761 |
+
if (r && r.id != null) laneMarks.set(Number(r.id), r);
|
| 762 |
+
}
|
| 763 |
+
}
|
| 764 |
+
const marksOf = id => laneMarks.get(id) || null;
|
| 765 |
+
|
| 766 |
+
function drawLanes(cols, pxPerBase) {
|
| 767 |
+
if (!shown.length) {
|
| 768 |
+
return text(L + 4, G.laneTop + 20,
|
| 769 |
+
'No lanes. Pick a feature below to read it base by base.', 'lane-silent', 'start');
|
| 770 |
+
}
|
| 771 |
+
let out = path(`M${L} ${G.laneRuleY}H${W - R}`, 'reg-rule');
|
| 772 |
+
out += compact
|
| 773 |
+
? text(L + 2, G.laneRegBase, 'ACTIVATION', 'reg-lab', 'start')
|
| 774 |
+
: text(L - 11, G.laneRegBase, 'ACTIVATION', 'reg-lab', 'end');
|
| 775 |
+
|
| 776 |
+
let plot = '', gutter = '';
|
| 777 |
+
shown.forEach((id, slot) => {
|
| 778 |
+
const t = trackOf(id);
|
| 779 |
+
if (!t) return;
|
| 780 |
+
const top = G.laneTop + slot * G.lanePitch + G.laneLab;
|
| 781 |
+
const bottom = top + G.laneH;
|
| 782 |
+
const colour = featColour(slot);
|
| 783 |
+
|
| 784 |
+
/* A bar per pixel column, its height the MAXIMUM measured under that
|
| 785 |
+
column, scaled to the feature's peak over the whole window so lanes stay
|
| 786 |
+
comparable as the reader zooms. A column with nothing above zero gets no
|
| 787 |
+
bar at all: zero means the feature did not fire. */
|
| 788 |
+
let localMax = 0;
|
| 789 |
+
const stems = [];
|
| 790 |
+
for (let c = 0; c < cols; c++) {
|
| 791 |
+
const from = Math.floor(start + c / pxPerBase);
|
| 792 |
+
const to = Math.max(from + 1, Math.floor(start + (c + 1) / pxPerBase));
|
| 793 |
+
let peak = 0;
|
| 794 |
+
for (let b = from; b < to && b < doc.n_bases; b++) if (t.v[b] > peak) peak = t.v[b];
|
| 795 |
+
if (peak > 0) {
|
| 796 |
+
if (peak > localMax) localMax = peak;
|
| 797 |
+
const h = Math.max(1, peak / t.peak * (G.laneH - 3));
|
| 798 |
+
stems.push(`M${(L + c + 0.5).toFixed(2)} ${bottom}V${(bottom - h).toFixed(2)}`);
|
| 799 |
+
}
|
| 800 |
+
}
|
| 801 |
+
const silent = localMax === 0;
|
| 802 |
+
plot += path(`M${L} ${bottom}H${W - R}`, silent ? 'lane-base silent' : 'lane-base');
|
| 803 |
+
if (stems.length) {
|
| 804 |
+
plot += `<path class="lane-stems" d="${stems.join('')}" stroke="${colour}"/>`;
|
| 805 |
+
}
|
| 806 |
+
|
| 807 |
+
const mk = marksOf(id);
|
| 808 |
+
const inWindow = !t.label && mk && mk.nice && mk.mcc_in_window != null;
|
| 809 |
+
const name = t.label || (inWindow ? mk.nice : `f${id}`);
|
| 810 |
+
const pre = (t.label || inWindow) ? `f${id} · ` : '';
|
| 811 |
+
const meta = pre + (silent ? 'silent here' : `max ${num(localMax)}`);
|
| 812 |
+
const fit = inWindow ? `marks ${num(mk.mcc_in_window)} here` : null;
|
| 813 |
+
const tip = `f${id}${t.label ? ' ' + t.label : ''}` +
|
| 814 |
+
(inWindow ? ` — the feature that marks ${mk.nice} best in this window, Matthews`
|
| 815 |
+
+ ` correlation ${num(mk.mcc_in_window)} at its own ${mk.cut_percentile}th percentile.`
|
| 816 |
+
+ ' Chosen and scored in this window, so it describes this picture rather than'
|
| 817 |
+
+ ' estimating how it behaves elsewhere.' : ' — ') + (silent
|
| 818 |
+
? `silent over this view; window peak ${num(t.peak)}.`
|
| 819 |
+
: `max in view ${num(localMax)}, window peak ${num(t.peak)}.`);
|
| 820 |
+
|
| 821 |
+
gutter += `<g><title>${esc(tip)}</title>`;
|
| 822 |
+
const dim = silent ? ' opacity="0.4"' : '';
|
| 823 |
+
if (compact) {
|
| 824 |
+
gutter += rect(L + 1, top - 11, 8, 8, colour, '', ' rx="2"' + dim);
|
| 825 |
+
gutter += text(L + 13, top - 4, name,
|
| 826 |
+
'halo ' + (t.label ? 'lane-name' : 'lane-name lane-id-name'), 'start');
|
| 827 |
+
gutter += text(W - R - 1, top - 4, meta + (fit ? ` · ${num(mk.mcc_in_window)}` : ''),
|
| 828 |
+
(silent ? 'lane-silent' : 'lane-max') + ' halo', 'end');
|
| 829 |
+
} else {
|
| 830 |
+
gutter += rect(L - 7, top, 3, G.laneH, colour, 'lane-key', ' rx="1.5"' + dim);
|
| 831 |
+
gutter += text(L - 11, top + 12, name,
|
| 832 |
+
t.label ? 'lane-name' : 'lane-name lane-id-name', 'end');
|
| 833 |
+
gutter += text(L - 11, top + 25, meta, silent ? 'lane-silent' : 'lane-max', 'end');
|
| 834 |
+
if (fit) gutter += text(L - 11, top + 37, fit, 'lane-fit', 'end');
|
| 835 |
+
}
|
| 836 |
+
gutter += '</g>';
|
| 837 |
+
});
|
| 838 |
+
|
| 839 |
+
return out + `<g clip-path="url(#feat-clip)">${plot}</g>` + gutter;
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
/* The base this window was chosen for: a solid triangle, deliberately paired
|
| 843 |
+
with the anchor's dashed outline — both say "look here", one from the
|
| 844 |
+
sampler and one from the selection, and neither is an annotation. For a
|
| 845 |
+
moved annotation both frames are drawn, filled for the position the
|
| 846 |
+
selection names and hollow for where the newer release puts it, with the
|
| 847 |
+
shift bracketed between them once there is room to label it. */
|
| 848 |
+
function drawFinding(pxPerBase) {
|
| 849 |
+
if (G.findBase === null) return '';
|
| 850 |
+
const marks = findingMarks(doc.selection).filter(m => m.base >= start && m.base <= end);
|
| 851 |
+
if (!marks.length) return '';
|
| 852 |
+
let out = '';
|
| 853 |
+
for (const m of marks) {
|
| 854 |
+
const px = x(m.base) + pxPerBase / 2;
|
| 855 |
+
out += `<path class="finding${m.filled ? '' : ' open'}" d="M${(px - 4.5).toFixed(2)} ${
|
| 856 |
+
G.findTop}h9l-4.5 7z"/>`;
|
| 857 |
+
}
|
| 858 |
+
const xs = marks.map(m => x(m.base) + pxPerBase / 2);
|
| 859 |
+
const gap = Math.abs(xs[xs.length - 1] - xs[0]);
|
| 860 |
+
const e = (doc.selection.evidence) || {};
|
| 861 |
+
const lim = Math.min(W - R, G.scaleLeft - 8);
|
| 862 |
+
const wide = txt => measureTracked(txt, FONTS.reg, 0.72);
|
| 863 |
+
if (marks.length > 1 && gap > 30 && typeof e.shift_bp === 'number') {
|
| 864 |
+
const a = Math.min(...xs), z = Math.max(...xs);
|
| 865 |
+
out += path(`M${a.toFixed(2)} ${G.findTop + 3.5}H${z.toFixed(2)}`, 'finding-bracket');
|
| 866 |
+
out += text((a + z) / 2, G.findBase - 6, `${fmt(e.shift_bp)} bp`, 'finding-lab', 'middle');
|
| 867 |
+
/* the side labels are a nicety; the bracket and the base-pair count carry
|
| 868 |
+
the claim, so they are dropped rather than pushed off the plot */
|
| 869 |
+
if (a - 4 - wide(marks[0].tag) > L) {
|
| 870 |
+
out += text(a - 4, G.findBase, marks[0].tag, 'finding-lab', 'end');
|
| 871 |
+
}
|
| 872 |
+
const last = marks[marks.length - 1].tag;
|
| 873 |
+
if (z + 4 + wide(last) < lim) out += text(z + 4, G.findBase, last, 'finding-lab', 'start');
|
| 874 |
+
} else {
|
| 875 |
+
const px = xs[0];
|
| 876 |
+
const long = marks.length > 1
|
| 877 |
+
? `${selKind(doc.selection).mark}, ${fmt(e.shift_bp || 0)} bp` : marks[0].tag;
|
| 878 |
+
const short = marks.length > 1 ? `${fmt(e.shift_bp || 0)} bp` : marks[0].tag;
|
| 879 |
+
let done = false;
|
| 880 |
+
for (const label of [long, short]) {
|
| 881 |
+
const lw = wide(label);
|
| 882 |
+
if (px + 8 + lw < lim) { out += text(px + 8, G.findBase, label, 'finding-lab', 'start'); done = true; break; }
|
| 883 |
+
if (px - 8 - lw > L) { out += text(px - 8, G.findBase, label, 'finding-lab', 'end'); done = true; break; }
|
| 884 |
+
}
|
| 885 |
+
if (!done) {
|
| 886 |
+
const lw = wide(short);
|
| 887 |
+
const cx = Math.max(L + lw / 2 + 1, Math.min(lim - lw / 2 - 1, px));
|
| 888 |
+
out += text(cx, G.findBase, short, 'finding-lab', 'middle');
|
| 889 |
+
}
|
| 890 |
+
}
|
| 891 |
+
return out;
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
/* The anchor is where the sampler looked. Outline only, and labelled, so it can
|
| 895 |
+
never read as an annotated base. */
|
| 896 |
+
function drawAnchor(pxPerBase) {
|
| 897 |
+
if (!doc.anchor || doc.anchor.offset == null) return '';
|
| 898 |
+
const at = doc.anchor.offset;
|
| 899 |
+
if (at < start || at > end) return '';
|
| 900 |
+
const px = x(at);
|
| 901 |
+
const w = Math.max(3, pxPerBase + 3);
|
| 902 |
+
let out = `<rect class="anchor" x="${(px - w / 2).toFixed(2)}" y="${(G.stripY - 3).toFixed(2)}" width="${
|
| 903 |
+
w.toFixed(2)}" height="${(G.stripH + 6).toFixed(2)}" rx="1.5"/>`;
|
| 904 |
+
out += path(`M${px.toFixed(2)} ${G.anchorTop + 11}V${(G.stripY - 4).toFixed(2)}`, 'anchor');
|
| 905 |
+
const label = `ANCHOR · ${String(doc.anchor.kind || '').toUpperCase()}`;
|
| 906 |
+
const lw = measureTracked(label, FONTS.reg, 0.9);
|
| 907 |
+
const left = px + 6 + lw < Math.min(W - R, G.scaleLeft - 8);
|
| 908 |
+
out += text(left ? px + 6 : px - 6, G.anchorBase, label, 'anchor-lab', left ? 'start' : 'end');
|
| 909 |
+
return out;
|
| 910 |
+
}
|
| 911 |
+
|
| 912 |
+
/* ---- coordinate bar ---------------------------------------------------- */
|
| 913 |
+
function paintRegion(pxPerBase) {
|
| 914 |
+
const span = nBasesShown();
|
| 915 |
+
const scale = pxPerBase >= 1
|
| 916 |
+
? `<b>${num(pxPerBase, pxPerBase >= 10 ? 0 : 1)}</b> px per base`
|
| 917 |
+
: `<b>${num(1 / pxPerBase, 1 / pxPerBase >= 10 ? 0 : 1)}</b> bp per column, max`;
|
| 918 |
+
region.innerHTML =
|
| 919 |
+
`<span class="fr-coord">${esc(doc.chrom)}:${fmt(coordMin())}–${fmt(coordMax())}</span>` +
|
| 920 |
+
`<span class="fr-item"><span class="fr-key">asm</span><b>${esc(doc.assembly)}</b></span>` +
|
| 921 |
+
`<span class="fr-item"><span class="fr-key">strand</span><b>${doc.strand < 0 ? 'minus' : 'plus'}</b></span>` +
|
| 922 |
+
`<span class="fr-item"><span class="fr-key">shown</span><b>${fmt(span)}</b> of ${fmt(doc.n_bases)} bp</span>` +
|
| 923 |
+
`<span class="fr-item"><span class="fr-key">scale</span>${scale}</span>`;
|
| 924 |
+
}
|
| 925 |
+
|
| 926 |
+
/* ---- cursor and readout ------------------------------------------------ */
|
| 927 |
+
function focus(base, polite) {
|
| 928 |
+
const g = svg.querySelector('#feat-focus');
|
| 929 |
+
if (!g) return;
|
| 930 |
+
if (base === null || base < start || base > end) { g.innerHTML = ''; return; }
|
| 931 |
+
const px = x(base);
|
| 932 |
+
const cls = pinned === base ? 'pin' : 'cursor';
|
| 933 |
+
let out = path(`M${px.toFixed(2)} ${G.axisY}V${G.gridBottom}`, cls);
|
| 934 |
+
if (pinned === base) {
|
| 935 |
+
out += `<path class="pin-cap" d="M${(px - 3.4).toFixed(2)} ${G.axisY}h6.8l-3.4 5z"/>`;
|
| 936 |
+
}
|
| 937 |
+
g.innerHTML = out;
|
| 938 |
+
describe(base, polite);
|
| 939 |
+
}
|
| 940 |
+
|
| 941 |
+
/* The readout is aria-live. Pointer hover would fire it on every pixel, so
|
| 942 |
+
politeness is switched off while the pointer drives it and switched back on
|
| 943 |
+
for the keyboard and for pinning, which is where a screen-reader user is. */
|
| 944 |
+
function setPoliteness(on) {
|
| 945 |
+
const want = on ? 'polite' : 'off';
|
| 946 |
+
if (readout.getAttribute('aria-live') !== want) readout.setAttribute('aria-live', want);
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
function describe(base, polite) {
|
| 950 |
+
setPoliteness(!!polite);
|
| 951 |
+
const letter = (doc.seq[base] || 'N').toUpperCase();
|
| 952 |
+
const baseTok = `var(--base-${'acgt'.includes(letter.toLowerCase()) ? letter.toLowerCase() : 'n'})`;
|
| 953 |
+
let posRows = `<span class="k">base</span><span class="v">${fmt(base + 1)}<span class="v dim"> / ${
|
| 954 |
+
fmt(doc.n_bases)}</span></span>` +
|
| 955 |
+
`<span class="k">chr${esc(doc.chrom)}</span><span class="v">${fmt(coordOf(base))}</span>` +
|
| 956 |
+
`<span class="k">seq</span><span class="v"><span class="seqbase" style="color:${baseTok}">${
|
| 957 |
+
esc(letter)}</span></span>`;
|
| 958 |
+
if (doc.anchor && doc.anchor.offset === base) {
|
| 959 |
+
posRows += `<span class="k">anchor</span><span class="v dim">sampled here</span>`;
|
| 960 |
+
}
|
| 961 |
+
|
| 962 |
+
let annRows = '';
|
| 963 |
+
for (const strand of strandsCovered()) {
|
| 964 |
+
const reg = regionAt(strand, base), site = siteAt(strand, base);
|
| 965 |
+
const genes = genesAt(strand, base).map(g => g.gene_name || g.gene_id).filter(Boolean);
|
| 966 |
+
let value = reg
|
| 967 |
+
? `<span class="achip"><i style="background:var(--ann-${esc(reg)})"></i>${esc(REGION_LABEL[reg] || reg)}</span>`
|
| 968 |
+
: '<span class="v dim">not annotated</span>';
|
| 969 |
+
if (site) {
|
| 970 |
+
value += ` <span class="achip"><i class="site" style="background:var(--ann-${esc(site)})"></i>${
|
| 971 |
+
esc(SITE_LABEL[site] || site)}</span>`;
|
| 972 |
+
}
|
| 973 |
+
if (genes.length) value += ` <span class="gname">${esc(genes.join(', '))}</span>`;
|
| 974 |
+
annRows += `<span class="k">${esc(strand)}</span><span class="v">${value}</span>`;
|
| 975 |
+
}
|
| 976 |
+
/* The featured locus carries the report's gene model instead of a label
|
| 977 |
+
track, so the same register is derived from its exons and introns. */
|
| 978 |
+
if (!annRows && doc.gene_report) {
|
| 979 |
+
const g = doc.gene_report;
|
| 980 |
+
const c = coordOf(base);
|
| 981 |
+
const inGene = c >= g.start && c <= g.end;
|
| 982 |
+
const inExon = (g.exons || []).some(([a, z]) => c >= a && c <= z);
|
| 983 |
+
const cls = !inGene ? 'intergenic' : inExon ? 'cds' : 'intron';
|
| 984 |
+
const word = !inGene ? 'outside the gene' : inExon ? 'CDS' : 'intron';
|
| 985 |
+
annRows = `<span class="k">gene model</span><span class="v">` +
|
| 986 |
+
`<span class="achip"><i style="background:var(--ann-${cls})"></i>${esc(word)}</span>` +
|
| 987 |
+
(inGene ? ` <span class="gname">${esc(g.gene_id)}</span>` : '') + `</span>`;
|
| 988 |
+
const con = doc.contested;
|
| 989 |
+
if (con) {
|
| 990 |
+
const which = con.araport11_donor === base
|
| 991 |
+
? { cls: 'donor', site: 'donor', who: 'where TAIR10 and Araport11 put it' }
|
| 992 |
+
: con.tair12_donor === base
|
| 993 |
+
? { cls: 'donor', site: 'donor', who: 'where the 1999 BAC and TAIR12 put it' }
|
| 994 |
+
: con.acceptor === base
|
| 995 |
+
? { cls: 'acceptor', site: 'acceptor', who: 'of the same intron' } : null;
|
| 996 |
+
if (which) {
|
| 997 |
+
annRows += `<span class="k">contested</span><span class="v">` +
|
| 998 |
+
`<span class="achip"><i class="site" style="background:var(--ann-${which.cls})"></i>${
|
| 999 |
+
esc(which.site)}</span> <span class="gname">${esc(which.who)}</span></span>`;
|
| 1000 |
+
}
|
| 1001 |
+
}
|
| 1002 |
+
}
|
| 1003 |
+
if (!annRows) {
|
| 1004 |
+
annRows = '<span class="k">register</span><span class="v dim">no annotation track for this window</span>';
|
| 1005 |
+
}
|
| 1006 |
+
|
| 1007 |
+
const hits = shown.map(id => {
|
| 1008 |
+
const t = trackOf(id);
|
| 1009 |
+
return t ? { id, t, v: t.v[base] } : null;
|
| 1010 |
+
}).filter(Boolean);
|
| 1011 |
+
const fired = hits.filter(h => h.v > 0).sort((a, b) => b.v - a.v);
|
| 1012 |
+
const zero = hits.filter(h => h.v === 0);
|
| 1013 |
+
let actBody = fired.length
|
| 1014 |
+
? '<div class="rows">' + fired.map(h =>
|
| 1015 |
+
`<span class="chip" style="background:${featColour(slotOf(h.id))}"></span>` +
|
| 1016 |
+
`<span class="fid">f${h.id}</span>` +
|
| 1017 |
+
`<span class="fnm">${h.t.label ? esc(h.t.label) : 'unnamed'}</span>` +
|
| 1018 |
+
`<span class="fv">${num(h.v)}</span>`).join('') + '</div>'
|
| 1019 |
+
: '<div class="ro-empty">None of the lanes fired at this base.</div>';
|
| 1020 |
+
if (zero.length) {
|
| 1021 |
+
actBody += `<div class="ro-silent">silent here · <code>${
|
| 1022 |
+
zero.map(h => 'f' + h.id).join(' ')}</code></div>`;
|
| 1023 |
+
}
|
| 1024 |
+
const gid = topGlobal(base);
|
| 1025 |
+
actBody += `<div class="ro-foot">${gid >= 0
|
| 1026 |
+
? `strongest of all 8,192 here: <b>f${gid}</b>` + (shown.includes(gid) ? '' : ' (no lane)')
|
| 1027 |
+
: 'no feature in the dictionary fired at this base'}</div>`;
|
| 1028 |
+
|
| 1029 |
+
readout.innerHTML =
|
| 1030 |
+
`<div class="ro-block ro-pos"><p class="ro-lab">Position${
|
| 1031 |
+
pinned === base ? '<span class="pinned">pinned</span>' : ''}</p>` +
|
| 1032 |
+
`<div class="ro-rows">${posRows}</div></div>` +
|
| 1033 |
+
`<div class="ro-block ro-ann"><p class="ro-lab">Annotation</p>` +
|
| 1034 |
+
`<div class="ro-rows">${annRows}</div></div>` +
|
| 1035 |
+
`<div class="ro-block ro-act"><p class="ro-lab">Activation<span>stride 1</span></p>${actBody}</div>`;
|
| 1036 |
+
|
| 1037 |
+
paintPanel(base);
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
/* ---- view control ------------------------------------------------------ */
|
| 1041 |
+
function setView(a, b) {
|
| 1042 |
+
const width = Math.round(Math.max(40, Math.min(doc.n_bases, b - a)));
|
| 1043 |
+
start = Math.max(0, Math.min(doc.n_bases - width, Math.round(a)));
|
| 1044 |
+
end = start + width;
|
| 1045 |
+
render();
|
| 1046 |
+
}
|
| 1047 |
+
/* Panning fires per pointermove; one render per frame is enough. */
|
| 1048 |
+
let viewRaf = 0, pendingView = null;
|
| 1049 |
+
function setViewSoon(a, b) {
|
| 1050 |
+
pendingView = [a, b];
|
| 1051 |
+
if (viewRaf) return;
|
| 1052 |
+
viewRaf = requestAnimationFrame(() => {
|
| 1053 |
+
viewRaf = 0;
|
| 1054 |
+
const [p, q] = pendingView;
|
| 1055 |
+
setView(p, q);
|
| 1056 |
+
});
|
| 1057 |
+
}
|
| 1058 |
+
function zoom(factor) {
|
| 1059 |
+
const centre = (pinned !== null && pinned >= start && pinned <= end) ? pinned : (start + end) / 2;
|
| 1060 |
+
const width = Math.max(40, Math.min(doc.n_bases, nBasesShown() * factor));
|
| 1061 |
+
setView(centre - width / 2, centre + width / 2);
|
| 1062 |
+
}
|
| 1063 |
+
|
| 1064 |
+
/* ---- window picker -----------------------------------------------------
|
| 1065 |
+
The layout is read out of the data, never assumed. When the windows carry a
|
| 1066 |
+
`selection` field the picker groups by species and labels each window by
|
| 1067 |
+
what it shows — its gene and whether the features agree with the annotation
|
| 1068 |
+
or contest it. Without that field it falls back to the species-by-anchor
|
| 1069 |
+
matrix, so the page never breaks while the window set is being swapped. */
|
| 1070 |
+
function pickerPlan() {
|
| 1071 |
+
const featured = index.windows.find(w => w.featured);
|
| 1072 |
+
const rest = index.windows.filter(w => !w.featured);
|
| 1073 |
+
const groups = [];
|
| 1074 |
+
const byKey = new Map();
|
| 1075 |
+
for (const w of rest) {
|
| 1076 |
+
if (!byKey.has(w.species)) {
|
| 1077 |
+
const g = { species: w.species, binomial: w.species_binomial, family: w.family, windows: [], cells: new Map() };
|
| 1078 |
+
byKey.set(w.species, g); groups.push(g);
|
| 1079 |
+
}
|
| 1080 |
+
const g = byKey.get(w.species);
|
| 1081 |
+
g.windows.push(w);
|
| 1082 |
+
g.cells.set((w.anchor && w.anchor.kind) || 'other', w);
|
| 1083 |
+
}
|
| 1084 |
+
const described = rest.some(w => w.selection && w.selection.kind);
|
| 1085 |
+
if (described) return { shape: 'selection', featured, groups };
|
| 1086 |
+
const kinds = [];
|
| 1087 |
+
for (const w of rest) {
|
| 1088 |
+
const k = (w.anchor && w.anchor.kind) || 'other';
|
| 1089 |
+
if (!kinds.includes(k)) kinds.push(k);
|
| 1090 |
+
}
|
| 1091 |
+
const cols = ANCHOR_ORDER.filter(k => kinds.includes(k))
|
| 1092 |
+
.concat(kinds.filter(k => !ANCHOR_ORDER.includes(k)));
|
| 1093 |
+
return { shape: 'anchor', featured, groups, cols };
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
/* what a window is called in the picker: its gene, else its uid */
|
| 1097 |
+
const windowGeneLabel = w => {
|
| 1098 |
+
const genes = asList(w.genes_sense);
|
| 1099 |
+
return genes.length ? shortList(genes) : w.window_uid;
|
| 1100 |
+
};
|
| 1101 |
+
|
| 1102 |
+
function paintPicker() {
|
| 1103 |
+
const plan = pickerPlan();
|
| 1104 |
+
let out = '';
|
| 1105 |
+
if (plan.featured) {
|
| 1106 |
+
const f = plan.featured;
|
| 1107 |
+
out += `<button type="button" class="win-hero" data-uid="${esc(f.window_uid)}">` +
|
| 1108 |
+
`<span class="wh-tag">THE LOCUS</span>` +
|
| 1109 |
+
`<span class="wh-name">${esc(f.window_uid)}</span>` +
|
| 1110 |
+
`<span class="wh-sub">the contested donor · <i>${esc(f.species_binomial)}</i>` +
|
| 1111 |
+
`<span class="wh-asm"> · ${esc(f.assembly)}</span></span></button>`;
|
| 1112 |
+
}
|
| 1113 |
+
|
| 1114 |
+
if (plan.shape === 'selection') {
|
| 1115 |
+
for (const g of plan.groups) {
|
| 1116 |
+
out += `<div class="win-block"><p class="win-sp"><span class="b">${esc(g.binomial)}</span>${
|
| 1117 |
+
g.family ? `<span class="f">${esc(g.family)}</span>` : ''}</p><div class="win-list">`;
|
| 1118 |
+
for (const w of g.windows) {
|
| 1119 |
+
const sel = w.selection || {};
|
| 1120 |
+
const k = selKind(sel);
|
| 1121 |
+
const name = windowGeneLabel(w);
|
| 1122 |
+
const metric = selMetric(sel);
|
| 1123 |
+
const usage = w.usage_check;
|
| 1124 |
+
const uv = usage ? usageVerdict(usage) : null;
|
| 1125 |
+
const aria = [`${name}, ${g.binomial}`, k.tag, sel.headline, sel.why,
|
| 1126 |
+
uv ? `usage: ${uv.tag}, ${uv.gloss}` : '', w.window_uid]
|
| 1127 |
+
.filter(Boolean).join(' — ');
|
| 1128 |
+
out += `<button type="button" class="win-card" data-uid="${esc(w.window_uid)}" data-kind="${
|
| 1129 |
+
esc(sel.kind || 'other')}" data-flag="${k.flag ? '1' : '0'}" aria-label="${
|
| 1130 |
+
esc(aria)}" title="${esc(aria)}">` +
|
| 1131 |
+
`<span class="wk">${esc(k.tag)}</span>` +
|
| 1132 |
+
`<span class="wn">${esc(name)}</span>` +
|
| 1133 |
+
(sel.headline ? `<span class="wh">${esc(sel.headline)}</span>` : '') +
|
| 1134 |
+
(metric ? `<span class="wm">${esc(metric)}</span>` : '') +
|
| 1135 |
+
(uv ? `<span class="wv" data-verdict="${esc(usage.verdict || 'untested')}">${
|
| 1136 |
+
esc(uv.tag)}</span>` : '') +
|
| 1137 |
+
`<span class="wu">${esc(w.window_uid)}</span>` +
|
| 1138 |
+
'</button>';
|
| 1139 |
+
}
|
| 1140 |
+
out += '</div></div>';
|
| 1141 |
+
}
|
| 1142 |
+
} else if (plan.groups.length) {
|
| 1143 |
+
out += `<div class="win-matrix" style="--wm-cols:${plan.cols.length}">`;
|
| 1144 |
+
out += '<span class="wm-corner" aria-hidden="true"></span>';
|
| 1145 |
+
for (const k of plan.cols) out += `<span class="wm-head">${esc(ANCHOR_COL[k] || k)}</span>`;
|
| 1146 |
+
for (const g of plan.groups) {
|
| 1147 |
+
out += `<span class="wm-sp"><span class="b">${esc(g.binomial)}</span>${
|
| 1148 |
+
g.family ? `<span class="f">${esc(g.family)}</span>` : ''}</span>`;
|
| 1149 |
+
for (const k of plan.cols) {
|
| 1150 |
+
const w = g.cells.get(k);
|
| 1151 |
+
if (!w) { out += '<span class="wm-empty" aria-hidden="true"></span>'; continue; }
|
| 1152 |
+
const aria = `${ANCHOR_TITLE[k] || k} window in ${g.binomial}, ${w.window_uid}`;
|
| 1153 |
+
out += `<button type="button" class="wm-cell" data-uid="${esc(w.window_uid)}" aria-label="${
|
| 1154 |
+
esc(aria)}"><span class="wc-anchor">${esc(ANCHOR_COL[k] || k)}</span>` +
|
| 1155 |
+
`<span class="wc-uid">${esc(w.window_uid)}</span></button>`;
|
| 1156 |
+
}
|
| 1157 |
+
}
|
| 1158 |
+
out += '</div>';
|
| 1159 |
+
}
|
| 1160 |
+
picker.innerHTML = out;
|
| 1161 |
+
picker.classList.toggle('win-wide', plan.shape === 'selection');
|
| 1162 |
+
/* one row per species when the viewport can hold it, however many windows a
|
| 1163 |
+
species turns out to have */
|
| 1164 |
+
const widest = plan.groups.reduce((n, g) => Math.max(n, g.windows.length), 1);
|
| 1165 |
+
picker.style.setProperty('--wl-cols', String(Math.min(8, Math.max(1, widest))));
|
| 1166 |
+
|
| 1167 |
+
/* Twenty-one buttons would be twenty-one tab stops. One radiogroup with a
|
| 1168 |
+
roving tabindex is one stop, and a screen reader announces the position. */
|
| 1169 |
+
picker.setAttribute('role', 'radiogroup');
|
| 1170 |
+
const cells = Array.from(picker.querySelectorAll('[data-uid]'));
|
| 1171 |
+
cells.forEach((b, i) => {
|
| 1172 |
+
b.setAttribute('role', 'radio');
|
| 1173 |
+
b.setAttribute('aria-checked', 'false');
|
| 1174 |
+
b.tabIndex = i === 0 ? 0 : -1;
|
| 1175 |
+
b.addEventListener('click', () => choose(b.dataset.uid));
|
| 1176 |
+
b.addEventListener('keydown', ev => {
|
| 1177 |
+
const step = { ArrowRight: 1, ArrowDown: 1, ArrowLeft: -1, ArrowUp: -1 }[ev.key];
|
| 1178 |
+
let next = null;
|
| 1179 |
+
if (step) next = cells[(i + step + cells.length) % cells.length];
|
| 1180 |
+
else if (ev.key === 'Home') next = cells[0];
|
| 1181 |
+
else if (ev.key === 'End') next = cells[cells.length - 1];
|
| 1182 |
+
if (!next) return;
|
| 1183 |
+
ev.preventDefault();
|
| 1184 |
+
next.focus();
|
| 1185 |
+
choose(next.dataset.uid);
|
| 1186 |
+
});
|
| 1187 |
+
});
|
| 1188 |
+
}
|
| 1189 |
+
|
| 1190 |
+
/* ---- feature picker ---------------------------------------------------- */
|
| 1191 |
+
/* Thirty-eight rows, grouped: the fourteen the report names, by what kind of
|
| 1192 |
+
thing they detect, then the rest ranked by how many bases they win here. */
|
| 1193 |
+
function paintList() {
|
| 1194 |
+
const named = doc.features.filter(f => f.curated);
|
| 1195 |
+
const ranked = doc.features.filter(f => !f.curated)
|
| 1196 |
+
.sort((a, b) => b.argmax_bases - a.argmax_bases || b.peak - a.peak);
|
| 1197 |
+
/* a window can store thousands of tracks; the list offers the ones that win
|
| 1198 |
+
the most bases, and the panel finds any of the rest at a base */
|
| 1199 |
+
const rest = ranked.slice(0, LIST_UNNAMED);
|
| 1200 |
+
const full = shown.length >= MAX_LANES;
|
| 1201 |
+
|
| 1202 |
+
const row = f => {
|
| 1203 |
+
const on = shown.includes(f.id);
|
| 1204 |
+
const slot = slotOf(f.id);
|
| 1205 |
+
const silent = !(f.peak > 0);
|
| 1206 |
+
const cov = doc.n_bases ? Math.min(100, Math.round(f.n_fired / doc.n_bases * 100)) : 0;
|
| 1207 |
+
const cls = ['fl-row', f.curated ? 'named' : '', on ? 'on' : '', silent ? 'silent' : ''].join(' ');
|
| 1208 |
+
const detail = silent
|
| 1209 |
+
? '<span class="fpeak zero">silent here</span>'
|
| 1210 |
+
: `<span class="fpeak">${num(f.peak)}</span>`;
|
| 1211 |
+
const covTitle = silent ? 'no base fired in this window'
|
| 1212 |
+
: `${fmt(f.n_fired)} of ${fmt(doc.n_bases)} bases fired (${cov}%)`;
|
| 1213 |
+
/* the checkbox itself is the lane's colour swatch once it is on */
|
| 1214 |
+
const tint = on ? ` style="accent-color:${featColour(slot)}"` : '';
|
| 1215 |
+
const what = f.label
|
| 1216 |
+
? esc(f.label)
|
| 1217 |
+
: `<span class="un">wins ${fmt(f.argmax_bases)} bp of the strip</span>`;
|
| 1218 |
+
return `<label class="${cls}" title="${esc(covTitle)}">` +
|
| 1219 |
+
`<input type="checkbox" data-id="${f.id}"${on ? ' checked' : ''}${!on && full ? ' disabled' : ''}${tint}>` +
|
| 1220 |
+
`<span class="fid">f${f.id}</span>` +
|
| 1221 |
+
`<span class="fname">${what}</span>` +
|
| 1222 |
+
detail +
|
| 1223 |
+
`<span class="fcov"><i style="width:${cov}%"></i></span>` +
|
| 1224 |
+
`</label>`;
|
| 1225 |
+
};
|
| 1226 |
+
|
| 1227 |
+
let namedRows = '';
|
| 1228 |
+
const seen = new Set();
|
| 1229 |
+
for (const kind of KIND_ORDER.concat(['other'])) {
|
| 1230 |
+
const bucket = named.filter(f => (KIND_ORDER.includes(f.kind) ? f.kind : 'other') === kind);
|
| 1231 |
+
if (!bucket.length) continue;
|
| 1232 |
+
bucket.sort((a, b) => b.peak - a.peak);
|
| 1233 |
+
namedRows += `<p class="fl-sub">${esc(KIND_GROUP[kind] || 'other')}</p>`;
|
| 1234 |
+
namedRows += bucket.map(f => { seen.add(f.id); return row(f); }).join('');
|
| 1235 |
+
}
|
| 1236 |
+
|
| 1237 |
+
/* the fold state is the reader's, not something a repaint gets to reset */
|
| 1238 |
+
if (restOpen === null) restOpen = false;
|
| 1239 |
+
const open = restOpen;
|
| 1240 |
+
list.innerHTML =
|
| 1241 |
+
`<div class="fl-bar"><span class="fl-count"><b>${shown.length}</b> of ${MAX_LANES} lanes${
|
| 1242 |
+
full ? ' <span class="full">· full</span>' : ''}</span>` +
|
| 1243 |
+
`<button type="button" class="fl-reset">Reset to the report's six</button></div>` +
|
| 1244 |
+
(namedRows
|
| 1245 |
+
? `<div class="fl-group"><p class="fl-h">Named in the report <span class="n">${named.length}</span>` +
|
| 1246 |
+
`<span class="why">the report measures what each one detects</span></p>` +
|
| 1247 |
+
`<div class="fl-rows">${namedRows}</div></div>`
|
| 1248 |
+
: '') +
|
| 1249 |
+
(rest.length
|
| 1250 |
+
? `<details class="fl-group"${open ? ' open' : ''}><summary>Unnamed <span class="n">${
|
| 1251 |
+
rest.length}${ranked.length > rest.length ? ' of ' + fmt(ranked.length) : ''}</span>` +
|
| 1252 |
+
`<span class="why">ranked by bases won in this window</span></summary>` +
|
| 1253 |
+
`<div class="fl-rows">${rest.map(row).join('')}</div></details>`
|
| 1254 |
+
: '') +
|
| 1255 |
+
`<p class="fl-note">Numbers are this window's peak activation for the feature; the bar is the share of
|
| 1256 |
+
the window's bases where it fired at all. Up to ${fmt(MAX_LANES)} lanes at a time${
|
| 1257 |
+
ranked.length > rest.length
|
| 1258 |
+
? `. This window stores tracks for ${fmt(doc.features.length)} features in all — <em>Top features</em> finds any of them at a base`
|
| 1259 |
+
: ''}.</p>`;
|
| 1260 |
+
|
| 1261 |
+
const details = list.querySelector('details.fl-group');
|
| 1262 |
+
if (details) details.addEventListener('toggle', () => { restOpen = details.open; });
|
| 1263 |
+
|
| 1264 |
+
list.querySelectorAll('input').forEach(box => {
|
| 1265 |
+
box.addEventListener('change', () => {
|
| 1266 |
+
const id = Number(box.dataset.id);
|
| 1267 |
+
if (box.checked) {
|
| 1268 |
+
if (shown.length < MAX_LANES) shown.push(id); else box.checked = false;
|
| 1269 |
+
} else shown = shown.filter(f => f !== id);
|
| 1270 |
+
/* the list is repainted, so hand keyboard focus back to the same row */
|
| 1271 |
+
const refocus = document.activeElement === box;
|
| 1272 |
+
paintList(); render();
|
| 1273 |
+
if (refocus) {
|
| 1274 |
+
const again = list.querySelector(`input[data-id="${id}"]`);
|
| 1275 |
+
if (again) again.focus({ preventScroll: true });
|
| 1276 |
+
}
|
| 1277 |
+
});
|
| 1278 |
+
});
|
| 1279 |
+
const reset = list.querySelector('.fl-reset');
|
| 1280 |
+
if (reset) reset.addEventListener('click', () => {
|
| 1281 |
+
shown = asList(doc.default_features).slice(0, MAX_LANES).filter(id => trackOf(id));
|
| 1282 |
+
paintList(); render();
|
| 1283 |
+
});
|
| 1284 |
+
}
|
| 1285 |
+
|
| 1286 |
+
/* ---- titles ------------------------------------------------------------ */
|
| 1287 |
+
/* Windows are named by the gene they cover and what they show. The sampler
|
| 1288 |
+
anchor is still drawn in the plot, but it is not what a window is called. */
|
| 1289 |
+
const shortList = names => names.slice(0, 2).join(', ') + (names.length > 2 ? '…' : '');
|
| 1290 |
+
const docGeneLabel = () => {
|
| 1291 |
+
const sense = doc.annotations && doc.annotations.sense;
|
| 1292 |
+
const own = asList(sense && sense.genes).map(g => g.gene_name || g.gene_id).filter(Boolean);
|
| 1293 |
+
if (own.length) return shortList(own);
|
| 1294 |
+
const meta = index && index.windows.find(w => w.window_uid === doc.window_uid);
|
| 1295 |
+
const listed = asList(meta && meta.genes_sense);
|
| 1296 |
+
if (listed.length) return shortList(listed);
|
| 1297 |
+
if (doc.gene_report && doc.gene_report.gene_id) return doc.gene_report.gene_id;
|
| 1298 |
+
return doc.window_uid;
|
| 1299 |
+
};
|
| 1300 |
+
const windowTitleText = () => doc.featured && doc.gene_report
|
| 1301 |
+
? `${doc.gene_report.gene_id}, the contested donor`
|
| 1302 |
+
: `${docGeneLabel()}${doc.selection ? ' — ' + selKind(doc.selection).tag.toLowerCase() : ''}`;
|
| 1303 |
+
const ariaLabel = () =>
|
| 1304 |
+
`${windowTitleText()}. Genome browser: a per-base top-feature strip, an annotation register and ${
|
| 1305 |
+
shown.length} activation lane${shown.length === 1 ? '' : 's'} over ${fmt(nBasesShown())} of ${
|
| 1306 |
+
fmt(doc.n_bases)} base pairs.`;
|
| 1307 |
+
|
| 1308 |
+
function paintHeading() {
|
| 1309 |
+
if (doc.featured && doc.gene_report) {
|
| 1310 |
+
title.innerHTML = `${esc(doc.gene_report.gene_id)}<span class="ft-dim">, the contested donor</span>`;
|
| 1311 |
+
} else {
|
| 1312 |
+
const k = doc.selection ? selKind(doc.selection) : null;
|
| 1313 |
+
title.innerHTML = esc(docGeneLabel()) + (k
|
| 1314 |
+
? ` <span class="ft-kind" data-flag="${k.flag ? '1' : '0'}">${esc(k.tag)}</span>` : '');
|
| 1315 |
+
}
|
| 1316 |
+
const bits = [`<span class="uid">${esc(doc.window_uid)}</span>`,
|
| 1317 |
+
`<span class="species">${esc(doc.species_binomial)}</span>`];
|
| 1318 |
+
if (doc.family) bits.push(esc(doc.family));
|
| 1319 |
+
bits.push(esc(doc.assembly));
|
| 1320 |
+
bits.push(`${fmt(doc.n_bases)} bp`);
|
| 1321 |
+
sub.innerHTML = bits.join('<span class="sep">/</span>') +
|
| 1322 |
+
(doc.split ? `<span class="split">${esc(doc.split)}</span>` : '');
|
| 1323 |
+
paintCase();
|
| 1324 |
+
}
|
| 1325 |
+
|
| 1326 |
+
/* What this window shows, as one island: the reason it was chosen, and — for the
|
| 1327 |
+
windows where a feature marks a site the annotation does not carry — the
|
| 1328 |
+
verdict the independent registers returned. A window with nothing to check
|
| 1329 |
+
shows only the reason, never an unanswered question. */
|
| 1330 |
+
function paintCase() {
|
| 1331 |
+
if (!caseEl) return;
|
| 1332 |
+
const sel = doc.selection;
|
| 1333 |
+
const u = doc.usage_check;
|
| 1334 |
+
if (!sel && !doc.note) { caseEl.hidden = true; caseEl.innerHTML = ''; return; }
|
| 1335 |
+
|
| 1336 |
+
const k = sel ? selKind(sel) : null;
|
| 1337 |
+
/* doc.note repeats the headline as its own first clause, so showing both
|
| 1338 |
+
would stutter; the headline is the chip and the rest is the description. */
|
| 1339 |
+
const why = (sel && sel.why) || doc.note || '';
|
| 1340 |
+
let html = '<p class="fc-head">' +
|
| 1341 |
+
(k ? `<span class="fc-kind" data-flag="${k.flag ? '1' : '0'}">${esc(k.tag)}</span>` : '') +
|
| 1342 |
+
(sel && sel.headline ? `<span class="fc-headline">${esc(sel.headline)}</span>` : '') +
|
| 1343 |
+
'</p>' +
|
| 1344 |
+
(why ? `<p class="fc-why">${esc(why)}</p>` : '');
|
| 1345 |
+
|
| 1346 |
+
if (u) {
|
| 1347 |
+
const v = usageVerdict(u);
|
| 1348 |
+
const rows = asList(u.registers).map(r => {
|
| 1349 |
+
const result = USAGE_RESULT[r.result] || String(r.result || '');
|
| 1350 |
+
return '<li>' +
|
| 1351 |
+
`<span class="ur-name">${esc(r.name)}</span>` +
|
| 1352 |
+
`<span class="ur-role">${esc(r.role)}</span>` +
|
| 1353 |
+
`<span class="ur-result" data-result="${esc(r.result || '')}">${esc(result)}</span>` +
|
| 1354 |
+
`<span class="ur-detail">${esc(r.detail)}</span>` +
|
| 1355 |
+
'</li>';
|
| 1356 |
+
}).join('');
|
| 1357 |
+
html += '<div class="usage-check">' +
|
| 1358 |
+
'<p class="uc-head">' +
|
| 1359 |
+
`<span class="uc-verdict" data-verdict="${esc(u.verdict || 'untested')}">${esc(v.tag)}</span>` +
|
| 1360 |
+
`<span class="uc-headline">${esc(u.headline || v.gloss)}</span>` +
|
| 1361 |
+
'</p>' +
|
| 1362 |
+
(u.summary ? `<p class="uc-summary">${esc(u.summary)}</p>` : '') +
|
| 1363 |
+
(rows ? `<ul class="uc-registers">${rows}</ul>` : '') +
|
| 1364 |
+
'<p class="uc-foot">Usage is asked of registers independent of the annotation the ' +
|
| 1365 |
+
'tracks were scored against, and never of the model. ' +
|
| 1366 |
+
'<a href="#sources">What each register is</a>.</p>' +
|
| 1367 |
+
'</div>';
|
| 1368 |
+
}
|
| 1369 |
+
|
| 1370 |
+
caseEl.innerHTML = html;
|
| 1371 |
+
caseEl.hidden = false;
|
| 1372 |
+
}
|
| 1373 |
+
|
| 1374 |
+
/* ---- window loading ---------------------------------------------------- */
|
| 1375 |
+
let gen = 0;
|
| 1376 |
+
async function load(uid) {
|
| 1377 |
+
const mine = ++gen;
|
| 1378 |
+
if (!cache.has(uid)) {
|
| 1379 |
+
host.setAttribute('aria-busy', 'true');
|
| 1380 |
+
region.textContent = `Loading ${uid}…`;
|
| 1381 |
+
let payload;
|
| 1382 |
+
try {
|
| 1383 |
+
const r = await fetch(`data/sae/${uid}.json`);
|
| 1384 |
+
if (!r.ok) throw new Error(`window ${uid} unavailable`);
|
| 1385 |
+
payload = await r.json();
|
| 1386 |
+
} finally {
|
| 1387 |
+
if (mine === gen) host.removeAttribute('aria-busy');
|
| 1388 |
+
}
|
| 1389 |
+
/* a slower earlier click must not overwrite a later one */
|
| 1390 |
+
if (mine !== gen) return;
|
| 1391 |
+
cache.set(uid, payload);
|
| 1392 |
+
}
|
| 1393 |
+
if (mine !== gen) return;
|
| 1394 |
+
doc = cache.get(uid);
|
| 1395 |
+
vectors = new Map();
|
| 1396 |
+
tracks = new Map();
|
| 1397 |
+
runIdx.clear();
|
| 1398 |
+
featureIndex = new Map(asList(doc.features).map(f => [f.id, f]));
|
| 1399 |
+
buildArgmaxLut();
|
| 1400 |
+
buildLaneMarks();
|
| 1401 |
+
shown = asList(doc.default_features).slice(0, MAX_LANES).filter(id => trackOf(id));
|
| 1402 |
+
start = 0; end = doc.n_bases;
|
| 1403 |
+
/* the cursor opens on the base the window was chosen for, so the readout
|
| 1404 |
+
already describes the finding without pinning it and blocking hover */
|
| 1405 |
+
cursor = doc.selection && doc.selection.position != null ? doc.selection.position
|
| 1406 |
+
: (doc.anchor && doc.anchor.offset != null ? doc.anchor.offset : Math.floor(doc.n_bases / 2));
|
| 1407 |
+
pinned = null;
|
| 1408 |
+
paintHeading();
|
| 1409 |
+
picker.querySelectorAll('[data-uid]').forEach(b => {
|
| 1410 |
+
const on = b.dataset.uid === uid;
|
| 1411 |
+
b.setAttribute('aria-checked', String(on));
|
| 1412 |
+
b.tabIndex = on ? 0 : -1;
|
| 1413 |
+
});
|
| 1414 |
+
if (findBtn) {
|
| 1415 |
+
const marks = findingMarks(doc.selection);
|
| 1416 |
+
findBtn.hidden = !marks.length;
|
| 1417 |
+
findBtn.textContent = marks.length > 1 ? 'Show the shift' : 'Zoom to change locus';
|
| 1418 |
+
findBtn.title = marks.length
|
| 1419 |
+
? `Frame ${marks.length > 1 ? 'both positions' : 'base ' + fmt(marks[0].base + 1)}, the reason this window is here`
|
| 1420 |
+
: '';
|
| 1421 |
+
}
|
| 1422 |
+
paintList();
|
| 1423 |
+
render();
|
| 1424 |
+
tools.forEach(b => { if (b) b.disabled = false; });
|
| 1425 |
+
}
|
| 1426 |
+
|
| 1427 |
+
/* ---- top-features panel ------------------------------------------------
|
| 1428 |
+
Orders the features firing at the cursor by reading the window's stored
|
| 1429 |
+
tracks at that base. Clicking a row gives the feature a lane. */
|
| 1430 |
+
let panel = null, panelDock = null, panelBase = null, findBtn = null;
|
| 1431 |
+
let controls = null, controlsDock = null;
|
| 1432 |
+
const PANEL_ROWS = 8;
|
| 1433 |
+
/* The window stores a full track for every feature that reaches a base's top
|
| 1434 |
+
`panel_depth` anywhere in it, so the strongest `panel_depth` at any base are
|
| 1435 |
+
all present and scanning the stored tracks gives the true ordering there.
|
| 1436 |
+
The data states the rule; asking for more rows than it guarantees is the one
|
| 1437 |
+
case where the panel has to qualify what it shows. */
|
| 1438 |
+
const storedDepth = () => {
|
| 1439 |
+
const c = doc && doc.coverage;
|
| 1440 |
+
const d = c && Number(c.panel_depth);
|
| 1441 |
+
return Number.isFinite(d) && d > 0 ? d : 0;
|
| 1442 |
+
};
|
| 1443 |
+
function topAt(base) {
|
| 1444 |
+
const rows = [];
|
| 1445 |
+
for (const f of doc.features) {
|
| 1446 |
+
const v = valueAt(f, base);
|
| 1447 |
+
if (v > 0) rows.push({ id: f.id, v, f });
|
| 1448 |
+
}
|
| 1449 |
+
rows.sort((a, b) => b.v - a.v);
|
| 1450 |
+
/* The claim holds when the panel asks for no more rows than the store
|
| 1451 |
+
guarantees; otherwise it says only what it can see. */
|
| 1452 |
+
const depth = storedDepth();
|
| 1453 |
+
return { rows: rows.slice(0, PANEL_ROWS), nz: rows.length,
|
| 1454 |
+
whole: depth > 0 && PANEL_ROWS <= depth };
|
| 1455 |
+
}
|
| 1456 |
+
|
| 1457 |
+
/* ---- floating docks ----------------------------------------------------
|
| 1458 |
+
Both movable surfaces — the top-features panel and the lanes dock — are
|
| 1459 |
+
positioned inside .feat-stage rather than the viewport, so they scroll away
|
| 1460 |
+
with the figure instead of following the reader down the page. A drag is
|
| 1461 |
+
clamped to the stage, and a remembered position that no longer falls inside
|
| 1462 |
+
it is discarded rather than restoring a panel where it cannot be reached. */
|
| 1463 |
+
const DOCK_KEY = 'botanic1.sae.docks';
|
| 1464 |
+
const docks = [];
|
| 1465 |
+
const stageEl = () => host.closest('.feat-stage') || host.closest('figure') || host.parentElement;
|
| 1466 |
+
|
| 1467 |
+
function readDocks() {
|
| 1468 |
+
try {
|
| 1469 |
+
const raw = sessionStorage.getItem(DOCK_KEY);
|
| 1470 |
+
const parsed = raw ? JSON.parse(raw) : null;
|
| 1471 |
+
return (parsed && typeof parsed === 'object') ? parsed : {};
|
| 1472 |
+
} catch (error) { return {}; }
|
| 1473 |
+
}
|
| 1474 |
+
function writeDocks(next) {
|
| 1475 |
+
try { sessionStorage.setItem(DOCK_KEY, JSON.stringify(next)); } catch (error) { /* private mode */ }
|
| 1476 |
+
}
|
| 1477 |
+
function saveDock(d) {
|
| 1478 |
+
const all = readDocks();
|
| 1479 |
+
const was = all[d.name] || {};
|
| 1480 |
+
const entry = { open: !d.el.hidden, left: was.left, top: was.top };
|
| 1481 |
+
if (d.floating() && !d.el.hidden) {
|
| 1482 |
+
const box = d.el.getBoundingClientRect(), stage = stageEl().getBoundingClientRect();
|
| 1483 |
+
entry.left = Math.round(box.left - stage.left);
|
| 1484 |
+
entry.top = Math.round(box.top - stage.top);
|
| 1485 |
+
}
|
| 1486 |
+
all[d.name] = entry;
|
| 1487 |
+
writeDocks(all);
|
| 1488 |
+
}
|
| 1489 |
+
|
| 1490 |
+
function makeDock(spec) {
|
| 1491 |
+
const el = spec.el;
|
| 1492 |
+
const d = {
|
| 1493 |
+
el, name: spec.name, toggle: spec.toggle, rest: spec.rest,
|
| 1494 |
+
floating: () => getComputedStyle(el).position === 'absolute'
|
| 1495 |
+
};
|
| 1496 |
+
/* place, in stage coordinates, clamped so the whole surface stays reachable */
|
| 1497 |
+
d.place = (left, top) => {
|
| 1498 |
+
if (!d.floating()) { el.style.maxHeight = ''; return; }
|
| 1499 |
+
const stage = stageEl().getBoundingClientRect();
|
| 1500 |
+
/* let it measure against the whole stage, clamp, then cap to what is left
|
| 1501 |
+
below wherever it landed, so its body scrolls instead of hanging out */
|
| 1502 |
+
el.style.maxHeight = `${Math.max(160, Math.round(stage.height - 20))}px`;
|
| 1503 |
+
const w = el.offsetWidth || 300, h = el.offsetHeight || 200;
|
| 1504 |
+
const x = Math.max(0, Math.min(Math.max(0, stage.width - w), left));
|
| 1505 |
+
const y = Math.max(0, Math.min(Math.max(0, stage.height - h), top));
|
| 1506 |
+
el.style.left = `${Math.round(x)}px`;
|
| 1507 |
+
el.style.top = `${Math.round(y)}px`;
|
| 1508 |
+
el.style.right = 'auto';
|
| 1509 |
+
el.style.bottom = 'auto';
|
| 1510 |
+
el.style.maxHeight = `${Math.max(160, Math.round(stage.height - y - 12))}px`;
|
| 1511 |
+
};
|
| 1512 |
+
d.inStage = (left, top) => {
|
| 1513 |
+
const stage = stageEl().getBoundingClientRect();
|
| 1514 |
+
const w = el.offsetWidth || 300, h = el.offsetHeight || 200;
|
| 1515 |
+
return left >= -2 && top >= -2 && left <= stage.width - w + 2 && top <= stage.height - h + 2;
|
| 1516 |
+
};
|
| 1517 |
+
d.setOpen = open => {
|
| 1518 |
+
el.hidden = !open;
|
| 1519 |
+
if (d.toggle) d.toggle.setAttribute('aria-expanded', String(open));
|
| 1520 |
+
if (open) {
|
| 1521 |
+
const saved = readDocks()[d.name] || {};
|
| 1522 |
+
requestAnimationFrame(() => {
|
| 1523 |
+
if (saved.left != null && saved.top != null && d.inStage(saved.left, saved.top)) {
|
| 1524 |
+
d.place(saved.left, saved.top);
|
| 1525 |
+
} else d.settle();
|
| 1526 |
+
});
|
| 1527 |
+
}
|
| 1528 |
+
saveDock(d);
|
| 1529 |
+
};
|
| 1530 |
+
d.settle = () => {
|
| 1531 |
+
if (!d.floating() || el.hidden) return;
|
| 1532 |
+
const at = d.rest(d);
|
| 1533 |
+
if (at) d.place(at.left, at.top);
|
| 1534 |
+
};
|
| 1535 |
+
|
| 1536 |
+
const handle = el.querySelector(spec.handle);
|
| 1537 |
+
if (handle) {
|
| 1538 |
+
let grab = null;
|
| 1539 |
+
handle.addEventListener('pointerdown', ev => {
|
| 1540 |
+
if (ev.target.closest('button')) return;
|
| 1541 |
+
if (!d.floating()) return;
|
| 1542 |
+
const box = el.getBoundingClientRect(), stage = stageEl().getBoundingClientRect();
|
| 1543 |
+
grab = { dx: ev.clientX - box.left, dy: ev.clientY - box.top, sx: stage.left, sy: stage.top };
|
| 1544 |
+
handle.classList.add('dragging');
|
| 1545 |
+
handle.setPointerCapture(ev.pointerId);
|
| 1546 |
+
});
|
| 1547 |
+
handle.addEventListener('pointermove', ev => {
|
| 1548 |
+
if (!grab) return;
|
| 1549 |
+
d.place(ev.clientX - grab.dx - grab.sx, ev.clientY - grab.dy - grab.sy);
|
| 1550 |
+
});
|
| 1551 |
+
const drop = ev => {
|
| 1552 |
+
if (!grab) return;
|
| 1553 |
+
grab = null;
|
| 1554 |
+
handle.classList.remove('dragging');
|
| 1555 |
+
if (handle.hasPointerCapture(ev.pointerId)) handle.releasePointerCapture(ev.pointerId);
|
| 1556 |
+
saveDock(d);
|
| 1557 |
+
};
|
| 1558 |
+
handle.addEventListener('pointerup', drop);
|
| 1559 |
+
handle.addEventListener('pointercancel', drop);
|
| 1560 |
+
}
|
| 1561 |
+
docks.push(d);
|
| 1562 |
+
return d;
|
| 1563 |
+
}
|
| 1564 |
+
|
| 1565 |
+
/* When there is room the stage grows a right gutter and the lanes dock rests
|
| 1566 |
+
in it, beside the canvas rather than on top of it. Narrower than that it is
|
| 1567 |
+
an overlay the reader can drag, and narrower still it drops into the flow. */
|
| 1568 |
+
const GUTTER_AT = 1081;
|
| 1569 |
+
function syncGutter() {
|
| 1570 |
+
const stage = stageEl();
|
| 1571 |
+
const want = !!(controls && controlsDock && !controls.hidden
|
| 1572 |
+
&& controlsDock.floating() && window.innerWidth >= GUTTER_AT);
|
| 1573 |
+
if (stage.classList.contains('stage-docked') !== want) {
|
| 1574 |
+
stage.classList.toggle('stage-docked', want);
|
| 1575 |
+
return true;
|
| 1576 |
+
}
|
| 1577 |
+
return false;
|
| 1578 |
+
}
|
| 1579 |
+
|
| 1580 |
+
/* a resting place inside the stage that keeps clear of the other open dock */
|
| 1581 |
+
function restIn(d, corner) {
|
| 1582 |
+
const stage = stageEl().getBoundingClientRect();
|
| 1583 |
+
const w = d.el.offsetWidth || 300, h = d.el.offsetHeight || 220;
|
| 1584 |
+
const pad = 14;
|
| 1585 |
+
const top = stage.height - h - pad;
|
| 1586 |
+
let y = corner === 'top' ? 92 : Math.max(92, top);
|
| 1587 |
+
const other = docks.find(o => o !== d && o.floating() && !o.el.hidden);
|
| 1588 |
+
let x = Math.max(pad, stage.width - w - pad);
|
| 1589 |
+
if (other) {
|
| 1590 |
+
const ob = other.el.getBoundingClientRect();
|
| 1591 |
+
const oy = ob.top - stage.top, oh = ob.height;
|
| 1592 |
+
const overlaps = y < oy + oh && y + h > oy;
|
| 1593 |
+
if (overlaps) {
|
| 1594 |
+
if (oy + oh + pad + h <= stage.height) y = oy + oh + pad;
|
| 1595 |
+
else if (oy - pad - h >= 0) y = oy - pad - h;
|
| 1596 |
+
else x = Math.max(pad, ob.left - stage.left - w - pad);
|
| 1597 |
+
}
|
| 1598 |
+
}
|
| 1599 |
+
return { left: x, top: y };
|
| 1600 |
+
}
|
| 1601 |
+
|
| 1602 |
+
/* the top-features panel: the page ships the shell, the fallback keeps it
|
| 1603 |
+
working if it ever does not */
|
| 1604 |
+
function ensurePanel() {
|
| 1605 |
+
if (panel) return;
|
| 1606 |
+
const stage = stageEl();
|
| 1607 |
+
panel = document.getElementById('feat-panel');
|
| 1608 |
+
if (!panel) { panel = document.createElement('aside'); panel.id = 'feat-panel'; }
|
| 1609 |
+
panel.className = 'fp feat-float';
|
| 1610 |
+
panel.setAttribute('role', 'group');
|
| 1611 |
+
panel.setAttribute('aria-label', 'Top features at the inspected base');
|
| 1612 |
+
panel.hidden = true;
|
| 1613 |
+
panel.innerHTML =
|
| 1614 |
+
'<div class="fp-head"><h3 id="fp-title">Top features here</h3>' +
|
| 1615 |
+
'<span class="fp-at"></span>' +
|
| 1616 |
+
'<button type="button" class="fp-close" aria-label="Close the top-features panel">×</button></div>' +
|
| 1617 |
+
'<p class="fp-src"></p><ol class="fp-rows"></ol><p class="fp-foot"></p>';
|
| 1618 |
+
/* absolute positioning has to resolve against the stage, and the figure
|
| 1619 |
+
clips its own overflow, so the panel lives as the stage's last child;
|
| 1620 |
+
in static flow that also puts it after the lanes dock */
|
| 1621 |
+
stage.appendChild(panel);
|
| 1622 |
+
|
| 1623 |
+
let toggle = document.getElementById('feat-panel-toggle');
|
| 1624 |
+
if (!toggle) {
|
| 1625 |
+
toggle = document.createElement('button');
|
| 1626 |
+
toggle.type = 'button';
|
| 1627 |
+
toggle.id = 'feat-panel-toggle';
|
| 1628 |
+
toggle.setAttribute('aria-controls', 'feat-panel');
|
| 1629 |
+
toggle.textContent = 'Top features';
|
| 1630 |
+
(document.querySelector('.feat-tools') || stage).appendChild(toggle);
|
| 1631 |
+
}
|
| 1632 |
+
toggle.disabled = false;
|
| 1633 |
+
panelDock = makeDock({
|
| 1634 |
+
el: panel, name: 'panel', toggle, handle: '.fp-head',
|
| 1635 |
+
rest: d => restIn(d, 'bottom')
|
| 1636 |
+
});
|
| 1637 |
+
toggle.addEventListener('click', () => {
|
| 1638 |
+
panelDock.setOpen(panel.hidden);
|
| 1639 |
+
if (!panel.hidden) paintPanel(panelBase === null ? cursor : panelBase);
|
| 1640 |
+
});
|
| 1641 |
+
panel.querySelector('.fp-close').addEventListener('click', () => {
|
| 1642 |
+
panelDock.setOpen(false);
|
| 1643 |
+
toggle.focus();
|
| 1644 |
+
});
|
| 1645 |
+
panel.addEventListener('keydown', ev => {
|
| 1646 |
+
if (ev.key === 'Escape') { panelDock.setOpen(false); toggle.focus(); }
|
| 1647 |
+
});
|
| 1648 |
+
const saved = readDocks().panel || {};
|
| 1649 |
+
if (saved.open) { panelDock.setOpen(true); paintPanel(cursor); }
|
| 1650 |
+
}
|
| 1651 |
+
|
| 1652 |
+
/* the lanes dock: colouring mode and the feature checklist, beside the canvas
|
| 1653 |
+
instead of a screen away from it */
|
| 1654 |
+
function ensureControls() {
|
| 1655 |
+
if (controls) return;
|
| 1656 |
+
controls = document.getElementById('feat-controls');
|
| 1657 |
+
if (!controls) return;
|
| 1658 |
+
controls.classList.add('feat-float');
|
| 1659 |
+
let close = controls.querySelector('.dock-close');
|
| 1660 |
+
const head = controls.querySelector('.dock-title');
|
| 1661 |
+
if (head && !close) {
|
| 1662 |
+
close = document.createElement('button');
|
| 1663 |
+
close.type = 'button';
|
| 1664 |
+
close.className = 'dock-close';
|
| 1665 |
+
close.setAttribute('aria-label', 'Close the lanes panel');
|
| 1666 |
+
close.innerHTML = '×';
|
| 1667 |
+
head.appendChild(close);
|
| 1668 |
+
}
|
| 1669 |
+
const toggle = document.getElementById('feat-controls-toggle');
|
| 1670 |
+
if (toggle) toggle.disabled = false;
|
| 1671 |
+
controlsDock = makeDock({
|
| 1672 |
+
el: controls, name: 'controls', toggle, handle: '.dock-title',
|
| 1673 |
+
rest: d => {
|
| 1674 |
+
if (stageEl().classList.contains('stage-docked')) {
|
| 1675 |
+
const stage = stageEl().getBoundingClientRect();
|
| 1676 |
+
return { left: stage.width - (d.el.offsetWidth || 336) - 7, top: 0 };
|
| 1677 |
+
}
|
| 1678 |
+
return restIn(d, 'top');
|
| 1679 |
+
}
|
| 1680 |
+
});
|
| 1681 |
+
const wrapped = controlsDock.setOpen;
|
| 1682 |
+
controlsDock.setOpen = open => {
|
| 1683 |
+
controls.hidden = !open;
|
| 1684 |
+
syncGutter();
|
| 1685 |
+
wrapped(open);
|
| 1686 |
+
};
|
| 1687 |
+
if (toggle) toggle.addEventListener('click', () => controlsDock.setOpen(controls.hidden));
|
| 1688 |
+
if (close) close.addEventListener('click', () => {
|
| 1689 |
+
controlsDock.setOpen(false);
|
| 1690 |
+
if (toggle) toggle.focus();
|
| 1691 |
+
});
|
| 1692 |
+
const saved = readDocks().controls || {};
|
| 1693 |
+
controlsDock.setOpen(saved.open !== false);
|
| 1694 |
+
}
|
| 1695 |
+
|
| 1696 |
+
function paintPanel(base) {
|
| 1697 |
+
panelBase = base;
|
| 1698 |
+
if (!panel || panel.hidden || base == null || !doc) return;
|
| 1699 |
+
const { rows, nz, whole } = topAt(base);
|
| 1700 |
+
panel.querySelector('.fp-at').textContent = `base ${fmt(base + 1)} · ${esc(doc.seq[base] || '')}`;
|
| 1701 |
+
panel.querySelector('.fp-src').innerHTML = whole
|
| 1702 |
+
? `The <b>${fmt(rows.length)}</b> strongest features at this base, out of all <b>8,192</b>.` +
|
| 1703 |
+
` This window carries a complete track for every feature that reaches a base's top ${
|
| 1704 |
+
fmt(storedDepth())} anywhere in it, so these are the true strongest here;` +
|
| 1705 |
+
` <b>${fmt(nz)}</b> of the ${fmt(doc.features.length)} it stores fire at this base.`
|
| 1706 |
+
: `The <b>${fmt(rows.length)}</b> strongest at this base among the <b>${
|
| 1707 |
+
fmt(doc.features.length)}</b> features this window stores.`;
|
| 1708 |
+
|
| 1709 |
+
const list = panel.querySelector('.fp-rows');
|
| 1710 |
+
if (!rows.length) {
|
| 1711 |
+
list.innerHTML = '<li><p class="fp-empty">No feature fired at this base.</p></li>';
|
| 1712 |
+
} else {
|
| 1713 |
+
list.innerHTML = rows.map(r => {
|
| 1714 |
+
const on = shown.includes(r.id);
|
| 1715 |
+
const colour = on ? featColour(slotOf(r.id)) : 'var(--rule-2)';
|
| 1716 |
+
const name = r.f.label ? esc(r.f.label) : '<span class="un">unnamed</span>';
|
| 1717 |
+
const tip = on ? 'Remove this lane' : 'Give this feature a lane';
|
| 1718 |
+
return `<li><button type="button" class="fp-row" data-id="${r.id}" aria-pressed="${
|
| 1719 |
+
on}" title="${esc(tip)}">` +
|
| 1720 |
+
`<span class="chip" style="background:${colour}"></span>` +
|
| 1721 |
+
`<span class="fid">f${r.id}</span><span class="fnm">${name}</span>` +
|
| 1722 |
+
`<span class="fv">${num(r.v)}</span></button></li>`;
|
| 1723 |
+
}).join('');
|
| 1724 |
+
list.querySelectorAll('.fp-row').forEach(btn => btn.addEventListener('click', () => {
|
| 1725 |
+
const id = Number(btn.dataset.id);
|
| 1726 |
+
if (shown.includes(id)) shown = shown.filter(f => f !== id);
|
| 1727 |
+
else if (shown.length < MAX_LANES) shown.push(id);
|
| 1728 |
+
else return;
|
| 1729 |
+
paintList(); render();
|
| 1730 |
+
}));
|
| 1731 |
+
}
|
| 1732 |
+
const gid = topGlobal(base);
|
| 1733 |
+
panel.querySelector('.fp-foot').innerHTML =
|
| 1734 |
+
(gid >= 0 ? `Rank 0 of all 8,192 here: <b>f${gid}</b>` : 'Nothing in the dictionary fired here') +
|
| 1735 |
+
`<span class="hint">Click a row to add or remove its lane, up to ${fmt(MAX_LANES)}.</span>`;
|
| 1736 |
+
}
|
| 1737 |
+
|
| 1738 |
+
/* A window that will not load is reported in the readout. The plot keeps the
|
| 1739 |
+
window it already has rather than being replaced by a fallback paragraph,
|
| 1740 |
+
which would detach the svg that render() writes into. */
|
| 1741 |
+
function choose(uid) {
|
| 1742 |
+
load(uid).catch(error => {
|
| 1743 |
+
host.removeAttribute('aria-busy');
|
| 1744 |
+
if (doc) paintRegion(G ? G.plotW / nBasesShown() : 1);
|
| 1745 |
+
readout.setAttribute('aria-live', 'polite');
|
| 1746 |
+
readout.innerHTML = `<div class="ro-block ro-fail"><p class="ro-lab">Window unavailable</p>` +
|
| 1747 |
+
`<div class="ro-empty">${esc(uid)} could not be loaded. The window on screen is unchanged;` +
|
| 1748 |
+
` pick another window or reload the page.</div></div>`;
|
| 1749 |
+
if (window.console) console.error(error);
|
| 1750 |
+
});
|
| 1751 |
+
}
|
| 1752 |
+
|
| 1753 |
+
/* ---- boot -------------------------------------------------------------- */
|
| 1754 |
+
try {
|
| 1755 |
+
const r = await fetch('data/sae/index.json');
|
| 1756 |
+
if (!r.ok) throw new Error('feature index unavailable');
|
| 1757 |
+
index = await r.json();
|
| 1758 |
+
|
| 1759 |
+
paintPicker();
|
| 1760 |
+
ensurePanel();
|
| 1761 |
+
ensureControls();
|
| 1762 |
+
|
| 1763 |
+
svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
| 1764 |
+
svg.setAttribute('role', 'group');
|
| 1765 |
+
svg.setAttribute('tabindex', '0');
|
| 1766 |
+
svg.setAttribute('aria-describedby', 'feat-help');
|
| 1767 |
+
svg.setAttribute('aria-label', 'Feature browser, loading');
|
| 1768 |
+
host.appendChild(svg);
|
| 1769 |
+
|
| 1770 |
+
/* the finding is a handful of bases in 8,191; one click frames it */
|
| 1771 |
+
findBtn = document.createElement('button');
|
| 1772 |
+
findBtn.type = 'button';
|
| 1773 |
+
findBtn.id = 'feat-finding';
|
| 1774 |
+
findBtn.hidden = true;
|
| 1775 |
+
findBtn.addEventListener('click', () => {
|
| 1776 |
+
const marks = findingMarks(doc.selection);
|
| 1777 |
+
if (!marks.length) return;
|
| 1778 |
+
const bases = marks.map(m => m.base);
|
| 1779 |
+
const lo = Math.min(...bases), hi = Math.max(...bases);
|
| 1780 |
+
const span = Math.max(240, (hi - lo) * 4);
|
| 1781 |
+
const centre = (lo + hi) / 2;
|
| 1782 |
+
cursor = marks[0].base;
|
| 1783 |
+
pinned = null;
|
| 1784 |
+
setView(centre - span / 2, centre + span / 2);
|
| 1785 |
+
focus(cursor, true);
|
| 1786 |
+
});
|
| 1787 |
+
(document.querySelector('.feat-tools') || host.parentElement).appendChild(findBtn);
|
| 1788 |
+
|
| 1789 |
+
document.getElementById('feat-in').addEventListener('click', () => zoom(0.5));
|
| 1790 |
+
document.getElementById('feat-out').addEventListener('click', () => zoom(2));
|
| 1791 |
+
document.getElementById('feat-full').addEventListener('click', () => setView(0, doc.n_bases));
|
| 1792 |
+
modeBtns.forEach(b => b.addEventListener('click', () => {
|
| 1793 |
+
mode = b.dataset.mode;
|
| 1794 |
+
modeBtns.forEach(o => o.setAttribute('aria-pressed', String(o === b)));
|
| 1795 |
+
render();
|
| 1796 |
+
}));
|
| 1797 |
+
|
| 1798 |
+
const pointBase = ev => {
|
| 1799 |
+
const p = svg.createSVGPoint();
|
| 1800 |
+
p.x = ev.clientX; p.y = ev.clientY;
|
| 1801 |
+
const q = p.matrixTransform(svg.getScreenCTM().inverse());
|
| 1802 |
+
if (q.x < L || q.x > W - R) return null;
|
| 1803 |
+
const base = Math.floor(start + (q.x - L) / G.plotW * nBasesShown());
|
| 1804 |
+
return base >= 0 && base < doc.n_bases ? base : null;
|
| 1805 |
+
};
|
| 1806 |
+
const midBase = () => {
|
| 1807 |
+
const [a, b] = Array.from(pointers.values());
|
| 1808 |
+
const p = svg.createSVGPoint();
|
| 1809 |
+
p.x = (a.x + b.x) / 2; p.y = (a.y + b.y) / 2;
|
| 1810 |
+
const q = p.matrixTransform(svg.getScreenCTM().inverse());
|
| 1811 |
+
return start + (Math.min(Math.max(q.x, L), W - R) - L) / G.plotW * nBasesShown();
|
| 1812 |
+
};
|
| 1813 |
+
const spread = () => {
|
| 1814 |
+
const [a, b] = Array.from(pointers.values());
|
| 1815 |
+
return Math.max(1, Math.hypot(a.x - b.x, a.y - b.y));
|
| 1816 |
+
};
|
| 1817 |
+
|
| 1818 |
+
svg.addEventListener('pointerdown', ev => {
|
| 1819 |
+
pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
|
| 1820 |
+
if (pointers.size === 2) {
|
| 1821 |
+
drag = null;
|
| 1822 |
+
const at = midBase();
|
| 1823 |
+
pinch = { d0: spread(), width: nBasesShown(), at, frac: (at - start) / Math.max(1, nBasesShown()) };
|
| 1824 |
+
} else if (pointers.size === 1) {
|
| 1825 |
+
drag = { px: ev.clientX, start, end, moved: false };
|
| 1826 |
+
}
|
| 1827 |
+
});
|
| 1828 |
+
svg.addEventListener('pointermove', ev => {
|
| 1829 |
+
if (pointers.has(ev.pointerId)) pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
|
| 1830 |
+
if (pinch && pointers.size >= 2) {
|
| 1831 |
+
const scale = pinch.d0 / spread();
|
| 1832 |
+
const width = Math.max(40, Math.min(doc.n_bases, pinch.width * scale));
|
| 1833 |
+
const f = Math.min(Math.max(pinch.frac, 0), 1);
|
| 1834 |
+
setViewSoon(pinch.at - width * f, pinch.at + width * (1 - f));
|
| 1835 |
+
return;
|
| 1836 |
+
}
|
| 1837 |
+
if (drag) {
|
| 1838 |
+
const delta = ev.clientX - drag.px;
|
| 1839 |
+
if (Math.abs(delta) > 5) drag.moved = true;
|
| 1840 |
+
if (drag.moved) {
|
| 1841 |
+
svg.setPointerCapture(ev.pointerId);
|
| 1842 |
+
const shift = -delta / svg.getBoundingClientRect().width * W / G.plotW * (drag.end - drag.start);
|
| 1843 |
+
setViewSoon(drag.start + shift, drag.end + shift);
|
| 1844 |
+
return;
|
| 1845 |
+
}
|
| 1846 |
+
}
|
| 1847 |
+
const base = pointBase(ev);
|
| 1848 |
+
if (base !== null && base !== cursor && pinned === null) { cursor = base; focus(base, false); }
|
| 1849 |
+
});
|
| 1850 |
+
svg.addEventListener('pointerup', ev => {
|
| 1851 |
+
if (drag && !drag.moved) {
|
| 1852 |
+
const base = pointBase(ev);
|
| 1853 |
+
if (base !== null) {
|
| 1854 |
+
pinned = pinned === base ? null : base;
|
| 1855 |
+
cursor = base;
|
| 1856 |
+
focus(pinned === null ? cursor : pinned, true);
|
| 1857 |
+
}
|
| 1858 |
+
}
|
| 1859 |
+
pointers.delete(ev.pointerId);
|
| 1860 |
+
if (pointers.size < 2) pinch = null;
|
| 1861 |
+
drag = null;
|
| 1862 |
+
if (svg.hasPointerCapture(ev.pointerId)) svg.releasePointerCapture(ev.pointerId);
|
| 1863 |
+
});
|
| 1864 |
+
svg.addEventListener('pointercancel', ev => {
|
| 1865 |
+
pointers.delete(ev.pointerId);
|
| 1866 |
+
if (pointers.size < 2) pinch = null;
|
| 1867 |
+
drag = null;
|
| 1868 |
+
});
|
| 1869 |
+
svg.addEventListener('pointerleave', () => {
|
| 1870 |
+
if (!drag) focus(pinned === null ? cursor : pinned, false);
|
| 1871 |
+
});
|
| 1872 |
+
svg.addEventListener('keydown', ev => {
|
| 1873 |
+
if (ev.key === '+' || ev.key === '=') { ev.preventDefault(); zoom(0.5); return; }
|
| 1874 |
+
if (ev.key === '-' || ev.key === '_') { ev.preventDefault(); zoom(2); return; }
|
| 1875 |
+
if (ev.key === 'Home') { ev.preventDefault(); setView(0, doc.n_bases); return; }
|
| 1876 |
+
if (ev.key === 'Escape') {
|
| 1877 |
+
if (pinned !== null) { ev.preventDefault(); pinned = null; focus(cursor, true); }
|
| 1878 |
+
return;
|
| 1879 |
+
}
|
| 1880 |
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
| 1881 |
+
ev.preventDefault();
|
| 1882 |
+
pinned = pinned === cursor ? null : cursor;
|
| 1883 |
+
focus(pinned === null ? cursor : pinned, true);
|
| 1884 |
+
return;
|
| 1885 |
+
}
|
| 1886 |
+
if (!['ArrowLeft', 'ArrowRight'].includes(ev.key)) return;
|
| 1887 |
+
ev.preventDefault();
|
| 1888 |
+
const step = ev.shiftKey ? Math.max(1, Math.round(nBasesShown() / 20)) : 1;
|
| 1889 |
+
cursor = Math.max(0, Math.min(doc.n_bases - 1, cursor + (ev.key === 'ArrowLeft' ? -step : step)));
|
| 1890 |
+
pinned = cursor;
|
| 1891 |
+
if (cursor < start || cursor > end) {
|
| 1892 |
+
const width = nBasesShown();
|
| 1893 |
+
setView(cursor - width / 2, cursor + width / 2);
|
| 1894 |
+
} else focus(cursor, true);
|
| 1895 |
+
});
|
| 1896 |
+
|
| 1897 |
+
if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => render());
|
| 1898 |
+
let settleRaf;
|
| 1899 |
+
const settleDocks = () => {
|
| 1900 |
+
cancelAnimationFrame(settleRaf);
|
| 1901 |
+
settleRaf = requestAnimationFrame(() => docks.forEach(d => {
|
| 1902 |
+
if (!d.floating() || d.el.hidden) return;
|
| 1903 |
+
const box = d.el.getBoundingClientRect(), stage = stageEl().getBoundingClientRect();
|
| 1904 |
+
d.place(box.left - stage.left, box.top - stage.top);
|
| 1905 |
+
}));
|
| 1906 |
+
};
|
| 1907 |
+
new ResizeObserver(settleDocks).observe(stageEl());
|
| 1908 |
+
window.addEventListener('resize', () => { syncGutter(); docks.forEach(d => d.settle()); settleDocks(); });
|
| 1909 |
+
|
| 1910 |
+
let raf;
|
| 1911 |
+
new ResizeObserver(() => { cancelAnimationFrame(raf); raf = requestAnimationFrame(render); }).observe(host);
|
| 1912 |
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => render());
|
| 1913 |
+
await load(index.windows[0].window_uid);
|
| 1914 |
+
} catch (error) {
|
| 1915 |
+
fail(error);
|
| 1916 |
+
}
|
| 1917 |
+
|
| 1918 |
+
/* The (?) beside the lane toggle. Hover opens it for a reader with a mouse and
|
| 1919 |
+
click pins it open, because a hover-opened panel that a click then shut would
|
| 1920 |
+
make the click look broken. Only a pinned panel survives the pointer leaving.
|
| 1921 |
+
The close is delayed a beat because the pointer has to cross a gap to reach
|
| 1922 |
+
the panel it just opened. Nothing here depends on a loaded window, so the
|
| 1923 |
+
button is live from the first paint. */
|
| 1924 |
+
function wireHelp() {
|
| 1925 |
+
const btn = document.getElementById('feat-help-toggle');
|
| 1926 |
+
const panel = document.getElementById('feat-help-panel');
|
| 1927 |
+
if (!btn || !panel) return;
|
| 1928 |
+
let shut, pinned = false;
|
| 1929 |
+
const set = open => {
|
| 1930 |
+
clearTimeout(shut);
|
| 1931 |
+
panel.hidden = !open;
|
| 1932 |
+
btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
| 1933 |
+
};
|
| 1934 |
+
const close = () => { if (!pinned) shut = setTimeout(() => set(false), 220); };
|
| 1935 |
+
const unpin = () => { pinned = false; set(false); };
|
| 1936 |
+
btn.addEventListener('click', () => {
|
| 1937 |
+
pinned = !pinned;
|
| 1938 |
+
set(pinned);
|
| 1939 |
+
});
|
| 1940 |
+
btn.addEventListener('pointerenter', () => set(true));
|
| 1941 |
+
btn.addEventListener('focus', () => set(true));
|
| 1942 |
+
btn.addEventListener('pointerleave', close);
|
| 1943 |
+
panel.addEventListener('pointerenter', () => clearTimeout(shut));
|
| 1944 |
+
panel.addEventListener('pointerleave', close);
|
| 1945 |
+
/* A click inside the panel must not shut it, but a click anywhere else must. */
|
| 1946 |
+
document.addEventListener('pointerdown', ev => {
|
| 1947 |
+
if (!panel.hidden && !panel.contains(ev.target) && ev.target !== btn) unpin();
|
| 1948 |
+
});
|
| 1949 |
+
document.addEventListener('keydown', ev => {
|
| 1950 |
+
if (ev.key === 'Escape' && !panel.hidden) { unpin(); btn.focus(); }
|
| 1951 |
+
});
|
| 1952 |
+
}
|
| 1953 |
+
function fail(error) {
|
| 1954 |
+
if (region) region.textContent = 'Feature windows';
|
| 1955 |
+
if (readout) readout.textContent = 'The precomputed windows could not be loaded. Reload the page to try again.';
|
| 1956 |
+
host.innerHTML = '<p class="feat-fallback">The interpretability results are described in the report.</p>';
|
| 1957 |
+
if (window.console) console.error(error);
|
| 1958 |
+
}
|
| 1959 |
+
})();
|
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Annotation-sources block. Kept in its own file so the figure's stylesheet and this
|
| 2 |
+
one are never edited together. Tokens only, so both themes follow the site. */
|
| 3 |
+
|
| 4 |
+
.src-table td { vertical-align: top; }
|
| 5 |
+
.src-acc { color: var(--ink-3); font-size: 10.5px; }
|
| 6 |
+
|
| 7 |
+
.src-badge { display: inline-block; font: 600 11px var(--sans); padding: 3px 9px;
|
| 8 |
+
border-radius: 999px; white-space: nowrap; border: 1px solid var(--rule-2); }
|
| 9 |
+
.src-badge.cur { background: var(--paper-2); color: var(--ink-2); }
|
| 10 |
+
.src-badge.up { background: var(--lime-2); color: var(--ink); border-color: var(--lime); }
|
| 11 |
+
|
| 12 |
+
.src-details { margin: 14px 0 0; border-top: 1px solid var(--rule); }
|
| 13 |
+
.src-details > summary { font: 600 13.5px var(--sans); color: var(--ink); cursor: pointer;
|
| 14 |
+
padding: 11px 0; list-style: none; display: flex; align-items: baseline; gap: 8px; }
|
| 15 |
+
.src-details > summary::-webkit-details-marker { display: none; }
|
| 16 |
+
.src-details > summary::before { content: '+'; font-family: var(--mono); color: var(--ink-3);
|
| 17 |
+
font-weight: 400; width: 1ch; }
|
| 18 |
+
.src-details[open] > summary::before { content: '\2212'; }
|
| 19 |
+
.src-details > summary:hover { color: var(--accent); }
|
| 20 |
+
.src-details > ul { margin: 0 0 14px; padding-left: 22px; }
|
| 21 |
+
.src-details > ul > li { margin-bottom: 8px; max-width: var(--measure); }
|
| 22 |
+
|
| 23 |
+
.src-block { margin: 0 0 16px; padding-left: 12px; border-left: 2px solid var(--rule); }
|
| 24 |
+
.src-block .label { margin: 0 0 5px; text-transform: none; letter-spacing: 0.04em;
|
| 25 |
+
font-size: 12px; color: var(--ink); }
|
| 26 |
+
.src-block p.small { margin: 0 0 6px; max-width: var(--measure); }
|
| 27 |
+
|
| 28 |
+
@media (max-width: 640px) {
|
| 29 |
+
.src-details > summary { padding: 13px 0; }
|
| 30 |
+
.src-badge { font-size: 10.5px; padding: 2px 7px; }
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* An absent value is named rather than left blank, so a reader can tell "no value
|
| 34 |
+
published" from "we forgot to fill this in". */
|
| 35 |
+
.na { color: var(--ink-3); font-family: var(--sans); font-size: 12px; font-style: italic; }
|
| 36 |
+
td.n .na { font-size: 11px; }
|
| 37 |
+
|
| 38 |
+
/* A lane chosen because it matches a category best in this window, as against a
|
| 39 |
+
feature the report names: the softer third line says which. */
|
| 40 |
+
#feat-plot text.lane-fit { fill: var(--ink-3); font-family: var(--mono); font-size: 9.5px; font-style: italic; }
|
| 41 |
+
|
| 42 |
+
/* ---- the usage register ------------------------------------------------------
|
| 43 |
+
Under the browser: what registers independent of the annotation found at the
|
| 44 |
+
site a feature marked. It is deliberately a quieter register than the plot —
|
| 45 |
+
nothing here is a measurement of the model — and the three verdicts are
|
| 46 |
+
distinguished by weight rather than by hue, so `untested` can never read as a
|
| 47 |
+
weaker `used`. Tokens only, so both themes follow the site. */
|
| 48 |
+
|
| 49 |
+
/* Nested in the case island, so it carries a rule off the island's top edge
|
| 50 |
+
rather than a box of its own. */
|
| 51 |
+
.feat-case .usage-check {
|
| 52 |
+
margin: 11px 0 0; padding: 11px 0 0; border-top: 1px solid var(--rule);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.uc-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; margin: 0 0 6px; }
|
| 56 |
+
.uc-verdict {
|
| 57 |
+
font: 600 10px var(--mono); letter-spacing: 0.12em; text-transform: uppercase;
|
| 58 |
+
padding: 3px 9px; border-radius: 999px; white-space: nowrap;
|
| 59 |
+
border: 1px solid var(--rule-2); background: var(--card); color: var(--ink-2);
|
| 60 |
+
}
|
| 61 |
+
.uc-verdict[data-verdict="used"] {
|
| 62 |
+
background: var(--lime-2); border-color: var(--lime); color: var(--ink);
|
| 63 |
+
}
|
| 64 |
+
.uc-verdict[data-verdict="untested"] {
|
| 65 |
+
background: none; border-style: dashed; color: var(--ink-3);
|
| 66 |
+
}
|
| 67 |
+
.uc-headline { font: 600 13px var(--sans); color: var(--ink); }
|
| 68 |
+
.uc-summary { margin: 0 0 9px; font: 13.5px/1.55 var(--serif); color: var(--ink-2); max-width: 74ch; }
|
| 69 |
+
|
| 70 |
+
.uc-registers { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px;
|
| 71 |
+
background: var(--rule); border: 1px solid var(--rule); border-radius: 5px; overflow: hidden; }
|
| 72 |
+
/* Register, what it answers and what it said on one line; the detail beneath on
|
| 73 |
+
its own. A four-column grid squeezes the detail to a ribbon at this width. */
|
| 74 |
+
.uc-registers > li {
|
| 75 |
+
display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 10px;
|
| 76 |
+
background: var(--card); padding: 8px 11px;
|
| 77 |
+
}
|
| 78 |
+
.ur-detail { flex: 1 0 100%; }
|
| 79 |
+
.ur-name { font: 600 11.5px var(--sans); color: var(--ink); }
|
| 80 |
+
.ur-role { font: 10px var(--mono); color: var(--ink-3); letter-spacing: 0.02em; }
|
| 81 |
+
.ur-result {
|
| 82 |
+
font: 600 10px var(--mono); letter-spacing: 0.08em; text-transform: uppercase;
|
| 83 |
+
color: var(--ink-3); white-space: nowrap;
|
| 84 |
+
}
|
| 85 |
+
.ur-result[data-result="used"] { color: var(--accent-ink); }
|
| 86 |
+
.ur-result[data-result="not consulted"] { font-style: italic; font-weight: 400; }
|
| 87 |
+
.ur-detail { font: 11.5px/1.45 var(--sans); color: var(--ink-2); }
|
| 88 |
+
|
| 89 |
+
.uc-foot { margin: 8px 0 0; font: 11px var(--sans); color: var(--ink-3); }
|
| 90 |
+
|
| 91 |
+
/* The picker chip. It repeats the verdict so the choice between windows is made
|
| 92 |
+
with it in view, not after loading one. */
|
| 93 |
+
.win-card .wv {
|
| 94 |
+
justify-self: start; font: 600 8.5px var(--mono); letter-spacing: 0.12em;
|
| 95 |
+
text-transform: uppercase; padding: 2px 6px; border-radius: 999px;
|
| 96 |
+
border: 1px solid var(--rule-2); color: var(--ink-3); background: var(--paper-2);
|
| 97 |
+
}
|
| 98 |
+
.win-card .wv[data-verdict="used"] {
|
| 99 |
+
background: var(--lime-2); border-color: var(--lime); color: var(--ink);
|
| 100 |
+
}
|
| 101 |
+
.win-card .wv[data-verdict="untested"] { background: none; border-style: dashed; }
|
| 102 |
+
|
| 103 |
+
/* The verdict key beside the register table. */
|
| 104 |
+
.usage-key { display: grid; grid-template-columns: max-content 1fr; gap: 5px 12px;
|
| 105 |
+
margin: 0 0 14px; max-width: var(--measure); }
|
| 106 |
+
.usage-key dt {
|
| 107 |
+
font: 600 10px var(--mono); letter-spacing: 0.12em; text-transform: uppercase;
|
| 108 |
+
padding: 3px 9px; border-radius: 999px; justify-self: start; white-space: nowrap;
|
| 109 |
+
border: 1px solid var(--rule-2); background: var(--card); color: var(--ink-2);
|
| 110 |
+
}
|
| 111 |
+
.usage-key dt[data-verdict="used"] { background: var(--lime-2); border-color: var(--lime); color: var(--ink); }
|
| 112 |
+
.usage-key dt[data-verdict="untested"] { background: none; border-style: dashed; color: var(--ink-3); }
|
| 113 |
+
.usage-key dd { margin: 0; font: 12.5px/1.5 var(--sans); color: var(--ink-2); align-self: center; }
|
| 114 |
+
|
| 115 |
+
@media (max-width: 640px) {
|
| 116 |
+
.uc-registers > li { padding: 9px 10px; }
|
| 117 |
+
.usage-key { grid-template-columns: 1fr; gap: 2px 0; }
|
| 118 |
+
.usage-key dd { margin-bottom: 8px; }
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
/* ---- the case island --------------------------------------------------------
|
| 122 |
+
What this window is, inside the figure, where the interaction help used to
|
| 123 |
+
sit: the kind, the headline, the reason the window was chosen, and — where a
|
| 124 |
+
feature marks a site the annotation does not carry — the usage verdict. The
|
| 125 |
+
interaction help now lives behind the (?) beside the lane toggle. */
|
| 126 |
+
|
| 127 |
+
.feat-case {
|
| 128 |
+
margin: 0; padding: 13px 24px 14px;
|
| 129 |
+
border-top: 1px solid var(--rule); background: var(--paper-2);
|
| 130 |
+
}
|
| 131 |
+
.fc-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; margin: 0 0 5px; }
|
| 132 |
+
.fc-kind {
|
| 133 |
+
font: 600 9px var(--mono); letter-spacing: 0.13em; text-transform: uppercase;
|
| 134 |
+
color: var(--ink-3); border: 1px solid var(--rule-2); border-radius: 999px;
|
| 135 |
+
padding: 3px 8px; white-space: nowrap;
|
| 136 |
+
}
|
| 137 |
+
.fc-kind[data-flag="1"] { color: var(--ink-2); border-color: var(--sel-flag); }
|
| 138 |
+
.fc-headline { font: 600 13.5px var(--sans); color: var(--ink); }
|
| 139 |
+
.fc-why {
|
| 140 |
+
margin: 0; max-width: 82ch;
|
| 141 |
+
font: 13.5px/1.55 var(--serif); color: var(--ink-2);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
/* ---- the (?) help disclosure ---------------------------------------------- */
|
| 145 |
+
|
| 146 |
+
/* The panel hangs off the tools group, so it needs the positioning context here
|
| 147 |
+
rather than in the figure's own stylesheet. .feat-browser clips its overflow,
|
| 148 |
+
which is what keeps the panel inside the figure. */
|
| 149 |
+
.feat-tools { position: relative; }
|
| 150 |
+
.feat-tools button.help {
|
| 151 |
+
min-width: 34px; padding: 3px 9px; font: 600 14px var(--sans); border-radius: 999px;
|
| 152 |
+
}
|
| 153 |
+
#feat-help-toggle[aria-expanded="true"] {
|
| 154 |
+
background: var(--sel-bg); color: var(--sel-fg); border-color: var(--sel-bg);
|
| 155 |
+
}
|
| 156 |
+
.feat-help-panel {
|
| 157 |
+
position: absolute; z-index: 6; right: 0; top: calc(100% + 9px);
|
| 158 |
+
/* .feat-browser is a container-query container, so the panel is sized against
|
| 159 |
+
the figure it must stay inside rather than against the viewport; the
|
| 160 |
+
subtrahend is the heading's own left+right padding. */
|
| 161 |
+
width: min(46ch, calc(100cqw - 48px));
|
| 162 |
+
padding: 13px 15px 14px;
|
| 163 |
+
background: var(--card); color: var(--ink-2);
|
| 164 |
+
border: 1px solid var(--rule-2); border-radius: 8px;
|
| 165 |
+
box-shadow: 0 10px 28px rgb(0 0 0 / 0.14);
|
| 166 |
+
font: 12px/1.5 var(--sans);
|
| 167 |
+
}
|
| 168 |
+
.feat-help-panel .fh-title {
|
| 169 |
+
margin: 0 0 7px; font: 600 12px var(--sans); color: var(--ink);
|
| 170 |
+
}
|
| 171 |
+
.feat-help-panel p { margin: 0 0 8px; max-width: none; }
|
| 172 |
+
.feat-help-panel p:last-child { margin-bottom: 0; }
|
| 173 |
+
.feat-help-panel .fh-lanes {
|
| 174 |
+
padding-top: 8px; border-top: 1px solid var(--rule); color: var(--ink-3); font-size: 11.5px;
|
| 175 |
+
}
|
| 176 |
+
.feat-help-panel em { font-style: italic; color: var(--ink-2); }
|
| 177 |
+
|
| 178 |
+
@media (max-width: 640px) {
|
| 179 |
+
.feat-case { padding: 12px 18px 13px; }
|
| 180 |
+
.feat-help-panel { right: 0; width: min(46ch, calc(100cqw - 36px)); }
|
| 181 |
+
.feat-tools button.help { min-width: 40px; }
|
| 182 |
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* The static tables beside the browser: the named features and their published
|
| 2 |
+
evidence, and the annotation sources. All of it is read out of
|
| 3 |
+
data/sae/index.json.
|
| 4 |
+
|
| 5 |
+
Captions are authored in sae-features.html as the first child of each table,
|
| 6 |
+
so filling a table keeps the caption that is already there. */
|
| 7 |
+
(async function () {
|
| 8 |
+
const named = document.getElementById('named-table');
|
| 9 |
+
const prov = document.getElementById('feat-prov');
|
| 10 |
+
const sources = document.getElementById('sources-body');
|
| 11 |
+
if (!named) return;
|
| 12 |
+
|
| 13 |
+
const esc = value => String(value ?? '').replace(/[&<>"']/g, ch =>
|
| 14 |
+
({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]));
|
| 15 |
+
|
| 16 |
+
/* A caption has to stay the table's first child, so it is lifted out and put
|
| 17 |
+
back ahead of the generated head and body. */
|
| 18 |
+
const fill = (table, html) => {
|
| 19 |
+
const caption = table.querySelector('caption');
|
| 20 |
+
table.innerHTML = (caption ? caption.outerHTML : '') + html;
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
/* The report's own vocabulary for what a feature resolves. */
|
| 24 |
+
const LEVEL = {
|
| 25 |
+
site: 'base-level', range: 'region-level', motif: 'motif', pattern: 'pattern',
|
| 26 |
+
};
|
| 27 |
+
|
| 28 |
+
/* Reading order: the detectors the report validates against annotation, then
|
| 29 |
+
the cis-element motifs and repeat patterns, then the two candidates it
|
| 30 |
+
rejects. Anything the index adds later lands in a fourth group rather than
|
| 31 |
+
disappearing. */
|
| 32 |
+
const GROUPS = [
|
| 33 |
+
{ title: 'Validated against annotation', ids: [4131, 2634, 586, 7708, 3930, 2618, 1200] },
|
| 34 |
+
{ title: 'Cis-element motifs and repeat patterns', ids: [4567, 145, 5804, 1474, 5349] },
|
| 35 |
+
{ title: 'Other named features', ids: [] },
|
| 36 |
+
];
|
| 37 |
+
/* Best detector of its class in all four annotated genomes. */
|
| 38 |
+
const BEST_IN_CLASS = new Set([4131, 586, 7708]);
|
| 39 |
+
|
| 40 |
+
/* MCC gets its own numeric column, so it comes off the front of the evidence
|
| 41 |
+
string; the validation-selected threshold is stated once in the caption. */
|
| 42 |
+
function splitEvidence(text) {
|
| 43 |
+
const raw = String(text ?? '').trim();
|
| 44 |
+
const hit = /^MCC\s+([0-9.]+)/.exec(raw);
|
| 45 |
+
return { mcc: hit ? hit[1] : '' };
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
try {
|
| 49 |
+
const index = await (await fetch('data/sae/index.json')).json();
|
| 50 |
+
const s = index.sae;
|
| 51 |
+
|
| 52 |
+
prov.textContent = `${s.model_name}, layer ${s.layer}, ${s.d_sae} features, k = ${s.k}`;
|
| 53 |
+
prov.title = `${s.sae_id} · sha256 ${s.sae_sha256.slice(0, 16)}… · ${s.threshold_mode} threshold ${s.sae_threshold.toFixed(4)}`;
|
| 54 |
+
|
| 55 |
+
const features = new Map(Object.entries(index.curated_features)
|
| 56 |
+
.map(([id, f]) => [Number(id), f]));
|
| 57 |
+
const grouped = new Set(GROUPS.flatMap(g => g.ids));
|
| 58 |
+
GROUPS[GROUPS.length - 1].ids = [...features.keys()].filter(id => !grouped.has(id));
|
| 59 |
+
|
| 60 |
+
const row = id => {
|
| 61 |
+
const f = features.get(id);
|
| 62 |
+
const { mcc } = splitEvidence(f.evidence);
|
| 63 |
+
const label = String(f.label ?? '').replace(/\s*\(rejected\)$/, '');
|
| 64 |
+
return `<tr${BEST_IN_CLASS.has(id) ? ' class="ours"' : ''}>` +
|
| 65 |
+
`<td class="num">f${id}</td><td>${esc(label)}</td>` +
|
| 66 |
+
`<td>${esc(LEVEL[f.kind] || f.kind)}</td>` +
|
| 67 |
+
// A dash, not a blank: an empty cell reads as an oversight. The title
|
| 68 |
+
// still says which kind of absence this is.
|
| 69 |
+
`<td class="n">${mcc ? esc(mcc)
|
| 70 |
+
: '<span class="na" title="the report publishes no MCC for this feature">–</span>'}</td>` +
|
| 71 |
+
'</tr>';
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
fill(named,
|
| 75 |
+
'<thead><tr><th>Feature</th><th>Named for</th><th>Level</th>' +
|
| 76 |
+
'<th class="n">MCC</th></tr></thead>' +
|
| 77 |
+
GROUPS.filter(g => g.ids.length).map(g =>
|
| 78 |
+
`<tbody><tr><th colspan="4" scope="colgroup">${g.title}</th></tr>` +
|
| 79 |
+
g.ids.filter(id => features.has(id)).map(row).join('') + '</tbody>').join(''));
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
if (sources) {
|
| 83 |
+
if (index.sources) renderSources(sources, index.sources);
|
| 84 |
+
else renderPendingSources(sources, index.windows);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
} catch (error) {
|
| 88 |
+
fill(named, '<tbody><tr><td>The feature table could not be loaded.</td></tr></tbody>');
|
| 89 |
+
if (window.console) console.error(error);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
/* The verified releases, URLs, citations and licences, once index.sources
|
| 94 |
+
carries them. */
|
| 95 |
+
function renderSources(host, src) {
|
| 96 |
+
// Two registers per species: the annotation the per-base tracks were built from,
|
| 97 |
+
// and the most current annotation on the SAME assembly. A newer assembly cannot be
|
| 98 |
+
// overlaid on these coordinates, so it is named separately rather than used.
|
| 99 |
+
const badge = r => r.currency === 'upgraded'
|
| 100 |
+
? '<span class="src-badge up">newer release applied</span>'
|
| 101 |
+
: '<span class="src-badge cur">already current</span>';
|
| 102 |
+
|
| 103 |
+
let html = `<p>${esc(src.intro)}</p>`;
|
| 104 |
+
|
| 105 |
+
html += '<div class="tablewrap"><table class="src-table">' +
|
| 106 |
+
'<caption>' + esc(src.caption) + '</caption>' +
|
| 107 |
+
'<thead><tr><th>Species</th><th>Assembly</th><th>Tracks built from</th>' +
|
| 108 |
+
'<th>Gene models shown</th><th>Status</th></tr></thead><tbody>';
|
| 109 |
+
for (const r of src.species) {
|
| 110 |
+
html += '<tr>' +
|
| 111 |
+
`<td><em>${esc(r.binomial)}</em></td>` +
|
| 112 |
+
`<td><span class="num">${esc(r.assembly)}</span><br><span class="src-acc num">${esc(r.assembly_accession)}</span></td>` +
|
| 113 |
+
`<td>${esc(r.annotation_build)}</td>` +
|
| 114 |
+
`<td>${r.url ? `<a href="${esc(r.url)}">${esc(r.annotation_current)}</a>` : esc(r.annotation_current)}` +
|
| 115 |
+
`${r.released ? `<br><span class="src-acc num">released ${esc(r.released)}</span>` : ''}</td>` +
|
| 116 |
+
`<td>${badge(r)}</td>` +
|
| 117 |
+
'</tr>';
|
| 118 |
+
}
|
| 119 |
+
html += '</tbody></table></div>';
|
| 120 |
+
|
| 121 |
+
html += '<details class="src-details"><summary>Per-species detail, licences and what supersedes each assembly</summary>';
|
| 122 |
+
for (const r of src.species) {
|
| 123 |
+
html += `<div class="src-block"><p class="label">${esc(r.binomial)}</p>`;
|
| 124 |
+
if (r.currency_note) html += `<p class="small">${esc(r.currency_note)}</p>`;
|
| 125 |
+
if (r.superseded_by) html += `<p class="small"><strong>Superseded by:</strong> ${esc(r.superseded_by)}</p>`;
|
| 126 |
+
if (r.licence) html += `<p class="small"><strong>Licence:</strong> ${esc(r.licence)}</p>`;
|
| 127 |
+
if (r.citation) html += `<p class="small">${esc(r.citation)}</p>`;
|
| 128 |
+
html += '</div>';
|
| 129 |
+
}
|
| 130 |
+
html += '</details>';
|
| 131 |
+
|
| 132 |
+
if (src.usage_registers) html += renderUsageRegisters(src.usage_registers);
|
| 133 |
+
|
| 134 |
+
if (src.notes && src.notes.length) {
|
| 135 |
+
html += '<details class="src-details"><summary>Notes on assembly currency and coordinates</summary><ul>' +
|
| 136 |
+
src.notes.map(n => `<li>${esc(n)}</li>`).join('') + '</ul></details>';
|
| 137 |
+
}
|
| 138 |
+
if (src.citations && src.citations.length) {
|
| 139 |
+
html += '<details class="src-details"><summary>Citations</summary><ul class="small">' +
|
| 140 |
+
src.citations.map(c => `<li>${esc(c)}</li>`).join('') + '</ul></details>';
|
| 141 |
+
}
|
| 142 |
+
host.innerHTML = html;
|
| 143 |
+
}
|
| 144 |
+
|
| 145 |
+
/* The registers that answer whether a site is used, as opposed to whether an
|
| 146 |
+
annotation carries it. They are a separate table because they are a separate
|
| 147 |
+
question, and because each one covers only Arabidopsis: the verdict a window
|
| 148 |
+
shows is only ever as wide as the register behind it. */
|
| 149 |
+
function renderUsageRegisters(reg) {
|
| 150 |
+
let html = '<h3 id="usage-registers">Usage registers</h3>' +
|
| 151 |
+
`<p>${esc(reg.intro)}</p>`;
|
| 152 |
+
|
| 153 |
+
if (reg.verdicts && reg.verdicts.length) {
|
| 154 |
+
html += '<dl class="usage-key">' + reg.verdicts.map(v =>
|
| 155 |
+
`<dt data-verdict="${esc(v.key)}">${esc(v.label)}</dt><dd>${esc(v.gloss)}</dd>`
|
| 156 |
+
).join('') + '</dl>';
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
html += '<div class="tablewrap"><table class="src-table usage-table">' +
|
| 160 |
+
`<caption>${esc(reg.caption)}</caption>` +
|
| 161 |
+
'<thead><tr><th>Register</th><th>Answers</th><th>Version</th><th>Covers</th>' +
|
| 162 |
+
'</tr></thead><tbody>';
|
| 163 |
+
for (const r of reg.rows) {
|
| 164 |
+
html += '<tr>' +
|
| 165 |
+
`<td>${r.url ? `<a href="${esc(r.url)}">${esc(r.name)}</a>` : esc(r.name)}</td>` +
|
| 166 |
+
`<td>${esc(r.role)}</td>` +
|
| 167 |
+
`<td><span class="num">${esc(r.version)}</span></td>` +
|
| 168 |
+
`<td>${esc(r.scope)}</td>` +
|
| 169 |
+
'</tr>';
|
| 170 |
+
}
|
| 171 |
+
html += '</tbody></table></div>';
|
| 172 |
+
|
| 173 |
+
html += '<details class="src-details"><summary>What each register contains, and what it ' +
|
| 174 |
+
'cannot answer</summary>';
|
| 175 |
+
for (const r of reg.rows) {
|
| 176 |
+
html += `<div class="src-block"><p class="label">${esc(r.name)}</p>` +
|
| 177 |
+
`<p class="small">${esc(r.content)}</p>` +
|
| 178 |
+
(r.caveat ? `<p class="small"><strong>Cannot answer:</strong> ${esc(r.caveat)}</p>` : '') +
|
| 179 |
+
(r.citation ? `<p class="small">${esc(r.citation)}</p>` : '') +
|
| 180 |
+
'</div>';
|
| 181 |
+
}
|
| 182 |
+
html += '</details>';
|
| 183 |
+
|
| 184 |
+
if (reg.note) html += `<p class="note">${esc(reg.note)}</p>`;
|
| 185 |
+
return html;
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
/* Until then: the assemblies the windows themselves name, and nothing more. */
|
| 189 |
+
function renderPendingSources(host, windows) {
|
| 190 |
+
const rows = new Map();
|
| 191 |
+
for (const w of windows) {
|
| 192 |
+
const key = `${w.species_binomial}|${w.assembly}`;
|
| 193 |
+
rows.set(key, (rows.get(key) || 0) + 1);
|
| 194 |
+
}
|
| 195 |
+
const body = [...rows.entries()]
|
| 196 |
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
| 197 |
+
.map(([key, n]) => {
|
| 198 |
+
const [binomial, assembly] = key.split('|');
|
| 199 |
+
return `<tr><td><em>${esc(binomial)}</em></td><td class="num">${esc(assembly)}</td>` +
|
| 200 |
+
`<td class="n">${n}</td></tr>`;
|
| 201 |
+
}).join('');
|
| 202 |
+
host.innerHTML =
|
| 203 |
+
'<div class="tablewrap"><table><caption>The four annotated genomes, with the windows ' +
|
| 204 |
+
'each contributes. The <em>AT1G65170</em> window is cut from TAIR10 and carries the ' +
|
| 205 |
+
'Araport11 gene model the report\'s figure draws.</caption>' +
|
| 206 |
+
'<thead><tr><th>Species</th><th>Assembly</th><th class="n">Windows</th></tr></thead>' +
|
| 207 |
+
`<tbody>${body}</tbody></table></div>` +
|
| 208 |
+
'<p class="note">The annotation release, download URL, citation and licence behind each ' +
|
| 209 |
+
'of these assemblies are being verified and will be listed here.</p>';
|
| 210 |
+
}
|
| 211 |
+
})();
|
|
@@ -0,0 +1,745 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Sparse-feature window browser. Sequence, annotations and per-base activations
|
| 2 |
+
come from data/sae/<window>.json; every colour is a token so dark mode is free.
|
| 3 |
+
|
| 4 |
+
Two registers are kept apart on purpose. Measurements — activations read out of
|
| 5 |
+
the dictionary — are drawn in dark, saturated hues on the card. Annotations are
|
| 6 |
+
drawn inside a recessed panel, in pale washes and mid-tone ticks, so a glance
|
| 7 |
+
can never mistake an annotation for a number the model produced. */
|
| 8 |
+
|
| 9 |
+
/* ---------------------------------------------------------------- tokens --- */
|
| 10 |
+
:root {
|
| 11 |
+
/* Measurement palette. Twelve hues, one per available lane, so a feature keeps
|
| 12 |
+
one colour everywhere on the page and no two lanes ever share one. The first
|
| 13 |
+
six are the default lane set and are the furthest apart in hue *and*
|
| 14 |
+
lightness, which is what one-pixel bars need; slots 7-12 only appear once a
|
| 15 |
+
reader adds lanes by hand. */
|
| 16 |
+
--feat-1: #14539b; /* blue */
|
| 17 |
+
--feat-2: #c9490c; /* orange */
|
| 18 |
+
--feat-3: #0b7550; /* green */
|
| 19 |
+
--feat-4: #c0187c; /* magenta */
|
| 20 |
+
--feat-5: #94711b; /* gold */
|
| 21 |
+
--feat-6: #4a5162; /* slate */
|
| 22 |
+
--feat-7: #7629bb; /* violet */
|
| 23 |
+
--feat-8: #09708c; /* cyan */
|
| 24 |
+
--feat-9: #9c1734; /* crimson */
|
| 25 |
+
--feat-10: #56701c; /* olive */
|
| 26 |
+
--feat-11: #7a4a1e; /* umber */
|
| 27 |
+
--feat-12: #5c3a52; /* plum */
|
| 28 |
+
--feat-none: #ccd3ca;
|
| 29 |
+
|
| 30 |
+
/* Annotation register: pale region washes, mid-tone site ticks, a muted brown
|
| 31 |
+
for gene extents. Deliberately lower in chroma and darker only in the ticks,
|
| 32 |
+
which have to survive being one pixel wide. */
|
| 33 |
+
--ann-panel: #fafbf7;
|
| 34 |
+
--ann-panel-rule: #e2e7dd;
|
| 35 |
+
--ann-intergenic: #f0f1eb;
|
| 36 |
+
--ann-cds: #c7dcc2;
|
| 37 |
+
--ann-utr5: #d6cdec;
|
| 38 |
+
--ann-utr3: #eecdd7;
|
| 39 |
+
--ann-intron: #e7dfc6;
|
| 40 |
+
--ann-ncrna: #c6dee9;
|
| 41 |
+
--ann-tss: #3f77b8;
|
| 42 |
+
--ann-tis: #2f8878;
|
| 43 |
+
--ann-donor: #bb4a42;
|
| 44 |
+
--ann-acceptor: #8560ad;
|
| 45 |
+
--ann-stop: #bd7c2a;
|
| 46 |
+
--ann-tts: #6455a8;
|
| 47 |
+
--ann-gene: #9c8878;
|
| 48 |
+
--ann-gene-ink: #6d5348;
|
| 49 |
+
|
| 50 |
+
/* Sequence letters. --base-* is the readout's ink, on the card.
|
| 51 |
+
A letter in the strip sits on one of the twelve measurement hues, so each
|
| 52 |
+
base also has a light and a dark variant; the drawing code picks whichever
|
| 53 |
+
has more contrast against the cell it lands on. Both variants keep the
|
| 54 |
+
base's hue, so A/C/G/T stay identifiable either way. */
|
| 55 |
+
--base-a: #18813f; --base-c: #3b7fe0; --base-g: #b57405; --base-t: #d8433f; --base-n: #9aa4b0;
|
| 56 |
+
--base-a-lit: #9ce8b4; --base-c-lit: #b4daff; --base-g-lit: #ffd980;
|
| 57 |
+
--base-t-lit: #ffcbc4; --base-n-lit: #d8ded9;
|
| 58 |
+
--base-a-dim: #0a5a26; --base-c-dim: #14357e; --base-g-dim: #6b4a00;
|
| 59 |
+
--base-t-dim: #8a1010; --base-n-dim: #3b4247;
|
| 60 |
+
|
| 61 |
+
/* Figure furniture. */
|
| 62 |
+
--fig-grid: #dfe5dc;
|
| 63 |
+
--fig-axis: #b6c1b4;
|
| 64 |
+
/* Selected control. --ink and --lime both flip role between themes, so
|
| 65 |
+
ink-on-lime is a light-theme-only pairing and has to be tokenised. */
|
| 66 |
+
--sel-bg: var(--ink);
|
| 67 |
+
--sel-fg: var(--lime);
|
| 68 |
+
/* Picker chrome only: the accent shared by the three kinds where the features
|
| 69 |
+
and the annotation do not simply agree. Not used inside the plot, so it
|
| 70 |
+
cannot be confused with either register. */
|
| 71 |
+
--sel-flag: #b4571f;
|
| 72 |
+
--sel-quiet: #6f9264;
|
| 73 |
+
/* Annotation label ink: quiet enough to stay in the paler register, dark
|
| 74 |
+
enough to read on every region wash. */
|
| 75 |
+
--ann-ink: var(--ink-2);
|
| 76 |
+
}
|
| 77 |
+
@media (prefers-color-scheme: dark) {
|
| 78 |
+
:root:not([data-theme="light"]) {
|
| 79 |
+
--feat-1: #74aef0; --feat-2: #f79055; --feat-3: #45cba0; --feat-4: #f68ac4;
|
| 80 |
+
--feat-5: #dcb84a; --feat-6: #a9b0c2; --feat-7: #c193ef; --feat-8: #55c9e6;
|
| 81 |
+
--feat-9: #f0808f; --feat-10: #b2cd5f;
|
| 82 |
+
--feat-11: #d3a06b; --feat-12: #c49bbb;
|
| 83 |
+
--feat-none: #3a423b;
|
| 84 |
+
--ann-panel: #1a201c;
|
| 85 |
+
--ann-panel-rule: #2b332c;
|
| 86 |
+
--ann-intergenic: #202724;
|
| 87 |
+
--ann-cds: #31462f; --ann-utr5: #3a3150; --ann-utr3: #4a323d;
|
| 88 |
+
--ann-intron: #423b28; --ann-ncrna: #26414c;
|
| 89 |
+
--ann-tss: #6ea6dd; --ann-tis: #55b09c; --ann-donor: #de7d72;
|
| 90 |
+
--ann-acceptor: #a98cd2; --ann-stop: #d9a05a; --ann-tts: #9186d6;
|
| 91 |
+
--ann-gene: #a8917f; --ann-gene-ink: #c9b6a4;
|
| 92 |
+
--base-a: #5cc47f; --base-c: #7fb0ea; --base-g: #e8b452; --base-t: #f0847f; --base-n: #7c848d;
|
| 93 |
+
--fig-grid: #262d27; --fig-axis: #414a42;
|
| 94 |
+
--sel-bg: var(--lime); --sel-fg: #10160f;
|
| 95 |
+
--sel-flag: #d9944f; --sel-quiet: #6f9264;
|
| 96 |
+
--sel-flag: #d9944f; --sel-quiet: #6f9264;
|
| 97 |
+
--ann-ink: var(--ink-2);
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
:root[data-theme="dark"] {
|
| 101 |
+
--feat-1: #74aef0; --feat-2: #f79055; --feat-3: #45cba0; --feat-4: #f68ac4;
|
| 102 |
+
--feat-5: #dcb84a; --feat-6: #a9b0c2; --feat-7: #c193ef; --feat-8: #55c9e6;
|
| 103 |
+
--feat-9: #f0808f; --feat-10: #b2cd5f;
|
| 104 |
+
--feat-11: #d3a06b; --feat-12: #c49bbb;
|
| 105 |
+
--feat-none: #3a423b;
|
| 106 |
+
--ann-panel: #1a201c;
|
| 107 |
+
--ann-panel-rule: #2b332c;
|
| 108 |
+
--ann-intergenic: #202724;
|
| 109 |
+
--ann-cds: #31462f; --ann-utr5: #3a3150; --ann-utr3: #4a323d;
|
| 110 |
+
--ann-intron: #423b28; --ann-ncrna: #26414c;
|
| 111 |
+
--ann-tss: #6ea6dd; --ann-tis: #55b09c; --ann-donor: #de7d72;
|
| 112 |
+
--ann-acceptor: #a98cd2; --ann-stop: #d9a05a; --ann-tts: #9186d6;
|
| 113 |
+
--ann-gene: #a8917f; --ann-gene-ink: #c9b6a4;
|
| 114 |
+
--base-a: #5cc47f; --base-c: #7fb0ea; --base-g: #e8b452; --base-t: #f0847f; --base-n: #7c848d;
|
| 115 |
+
--fig-grid: #262d27; --fig-axis: #414a42;
|
| 116 |
+
--sel-bg: var(--lime); --sel-fg: #10160f;
|
| 117 |
+
--ann-ink: var(--ink-2);
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
/* ------------------------------------------------------- the browser shell --- */
|
| 121 |
+
/* The stage is the positioning context for the two movable surfaces, so they
|
| 122 |
+
travel with the figure when the page scrolls instead of following the reader. */
|
| 123 |
+
.feat-stage { position: relative; margin: 34px 0 26px; }
|
| 124 |
+
.feat-stage > .feat-browser { margin: 0; }
|
| 125 |
+
.feat-float { position: absolute; z-index: 4; }
|
| 126 |
+
/* room beside the canvas for the lanes dock, when the page is wide enough */
|
| 127 |
+
@media (min-width: 1081px) {
|
| 128 |
+
.feat-stage.stage-docked { padding-right: 350px; }
|
| 129 |
+
}
|
| 130 |
+
.feat-float[hidden] { display: none !important; }
|
| 131 |
+
|
| 132 |
+
.feat-browser {
|
| 133 |
+
/* the figure narrows when the lanes dock takes a gutter, so its own blocks
|
| 134 |
+
respond to its width rather than the viewport's */
|
| 135 |
+
container-type: inline-size;
|
| 136 |
+
margin: 34px 0 26px;
|
| 137 |
+
border: 1px solid var(--rule-2);
|
| 138 |
+
border-radius: 12px;
|
| 139 |
+
background: var(--card);
|
| 140 |
+
overflow: hidden;
|
| 141 |
+
font-family: var(--sans);
|
| 142 |
+
}
|
| 143 |
+
.feat-heading {
|
| 144 |
+
display: flex; justify-content: space-between; align-items: flex-end;
|
| 145 |
+
gap: 14px 20px; flex-wrap: wrap;
|
| 146 |
+
padding: 17px 24px 15px;
|
| 147 |
+
border-bottom: 1px solid var(--rule);
|
| 148 |
+
background: var(--sage);
|
| 149 |
+
}
|
| 150 |
+
.feat-heading > div:first-child { min-width: 0; }
|
| 151 |
+
.feat-browser h2 {
|
| 152 |
+
margin: 0 0 6px; font-size: 21px; line-height: 1.18; letter-spacing: -0.02em; max-width: 34ch;
|
| 153 |
+
}
|
| 154 |
+
.feat-browser h2 i, .feat-browser h2 em { font-style: italic; font-weight: 600; }
|
| 155 |
+
.feat-browser h2 .ft-dim { color: var(--ink-2); font-weight: 600; }
|
| 156 |
+
.feat-browser h2 .ft-kind {
|
| 157 |
+
display: inline-block; vertical-align: 3px;
|
| 158 |
+
font: 600 9.5px var(--mono); letter-spacing: 0.12em; text-transform: uppercase;
|
| 159 |
+
color: var(--ink-2); background: var(--card);
|
| 160 |
+
border: 1px solid var(--rule-2); border-left: 3px solid var(--sel-quiet); border-radius: 4px;
|
| 161 |
+
padding: 2px 7px; margin-left: 6px; white-space: nowrap;
|
| 162 |
+
}
|
| 163 |
+
.feat-browser h2 .ft-kind[data-flag="1"] { border-left-color: var(--sel-flag); color: var(--ink); }
|
| 164 |
+
.feat-heading > div:first-child > p {
|
| 165 |
+
margin: 0; color: var(--ink-2); font-size: 12px; line-height: 1.5;
|
| 166 |
+
display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 9px;
|
| 167 |
+
}
|
| 168 |
+
.feat-heading > div:first-child > p .species { font-style: italic; }
|
| 169 |
+
.feat-heading > div:first-child > p .uid { font-family: var(--mono); font-size: 11px; color: var(--ink-2); }
|
| 170 |
+
.feat-heading > div:first-child > p .sep { color: var(--rule-2); }
|
| 171 |
+
.feat-heading > div:first-child > p .split {
|
| 172 |
+
font-family: var(--mono); font-size: 10px; letter-spacing: 0.06em; color: var(--ink-3);
|
| 173 |
+
border: 1px solid var(--rule-2); border-radius: 3px; padding: 1px 5px;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.feat-tools { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
| 177 |
+
.feat-tools button {
|
| 178 |
+
font: 600 12px var(--sans); color: var(--ink-2); background: var(--card);
|
| 179 |
+
border: 1px solid var(--rule-2); border-radius: 6px; padding: 7px 11px;
|
| 180 |
+
cursor: pointer; min-height: 34px;
|
| 181 |
+
}
|
| 182 |
+
.feat-tools button.zoom { font-size: 17px; min-width: 36px; padding: 3px 9px; }
|
| 183 |
+
.feat-tools button:hover:not(:disabled) { background: var(--lime-2); color: var(--ink); border-color: var(--lime); }
|
| 184 |
+
.feat-tools button:disabled { opacity: .4; cursor: default; }
|
| 185 |
+
|
| 186 |
+
/* the coordinate bar between the heading and the plot */
|
| 187 |
+
.feat-browser .feat-region {
|
| 188 |
+
margin: 0; padding: 11px 24px 3px; max-width: none;
|
| 189 |
+
font: 11px var(--sans); color: var(--ink-2);
|
| 190 |
+
display: flex; flex-wrap: wrap; align-items: baseline; gap: 3px 16px;
|
| 191 |
+
}
|
| 192 |
+
.feat-region .fr-coord {
|
| 193 |
+
font: 500 12.5px var(--mono); color: var(--ink); font-variant-numeric: tabular-nums;
|
| 194 |
+
letter-spacing: -0.01em;
|
| 195 |
+
}
|
| 196 |
+
.feat-region .fr-item { color: var(--ink-3); display: inline-flex; gap: 5px; align-items: baseline; }
|
| 197 |
+
.feat-region .fr-item b { font-weight: 500; color: var(--ink-2); font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
| 198 |
+
.feat-region .fr-key { text-transform: uppercase; letter-spacing: 0.1em; font-size: 9.5px; font-family: var(--mono); }
|
| 199 |
+
|
| 200 |
+
.feat-scroll { overflow-x: hidden; padding: 2px 14px 0; }
|
| 201 |
+
#feat-plot { min-width: 0; }
|
| 202 |
+
#feat-plot svg { display: block; width: 100%; height: auto; touch-action: pan-y; cursor: crosshair; }
|
| 203 |
+
|
| 204 |
+
/* ------------------------------------------------------------- plot marks --- */
|
| 205 |
+
#feat-plot text { fill: var(--ink-2); font: 10.5px var(--mono); }
|
| 206 |
+
#feat-plot .ruler-tick { font: 10px var(--mono); fill: var(--ink-3); font-variant-numeric: tabular-nums; }
|
| 207 |
+
#feat-plot .axis { stroke: var(--fig-axis); stroke-width: .8; fill: none; }
|
| 208 |
+
#feat-plot .grid { stroke: var(--fig-grid); stroke-width: .7; fill: none; }
|
| 209 |
+
|
| 210 |
+
/* register labels: two tiny keys in the gutter that name what each block is */
|
| 211 |
+
#feat-plot .reg-lab { font: 500 9px var(--mono); letter-spacing: 0.13em; fill: var(--ink-3); }
|
| 212 |
+
#feat-plot .reg-rule { stroke: var(--rule); stroke-width: .8; }
|
| 213 |
+
|
| 214 |
+
/* the headline strip */
|
| 215 |
+
#feat-plot .strip-frame { fill: none; stroke: var(--fig-axis); stroke-width: .8; }
|
| 216 |
+
#feat-plot .seqletter { font: 600 10px var(--mono); }
|
| 217 |
+
#feat-plot .nofire { fill: var(--feat-none); }
|
| 218 |
+
|
| 219 |
+
/* the annotation panel. A recessed plate keeps annotations in their own register. */
|
| 220 |
+
#feat-plot .ann-panel { fill: var(--ann-panel); stroke: var(--ann-panel-rule); stroke-width: .8; }
|
| 221 |
+
#feat-plot .ann-band { }
|
| 222 |
+
#feat-plot .ann-band-rule { stroke: var(--ann-panel-rule); stroke-width: .7; fill: none; }
|
| 223 |
+
#feat-plot .ann-sitelab { font: 9px var(--sans); fill: var(--ink-3); }
|
| 224 |
+
#feat-plot .ann-bandlab { font: 500 8.5px var(--sans); fill: var(--ann-ink); }
|
| 225 |
+
#feat-plot .gene-line { stroke: var(--ann-gene); stroke-width: 1.1; fill: none; }
|
| 226 |
+
#feat-plot .gene-label { font: italic 10px var(--sans); fill: var(--ann-gene-ink); }
|
| 227 |
+
#feat-plot .gene-exon { fill: var(--ann-cds); stroke: var(--ann-gene); stroke-width: .7; }
|
| 228 |
+
#feat-plot .strand-name { font: 500 10px var(--sans); fill: var(--ink-2); letter-spacing: 0.01em; }
|
| 229 |
+
|
| 230 |
+
/* lanes: a measurement register, so names are ink and the hue lives in a key bar */
|
| 231 |
+
#feat-plot .lane-key { stroke-linecap: round; }
|
| 232 |
+
#feat-plot .lane-name { font: 600 11px var(--sans); fill: var(--ink); }
|
| 233 |
+
#feat-plot .lane-name.lane-id-name { font-family: var(--mono); font-size: 11px; letter-spacing: -0.01em; }
|
| 234 |
+
#feat-plot .lane-id { font: 500 10px var(--mono); fill: var(--ink-3); }
|
| 235 |
+
#feat-plot .lane-max { font: 500 10px var(--mono); fill: var(--ink-2); font-variant-numeric: tabular-nums; }
|
| 236 |
+
#feat-plot .lane-max .u { fill: var(--ink-3); font-weight: 400; }
|
| 237 |
+
#feat-plot .lane-silent { font: italic 10px var(--sans); fill: var(--ink-3); }
|
| 238 |
+
#feat-plot .lane-base { stroke: var(--fig-axis); stroke-width: .8; fill: none; }
|
| 239 |
+
#feat-plot .lane-base.silent { stroke: var(--rule); stroke-dasharray: 2 3; }
|
| 240 |
+
#feat-plot .lane-stems { fill: none; stroke-width: 1.05; }
|
| 241 |
+
#feat-plot .reg-note { font: 9.5px var(--sans); fill: var(--ink-3); }
|
| 242 |
+
|
| 243 |
+
/* cursor and pin */
|
| 244 |
+
#feat-plot .cursor { stroke: var(--ink-3); stroke-width: 1; fill: none; }
|
| 245 |
+
#feat-plot .pin { stroke: var(--ink); stroke-width: 1.3; fill: none; }
|
| 246 |
+
#feat-plot .pin-cap { fill: var(--ink); stroke: none; }
|
| 247 |
+
|
| 248 |
+
/* The anchor is where the sampler looked, not an annotation of that base, so it
|
| 249 |
+
is outlined, never filled, and it is labelled as its own thing. */
|
| 250 |
+
#feat-plot .anchor { fill: none; stroke: var(--ink); stroke-width: 1.2; stroke-dasharray: 2 2; }
|
| 251 |
+
#feat-plot .anchor-lab { font: 500 9px var(--mono); letter-spacing: 0.1em; fill: var(--ink-2); }
|
| 252 |
+
/* the base a window was chosen for: solid, to pair with the anchor's dashed outline */
|
| 253 |
+
#feat-plot .finding { fill: var(--ink); stroke: none; }
|
| 254 |
+
#feat-plot .finding.open { fill: var(--card); stroke: var(--ink); stroke-width: 1.2; }
|
| 255 |
+
#feat-plot .finding-bracket { fill: none; stroke: var(--ink); stroke-width: 1; }
|
| 256 |
+
#feat-plot .finding-lab { font: 500 9px var(--mono); letter-spacing: 0.08em; fill: var(--ink); }
|
| 257 |
+
|
| 258 |
+
/* halo, for the labels that sit over the plot at narrow widths */
|
| 259 |
+
#feat-plot .halo { paint-order: stroke; stroke: var(--card); stroke-width: 3.4px; stroke-linejoin: round; }
|
| 260 |
+
#feat-plot .halo-panel { paint-order: stroke; stroke: var(--ann-panel); stroke-width: 3.4px; stroke-linejoin: round; }
|
| 261 |
+
|
| 262 |
+
/* scale bar: a bracket the width of one round number of bases */
|
| 263 |
+
#feat-plot .scalebar { stroke: var(--ink-3); stroke-width: 1; fill: none; }
|
| 264 |
+
#feat-plot .scale-pill { fill: var(--paper-2); stroke: var(--rule-2); stroke-width: .8; }
|
| 265 |
+
#feat-plot .scale-lab { font: 500 9.5px var(--mono); fill: var(--ink-2); letter-spacing: 0.02em; }
|
| 266 |
+
|
| 267 |
+
/* the global-mode key, shown only when the strip reads the whole dictionary */
|
| 268 |
+
#feat-plot .gkey-id { font: 500 9.5px var(--mono); fill: var(--ink-2); }
|
| 269 |
+
|
| 270 |
+
/* ---------------------------------------------------------------- readout --- */
|
| 271 |
+
.feat-readout {
|
| 272 |
+
display: grid;
|
| 273 |
+
grid-template-columns: minmax(140px, 0.75fr) minmax(160px, 1fr) minmax(250px, 1.55fr);
|
| 274 |
+
gap: 0;
|
| 275 |
+
min-height: 104px;
|
| 276 |
+
padding: 0;
|
| 277 |
+
border-top: 1px solid var(--rule);
|
| 278 |
+
background: var(--paper-2);
|
| 279 |
+
font-family: var(--sans);
|
| 280 |
+
color: var(--ink-2);
|
| 281 |
+
}
|
| 282 |
+
.feat-readout .ro-block { padding: 12px 20px 13px; min-width: 0; }
|
| 283 |
+
.feat-readout .ro-block + .ro-block { border-left: 1px solid var(--rule); }
|
| 284 |
+
.feat-readout .ro-lab {
|
| 285 |
+
font: 500 9.5px var(--mono); letter-spacing: 0.14em; text-transform: uppercase;
|
| 286 |
+
color: var(--ink-3); margin: 0 0 7px; display: flex; align-items: baseline; gap: 8px;
|
| 287 |
+
}
|
| 288 |
+
.feat-readout .ro-lab > span:not(.pinned) {
|
| 289 |
+
text-transform: none; letter-spacing: 0.02em; font-family: var(--sans);
|
| 290 |
+
font-size: 10px; color: var(--ink-3);
|
| 291 |
+
}
|
| 292 |
+
.feat-readout .ro-lab .pinned {
|
| 293 |
+
text-transform: none; letter-spacing: 0.04em; color: var(--ink);
|
| 294 |
+
background: var(--lime); border-radius: 3px; padding: 1px 5px; font-size: 9px;
|
| 295 |
+
}
|
| 296 |
+
.feat-readout .ro-rows { display: grid; grid-template-columns: auto 1fr; gap: 3px 12px; align-items: baseline; }
|
| 297 |
+
.feat-readout .k { font: 11px var(--sans); color: var(--ink-3); }
|
| 298 |
+
.feat-readout .v { font: 500 12px var(--mono); color: var(--ink); font-variant-numeric: tabular-nums; }
|
| 299 |
+
.feat-readout .v.dim { color: var(--ink-3); font-weight: 400; }
|
| 300 |
+
.feat-readout .seqbase { font: 700 14px var(--mono); letter-spacing: 0.06em; }
|
| 301 |
+
.feat-readout .ro-ann .ro-rows { grid-template-columns: auto 1fr; }
|
| 302 |
+
.feat-readout .ro-ann .v { font-family: var(--sans); font-size: 11.5px; font-weight: 500; }
|
| 303 |
+
.feat-readout .achip {
|
| 304 |
+
display: inline-flex; align-items: center; gap: 5px;
|
| 305 |
+
font: 500 11px var(--sans); color: var(--ink);
|
| 306 |
+
}
|
| 307 |
+
.feat-readout .achip i { width: 9px; height: 9px; border-radius: 2px; border: 1px solid color-mix(in srgb, var(--ink) 16%, transparent); flex: none; }
|
| 308 |
+
.feat-readout .achip i.site { width: 3px; height: 11px; border-radius: 0; border: 0; }
|
| 309 |
+
.feat-readout .gname { font-style: italic; color: var(--ink-2); font-size: 11px; }
|
| 310 |
+
.feat-readout .ro-act .rows { display: grid; grid-template-columns: 10px auto minmax(0, 1fr) auto; gap: 2px 9px; align-items: baseline; }
|
| 311 |
+
.feat-readout .ro-act .chip { width: 9px; height: 9px; border-radius: 2px; align-self: center; }
|
| 312 |
+
.feat-readout .ro-act .fid { font: 600 11px var(--mono); color: var(--ink); }
|
| 313 |
+
.feat-readout .ro-act .fnm { font: 11.5px var(--sans); color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 314 |
+
.feat-readout .ro-act .fv { font: 600 12px var(--mono); color: var(--ink); font-variant-numeric: tabular-nums; }
|
| 315 |
+
.feat-readout .ro-foot { margin-top: 8px; font: 11px var(--sans); color: var(--ink-3); }
|
| 316 |
+
.feat-readout .ro-foot b { font: 600 11px var(--mono); color: var(--ink-2); }
|
| 317 |
+
.feat-readout .ro-silent { margin-top: 7px; font: 11px var(--sans); color: var(--ink-3); }
|
| 318 |
+
.feat-readout .ro-silent code {
|
| 319 |
+
background: none; padding: 0; font-family: var(--mono); font-size: 10.5px; color: var(--ink-3);
|
| 320 |
+
}
|
| 321 |
+
.feat-readout .ro-empty { font: italic 11.5px var(--sans); color: var(--ink-3); }
|
| 322 |
+
.feat-readout .ro-fail { grid-column: 1 / -1; }
|
| 323 |
+
|
| 324 |
+
/* ------------------------------------------------------------- figcaption --- */
|
| 325 |
+
.feat-browser figcaption {
|
| 326 |
+
margin: 0; padding: 11px 24px 12px; max-width: none;
|
| 327 |
+
display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
|
| 328 |
+
gap: 6px 24px;
|
| 329 |
+
font: 11.5px var(--sans); color: var(--ink-2);
|
| 330 |
+
border-top: 1px solid var(--rule);
|
| 331 |
+
background: var(--card);
|
| 332 |
+
}
|
| 333 |
+
.feat-browser figcaption a { color: var(--ink-2); }
|
| 334 |
+
.feat-browser figcaption #feat-prov { color: var(--ink-3); }
|
| 335 |
+
.feat-browser figcaption > span:last-child { color: var(--ink-3); }
|
| 336 |
+
.feat-browser .feat-fallback { margin: 18px 24px; font-size: 14px; font-family: var(--serif); }
|
| 337 |
+
|
| 338 |
+
/* ------------------------------------------------------------- lanes dock --- */
|
| 339 |
+
.feat-dock {
|
| 340 |
+
width: 336px; max-width: calc(100% - 24px);
|
| 341 |
+
background: var(--card); color: var(--ink);
|
| 342 |
+
border: 1px solid var(--rule-2); border-radius: 10px;
|
| 343 |
+
box-shadow: 0 8px 26px color-mix(in srgb, var(--ink) 16%, transparent);
|
| 344 |
+
font-family: var(--sans);
|
| 345 |
+
display: flex; flex-direction: column;
|
| 346 |
+
overflow: hidden;
|
| 347 |
+
}
|
| 348 |
+
#feat-controls .dock-title {
|
| 349 |
+
margin: 0; max-width: none; flex: none;
|
| 350 |
+
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
| 351 |
+
padding: 8px 8px 8px 13px;
|
| 352 |
+
font: 700 12.5px var(--sans); letter-spacing: -0.01em; color: var(--ink);
|
| 353 |
+
background: var(--sage); border-bottom: 1px solid var(--rule);
|
| 354 |
+
cursor: grab; touch-action: none; user-select: none;
|
| 355 |
+
}
|
| 356 |
+
#feat-controls .dock-title.dragging { cursor: grabbing; }
|
| 357 |
+
.dock-close {
|
| 358 |
+
font: 400 16px/1 var(--sans); color: var(--ink-2); background: none; border: 0;
|
| 359 |
+
border-radius: 5px; width: 26px; height: 26px; flex: none; cursor: pointer;
|
| 360 |
+
}
|
| 361 |
+
.dock-close:hover { background: var(--paper-2); color: var(--ink); }
|
| 362 |
+
.dock-body { padding: 4px 13px 12px; overflow-y: auto; min-height: 0; }
|
| 363 |
+
#feat-controls .label { margin: 12px 0 6px; max-width: none; }
|
| 364 |
+
#feat-controls .label:first-child { margin-top: 8px; }
|
| 365 |
+
#feat-controls .seg { flex-wrap: wrap; }
|
| 366 |
+
#feat-controls .seg button { font-size: 12px; padding: 6px 10px; }
|
| 367 |
+
#feat-controls #feat-list { margin: 0; }
|
| 368 |
+
#feat-controls .fl-rows { grid-template-columns: 1fr; gap: 0; }
|
| 369 |
+
#feat-controls-toggle[aria-expanded="true"] { background: var(--sel-bg); color: var(--sel-fg); border-color: var(--sel-bg); }
|
| 370 |
+
|
| 371 |
+
/* ---------------------------------------------------------- window picker --- */
|
| 372 |
+
.win-picker { margin: 16px 0 10px; display: grid; gap: 14px; max-width: 900px; }
|
| 373 |
+
#win-picker.win-wide { max-width: 1120px; gap: 10px; }
|
| 374 |
+
|
| 375 |
+
.win-hero {
|
| 376 |
+
display: grid; grid-template-columns: auto 1fr; align-items: center; gap: 4px 14px;
|
| 377 |
+
text-align: left; width: 100%;
|
| 378 |
+
font: inherit; cursor: pointer;
|
| 379 |
+
background: var(--card); color: var(--ink);
|
| 380 |
+
border: 1px solid var(--rule-2); border-left: 4px solid var(--lime); border-radius: 8px;
|
| 381 |
+
padding: 10px 15px; max-width: 540px;
|
| 382 |
+
}
|
| 383 |
+
.win-hero:hover { background: var(--lime-2); border-color: var(--lime); }
|
| 384 |
+
.win-hero[aria-checked="true"] { background: var(--sage-2); border-color: var(--rule-2); border-left-width: 6px; }
|
| 385 |
+
.win-hero[aria-checked="true"] .wh-tag { color: var(--ink-2); }
|
| 386 |
+
.win-hero .wh-tag {
|
| 387 |
+
grid-row: 1 / span 2; align-self: center;
|
| 388 |
+
font: 500 9.5px var(--mono); letter-spacing: 0.13em; color: var(--ink-3);
|
| 389 |
+
writing-mode: horizontal-tb;
|
| 390 |
+
}
|
| 391 |
+
.win-hero .wh-name { font: 700 15px var(--sans); letter-spacing: -0.01em; }
|
| 392 |
+
.win-hero .wh-sub { font: 12px var(--sans); color: var(--ink-2); }
|
| 393 |
+
.win-hero .wh-sub i { font-style: italic; }
|
| 394 |
+
|
| 395 |
+
/* selection-shaped picker: species blocks of cards labelled by what the window
|
| 396 |
+
shows, used as soon as the window files carry a `selection` field */
|
| 397 |
+
.win-block + .win-block { margin-top: 4px; }
|
| 398 |
+
#win-picker .win-sp {
|
| 399 |
+
display: flex; align-items: baseline; gap: 9px; margin: 0 0 6px; max-width: none;
|
| 400 |
+
border-top: 1px solid var(--rule); padding-top: 8px;
|
| 401 |
+
}
|
| 402 |
+
#win-picker .win-block:first-of-type .win-sp { border-top: 0; padding-top: 0; }
|
| 403 |
+
#win-picker .win-sp .b { font: italic 600 13px var(--sans); color: var(--ink); }
|
| 404 |
+
#win-picker .win-sp .f { font: 10px var(--mono); color: var(--ink-3); letter-spacing: 0.04em; }
|
| 405 |
+
.win-list { display: grid; grid-template-columns: repeat(var(--wl-cols, 5), minmax(0, 1fr)); gap: 7px; }
|
| 406 |
+
.win-card {
|
| 407 |
+
display: grid; gap: 2px; text-align: left; font: inherit; cursor: pointer;
|
| 408 |
+
background: var(--card); color: var(--ink);
|
| 409 |
+
border: 1px solid var(--rule-2); border-left: 3px solid var(--rule-2); border-radius: 7px;
|
| 410 |
+
padding: 8px 11px;
|
| 411 |
+
}
|
| 412 |
+
.win-card[data-flag="0"] { border-left-color: var(--sel-quiet); }
|
| 413 |
+
.win-card[data-flag="1"] { border-left-color: var(--sel-flag); }
|
| 414 |
+
.win-card:hover { background: var(--lime-2); border-color: var(--lime); }
|
| 415 |
+
.win-card[aria-checked="true"] { background: var(--sage-2); border-color: var(--rule-2); border-left-width: 6px; }
|
| 416 |
+
.win-card .wk {
|
| 417 |
+
font: 500 8.5px var(--mono); letter-spacing: 0.13em; text-transform: uppercase; color: var(--ink-3);
|
| 418 |
+
}
|
| 419 |
+
.win-card[data-flag="1"] .wk { color: var(--ink-2); font-weight: 600; }
|
| 420 |
+
.win-card .wn { font: 600 13px var(--sans); letter-spacing: -0.01em; }
|
| 421 |
+
.win-card .wh { font: 11.5px var(--sans); color: var(--ink-2); line-height: 1.35; }
|
| 422 |
+
.win-card .wm {
|
| 423 |
+
font: 10.5px var(--mono); color: var(--ink-3); line-height: 1.4; letter-spacing: -0.01em;
|
| 424 |
+
overflow-wrap: anywhere;
|
| 425 |
+
}
|
| 426 |
+
.win-card .wu { font: 10px var(--mono); color: var(--ink-3); letter-spacing: -0.02em; }
|
| 427 |
+
|
| 428 |
+
.win-matrix {
|
| 429 |
+
display: grid;
|
| 430 |
+
grid-template-columns: max-content repeat(var(--wm-cols, 5), minmax(0, 1fr));
|
| 431 |
+
gap: 5px 6px;
|
| 432 |
+
align-items: stretch;
|
| 433 |
+
}
|
| 434 |
+
.win-matrix .wm-corner { }
|
| 435 |
+
.win-matrix .wm-head {
|
| 436 |
+
font: 500 9.5px var(--mono); letter-spacing: 0.12em; text-transform: uppercase;
|
| 437 |
+
color: var(--ink-3); padding: 0 0 2px 2px; align-self: end;
|
| 438 |
+
}
|
| 439 |
+
.win-matrix .wm-sp {
|
| 440 |
+
display: grid; align-content: center; padding: 0 14px 0 2px; min-width: 0;
|
| 441 |
+
}
|
| 442 |
+
.win-matrix .wm-sp .b { font: italic 600 12.5px var(--sans); color: var(--ink); }
|
| 443 |
+
.win-matrix .wm-sp .f { font: 10px var(--mono); color: var(--ink-3); letter-spacing: 0.04em; }
|
| 444 |
+
.win-matrix .wm-cell {
|
| 445 |
+
font: inherit; cursor: pointer; text-align: left;
|
| 446 |
+
display: grid; align-content: center; gap: 1px;
|
| 447 |
+
background: var(--card); color: var(--ink);
|
| 448 |
+
border: 1px solid var(--rule-2); border-radius: 6px;
|
| 449 |
+
padding: 7px 9px; min-height: 38px;
|
| 450 |
+
}
|
| 451 |
+
.win-matrix .wm-cell:hover { background: var(--lime-2); border-color: var(--lime); }
|
| 452 |
+
.win-matrix .wm-cell[aria-checked="true"] { background: var(--sel-bg); border-color: var(--sel-bg); }
|
| 453 |
+
.win-matrix .wm-cell .wc-uid { font: 500 10.5px var(--mono); color: var(--ink-2); letter-spacing: -0.02em; }
|
| 454 |
+
.win-matrix .wm-cell .wc-anchor { display: none; font: 600 11.5px var(--sans); }
|
| 455 |
+
.win-matrix .wm-cell[aria-checked="true"] .wc-uid,
|
| 456 |
+
.win-matrix .wm-cell[aria-checked="true"] .wc-anchor { color: var(--sel-fg); }
|
| 457 |
+
/* site.css pairs ink-on-lime for a pressed segmented control, which inverts to
|
| 458 |
+
an illegible 1.1:1 in dark; the token pair restores it. */
|
| 459 |
+
#feat-mode button[aria-pressed="true"] { background: var(--sel-bg); color: var(--sel-fg); }
|
| 460 |
+
.win-matrix .wm-empty { border: 1px dashed var(--rule); border-radius: 6px; min-height: 38px; }
|
| 461 |
+
|
| 462 |
+
/* --------------------------------------------------------- feature picker --- */
|
| 463 |
+
#feat-list { margin: 16px 0 8px; }
|
| 464 |
+
.fl-bar {
|
| 465 |
+
display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline;
|
| 466 |
+
gap: 8px 16px; padding-bottom: 7px; border-bottom: 1px solid var(--rule-2);
|
| 467 |
+
}
|
| 468 |
+
.fl-count { font: 12px var(--sans); color: var(--ink-3); }
|
| 469 |
+
.fl-count b { font: 600 13px var(--sans); color: var(--ink); font-variant-numeric: tabular-nums; }
|
| 470 |
+
.fl-count .full { color: var(--ink-2); }
|
| 471 |
+
.fl-reset {
|
| 472 |
+
font: 600 11.5px var(--sans); color: var(--ink-2); background: var(--card);
|
| 473 |
+
border: 1px solid var(--rule-2); border-radius: 999px; padding: 5px 12px; cursor: pointer;
|
| 474 |
+
}
|
| 475 |
+
.fl-reset:hover { background: var(--lime-2); border-color: var(--lime); color: var(--ink); }
|
| 476 |
+
|
| 477 |
+
.fl-group { margin-top: 16px; }
|
| 478 |
+
#feat-list .fl-group > .fl-h, #feat-list .fl-group > summary {
|
| 479 |
+
display: flex; align-items: baseline; gap: 9px;
|
| 480 |
+
font: 700 13px var(--sans); color: var(--ink); letter-spacing: -0.01em;
|
| 481 |
+
margin: 0 0 6px; list-style: none;
|
| 482 |
+
}
|
| 483 |
+
.fl-group > summary { cursor: pointer; }
|
| 484 |
+
.fl-group > summary::-webkit-details-marker { display: none; }
|
| 485 |
+
.fl-group > summary::before {
|
| 486 |
+
content: "";
|
| 487 |
+
width: 0; height: 0; flex: none; align-self: center;
|
| 488 |
+
border-left: 5px solid var(--ink-2); border-top: 4px solid transparent; border-bottom: 4px solid transparent;
|
| 489 |
+
}
|
| 490 |
+
.fl-group[open] > summary::before { transform: rotate(90deg); }
|
| 491 |
+
.fl-group > .fl-h .n, .fl-group > summary .n {
|
| 492 |
+
font: 500 10.5px var(--mono); color: var(--ink-3);
|
| 493 |
+
border: 1px solid var(--rule); border-radius: 999px; padding: 1px 7px;
|
| 494 |
+
}
|
| 495 |
+
.fl-group > .fl-h .why, .fl-group > summary .why { font: 400 11.5px var(--sans); color: var(--ink-3); }
|
| 496 |
+
|
| 497 |
+
.fl-rows { display: grid; grid-template-columns: repeat(auto-fill, minmax(288px, 1fr)); gap: 0 28px; }
|
| 498 |
+
#feat-list .fl-sub {
|
| 499 |
+
grid-column: 1 / -1;
|
| 500 |
+
display: flex; align-items: center; gap: 9px;
|
| 501 |
+
font: 500 9.5px var(--mono); letter-spacing: 0.13em; text-transform: uppercase;
|
| 502 |
+
color: var(--ink-3); margin: 9px 0 1px;
|
| 503 |
+
}
|
| 504 |
+
.fl-sub::after { content: ""; flex: 1; height: 1px; background: var(--rule); }
|
| 505 |
+
|
| 506 |
+
.fl-row {
|
| 507 |
+
display: grid;
|
| 508 |
+
grid-template-columns: 15px auto minmax(0, 1fr) auto 40px;
|
| 509 |
+
align-items: center; gap: 0 9px;
|
| 510 |
+
padding: 5px 2px; cursor: pointer;
|
| 511 |
+
border-bottom: 1px solid color-mix(in srgb, var(--rule) 55%, transparent);
|
| 512 |
+
font-family: var(--sans);
|
| 513 |
+
}
|
| 514 |
+
.fl-row:hover { background: color-mix(in srgb, var(--paper-2) 70%, transparent); }
|
| 515 |
+
.fl-row input { margin: 0; accent-color: var(--ink); justify-self: center; }
|
| 516 |
+
.fl-row .fid { font: 500 11px var(--mono); color: var(--ink-2); }
|
| 517 |
+
.fl-row.on .fid { color: var(--ink-2); }
|
| 518 |
+
.fl-row .fname {
|
| 519 |
+
font: 12.5px var(--sans); color: var(--ink);
|
| 520 |
+
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
| 521 |
+
}
|
| 522 |
+
.fl-row.named .fname { font-weight: 600; }
|
| 523 |
+
.fl-row .fname .un { color: var(--ink-3); font-family: var(--mono); font-size: 10.5px; font-weight: 400; }
|
| 524 |
+
.fl-row .fpeak {
|
| 525 |
+
font: 500 11px var(--mono); color: var(--ink-2); font-variant-numeric: tabular-nums;
|
| 526 |
+
white-space: nowrap;
|
| 527 |
+
}
|
| 528 |
+
.fl-row .fpeak.zero { color: var(--ink-3); font-style: italic; font-family: var(--sans); font-size: 10.5px; }
|
| 529 |
+
.fl-row .fcov { height: 5px; background: color-mix(in srgb, var(--rule) 60%, transparent); border-radius: 3px; overflow: hidden; }
|
| 530 |
+
.fl-row .fcov i { display: block; height: 100%; background: var(--ink-3); border-radius: 3px; min-width: 1px; }
|
| 531 |
+
.fl-row.on .fcov i { background: var(--ink); }
|
| 532 |
+
.fl-row.silent { color: var(--ink-3); }
|
| 533 |
+
.fl-row.silent .fname { color: var(--ink-3); }
|
| 534 |
+
.fl-row input:disabled { cursor: not-allowed; }
|
| 535 |
+
.fl-row:has(input:disabled) { cursor: default; opacity: .55; }
|
| 536 |
+
.fl-row:has(input:disabled):hover { background: none; }
|
| 537 |
+
#feat-list .fl-note { margin: 10px 0 0; font: 11.5px var(--sans); color: var(--ink-3); }
|
| 538 |
+
|
| 539 |
+
/* ---------------------------------------------------------------- legends --- */
|
| 540 |
+
/* The annotation legend sits on the same recessed plate as the annotation panel
|
| 541 |
+
in the figure, so the reader learns the register once. Region washes fill the
|
| 542 |
+
first rows; the point sites are forced onto a fresh row so the two kinds of
|
| 543 |
+
annotation do not read as one undifferentiated run of chips. */
|
| 544 |
+
.ann-legend {
|
| 545 |
+
display: grid;
|
| 546 |
+
grid-template-columns: repeat(auto-fit, minmax(108px, max-content));
|
| 547 |
+
gap: 7px 20px;
|
| 548 |
+
margin: 20px 0 0;
|
| 549 |
+
padding: 27px 15px 12px;
|
| 550 |
+
border: 1px solid var(--rule); border-radius: 8px; background: var(--ann-panel);
|
| 551 |
+
font: 11.5px var(--sans); color: var(--ink-2);
|
| 552 |
+
position: relative;
|
| 553 |
+
}
|
| 554 |
+
.ann-legend::before {
|
| 555 |
+
content: "Annotation register — never a measurement";
|
| 556 |
+
position: absolute; top: 9px; left: 15px;
|
| 557 |
+
font: 500 9px var(--mono); letter-spacing: 0.13em; text-transform: uppercase; color: var(--ink-3);
|
| 558 |
+
}
|
| 559 |
+
.ann-legend span { display: inline-flex; align-items: center; gap: 7px; white-space: nowrap; }
|
| 560 |
+
.ann-legend i {
|
| 561 |
+
width: 15px; height: 11px; flex: none; display: inline-block;
|
| 562 |
+
border: 1px solid var(--ann-panel-rule); border-radius: 2px;
|
| 563 |
+
}
|
| 564 |
+
.ann-legend i.site { width: 3px; height: 13px; border: 0; border-radius: 0; }
|
| 565 |
+
/* the first point site starts a new row, whatever the column count */
|
| 566 |
+
.ann-legend span:nth-child(7) { grid-column-start: 1; }
|
| 567 |
+
|
| 568 |
+
/* ----------------------------------------------------------- narrow widths --- */
|
| 569 |
+
@media (max-width: 1020px) {
|
| 570 |
+
.feat-readout { grid-template-columns: minmax(150px, 0.9fr) minmax(0, 1.6fr); }
|
| 571 |
+
.feat-readout .ro-act { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--rule); }
|
| 572 |
+
}
|
| 573 |
+
@media (max-width: 860px) {
|
| 574 |
+
.win-matrix { grid-template-columns: repeat(var(--wm-cols, 5), minmax(0, 1fr)); gap: 5px; }
|
| 575 |
+
.win-matrix .wm-corner, .win-matrix .wm-head { display: none; }
|
| 576 |
+
.win-matrix .wm-sp {
|
| 577 |
+
grid-column: 1 / -1; padding: 8px 0 1px; display: flex; align-items: baseline; gap: 8px;
|
| 578 |
+
border-top: 1px solid var(--rule);
|
| 579 |
+
}
|
| 580 |
+
.win-matrix .wm-sp:first-of-type { border-top: 0; padding-top: 2px; }
|
| 581 |
+
.win-matrix .wm-cell { padding: 6px 6px; min-height: 40px; }
|
| 582 |
+
.win-matrix .wm-cell .wc-uid { display: none; }
|
| 583 |
+
.win-matrix .wm-cell .wc-anchor { display: block; font-size: 11px; line-height: 1.15; }
|
| 584 |
+
.ann-legend { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
| 585 |
+
}
|
| 586 |
+
@media (max-width: 700px) {
|
| 587 |
+
/* full-bleed the figure: at 420 px the page gutter is a third of the plot */
|
| 588 |
+
.feat-browser {
|
| 589 |
+
margin-left: -28px; margin-right: -28px; margin-top: 26px;
|
| 590 |
+
border-radius: 0; border-left: 0; border-right: 0;
|
| 591 |
+
}
|
| 592 |
+
.feat-heading { padding: 15px 18px 13px; gap: 12px; }
|
| 593 |
+
.feat-browser h2 { font-size: 19px; }
|
| 594 |
+
.feat-browser .feat-region { padding: 10px 18px 2px; gap: 2px 12px; }
|
| 595 |
+
.feat-region .fr-coord { font-size: 11.5px; }
|
| 596 |
+
.feat-scroll { padding: 2px 4px 0; }
|
| 597 |
+
.feat-readout { grid-template-columns: 1fr 1fr; min-height: 0; }
|
| 598 |
+
.feat-readout .ro-block { padding: 10px 15px; }
|
| 599 |
+
.feat-readout .ro-rows { gap: 2px 10px; }
|
| 600 |
+
.feat-readout .ro-ann { border-left: 1px solid var(--rule); }
|
| 601 |
+
.feat-readout .ro-act { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--rule); }
|
| 602 |
+
.feat-browser figcaption { padding: 11px 18px 12px; }
|
| 603 |
+
.feat-tools button { min-height: 40px; }
|
| 604 |
+
.feat-tools button.zoom { min-width: 44px; font-size: 19px; }
|
| 605 |
+
.fl-rows { grid-template-columns: 1fr; gap: 0; }
|
| 606 |
+
.fl-row { grid-template-columns: 17px auto minmax(0, 1fr) auto 32px; padding: 7px 2px; }
|
| 607 |
+
.win-hero { padding: 10px 13px; }
|
| 608 |
+
}
|
| 609 |
+
/* Between a phone and a wide screen the five cards of a species would wrap to a
|
| 610 |
+
second row, doubling the block's height; narrower columns keep one row. */
|
| 611 |
+
@media (max-width: 1080px) and (min-width: 621px) {
|
| 612 |
+
.win-list { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
|
| 613 |
+
.win-card { padding: 7px 9px; }
|
| 614 |
+
.win-card .wn { font-size: 12px; }
|
| 615 |
+
/* the tag already says what the headline says in three words, so at this width
|
| 616 |
+
the evidence line is the one worth the space */
|
| 617 |
+
.win-card .wh { display: none; }
|
| 618 |
+
.win-card .wm { font-size: 10px; }
|
| 619 |
+
}
|
| 620 |
+
@media (max-width: 620px) {
|
| 621 |
+
/* the reason a window earns its place is long prose; on a narrow screen it
|
| 622 |
+
stays in the button's title and aria-label instead of on the card */
|
| 623 |
+
#win-picker.win-wide { max-width: none; }
|
| 624 |
+
.win-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; }
|
| 625 |
+
.win-card { padding: 7px 9px; }
|
| 626 |
+
/* on a phone a card is the gene and the kind; the headline, the evidence and
|
| 627 |
+
the uid stay in its title and aria-label, and the note under the figure
|
| 628 |
+
carries the full reasoning once a window is chosen */
|
| 629 |
+
.win-card .wh, .win-card .wm, .win-card .wu { display: none; }
|
| 630 |
+
.win-card .wn { font-size: 12.5px; line-height: 1.2; }
|
| 631 |
+
.win-card .wk { font-size: 8px; }
|
| 632 |
+
/* five columns still fit if the anchor names are allowed to wrap */
|
| 633 |
+
.win-matrix { gap: 4px; }
|
| 634 |
+
.win-matrix .wm-cell { padding: 5px 5px; min-height: 42px; }
|
| 635 |
+
.win-matrix .wm-cell .wc-anchor { font-size: 10px; line-height: 1.15; white-space: normal; }
|
| 636 |
+
.win-hero { grid-template-columns: 1fr; gap: 1px; }
|
| 637 |
+
.win-hero .wh-tag { grid-row: auto; }
|
| 638 |
+
.win-hero .wh-asm { display: none; }
|
| 639 |
+
}
|
| 640 |
+
@media (max-width: 460px) {
|
| 641 |
+
.ann-legend { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px 12px; }
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
/* ------------------------------------------- floating top-features panel --- */
|
| 645 |
+
/* Reads the strongest features at the cursor, so a reader can find a feature
|
| 646 |
+
that has no lane yet and give it one. Floats over the page at wide widths,
|
| 647 |
+
docks under the readout when there is no room to float. */
|
| 648 |
+
#feat-panel-toggle[aria-expanded="true"] { background: var(--sel-bg); color: var(--sel-fg); border-color: var(--sel-bg); }
|
| 649 |
+
|
| 650 |
+
.fp {
|
| 651 |
+
z-index: 5;
|
| 652 |
+
width: 300px; max-width: calc(100% - 24px);
|
| 653 |
+
background: var(--card); color: var(--ink);
|
| 654 |
+
border: 1px solid var(--rule-2); border-radius: 10px;
|
| 655 |
+
box-shadow: 0 8px 26px color-mix(in srgb, var(--ink) 18%, transparent);
|
| 656 |
+
font-family: var(--sans);
|
| 657 |
+
overflow: hidden;
|
| 658 |
+
}
|
| 659 |
+
.fp-head {
|
| 660 |
+
display: grid; grid-template-columns: 1fr auto auto; align-items: center; gap: 4px 8px;
|
| 661 |
+
padding: 8px 8px 8px 12px; background: var(--sage); border-bottom: 1px solid var(--rule);
|
| 662 |
+
cursor: grab; touch-action: none; user-select: none;
|
| 663 |
+
}
|
| 664 |
+
.fp-head.dragging { cursor: grabbing; }
|
| 665 |
+
#feat-panel .fp-head h3 { margin: 0; font: 700 12.5px var(--sans); letter-spacing: -0.01em; }
|
| 666 |
+
.fp-at { font: 500 10.5px var(--mono); color: var(--ink-2); font-variant-numeric: tabular-nums; }
|
| 667 |
+
.fp-grip {
|
| 668 |
+
grid-row: 1; width: 12px; height: 12px; opacity: .5;
|
| 669 |
+
background:
|
| 670 |
+
radial-gradient(circle at 2px 2px, currentColor 1px, transparent 1.2px) 0 0 / 5px 5px;
|
| 671 |
+
color: var(--ink-3);
|
| 672 |
+
}
|
| 673 |
+
.fp-close {
|
| 674 |
+
font: 400 16px/1 var(--sans); color: var(--ink-2); background: none; border: 0;
|
| 675 |
+
border-radius: 5px; width: 26px; height: 26px; cursor: pointer;
|
| 676 |
+
}
|
| 677 |
+
.fp-close:hover { background: var(--paper-2); color: var(--ink); }
|
| 678 |
+
#feat-panel .fp-src {
|
| 679 |
+
margin: 0; padding: 7px 12px 6px;
|
| 680 |
+
font: 10.5px var(--sans); color: var(--ink-3); line-height: 1.35;
|
| 681 |
+
border-bottom: 1px solid var(--rule);
|
| 682 |
+
}
|
| 683 |
+
#feat-panel .fp-src b { font: 500 10px var(--mono); color: var(--ink-2); }
|
| 684 |
+
#feat-panel .fp-rows { margin: 0; padding: 4px 6px; list-style: none; max-height: 268px; overflow-y: auto; }
|
| 685 |
+
#feat-panel .fp-rows li { margin: 0; }
|
| 686 |
+
.fp-row {
|
| 687 |
+
display: grid; grid-template-columns: 10px auto minmax(0, 1fr) auto; align-items: center;
|
| 688 |
+
gap: 0 8px; width: 100%; text-align: left;
|
| 689 |
+
font: inherit; background: none; border: 0; border-radius: 5px;
|
| 690 |
+
padding: 5px 6px; cursor: pointer;
|
| 691 |
+
}
|
| 692 |
+
.fp-row:hover { background: var(--paper-2); }
|
| 693 |
+
.fp-row .chip { width: 9px; height: 9px; border-radius: 2px; background: var(--rule-2); }
|
| 694 |
+
.fp-row[aria-pressed="true"] .chip { box-shadow: 0 0 0 2px color-mix(in srgb, var(--ink) 22%, transparent); }
|
| 695 |
+
.fp-row .fid { font: 600 11px var(--mono); color: var(--ink); }
|
| 696 |
+
.fp-row .fnm {
|
| 697 |
+
font: 11.5px var(--sans); color: var(--ink-2);
|
| 698 |
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
| 699 |
+
}
|
| 700 |
+
.fp-row .fnm .un { color: var(--ink-3); font-family: var(--mono); font-size: 10.5px; }
|
| 701 |
+
.fp-row .fv { font: 600 11.5px var(--mono); color: var(--ink); font-variant-numeric: tabular-nums; }
|
| 702 |
+
.fp-row[aria-pressed="true"] .fnm { color: var(--ink); }
|
| 703 |
+
#feat-panel .fp-empty { padding: 12px; margin: 0; font: italic 11.5px var(--sans); color: var(--ink-3); }
|
| 704 |
+
#feat-panel .fp-foot {
|
| 705 |
+
margin: 0; padding: 8px 12px 10px; border-top: 1px solid var(--rule);
|
| 706 |
+
background: var(--paper-2); font: 10.5px var(--sans); color: var(--ink-3); line-height: 1.4;
|
| 707 |
+
}
|
| 708 |
+
#feat-panel .fp-foot b { font: 600 10.5px var(--mono); color: var(--ink-2); }
|
| 709 |
+
#feat-panel .fp-foot .hint { display: block; margin-top: 3px; }
|
| 710 |
+
|
| 711 |
+
/* No room to float: both surfaces drop into the flow under the figure, in the
|
| 712 |
+
order the reader needs them — the figure and its readout, then the lanes,
|
| 713 |
+
then the top-features panel. */
|
| 714 |
+
@media (max-width: 900px) {
|
| 715 |
+
.feat-float {
|
| 716 |
+
position: static !important;
|
| 717 |
+
width: auto; max-width: none; inset: auto !important;
|
| 718 |
+
border-radius: 0; border-left: 0; border-right: 0;
|
| 719 |
+
box-shadow: none;
|
| 720 |
+
}
|
| 721 |
+
.feat-dock, .fp { border-top: 1px solid var(--rule-2); border-bottom: 0; margin-top: 0; }
|
| 722 |
+
.fp-head, #feat-controls .dock-title { cursor: default; }
|
| 723 |
+
.fp-grip { display: none; }
|
| 724 |
+
/* in the flow they still scroll internally: 32 ranked rows and 38 lane rows
|
| 725 |
+
would otherwise add two screens of page */
|
| 726 |
+
#feat-panel .fp-rows { max-height: 320px; }
|
| 727 |
+
#feat-controls .dock-body { max-height: 460px; }
|
| 728 |
+
#feat-panel .fp-rows, #feat-panel .fp-src, #feat-panel .fp-foot { max-width: 620px; }
|
| 729 |
+
#feat-controls .dock-body { max-width: 640px; }
|
| 730 |
+
#feat-controls .fl-rows { grid-template-columns: repeat(auto-fill, minmax(268px, 1fr)); gap: 0 24px; }
|
| 731 |
+
}
|
| 732 |
+
|
| 733 |
+
/* ------------------------------------------------- figure-width responses --- */
|
| 734 |
+
@container (max-width: 1020px) {
|
| 735 |
+
.feat-readout { grid-template-columns: minmax(150px, 0.9fr) minmax(0, 1.6fr); }
|
| 736 |
+
.feat-readout .ro-act { grid-column: 1 / -1; border-left: 0; border-top: 1px solid var(--rule); }
|
| 737 |
+
}
|
| 738 |
+
@container (max-width: 700px) {
|
| 739 |
+
.feat-readout { grid-template-columns: 1fr 1fr; min-height: 0; }
|
| 740 |
+
.feat-readout .ro-ann { border-left: 1px solid var(--rule); }
|
| 741 |
+
}
|
| 742 |
+
@container (max-width: 430px) {
|
| 743 |
+
.feat-readout { grid-template-columns: 1fr; }
|
| 744 |
+
.feat-readout .ro-ann { border-left: 0; border-top: 1px solid var(--rule); }
|
| 745 |
+
}
|
|
@@ -13,7 +13,7 @@
|
|
| 13 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 14 |
</head>
|
| 15 |
<body>
|
| 16 |
-
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html" aria-current="page">Causal variants</a><a href="use-case.html">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 17 |
<div class="page">
|
| 18 |
|
| 19 |
<main class="prose">
|
|
|
|
| 13 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 14 |
</head>
|
| 15 |
<body>
|
| 16 |
+
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html" aria-current="page">Causal variants</a><a href="sae-features.html">SAE features</a><a href="use-case.html">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 17 |
<div class="page">
|
| 18 |
|
| 19 |
<main class="prose">
|
|
@@ -12,7 +12,7 @@
|
|
| 12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
-
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="use-case.html">Use case</a><a href="corpus.html" aria-current="page">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 16 |
<div class="page">
|
| 17 |
|
| 18 |
<main class="prose">
|
|
|
|
| 12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
+
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="sae-features.html">SAE features</a><a href="use-case.html">Use case</a><a href="corpus.html" aria-current="page">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 16 |
<div class="page">
|
| 17 |
|
| 18 |
<main class="prose">
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
{"what":"Every verified annotation change we could locate in the corpus, scored by whether the report's splice feature reads higher at the position the newer release places the site or at the position it superseded.","method":"Candidate sites come from diffing each species' two annotation builds per transcript. A site is kept only if the window's own annotation carries a site of the expected class at the expected base, the new position carries none, the new boundary is absent from every isoform of the old build (so a re-pick is not counted as a move), and both positions lie at least 200 bp from a window edge. Each position is scored as the feature's maximum within 2 bases of it. A site counts as decided when one position exceeds the other by more than 1.5x.","feature_used":{"moved_donor":4131,"moved_acceptor":586},"totals":{"sites":98,"windows":82,"decided_sites":70,"favour_revised_sites":61,"favour_superseded_sites":9,"no_separation_sites":28,"decided_windows":59,"favour_revised_windows":50,"favour_superseded_windows":9,"exactly_zero_at_superseded":41,"binomial_p_per_site":1.28e-10,"binomial_p_per_window":5.26e-08},"by_species":{"slycopersicum":{"newer_release":"ITAG3.2","same_assembly":true,"favour_revised_windows":29,"favour_superseded_windows":2},"osativa":{"newer_release":"RAP-DB 2026-02-05","same_assembly":true,"favour_revised_windows":16,"favour_superseded_windows":2},"athaliana":{"newer_release":"TAIR12_1Feb26","same_assembly":false,"favour_revised_windows":5,"favour_superseded_windows":5,"note":"TAIR12 is a new assembly, so each position had to be tied to the TAIR10 frame by anchoring on identical flanking sequence, and its revisions are often whole-model rather than a single moved site"}},"limits":["Sites were selected by annotation change alone, with no reference to how the feature behaves, so the comparison is not circular - but neither is it a controlled benchmark.","29% of sites show no separation and are excluded from the ratio.","One dictionary, one layer, three annotation updates, two of them on the same assembly as the windows.","A feature reading higher at a position is not evidence that the position is correct; it is evidence that the model's representation prefers it."],"sites":[{"uid":"B00005282","event":"moved_acceptor","old":2737,"new":2759,"gene":"ELP1","species":"athaliana","build_new":"TAIR12 1Feb26","shift":22,"old_val":1.319,"new_val":11.445,"verdict":"new"},{"uid":"B00000073","event":"moved_acceptor","old":2632,"new":2560,"gene":"AT4G21640","species":"athaliana","build_new":"TAIR12 1Feb26","shift":72,"old_val":1.353,"new_val":6.887,"verdict":"new"},{"uid":"B00001834","event":"moved_donor","old":6086,"new":6167,"gene":"AT4G38780","species":"athaliana","build_new":"TAIR12 1Feb26","shift":81,"old_val":0.0,"new_val":15.617,"verdict":"new"},{"uid":"B00005461","event":"moved_acceptor","old":4314,"new":4281,"gene":"EME1B","species":"athaliana","build_new":"TAIR12 1Feb26","shift":33,"old_val":14.016,"new_val":6.441,"verdict":"old"},{"uid":"B00001623","event":"moved_acceptor","old":7320,"new":7239,"gene":"PDIL1-4","species":"athaliana","build_new":"TAIR12 1Feb26","shift":81,"old_val":14.375,"new_val":7.449,"verdict":"old"},{"uid":"B00014342","event":"moved_donor","old":2993,"new":3019,"gene":"Os07g0192900","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":26,"old_val":0.0,"new_val":14.477,"verdict":"new"},{"uid":"B00011405","event":"moved_acceptor","old":1872,"new":1884,"gene":"Os03g0356638","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":12,"old_val":0.0,"new_val":15.453,"verdict":"new"},{"uid":"B00013003","event":"moved_donor","old":3612,"new":3595,"gene":"Os08g0422600","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":17,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00025527","event":"moved_donor","old":4836,"new":4782,"gene":"Solyc07g043620","species":"slycopersicum","build_new":"ITAG3.2","shift":54,"old_val":0.0,"new_val":11.117,"verdict":"new"},{"uid":"B00028267","event":"moved_donor","old":5910,"new":5916,"gene":"Solyc04g050520","species":"slycopersicum","build_new":"ITAG3.2","shift":6,"old_val":1.815,"new_val":12.594,"verdict":"new"},{"uid":"B00029213","event":"moved_donor","old":2720,"new":2645,"gene":"Solyc08g061500","species":"slycopersicum","build_new":"ITAG3.2","shift":75,"old_val":0.0,"new_val":16.031,"verdict":"new"},{"uid":"B00024301","event":"moved_acceptor","old":1722,"new":1806,"gene":"Solyc06g068480","species":"slycopersicum","build_new":"ITAG3.2","shift":84,"old_val":1.3,"new_val":13.93,"verdict":"new"},{"uid":"B00008641","event":"moved_donor","old":3381,"new":3357,"gene":"Os03g0784100","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":24,"old_val":2.65,"new_val":14.273,"verdict":"new"},{"uid":"B00011851","event":"moved_donor","old":3343,"new":3443,"gene":"Os11g0669200","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":100,"old_val":2.062,"new_val":16.344,"verdict":"new"},{"uid":"B00024855","event":"moved_donor","old":5766,"new":5799,"gene":"Solyc06g063210","species":"slycopersicum","build_new":"ITAG3.2","shift":33,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00011656","event":"moved_acceptor","old":6685,"new":6670,"gene":"Os07g0273700","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":15,"old_val":9.023,"new_val":14.781,"verdict":"new"},{"uid":"B00009010","event":"moved_acceptor","old":5193,"new":5188,"gene":"Os07g0637600","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":5,"old_val":0.0,"new_val":16.344,"verdict":"new"},{"uid":"B00024506","event":"moved_donor","old":4616,"new":4679,"gene":"Solyc03g118040","species":"slycopersicum","build_new":"ITAG3.2","shift":63,"old_val":0.0,"new_val":16.406,"verdict":"new"},{"uid":"B00028185","event":"moved_donor","old":4619,"new":4667,"gene":"Solyc02g078010","species":"slycopersicum","build_new":"ITAG3.2","shift":48,"old_val":0.0,"new_val":15.156,"verdict":"new"},{"uid":"B00028185","event":"moved_acceptor","old":3174,"new":3159,"gene":"Solyc02g078010","species":"slycopersicum","build_new":"ITAG3.2","shift":15,"old_val":0.0,"new_val":16.281,"verdict":"new"},{"uid":"B00029108","event":"moved_donor","old":6178,"new":6205,"gene":"Solyc02g089340","species":"slycopersicum","build_new":"ITAG3.2","shift":27,"old_val":0.0,"new_val":17.172,"verdict":"new"},{"uid":"B00028092","event":"moved_acceptor","old":5615,"new":5672,"gene":"Solyc05g007190","species":"slycopersicum","build_new":"ITAG3.2","shift":57,"old_val":3.477,"new_val":16.844,"verdict":"new"},{"uid":"B00028092","event":"moved_acceptor","old":5134,"new":5166,"gene":"Solyc05g007190","species":"slycopersicum","build_new":"ITAG3.2","shift":32,"old_val":0.0,"new_val":15.578,"verdict":"new"},{"uid":"B00028092","event":"moved_acceptor","old":3452,"new":3427,"gene":"Solyc05g007190","species":"slycopersicum","build_new":"ITAG3.2","shift":25,"old_val":0.0,"new_val":16.578,"verdict":"new"},{"uid":"B00027566","event":"moved_acceptor","old":6260,"new":6320,"gene":"Solyc07g049280","species":"slycopersicum","build_new":"ITAG3.2","shift":60,"old_val":13.867,"new_val":16.172,"verdict":"tie"},{"uid":"B00010384","event":"moved_donor","old":3759,"new":3766,"gene":"Os03g0196000","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":7,"old_val":2.848,"new_val":13.805,"verdict":"new"},{"uid":"B00014209","event":"moved_acceptor","old":2944,"new":2948,"gene":"Os04g0420600","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":4,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00029205","event":"moved_acceptor","old":5367,"new":5415,"gene":"Solyc02g072310","species":"slycopersicum","build_new":"ITAG3.2","shift":48,"old_val":0.82,"new_val":9.117,"verdict":"new"},{"uid":"B00028091","event":"moved_donor","old":6636,"new":6576,"gene":"Solyc04g005330","species":"slycopersicum","build_new":"ITAG3.2","shift":60,"old_val":0.0,"new_val":15.562,"verdict":"new"},{"uid":"B00028091","event":"moved_donor","old":4285,"new":4300,"gene":"Solyc04g005330","species":"slycopersicum","build_new":"ITAG3.2","shift":15,"old_val":2.965,"new_val":16.406,"verdict":"new"},{"uid":"B00028091","event":"moved_acceptor","old":2514,"new":2463,"gene":"Solyc04g005330","species":"slycopersicum","build_new":"ITAG3.2","shift":51,"old_val":0.0,"new_val":14.312,"verdict":"new"},{"uid":"B00027887","event":"moved_donor","old":5168,"new":5213,"gene":"Solyc08g081340","species":"slycopersicum","build_new":"ITAG3.2","shift":45,"old_val":0.0,"new_val":13.938,"verdict":"new"},{"uid":"B00028023","event":"moved_donor","old":4284,"new":4269,"gene":"Solyc03g121950","species":"slycopersicum","build_new":"ITAG3.2","shift":15,"old_val":10.055,"new_val":13.547,"verdict":"tie"},{"uid":"B00013733","event":"moved_acceptor","old":3359,"new":3224,"gene":"Os04g0472500","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":135,"old_val":12.016,"new_val":1.589,"verdict":"old"},{"uid":"B00001688","event":"moved_acceptor","old":4047,"new":4035,"gene":"AT5G07170","species":"athaliana","build_new":"TAIR12 1Feb26","shift":12,"old_val":16.0,"new_val":4.965,"verdict":"old"},{"uid":"B00001688","event":"moved_donor","old":4462,"new":4482,"gene":"AT5G07170","species":"athaliana","build_new":"TAIR12 1Feb26","shift":20,"old_val":14.039,"new_val":10.914,"verdict":"tie"},{"uid":"B00000376","event":"moved_donor","old":4634,"new":4645,"gene":"P4H13","species":"athaliana","build_new":"TAIR12 1Feb26","shift":11,"old_val":9.07,"new_val":8.023,"verdict":"tie"},{"uid":"B00002232","event":"moved_donor","old":4941,"new":4953,"gene":"AT1G06590","species":"athaliana","build_new":"TAIR12 1Feb26","shift":12,"old_val":12.055,"new_val":6.828,"verdict":"old"},{"uid":"B00000243","event":"moved_acceptor","old":6256,"new":6241,"gene":"AT1G07705","species":"athaliana","build_new":"TAIR12 1Feb26","shift":15,"old_val":15.688,"new_val":10.141,"verdict":"old"},{"uid":"B00005939","event":"moved_acceptor","old":2236,"new":2203,"gene":"AT1G19650","species":"athaliana","build_new":"TAIR12 1Feb26","shift":33,"old_val":13.094,"new_val":15.508,"verdict":"tie"},{"uid":"B00000106","event":"moved_acceptor","old":4454,"new":4421,"gene":"DWA3","species":"athaliana","build_new":"TAIR12 1Feb26","shift":33,"old_val":4.941,"new_val":12.758,"verdict":"new"},{"uid":"B00008882","event":"moved_donor","old":4692,"new":4704,"gene":"Os08g0190300","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":12,"old_val":3.307,"new_val":13.0,"verdict":"new"},{"uid":"B00008384","event":"moved_acceptor","old":4215,"new":4070,"gene":"Os01g0276200","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":145,"old_val":0.0,"new_val":14.312,"verdict":"new"},{"uid":"B00027450","event":"moved_donor","old":2406,"new":2463,"gene":"Solyc01g008840","species":"slycopersicum","build_new":"ITAG3.2","shift":57,"old_val":3.09,"new_val":17.281,"verdict":"new"},{"uid":"B00029839","event":"moved_acceptor","old":3339,"new":3372,"gene":"Solyc01g108460","species":"slycopersicum","build_new":"ITAG3.2","shift":33,"old_val":0.0,"new_val":14.781,"verdict":"new"},{"uid":"B00029497","event":"moved_donor","old":4926,"new":4920,"gene":"Solyc02g081390","species":"slycopersicum","build_new":"ITAG3.2","shift":6,"old_val":0.0,"new_val":16.094,"verdict":"new"},{"uid":"B00029961","event":"moved_donor","old":1998,"new":2004,"gene":"Solyc02g088230","species":"slycopersicum","build_new":"ITAG3.2","shift":6,"old_val":2.01,"new_val":13.336,"verdict":"new"},{"uid":"B00029961","event":"moved_acceptor","old":1657,"new":1744,"gene":"Solyc02g088230","species":"slycopersicum","build_new":"ITAG3.2","shift":87,"old_val":0.0,"new_val":11.188,"verdict":"new"},{"uid":"B00029369","event":"moved_donor","old":6545,"new":6539,"gene":"Solyc06g007780","species":"slycopersicum","build_new":"ITAG3.2","shift":6,"old_val":11.969,"new_val":5.086,"verdict":"old"},{"uid":"B00029313","event":"moved_donor","old":5807,"new":5789,"gene":"Solyc10g008180","species":"slycopersicum","build_new":"ITAG3.2","shift":18,"old_val":10.047,"new_val":12.0,"verdict":"tie"},{"uid":"B00028667","event":"moved_donor","old":2744,"new":2696,"gene":"Solyc10g085570","species":"slycopersicum","build_new":"ITAG3.2","shift":48,"old_val":11.789,"new_val":14.07,"verdict":"tie"},{"uid":"B00029170","event":"moved_acceptor","old":1897,"new":1882,"gene":"Solyc11g007300","species":"slycopersicum","build_new":"ITAG3.2","shift":15,"old_val":2.102,"new_val":14.703,"verdict":"new"},{"uid":"B00027370","event":"moved_donor","old":1911,"new":1903,"gene":"Solyc12g044300","species":"slycopersicum","build_new":"ITAG3.2","shift":8,"old_val":14.711,"new_val":1.602,"verdict":"old"},{"uid":"B00029720","event":"moved_acceptor","old":5876,"new":5925,"gene":"Solyc01g094060","species":"slycopersicum","build_new":"ITAG3.2","shift":49,"old_val":0.0,"new_val":13.906,"verdict":"new"},{"uid":"B00029720","event":"moved_donor","old":5971,"new":5996,"gene":"Solyc01g094060","species":"slycopersicum","build_new":"ITAG3.2","shift":25,"old_val":1.594,"new_val":13.367,"verdict":"new"},{"uid":"B00029484","event":"moved_donor","old":4051,"new":4075,"gene":"Solyc01g074040","species":"slycopersicum","build_new":"ITAG3.2","shift":24,"old_val":0.0,"new_val":14.719,"verdict":"new"},{"uid":"B00029941","event":"moved_donor","old":5999,"new":5995,"gene":"Solyc03g006070","species":"slycopersicum","build_new":"ITAG3.2","shift":4,"old_val":4.723,"new_val":14.617,"verdict":"new"},{"uid":"B00029941","event":"moved_acceptor","old":5938,"new":5934,"gene":"Solyc03g006070","species":"slycopersicum","build_new":"ITAG3.2","shift":4,"old_val":0.0,"new_val":14.977,"verdict":"new"},{"uid":"B00027784","event":"moved_donor","old":891,"new":885,"gene":"Solyc07g049280","species":"slycopersicum","build_new":"ITAG3.2","shift":6,"old_val":1.053,"new_val":13.703,"verdict":"new"},{"uid":"B00026158","event":"moved_donor","old":6872,"new":6946,"gene":"Solyc12g088110","species":"slycopersicum","build_new":"ITAG3.2","shift":74,"old_val":0.0,"new_val":10.805,"verdict":"new"},{"uid":"B00010384","event":"moved_donor","old":3615,"new":3611,"gene":"Os03g0196000","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":4,"old_val":4.121,"new_val":13.531,"verdict":"new"},{"uid":"B00027883","event":"moved_donor","old":6722,"new":6707,"gene":"Solyc02g093870","species":"slycopersicum","build_new":"ITAG3.2","shift":15,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00027883","event":"moved_acceptor","old":5240,"new":5204,"gene":"Solyc02g093870","species":"slycopersicum","build_new":"ITAG3.2","shift":36,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00029302","event":"moved_acceptor","old":1654,"new":1690,"gene":"Solyc11g019960","species":"slycopersicum","build_new":"ITAG3.2","shift":36,"old_val":9.305,"new_val":9.055,"verdict":"tie"},{"uid":"B00005274","event":"moved_donor","old":5261,"new":5237,"gene":"CRK16","species":"athaliana","build_new":"TAIR12 1Feb26","shift":24,"old_val":7.934,"new_val":9.789,"verdict":"tie"},{"uid":"B00014205","event":"moved_acceptor","old":6774,"new":6858,"gene":"Os04g0452600","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":84,"old_val":0.0,"new_val":14.641,"verdict":"new"},{"uid":"B00029071","event":"moved_donor","old":6799,"new":6883,"gene":"Solyc05g013490","species":"slycopersicum","build_new":"ITAG3.2","shift":84,"old_val":0.0,"new_val":0.814,"verdict":"new"},{"uid":"B00028617","event":"moved_acceptor","old":4341,"new":4359,"gene":"Solyc05g050000","species":"slycopersicum","build_new":"ITAG3.2","shift":18,"old_val":7.555,"new_val":13.68,"verdict":"new"},{"uid":"B00029845","event":"moved_donor","old":3000,"new":3083,"gene":"Solyc06g065620","species":"slycopersicum","build_new":"ITAG3.2","shift":83,"old_val":0.0,"new_val":13.438,"verdict":"new"},{"uid":"B00029845","event":"moved_acceptor","old":6863,"new":6885,"gene":"Solyc06g065620","species":"slycopersicum","build_new":"ITAG3.2","shift":22,"old_val":4.711,"new_val":15.18,"verdict":"new"},{"uid":"B00010782","event":"moved_donor","old":4170,"new":4258,"gene":"Os02g0641000","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":88,"old_val":0.0,"new_val":15.805,"verdict":"new"},{"uid":"B00003121","event":"moved_donor","old":2934,"new":2907,"gene":"AT1G31530","species":"athaliana","build_new":"TAIR12 1Feb26","shift":27,"old_val":4.578,"new_val":14.094,"verdict":"new"},{"uid":"B00005922","event":"moved_donor","old":7165,"new":7195,"gene":"ATX2","species":"athaliana","build_new":"TAIR12 1Feb26","shift":30,"old_val":12.914,"new_val":11.586,"verdict":"tie"},{"uid":"B00029708","event":"moved_donor","old":6388,"new":6334,"gene":"Solyc06g010190","species":"slycopersicum","build_new":"ITAG3.2","shift":54,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00027719","event":"moved_acceptor","old":1514,"new":1489,"gene":"Solyc06g051930","species":"slycopersicum","build_new":"ITAG3.2","shift":25,"old_val":1.818,"new_val":14.039,"verdict":"new"},{"uid":"B00011423","event":"moved_donor","old":895,"new":1049,"gene":"Os08g0562700","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":154,"old_val":0.831,"new_val":15.883,"verdict":"new"},{"uid":"B00011177","event":"moved_acceptor","old":2648,"new":2465,"gene":"Os09g0551251","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":183,"old_val":12.203,"new_val":12.68,"verdict":"tie"},{"uid":"B00027935","event":"moved_acceptor","old":3242,"new":3257,"gene":"Solyc05g051770","species":"slycopersicum","build_new":"ITAG3.2","shift":15,"old_val":2.117,"new_val":12.812,"verdict":"new"},{"uid":"B00029825","event":"moved_donor","old":7895,"new":7898,"gene":"Solyc01g103120","species":"slycopersicum","build_new":"ITAG3.2","shift":3,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00028958","event":"moved_donor","old":6163,"new":6091,"gene":"Solyc09g089830","species":"slycopersicum","build_new":"ITAG3.2","shift":72,"old_val":0.0,"new_val":14.047,"verdict":"new"},{"uid":"B00029587","event":"moved_donor","old":1377,"new":1361,"gene":"Solyc04g072870","species":"slycopersicum","build_new":"ITAG3.2","shift":16,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00010015","event":"moved_donor","old":6595,"new":6510,"gene":"Os06g0584000","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":85,"old_val":10.539,"new_val":0.0,"verdict":"old"},{"uid":"B00028760","event":"moved_acceptor","old":4490,"new":4485,"gene":"Solyc10g084280","species":"slycopersicum","build_new":"ITAG3.2","shift":5,"old_val":1.901,"new_val":13.016,"verdict":"new"},{"uid":"B00013712","event":"moved_acceptor","old":675,"new":497,"gene":"Os04g0588200","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":178,"old_val":16.703,"new_val":16.266,"verdict":"tie"},{"uid":"B00013712","event":"moved_donor","old":433,"new":555,"gene":"Os04g0588200","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":122,"old_val":14.68,"new_val":14.805,"verdict":"tie"},{"uid":"B00010057","event":"moved_donor","old":2809,"new":2910,"gene":"Os12g0611300","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":101,"old_val":0.0,"new_val":15.125,"verdict":"new"},{"uid":"B00028405","event":"moved_donor","old":1238,"new":1181,"gene":"Solyc02g084520","species":"slycopersicum","build_new":"ITAG3.2","shift":57,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00011618","event":"moved_acceptor","old":1654,"new":1496,"gene":"Os05g0256500","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":158,"old_val":9.023,"new_val":8.031,"verdict":"tie"},{"uid":"B00011618","event":"moved_donor","old":1409,"new":1569,"gene":"Os05g0256500","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":160,"old_val":12.695,"new_val":10.383,"verdict":"tie"},{"uid":"B00011918","event":"moved_donor","old":413,"new":556,"gene":"Os06g0162550","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":143,"old_val":15.125,"new_val":11.859,"verdict":"tie"},{"uid":"B00011918","event":"moved_acceptor","old":660,"new":492,"gene":"Os06g0162550","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":168,"old_val":12.367,"new_val":13.922,"verdict":"tie"},{"uid":"B00030204","event":"moved_acceptor","old":357,"new":363,"gene":"Solyc01g098870","species":"slycopersicum","build_new":"ITAG3.2","shift":6,"old_val":11.008,"new_val":10.539,"verdict":"tie"},{"uid":"B00011970","event":"moved_acceptor","old":6503,"new":6508,"gene":"Os11g0545700","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":5,"old_val":6.863,"new_val":15.742,"verdict":"new"},{"uid":"B00029693","event":"moved_donor","old":803,"new":892,"gene":"Solyc03g117440","species":"slycopersicum","build_new":"ITAG3.2","shift":89,"old_val":0.0,"new_val":15.789,"verdict":"new"},{"uid":"B00009937","event":"moved_acceptor","old":4068,"new":3945,"gene":"Os09g0327800","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":123,"old_val":0.914,"new_val":14.875,"verdict":"new"},{"uid":"B00010789","event":"moved_acceptor","old":6653,"new":6790,"gene":"Os08g0100200","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":137,"old_val":0.0,"new_val":0.0,"verdict":"tie"},{"uid":"B00013801","event":"moved_donor","old":284,"new":275,"gene":"Os02g0803250","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":9,"old_val":3.211,"new_val":13.75,"verdict":"new"},{"uid":"B00014205","event":"moved_donor","old":7629,"new":7815,"gene":"Os04g0452600","species":"osativa","build_new":"RAP-DB 2026-02-05","shift":186,"old_val":0.0,"new_val":16.297,"verdict":"new"}]}
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -15,7 +15,7 @@
|
|
| 15 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 16 |
</head>
|
| 17 |
<body class="factory">
|
| 18 |
-
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html" aria-current="page">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="use-case.html">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 19 |
<div class="page">
|
| 20 |
<main class="prose">
|
| 21 |
<p class="kicker">Methods</p>
|
|
|
|
| 15 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 16 |
</head>
|
| 17 |
<body class="factory">
|
| 18 |
+
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html" aria-current="page">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="sae-features.html">SAE features</a><a href="use-case.html">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 19 |
<div class="page">
|
| 20 |
<main class="prose">
|
| 21 |
<p class="kicker">Methods</p>
|
|
@@ -17,7 +17,7 @@
|
|
| 17 |
<script src="assets/overview.js" defer></script>
|
| 18 |
</head>
|
| 19 |
<body class="landing">
|
| 20 |
-
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html" aria-current="page">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="use-case.html">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 21 |
|
| 22 |
<main>
|
| 23 |
<section class="hero" aria-labelledby="intro-title">
|
|
|
|
| 17 |
<script src="assets/overview.js" defer></script>
|
| 18 |
</head>
|
| 19 |
<body class="landing">
|
| 20 |
+
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html" aria-current="page">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="sae-features.html">SAE features</a><a href="use-case.html">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 21 |
|
| 22 |
<main>
|
| 23 |
<section class="hero" aria-labelledby="intro-title">
|
|
@@ -12,7 +12,7 @@
|
|
| 12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
-
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html" aria-current="page">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="use-case.html">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 16 |
<div class="page">
|
| 17 |
|
| 18 |
<main class="prose">
|
|
|
|
| 12 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 13 |
</head>
|
| 14 |
<body>
|
| 15 |
+
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html" aria-current="page">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="sae-features.html">SAE features</a><a href="use-case.html">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 16 |
<div class="page">
|
| 17 |
|
| 18 |
<main class="prose">
|
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>Botanic1 sparse features</title>
|
| 7 |
+
<meta name="description" content="Sparse autoencoder features of Botanic1-M read out base by base over annotated genomic windows in four plant species.">
|
| 8 |
+
<script>(function(){try{var t=new URLSearchParams(location.search).get('__theme');if(t==='dark'||t==='light')document.documentElement.setAttribute('data-theme',t);}catch(e){}})();</script>
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700;800&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap">
|
| 12 |
+
<link rel="stylesheet" href="assets/site.css?v=20260908-use-case">
|
| 13 |
+
<link rel="stylesheet" href="assets/sae.css?v=20260908-case2">
|
| 14 |
+
<link rel="stylesheet" href="assets/sae-sources.css?v=20260908-case2">
|
| 15 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 16 |
+
</head>
|
| 17 |
+
<body>
|
| 18 |
+
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="sae-features.html" aria-current="page">SAE features</a><a href="use-case.html">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 19 |
+
<div class="page">
|
| 20 |
+
|
| 21 |
+
<main class="prose">
|
| 22 |
+
<p class="kicker">Interpretability</p>
|
| 23 |
+
<h1>Sparse features are readable genomic concepts</h1>
|
| 24 |
+
<p class="lede">A sparse autoencoder decomposes one layer of a frozen Botanic1-M into 8,192
|
| 25 |
+
features, fitted with no genomic annotation at all. Single features nonetheless detect
|
| 26 |
+
splice donors and acceptors at MCC 0.871 and 0.873, intron bodies, and each of the three
|
| 27 |
+
positions in a codon. Two of them read zero where TAIR10 and Araport11 annotate a splice
|
| 28 |
+
donor — and in 2026 TAIR12 moved that donor to where they fire.</p>
|
| 29 |
+
|
| 30 |
+
<p>The browser below reads that dictionary out base by base over 21 precomputed windows.
|
| 31 |
+
Every value drawn is a measurement; the annotation rows are a separate, paler register and
|
| 32 |
+
never an input to a number on this page.</p>
|
| 33 |
+
|
| 34 |
+
<h2 id="choose">Choose a window</h2>
|
| 35 |
+
<p>Each window is 8,191 bp, the model's native context, and each was chosen because it
|
| 36 |
+
shows something. Ten are windows where the features track the annotation closely: over 384
|
| 37 |
+
windows scanned, the splice features fire at a median 95% of the annotated donors and
|
| 38 |
+
acceptors, and these are the clearest of them. Eight are disagreements — five where a
|
| 39 |
+
feature marks a splice site the canonical transcript drops but another annotated isoform
|
| 40 |
+
uses, three where it marks a site no annotated transcript uses at all. Each of those eight
|
| 41 |
+
also carries a verdict from a register independent of the annotation, and one of the three
|
| 42 |
+
is a site an RNA-seq atlas measures in use. Two are loci where the annotation itself moved
|
| 43 |
+
between releases. The last is the <em>AT1G65170</em> locus the report's
|
| 44 |
+
annotation-correction result is drawn from.</p>
|
| 45 |
+
|
| 46 |
+
<p>A released annotation says which splice sites someone drew a transcript through. It
|
| 47 |
+
does not say which sites are <em>used</em>, and the two questions come apart exactly where
|
| 48 |
+
a feature disagrees with the annotation. So each of the eight disagreements carries a second
|
| 49 |
+
register, shown under the browser: for Arabidopsis, AtRTD3's 169,503 long-read transcripts,
|
| 50 |
+
the 2026 TAIR12 reannotation, and PastDB's usage measurements over 202 RNA-seq samples, all
|
| 51 |
+
three on the same TAIR10 coordinates the windows are cut from.</p>
|
| 52 |
+
|
| 53 |
+
<p class="note">The three verdicts are different claims, and only one of them is a
|
| 54 |
+
discovery. <strong>Used</strong> means an independent register records the site in use:
|
| 55 |
+
chr1:21,735,155, which no transcript model carries, is an alternative 3′ splice site
|
| 56 |
+
in PastDB running at up to 7.5% against the dominant site's 99.8%, and both Arabidopsis
|
| 57 |
+
alternative-isoform sites are ones TAIR12 keeps while dropping the canonical site the
|
| 58 |
+
feature reads near zero at. <strong>Untestable</strong> means the registers cover the locus
|
| 59 |
+
and still cannot answer: chr5:5,427,495 sits 23 bp upstream of the start codon of a gene
|
| 60 |
+
with one transcript, no annotated 5′ UTR and a median expression of 0.00 cRPKM, so no
|
| 61 |
+
read can confirm or refute it. <strong>Untested</strong> means no register was consulted at
|
| 62 |
+
all — the rice and maize disagreements, where no long-read transcriptome or splicing
|
| 63 |
+
atlas on those assemblies was checked. A feature firing where nothing is annotated is not
|
| 64 |
+
evidence of a discovery until a register says so, and it is not presented as one here.</p>
|
| 65 |
+
|
| 66 |
+
<p class="label" id="win-label">Window</p>
|
| 67 |
+
<div class="win-picker" id="win-picker" role="group" aria-labelledby="win-label"></div>
|
| 68 |
+
|
| 69 |
+
<noscript><p class="small">The window picker requires JavaScript.</p></noscript>
|
| 70 |
+
|
| 71 |
+
<div class="feat-stage">
|
| 72 |
+
<figure class="feat-browser" aria-labelledby="feat-title">
|
| 73 |
+
<div class="feat-heading">
|
| 74 |
+
<div>
|
| 75 |
+
<h2 id="feat-title">A genomic window, feature by feature</h2>
|
| 76 |
+
<p id="feat-sub"></p>
|
| 77 |
+
</div>
|
| 78 |
+
<div class="feat-tools" role="group" aria-label="Window navigation">
|
| 79 |
+
<button type="button" class="zoom" id="feat-out" aria-label="Zoom out" disabled>−</button>
|
| 80 |
+
<button type="button" class="zoom" id="feat-in" aria-label="Zoom in" disabled>+</button>
|
| 81 |
+
<button type="button" id="feat-full" disabled>Whole window</button>
|
| 82 |
+
<button type="button" id="feat-panel-toggle" aria-expanded="false" aria-controls="feat-panel" disabled>Top features</button>
|
| 83 |
+
<button type="button" id="feat-controls-toggle" aria-expanded="true" aria-controls="feat-controls" disabled>Lanes</button>
|
| 84 |
+
<button type="button" class="help" id="feat-help-toggle" aria-expanded="false" aria-controls="feat-help-panel" aria-label="How to read this browser">?</button>
|
| 85 |
+
<div class="feat-help-panel" id="feat-help-panel" role="group" aria-labelledby="feat-help-title" hidden>
|
| 86 |
+
<p class="fh-title" id="feat-help-title">How to read this browser</p>
|
| 87 |
+
<p id="feat-help">Hover a base to inspect it; click or press Enter to pin it, Escape to
|
| 88 |
+
unpin. Drag to pan, pinch or + and − to zoom, arrow keys to step base by base (hold
|
| 89 |
+
Shift to stride), Home for the whole window. <em>Top features</em> opens a movable panel
|
| 90 |
+
ranking the strongest features at the pinned base.</p>
|
| 91 |
+
<p class="fh-lanes">Each window opens on one lane per genomic category — splice
|
| 92 |
+
donor, splice acceptor, intron, coding sequence, the three codon positions, and whichever
|
| 93 |
+
other annotated element the dictionary marks best here. Each lane is the feature that
|
| 94 |
+
matches that category best in <em>this</em> window, searched over every feature the window
|
| 95 |
+
stores rather than a shortlist, scored as Matthews correlation against the annotated
|
| 96 |
+
category. The threshold is the best of six percentiles of the feature's own values in the
|
| 97 |
+
window, so the score describes the picture in front of you and is not an out-of-sample
|
| 98 |
+
estimate. Codon positions are derived from the canonical transcripts' coding sequence in
|
| 99 |
+
transcription order, because the label tracks carry region and site classes but not
|
| 100 |
+
reading frame. <em>Top features</em> ranks the strongest features at the pinned base out
|
| 101 |
+
of all 8,192 and any of them can be given a lane: this dictionary keeps 64 features per
|
| 102 |
+
base and zeroes the rest, and each window carries a complete track for every feature that
|
| 103 |
+
reaches a base's top eight anywhere in it.</p>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
</div>
|
| 107 |
+
<p class="feat-region" id="feat-region">Select a window to read its features base by base.</p>
|
| 108 |
+
<div class="feat-scroll"><div id="feat-plot"></div></div>
|
| 109 |
+
<div class="feat-readout" id="feat-readout" aria-live="polite" aria-atomic="true"></div>
|
| 110 |
+
<aside id="feat-panel" hidden></aside>
|
| 111 |
+
<div class="feat-case" id="feat-case" aria-label="What this window shows"></div>
|
| 112 |
+
<figcaption>
|
| 113 |
+
<span>Activations: <span class="num" id="feat-prov"></span></span>
|
| 114 |
+
</figcaption>
|
| 115 |
+
<noscript><p class="feat-fallback">Enable JavaScript to read the features base by base.
|
| 116 |
+
<a href="https://www.biorxiv.org/content/10.64898/2026.09.04.749355v1">Read the interpretability section in the report ↗</a></p></noscript>
|
| 117 |
+
</figure>
|
| 118 |
+
<aside id="feat-controls" class="feat-dock" aria-labelledby="controls-title">
|
| 119 |
+
<p class="dock-title" id="controls-title">Colouring and lanes</p>
|
| 120 |
+
<div class="dock-body">
|
| 121 |
+
<p class="label" id="mode-label">Colouring</p>
|
| 122 |
+
<div class="seg" id="feat-mode" role="group" aria-labelledby="mode-label">
|
| 123 |
+
<button type="button" data-mode="selected" aria-pressed="true">Top of the shown lanes</button>
|
| 124 |
+
<button type="button" data-mode="global" aria-pressed="false">Top of all 8,192</button>
|
| 125 |
+
</div>
|
| 126 |
+
<p class="label" id="list-label">Feature lanes</p>
|
| 127 |
+
<div class="feat-list" id="feat-list" role="group" aria-labelledby="list-label"></div>
|
| 128 |
+
</div>
|
| 129 |
+
</aside>
|
| 130 |
+
</div>
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
<h2 id="read">How to read the tracks</h2>
|
| 134 |
+
<ul>
|
| 135 |
+
<li><strong>Top strip.</strong> Every base takes the colour of the feature that fires
|
| 136 |
+
hardest on it, and stays grey where nothing fires. Base letters appear once the view is
|
| 137 |
+
narrow enough to hold them.</li>
|
| 138 |
+
<li><strong>Annotation rows.</strong> Region bands, single-base site ticks and gene
|
| 139 |
+
extents, one set per strand, in the paler register. They come from the species' released
|
| 140 |
+
annotation, never from the model.</li>
|
| 141 |
+
<li><strong>Feature lanes.</strong> One lane per shown feature. A stem's height is the
|
| 142 |
+
activation at that base as a fraction of that feature's peak in this window, so lanes are
|
| 143 |
+
scaled independently; the gutter carries each lane's maximum in view. A blank base is a
|
| 144 |
+
zero — the feature did not fire.</li>
|
| 145 |
+
<li><strong>Dashed outline.</strong> The anchor: the position the window sampler selected
|
| 146 |
+
on. It marks where we looked, not an annotation of that base.</li>
|
| 147 |
+
<li><strong>Readout.</strong> Under the plot: the coordinate, the base, the annotation on
|
| 148 |
+
both strands, every shown feature firing there, and the strongest feature of all 8,192.</li>
|
| 149 |
+
</ul>
|
| 150 |
+
|
| 151 |
+
<div class="ann-legend" aria-label="Annotation legend">
|
| 152 |
+
<span><i style="background:var(--ann-cds)"></i>CDS</span>
|
| 153 |
+
<span><i style="background:var(--ann-utr5)"></i>5′ UTR</span>
|
| 154 |
+
<span><i style="background:var(--ann-utr3)"></i>3′ UTR</span>
|
| 155 |
+
<span><i style="background:var(--ann-intron)"></i>intron</span>
|
| 156 |
+
<span><i style="background:var(--ann-ncrna)"></i>non-coding</span>
|
| 157 |
+
<span><i style="background:var(--ann-intergenic)"></i>intergenic</span>
|
| 158 |
+
<span><i class="site" style="background:var(--ann-tss)"></i>TSS</span>
|
| 159 |
+
<span><i class="site" style="background:var(--ann-tis)"></i>start codon</span>
|
| 160 |
+
<span><i class="site" style="background:var(--ann-donor)"></i>donor</span>
|
| 161 |
+
<span><i class="site" style="background:var(--ann-acceptor)"></i>acceptor</span>
|
| 162 |
+
<span><i class="site" style="background:var(--ann-stop)"></i>stop</span>
|
| 163 |
+
<span><i class="site" style="background:var(--ann-tts)"></i>TTS</span>
|
| 164 |
+
</div>
|
| 165 |
+
|
| 166 |
+
<h2 id="named">The twelve named features</h2>
|
| 167 |
+
<p>The dictionary carries no names: nothing upstream assigns a feature a meaning. These
|
| 168 |
+
twelve have one because the report measures what they detect. The two splice features
|
| 169 |
+
and the intron feature are the best detector of their annotation class in every one of the
|
| 170 |
+
four annotated genomes, at MCC 0.821 to 0.937 for the splice classes and 0.650 to 0.741
|
| 171 |
+
for intron, so the naming does not rest on one genome. f4131 and f2634 are also the pair a
|
| 172 |
+
two-level decision tree selects in all five chromosome folds, sorting a candidate
|
| 173 |
+
<code>GT</code> into decoy, intermediate-usage or constitutive donor at a balanced accuracy
|
| 174 |
+
of 0.841 [0.827, 0.855].</p>
|
| 175 |
+
|
| 176 |
+
<p class="note">Two features the report names are deliberately absent here. Asked for a
|
| 177 |
+
transcription start site or a termination site, the best feature in the dictionary reaches
|
| 178 |
+
MCC 0.035 and 0.029 — consistent with neither site having a sharp sequence consensus in
|
| 179 |
+
plants. They are a real negative result, and a lane for either would mark nothing, so neither
|
| 180 |
+
is offered.</p>
|
| 181 |
+
<noscript><p class="small">The feature tables are built from <code>data/sae/index.json</code> and require JavaScript.</p></noscript>
|
| 182 |
+
<div class="tablewrap"><table id="named-table"><caption>Evidence as published. Bold marks the three features that are the best detector of their annotation class in every one of the four annotated genomes.</caption></table></div>
|
| 183 |
+
|
| 184 |
+
<h2 id="sources">Annotation sources</h2>
|
| 185 |
+
<p>The annotation register is read from each species' released annotation, and the browser
|
| 186 |
+
names the assembly of every window it draws.</p>
|
| 187 |
+
<div id="sources-body"></div>
|
| 188 |
+
|
| 189 |
+
<p class="note">The report's interpretability section carries the methodology, the full
|
| 190 |
+
figure and the annotation history of the locus:
|
| 191 |
+
<a href="https://www.biorxiv.org/content/10.64898/2026.09.04.749355v1">Botanic1 technical report ↗</a></p>
|
| 192 |
+
</main>
|
| 193 |
+
|
| 194 |
+
<footer class="site-footer">
|
| 195 |
+
<p>Living Models, Paris. Botanic1 is released for research use under the Living Models research licence.</p>
|
| 196 |
+
<p>Activations are produced by the sparse autoencoder of the report's interpretability section, read out over annotated windows through the interpretability engine.</p>
|
| 197 |
+
</footer>
|
| 198 |
+
</div>
|
| 199 |
+
<div class="tip" id="tip" role="tooltip"></div>
|
| 200 |
+
<script src="assets/hero.js"></script>
|
| 201 |
+
<script src="assets/sae-browser.js?v=20260908-case2" defer></script>
|
| 202 |
+
<script src="assets/sae-tables.js?v=20260908-case" defer></script>
|
| 203 |
+
</body>
|
| 204 |
+
</html>
|
|
@@ -13,7 +13,7 @@
|
|
| 13 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 14 |
</head>
|
| 15 |
<body>
|
| 16 |
-
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="use-case.html" aria-current="page">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 17 |
<div class="page">
|
| 18 |
<main class="prose use-case-prose">
|
| 19 |
<p class="kicker">Use case</p>
|
|
|
|
| 13 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"></script>
|
| 14 |
</head>
|
| 15 |
<body>
|
| 16 |
+
<header class="topbar"><div class="inner"><a class="lockup" href="index.html"><div class="wordmark" aria-label="BOTANIC-1"><span class="cell">B</span><span class="cell">O</span><span class="cell">T</span><span class="cell">A</span><span class="cell">N</span><span class="cell">I</span><span class="cell">C</span><span class="cell tok" data-state="masked"><span class="probs"><i></i><i class="top"></i><i></i><i></i></span>?</span></div><span class="org"><svg class="living-models-mark" width="18" height="20" fill="currentColor" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 85.90 91.50"><rect x="55.1000" y="2.1000" width="3.2000" height="53.2000" /><rect x="55.1000" y="61.3000" width="3.2000" height="28.1000" /><rect x="68.9000" y="9.2000" width="3.2000" height="15.5000" /><rect x="68.9000" y="30.6000" width="3.2000" height="51.7000" /><rect x="82.7000" y="24.7000" width="3.2000" height="42.2000" /><rect x="41.4000" y="0.0000" width="3.2000" height="11.6000" /><rect x="41.4000" y="17.5000" width="3.2000" height="74.0000" /><rect x="27.6000" y="2.1000" width="3.2000" height="70.7000" /><rect x="27.6000" y="78.7000" width="3.2000" height="10.6000" /><rect x="13.8000" y="9.2000" width="3.2000" height="25.0000" /><rect x="13.8000" y="40.2000" width="3.2000" height="42.2000" /><rect x="0.0000" y="24.7000" width="3.2000" height="42.2000" /></svg>Living Models</span></a><nav aria-label="Pages"><a href="index.html">Overview</a><a href="factory.html">Model factory</a><a href="leaderboard.html">Leaderboard</a><a href="causal-variants.html">Causal variants</a><a href="sae-features.html">SAE features</a><a href="use-case.html" aria-current="page">Use case</a><a href="corpus.html">Corpus</a><a class="ext" href="https://huggingface.co/collections/living-models/botanic1" target="_blank" rel="noopener">Models</a></nav></div></header>
|
| 17 |
<div class="page">
|
| 18 |
<main class="prose use-case-prose">
|
| 19 |
<p class="kicker">Use case</p>
|