Source code for ergodic_insurance.visualization.core

"""Core visualization utilities and constants.

This module provides the foundational elements for visualization including
WSJ-style color palettes, formatters, and base configuration settings.
"""

import matplotlib.pyplot as plt

# WSJ Color Palette
WSJ_COLORS = {
    "light_blue": "#ADD8E6",  # Light Blue for additional styling
    "blue": "#0080C7",  # Primary blue
    "dark_blue": "#003F5C",  # Dark blue
    "red": "#D32F2F",  # Red for negative/warning
    "green": "#4CAF50",  # Green for positive
    "gray": "#666666",  # Gray for secondary
    "light_gray": "#E0E0E0",  # Light gray for grid
    "black": "#000000",  # Black for text
    "orange": "#FF9800",  # Orange for highlights
    "yellow": "#FFD700",  # Yellow for highlights
    "purple": "#7B1FA2",  # Purple for special
    "teal": "#00796B",  # Teal for alternative
}

# Professional color sequence for multiple series
COLOR_SEQUENCE = [
    WSJ_COLORS["blue"],
    WSJ_COLORS["red"],
    WSJ_COLORS["green"],
    WSJ_COLORS["orange"],
    WSJ_COLORS["purple"],
    WSJ_COLORS["teal"],
    WSJ_COLORS["dark_blue"],
]


[docs] def set_wsj_style(): """Set matplotlib to use WSJ-style formatting. This function applies Wall Street Journal aesthetic styling to matplotlib plots including font choices, spine visibility, grid settings, and colors. """ # Set the style plt.style.use("seaborn-v0_8-whitegrid") # Update rcParams for WSJ style plt.rcParams.update( { "font.family": "sans-serif", "font.sans-serif": ["Arial", "Helvetica", "DejaVu Sans"], "font.size": 11, "axes.titlesize": 14, "axes.labelsize": 12, "xtick.labelsize": 10, "ytick.labelsize": 10, "legend.fontsize": 10, "figure.titlesize": 16, "axes.spines.top": False, "axes.spines.right": False, "axes.spines.left": True, "axes.spines.bottom": True, "axes.edgecolor": WSJ_COLORS["gray"], "axes.linewidth": 0.8, "grid.color": WSJ_COLORS["light_gray"], "grid.linewidth": 0.5, "grid.alpha": 0.5, "lines.linewidth": 2, "patch.linewidth": 0.5, "xtick.major.width": 0.8, "ytick.major.width": 0.8, "xtick.minor.width": 0.4, "ytick.minor.width": 0.4, } )
[docs] def format_currency(value: float, decimals: int = 0, abbreviate: bool = False) -> str: """Format value as currency. Args: value: Numeric value to format decimals: Number of decimal places abbreviate: If True, use K/M/B notation for large numbers Returns: Formatted string (e.g., "$1,000" or "$1K" if abbreviate=True) Examples: >>> format_currency(1000) '$1,000' >>> format_currency(1500000, abbreviate=True) '$1.5M' >>> format_currency(2500.50, decimals=2) '$2,500.50' """ if abbreviate: if abs(value) >= 1e9: return f"${value/1e9:.{decimals}f}B" if abs(value) >= 1e6: return f"${value/1e6:.{decimals}f}M" if abs(value) >= 1e3: return f"${value/1e3:.{decimals}f}K" return f"${value:.{decimals}f}" # Handle negative values if value < 0: return f"-${abs(value):,.{decimals}f}" return f"${value:,.{decimals}f}"
[docs] def format_percentage(value: float, decimals: int = 1) -> str: """Format value as percentage. Args: value: Numeric value (0.05 = 5%) decimals: Number of decimal places Returns: Formatted string (e.g., "5.0%") Examples: >>> format_percentage(0.05) '5.0%' >>> format_percentage(0.1234, decimals=2) '12.34%' """ return f"{value*100:.{decimals}f}%"
[docs] class WSJFormatter: """Formatter for WSJ-style axis labels. This class provides static methods for formatting axis values in various styles consistent with Wall Street Journal publication standards. Methods: currency_formatter: Format axis values as currency currency: Format value as currency (shortened method name) percentage_formatter: Format axis values as percentage percentage: Format value as percentage (shortened method name) number: Format large numbers with appropriate suffix millions_formatter: Format axis values in millions """
[docs] @staticmethod def currency_formatter(x, pos): """Format axis values as currency. Args: x: The value to format pos: The position (unused but required by matplotlib) Returns: Formatted currency string """ return format_currency(x, decimals=0, abbreviate=True)
[docs] @staticmethod def currency(x: float, decimals: int = 1) -> str: # pylint: disable=too-many-return-statements """Format value as currency (shortened method name). Args: x: The value to format decimals: Number of decimal places Returns: Formatted currency string with appropriate suffix """ sign = "-" if x < 0 else "" x = abs(x) if x >= 1e12: if x == int(x / 1e12) * 1e12: # Whole trillions return f"{sign}${int(x/1e12)}T" return f"{sign}${x/1e12:.{decimals}f}T" if x >= 1e9: if x == int(x / 1e9) * 1e9: # Whole billions return f"{sign}${int(x/1e9)}B" return f"{sign}${x/1e9:.{decimals}f}B" if x >= 1e6: if x == int(x / 1e6) * 1e6: # Whole millions return f"{sign}${int(x/1e6)}M" return f"{sign}${x/1e6:.{decimals}f}M" if x >= 1e3: if x == int(x / 1e3) * 1e3: # Whole thousands return f"{sign}${int(x/1e3)}K" return f"{sign}${x/1e3:.{decimals}f}K" if 0 < x < 1: return f"{sign}${x:.2f}" return f"{sign}${int(x)}" if x == int(x) else f"{sign}${x:.{decimals}f}"
[docs] @staticmethod def percentage_formatter(x, pos): """Format axis values as percentage. Args: x: The value to format pos: The position (unused but required by matplotlib) Returns: Formatted percentage string """ return format_percentage(x, decimals=0)
[docs] @staticmethod def percentage(x: float, decimals: int = 1) -> str: """Format value as percentage (shortened method name). Args: x: The value to format (0.05 = 5%) decimals: Number of decimal places Returns: Formatted percentage string """ return f"{x*100:.{decimals}f}%"
[docs] @staticmethod def number(x: float, decimals: int = 2) -> str: """Format large numbers with appropriate suffix. Args: x: The value to format decimals: Number of decimal places Returns: Formatted number string with K/M/B/T suffix Examples: >>> WSJFormatter.number(1500000) '1.50M' >>> WSJFormatter.number(2500) '2.50K' """ if abs(x) >= 1e12: if abs(x) >= 1e15: # Very large numbers - show in trillions with multiplier return f"{int(x/1e12)}T" return f"{x/1e12:.{decimals}f}T" if abs(x) >= 1e9: return f"{x/1e9:.{decimals}f}B" if abs(x) >= 1e6: return f"{x/1e6:.{decimals}f}M" if abs(x) >= 1e3: return f"{x/1e3:.{decimals}f}K" return f"{int(x)}" if x == int(x) else f"{x:.{decimals}f}"
[docs] @staticmethod def millions_formatter(x, pos): """Format axis values in millions. Args: x: The value to format pos: The position (unused but required by matplotlib) Returns: Value formatted as millions with M suffix """ return f"{x/1e6:.0f}M"