feat(utils): add shared styling and color palette
This commit is contained in:
127
src/utils/styling.py
Normal file
127
src/utils/styling.py
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
"""Shared styling utilities and color palette for all charts."""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import matplotlib.figure
|
||||||
|
import matplotlib.axes
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Export & sizing constants
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
EXPORT_DPI = 300
|
||||||
|
FIGURE_SIZE_DEFAULT = (12, 7) # width, height in inches
|
||||||
|
FIGURE_SIZE_SQUARE = (8, 8)
|
||||||
|
FIGURE_SIZE_WIDE = (16, 10)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Zone colors (risk / status shading)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
BUBBLE_ZONE = "#e74c3c" # Red — danger/bubble
|
||||||
|
WARNING_ZONE = "#f39c12" # Orange — warning
|
||||||
|
NORMAL_ZONE = "#27ae60" # Green — normal/healthy
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Data series colors
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AI_SPEND = "#2980b9" # Blue — AI spending
|
||||||
|
REVENUE = "#27ae60" # Green — revenue
|
||||||
|
AGENT_GROWTH = "#8e44ad" # Purple — agent adoption
|
||||||
|
DEBT = "#c0392b" # Dark red — debt
|
||||||
|
PRODUCTIVITY = "#16a085" # Teal — productivity metrics
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Neutral colors
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
GRAY_LIGHT = "#ecf0f1"
|
||||||
|
GRAY_MEDIUM = "#95a5a6"
|
||||||
|
GRAY_DARK = "#2c3e50"
|
||||||
|
BLACK = "#1a1a2e"
|
||||||
|
WHITE = "#ffffff"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Theme
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def get_theme() -> dict:
|
||||||
|
"""Return a matplotlib rcParams dict with a clean, professional look."""
|
||||||
|
return {
|
||||||
|
"font.family": "DejaVu Sans",
|
||||||
|
"font.size": 12,
|
||||||
|
"figure.facecolor": WHITE,
|
||||||
|
"figure.dpi": EXPORT_DPI,
|
||||||
|
"axes.facecolor": "#fafafa",
|
||||||
|
"axes.edgecolor": "#dddddd",
|
||||||
|
"axes.grid": True,
|
||||||
|
"axes.axisbelow": True,
|
||||||
|
"grid.color": "#e0e0e0",
|
||||||
|
"grid.linestyle": "-",
|
||||||
|
"grid.linewidth": 0.5,
|
||||||
|
"grid.alpha": 0.7,
|
||||||
|
"xtick.labelsize": 10,
|
||||||
|
"ytick.labelsize": 10,
|
||||||
|
"axes.titlesize": 16,
|
||||||
|
"axes.titleweight": "bold",
|
||||||
|
"axes.labelsize": 12,
|
||||||
|
"legend.fontsize": 9,
|
||||||
|
"figure.titlesize": 16,
|
||||||
|
"figure.titleweight": "bold",
|
||||||
|
"savefig.dpi": EXPORT_DPI,
|
||||||
|
"savefig.facecolor": WHITE,
|
||||||
|
"savefig.bbox": "tight",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def apply_theme(fig: matplotlib.figure.Figure, ax: Optional[matplotlib.axes.Axes] = None) -> None:
|
||||||
|
"""Apply the professional theme to a figure and optionally its axes.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
fig : matplotlib Figure
|
||||||
|
The figure to theme.
|
||||||
|
ax : matplotlib Axes, optional
|
||||||
|
If provided, spine cleanup is applied to this specific axes.
|
||||||
|
If *None*, every axes in the figure is cleaned.
|
||||||
|
"""
|
||||||
|
plt.rcParams.update(get_theme())
|
||||||
|
fig.set_facecolor(WHITE)
|
||||||
|
|
||||||
|
targets = [ax] if ax is not None else fig.axes
|
||||||
|
for axes in targets:
|
||||||
|
axes.set_facecolor("#fafafa")
|
||||||
|
axes.spines["top"].set_visible(False)
|
||||||
|
axes.spines["right"].set_visible(False)
|
||||||
|
axes.spines["left"].set_color("#cccccc")
|
||||||
|
axes.spines["bottom"].set_color("#cccccc")
|
||||||
|
|
||||||
|
|
||||||
|
def get_bubble_zone_colors() -> dict:
|
||||||
|
"""Return zone colours suitable for shaded risk regions."""
|
||||||
|
return {
|
||||||
|
"bubble": BUBBLE_ZONE,
|
||||||
|
"warning": WARNING_ZONE,
|
||||||
|
"normal": NORMAL_ZONE,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_company_colors() -> dict:
|
||||||
|
"""Return consistent brand-colour mapping for hyperscalers."""
|
||||||
|
return {
|
||||||
|
"Microsoft": "#00a4ef",
|
||||||
|
"Alphabet": "#4285f4",
|
||||||
|
"Meta": "#1877f2",
|
||||||
|
"Amazon": "#ff9900",
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user