| """ |
| ui_theme.py |
| ----------- |
| Miami University branded theme and styling utilities. |
| |
| Provides: |
| - Gradio theme subclass (MiamiTheme) with Miami branding |
| - Custom CSS string for elements beyond theme control |
| - Matplotlib rcParams styled with Miami branding |
| - ColorBrewer palette loading via palettable with graceful fallback |
| - Color-swatch preview figure generation |
| """ |
|
|
| from __future__ import annotations |
|
|
| import itertools |
| from typing import Dict, List, Optional |
|
|
| import gradio as gr |
| from gradio.themes.base import Base |
| from gradio.themes.utils import colors, fonts, sizes |
| import matplotlib.figure |
| import matplotlib.pyplot as plt |
|
|
| |
| |
| |
| MIAMI_RED: str = "#C41230" |
| MIAMI_BLACK: str = "#000000" |
| MIAMI_WHITE: str = "#FFFFFF" |
|
|
| |
| _WHITE = "#FFFFFF" |
| _BLACK = "#000000" |
| _LIGHT_GRAY = "#F5F5F5" |
| _BORDER_GRAY = "#E0E0E0" |
| _DARK_TEXT = "#000000" |
| _HOVER_RED = "#9E0E26" |
|
|
|
|
| |
| |
| |
|
|
| _miami_red_palette = colors.Color( |
| c50="#fff5f6", |
| c100="#ffe0e4", |
| c200="#ffc7ce", |
| c300="#ffa3ad", |
| c400="#ff6b7d", |
| c500="#C41230", |
| c600="#a30f27", |
| c700="#850c1f", |
| c800="#6b0a19", |
| c900="#520714", |
| c950="#3d0510", |
| name="miami_red", |
| ) |
|
|
|
|
| class MiamiTheme(Base): |
| """Gradio theme subclass with Miami University branding.""" |
|
|
| def __init__(self, **kwargs): |
| super().__init__( |
| primary_hue=_miami_red_palette, |
| secondary_hue=colors.gray, |
| neutral_hue=colors.gray, |
| spacing_size=sizes.spacing_md, |
| radius_size=sizes.radius_sm, |
| text_size=sizes.text_md, |
| font=( |
| fonts.GoogleFont("Source Sans Pro"), |
| fonts.Font("ui-sans-serif"), |
| fonts.Font("system-ui"), |
| fonts.Font("sans-serif"), |
| ), |
| font_mono=( |
| fonts.Font("ui-monospace"), |
| fonts.Font("SFMono-Regular"), |
| fonts.Font("monospace"), |
| ), |
| **kwargs, |
| ) |
| super().set( |
| |
| button_primary_background_fill="*primary_500", |
| button_primary_background_fill_hover="*primary_700", |
| button_primary_text_color="white", |
| button_primary_border_color="*primary_500", |
| |
| block_title_text_weight="600", |
| block_title_text_color="*primary_500", |
| |
| body_text_color="*neutral_900", |
| |
| block_border_width="1px", |
| block_border_color="*neutral_200", |
| |
| checkbox_background_color_selected="*primary_500", |
| checkbox_border_color_selected="*primary_500", |
| ) |
|
|
|
|
| def get_miami_css() -> str: |
| """Return custom CSS for elements that ``gr.themes.Base`` cannot control. |
| |
| This string is passed to ``gr.Blocks(css=...)`` alongside the |
| :class:`MiamiTheme`. |
| """ |
| return f""" |
| /* ---- Sidebar header accent ---- */ |
| .sidebar > .panel {{ |
| border-top: 4px solid {MIAMI_RED} !important; |
| }} |
| |
| /* ---- Developer card ---- */ |
| .dev-card {{ |
| padding: 0; |
| background: transparent; |
| }} |
| .dev-row {{ |
| display: flex; |
| gap: 0.5rem; |
| align-items: flex-start; |
| }} |
| .dev-avatar {{ |
| width: 28px; |
| height: 28px; |
| min-width: 28px; |
| fill: {_BLACK}; |
| }} |
| .dev-name {{ |
| font-weight: 600; |
| color: {_BLACK}; |
| font-size: 0.82rem; |
| line-height: 1.3; |
| }} |
| .dev-role {{ |
| font-size: 0.7rem; |
| color: #6c757d; |
| line-height: 1.3; |
| }} |
| .dev-links {{ |
| display: flex; |
| gap: 0.3rem; |
| flex-wrap: wrap; |
| margin-top: 0.35rem; |
| }} |
| .dev-link, |
| .dev-link:visited, |
| .dev-link:link {{ |
| display: inline-flex; |
| align-items: center; |
| gap: 0.2rem; |
| padding: 0.15rem 0.4rem; |
| border: 1px solid {MIAMI_RED}; |
| border-radius: 4px; |
| font-size: 0.65rem; |
| color: {MIAMI_RED} !important; |
| text-decoration: none; |
| background: {_WHITE}; |
| line-height: 1.4; |
| white-space: nowrap; |
| }} |
| .dev-link svg {{ |
| width: 11px; |
| height: 11px; |
| fill: {MIAMI_RED}; |
| }} |
| .dev-link:hover {{ |
| background-color: {MIAMI_RED}; |
| color: {_WHITE} !important; |
| }} |
| .dev-link:hover svg {{ |
| fill: {_WHITE}; |
| }} |
| |
| /* ---- Metric-like stat cards ---- */ |
| .stat-card {{ |
| background-color: {_LIGHT_GRAY}; |
| box-shadow: inset 4px 0 0 0 {MIAMI_RED}; |
| border-radius: 6px; |
| padding: 0.6rem 0.75rem 0.6rem 1rem; |
| }} |
| .stat-card .stat-label {{ |
| color: {_BLACK}; |
| font-size: 0.78rem; |
| }} |
| .stat-card .stat-value {{ |
| color: {_BLACK}; |
| font-weight: 700; |
| font-size: 0.95rem; |
| }} |
| |
| /* ---- Step cards on welcome screen ---- */ |
| .step-card {{ |
| background: {_LIGHT_GRAY}; |
| border-radius: 8px; |
| padding: 1rem; |
| border-left: 4px solid {MIAMI_RED}; |
| height: 100%; |
| }} |
| .step-card .step-number {{ |
| font-size: 1.6rem; |
| font-weight: 700; |
| color: {MIAMI_RED}; |
| }} |
| .step-card .step-title {{ |
| font-weight: 600; |
| margin: 0.3rem 0 0.2rem; |
| }} |
| .step-card .step-desc {{ |
| font-size: 0.82rem; |
| color: #444; |
| }} |
| |
| /* ---- App title in sidebar ---- */ |
| .app-title {{ |
| text-align: center; |
| margin-bottom: 0.5rem; |
| }} |
| .app-title .title-text {{ |
| font-size: 1.6rem; |
| font-weight: 800; |
| color: {MIAMI_RED}; |
| }} |
| .app-title .subtitle-text {{ |
| font-size: 0.82rem; |
| color: {_BLACK}; |
| }} |
| """ |
|
|
|
|
| |
| |
| |
| def get_miami_mpl_style() -> Dict[str, object]: |
| """Return a dictionary of matplotlib rcParams for Miami branding. |
| |
| Usage:: |
| |
| import matplotlib as mpl |
| mpl.rcParams.update(get_miami_mpl_style()) |
| |
| Or apply to a single figure:: |
| |
| with mpl.rc_context(get_miami_mpl_style()): |
| fig, ax = plt.subplots() |
| ... |
| """ |
| return { |
| |
| "figure.facecolor": _WHITE, |
| "figure.edgecolor": _WHITE, |
| "figure.figsize": (10, 5), |
| "figure.dpi": 100, |
| |
| "axes.facecolor": _WHITE, |
| "axes.edgecolor": _BLACK, |
| "axes.labelcolor": _BLACK, |
| "axes.titlecolor": MIAMI_RED, |
| "axes.labelsize": 12, |
| "axes.titlesize": 14, |
| "axes.titleweight": "bold", |
| "axes.prop_cycle": plt.cycler( |
| color=[MIAMI_RED, _BLACK, "#4E79A7", "#F28E2B", "#76B7B2"] |
| ), |
| |
| "axes.grid": True, |
| "grid.color": _BORDER_GRAY, |
| "grid.linestyle": "--", |
| "grid.linewidth": 0.6, |
| "grid.alpha": 0.7, |
| |
| "xtick.color": _BLACK, |
| "ytick.color": _BLACK, |
| "xtick.labelsize": 10, |
| "ytick.labelsize": 10, |
| |
| "legend.fontsize": 10, |
| "legend.frameon": True, |
| "legend.framealpha": 0.9, |
| "legend.edgecolor": _BORDER_GRAY, |
| |
| "font.size": 11, |
| "font.family": "sans-serif", |
| |
| "savefig.dpi": 150, |
| "savefig.bbox": "tight", |
| } |
|
|
|
|
| |
| |
| |
|
|
| |
| _PALETTE_MAP: Dict[str, str] = { |
| "Set1": "colorbrewer.qualitative.Set1", |
| "Set2": "colorbrewer.qualitative.Set2", |
| "Set3": "colorbrewer.qualitative.Set3", |
| "Dark2": "colorbrewer.qualitative.Dark2", |
| "Paired": "colorbrewer.qualitative.Paired", |
| "Pastel1": "colorbrewer.qualitative.Pastel1", |
| "Pastel2": "colorbrewer.qualitative.Pastel2", |
| "Accent": "colorbrewer.qualitative.Accent", |
| "Tab10": "colorbrewer.qualitative.Set1", |
| } |
|
|
| _FALLBACK_COLORS: List[str] = [ |
| MIAMI_RED, |
| MIAMI_BLACK, |
| "#4E79A7", |
| "#F28E2B", |
| "#76B7B2", |
| "#E15759", |
| "#59A14F", |
| "#EDC948", |
| ] |
|
|
|
|
| def _resolve_palette(name: str) -> Optional[List[str]]: |
| """Dynamically import a palettable ColorBrewer palette by *name*. |
| |
| Palettable organises palettes by maximum number of classes, e.g. |
| ``colorbrewer.qualitative.Set2_8``. We find the variant with the |
| most colours available so the caller gets the richest palette. |
| """ |
| import importlib |
|
|
| module_path = _PALETTE_MAP.get(name) |
| if module_path is None: |
| |
| module_path = f"colorbrewer.qualitative.{name}" |
|
|
| |
| try: |
| mod = importlib.import_module(f"palettable.{module_path}") |
| except (ImportError, ModuleNotFoundError): |
| return None |
|
|
| |
| best = None |
| best_n = 0 |
| base = name.split(".")[-1] if "." in name else name |
| for attr_name in dir(mod): |
| if not attr_name.startswith(base + "_"): |
| continue |
| try: |
| suffix = int(attr_name.split("_")[-1]) |
| except ValueError: |
| continue |
| if suffix > best_n: |
| best_n = suffix |
| best = attr_name |
|
|
| if best is None: |
| return None |
|
|
| palette_obj = getattr(mod, best, None) |
| if palette_obj is None: |
| return None |
|
|
| return [ |
| "#{:02X}{:02X}{:02X}".format(*rgb) for rgb in palette_obj.colors |
| ] |
|
|
|
|
| def get_palette_colors(name: str = "Set2", n: int = 8) -> List[str]: |
| """Load *n* hex colour strings from a ColorBrewer palette. |
| |
| Parameters |
| ---------- |
| name: |
| Friendly palette name such as ``"Set2"``, ``"Dark2"``, ``"Paired"``. |
| n: |
| Number of colours required. If *n* exceeds the palette length the |
| colours are cycled. |
| |
| Returns |
| ------- |
| list[str] |
| List of *n* hex colour strings (e.g. ``["#66C2A5", ...]``). |
| |
| Notes |
| ----- |
| If the requested palette cannot be found, a sensible fallback list is |
| returned so that calling code never receives an empty list. |
| """ |
| n = max(1, n) |
| colors = _resolve_palette(name) |
| if colors is None: |
| colors = _FALLBACK_COLORS |
|
|
| |
| cycled = list(itertools.islice(itertools.cycle(colors), n)) |
| return cycled |
|
|
|
|
| |
| |
| |
| def render_palette_preview( |
| colors: List[str], |
| swatch_width: float = 1.0, |
| swatch_height: float = 0.4, |
| ) -> matplotlib.figure.Figure: |
| """Create a small matplotlib figure showing colour swatches. |
| |
| Parameters |
| ---------- |
| colors: |
| List of hex colour strings to display. |
| swatch_width: |
| Width of each individual swatch in inches. |
| swatch_height: |
| Height of the swatch strip in inches. |
| |
| Returns |
| ------- |
| matplotlib.figure.Figure |
| A Figure instance ready to be passed to ``gr.Plot`` or saved. |
| """ |
| n = len(colors) |
| fig_width = max(swatch_width * n, 2.0) |
| fig, ax = plt.subplots( |
| figsize=(fig_width, swatch_height + 0.3), dpi=100 |
| ) |
|
|
| for i, colour in enumerate(colors): |
| ax.add_patch( |
| plt.Rectangle( |
| (i, 0), |
| width=1, |
| height=1, |
| facecolor=colour, |
| edgecolor=_WHITE, |
| linewidth=1.5, |
| ) |
| ) |
|
|
| ax.set_xlim(0, n) |
| ax.set_ylim(0, 1) |
| ax.set_aspect("equal") |
| ax.axis("off") |
| fig.subplots_adjust(left=0, right=1, top=1, bottom=0) |
| plt.close(fig) |
| return fig |
|
|