-
-
Notifications
You must be signed in to change notification settings - Fork 21
Add colorblind-safe visualization theme and resolve merge conflict #136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,11 +40,12 @@ | |
|
|
||
| from __future__ import annotations | ||
|
|
||
| import warnings | ||
| from collections.abc import Iterator | ||
| from copy import deepcopy | ||
| from contextlib import contextmanager | ||
| from dataclasses import dataclass, field | ||
| from typing import Any | ||
| import warnings | ||
|
|
||
| import matplotlib as mpl | ||
| import matplotlib.pyplot as plt | ||
|
|
@@ -157,6 +158,25 @@ | |
| "bar_alpha": 0.85, | ||
| } | ||
|
|
||
| COLORBLIND_PALETTE: list[str] = [ | ||
| "#0072B2", | ||
| "#E69F00", | ||
| "#009E73", | ||
| "#CC79A7", | ||
| "#56B4E9", | ||
| "#D55E00", | ||
| "#F0E442", | ||
| ] | ||
|
|
||
| COLORBLIND_SEMANTIC_COLORS: dict[str, dict[str, str]] = { | ||
| "severity": { | ||
| "acceptable": "#0072B2", | ||
| "moderate": "#E69F00", | ||
| "severe": "#D55E00", | ||
| "unknown": "#999999", | ||
| } | ||
| } | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Theme — frozen dataclass bundling everything above for future extensibility | ||
| # --------------------------------------------------------------------------- | ||
|
|
@@ -171,28 +191,6 @@ class Theme: | |
| that future themes (``"dark"``, ``"colorblind"``, ``"publication"``) can be | ||
| added by registering a new :class:`Theme` instance without touching | ||
| plotting code. | ||
|
|
||
| Attributes | ||
| ---------- | ||
| name : str | ||
| Identifier (e.g. ``"default"``). | ||
| base_style : str | ||
| Matplotlib base style sheet applied inside :func:`apply_style`. | ||
| palette : list[str] | ||
| Categorical palette as ordered hex strings. | ||
| brand : dict[str, str] | ||
| Named brand colors. | ||
| semantic : dict[str, dict[str, str]] | ||
| Semantic color groups (``severity``, ``verdict``, ``grade``, | ||
| ``direction``, ``neutral``). | ||
| typography : dict[str, Any] | ||
| Font family and size settings. | ||
| grid : dict[str, Any] | ||
| Grid alpha and linewidth. | ||
| fig_defaults : dict[str, Any] | ||
| Default figure size, facecolor, and DPI. | ||
| spacing : dict[str, Any] | ||
| Padding, alpha, and bar styling. | ||
| """ | ||
|
|
||
| name: str = "default" | ||
|
|
@@ -210,6 +208,12 @@ class Theme: | |
|
|
||
| DEFAULT_THEME: Theme = Theme() | ||
|
|
||
| COLORBLIND_THEME: Theme = Theme( | ||
| name="colorblind", | ||
| palette=list(COLORBLIND_PALETTE), | ||
| semantic=deepcopy(COLORBLIND_SEMANTIC_COLORS), | ||
| ) | ||
|
Comment on lines
+211
to
+215
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2 | Confidence: Medium Speculative: The new Code Suggestion: def apply_style(theme: Theme | None = None) -> Iterator[Theme]:
if theme is None:
# Optionally allow global setting
theme = _ACTIVE_THEME # or DEFAULT_THEME
... |
||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Context manager — scoped rcParams mutations | ||
|
|
@@ -220,29 +224,6 @@ class Theme: | |
| def apply_style(theme: Theme | None = None) -> Iterator[Theme]: | ||
| """ | ||
| Apply a TrustLens theme inside a ``with`` block, restoring state on exit. | ||
|
|
||
| This context manager scopes all ``matplotlib.rcParams`` mutations and base | ||
| style sheet changes to the ``with`` block. On exit, the previous rcParams | ||
| are restored. This avoids leaking TrustLens styling into the user's | ||
| matplotlib session — important when TrustLens runs inside notebooks or | ||
| larger ML pipelines. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| theme : Theme, optional | ||
| Theme to apply. Defaults to :data:`DEFAULT_THEME`. | ||
|
|
||
| Yields | ||
| ------ | ||
| Theme | ||
| The active theme, so the caller can read its constants without a | ||
| separate import. | ||
|
|
||
| Examples | ||
| -------- | ||
| >>> with apply_style() as theme: | ||
| ... fig, ax = plt.subplots() | ||
| ... ax.plot([0, 1], [0, 1], color=theme.brand["blue"]) | ||
| """ | ||
| active = theme if theme is not None else DEFAULT_THEME | ||
| previous = mpl.rcParams.copy() | ||
|
|
@@ -290,27 +271,6 @@ def styled_figure( | |
| ) -> tuple[plt.Figure, Any]: | ||
| """ | ||
| Create a figure and axes pre-configured with TrustLens styling. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| figsize : tuple of float, optional | ||
| Figure size. Defaults to ``theme.fig_defaults["figsize"]``. | ||
| nrows : int | ||
| Number of subplot rows. | ||
| ncols : int | ||
| Number of subplot columns. | ||
| theme : Theme, optional | ||
| Theme to source defaults from. Defaults to :data:`DEFAULT_THEME`. | ||
| grid : bool | ||
| Whether to enable the themed grid on the axes. | ||
| **subplots_kwargs | ||
| Forwarded to :func:`matplotlib.pyplot.subplots`. | ||
|
|
||
| Returns | ||
| ------- | ||
| fig : matplotlib.figure.Figure | ||
| ax : matplotlib.axes.Axes or array of Axes | ||
| Same shape as :func:`matplotlib.pyplot.subplots` returns. | ||
| """ | ||
| active = theme if theme is not None else DEFAULT_THEME | ||
| size = figsize if figsize is not None else active.fig_defaults["figsize"] | ||
|
|
@@ -335,26 +295,6 @@ def styled_figure( | |
| def get_categorical_colors(n: int, theme: Theme | None = None) -> list[str]: | ||
| """ | ||
| Return ``n`` categorical colors from the active theme palette. | ||
|
|
||
| Cycles through the palette when ``n`` exceeds its length, so callers can | ||
| request any positive ``n`` without bounds checking. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| n : int | ||
| Number of colors requested. Must be non-negative. | ||
| theme : Theme, optional | ||
| Theme whose palette to draw from. Defaults to :data:`DEFAULT_THEME`. | ||
|
|
||
| Returns | ||
| ------- | ||
| list of str | ||
| ``n`` hex color strings. | ||
|
|
||
| Raises | ||
| ------ | ||
| ValueError | ||
| When ``n < 0`` or when the theme's palette is empty. | ||
| """ | ||
| if n < 0: | ||
| raise ValueError(f"n must be non-negative, got {n}") | ||
|
|
@@ -363,3 +303,20 @@ def get_categorical_colors(n: int, theme: Theme | None = None) -> list[str]: | |
| if not palette: | ||
| raise ValueError("theme palette must be non-empty") | ||
| return [palette[i % len(palette)] for i in range(n)] | ||
|
|
||
|
|
||
| __all__ = [ | ||
| "BRAND_COLORS", | ||
| "PALETTE", | ||
| "SEMANTIC_COLORS", | ||
| "TYPOGRAPHY", | ||
| "GRID", | ||
| "FIG_DEFAULTS", | ||
| "SPACING", | ||
| "Theme", | ||
| "DEFAULT_THEME", | ||
| "COLORBLIND_THEME", | ||
| "apply_style", | ||
| "styled_figure", | ||
| "get_categorical_colors", | ||
| ] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing semantic color mappings will cause KeyError at runtime.
COLORBLIND_SEMANTIC_COLORSonly defines the"severity"key (lines 171-178), but the defaultSEMANTIC_COLORSprovides five semantic mappings:"severity","verdict","grade","direction", and"neutral". By explicitly passingsemantic=deepcopy(COLORBLIND_SEMANTIC_COLORS), the colorblind theme will lack the other four mappings. Code that accessestheme.semantic["verdict"]or other missing keys when usingCOLORBLIND_THEMEwill raiseKeyError.🛡️ Proposed fixes
Option 1 (recommended): Merge with defaults, override severity only
COLORBLIND_THEME: Theme = Theme( name="colorblind", palette=list(COLORBLIND_PALETTE), - semantic=deepcopy(COLORBLIND_SEMANTIC_COLORS), + semantic={ + **{k: dict(v) for k, v in SEMANTIC_COLORS.items()}, + **COLORBLIND_SEMANTIC_COLORS, + }, )Option 2: Define all semantic mappings in
COLORBLIND_SEMANTIC_COLORSAdd the missing keys to
COLORBLIND_SEMANTIC_COLORS(lines 171-178):COLORBLIND_SEMANTIC_COLORS: dict[str, dict[str, str]] = { "severity": { "acceptable": "`#0072B2`", "moderate": "`#E69F00`", "severe": "`#D55E00`", "unknown": "`#999999`", - } + }, + "verdict": { + "deploy": "`#0072B2`", + "caution": "`#E69F00`", + "do_not_deploy": "`#D55E00`", + }, + "grade": { + "A": "`#009E73`", + "B": "`#0072B2`", + "C": "`#E69F00`", + "D": "`#D55E00`", + }, + "direction": { + "positive": "`#0072B2`", + "negative": "`#E69F00`", + }, + "neutral": { + "reference": "`#999999`", + "edge": "`#FFFFFF`", + "annotation_edge": "`#CCCCCC`", + "annotation_face": "`#FFFFFF`", + }, }Then update line 214:
📝 Committable suggestion
🤖 Prompt for AI Agents