Hop atlas home · Overview · Changelog · Architecture · Adding_Data · Introgression · Ribbon · Dosage · Phylogeny · Bundle · Ingest · Ingest_Gbs · Ingest_Wgs · Ingest_Grasdi · Ingest_Rnaseq · Ingest_Assembly
Rendered from hop_atlas/ARCHITECTURE.md (2026-10-13)
Written 2026-10-08, prompted by "the atlases feel laggy; is this quick and dirty, or something we can grow, eventually into a public resource?". These are measurements and engineering notes, not scientific claims.
Fully static.
- Server: one Python ThreadingHTTPServer on :8900 (server/nocache_server.py) serves files from scratch_plots/. There is no backend, database or API.
- Introgression atlas:
- One hand-written page (index.html, about 113 kB) draws everything on a single canvas.
- Data is about 30 JSON files per resolution, written by dated, cohort-specific scripts.
- The haplotype-resolution and pedigree layers attach through 36 exact-once string patches (tools/atlas_layers.py patch).
- Large detail data lives on /data, reached through symlinks.
- Ribbon atlas: two generated, self-contained pages (16 MB and 28 MB) with all geometry embedded, plus the small similarity page.
- Dosage atlas: a generated page plus gzip + base64 JavaScript data assets (about 290 MB in total).
Strengths to keep:
- Every product is labelled tentative and has provenance.
- Each component has browser test suites with must-fail checks.
- Changes go through stage → test → deploy (tools/atlas_deploy.py) with drift checks and backups.
- The data is static, so it could be hosted almost anywhere.
| What | Measured |
|---|---|
| first load (5 Mb) | 16 requests, 7.8 MB uncompressed JSON, ready in 2.7 s |
| largest files | concordance 1.9 MB, samples 1.9 MB, atlas_data 1.8 MB, transitions 0.9 MB |
| server headers | Cache-Control: no-store, no compression (Python SimpleHTTP) |
| one redraw (all chromosomes) | 2.1 s for 2,372 rows onto one 29,187-px-tall canvas |
| switch to 500 kb | +28.5 MB, 4.6 s |
| JS heap | about 100 MB |
Why it feels slow: 1. Every control change redraws all rows on the main thread, including rows far off screen. That costs about 2 s per change at 5 Mb and more at finer resolutions, and it is the lag you feel. 2. Nothing is compressed or cached. Each visit re-downloads 7.8 MB, or about 36 MB at 500 kb. On a remote or VPN link that dominates. 3. Whole-genome JSON is loaded up front, not by chromosome or region. 4. The ribbon pages embed 16–28 MB each, parsed in full on every open.
It is a disciplined prototype: well tested and documented, but grown feature by feature. It is not quick and dirty, but it is not built to scale either. - Can it keep growing? Yes, but each new cohort or layer adds more JSON and more string patches, so load time and fragility grow too. - As a public resource? Not as it stands. It needs the steps below first, and none of them requires a backend: the data is static and can be served from static hosting or a CDN.
Quick wins (hours; no redesign; behaviour unchanged):
1. Compression + caching. Serve with gzip/brotli and cache-friendly headers (versioned file names or ETags) instead of no-store. Expect 5–10× less transfer and near-instant reloads. Options: add both to the Python server, or put nginx/Caddy in front.
2. Draw only what is visible (required before the 4,000+ collection; see the opt-in design below). Use a viewport-sized canvas and draw only the rows on screen; redraw cost then scales with the screen, not the 2,372 rows. This is the biggest fix for the lag.
3. Debounce redraws and move per-bin aggregation (frequency tracks, lineage strings) to a Web Worker.
Medium (days to weeks):
4. One data build. A cohort manifest (accessions, assay, parents, roles) plus parameterised stages that regenerate every served file. That replaces the dated per-cohort scripts and the hand patches to index.html, and is the same step that closes the SRA → atlas ingestion gap in components/*.md.
5. Data by chromosome and resolution. Binary/columnar files (typed arrays or Arrow/Parquet), fetched on demand instead of genome-wide JSON. Ribbon geometry gets the same treatment, out of the HTML.
6. One front-end shell. Shared navigation, sample identity and URL state across the three components; front-end code as modules rather than patches.
7. Versioned data releases. releases/<date>/ with a manifest and checksums, and the viewer pinned to a release.
Public resource (beyond that): 8. Data rights. Check the release rights of every input, project by project. In particular, the CLAUDE.md note says the phased Apollo FASTA is embargoed and our phased Apollo is a DIY assembly; also check collaborator and unpublished material. Strip internal paths and notes. 9. Hosting. Static hosting (object store + CDN) with a release DOI. It needs a citation and provenance page, a tentative-status policy, browser and accessibility checks, and a light privacy-free analytics or feedback channel if wanted.
Why now. A collection of 4,000+ samples, mostly F1s at lower resolution, is coming. Many of them are relatives of OB79, the wild-neomexicanus-derived Wye male. - Today: 2,277 sample rows from 11 cohorts, all loaded on every visit. The largest cohorts are OSU 826 and Saaz 257. - The 5 Mb samples file is 1.9 MB; the 500 kb one is 13.5 MB. - Rows are drawn on one canvas 29,187 px tall. - At about 6,300 rows: - transfer about triples, with each redraw time; - the canvas would need about 77,000 px, beyond the browser's per-side canvas limit (about 32,767 px in Chrome and Firefox).
So "load everything, draw everything" stops working, not just slows down. The user has to choose what to see.
Pattern: a small index loaded always, data loaded on opt-in, rows drawn only when visible, families folded, presets as saved queries.
ingest/), so the ingest pipeline writes it.?ds=havill,cascade,…), so links reproduce views, and is remembered per browser.{datasets, selector, sort, sections}. Choosing one switches on only the datasets it needs.Build order (each step is usable on its own; ties to roadmap 1, 2, 4, 5):
- (a) index + sample directory, and per-dataset file split, from the cohort manifests;
- (b) Datasets drawer + lazy loading + URL state;
- (c) visible-row drawing;
- (d) folded families;
- (e) presets as queries, including OB79;
- (f) ingest the new collection as one new dataset (one experiment = one cohort; see ingest/README.md D1).
Steps (a)–(c) are needed before the new collection can be shown at all. Compression (roadmap 1) is a same-day win alongside.
Open questions for the PI: - which datasets are on by default; - is the incoming collection the Zhang 2017 HapMap or a different set; - should the OB79 preset include genotype-inferred relatives, as a separate section.
| Roadmap item | Status | Where |
|---|---|---|
Opt-in datasets: Datasets drawer, ?ds= + per-browser state, presets switch on their datasets (design steps b, e in part) |
done 2026-10-08 | /*zhfold*/, tools/patch_datasets_guard.py |
| Lazy loading of an opt-in dataset (Zhang 2017, 4,453 rows, +18.5 MB at 5 Mb loaded only when selected) | done 2026-10-08 for the opt-in files; the default cohorts are still one mixed file per resolution (step a not done) | multires/*.zhang2017.json, tools/zhang_regroup.py |
| Canvas guard (fold the largest sections or clamp past the ~32,000 px limit, never a blank canvas) | done 2026-10-08; a stop-gap, not visible-row drawing | /*zhfold*/, tools/probe_canvas.py |
| Presets as saved queries | partly: presets carry their datasets (zhang2017, wild_wgs, not_19058m, pilgrim_wye); pedigree-relation selectors (OB79 relatives) not built |
index.html PRESETS |
| One front-end shell: shared navigation; cross-component links | partly: shared nav bar on every page (2026-10-08); ?sample= links from the Phylogeny view (2026-10-10); no shared state or modules |
www/nav.js, /*urlsample*/ |
| Phylogeny component | done 2026-10-10 | components/PHYLOGENY.md |
| Per-reference calls (calls re-made on other assemblies instead of lifted display frames) | done 2026-10-13 for one cohort (Zenith x 21058M) on 5 references; liftover frames and lifted geometry removed; more cohorts in progress | components/INTROGRESSION.md §8e, annevo_eval/methods/per_frame_ancestry/ |
| Cohort manifests (roadmap 4, first part) | partly: 9 manifests + loader + jobs generator; Carey 2026 ingested through a manifest + numbered stage scripts; no call / export generator |
cohorts/, ingest/pipeline/, cohorts/carey2026/pipeline/ |
| Versioned releases (roadmap 7) | a first step: a self-contained snapshot zip with a generated CLAUDE.md and an md5 manifest; not yet a pinned release the live viewer reads | components/BUNDLE.md, tools/package_atlas_bundle.py |
Still to do: compression + caching (roadmap 1), visible-row drawing (2; required before thousands more rows are on by default), debounced redraws / Web Worker (3), the base-atlas generator (4), data by chromosome in binary form (5), modular front end (6; the page is still grown by anchored patches, now about 20 markers), folded families (design step d), pedigree-relation presets (e), the per-dataset split of the default files (a), and everything under "Public resource". The load figures above were measured before the Zhang, Carey and Reference changes; the default view now has 2,299 sample rows on a 29,551 px canvas (canvas probe after Carey 2026, 2026-10-09), close to the limit, and the timings need re-measuring with probe_load.py when the PI asks.
hop_atlas/tools/probe_load.py (Playwright: bytes per load, time to ready, redraw time, resolution switch). Re-run it after each roadmap step.