Files
ETPlot/plotstyle/annotations.py
T
lars 3b9d1ef1a8 Rewrite CI to lint/typecheck/audit/test only; add HPC deployment path
- Replace the Docker build/publish CI stages with ruff (lint + format
  check), ty (type check), pip-audit, and pytest run directly against
  python:3.11-slim; Docker remains for manual/server deployment only.
- Swap black/pylint/mypy for ruff/ty across pyproject.toml, and fix
  every resulting lint, format, and type diagnostic in gallery/ and
  plotstyle/.
- Fix tests broken/stale from before the package restructuring: wrong
  `utils.*` import paths, mock patch targets pointed at the wrong
  module, and PDF-conversion tests still assuming ImageMagick instead
  of the current PyMuPDF-first path. Drop test_container.py (obsolete
  Docker-container smoke tests, fully superseded elsewhere).
- Add a plain-venv + systemd --user timer deployment path
  (deploy/systemd/) for HPC login nodes without a Docker daemon, where
  public_html is already served by existing infrastructure.
- Document both in CLAUDE.md, including running CI's checks locally
  before committing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 15:29:55 +02:00

102 lines
3.1 KiB
Python

"""Legend and panel-label helpers for consistent multi-panel figures."""
from __future__ import annotations
import warnings
from typing import Optional
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: Optional[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()
assert fig is not None, "ax must be attached to a 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()
# matplotlib-stubs' `loc` Literal doesn't include the "outside ..." compound
# locations matplotlib actually supports at runtime (e.g. "outside right upper").
legend = fig.legend(handles, labels, loc=loc, frameon=frameon, title=title, **kwargs) # ty: ignore[invalid-argument-type]
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,
)