Spaces:
Running
Running
Download app/figure/engine.py from Samad14/bio-nexus-api: direct link, hf CLI and curl.
- Browser
- Download file 6.85 kB
-
https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/app/figure/engine.py
- Command line
-
hf download hf://spaces/Samad14/bio-nexus-api/app/figure/engine.py
-
curl -L -o engine.py https://huggingface.co/spaces/Samad14/bio-nexus-api/resolve/main/app/figure/engine.py
6.85 kB
| """Figure Engine (BioNexus 2.0, Component 8). | |
| Turns engine results into *publication figures* rather than charts: every | |
| rendered figure carries panel labels (A, B, ...), a title, an auto-generated | |
| caption, a legend, statistical notes and a citation/footer block — Nature/IEEE | |
| readable SVG with zero binary dependencies. | |
| Renderers here are pure string SVG. Higher-level code (app/services/ | |
| experiment_figures.py) composes per-experiment multi-panel figures from a | |
| stored result context using the panel primitives below. | |
| """ | |
| from __future__ import annotations | |
| import html | |
| from typing import Any | |
| #: Six-color palette tuned for color-blind-safe scientific figures. | |
| PALETTE = ["#2563eb", "#dc2626", "#059669", "#d97706", "#7c3aed", "#0e7490"] | |
| def esc(text: Any) -> str: | |
| return html.escape(str(text if text is not None else ""), quote=True) | |
| def svg_canvas(width: int, height: int) -> str: | |
| return ( | |
| f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" ' | |
| f'viewBox="0 0 {width} {height}" font-family="Helvetica, Arial, sans-serif">' | |
| ) | |
| def svg_close() -> str: | |
| return "</svg>" | |
| def title_block(text: str, x: int = 20, y: int = 34, size: int = 20) -> str: | |
| return f'<text x="{x}" y="{y}" font-size="{size}" font-weight="bold" fill="#111827">{esc(text)}</text>' | |
| def footer_block(generated: str, citations: list[str], x: int = 20, y: int = 780) -> str: | |
| """Attribution + citation block pinned near the bottom of the canvas.""" | |
| parts = [f'<text x="{x}" y="{y}" font-size="11" font-weight="bold" fill="#374151">Generated by BioNexus Figure Engine · {esc(generated)}</text>'] | |
| y += 18 | |
| for i, cite in enumerate(citations[:4]): | |
| parts.append(f'<text x="{x}" y="{y}" font-size="9" fill="#6b7280">{esc(cite[:140])}</text>') | |
| y += 13 | |
| return "".join(parts) | |
| def panel_header(letter: str, title: str, x: int, y: int) -> str: | |
| badge = ( | |
| f'<circle cx="{x + 10}" cy="{y + 8}" r="10" fill="#111827" stroke="none"/>' | |
| f'<text x="{x + 10}" y="{y + 12}" font-size="11" font-weight="bold" ' | |
| f'text-anchor="middle" fill="#ffffff">{esc(letter)}</text>' | |
| ) | |
| ttl = f'<text x="{x + 28}" y="{y + 12}" font-size="13" font-weight="bold" fill="#111827">{esc(title)}</text>' | |
| return badge + ttl | |
| def panel_caption(text: str, x: int, y: int, width: int = 380) -> str: | |
| """A wrapped caption line under a panel (statistical note).""" | |
| words = str(text).split() | |
| lines: list[str] = [] | |
| cur = "" | |
| for w in words: | |
| if len(cur) + len(w) + 1 <= width // 6: | |
| cur = f"{cur} {w}".strip() | |
| else: | |
| lines.append(cur) | |
| cur = w | |
| if cur: | |
| lines.append(cur) | |
| out = [] | |
| y0 = y | |
| for line in lines[:3]: | |
| out.append(f'<text x="{x}" y="{y0}" font-size="9" fill="#6b7280">{esc(line)}</text>') | |
| y0 += 12 | |
| return "".join(out) | |
| def bar_chart_panel( | |
| rows: list[tuple[str, float]], | |
| *, | |
| x: int, y: int, w: int, h: int, | |
| value_label: str = "value", | |
| max_value: float | None = None, | |
| ) -> str: | |
| """Horizontal bar chart inside the (w x h) viewport anchored at (x, y).""" | |
| if not rows: | |
| return f'<text x="{x}" y="{y + 20}" font-size="11" fill="#9ca3af">No data</text>' | |
| max_val = max_value or max(r[1] for r in rows) or 1.0 | |
| bar_h = 22 | |
| bar_gap = 30 | |
| top = y | |
| out = [] | |
| for i, (label, value) in enumerate(rows[:9]): | |
| by = top + i * bar_gap | |
| bw = max(2, int((value / max_val) * (w - 110))) | |
| color = PALETTE[i % len(PALETTE)] | |
| out.append( | |
| f'<rect x="{x}" y="{by}" width="{bw}" height="{bar_h}" rx="3" fill="{color}" opacity="0.85"/>' | |
| ) | |
| out.append( | |
| f'<text x="{x}" y="{by + 15}" font-size="10" fill="#374151">{esc(label[:26])}</text>' | |
| ) | |
| out.append( | |
| f'<text x="{x + w - 30}" y="{by + 15}" font-size="10" font-weight="bold" ' | |
| f'text-anchor="end" fill="#111827">{int(value)} {esc(value_label)}</text>' | |
| ) | |
| out.append(f'<line x1="{x}" y1="{by + bar_h + 4}" x2="{x + w}" y2="{by + bar_h + 4}" stroke="#e5e7eb" stroke-width="1"/>') | |
| return "".join(out) | |
| def domain_blocks_panel( | |
| domains: list[dict], | |
| *, | |
| seq_len: int, | |
| x: int, y: int, w: int, h: int, | |
| ) -> str: | |
| """Domain architecture: colored blocks along a scaled sequence axis.""" | |
| if not domains or not seq_len: | |
| return f'<text x="{x}" y="{y + 20}" font-size="11" fill="#9ca3af">No domain data</text>' | |
| baseline = y + h // 2 | |
| bar = ( | |
| f'<line x1="{x}" y1="{baseline}" x2="{x + w}" y2="{baseline}" ' | |
| f'stroke="#9ca3af" stroke-width="3"/>' | |
| ) | |
| out = [bar] | |
| block_h = 26 | |
| for i, d in enumerate(domains[:16]): | |
| start = max(1, int(d.get("start") or 1)) | |
| end = min(seq_len, int(d.get("end") or seq_len)) | |
| px = x + int((start - 1) / max(1, seq_len) * w) | |
| pw = max(4, int((end - start + 1) / max(1, seq_len) * w)) | |
| color = PALETTE[i % len(PALETTE)] | |
| out.append( | |
| f'<rect x="{px}" y="{baseline - block_h // 2}" width="{pw}" height="{block_h}" ' | |
| f'rx="3" fill="{color}" opacity="0.75"/>' | |
| ) | |
| label = esc(d.get("name") or d.get("accession") or "?") | |
| out.append(f'<text x="{px + 4}" y="{baseline - 4}" font-size="8" fill="#ffffff">{label[:10]}</text>') | |
| out.append(f'<text x="{x}" y="{baseline + 40}" font-size="10" fill="#374151">1</text>') | |
| out.append(f'<text x="{x + w - 30}" y="{baseline + 40}" font-size="10" fill="#374151">{seq_len} aa</text>') | |
| return "".join(out) | |
| def text_lines_panel( | |
| lines: list[str], | |
| *, | |
| x: int, y: int, w: int, h: int, | |
| size: int = 11, | |
| max_lines: int = 10, | |
| ) -> str: | |
| """Wrapped text panel (interpretation previews, phylogeny, notes).""" | |
| if not lines: | |
| return f'<text x="{x}" y="{y + 16}" font-size="11" fill="#9ca3af">No text</text>' | |
| out = [] | |
| row = y + 14 | |
| for line in lines[:max_lines]: | |
| out.append(f'<text x="{x}" y="{row}" font-size="{size}" fill="#374151">{esc(line)}</text>') | |
| row += size + 5 | |
| return "".join(out) | |
| def wrap_text(text: str, width_chars: int = 58) -> list[str]: | |
| words = str(text).split() | |
| lines: list[str] = [] | |
| cur = "" | |
| for word in words: | |
| if len(cur) + len(word) + 1 <= width_chars: | |
| cur = f"{cur} {word}".strip() | |
| else: | |
| lines.append(cur) | |
| cur = word | |
| if cur: | |
| lines.append(cur) | |
| return lines | |
| # --- panel position grid --------------------------------------------------- | |
| #: Fixed 3-col grid on a 1200x800 canvas. Each panel 380x200. | |
| CELL_W, CELL_H = 380, 200 | |
| GRID = [(40 + i * 400, 60 + j * 230) for j in range(3) for i in range(3)] | |
| def panel_xy(index: int) -> tuple[int, int]: | |
| x, y = GRID[index % len(GRID)] | |
| return x, y |