Source code for figrecipe._captions._public

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Public caption API — clean, simple, no Markdown noise.

This module provides the figrecipe public caption surface:
- ``add_figure_caption(fig, text, position="bottom")``
- ``add_panel_captions(fig, axes, texts, position="top_left")``

The internal ``ScientificCaption`` class adds Markdown formatting
(e.g. ``**Figure 1.**``) which matplotlib cannot render.  This wrapper
strips it so users get what they typed.
"""

from typing import Any, Dict, List, Optional, Tuple, Union

from ._caption import caption_manager


def _strip_markdown(text: str) -> str:
    """Remove Markdown bold/italic markers from text."""
    return text.replace("**", "").replace("*", "")


class _NullRecord:
    """Stand-in record for a bare matplotlib Figure (no ``fig.record``).

    The band helpers mirror geometry into a record so the recipe round-trips;
    when there is nothing to round-trip (a plain ``Figure``), they harmlessly
    write to this throwaway object instead.
    """

    def __init__(self) -> None:
        self.figsize = None
        self.layout = None
        self.mm_layout = None
        self.figure_texts: list = []
        self.caption = None


[docs] def add_figure_caption( fig: Any, caption: str, *, figure_label: Optional[str] = None, style: str = "scientific", position: str = "bottom", width_ratio: float = 0.9, font_size: Union[str, int] = "small", wrap_width: int = 80, align: str = "justify", pad_mm: float = 2.0, save_to_file: bool = False, file_path: Optional[str] = None, ) -> str: """Add a figure caption. The caption text is stripped of Markdown formatting (bold/italic) so it renders cleanly in matplotlib. The caption is also recorded on ``fig.record.caption`` so it survives save→reproduce round-trip. Parameters ---------- fig : matplotlib.figure.Figure or RecordingFigure The figure to add caption to. caption : str The caption text (Markdown formatting is automatically stripped). figure_label : str, optional Custom figure label (e.g. "Figure 1"). Auto-generated if None. style : str Caption style: "scientific", "nature", "ieee", "apa". position : str Caption position: "bottom" (default) or "top". width_ratio : float Width of caption relative to figure width. font_size : str or int Font size for caption text. wrap_width : int Character width for text wrapping. align : str Caption alignment: "justify" (default), "left", or "center". "justify" spans the full content width with the last line left-aligned. pad_mm : float Gap (mm) between the caption band and the axes / figure edge. save_to_file : bool Whether to also save caption to a separate file. file_path : str, optional Path for caption file. Returns ------- str The rendered caption text (Markdown-stripped). """ # Fail loud on caption content that breaks downstream LaTeX (FR-CAP-001). from ._validate import check_caption_latex_safe check_caption_latex_safe(caption, "the figure caption") # Resolve the underlying matplotlib Figure (RecordingFigure wraps it). mpl_fig = fig._fig if hasattr(fig, "_fig") else fig # Manuscript mode: record the caption (canonical YAML + numbering/sidecar) # but do NOT reserve canvas space or draw it — LaTeX typesets the caption # from the emitted .tex sidecar, so baking it onto the PNG would double- # render. The image stays clean; the drawn copy is a derived view. from ._manuscript_mode import is_manuscript_mode if is_manuscript_mode(): if hasattr(fig, "record"): fig.record.caption = caption caption_manager.add_figure_caption( fig, caption, figure_label=figure_label, style=style, position=position, width_ratio=width_ratio, font_size=font_size, wrap_width=wrap_width, save_to_file=save_to_file, file_path=file_path, render=False, ) return _strip_markdown(caption) # ADDITIVE caption band: the figure GROWS by a caption band and the axes # keep their EXACT mm size/position (identical to the no-caption figure). # The previous implementation reserved space by SHRINKING the axes (a fixed # 0.15 strip + per-axes ``set_position``), which on figrecipe's mm-precise # figures grew the multi-line caption into the axes/xlabel and crowded the # ylabel. We never shrink axes now: ``extend_figure_for_caption`` enlarges # the canvas + rewrites subplotpars so the axes land at the same physical # place, and ``place_caption`` draws the caption into the freed band and # records every drawn fragment for verbatim save->reproduce replay. from ._band import ( caption_band_inches, extend_figure_for_caption, place_caption, resolve_caption_font_pt, wrap_caption_lines, ) clean_caption = _strip_markdown(caption) font_pt = resolve_caption_font_pt(font_size, mpl_fig) lines = wrap_caption_lines(clean_caption, wrap_width) band_in = caption_band_inches(len(lines), font_pt, pad_mm) # Persist the caption text itself for round-trip metadata. if hasattr(fig, "record"): fig.record.caption = caption record = fig.record if hasattr(fig, "record") else _NullRecord() extend_figure_for_caption(mpl_fig, record, band_in, position) # Render the caption text — ONCE. This is the single source of visual truth; # ``caption_manager.add_figure_caption`` below is called with render=False so # it only registers metadata and does NOT draw a second copy. place_caption( mpl_fig, record, lines, font_pt, position, align, band_in, width_ratio, ) # Register with internal manager for numbering/export ONLY. caption_manager.add_figure_caption( fig, caption, figure_label=figure_label, style=style, position=position, width_ratio=width_ratio, font_size=font_size, wrap_width=wrap_width, save_to_file=save_to_file, file_path=file_path, render=False, ) return clean_caption
[docs] def add_panel_captions( fig: Any, axes: Any, panel_captions: Union[List[str], Dict[str, str]], *, main_caption: str = "", figure_label: Optional[str] = None, panel_style: str = "letter_bold", position: str = "top_left", font_size: Union[str, int] = "medium", offset: Tuple[float, float] = (0.02, 0.98), ) -> Dict[str, str]: """Add panel captions (A, B, C, etc.) to subplot panels. Panel labels are rendered with Markdown-stripped text so they display cleanly in matplotlib. Parameters ---------- fig : matplotlib.figure.Figure or RecordingFigure The figure containing panels. axes : Axes, list, or ndarray Axes objects for each panel. panel_captions : list or dict Caption text for each panel. main_caption : str Main figure caption (optional, auto-attached). figure_label : str, optional Figure label for the main caption. panel_style : str Panel label style: "letter_bold", "letter_italic", "number". position : str Panel label position: "top_left", "top_right", etc. font_size : str or int Font size for panel labels. offset : tuple Position offset for panel labels. Returns ------- dict Dictionary mapping panel labels to full caption text. """ return caption_manager.add_panel_captions( fig, axes, panel_captions, main_caption=main_caption, figure_label=figure_label, panel_style=panel_style, position=position, font_size=font_size, offset=offset, )
__all__ = [ "add_figure_caption", "add_panel_captions", ]