Ashkan Taghipour (The University of Western Australia)
UI overhaul: immersive chapter-based experience
14ba315 | """Shared helpers for the Pigeon Pea Pangenome Atlas.""" | |
| import os | |
| import logging | |
| import time | |
| from pathlib import Path | |
| from functools import wraps | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") | |
| logger = logging.getLogger("pangenome") | |
| PROJECT_ROOT = Path(__file__).resolve().parent.parent | |
| DATA_DIR = PROJECT_ROOT / "data" | |
| PRECOMPUTED_DIR = PROJECT_ROOT / "precomputed" | |
| def find_file(directory: Path, pattern: str) -> Path: | |
| """Find first file matching glob pattern in directory.""" | |
| matches = list(directory.glob(pattern)) | |
| if not matches: | |
| raise FileNotFoundError(f"No file matching '{pattern}' in {directory}") | |
| return matches[0] | |
| def timer(func): | |
| """Decorator that logs execution time.""" | |
| def wrapper(*args, **kwargs): | |
| t0 = time.time() | |
| result = func(*args, **kwargs) | |
| dt = time.time() - t0 | |
| logger.info(f"{func.__name__} completed in {dt:.2f}s") | |
| return result | |
| return wrapper | |
| KNOWN_COUNTRIES = { | |
| "India", "Myanmar", "Unknown", "Zaire", "Uganda", "Indonesia", "Jamaica", | |
| "South_Africa", "Puerto_Rico", "Philippines", "Sierra_Leone", "Nigeria", | |
| "Malawi", "Italy", "Kenya", "Sri_Lanka", "Thailand", "Nepal", | |
| } | |
| # Approximate centroid coordinates (lat, lon) for each country. | |
| COUNTRY_COORDS = { | |
| "India": (20.59, 78.96), | |
| "Philippines": (12.88, 121.77), | |
| "Kenya": (-1.29, 36.82), | |
| "Nepal": (28.39, 84.12), | |
| "Myanmar": (21.92, 95.96), | |
| "Uganda": (1.37, 32.29), | |
| "Zaire": (-4.04, 21.76), | |
| "Indonesia": (-0.79, 113.92), | |
| "Jamaica": (18.11, -77.30), | |
| "South_Africa": (-30.56, 22.94), | |
| "Puerto_Rico": (18.22, -66.59), | |
| "Sierra_Leone": (8.46, -11.78), | |
| "Nigeria": (9.08, 7.49), | |
| "Malawi": (-13.25, 34.30), | |
| "Italy": (41.87, 12.57), | |
| "Sri_Lanka": (7.87, 80.77), | |
| "Thailand": (15.87, 100.99), | |
| } | |
| def parse_country(line_id: str) -> str: | |
| """Extract country from line ID (last token after underscore).""" | |
| parts = line_id.rsplit("_", 1) | |
| if len(parts) == 2 and parts[1] in KNOWN_COUNTRIES: | |
| return parts[1] | |
| # Try two-word countries | |
| parts2 = line_id.rsplit("_", 2) | |
| if len(parts2) >= 3: | |
| two_word = f"{parts2[-2]}_{parts2[-1]}" | |
| if two_word in KNOWN_COUNTRIES: | |
| return two_word | |
| return "Unknown" | |
| # ===================================================================== | |
| # HTML builder helpers | |
| # ===================================================================== | |
| def build_hero_header( | |
| total_genes: int, | |
| n_lines: int, | |
| n_countries: int, | |
| n_clusters: int, | |
| ) -> str: | |
| """Return an HTML string for the hero dashboard header. | |
| Renders a dark (#1a2332) banner with large stat numbers and small | |
| uppercase labels. Uses the ``.hero-header``, ``.hero-stat``, and | |
| ``.hero-subtitle`` CSS classes defined in ``ui/theme.py``. | |
| Parameters | |
| ---------- | |
| total_genes : int | |
| Total number of genes in the pangenome. | |
| n_lines : int | |
| Number of accession lines (e.g. 89 + reference). | |
| n_countries : int | |
| Number of countries of origin. | |
| n_clusters : int | |
| Number of genomic clusters. | |
| """ | |
| stats = [ | |
| (f"{total_genes:,}", "Total Genes"), | |
| (str(n_lines), "Lines"), | |
| (str(n_countries), "Countries"), | |
| (str(n_clusters), "Clusters"), | |
| ] | |
| stat_html = "\n".join( | |
| f'<span class="hero-stat">' | |
| f'<span class="stat-number">{value}</span>' | |
| f'<span class="stat-label">{label}</span>' | |
| f"</span>" | |
| for value, label in stats | |
| ) | |
| return ( | |
| '<div class="hero-header">' | |
| "<h1>Pigeon Pea Pangenome Atlas</h1>" | |
| '<p class="hero-subtitle">' | |
| "An interactive exploration of presence-absence variation across " | |
| "pigeon pea accessions worldwide." | |
| "</p>" | |
| '<div class="hero-stats">' | |
| f"{stat_html}" | |
| "</div>" | |
| "</div>" | |
| ) | |
| def build_metric_card(value: str, label: str, color: str = "green") -> str: | |
| """Return an HTML string for a single metric card. | |
| Parameters | |
| ---------- | |
| value : str | |
| The large number or text to display (e.g. ``"55,512"``). | |
| label : str | |
| Short uppercase caption below the number (e.g. ``"TOTAL GENES"``). | |
| color : str, optional | |
| Color accent for the top border. One of ``"green"`` (default), | |
| ``"amber"``, ``"red"``, or ``"blue"``. Maps to CSS modifier | |
| classes on ``.metric-card``. | |
| """ | |
| color_class = "" | |
| if color in ("amber", "red", "blue"): | |
| color_class = f" {color}" | |
| return ( | |
| f'<div class="metric-card{color_class}">' | |
| f'<div class="metric-value">{value}</div>' | |
| f'<div class="metric-label">{label}</div>' | |
| "</div>" | |
| ) | |
| def build_progress_stepper(current_step: int = 0, total_steps: int = 6) -> str: | |
| """Return an HTML string for a visual progress stepper. | |
| Renders a horizontal row of numbered circles connected by lines. | |
| Steps before *current_step* are marked complete (green filled), | |
| *current_step* itself gets a green ring, and later steps are dimmed. | |
| Uses ``.progress-stepper``, ``.step-complete``, ``.step-current``, | |
| and ``.step-future`` CSS classes from ``ui/theme.py``. | |
| Parameters | |
| ---------- | |
| current_step : int | |
| Zero-based index of the active step. | |
| total_steps : int | |
| Total number of steps (default 6, matching the quest count). | |
| """ | |
| step_labels = [ | |
| "Explorer", | |
| "Map the World", | |
| "Core vs Accessory", | |
| "Genome Landmarks", | |
| "Protein Relics", | |
| "Field Report", | |
| ] | |
| parts: list[str] = [] | |
| for i in range(total_steps): | |
| label = step_labels[i] if i < len(step_labels) else f"Step {i + 1}" | |
| if i < current_step: | |
| cls = "step step-complete" | |
| dot_content = "✓" # checkmark | |
| elif i == current_step: | |
| cls = "step step-current" | |
| dot_content = str(i + 1) | |
| else: | |
| cls = "step step-future" | |
| dot_content = str(i + 1) | |
| parts.append( | |
| f'<div class="{cls}">' | |
| f'<span class="dot">{dot_content}</span>' | |
| f"<span>{label}</span>" | |
| f"</div>" | |
| ) | |
| return '<div class="progress-stepper">' + "".join(parts) + "</div>" | |