diff --git a/CHANGELOG.md b/CHANGELOG.md index 905117f..b736bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,10 @@ and Recore uses semantic versioning. have, if the reference misses an option or gets a default wrong, or if the example run no longer gives the counts the page promises. The long-form pages stay as markdown, rendered on GitHub, and MkDocs is gone from the repository. + The site opens dark whatever the system setting, with a light toggle the + embedded report follows; pointing at a cell of the opening matrix says what + happened to it; the policy section shows which of ten found genes each policy + puts back; and GitHub is a button in the header, the opening and the end. - `publication_status` in `orthology_evidence.tsv` follows `matrix_action`. Evidence the matrix policy or a quality floor kept out reads `held_out_policy` or `held_out_quality` instead of a `corrected...` value, and a strict diff --git a/docs/assets/site.css b/docs/assets/site.css index 0a16fc8..48e9230 100644 --- a/docs/assets/site.css +++ b/docs/assets/site.css @@ -2,74 +2,9 @@ opens from a clone as well as from GitHub Pages. The cell colours are the report's, so the page teaches the report's visual language. */ +/* Dark by default, whatever the system says; light only when chosen with the + toggle, which the report shares (its key is recore.theme). */ :root { - --bg: #fbfbfa; - --bg-elev: #ffffff; - --bg-subtle: #f3f4f6; - --ink: #16171d; - --ink-soft: #3d3f48; - --muted: #5f616b; - --line: #e4e5e9; - --line-strong: #cfd1d7; - --brand-a: #02a79e; - --brand-b: #0265ba; - --brand-c: #3912c0; - --grad-a: #02a79e; - --grad-b: #0265ba; - --grad-c: #3912c0; - --accent: #0b5cc4; - --accent-hover: #0849a0; - --accent-soft: #e8f0fc; - --code-bg: #12141b; - --code-ink: #e6e8ef; - --code-muted: #8b90a0; - --code-flag: #7cc4ff; - --code-value: #f4c27a; - --present: #b3bbc7; - --rescue: #0f6fd6; - --nsc: #e07b00; - --kept: #c01048; - --absent: #eef0f3; - --absent-stroke: #d2d5db; - --ok: #0f7a52; - --radius: 12px; - --radius-sm: 8px; - --shadow: 0 1px 2px rgba(16, 18, 27, 0.06), 0 8px 24px rgba(16, 18, 27, 0.06); - --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, "Helvetica Neue", Arial, sans-serif; - --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; - color-scheme: light; -} - -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) { - --bg: #0c0d10; - --bg-elev: #15171c; - --bg-subtle: #1b1d23; - --ink: #f3f4f6; - --ink-soft: #d6d8de; - --muted: #a2a5b0; - --line: #262830; - --line-strong: #383b45; - --grad-a: #35d4c8; - --grad-b: #5aa8ff; - --grad-c: #a493ff; - --accent: #5aa2ff; - --accent-hover: #86baff; - --accent-soft: #14243b; - --code-bg: #090a0d; - --present: #5d6676; - --rescue: #4b9bff; - --nsc: #ff9a2e; - --kept: #ff4f80; - --absent: #1f2128; - --absent-stroke: #454856; - --ok: #3fcf8e; - --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); - color-scheme: dark; - } -} - -:root[data-theme="dark"] { --bg: #0c0d10; --bg-elev: #15171c; --bg-subtle: #1b1d23; @@ -78,13 +13,21 @@ --muted: #a2a5b0; --line: #262830; --line-strong: #383b45; + --brand-a: #02a79e; + --brand-b: #0265ba; + --brand-c: #3912c0; --grad-a: #35d4c8; --grad-b: #5aa8ff; --grad-c: #a493ff; --accent: #5aa2ff; --accent-hover: #86baff; --accent-soft: #14243b; + --on-accent: #0a1220; --code-bg: #090a0d; + --code-ink: #e6e8ef; + --code-muted: #8b90a0; + --code-flag: #7cc4ff; + --code-value: #f4c27a; --present: #5d6676; --rescue: #4b9bff; --nsc: #ff9a2e; @@ -92,10 +35,42 @@ --absent: #1f2128; --absent-stroke: #454856; --ok: #3fcf8e; + --radius: 12px; + --radius-sm: 8px; --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 8px 24px rgba(0, 0, 0, 0.35); + --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, "Helvetica Neue", Arial, sans-serif; + --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace; color-scheme: dark; } +:root[data-theme="light"] { + --bg: #fbfbfa; + --bg-elev: #ffffff; + --bg-subtle: #f3f4f6; + --ink: #16171d; + --ink-soft: #3d3f48; + --muted: #5f616b; + --line: #e4e5e9; + --line-strong: #cfd1d7; + --grad-a: #02a79e; + --grad-b: #0265ba; + --grad-c: #3912c0; + --accent: #0b5cc4; + --accent-hover: #0849a0; + --accent-soft: #e8f0fc; + --on-accent: #ffffff; + --code-bg: #12141b; + --present: #b3bbc7; + --rescue: #0f6fd6; + --nsc: #e07b00; + --kept: #c01048; + --absent: #eef0f3; + --absent-stroke: #d2d5db; + --ok: #0f7a52; + --shadow: 0 1px 2px rgba(16, 18, 27, 0.06), 0 8px 24px rgba(16, 18, 27, 0.06); + color-scheme: light; +} + *, *::before, *::after { @@ -221,28 +196,18 @@ p { display: block; } -.logo-dark { +.logo-light { display: none !important; } -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) .logo-light { - display: none !important; - } - - :root:not([data-theme="light"]) .logo-dark { - display: block !important; - } +:root[data-theme="light"] .logo-light { + display: block !important; } -:root[data-theme="dark"] .logo-light { +:root[data-theme="light"] .logo-dark { display: none !important; } -:root[data-theme="dark"] .logo-dark { - display: block !important; -} - .nav { display: flex; gap: 4px; @@ -264,6 +229,38 @@ p { background: var(--bg-subtle); } +.gh-link { + display: inline-flex; + align-items: center; + gap: 7px; + height: 36px; + padding: 0 12px; + border: 1px solid var(--line); + border-radius: 9px; + background: var(--bg-elev); + color: var(--ink-soft); + font-size: 0.9rem; + font-weight: 600; + text-decoration: none; + flex: none; +} + +.gh-link:hover { + color: var(--ink); + border-color: var(--line-strong); +} + +.gh-link svg { + width: 17px; + height: 17px; +} + +@media (max-width: 420px) { + .gh-link span { + display: none; + } +} + .icon-btn { display: inline-grid; place-items: center; @@ -309,26 +306,20 @@ p { line-height: 1.2; } +.btn svg { + width: 17px; + height: 17px; + flex: none; +} + .btn-primary { background: var(--accent); - color: #fff; + color: var(--on-accent); } .btn-primary:hover { background: var(--accent-hover); - color: #fff; -} - -:root[data-theme="dark"] .btn-primary, -:root[data-theme="dark"] .btn-primary:hover { - color: #0a1220; -} - -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) .btn-primary, - :root:not([data-theme="light"]) .btn-primary:hover { - color: #0a1220; - } + color: var(--on-accent); } .btn-ghost { @@ -414,6 +405,7 @@ p { /* The animated matrix: genes across, genomes down. */ .demo { + position: relative; background: var(--bg-elev); border: 1px solid var(--line); border-radius: 16px; @@ -436,6 +428,52 @@ p { color: var(--ink-soft); } +.demo-ctrl { + display: inline-flex; + align-items: center; + gap: 10px; + flex: none; +} + +.mini-btn { + border: 1px solid var(--line-strong); + background: var(--bg-subtle); + color: var(--ink-soft); + font: 600 0.78rem var(--font); + padding: 4px 9px; + border-radius: 7px; + cursor: pointer; +} + +.mini-btn:hover { + color: var(--ink); +} + +.grid .cell:hover { + outline: 2px solid var(--ink); + outline-offset: 1px; +} + +.cell-tip { + position: absolute; + z-index: 5; + max-width: 250px; + background: var(--bg); + color: var(--ink); + border: 1px solid var(--line-strong); + border-radius: 10px; + padding: 8px 11px; + font-size: 0.8rem; + line-height: 1.4; + box-shadow: var(--shadow); + pointer-events: none; +} + +.cell-tip b { + display: block; + margin-bottom: 2px; +} + .grid { --cols: 16; display: grid; @@ -668,6 +706,13 @@ p { tab-size: 2; } +/* A one-line command that would hide under the copy button wraps instead; + what is copied is the text, not the wrapping. */ +.code.wrap pre { + white-space: pre-wrap; + overflow-wrap: anywhere; +} + .code .p { color: var(--code-muted); user-select: none; @@ -798,17 +843,7 @@ p { .seg button[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); - color: #fff; -} - -:root[data-theme="dark"] .seg button[aria-pressed="true"] { - color: #0a1220; -} - -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) .seg button[aria-pressed="true"] { - color: #0a1220; - } + color: var(--on-accent); } .opt { @@ -1057,23 +1092,13 @@ p { .report-cover span { background: var(--accent); - color: #fff; + color: var(--on-accent); padding: 11px 18px; border-radius: 10px; font-weight: 650; box-shadow: var(--shadow); } -:root[data-theme="dark"] .report-cover span { - color: #0a1220; -} - -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) .report-cover span { - color: #0a1220; - } -} - .report-stage.live .report-cover { display: none; } @@ -1102,7 +1127,7 @@ p { width: 100%; height: 720px; border: 0; - background: #fff; + background: var(--bg-elev); } .hints { @@ -1149,13 +1174,26 @@ p { .policy { position: relative; + /* A button centres what it holds; the cards read from the top. */ + display: flex; + flex-direction: column; + justify-content: flex-start; + width: 100%; + text-align: left; + font: inherit; + color: inherit; background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius); padding: 20px; + cursor: pointer; } -.policy.recommended { +.policy:hover { + border-color: var(--line-strong); +} + +.policy[aria-pressed="true"] { border: 2px solid var(--accent); padding: 19px; } @@ -1165,7 +1203,7 @@ p { top: -11px; left: 18px; background: var(--accent); - color: #fff; + color: var(--on-accent); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; @@ -1174,24 +1212,20 @@ p { border-radius: 999px; } -:root[data-theme="dark"] .badge { - color: #0a1220; -} - -@media (prefers-color-scheme: dark) { - :root:not([data-theme="light"]) .badge { - color: #0a1220; - } +.policy-name { + display: block; + font-weight: 700; } -.policy h3 code { +.policy-name code { font-size: 1.02rem; background: none; border: none; padding: 0; } -.policy p { +.policy-text { + display: block; color: var(--ink-soft); font-size: 0.93rem; margin-top: 8px; @@ -1217,11 +1251,102 @@ p { } .policy .meter-label { + display: block; font-size: 0.8rem; color: var(--muted); margin-top: 6px; } +/* What the chosen policy puts back, out of ten genes Recore found. */ + +.evidence { + margin-top: 18px; + background: var(--bg-elev); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 18px 20px; +} + +.ev-head { + font-weight: 650; + font-size: 0.95rem; + margin-bottom: 10px; +} + +.ev-row { + display: grid; + grid-template-columns: 82px minmax(0, 1fr); + gap: 14px; + align-items: center; + padding: 5px 0; +} + +.ev-cells { + display: flex; + gap: 5px; +} + +.ev-cell { + width: 20px; + height: 20px; + border-radius: 4px; + background: var(--absent); + box-shadow: inset 0 0 0 1px var(--absent-stroke); + transition: background-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease; +} + +.ev-cell.rescued { + background: var(--rescue); + box-shadow: none; + transform: scale(1.06); +} + +.ev-cell.kept { + background: var(--absent); + box-shadow: inset 0 0 0 2px var(--kept); + background-image: repeating-linear-gradient(135deg, transparent 0 3px, color-mix(in srgb, var(--kept) 45%, transparent) 3px 4.5px); +} + +.ev-label { + font-size: 0.92rem; + color: var(--ink-soft); +} + +.ev-sum { + margin-top: 12px; + font-size: 0.92rem; + color: var(--muted); +} + +.ev-sum b { + color: var(--ink); +} + +/* ── Where the project lives ── */ + +.cta { + background: + radial-gradient(60% 120% at 0% 0%, color-mix(in srgb, var(--brand-b) 22%, transparent), transparent 70%), + radial-gradient(60% 120% at 100% 100%, color-mix(in srgb, var(--brand-c) 22%, transparent), transparent 70%); +} + +.cta-wrap { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 24px; +} + +.cta p { + color: var(--ink-soft); + margin-top: 8px; +} + +.cta .actions { + margin-top: 0; +} + /* ── FAQ ── */ .faq { diff --git a/docs/assets/site.js b/docs/assets/site.js index d5f0192..148c661 100644 --- a/docs/assets/site.js +++ b/docs/assets/site.js @@ -7,28 +7,58 @@ /* ── Theme ── */ - function systemDark() { - return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; - } + // Dark unless the reader chose light. The key is the report's own, so the + // example report below, which reads it when it starts, opens in the same + // theme as the page, and a toggle here is a toggle there. + var THEME_KEY = "recore.theme"; function currentTheme() { - var set = document.documentElement.dataset.theme; - if (set === "light" || set === "dark") return set; - return systemDark() ? "dark" : "light"; + return document.documentElement.dataset.theme === "light" ? "light" : "dark"; + } + + function remember(theme) { + try { + localStorage.setItem(THEME_KEY, theme); + } catch (e) { + /* private mode: the choice lasts for this page only */ + } + } + + // An embedded report already running is switched with its own button, so + // it redraws its canvases; one that cannot be reached is left alone. + function syncReports() { + var theme = currentTheme(); + document.querySelectorAll("[data-report-stage] iframe").forEach(function (frame) { + try { + var doc = frame.contentDocument; + var root = doc && doc.documentElement; + if (!root || !root.getAttribute("data-theme")) return; + if (root.getAttribute("data-theme") !== theme) { + var button = doc.getElementById("themeToggle"); + if (button) button.click(); + else root.setAttribute("data-theme", theme); + } + } catch (e) { + /* another origin, or file:// in a strict browser */ + } + }); } + remember(currentTheme()); + document.querySelectorAll("[data-theme-toggle]").forEach(function (button) { button.addEventListener("click", function () { var next = currentTheme() === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = next; - try { - localStorage.setItem("recore-theme", next); - } catch (e) { - /* private mode: the choice lasts for this page only */ - } + remember(next); + syncReports(); }); }); + document.querySelectorAll("[data-report-stage] iframe").forEach(function (frame) { + frame.addEventListener("load", syncReports); + }); + /* ── Tabs ── */ document.querySelectorAll('[role="tablist"]').forEach(function (list) { @@ -332,6 +362,7 @@ absent: document.querySelector('[data-tally="absent"]') }; var counts; + var resolved = {}; var generation = 0; var visible = true; @@ -343,6 +374,7 @@ function reset() { counts = { rescued: 0, kept: 0, absent: 0 }; + resolved = {}; cells.forEach(function (c, idx) { c.className = OUTCOME[idx] ? "cell gap" : "cell"; }); @@ -355,7 +387,10 @@ cells.forEach(function (c, idx) { var kind = OUTCOME[idx]; c.className = !kind ? "cell" : kind === "absent" ? "cell gap" : "cell " + kind; - if (kind) counts[kind] += 1; + if (kind) { + counts[kind] += 1; + resolved[idx] = true; + } }); setTally(); if (stepLabel) stepLabel.textContent = "After Recore"; @@ -401,6 +436,7 @@ cells[idx].classList.add(kind); } counts[kind] += 1; + resolved[idx] = true; }); setTally(); }); @@ -417,6 +453,53 @@ }); } + // What a cell is, for whoever points at it. + var tip = document.querySelector("[data-demo-tip]"); + var demo = grid.closest(".demo"); + function explain(idx) { + var kind = OUTCOME[idx]; + var where = "Gene " + ((idx % COLS) + 1) + ", genome " + (Math.floor(idx / COLS) + 1); + if (!kind) return [where, "Present in the input matrix."]; + if (!resolved[idx]) return [where, "Empty in the input matrix. Recore searches the assembly for it."]; + if (kind === "rescued") return [where, "Put back: the matrix said absent, but the whole gene is in the assembly."]; + if (kind === "kept") { + return [where, "Kept out: the gene is there but disrupted, a pseudogene. Written down with the reason, not counted as present."]; + } + return [where, "Really absent: searched, and the gene is not in the assembly."]; + } + function showTip(cell) { + var idx = cells.indexOf(cell); + if (idx < 0 || !tip || !demo) return; + var text = explain(idx); + tip.innerHTML = ""; + tip.firstChild.textContent = text[0]; + tip.lastChild.textContent = text[1]; + tip.hidden = false; + var box = demo.getBoundingClientRect(); + var c = cell.getBoundingClientRect(); + var left = c.left - box.left + c.width / 2 - tip.offsetWidth / 2; + left = Math.max(8, Math.min(left, box.width - tip.offsetWidth - 8)); + var top = c.top - box.top - tip.offsetHeight - 8; + if (top < 4) top = c.bottom - box.top + 8; + tip.style.left = left + "px"; + tip.style.top = top + "px"; + } + grid.addEventListener("pointerover", function (event) { + var cell = event.target.closest(".cell"); + if (cell) showTip(cell); + }); + grid.addEventListener("pointerleave", function () { + if (tip) tip.hidden = true; + }); + + var replay = document.querySelector("[data-demo-replay]"); + if (replay) { + if (reduceMotion) replay.hidden = true; + replay.addEventListener("click", function () { + run(); + }); + } + if (reduceMotion) { finalState(); } else { @@ -429,6 +512,58 @@ } } + /* ── Matrix policies ── */ + + // Which of the ten found genes each policy puts back. Nothing a policy + // leaves out is lost: it stays in the audit files with the reason. + var PUTS_BACK = { + strict: ["locus"], + balanced: ["locus", "sequence"], + sensitive: ["locus", "sequence", "partial"] + }; + var picker = document.querySelector("[data-evidence]"); + if (picker) { + var rows = Array.prototype.slice.call(picker.querySelectorAll(".ev-row")); + var total = 0; + rows.forEach(function (row) { + var n = parseInt(row.getAttribute("data-ev-n"), 10) || 0; + total += n; + var holder = row.querySelector(".ev-cells"); + for (var k = 0; k < n; k++) { + var cell = document.createElement("span"); + cell.className = "ev-cell"; + holder.appendChild(cell); + } + }); + var summary = picker.querySelector("[data-ev-sum]"); + var buttons = Array.prototype.slice.call(document.querySelectorAll("[data-policy]")); + function choose(policy) { + var back = PUTS_BACK[policy] || []; + var putBack = 0; + rows.forEach(function (row) { + var on = back.indexOf(row.getAttribute("data-ev")) !== -1; + row.querySelectorAll(".ev-cell").forEach(function (cell) { + cell.className = "ev-cell " + (on ? "rescued" : "kept"); + if (on) putBack += 1; + }); + }); + buttons.forEach(function (b) { + b.setAttribute("aria-pressed", b.getAttribute("data-policy") === policy ? "true" : "false"); + }); + if (summary) { + summary.innerHTML = + "With " + policy + ", " + putBack + " of " + total + " go into the corrected matrix. " + + "The other " + (total - putBack) + " stay in the states file and orthology_evidence.tsv, with the reason."; + } + } + buttons.forEach(function (b) { + b.addEventListener("click", function () { + choose(b.getAttribute("data-policy")); + }); + }); + choose("balanced"); + } + /* ── Embedded report ── */ // The report takes the mouse wheel for zooming, so it only takes the mouse diff --git a/docs/index.html b/docs/index.html index f3cdb52..9777452 100644 --- a/docs/index.html +++ b/docs/index.html @@ -7,7 +7,7 @@ - + @@ -24,9 +24,7 @@ Try it Report Reference - - - + GitHub @@ -44,16 +42,18 @@

Missing from the matrix. Still in the genome.

Get started Open a real report ↗ + GitHub

Reads your pangenome and the assemblies. Writes a corrected matrix and a report. Never edits your files.

- Genes across, genomes down - Input matrix + Genes across, genomes down. Point at a cell. + Input matrix
+
present put back @@ -96,8 +96,8 @@

Install

Recore
-
$ cargo install --git https://github.com/PathoGenOmics-Lab/recore --locked
-

Needs Rust 1.85 or newer (rustup.rs), about a minute to build. Check with recore --version.

+
$ cargo install --git https://github.com/PathoGenOmics-Lab/recore --locked
+

Needs Rust 1.85 or newer (rustup.rs), about a minute to build. Check with recore --version. The source is on GitHub.

@@ -223,26 +223,34 @@

Explore the report

One setting that matters

--matrix-policy picks which evidence reaches the corrected matrix. All of it stays in the audit files.

-
-
-

strict

-

Only genes tied to the same locus by the annotation or by their neighbours.

- -
fewest changes
-
- +
+
Ten genes Recore found where the matrix said absent. Pick a policy:
+
Same locus, by annotation or by its neighbours
+
Full-length gene, found by sequence
+
Partial copy or pseudogene
+
Neighbours disagree, or it is a paralog
+

@@ -286,6 +294,19 @@

If something looks off

+
+
+
+

Open source, on GitHub

+

Read the code, follow the releases, ask a question or report a bug.

+
+ +
+