"""Shared chart styling: palette, matplotlib defaults and figure saving.""" from pathlib import Path import matplotlib as mpl import matplotlib.pyplot as plt # --- Diverging poles for sentiment polarity --- NEGATIVE = "#e34948" # red pole NEUTRAL = "#898781" # neutral gray midpoint POSITIVE = "#2a78d6" # blue pole #: Sentiment label -> colour. SENTIMENT_COLORS = { "Negative": NEGATIVE, "Neutral": NEUTRAL, "Positive": POSITIVE, } # --- Categorical slots (first three validate on all pairs) --- SERIES = ("#2a78d6", "#eb6834", "#1baf7a") # --- Sequential blue ramp, light -> dark (magnitude) --- SEQUENTIAL = ["#cde2fb", "#b7d3f6", "#9ec5f4", "#86b6ef", "#6da7ec", "#5598e7", "#3987e5", "#2a78d6", "#256abf", "#1c5cab", "#184f95", "#104281", "#0d366b"] # --- Chrome and ink --- SURFACE = "#fcfcfb" INK = "#0b0b0b" INK_SECONDARY = "#52514e" INK_MUTED = "#898781" GRIDLINE = "#e1e0d9" BASELINE = "#c3c2b7" ACCENT = "#2a78d6" #: Default folder for saved charts. PLOTS_SUBDIR = "plots" def sequential_cmap(): """Return the one-hue blue ramp as a matplotlib colormap.""" return mpl.colors.LinearSegmentedColormap.from_list("asa_blue", SEQUENTIAL) def apply_chart_style(): """Set matplotlib defaults: thin marks, hairline solid grid, recessive axes.""" mpl.rcParams.update({ "figure.facecolor": SURFACE, "axes.facecolor": SURFACE, "savefig.facecolor": SURFACE, "font.family": ["DejaVu Sans"], "font.size": 10, "text.color": INK, "axes.labelcolor": INK_SECONDARY, "axes.edgecolor": BASELINE, "axes.linewidth": 0.8, "axes.titlecolor": INK, "axes.titlesize": 12, "axes.titleweight": "normal", # DejaVu has no medium; avoids a warning "axes.titlepad": 12, "axes.grid": True, "axes.axisbelow": True, "grid.color": GRIDLINE, "grid.linewidth": 0.8, "grid.linestyle": "-", # never dashed - dashes read as a threshold "xtick.color": INK_MUTED, "ytick.color": INK_MUTED, "xtick.labelcolor": INK_SECONDARY, "ytick.labelcolor": INK_SECONDARY, "xtick.direction": "out", "ytick.direction": "out", "legend.frameon": False, "legend.fontsize": 9, "figure.autolayout": False, }) def strip_spines(ax, keep=("left", "bottom")): """Remove chart-junk spines, keeping only the ones that carry meaning.""" for side, spine in ax.spines.items(): spine.set_visible(side in keep) def finish(fig, save_path=None, show=True, dpi=150): """Save and/or show ``fig``, then close it when it is not being shown.""" fig.tight_layout() written = None if save_path is not None: written = Path(save_path) written.parent.mkdir(parents=True, exist_ok=True) fig.savefig(written, dpi=dpi, bbox_inches="tight") if show: plt.show() else: plt.close(fig) return written