36ae18e880
Introduces a new plotstyle package providing consistent, presentation/thesis-ready matplotlib figures: a KIT corporate-design color palette (categorical, sequential, diverging, status), a theme applied via use() (KIT-black bottom spine only, left/bottom ticks, horizontal gridlines, left-aligned titles, LaTeX text in Latin Modern Sans), new_figure() with size presets and figure-level title/params subtitles, a same-size colorbar() helper, style_legend() and panel_label() building blocks, and savefig(). Ships as an optional "plotting" extra so core gallery installs stay lightweight, with a full test suite and a runnable example notebook. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
98 lines
2.8 KiB
Python
98 lines
2.8 KiB
Python
"""Legend and panel-label helpers for consistent multi-panel figures."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import warnings
|
|
|
|
from matplotlib.axes import Axes
|
|
from matplotlib.colors import to_rgba
|
|
|
|
from .colors import INK
|
|
|
|
_PANEL_LOCS = {
|
|
"upper left": (0.02, 0.98, "left", "top"),
|
|
"upper right": (0.98, 0.98, "right", "top"),
|
|
"lower left": (0.02, 0.08, "left", "bottom"),
|
|
"lower right": (0.98, 0.08, "right", "bottom"),
|
|
}
|
|
|
|
|
|
def style_legend(
|
|
ax: Axes,
|
|
loc: str = "outside right upper",
|
|
frameon: bool = False,
|
|
title: str = None,
|
|
**kwargs,
|
|
):
|
|
"""Add a figure-level legend, placed outside the axes by default.
|
|
|
|
Builds on `ax`'s handles/labels (or explicit `handles=`/`labels=` kwargs)
|
|
but attaches the legend to `ax`'s figure via `fig.legend(...)`, so it sits
|
|
outside the plot area rather than overlapping the data.
|
|
|
|
A `title` is strongly encouraged: an untitled legend floating outside the
|
|
axes loses its visual link to what it's describing, e.g.
|
|
`style_legend(ax, title="Series")`. Without one, this emits a warning
|
|
rather than failing — the legend still renders.
|
|
"""
|
|
if title is None:
|
|
warnings.warn(
|
|
"style_legend() called without a title — an outside legend reads better with one, "
|
|
"e.g. style_legend(ax, title='Series').",
|
|
stacklevel=2,
|
|
)
|
|
|
|
fig = ax.get_figure()
|
|
handles = kwargs.pop("handles", None)
|
|
labels = kwargs.pop("labels", None)
|
|
if handles is None or labels is None:
|
|
handles, labels = ax.get_legend_handles_labels()
|
|
|
|
legend = fig.legend(handles, labels, loc=loc, frameon=frameon, title=title, **kwargs)
|
|
if legend.get_title() is not None:
|
|
legend.get_title().set_fontweight("bold")
|
|
return legend
|
|
|
|
|
|
def panel_label(
|
|
ax: Axes,
|
|
label: str,
|
|
loc: str = "lower right",
|
|
fontweight: str = "bold",
|
|
box: bool = True,
|
|
**kwargs,
|
|
):
|
|
"""Add a panel label like "(a)" for multi-panel thesis/paper figures.
|
|
|
|
Defaults to the bottom-right corner, colored to match the axes' xlabel,
|
|
on a light-grey semi-transparent rounded box with a slim solid border.
|
|
Pass `box=False` for bare text with no box.
|
|
"""
|
|
try:
|
|
x, y, ha, va = _PANEL_LOCS[loc]
|
|
except KeyError as exc:
|
|
raise ValueError(f"Unknown panel_label loc {loc!r}. Choose from {sorted(_PANEL_LOCS)}.") from exc
|
|
|
|
kwargs.setdefault("color", ax.xaxis.label.get_color())
|
|
if box:
|
|
kwargs.setdefault(
|
|
"bbox",
|
|
dict(
|
|
boxstyle="round,pad=0.3",
|
|
facecolor=to_rgba(INK["gridline"], alpha=0.8),
|
|
edgecolor=INK["baseline"],
|
|
linewidth=0.8,
|
|
),
|
|
)
|
|
|
|
return ax.text(
|
|
x,
|
|
y,
|
|
f"({label})",
|
|
transform=ax.transAxes,
|
|
ha=ha,
|
|
va=va,
|
|
fontweight=fontweight,
|
|
**kwargs,
|
|
)
|