Label every group with an English name and fix the hover treatment (#2)
Browse files- Label every group with an English name and fix the hover treatment (dd2b8fbb121d49f4398aabd348d13281a9bb8b78)
- FEATURE_BACKLOG.md +2 -0
- README.md +3 -1
- atlas.css +6 -4
- data/common_names.json +0 -0
- refresh_common_names.py +273 -0
- taxonomy.py +40 -18
- tests/test_taxonomy.py +35 -1
FEATURE_BACKLOG.md
CHANGED
|
@@ -54,7 +54,9 @@ Desired behavior:
|
|
| 54 |
Decisions and dependencies for implementation:
|
| 55 |
|
| 56 |
- Implemented: eukaryotic tree with bounded subtrees and exact, expandable “Other lineages” aggregates; NCBI taxonomy with source checksums and dates; merged IDs resolved. Unresolved taxonomy is excluded from the eukaryotic view.
|
|
|
|
| 57 |
- Remaining: connect the taxonomy inventory to accession-level annotation results and distinguish partial/complete base coverage.
|
|
|
|
| 58 |
|
| 59 |
## F003 — Comparison with RefSeq annotations
|
| 60 |
|
|
|
|
| 54 |
Decisions and dependencies for implementation:
|
| 55 |
|
| 56 |
- Implemented: eukaryotic tree with bounded subtrees and exact, expandable “Other lineages” aggregates; NCBI taxonomy with source checksums and dates; merged IDs resolved. Unresolved taxonomy is excluded from the eukaryotic view.
|
| 57 |
+
- Implemented: English names beside every scientific name, from NCBI Taxonomy common names plus a curated gloss table (`refresh_common_names.py`).
|
| 58 |
- Remaining: connect the taxonomy inventory to accession-level annotation results and distinguish partial/complete base coverage.
|
| 59 |
+
- Remaining: clade icons. [PhyloPic](https://www.phylopic.org) is the usable source and resolves NCBI taxids directly, walking up the tree when a clade has no silhouette of its own: `GET https://api.phylopic.org/resolve/ncbi.nlm.nih.gov/taxid/{taxid}?embed_primaryImage=true` with `Accept: application/vnd.phylopic.v2+json`, which returns an SVG under `_embedded.primaryImage._links.vectorFile.href`. Two things to settle before using it: licences differ per image (CC0, CC BY 4.0, CC BY-SA 3.0 and at least one CC BY-NC-SA 3.0 among the groups the atlas shows first), so each icon needs its own credit line and the non-commercial ones need replacing or omitting; and the SVGs should be fetched once into the repo rather than hot-linked, to keep the Space free of external requests.
|
| 60 |
|
| 61 |
## F003 — Comparison with RefSeq annotations
|
| 62 |
|
README.md
CHANGED
|
@@ -17,7 +17,7 @@ Future features and open design decisions are tracked in the [feature backlog](F
|
|
| 17 |
|
| 18 |
## GenBank taxonomy
|
| 19 |
|
| 20 |
-
The landing page opens with an interactive **tree of eukaryotic life**, using NCBI's current GenBank assembly summary and taxonomy. It is drawn as a Sankey diagram: each group is a bar split into annotated (green) and not-annotated (gray) assemblies, and flows carry both parts from a group into its lineages. Counts and percentages appear beside the groups, with details on hover. The default **Genome Atlas** tab contains the tree and its lineage controls. A separate **Database** tab holds accession search, segment visualization, and downloads, using the full published-file snapshot. RefSeq exploration can be added beneath the tree. The two equal-width navigation options span the top of the page.
|
| 21 |
|
| 22 |
The viewer is restricted to **Eukaryota (NCBI taxid 2759) and its descendants**, including search, breadcrumbs, and parent navigation. The model predicts eukaryotic annotations; bacteria, archaea, and viruses are outside this display. The current eukaryotic snapshot contains **70,395 assemblies**, of which **17,561 (24.9%)** have published annotations. The underlying SQLite snapshot retains the full NCBI inventory for refresh/provenance, while the interface uses only the eukaryotic subtree and its denominators. Assemblies with unresolved taxonomy cannot be assigned to that subtree and are excluded.
|
| 23 |
|
|
@@ -25,6 +25,8 @@ The inventory accepts `GCA_` accessions with `version_status=latest`. Each assem
|
|
| 25 |
|
| 26 |
Click a group to expand its direct lineages into a new column on the right; every ancestor column stays visible, so the full path from Eukaryota is always on screen. Clicking a group in an earlier column replaces the columns after it. Use breadcrumbs, “Collapse last group”, or shortcuts to go back. Nodes support Enter/Space keyboard navigation. The opening view expands the actual animal/fungal ancestor, Opisthokonta. Each column shows six direct lineages, an expandable remainder, and the selected lineage if it falls outside the top six. Bar and flow heights follow assembly counts within a column; every column is rescaled to the full height. “Other lineages” is an explicitly labeled display aggregate, not an invented clade; its counts sum the remaining siblings. Repeated expansion reaches all siblings without truncating them. Scientific-name/taxon-ID search also reaches individual taxa. Column spacing does not encode time. The view scrolls horizontally, and jumps to the newest column after each expansion.
|
| 27 |
|
|
|
|
|
|
|
| 28 |
The graphic uses server-rendered SVG/HTML with scoped styles in `atlas.css` and delegated click/keyboard handlers in `atlas.js`; no external graphics library or image assets are required. Include these files when deploying.
|
| 29 |
|
| 30 |
The annotation workspace shares the atlas palette and typography through `style.py` and `app.css`: warm panels, serif section headings, green controls, and matching tables, downloads, and disclosures. CDS plots use a solid green positive strand and a dashed warm-brown negative strand; binary labels use green. The fixed light palette stays consistent with the landing graphic under either OS color preference. Include both style files when deploying; Gradio 6 receives the theme and stylesheet in `launch()`.
|
|
|
|
| 17 |
|
| 18 |
## GenBank taxonomy
|
| 19 |
|
| 20 |
+
The landing page opens with an interactive **tree of eukaryotic life**, using NCBI's current GenBank assembly summary and taxonomy. It is drawn as a Sankey diagram: each group is a bar split into annotated (green) and not-annotated (gray) assemblies, and flows carry both parts from a group into its lineages. Counts and percentages appear beside the groups, with details on hover. Each group is labelled with its scientific name and, where one exists, a plain-English name underneath, so the Latin backbone of the tree (Opisthokonta, Eumetazoa, Ecdysozoa) stays readable without prior knowledge. The default **Genome Atlas** tab contains the tree and its lineage controls. A separate **Database** tab holds accession search, segment visualization, and downloads, using the full published-file snapshot. RefSeq exploration can be added beneath the tree. The two equal-width navigation options span the top of the page.
|
| 21 |
|
| 22 |
The viewer is restricted to **Eukaryota (NCBI taxid 2759) and its descendants**, including search, breadcrumbs, and parent navigation. The model predicts eukaryotic annotations; bacteria, archaea, and viruses are outside this display. The current eukaryotic snapshot contains **70,395 assemblies**, of which **17,561 (24.9%)** have published annotations. The underlying SQLite snapshot retains the full NCBI inventory for refresh/provenance, while the interface uses only the eukaryotic subtree and its denominators. Assemblies with unresolved taxonomy cannot be assigned to that subtree and are excluded.
|
| 23 |
|
|
|
|
| 25 |
|
| 26 |
Click a group to expand its direct lineages into a new column on the right; every ancestor column stays visible, so the full path from Eukaryota is always on screen. Clicking a group in an earlier column replaces the columns after it. Use breadcrumbs, “Collapse last group”, or shortcuts to go back. Nodes support Enter/Space keyboard navigation. The opening view expands the actual animal/fungal ancestor, Opisthokonta. Each column shows six direct lineages, an expandable remainder, and the selected lineage if it falls outside the top six. Bar and flow heights follow assembly counts within a column; every column is rescaled to the full height. “Other lineages” is an explicitly labeled display aggregate, not an invented clade; its counts sum the remaining siblings. Repeated expansion reaches all siblings without truncating them. Scientific-name/taxon-ID search also reaches individual taxa. Column spacing does not encode time. The view scrolls horizontally, and jumps to the newest column after each expansion.
|
| 27 |
|
| 28 |
+
English names come from `data/common_names.json`, built by `refresh_common_names.py` from NCBI Taxonomy's `genbank common name` and `common name` entries (about 9,200 taxa) plus a curated table of roughly 190 short glosses for the large unranked clades NCBI leaves unnamed. Glosses are plain-language summaries of the living members of a clade, not formal synonyms; the scientific name stays the primary label and a gloss that merely repeats it is dropped at build time. Rebuild it with `python refresh_common_names.py` once `.cache/coverage/taxdump.tar.gz` is present; the build fails if a curated taxid no longer matches its scientific name in the snapshot.
|
| 29 |
+
|
| 30 |
The graphic uses server-rendered SVG/HTML with scoped styles in `atlas.css` and delegated click/keyboard handlers in `atlas.js`; no external graphics library or image assets are required. Include these files when deploying.
|
| 31 |
|
| 32 |
The annotation workspace shares the atlas palette and typography through `style.py` and `app.css`: warm panels, serif section headings, green controls, and matching tables, downloads, and disclosures. CDS plots use a solid green positive strand and a dashed warm-brown negative strand; binary labels use green. The fixed light palette stays consistent with the landing graphic under either OS color preference. Include both style files when deploying; Gradio 6 receives the theme and stylesheet in `launch()`.
|
atlas.css
CHANGED
|
@@ -31,17 +31,19 @@
|
|
| 31 |
.bar-outline { fill:none; stroke:#f7f8f0; stroke-width:1; }
|
| 32 |
.tree-node { cursor:pointer; }
|
| 33 |
.tree-node.is-direct { cursor:default; }
|
| 34 |
-
.tree-hit { fill:transparent;
|
| 35 |
-
.tree-node:hover .tree-hit,.tree-node:focus .tree-hit { fill:#e5ecdd99; }
|
| 36 |
.tree-node:focus { outline:none; }
|
| 37 |
.tree-node:focus .bar-outline { stroke:#173c30; stroke-width:2; }
|
| 38 |
.tree-node.is-selected .bar-outline { stroke:#173c30; stroke-width:1.5; }
|
| 39 |
.tree-node.is-sibling { opacity:.62; transition:opacity .15s; }
|
| 40 |
.tree-node.is-sibling:hover,.tree-node.is-sibling:focus { opacity:1; }
|
| 41 |
-
.tree-label,.tree-count { paint-order:stroke; stroke:#f7f8f0; stroke-width:4px; stroke-linejoin:round; }
|
| 42 |
-
.tree-label { font:600 13px Arial,Helvetica,sans-serif; fill:#254735; }
|
| 43 |
.tree-node.is-selected .tree-label { font-weight:700; fill:#173c30; }
|
|
|
|
| 44 |
.tree-count { font:11px Arial,Helvetica,sans-serif; fill:#697966; }
|
|
|
|
|
|
|
| 45 |
.atlas-footer { display:flex; align-items:center; flex-wrap:wrap; gap:15px; justify-content:space-between; border-top:1px solid #dde4d5; padding-top:14px; color:#6c7a66; font-size:11px; line-height:1.6; }
|
| 46 |
.atlas-legend { display:flex; gap:7px; align-items:center; }
|
| 47 |
.atlas-swatch { width:10px; height:10px; border-radius:2px; }
|
|
|
|
| 31 |
.bar-outline { fill:none; stroke:#f7f8f0; stroke-width:1; }
|
| 32 |
.tree-node { cursor:pointer; }
|
| 33 |
.tree-node.is-direct { cursor:default; }
|
| 34 |
+
.tree-hit { fill:transparent; }
|
|
|
|
| 35 |
.tree-node:focus { outline:none; }
|
| 36 |
.tree-node:focus .bar-outline { stroke:#173c30; stroke-width:2; }
|
| 37 |
.tree-node.is-selected .bar-outline { stroke:#173c30; stroke-width:1.5; }
|
| 38 |
.tree-node.is-sibling { opacity:.62; transition:opacity .15s; }
|
| 39 |
.tree-node.is-sibling:hover,.tree-node.is-sibling:focus { opacity:1; }
|
| 40 |
+
.tree-label,.tree-english,.tree-count { paint-order:stroke; stroke:#f7f8f0; stroke-width:4px; stroke-linejoin:round; }
|
| 41 |
+
.tree-label { font:600 13px Arial,Helvetica,sans-serif; fill:#254735; transition:fill .15s; }
|
| 42 |
.tree-node.is-selected .tree-label { font-weight:700; fill:#173c30; }
|
| 43 |
+
.tree-english { font:italic 11.5px Georgia,'Times New Roman',serif; fill:#5d7a66; }
|
| 44 |
.tree-count { font:11px Arial,Helvetica,sans-serif; fill:#697966; }
|
| 45 |
+
.tree-node:not(.is-direct):hover .tree-label,.tree-node:focus .tree-label { fill:#0f2e22; text-decoration:underline; }
|
| 46 |
+
.tree-node:not(.is-direct):hover .bar-outline { stroke:#287557; stroke-width:2; }
|
| 47 |
.atlas-footer { display:flex; align-items:center; flex-wrap:wrap; gap:15px; justify-content:space-between; border-top:1px solid #dde4d5; padding-top:14px; color:#6c7a66; font-size:11px; line-height:1.6; }
|
| 48 |
.atlas-legend { display:flex; gap:7px; align-items:center; }
|
| 49 |
.atlas-swatch { width:10px; height:10px; border-radius:2px; }
|
data/common_names.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
refresh_common_names.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the English name lookup used by the Genome Atlas labels.
|
| 2 |
+
|
| 3 |
+
Two sources are merged, curated names winning over NCBI:
|
| 4 |
+
|
| 5 |
+
* NCBI Taxonomy ``names.dmp`` supplies ``genbank common name`` and ``common name``
|
| 6 |
+
for roughly 9,000 taxa, almost all of them species, genera and familiar classes.
|
| 7 |
+
* A curated table below covers the large unranked clades that NCBI leaves
|
| 8 |
+
unnamed. These are exactly the groups the atlas shows first — Opisthokonta,
|
| 9 |
+
Eumetazoa, Ecdysozoa and their kin — so without them the top of the tree reads
|
| 10 |
+
as untranslated Latin.
|
| 11 |
+
|
| 12 |
+
Curated glosses are short, lower-case noun phrases describing the living members
|
| 13 |
+
of the clade. They are plain-language summaries, not formal synonyms; the
|
| 14 |
+
scientific name stays the primary label everywhere in the interface.
|
| 15 |
+
"""
|
| 16 |
+
import argparse
|
| 17 |
+
from collections import Counter
|
| 18 |
+
from datetime import datetime, timezone
|
| 19 |
+
import json
|
| 20 |
+
from pathlib import Path
|
| 21 |
+
import sqlite3
|
| 22 |
+
import tarfile
|
| 23 |
+
|
| 24 |
+
ROOT = Path(__file__).resolve().parent
|
| 25 |
+
NAME_CLASSES = ("genbank common name", "common name")
|
| 26 |
+
|
| 27 |
+
# taxid: (scientific name, English gloss). The scientific name is verified
|
| 28 |
+
# against the snapshot so a wrong taxid fails the build instead of mislabelling
|
| 29 |
+
# a clade.
|
| 30 |
+
CURATED = {
|
| 31 |
+
2698737: ("Sar", "marine algae and protists"),
|
| 32 |
+
554915: ("Amoebozoa", "amoebas"),
|
| 33 |
+
2611352: ("Discoba", "flagellate protists"),
|
| 34 |
+
33682: ("Euglenozoa", "euglenids and trypanosomes"),
|
| 35 |
+
2704647: ("Metakinetoplastina", "trypanosomes and kin"),
|
| 36 |
+
2696291: ("Ochrophyta", "brown algae and diatoms"),
|
| 37 |
+
431838: ("Intramacronucleata", "ciliates"),
|
| 38 |
+
422676: ("Aconoidasida", "malaria parasites and kin"),
|
| 39 |
+
1280412: ("Conoidasida", "gut parasites"),
|
| 40 |
+
2692248: ("core chlorophytes", "green algae"),
|
| 41 |
+
3166: ("Chlorophyceae", "green algae"),
|
| 42 |
+
75966: ("Trebouxiophyceae", "green algae"),
|
| 43 |
+
1035538: ("Mamiellophyceae", "tiny marine green algae"),
|
| 44 |
+
13792: ("Mamiellales", "tiny marine green algae"),
|
| 45 |
+
127916: ("Ichthyosporea", "parasites of fish and kin"),
|
| 46 |
+
2687318: ("Filasterea", "single-celled animal relatives"),
|
| 47 |
+
2686024: ("Rotosphaerida", "amoeboid protists"),
|
| 48 |
+
6042: ("Demospongiae", "common sponges"),
|
| 49 |
+
1913637: ("Mucoromycota", "bread moulds and kin"),
|
| 50 |
+
451507: ("Mucoromycotina", "bread moulds"),
|
| 51 |
+
3704128: ("Kickxellomycota", "thread-like fungi"),
|
| 52 |
+
112252: ("Fungi incertae sedis", "fungi of uncertain position"),
|
| 53 |
+
33154: ("Opisthokonta", "animals and fungi"),
|
| 54 |
+
6072: ("Eumetazoa", "animals with true tissues"),
|
| 55 |
+
33213: ("Bilateria", "two-sided animals"),
|
| 56 |
+
33317: ("Protostomia", "mouth-first animals"),
|
| 57 |
+
1206794: ("Ecdysozoa", "moulting animals"),
|
| 58 |
+
2697495: ("Spiralia", "spiral-cleaving animals"),
|
| 59 |
+
1206795: ("Lophotrochozoa", "molluscs, worms and kin"),
|
| 60 |
+
88770: ("Panarthropoda", "arthropods and kin"),
|
| 61 |
+
3701028: ("Altocrustacea", "crustaceans and insects"),
|
| 62 |
+
3701030: ("Allotriocarida", "insects and some crustaceans"),
|
| 63 |
+
85512: ("Dicondylia", "most insects"),
|
| 64 |
+
33340: ("Neoptera", "wing-folding insects"),
|
| 65 |
+
33342: ("Paraneoptera", "bugs, lice and thrips"),
|
| 66 |
+
3701061: ("Eumetabola", "advanced insects"),
|
| 67 |
+
33392: ("Endopterygota", "insects with metamorphosis"),
|
| 68 |
+
3701062: ("Aparaglossata", "most metamorphic insects"),
|
| 69 |
+
3701063: ("Panorpida", "flies, moths and kin"),
|
| 70 |
+
3701064: ("Neuropteroidea", "beetles and lacewings"),
|
| 71 |
+
41084: ("Polyphaga", "most beetles"),
|
| 72 |
+
41088: ("Cucujiformia", "plant-eating beetles"),
|
| 73 |
+
7434: ("Aculeata", "bees, wasps and ants"),
|
| 74 |
+
85604: ("Amphiesmenoptera", "moths and caddisflies"),
|
| 75 |
+
41191: ("Glossata", "moths and butterflies"),
|
| 76 |
+
41196: ("Neolepidoptera", "moths and butterflies"),
|
| 77 |
+
41197: ("Heteroneura", "moths and butterflies"),
|
| 78 |
+
37567: ("Ditrysia", "most moths and butterflies"),
|
| 79 |
+
104430: ("Apoditrysia", "moths and butterflies"),
|
| 80 |
+
104431: ("Obtectomera", "macro-moths and butterflies"),
|
| 81 |
+
37570: ("Noctuoidea", "owlet moths"),
|
| 82 |
+
40092: ("Hesperioidea", "skipper butterflies"),
|
| 83 |
+
37568: ("Tortricoidea", "leafroller moths"),
|
| 84 |
+
7139: ("Tortricidae", "leafroller moths"),
|
| 85 |
+
7203: ("Brachycera", "short-horned flies"),
|
| 86 |
+
480118: ("Eremoneura", "advanced flies"),
|
| 87 |
+
480117: ("Cyclorrhapha", "advanced flies"),
|
| 88 |
+
43733: ("Muscomorpha", "house flies and kin"),
|
| 89 |
+
43738: ("Schizophora", "advanced flies"),
|
| 90 |
+
43741: ("Acalyptratae", "small flies"),
|
| 91 |
+
43746: ("Ephydroidea", "fruit flies and kin"),
|
| 92 |
+
43845: ("Drosophilinae", "fruit flies"),
|
| 93 |
+
46877: ("Drosophilini", "fruit flies"),
|
| 94 |
+
119089: ("Chromadorea", "roundworms"),
|
| 95 |
+
6236: ("Rhabditida", "roundworms"),
|
| 96 |
+
89593: ("Craniata", "animals with a skull"),
|
| 97 |
+
117570: ("Teleostomi", "jawed vertebrates with bone"),
|
| 98 |
+
8287: ("Sarcopterygii", "lobe-finned fish and kin"),
|
| 99 |
+
1338369: ("Dipnotetrapodomorpha", "lungfish and land vertebrates"),
|
| 100 |
+
186623: ("Actinopteri", "ray-finned fish"),
|
| 101 |
+
41665: ("Neopterygii", "modern ray-finned fish"),
|
| 102 |
+
1489341: ("Osteoglossocephalai", "bony-tongued fish and kin"),
|
| 103 |
+
186625: ("Clupeocephala", "most teleost fish"),
|
| 104 |
+
1489388: ("Euteleosteomorpha", "advanced teleost fish"),
|
| 105 |
+
123365: ("Neoteleostei", "advanced bony fish"),
|
| 106 |
+
123366: ("Eurypterygia", "advanced bony fish"),
|
| 107 |
+
123367: ("Ctenosquamata", "advanced bony fish"),
|
| 108 |
+
123368: ("Acanthomorphata", "spiny-rayed fish"),
|
| 109 |
+
123369: ("Euacanthomorphacea", "spiny-rayed fish"),
|
| 110 |
+
1489872: ("Percomorphaceae", "perch-like fish"),
|
| 111 |
+
1489922: ("Eupercaria", "perch-like fish"),
|
| 112 |
+
1489908: ("Ovalentaria", "cichlids and relatives"),
|
| 113 |
+
1489910: ("Cichlomorphae", "cichlids and kin"),
|
| 114 |
+
318546: ("Pseudocrenilabrinae", "African cichlids"),
|
| 115 |
+
186634: ("Otomorpha", "carps, catfish and herrings"),
|
| 116 |
+
32519: ("Ostariophysi", "carps and catfish"),
|
| 117 |
+
186626: ("Otophysi", "carps and catfish"),
|
| 118 |
+
186627: ("Cypriniphysae", "carps and loaches"),
|
| 119 |
+
1329799: ("Archelosauria", "turtles, birds and crocodiles"),
|
| 120 |
+
8492: ("Archosauria", "birds and crocodiles"),
|
| 121 |
+
436489: ("Saurischia", "dinosaurs, birds today"),
|
| 122 |
+
436491: ("Theropoda", "dinosaurs, birds today"),
|
| 123 |
+
436492: ("Coelurosauria", "dinosaurs, birds today"),
|
| 124 |
+
8825: ("Neognathae", "most modern birds"),
|
| 125 |
+
3078114: ("Neoaves", "most modern birds"),
|
| 126 |
+
3073809: ("Australaves", "parrots and songbirds"),
|
| 127 |
+
1329950: ("Unidentata", "lizards and snakes"),
|
| 128 |
+
1329912: ("Episquamata", "lizards and snakes"),
|
| 129 |
+
32525: ("Theria", "live-bearing mammals"),
|
| 130 |
+
1437010: ("Boreoeutheria", "most placental mammals"),
|
| 131 |
+
314146: ("Euarchontoglires", "primates and rodents"),
|
| 132 |
+
314145: ("Laurasiatheria", "carnivores, bats and ungulates"),
|
| 133 |
+
376913: ("Haplorrhini", "monkeys, apes and tarsiers"),
|
| 134 |
+
314293: ("Simiiformes", "monkeys and apes"),
|
| 135 |
+
9526: ("Catarrhini", "Old World monkeys and apes"),
|
| 136 |
+
207598: ("Homininae", "great apes and humans"),
|
| 137 |
+
9845: ("Ruminantia", "ruminants"),
|
| 138 |
+
35500: ("Pecora", "deer, cattle and kin"),
|
| 139 |
+
9895: ("Bovidae", "cattle, sheep and antelope"),
|
| 140 |
+
451864: ("Dikarya", "higher fungi"),
|
| 141 |
+
716545: ("saccharomyceta", "yeasts and sac fungi"),
|
| 142 |
+
716546: ("leotiomyceta", "filamentous sac fungi"),
|
| 143 |
+
715989: ("sordariomyceta", "flask fungi and kin"),
|
| 144 |
+
1520881: ("OSLEUM clade", "filamentous sac fungi"),
|
| 145 |
+
147550: ("Sordariomycetes", "flask fungi"),
|
| 146 |
+
222544: ("Sordariomycetidae", "flask fungi"),
|
| 147 |
+
5139: ("Sordariales", "flask fungi"),
|
| 148 |
+
222543: ("Hypocreomycetidae", "mould-forming sac fungi"),
|
| 149 |
+
5125: ("Hypocreales", "moulds and insect fungi"),
|
| 150 |
+
110618: ("Nectriaceae", "Fusarium and relatives"),
|
| 151 |
+
474943: ("Cordycipitaceae", "insect-killing fungi"),
|
| 152 |
+
1028384: ("Glomerellales", "anthracnose fungi"),
|
| 153 |
+
681950: ("Glomerellaceae", "anthracnose fungi"),
|
| 154 |
+
5455: ("Colletotrichum", "anthracnose fungi"),
|
| 155 |
+
639021: ("Magnaporthales", "rice blast fungi"),
|
| 156 |
+
2528436: ("Pyriculariaceae", "rice blast fungi"),
|
| 157 |
+
48558: ("Pyricularia", "rice blast fungi"),
|
| 158 |
+
715962: ("dothideomyceta", "sooty moulds and kin"),
|
| 159 |
+
147541: ("Dothideomycetes", "sooty moulds and leaf fungi"),
|
| 160 |
+
451867: ("Dothideomycetidae", "sooty moulds"),
|
| 161 |
+
451868: ("Pleosporomycetidae", "leaf-spot fungi"),
|
| 162 |
+
92860: ("Pleosporales", "leaf-spot fungi"),
|
| 163 |
+
715340: ("Pleosporineae", "leaf-spot fungi"),
|
| 164 |
+
28556: ("Pleosporaceae", "leaf-spot fungi"),
|
| 165 |
+
451869: ("Botryosphaeriales", "canker fungi"),
|
| 166 |
+
45131: ("Botryosphaeriaceae", "canker fungi"),
|
| 167 |
+
35724: ("Macrophomina", "charcoal rot fungi"),
|
| 168 |
+
147545: ("Eurotiomycetes", "moulds and relatives"),
|
| 169 |
+
451871: ("Eurotiomycetidae", "moulds and relatives"),
|
| 170 |
+
1131492: ("Aspergillaceae", "Aspergillus and Penicillium"),
|
| 171 |
+
33183: ("Onygenales", "skin and hair fungi"),
|
| 172 |
+
147548: ("Leotiomycetes", "cup fungi and moulds"),
|
| 173 |
+
4891: ("Saccharomycetes", "budding yeasts"),
|
| 174 |
+
4892: ("Saccharomycetales", "budding yeasts"),
|
| 175 |
+
4893: ("Saccharomycetaceae", "baker's yeast family"),
|
| 176 |
+
4930: ("Saccharomyces", "baker's yeasts"),
|
| 177 |
+
3239874: ("Pichiomycetes", "budding yeasts"),
|
| 178 |
+
3243775: ("Pichiales", "budding yeasts"),
|
| 179 |
+
1156497: ("Pichiaceae", "budding yeasts"),
|
| 180 |
+
3239873: ("Dipodascomycetes", "budding yeasts"),
|
| 181 |
+
3243772: ("Dipodascales", "budding yeasts"),
|
| 182 |
+
766764: ("Debaryomycetaceae", "yeasts"),
|
| 183 |
+
27319: ("Metschnikowiaceae", "yeasts"),
|
| 184 |
+
5302: ("Agaricomycotina", "mushroom-forming fungi"),
|
| 185 |
+
155619: ("Agaricomycetes", "mushrooms and bracket fungi"),
|
| 186 |
+
452333: ("Agaricomycetidae", "mushroom-forming fungi"),
|
| 187 |
+
68889: ("Boletales", "boletes"),
|
| 188 |
+
155616: ("Tremellomycetes", "jelly fungi"),
|
| 189 |
+
4762: ("Oomycota", "water moulds"),
|
| 190 |
+
3418804: ("Peronosporomycetes", "water moulds"),
|
| 191 |
+
35493: ("Streptophyta", "land plants and their algae"),
|
| 192 |
+
131221: ("Streptophytina", "land plants and stoneworts"),
|
| 193 |
+
78536: ("Euphyllophyta", "ferns and seed plants"),
|
| 194 |
+
1437183: ("Mesangiospermae", "core flowering plants"),
|
| 195 |
+
1437197: ("Petrosaviidae", "most monocot plants"),
|
| 196 |
+
4734: ("commelinids", "grasses, palms and kin"),
|
| 197 |
+
38820: ("Poales", "grasses and sedges"),
|
| 198 |
+
147368: ("Pooideae", "temperate grasses"),
|
| 199 |
+
359160: ("BOP clade", "bamboo, rice and wheat grasses"),
|
| 200 |
+
1648038: ("Triticodae", "wheat and barley grasses"),
|
| 201 |
+
91827: ("Gunneridae", "core eudicot plants"),
|
| 202 |
+
1437201: ("Pentapetalae", "core eudicot plants"),
|
| 203 |
+
71275: ("rosids", "rose-related plants"),
|
| 204 |
+
91835: ("fabids", "legume-related plants"),
|
| 205 |
+
91836: ("malvids", "mallow-related plants"),
|
| 206 |
+
71274: ("asterids", "daisy-related plants"),
|
| 207 |
+
91888: ("lamiids", "mint-related plants"),
|
| 208 |
+
72025: ("Fabales", "legumes and kin"),
|
| 209 |
+
3814: ("Papilionoideae", "pea-flowered legumes"),
|
| 210 |
+
2231393: ("50 kb inversion clade", "legumes"),
|
| 211 |
+
3699: ("Brassicales", "cabbages and mustards"),
|
| 212 |
+
980083: ("Camelineae", "thale cress and kin"),
|
| 213 |
+
3701: ("Arabidopsis", "thale cresses"),
|
| 214 |
+
3744: ("Rosales", "roses, figs and kin"),
|
| 215 |
+
3524: ("Caryophyllales", "cacti, beets and kin"),
|
| 216 |
+
3646: ("Malpighiales", "willows and spurges"),
|
| 217 |
+
4069: ("Solanales", "nightshades and kin"),
|
| 218 |
+
4143: ("Lamiales", "mints and kin"),
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
def dump_rows(archive, name):
|
| 223 |
+
with archive.extractfile(name) as stream:
|
| 224 |
+
for line in stream:
|
| 225 |
+
yield [part.strip() for part in line.decode("utf-8").split("|")]
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
def build(taxdump, database, output):
|
| 229 |
+
with sqlite3.connect(f"{Path(database).resolve().as_uri()}?mode=ro", uri=True) as conn:
|
| 230 |
+
known = dict(conn.execute("SELECT taxid, name FROM taxa"))
|
| 231 |
+
wrong = {t: (want, known.get(t)) for t, (want, _) in CURATED.items() if known.get(t) != want}
|
| 232 |
+
if wrong:
|
| 233 |
+
raise ValueError(f"Curated taxids do not match the snapshot: {wrong}")
|
| 234 |
+
|
| 235 |
+
names, ranked = {}, {}
|
| 236 |
+
with tarfile.open(taxdump, "r:gz") as archive:
|
| 237 |
+
for row in dump_rows(archive, "names.dmp"):
|
| 238 |
+
taxid = int(row[0])
|
| 239 |
+
if taxid not in known or row[3] not in NAME_CLASSES:
|
| 240 |
+
continue
|
| 241 |
+
rank = NAME_CLASSES.index(row[3])
|
| 242 |
+
if taxid not in ranked or rank < ranked[taxid]:
|
| 243 |
+
names[taxid], ranked[taxid] = row[1], rank
|
| 244 |
+
sources = Counter(ncbi=len(names))
|
| 245 |
+
for taxid, (_, english) in CURATED.items():
|
| 246 |
+
sources["curated_override" if taxid in names else "curated"] += 1
|
| 247 |
+
names[taxid] = english
|
| 248 |
+
# A gloss that only repeats the scientific name teaches nothing (Fungi ->
|
| 249 |
+
# "fungi"), and the interface prints the scientific name anyway.
|
| 250 |
+
redundant = [t for t, english in names.items() if english.casefold() == known[t].casefold()]
|
| 251 |
+
for taxid in redundant:
|
| 252 |
+
del names[taxid]
|
| 253 |
+
payload = {"created_at": datetime.now(timezone.utc).isoformat(),
|
| 254 |
+
"sources": {"ncbi_taxonomy": "https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz",
|
| 255 |
+
"name_classes": list(NAME_CLASSES),
|
| 256 |
+
"curated": "refresh_common_names.py, plain-language glosses for unnamed clades"},
|
| 257 |
+
"counts": dict(sources, redundant_dropped=len(redundant), total=len(names)),
|
| 258 |
+
"names": {str(t): names[t] for t in sorted(names)}}
|
| 259 |
+
Path(output).write_text(json.dumps(payload, indent=1, sort_keys=False))
|
| 260 |
+
print(json.dumps(payload["counts"], indent=2))
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def main():
|
| 264 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 265 |
+
parser.add_argument("--taxdump", type=Path, default=ROOT / ".cache/coverage/taxdump.tar.gz")
|
| 266 |
+
parser.add_argument("--database", type=Path, default=ROOT / "data/taxonomy.sqlite")
|
| 267 |
+
parser.add_argument("--output", type=Path, default=ROOT / "data/common_names.json")
|
| 268 |
+
args = parser.parse_args()
|
| 269 |
+
build(args.taxdump, args.database, args.output)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
if __name__ == "__main__":
|
| 273 |
+
main()
|
taxonomy.py
CHANGED
|
@@ -9,15 +9,17 @@ import gradio as gr
|
|
| 9 |
|
| 10 |
ROOT = Path(__file__).resolve().parent
|
| 11 |
DATABASE = ROOT / "data/taxonomy.sqlite"
|
|
|
|
| 12 |
EUKARYOTA = 2759
|
| 13 |
EUK_TREE = """WITH RECURSIVE euk(taxid) AS (
|
| 14 |
SELECT 2759 UNION ALL
|
| 15 |
SELECT t.taxid FROM taxa t JOIN euk e ON t.parent_id=e.taxid
|
| 16 |
WHERE t.taxid != t.parent_id
|
| 17 |
) """
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
| 21 |
|
| 22 |
|
| 23 |
COLUMN_LIMIT = 6
|
|
@@ -31,8 +33,13 @@ def encode_path(steps):
|
|
| 31 |
|
| 32 |
|
| 33 |
class Taxonomy:
|
| 34 |
-
def __init__(self, path=DATABASE):
|
| 35 |
self.path = Path(path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
with closing(self.connect()) as conn:
|
| 37 |
self.metadata = json.loads(conn.execute("SELECT value FROM metadata").fetchone()[0])
|
| 38 |
self.eukaryote_ids = frozenset(r[0] for r in conn.execute(EUK_TREE + "SELECT taxid FROM euk"))
|
|
@@ -87,13 +94,13 @@ class Taxonomy:
|
|
| 87 |
return dict(conn.execute("SELECT * FROM taxa WHERE taxid=?", (t,)).fetchone())
|
| 88 |
|
| 89 |
def node(row, offset=0, **extra):
|
| 90 |
-
return dict(taxid=row['taxid'], offset=offset, name=row['name'],
|
| 91 |
-
|
| 92 |
total=row['total_count'], covered=row.get('covered_total', 0), **extra)
|
| 93 |
|
| 94 |
def remainder(rows, parent, start):
|
| 95 |
return dict(taxid=parent, offset=start, name=f"Other lineages · {len(rows):,} groups (display aggregate)",
|
| 96 |
-
label=f"Other lineages ({len(rows):,})", total=sum(r['total_count'] for r in rows),
|
| 97 |
covered=sum(r.get('covered_total', 0) for r in rows), aggregate=True)
|
| 98 |
|
| 99 |
def column(step, selected):
|
|
@@ -111,7 +118,7 @@ class Taxonomy:
|
|
| 111 |
row = taxon(parent)
|
| 112 |
if not offset and row['direct_count'] and rows:
|
| 113 |
nodes.append(dict(taxid=parent, offset=0, name="Assemblies assigned directly to this taxon",
|
| 114 |
-
label="Direct assignments", total=row['direct_count'],
|
| 115 |
covered=row.get('covered_direct', 0), direct=True))
|
| 116 |
return nodes
|
| 117 |
|
|
@@ -142,7 +149,7 @@ class Taxonomy:
|
|
| 142 |
|
| 143 |
def stack(nodes):
|
| 144 |
"""Sankey layout: bar heights follow assembly counts, with room for every label."""
|
| 145 |
-
gap, slot = (12 if len(nodes) > 1 else 0),
|
| 146 |
fixed, scale = set(), 0
|
| 147 |
for _ in range(len(nodes) + 1):
|
| 148 |
free = chart - gap * (len(nodes) - 1) - slot * len(fixed)
|
|
@@ -159,6 +166,9 @@ class Taxonomy:
|
|
| 159 |
n['y'] = y + (room - n['h']) / 2
|
| 160 |
y += room + gap
|
| 161 |
|
|
|
|
|
|
|
|
|
|
| 162 |
def share(n):
|
| 163 |
return n['covered'] / n['total'] if self.coverage and n['total'] else 0
|
| 164 |
|
|
@@ -198,8 +208,9 @@ class Taxonomy:
|
|
| 198 |
x, y, h = n['x'], n['y'], n['h']
|
| 199 |
f = share(n)
|
| 200 |
percent = 100 * f
|
| 201 |
-
|
| 202 |
-
|
|
|
|
| 203 |
if n.get('aggregate'):
|
| 204 |
detail += " Display aggregate, not a taxonomic clade. Activate to see its lineages."
|
| 205 |
elif not n.get('direct') and not n['selected']:
|
|
@@ -207,19 +218,24 @@ class Taxonomy:
|
|
| 207 |
action = "" if n.get('direct') else f'data-path="{n["path"]}" role="button" tabindex="0"'
|
| 208 |
classes = "tree-node" + (" is-selected" if n['selected'] else " is-sibling" if on_path else "") + \
|
| 209 |
(" is-direct" if n.get('direct') else "")
|
| 210 |
-
nodes.append(f'<g class="{classes}" {action} aria-label="{escape(detail, quote=True)}" data-detail="{escape(detail, quote=True)}">
|
| 211 |
cy = y + h / 2
|
| 212 |
-
nodes.append(f'<rect x="{x - 6}" y="{min(y, cy -
|
| 213 |
green = h * f
|
| 214 |
if green > 0:
|
| 215 |
nodes.append(f'<rect x="{x}" y="{y:.1f}" width="{bar}" height="{green:.1f}" class="bar bar-annotated"/>')
|
| 216 |
if green < h:
|
| 217 |
nodes.append(f'<rect x="{x}" y="{y + green:.1f}" width="{bar}" height="{h - green:.1f}" class="bar bar-missing"/>')
|
| 218 |
nodes.append(f'<rect x="{x}" y="{y:.1f}" width="{bar}" height="{h:.1f}" class="bar-outline"/>')
|
| 219 |
-
label = n['label']
|
| 220 |
stats = f"{percent:.1f}% · {n['covered']:,} / {n['total']:,}" if self.coverage else f"{n['total']:,} assemblies"
|
| 221 |
-
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
svg = (f'<svg viewBox="0 0 {width} {height}" class="life-tree" data-route="{encode_path(steps)}" style="min-width:{width if width > 1040 else 760}px" '
|
| 224 |
f'aria-label="Eukaryotic taxonomy and annotation coverage">' + ''.join(flows) + ''.join(nodes) + '</svg>')
|
| 225 |
|
|
@@ -227,9 +243,11 @@ class Taxonomy:
|
|
| 227 |
total, covered = focus['total'], focus['covered']
|
| 228 |
percent = 100 * covered / total if self.coverage and total else 0
|
| 229 |
selected_name = f"Other lineages of {lineage[-1]['name']}" if focus.get('aggregate') else focus['label']
|
|
|
|
|
|
|
| 230 |
crumbs = []
|
| 231 |
for depth, (row, (_, offset)) in enumerate(zip(lineage, steps)):
|
| 232 |
-
name = "Other lineages" if offset else
|
| 233 |
crumbs.append(f'<button data-path="{encode_path(steps[:depth + 1])}">{escape(name)}</button>')
|
| 234 |
breadcrumbs = ' <span class="crumb-divider">/</span> '.join(crumbs)
|
| 235 |
parent = encode_path(steps[:-1] or steps)
|
|
@@ -307,11 +325,15 @@ def build_taxonomy_tab():
|
|
| 307 |
"Each expanded group adds a column on the right with up to six direct lineages plus any remainder, so every ancestor stays visible. "
|
| 308 |
"The opening view also expands the animal/fungal branch. Bars and flows are drawn like a Sankey diagram: "
|
| 309 |
"heights follow **actual assembly counts** within each column, split into annotated and not-annotated assemblies.\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
"Coverage means the fraction of current GenBank assembly versions with published annotations, including partial assemblies. "
|
| 311 |
"It does not measure the fraction of bases annotated. Parent counts include descendants once each. "
|
| 312 |
"Exact accession versions are matched. The Database tab searches a dated snapshot of all published annotation files.\n\n"
|
| 313 |
"Sources: [NCBI assembly summary](https://ftp.ncbi.nlm.nih.gov/genomes/ASSEMBLY_REPORTS/assembly_summary_genbank.txt), "
|
| 314 |
-
"[NCBI Taxonomy](https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz), "
|
| 315 |
"and [published annotations](https://huggingface.co/buckets/HuggingFaceBio/genbank-annotations).")
|
| 316 |
coverage = taxonomy.coverage or {}
|
| 317 |
gr.JSON({"taxonomy_updated": taxonomy.metadata['created_at'], "coverage_updated": coverage.get('created_at'),
|
|
|
|
| 9 |
|
| 10 |
ROOT = Path(__file__).resolve().parent
|
| 11 |
DATABASE = ROOT / "data/taxonomy.sqlite"
|
| 12 |
+
COMMON_NAMES = ROOT / "data/common_names.json"
|
| 13 |
EUKARYOTA = 2759
|
| 14 |
EUK_TREE = """WITH RECURSIVE euk(taxid) AS (
|
| 15 |
SELECT 2759 UNION ALL
|
| 16 |
SELECT t.taxid FROM taxa t JOIN euk e ON t.parent_id=e.taxid
|
| 17 |
WHERE t.taxid != t.parent_id
|
| 18 |
) """
|
| 19 |
+
# Scientific names stay the primary label; English glosses are a secondary line.
|
| 20 |
+
# See refresh_common_names.py for how the lookup is built.
|
| 21 |
+
MAX_ENGLISH = 30
|
| 22 |
+
MAX_LABEL = 24
|
| 23 |
|
| 24 |
|
| 25 |
COLUMN_LIMIT = 6
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
class Taxonomy:
|
| 36 |
+
def __init__(self, path=DATABASE, common_names=COMMON_NAMES):
|
| 37 |
self.path = Path(path)
|
| 38 |
+
self.english, self.english_sources = {}, {}
|
| 39 |
+
if Path(common_names).exists():
|
| 40 |
+
payload = json.loads(Path(common_names).read_text())
|
| 41 |
+
self.english = {int(t): name for t, name in payload["names"].items()}
|
| 42 |
+
self.english_sources = payload.get("sources", {})
|
| 43 |
with closing(self.connect()) as conn:
|
| 44 |
self.metadata = json.loads(conn.execute("SELECT value FROM metadata").fetchone()[0])
|
| 45 |
self.eukaryote_ids = frozenset(r[0] for r in conn.execute(EUK_TREE + "SELECT taxid FROM euk"))
|
|
|
|
| 94 |
return dict(conn.execute("SELECT * FROM taxa WHERE taxid=?", (t,)).fetchone())
|
| 95 |
|
| 96 |
def node(row, offset=0, **extra):
|
| 97 |
+
return dict(taxid=row['taxid'], offset=offset, name=row['name'], label=row['name'],
|
| 98 |
+
english=self.english.get(row['taxid'], ""),
|
| 99 |
total=row['total_count'], covered=row.get('covered_total', 0), **extra)
|
| 100 |
|
| 101 |
def remainder(rows, parent, start):
|
| 102 |
return dict(taxid=parent, offset=start, name=f"Other lineages · {len(rows):,} groups (display aggregate)",
|
| 103 |
+
label=f"Other lineages ({len(rows):,})", english="", total=sum(r['total_count'] for r in rows),
|
| 104 |
covered=sum(r.get('covered_total', 0) for r in rows), aggregate=True)
|
| 105 |
|
| 106 |
def column(step, selected):
|
|
|
|
| 118 |
row = taxon(parent)
|
| 119 |
if not offset and row['direct_count'] and rows:
|
| 120 |
nodes.append(dict(taxid=parent, offset=0, name="Assemblies assigned directly to this taxon",
|
| 121 |
+
label="Direct assignments", english="", total=row['direct_count'],
|
| 122 |
covered=row.get('covered_direct', 0), direct=True))
|
| 123 |
return nodes
|
| 124 |
|
|
|
|
| 149 |
|
| 150 |
def stack(nodes):
|
| 151 |
"""Sankey layout: bar heights follow assembly counts, with room for every label."""
|
| 152 |
+
gap, slot = (12 if len(nodes) > 1 else 0), 46
|
| 153 |
fixed, scale = set(), 0
|
| 154 |
for _ in range(len(nodes) + 1):
|
| 155 |
free = chart - gap * (len(nodes) - 1) - slot * len(fixed)
|
|
|
|
| 166 |
n['y'] = y + (room - n['h']) / 2
|
| 167 |
y += room + gap
|
| 168 |
|
| 169 |
+
def clip(text, limit):
|
| 170 |
+
return text if len(text) <= limit else text[:limit - 2] + "…"
|
| 171 |
+
|
| 172 |
def share(n):
|
| 173 |
return n['covered'] / n['total'] if self.coverage and n['total'] else 0
|
| 174 |
|
|
|
|
| 208 |
x, y, h = n['x'], n['y'], n['h']
|
| 209 |
f = share(n)
|
| 210 |
percent = 100 * f
|
| 211 |
+
title = f"{n['name']} ({n['english']})" if n['english'] else n['name']
|
| 212 |
+
detail = (f"{title} · {n['covered']:,} of {n['total']:,} assemblies have published annotations ({percent:.2f}%). Includes partial assemblies."
|
| 213 |
+
if self.coverage else f"{title} · {n['total']:,} assemblies. Coverage inventory unavailable.")
|
| 214 |
if n.get('aggregate'):
|
| 215 |
detail += " Display aggregate, not a taxonomic clade. Activate to see its lineages."
|
| 216 |
elif not n.get('direct') and not n['selected']:
|
|
|
|
| 218 |
action = "" if n.get('direct') else f'data-path="{n["path"]}" role="button" tabindex="0"'
|
| 219 |
classes = "tree-node" + (" is-selected" if n['selected'] else " is-sibling" if on_path else "") + \
|
| 220 |
(" is-direct" if n.get('direct') else "")
|
| 221 |
+
nodes.append(f'<g class="{classes}" {action} aria-label="{escape(detail, quote=True)}" data-detail="{escape(detail, quote=True)}">')
|
| 222 |
cy = y + h / 2
|
| 223 |
+
nodes.append(f'<rect x="{x - 6}" y="{min(y, cy - 23):.1f}" width="{spacing - 40}" height="{max(h, 46):.1f}" rx="10" class="tree-hit"/>')
|
| 224 |
green = h * f
|
| 225 |
if green > 0:
|
| 226 |
nodes.append(f'<rect x="{x}" y="{y:.1f}" width="{bar}" height="{green:.1f}" class="bar bar-annotated"/>')
|
| 227 |
if green < h:
|
| 228 |
nodes.append(f'<rect x="{x}" y="{y + green:.1f}" width="{bar}" height="{h - green:.1f}" class="bar bar-missing"/>')
|
| 229 |
nodes.append(f'<rect x="{x}" y="{y:.1f}" width="{bar}" height="{h:.1f}" class="bar-outline"/>')
|
| 230 |
+
label, english = clip(n['label'], MAX_LABEL), clip(n['english'], MAX_ENGLISH)
|
| 231 |
stats = f"{percent:.1f}% · {n['covered']:,} / {n['total']:,}" if self.coverage else f"{n['total']:,} assemblies"
|
| 232 |
+
text_x = x + bar + 8
|
| 233 |
+
# Three lines when there is an English gloss, two without, both centred on the bar.
|
| 234 |
+
head, stat_line = (cy - 10, cy + 16) if english else (cy - 3, cy + 13)
|
| 235 |
+
nodes.append(f'<text x="{text_x}" y="{head:.1f}" class="tree-label">{escape(label)}</text>')
|
| 236 |
+
if english:
|
| 237 |
+
nodes.append(f'<text x="{text_x}" y="{cy + 3:.1f}" class="tree-english">{escape(english)}</text>')
|
| 238 |
+
nodes.append(f'<text x="{text_x}" y="{stat_line:.1f}" class="tree-count">{stats}</text></g>')
|
| 239 |
svg = (f'<svg viewBox="0 0 {width} {height}" class="life-tree" data-route="{encode_path(steps)}" style="min-width:{width if width > 1040 else 760}px" '
|
| 240 |
f'aria-label="Eukaryotic taxonomy and annotation coverage">' + ''.join(flows) + ''.join(nodes) + '</svg>')
|
| 241 |
|
|
|
|
| 243 |
total, covered = focus['total'], focus['covered']
|
| 244 |
percent = 100 * covered / total if self.coverage and total else 0
|
| 245 |
selected_name = f"Other lineages of {lineage[-1]['name']}" if focus.get('aggregate') else focus['label']
|
| 246 |
+
if focus.get('english'):
|
| 247 |
+
selected_name += f" · {focus['english']}"
|
| 248 |
crumbs = []
|
| 249 |
for depth, (row, (_, offset)) in enumerate(zip(lineage, steps)):
|
| 250 |
+
name = "Other lineages" if offset else row['name']
|
| 251 |
crumbs.append(f'<button data-path="{encode_path(steps[:depth + 1])}">{escape(name)}</button>')
|
| 252 |
breadcrumbs = ' <span class="crumb-divider">/</span> '.join(crumbs)
|
| 253 |
parent = encode_path(steps[:-1] or steps)
|
|
|
|
| 325 |
"Each expanded group adds a column on the right with up to six direct lineages plus any remainder, so every ancestor stays visible. "
|
| 326 |
"The opening view also expands the animal/fungal branch. Bars and flows are drawn like a Sankey diagram: "
|
| 327 |
"heights follow **actual assembly counts** within each column, split into annotated and not-annotated assemblies.\n\n"
|
| 328 |
+
"Each group shows its scientific name with a plain-English name underneath where one is available. These come from "
|
| 329 |
+
"NCBI Taxonomy's common names plus a curated set of short glosses for the large unranked clades NCBI leaves unnamed, "
|
| 330 |
+
"such as Opisthokonta (animals and fungi) or Ecdysozoa (moulting animals). The glosses describe the living members of a "
|
| 331 |
+
"clade in everyday words; they are not formal taxonomic synonyms, so the scientific name stays the primary label.\n\n"
|
| 332 |
"Coverage means the fraction of current GenBank assembly versions with published annotations, including partial assemblies. "
|
| 333 |
"It does not measure the fraction of bases annotated. Parent counts include descendants once each. "
|
| 334 |
"Exact accession versions are matched. The Database tab searches a dated snapshot of all published annotation files.\n\n"
|
| 335 |
"Sources: [NCBI assembly summary](https://ftp.ncbi.nlm.nih.gov/genomes/ASSEMBLY_REPORTS/assembly_summary_genbank.txt), "
|
| 336 |
+
"[NCBI Taxonomy](https://ftp.ncbi.nlm.nih.gov/pub/taxonomy/taxdump.tar.gz) for both the tree and its common names, "
|
| 337 |
"and [published annotations](https://huggingface.co/buckets/HuggingFaceBio/genbank-annotations).")
|
| 338 |
coverage = taxonomy.coverage or {}
|
| 339 |
gr.JSON({"taxonomy_updated": taxonomy.metadata['created_at'], "coverage_updated": coverage.get('created_at'),
|
tests/test_taxonomy.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
|
|
| 1 |
import re
|
|
|
|
| 2 |
import unittest
|
| 3 |
|
| 4 |
-
from
|
|
|
|
| 5 |
|
| 6 |
|
| 7 |
@unittest.skipUnless(DATABASE.exists(), "taxonomy snapshot unavailable")
|
|
@@ -30,6 +33,37 @@ class TaxonomyTests(unittest.TestCase):
|
|
| 30 |
html = self.taxonomy.view("2759/2759~6")
|
| 31 |
self.assertIn('data-path="2759/2759~6/2759~12"', html)
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
def test_rejects_paths_outside_the_tree(self):
|
| 34 |
for path in ("1", "33208", "2759/33208", "2759/2759~3", "2759/9606", "abc"):
|
| 35 |
with self.assertRaises(ValueError, msg=path):
|
|
|
|
| 1 |
+
import json
|
| 2 |
import re
|
| 3 |
+
import sqlite3
|
| 4 |
import unittest
|
| 5 |
|
| 6 |
+
from refresh_common_names import CURATED
|
| 7 |
+
from taxonomy import COMMON_NAMES, DATABASE, DEFAULT_PATH, MAX_ENGLISH, Taxonomy
|
| 8 |
|
| 9 |
|
| 10 |
@unittest.skipUnless(DATABASE.exists(), "taxonomy snapshot unavailable")
|
|
|
|
| 33 |
html = self.taxonomy.view("2759/2759~6")
|
| 34 |
self.assertIn('data-path="2759/2759~6/2759~12"', html)
|
| 35 |
|
| 36 |
+
def test_english_names_label_latin_clades(self):
|
| 37 |
+
html = self.taxonomy.view("2759/33154/33208")
|
| 38 |
+
for scientific, english in (("Opisthokonta", "animals and fungi"), ("Metazoa", "animals"),
|
| 39 |
+
("Eumetazoa", "animals with true tissues"), ("Porifera", "sponges")):
|
| 40 |
+
self.assertIn(f'class="tree-label">{scientific}</text>', html)
|
| 41 |
+
self.assertIn(f'class="tree-english">{english}</text>', html)
|
| 42 |
+
# A gloss that only repeats the scientific name is dropped by the build.
|
| 43 |
+
self.assertIn('class="tree-label">Fungi</text>', html)
|
| 44 |
+
self.assertNotIn('class="tree-english">fungi</text>', html)
|
| 45 |
+
|
| 46 |
+
def test_one_tooltip_per_node(self):
|
| 47 |
+
html = self.taxonomy.view(DEFAULT_PATH)
|
| 48 |
+
# <title> would render the browser's own tooltip on top of the styled one.
|
| 49 |
+
self.assertNotIn("<title>", html)
|
| 50 |
+
self.assertIn('data-detail="Viridiplantae (green plants) ·', html)
|
| 51 |
+
self.assertEqual(html.count("data-detail="), html.count('class="tree-hit"'))
|
| 52 |
+
|
| 53 |
+
def test_curated_glosses_match_the_snapshot(self):
|
| 54 |
+
with sqlite3.connect(f"{DATABASE.resolve().as_uri()}?mode=ro", uri=True) as conn:
|
| 55 |
+
for taxid, (scientific, english) in CURATED.items():
|
| 56 |
+
row = conn.execute("SELECT name FROM taxa WHERE taxid=?", (taxid,)).fetchone()
|
| 57 |
+
self.assertIsNotNone(row, f"taxid {taxid} is not in the snapshot")
|
| 58 |
+
self.assertEqual(row[0], scientific, f"taxid {taxid}")
|
| 59 |
+
self.assertLessEqual(len(english), MAX_ENGLISH, f"taxid {taxid} gloss is clipped in the chart")
|
| 60 |
+
|
| 61 |
+
def test_published_names_cover_the_curated_table(self):
|
| 62 |
+
names = json.loads(COMMON_NAMES.read_text())["names"]
|
| 63 |
+
redundant = {t for t, (scientific, english) in CURATED.items() if scientific.casefold() == english.casefold()}
|
| 64 |
+
self.assertEqual({int(t) for t in names} & set(CURATED), set(CURATED) - redundant)
|
| 65 |
+
self.assertEqual(names["6040"], "sponges") # straight from NCBI Taxonomy
|
| 66 |
+
|
| 67 |
def test_rejects_paths_outside_the_tree(self):
|
| 68 |
for path in ("1", "33208", "2759/33208", "2759/2759~3", "2759/9606", "abc"):
|
| 69 |
with self.assertRaises(ValueError, msg=path):
|