Source code for gufo.style.theme

"""Theme — immutable style object with registry and context manager support."""
import contextlib
import matplotlib.pyplot as plt

from .color import GUFO_PALETTE


[docs] class Theme: """ An immutable description of visual style for a chart. Themes do not mutate global state until explicitly applied. Use as_context() to scope a theme to a with-block. Use apply_global() (or gufo.set_theme()) to set the global default. """
[docs] def __init__(self, name, rc): self._name = name self._rc = dict(rc)
@property def name(self): """The theme's registered name.""" return self._name
[docs] def merge(self, overrides): """ Return a new Theme that is this theme with overrides applied. This theme is not modified. """ merged_rc = {**self._rc, **overrides} return Theme(self._name, merged_rc)
[docs] def rename(self, name): """Return a copy of this theme with a new name.""" return Theme(name, self._rc)
[docs] @contextlib.contextmanager def as_context(self): """Scope this theme to a with-block using plt.rc_context.""" with plt.rc_context(self._rc): yield
[docs] def apply_global(self): """Apply this theme to matplotlib's global rcParams.""" plt.rcParams.update(self._rc)
# --------------------------------------------------------------------------- # Built-in themes # --------------------------------------------------------------------------- _GUFO_MODERN_RC = { "axes.facecolor": "#f5f5f5", "figure.facecolor": "#ffffff", "axes.grid": True, "grid.color": "#ffffff", "grid.linewidth": 1.2, "axes.spines.top": False, "axes.spines.right": False, "axes.spines.left": False, "axes.spines.bottom": False, "lines.linewidth": 2.0, "font.family": "sans-serif", "axes.prop_cycle": plt.cycler(color=GUFO_PALETTE.categorical), } _GUFO_DARK_PALETTE = [ "#7EB8DA", "#F2A65A", "#7DC87D", "#E87070", "#B09ADB", "#C4A97D", "#F0A8D8", "#B0B0B0", ] _GUFO_DARK_RC = { "axes.facecolor": "#1e1e2e", "figure.facecolor": "#13131e", "axes.grid": True, "grid.color": "#2e2e4e", "grid.linewidth": 1.0, "axes.spines.top": False, "axes.spines.right": False, "axes.spines.left": False, "axes.spines.bottom": False, "lines.linewidth": 2.0, "text.color": "#cccccc", "axes.labelcolor": "#cccccc", "xtick.color": "#cccccc", "ytick.color": "#cccccc", "font.family": "sans-serif", "axes.prop_cycle": plt.cycler(color=_GUFO_DARK_PALETTE), } _GUFO_PRINT_RC = { "axes.facecolor": "#ffffff", "figure.facecolor": "#ffffff", "axes.grid": True, "grid.color": "#cccccc", "grid.linewidth": 0.8, "axes.spines.top": False, "axes.spines.right": False, "lines.linewidth": 1.5, "font.family": "serif", "axes.prop_cycle": plt.cycler(color=[ "#000000", "#444444", "#888888", "#bbbbbb", ]), } # --------------------------------------------------------------------------- # Registry # --------------------------------------------------------------------------- _REGISTRY: dict = {}
[docs] def register_theme(theme: Theme) -> None: """Register a Theme so it can be referenced by name.""" _REGISTRY[theme.name] = theme
[docs] def get_theme(name: str) -> Theme: """Return the Theme registered under name.""" if name not in _REGISTRY: available = list(_REGISTRY) raise ValueError(f"Unknown theme '{name}'. Available: {available}") return _REGISTRY[name]
[docs] def set_theme(name: str = "gufo_modern") -> None: """Apply a theme globally to all subsequent charts.""" get_theme(name).apply_global()
[docs] @contextlib.contextmanager def theme_context(name: str): """Temporarily apply a theme within a with-block.""" with get_theme(name).as_context(): yield
class _PassthroughTheme(Theme): """A theme that does nothing — inherits the ambient matplotlib state.""" def __init__(self): super().__init__("_passthrough", {}) @contextlib.contextmanager def as_context(self): """Pass-through context: inherit whatever rcParams are currently active.""" yield _PASSTHROUGH = _PassthroughTheme() def _resolve_theme(override): """Internal: return the Theme to use for a single render call.""" if override is None: # No chart-level override — inherit whatever is currently active # (including any global theme set via set_theme()). return _PASSTHROUGH if isinstance(override, str): return get_theme(override) if isinstance(override, Theme): return override raise TypeError(f"theme() expects a theme name (str) or Theme object, got {type(override)}") # Register built-ins at import time register_theme(Theme("gufo_modern", _GUFO_MODERN_RC)) register_theme(Theme("gufo_dark", _GUFO_DARK_RC)) register_theme(Theme("gufo_print", _GUFO_PRINT_RC))