From c71f1b63332658e8742dd26a688de5b4cac03e63 Mon Sep 17 00:00:00 2001 From: Kemal Yaylali Date: Wed, 30 Sep 2026 08:01:30 +0100 Subject: [PATCH] Apply the brand across the repo: reports, README, guide * simurg/core/brand.py: single source of the mark geometry, colours, favicon and the report header band (scripts/brand now load it from there). * HTML reports (QC report, compare dashboard, methylation QC): offline SVG favicon, indigo header band with the mark, version and a sha256 of the data shown; Noto Sans font stack; methylation plots in Okabe-Ito colours. * README banner, guide logo, brand/blog links; branding brief updated to the final identity; Noto (OFL) and Okabe-Ito entries in THIRD_PARTY_NOTICES; CHANGELOG (Unreleased). Unit tests + P5 QC oracle: 285 passed; ruff and mypy clean. --- CHANGELOG.md | 8 ++ README.md | 3 + THIRD_PARTY_NOTICES.md | 2 + blog/branding_prompt.md | 19 ++-- docs/user_guide.md | 2 + scripts/brand/simurg_mark.py | 94 ++++--------------- src/simurg/core/brand.py | 144 +++++++++++++++++++++++++++++ src/simurg/pipeline/compare.py | 14 ++- src/simurg/pipeline/methyl.py | 24 +++-- src/simurg/pipeline/qc_report.py | 9 +- src/simurg/qc/report/report.css | 6 +- src/simurg/qc/report/template.html | 3 +- 12 files changed, 222 insertions(+), 106 deletions(-) create mode 100644 src/simurg/core/brand.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 19d0740..182ec3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +**Branding.** A visual identity (`branding/`, guide in `branding/README.md`): a dotted simurg mark in detailed, simple +and micro drawings, wordmark and lockups, icons, README banner, social preview and featured-image templates, with +contrast and colour-vision checks. HTML reports (QC report, compare dashboard, methylation QC) get a favicon and a +header band with the mark, the software version and a checksum of the data they show. The mark geometry lives in +`simurg/core/brand.py`. Blog posts and featured images in `blog/`. + ## 2.0.0 — 2026-09-30 (v2 modules) The Python package is 2.0.0. The optional Rust kernels are unchanged and stay `simurg_core` 1.1.0. diff --git a/README.md b/README.md index 8d22623..179ceaf 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # SimurgArray +

SimurgArray: Thirty birds, one picture.

+ Personal research toolkit for Illumina Infinium genotyping BeadChip data (IDAT → normalization → genotype calling → BAF/LRR → QC → VCF/PLINK; CN/LOH/mosaicism, PGx, cluster training, methylation control QC). @@ -7,6 +9,7 @@ methylation control QC). - **SIMURGARRAY_MASTER_PLAN.md** — architecture, algorithms, roadmap - **SIMURGARRAY_FUNCTIONAL_SPEC.md** — 416 itemised requirements (CLI, inputs, outputs, QC, CN, PGx, methylation, edge cases) - `docs/validation/` — per-phase validation reports · `docs/known_unknowns.md` — findings log · `docs/research/` — background +- `branding/` — logo, icons, templates and the brand guide · `blog/` — five posts about the project ## Status diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 69102e0..8e9187b 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -19,6 +19,8 @@ used only as external subprocess oracles and are not copied or linked. | [PharmVar](https://www.pharmvar.org) haplotype definitions (Gaedigk et al. 2018 CPT 103:399) | PharmVar terms of use (attribution) | per-gene zips downloaded by `scripts/pgx/fetch_sources.sh` into `data/` (never committed); `simurg pgx db build` derives core-allele definitions for genes CPIC does not define | | [GeT-RM](https://www.cdc.gov/lab-quality/php/get-rm/reference-materials.html) consolidated PGx/HLA table (CDC) | US government work | validation truth only (`scripts/pgx/getrm_truth.py`), not redistributed | | [ClinPGx](https://www.clinpgx.org) (formerly PharmGKB) clinical-annotation variants | CC BY-SA 4.0 (attribution: ClinPGx, https://www.clinpgx.org/page/citingClinpgx) | `clinicalVariants.zip` downloaded by `scripts/pgx/fetch_sources.sh` into `data/` (never committed); rsIDs become genotype-only variant-based genes in the local PGx DB | +| [Noto Sans, Noto Sans Mono](https://fonts.google.com/noto) — Copyright 2022 The Noto Project Authors | SIL Open Font License 1.1 | Glyph outlines of the wordmark in `branding/logo/` (converted to SVG paths by `scripts/brand/make_brand.py`); named in report CSS but never bundled | +| Okabe and Ito colour-universal-design palette (Okabe & Ito 2008, *Color Universal Design*) | free to use (cited) | Chart colours in `simurg/core/brand.py` and the HTML reports | ## BSD-2-Clause (BeadArrayFiles, verbatim from its README) diff --git a/blog/branding_prompt.md b/blog/branding_prompt.md index cca4741..a293735 100644 --- a/blog/branding_prompt.md +++ b/blog/branding_prompt.md @@ -1,6 +1,6 @@ # Branding brief for SimurgArray -Copy everything below the line into the branding agent. +Copy everything below the line into a branding agent. The identity it asks for now exists in `branding/`; this brief is kept for future refinements. ________________________________________________________________________________ @@ -26,16 +26,13 @@ Precise, calm, honest, a little dry, quietly clever. Think of a well made scient ## Existing visual direction (keep and refine it, do not replace it) -* **Core motif:** a bird in flight drawn only with round dots on a square grid, like beads on a BeadChip. The current bird faces right, wings raised and swept back, with a small crest, a hooked beak and three long tail plumes ending in round "peacock eye" dots. -* **Channel colors:** the scanner has a red and a green channel. Dots on the outline of the bird are red and green; the inner dots, where both channels "overlap", are gold. -* **Palette (starting point, you may tune values for contrast):** - * Channel red #C8453A - * Channel green #2F8F5B - * Simurg gold #D4A017 - * Night indigo #1E2240 (backgrounds; most of the work happened at night) - * Paper #F7F5EF (documents, light backgrounds) -* **Type so far:** Noto Sans for text, Noto Sans Mono for anything a user might paste into a terminal. -* Five blog featured images already exist in this style (1200 x 630, indigo background, faint bead grid, bird on the right, title on the left, footer with three colored dots, the name and the tagline). Stay consistent with them. +The identity already exists in `branding/` (guide: `branding/README.md`). Treat it as the starting point. + +* **Core motif:** a simurg seen from the front, wings raised in a V with three feathers each, a three dot crown, drawn only with round dots on a square grid like beads on a BeadChip. Three drawings: detailed (31 x 27 dots), simple (11 x 10) and micro (7 x 7). +* **Channel colours:** the body, wings and crown are gold, "where the two channels meet". Three tail plumes: red on the left (red channel), gold in the middle, green on the right (green channel), each ending in a round peacock eye. +* **Palette:** night indigo #1E2240, paper #F7F5EF, simurg gold #D4A017, gold deep #A87A00 (the bird on light backgrounds), gold ink #8C6500 (accent text on light), channel red #C8453A, channel green #2F8F5B, muted #A3A7C9 on dark and #5A5E7E on light. Charts use the Okabe and Ito colours with shapes. +* **Type:** Noto Sans (wordmark in SemiBold, "Array" in the accent colour) and Noto Sans Mono. +* The five blog featured images, the README banner, the social preview, the avatar and the HTML report header band already use it. ## Deliverables diff --git a/docs/user_guide.md b/docs/user_guide.md index 021e11a..0e48ec0 100644 --- a/docs/user_guide.md +++ b/docs/user_guide.md @@ -1,3 +1,5 @@ +SimurgArray + # SimurgArray user guide (v2.0) SimurgArray turns Illumina Infinium genotyping BeadChip scans (IDAT files) into genotype calls, QC tables and diff --git a/scripts/brand/simurg_mark.py b/scripts/brand/simurg_mark.py index efb2ee1..bba28f1 100644 --- a/scripts/brand/simurg_mark.py +++ b/scripts/brand/simurg_mark.py @@ -1,86 +1,24 @@ -"""Geometry of the SimurgArray mark on its dot grid (no dependencies beyond the standard library). +"""The mark geometry for the image scripts; the single source is ``src/simurg/core/brand.py``. -A frontal simurg with raised wings (three feathers each), a three-dot crown and three tail plumes: red channel, -gold, green channel, each ending in a peacock eye. Shapes are capsules, circles and an ellipse in grid units; a cell -belongs to the first shape that contains its centre. ``SIMPLE`` and ``MICRO`` are hand-drawn small versions. +Loaded straight from the file, so these scripts run with any Python that has Pillow, without installing SimurgArray. """ from __future__ import annotations -import math -from typing import Any +import importlib.util +from pathlib import Path +_path = Path(__file__).resolve().parents[2] / "src" / "simurg" / "core" / "brand.py" +_spec = importlib.util.spec_from_file_location("simurg_brand", _path) +assert _spec is not None and _spec.loader is not None +_brand = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_brand) -def _seg(px: float, py: float, a: tuple[float, float], b: tuple[float, float]) -> float: - dx, dy = b[0] - a[0], b[1] - a[1] - t = max(0.0, min(1.0, ((px - a[0]) * dx + (py - a[1]) * dy) / ((dx * dx + dy * dy) or 1.0))) - return math.hypot(px - a[0] - t * dx, py - a[1] - t * dy) +COLOURS = _brand.COLOURS +MICRO = _brand.MICRO +SIMPLE = _brand.SIMPLE +Grid = _brand.Grid +art = _brand.art +detailed = _brand.detailed - -CX = 15.5 -_WING = [([(13.6, 9.6), (8, 5.2), (1.8, 1.4)], 1.25), ([(13.4, 11), (7.5, 8.4), (1.2, 6.6)], 1.25), - ([(13.4, 12.6), (8, 12), (2.6, 11.8)], 1.2)] # fmt: skip -SHAPES: list[tuple[str, Any, str]] = [] -for pts, r in _WING: - SHAPES.append(("cap", (pts, r), "gold")) - SHAPES.append(("cap", ([(2 * CX - x, y) for x, y in pts], r), "gold")) -SHAPES += [ - ("circ", (CX, 4.4, 2.3), "gold"), ("cap", ([(CX, 6), (CX, 8)], 1.3), "gold"), # head, neck - ("circ", (CX, 1.2, 0.62), "gold"), ("circ", (13.6, 1.9, 0.62), "gold"), ("circ", (17.4, 1.9, 0.62), "gold"), - ("ell", (CX, 12.5, 2.9, 5.4), "gold"), # body - ("cap", ([(14.8, 17), (12.6, 20.5), (10.4, 23.2)], 0.7), "red"), ("circ", (10, 24.3, 1.6), "red"), - ("cap", ([(CX, 17.5), (CX, 23.2)], 0.7), "gold"), ("circ", (CX, 24.6, 1.6), "gold"), - ("cap", ([(16.2, 17), (18.4, 20.5), (20.6, 23.2)], 0.7), "green"), ("circ", (21, 24.3, 1.6), "green"), -] # fmt: skip -DETAILED_SIZE = (31, 27) -SIMPLE = """ -X.........X -XX.......XX -.XX..X..XX. -..XXXXXXX.. -XXXXXXXXXXX -...XXXXX... -....XXX.... -...R.X.G... -..R..X..G.. -.RR.XXX.GG. -""" -MICRO = """ -X.....X -XX.X.XX -.XXXXX. -..XXX.. -...X... -.R.X.G. -R..X..G -""" -Grid = dict[tuple[int, int], str] - - -def _inside(x: float, y: float, kind: str, p: Any) -> bool: - if kind == "circ": - return math.hypot(x - p[0], y - p[1]) <= p[2] - if kind == "cap": - pts, r = p - return min(_seg(x, y, pts[i], pts[i + 1]) for i in range(len(pts) - 1)) <= r - cx, cy, rx, ry = p - return ((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2 <= 1 - - -def detailed() -> tuple[Grid, int, int]: - cols, rows = DETAILED_SIZE - g: Grid = {} - for r in range(rows): - for c in range(cols): - for kind, p, layer in SHAPES: - if _inside(c + 0.5, r + 0.5, kind, p): - g[(c, r)] = layer - break - return g, cols, rows - - -def art(s: str) -> tuple[Grid, int, int]: - lines = s.strip().splitlines() - g = {(c, r): {"X": "gold", "R": "red", "G": "green"}[ch] for r, ln in enumerate(lines) for c, ch in enumerate(ln) - if ch != "."} # fmt: skip - return g, len(lines[0]), len(lines) +__all__ = ["COLOURS", "MICRO", "SIMPLE", "Grid", "art", "detailed"] diff --git a/src/simurg/core/brand.py b/src/simurg/core/brand.py new file mode 100644 index 0000000..363960b --- /dev/null +++ b/src/simurg/core/brand.py @@ -0,0 +1,144 @@ +"""SimurgArray visual identity: the dotted simurg mark, brand colours, and the header band used by every HTML report. + +Pure and dependency free (no I/O). ``branding/README.md`` is the guide; ``scripts/brand/make_brand.py`` renders the +logo files from the same geometry, so reports, icons and images always show the same dots [OURS]. + +The mark is a frontal simurg on a dot grid: gold body and wings, a three dot crown, and three tail plumes (red +channel, gold, green channel) ending in peacock eyes. ``detailed()`` is drawn from shapes in grid units (a cell +belongs to the first shape containing its centre); ``SIMPLE`` and ``MICRO`` are hand drawn for small sizes. +""" + +from __future__ import annotations + +import html +import math +from typing import Any +from urllib.parse import quote + +COLOURS = { + "indigo": "#1E2240", "indigo_deep": "#12142A", "paper": "#F7F5EF", "red": "#C8453A", "green": "#2F8F5B", + "gold": "#D4A017", "gold_deep": "#A87A00", "gold_ink": "#8C6500", "muted_dark": "#A3A7C9", + "muted_light": "#5A5E7E", +} # fmt: skip +CHART = {"AA": "#D55E00", "AB": "#009E73", "BB": "#0072B2"} # Okabe and Ito; always paired with a shape or label +TAGLINE = "Thirty birds, one picture." +FONT_STACK = '"Noto Sans",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif' +MONO_STACK = '"Noto Sans Mono",ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace' + + +def _seg(px: float, py: float, a: tuple[float, float], b: tuple[float, float]) -> float: + dx, dy = b[0] - a[0], b[1] - a[1] + t = max(0.0, min(1.0, ((px - a[0]) * dx + (py - a[1]) * dy) / ((dx * dx + dy * dy) or 1.0))) + return math.hypot(px - a[0] - t * dx, py - a[1] - t * dy) + + +CX = 15.5 +_WING = [([(13.6, 9.6), (8, 5.2), (1.8, 1.4)], 1.25), ([(13.4, 11), (7.5, 8.4), (1.2, 6.6)], 1.25), + ([(13.4, 12.6), (8, 12), (2.6, 11.8)], 1.2)] # fmt: skip +SHAPES: list[tuple[str, Any, str]] = [] +for pts, r in _WING: + SHAPES.append(("cap", (pts, r), "gold")) + SHAPES.append(("cap", ([(2 * CX - x, y) for x, y in pts], r), "gold")) +SHAPES += [ + ("circ", (CX, 4.4, 2.3), "gold"), ("cap", ([(CX, 6), (CX, 8)], 1.3), "gold"), # head, neck + ("circ", (CX, 1.2, 0.62), "gold"), ("circ", (13.6, 1.9, 0.62), "gold"), ("circ", (17.4, 1.9, 0.62), "gold"), + ("ell", (CX, 12.5, 2.9, 5.4), "gold"), # body + ("cap", ([(14.8, 17), (12.6, 20.5), (10.4, 23.2)], 0.7), "red"), ("circ", (10, 24.3, 1.6), "red"), + ("cap", ([(CX, 17.5), (CX, 23.2)], 0.7), "gold"), ("circ", (CX, 24.6, 1.6), "gold"), + ("cap", ([(16.2, 17), (18.4, 20.5), (20.6, 23.2)], 0.7), "green"), ("circ", (21, 24.3, 1.6), "green"), +] # fmt: skip +DETAILED_SIZE = (31, 27) +SIMPLE = """ +X.........X +XX.......XX +.XX..X..XX. +..XXXXXXX.. +XXXXXXXXXXX +...XXXXX... +....XXX.... +...R.X.G... +..R..X..G.. +.RR.XXX.GG. +""" +MICRO = """ +X.....X +XX.X.XX +.XXXXX. +..XXX.. +...X... +.R.X.G. +R..X..G +""" +Grid = dict[tuple[int, int], str] + + +def _inside(x: float, y: float, kind: str, p: Any) -> bool: + if kind == "circ": + return bool(math.hypot(x - p[0], y - p[1]) <= p[2]) + if kind == "cap": + pts, r = p + return bool(min(_seg(x, y, pts[i], pts[i + 1]) for i in range(len(pts) - 1)) <= r) + cx, cy, rx, ry = p + return bool(((x - cx) / rx) ** 2 + ((y - cy) / ry) ** 2 <= 1) + + +def detailed() -> tuple[Grid, int, int]: + cols, rows = DETAILED_SIZE + g: Grid = {} + for r in range(rows): + for c in range(cols): + for kind, p, layer in SHAPES: + if _inside(c + 0.5, r + 0.5, kind, p): + g[(c, r)] = layer + break + return g, cols, rows + + +def art(s: str) -> tuple[Grid, int, int]: + lines = s.strip().splitlines() + g = {(c, r): {"X": "gold", "R": "red", "G": "green"}[ch] for r, ln in enumerate(lines) for c, ch in enumerate(ln) + if ch != "."} # fmt: skip + return g, len(lines[0]), len(lines) + + +def mark_svg(size: str = "simple", height: float = 24, *, scheme: str = "dark", title: str = "SimurgArray") -> str: + """Inline SVG of the mark (``size``: detailed, simple or micro); ``scheme`` dark (gold) or light (deep gold).""" + g, cols, rows = detailed() if size == "detailed" else art(SIMPLE if size == "simple" else MICRO) + gold = COLOURS["gold"] if scheme == "dark" else COLOURS["gold_deep"] + fill = {"gold": gold, "red": COLOURS["red"], "green": COLOURS["green"]} + width = height * (cols + 1) / (rows + 1) + dots = "".join(f'' + for (c, r), layer in sorted(g.items(), key=lambda kv: (kv[0][1], kv[0][0]))) # fmt: skip + return (f'' + f"{html.escape(title)}{dots}") # fmt: skip + + +def favicon_link() -> str: + """```` with the micro mark on an indigo tile as an inline data URI (reports stay offline).""" + g, cols, _ = art(MICRO) + fill = {"gold": COLOURS["gold"], "red": COLOURS["red"], "green": COLOURS["green"]} + n = cols + 1 + dots = "".join(f'' for (c, r), layer in g.items()) + svg = (f'' + f'{dots}') # fmt: skip + return f'' + + +BAR_CSS = ( + f".sa-bar{{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 16px;background:{COLOURS['indigo']};" + f"color:{COLOURS['paper']};font-family:{FONT_STACK}}}" + f".sa-bar .sa-name{{font-weight:600;letter-spacing:.2px}}" + f".sa-bar .sa-name b{{color:{COLOURS['gold']};font-weight:600}}" + f".sa-bar .sa-title{{font-size:17px;font-weight:600;margin-left:6px}}" + f".sa-bar .sa-meta{{margin-left:auto;font:12px {MONO_STACK};color:{COLOURS['muted_dark']}}}" +) + + +def report_bar(title: str, version: str, checksum_label: str, checksum: str) -> str: + """Header band for HTML reports: the mark, the name, the report title, and on the right the software version + and a checksum of what the report shows. The mark is only recognition; the checksum is what proves anything.""" + return (f'
{mark_svg("simple", 22)}SimurgArray' + f'{html.escape(title)}' + f'{html.escape(version)} · {html.escape(checksum_label)} ' + f"{html.escape(checksum[:12])}
") # fmt: skip diff --git a/src/simurg/pipeline/compare.py b/src/simurg/pipeline/compare.py index 089364b..661127b 100644 --- a/src/simurg/pipeline/compare.py +++ b/src/simurg/pipeline/compare.py @@ -7,6 +7,7 @@ A and B are two files or two folders; folders are paired by relative path. Outpu from __future__ import annotations import gzip +import hashlib import html import io import json @@ -228,7 +229,8 @@ DASH_CSS = """ :root{--bg:#fafafa;--fg:#222;--muted:#666;--line:#ddd;--ok:#d6e6f5;--near:#e8f2d6;--bad:#f9d3ac;--unk:#e6e6e6} @media (prefers-color-scheme: dark){:root{--bg:#1d1f21;--fg:#e6e6e6;--muted:#aaa;--line:#444; --ok:#23405c;--near:#35451f;--bad:#6b3d10;--unk:#3a3a3a}} -body{margin:0;padding:16px;background:var(--bg);color:var(--fg);font:14px/1.4 system-ui,sans-serif} +body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.4 "Noto Sans",system-ui,sans-serif} +.sa-main{padding:16px} table{border-collapse:collapse;margin:6px 0} td,th{border:1px solid var(--line);padding:3px 8px;text-align:right} td:first-child,th:first-child{text-align:left} @@ -294,12 +296,18 @@ def render_dashboard(summary: dict[str, Any], examples: dict[str, list[dict[str, + _table(["pair", "locus", "A", "B"], body) ) parts.append('

Every discrepancy and every unmatched record is listed in discrepancies.tsv.gz.

') + from simurg.core.brand import BAR_CSS, favicon_link, report_bar + + checksum = hashlib.sha256(json.dumps(summary, sort_keys=True, default=str).encode("utf-8")).hexdigest() + bar = report_bar("Differential test", __version__, "summary sha256", checksum) return ( '' '' - f"{title} — {PRODUCT_NAME}" + f"{title} · {PRODUCT_NAME}{favicon_link()}" + + bar + + '
' + "\n".join(parts) - + "\n" + + "
\n" ) diff --git a/src/simurg/pipeline/methyl.py b/src/simurg/pipeline/methyl.py index a072e4e..c194516 100644 --- a/src/simurg/pipeline/methyl.py +++ b/src/simurg/pipeline/methyl.py @@ -9,6 +9,7 @@ from __future__ import annotations import datetime as _dt import gzip +import hashlib import html import json import math @@ -390,22 +391,29 @@ def _write_html( th_rows = "".join( f"{m.column}{m.key}{_cell(th.get(m.key).value)}" for m in metrics ) + from simurg import __version__ + from simurg.core.brand import BAR_CSS, CHART, FONT_STACK, favicon_link, report_bar + generated = _dt.datetime.now(_dt.UTC).strftime("%Y-%m-%d %H:%M UTC") + checksum = hashlib.sha256(json.dumps(rows, sort_keys=True, default=str).encode("utf-8")).hexdigest() + bar = report_bar("Methylation control QC", __version__, "samples sha256", checksum) page = f""" -Methylation QC - +.plot{{width:min(560px,100%);height:380px}}.note{{color:#666}}{BAR_CSS} -

Methylation control QC

+{bar}

{len(rows)} samples · manifest {html.escape(Path(man.path).name)} - · profile {html.escape(th.profile or "none")} · generated {generated} · SimurgArray

+ · profile {html.escape(th.profile or "none")} · generated {generated}

Samples

{"".join(f"" for c in head)}{trs}
{c}

Thresholds

{th_rows}
metrickeythreshold
+
""" tmp = path.with_suffix(".html.part") tmp.write_text(page, encoding="utf-8") diff --git a/src/simurg/pipeline/qc_report.py b/src/simurg/pipeline/qc_report.py index 2e4fc2e..0e1444a 100644 --- a/src/simurg/pipeline/qc_report.py +++ b/src/simurg/pipeline/qc_report.py @@ -536,9 +536,14 @@ def payload(datasets: Sequence[Dataset], th: Thresholds, generated: str, metadat def render_html(data: Mapping[str, Any]) -> str: blob = json.dumps(data, sort_keys=True, ensure_ascii=False, separators=(",", ":"), allow_nan=False) packed = base64.b64encode(gzip.compress(blob.encode("utf-8"), compresslevel=9, mtime=0)).decode("ascii") - page = _asset("template.html").replace("__TITLE__", f"{PRODUCT_NAME} QC Report").replace("__DATA__", packed) + from simurg.core.brand import BAR_CSS, favicon_link, report_bar + + checksum = hashlib.sha256(blob.encode("utf-8")).hexdigest() + bar = report_bar("QC Report", __version__, "data sha256", checksum) + page = (_asset("template.html").replace("__TITLE__", f"{PRODUCT_NAME} QC Report").replace("__DATA__", packed) + .replace("__FAVICON__", favicon_link()).replace("__BRANDBAR__", bar)) # fmt: skip for marker, text in ( - ("/*__STYLE__*/", _asset("report.css")), + ("/*__STYLE__*/", _asset("report.css") + "\n" + BAR_CSS), ("/*__APP__*/", _asset("report.js")), ("/*__PLOTLY__*/", _plotly().replace(" __TITLE__ +__FAVICON__ +__BRANDBAR__
-
__TITLE__
loading…