Repository Public Research preview
1"""Publication figure style: validated palette (categorical fixed order, one-hue sequential ramp,2blue-red diverging pair with neutral grey midpoint), vector output, recessive axes.34Palette values follow the project data-visualization reference (light surface). Categorical hues5are assigned in fixed slot order to fixed entities and never cycled.6"""7from __future__ import annotations89import matplotlib10import matplotlib.pyplot as plt11from matplotlib.colors import LinearSegmentedColormap1213CATEGORICAL = ["#2a78d6", "#eb6834", "#1baf7a", "#eda100", "#e87ba4", "#008300", "#6250d6", "#e34948"]14ENTITY_COLORS = {"donor": CATEGORICAL[0], "recipient": CATEGORICAL[1], "graft": CATEGORICAL[2], "sham": CATEGORICAL[3],15 "random": CATEGORICAL[4], "chance": "#9a9892"}16SEQUENTIAL_STEPS = ["#fcfcfb", "#cde2fb", "#9ec5f4", "#6da7ec", "#3987e5", "#256abf", "#184f95", "#0d366b"]17DIVERGING_STEPS = ["#0d366b", "#3987e5", "#9ec5f4", "#f0efec", "#f2a09f", "#e34948", "#8f1d1c"]18TEXT_PRIMARY, TEXT_SECONDARY, GRID = "#0b0b0b", "#52514e", "#e2e1dd"1920SEQUENTIAL_CMAP = LinearSegmentedColormap.from_list("wp_seq_blue", SEQUENTIAL_STEPS)21DIVERGING_CMAP = LinearSegmentedColormap.from_list("wp_div_blue_red", DIVERGING_STEPS)2223COLUMN_WIDTH_IN = 3.35 # single journal column24DOUBLE_WIDTH_IN = 6.9252627def apply_style() -> None:28 matplotlib.rcParams.update({29 "pdf.fonttype": 42, "ps.fonttype": 42, "svg.fonttype": "none",30 "font.family": "sans-serif", "font.size": 8, "axes.titlesize": 8.5, "axes.labelsize": 8,31 "xtick.labelsize": 7, "ytick.labelsize": 7, "legend.fontsize": 7,32 "axes.edgecolor": GRID, "axes.linewidth": 0.6, "xtick.color": TEXT_SECONDARY, "ytick.color": TEXT_SECONDARY,33 "axes.labelcolor": TEXT_PRIMARY, "text.color": TEXT_PRIMARY, "axes.grid": False,34 "axes.spines.top": False, "axes.spines.right": False, "legend.frameon": False,35 "figure.dpi": 150, "savefig.bbox": "tight", "savefig.pad_inches": 0.02,36 })373839def heatmap(ax, data, row_labels, col_labels, *, cmap, vmin, vmax, cbar_label: str, title: str | None = None, annotate: bool = False):40 im = ax.imshow(data, cmap=cmap, vmin=vmin, vmax=vmax, aspect="auto", interpolation="nearest")41 ax.set_xticks(range(len(col_labels)))42 ax.set_xticklabels(col_labels, rotation=90 if max(len(str(c)) for c in col_labels) > 3 else 0)43 ax.set_yticks(range(len(row_labels)))44 ax.set_yticklabels(row_labels)45 ax.tick_params(length=0)46 for s in ax.spines.values():47 s.set_visible(False)48 if title:49 ax.set_title(title, loc="left", color=TEXT_PRIMARY)50 if annotate:51 for i in range(data.shape[0]):52 for j in range(data.shape[1]):53 v = data[i, j]54 ax.text(j, i, f"{v:.2f}", ha="center", va="center", fontsize=5.5,55 color="#ffffff" if (v - vmin) / max(vmax - vmin, 1e-9) > 0.6 else TEXT_PRIMARY)56 cb = plt.colorbar(im, ax=ax, fraction=0.046, pad=0.03)57 cb.set_label(cbar_label, color=TEXT_SECONDARY)58 cb.outline.set_visible(False)59 cb.ax.tick_params(length=0, labelsize=6.5, colors=TEXT_SECONDARY)60 return im61