# viz/plots.py — Python 3.8 compatible plotting utilities from __future__ import annotations import math import numpy as np import matplotlib.pyplot as plt from cycler import cycler # ----- Unified style (kept lightweight) ----- default_cycler = ( cycler(color=["C0", "C1", "C2", "C3", "C4", "C5"]) + cycler(marker=["s", "D", "^", "v", "o", "x"]) + cycler(linestyle=[":", "--", "-", "-.", "--", ":"]) ) plt.rc("axes", prop_cycle=default_cycler) # ----- 95% CI (t critical) ----- def tcrit_95(n: int) -> float: """Return ~95% t critical value for sample size n (simple, conservative).""" if n <= 1: return float("inf") if n < 30: # Conservative constant (close to df=9..29 range) return 2.262 return 1.96 def mean_ci95(vals): """Return (mean, half_width) for 95% CI.""" arr = np.array(list(vals), dtype=float) n = len(arr) if n == 0: return 0.0, 0.0 if n == 1: return float(arr[0]), 0.0 m = float(arr.mean()) s = float(arr.std(ddof=1)) half = tcrit_95(n) * (s / math.sqrt(n)) return m, half def plot_with_ci_band(ax, xs, mean, half, *, label, line_kwargs=None, band_kwargs=None): """Plot mean line and shaded CI band (Python 3.8 safe).""" line_kwargs = {} if line_kwargs is None else dict(line_kwargs) band = {"alpha": 0.25} if band_kwargs is not None: band.update(dict(band_kwargs)) line, = ax.plot(xs, mean, label=label, **line_kwargs) ax.fill_between(xs, mean - half, mean + half, **band) return line