Skip to content

API Reference¤

This section contains the API reference for the bessaplots library. It provides detailed information about the classes, functions, and modules available in the package.

bessaplots ¤

Bessa Plots - Plottting utilities for scientific papers within the Bessa Group

bessaplots_path = '/home/docs/checkouts/readthedocs.org/user_builds/bessaplots/checkouts/latest/src/bessaplots' module-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

styles_path = '/home/docs/checkouts/readthedocs.org/user_builds/bessaplots/checkouts/latest/src/bessaplots/styles' module-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

stylesheets = {'bessaplots': RcParams({'axes.linewidth': 0.5,'figure.dpi': 600.0,'figure.figsize': [3.3, 2.5],'font.family': ['serif'],'font.serif': ['Times'],'font.size': 8.0,'grid.linewidth': 0.5,'legend.frameon': False,'lines.linewidth': 1.0,'mathtext.fontset': 'dejavuserif','savefig.bbox': None,'savefig.pad_inches': 0.01,'text.usetex': False,'xtick.direction': 'in','xtick.major.size': 3.0,'xtick.major.width': 0.5,'xtick.minor.size': 1.5,'xtick.minor.visible': True,'xtick.minor.width': 0.5,'xtick.top': False,'ytick.direction': 'in','ytick.major.size': 3.0,'ytick.major.width': 0.5,'ytick.minor.size': 1.5,'ytick.minor.visible': True,'ytick.minor.width': 0.5,'ytick.right': False}), 'dissertation': RcParams({'axes.formatter.use_mathtext': False,'axes.formatter.useoffset': False,'axes.labelsize': 10.0,'axes.linewidth': 0.5,'axes.titlesize': 10.0,'figure.dpi': 150.0,'figure.figsize': [5.02, 3.1],'font.family': ['serif'],'font.size': 10.0,'grid.linewidth': 0.5,'legend.fontsize': 9.0,'legend.frameon': False,'lines.linewidth': 1.0,'pgf.preamble': '\\usepackage[utf8]{inputenc}\\usepackage[T1]{fontenc}\\usepackage{microtype}\\usepackage{siunitx}','pgf.rcfonts': False,'pgf.texsystem': 'pdflatex','text.usetex': True,'xtick.direction': 'in','xtick.labelsize': 9.0,'xtick.major.size': 3.0,'xtick.major.width': 0.5,'xtick.minor.size': 1.5,'xtick.minor.visible': True,'xtick.minor.width': 0.5,'xtick.top': False,'ytick.direction': 'in','ytick.labelsize': 9.0,'ytick.major.size': 3.0,'ytick.major.width': 0.5,'ytick.minor.size': 1.5,'ytick.minor.visible': True,'ytick.minor.width': 0.5,'ytick.right': False})} module-attribute ¤

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

FigureGrid ¤

Size and save figures for a grid layout with shared axes.

Computes figure dimensions so that every cell in the grid has an identical axes (plot area) size. When sharey=True the first column is wider (it retains the y-axis label and tick labels) and the remaining columns are narrower. When sharex=True the bottom row is taller (it retains the x-axis label and tick labels) and the upper rows are shorter.

The gutter parameter controls the visible gap between adjacent figures. This space lives outside the figures and should be distributed by \hfill in the LaTeX subfigure environment.

Attributes:

Name Type Description
n_figures int

Number of figures per row (columns in the grid, must be >= 1).

paper_size str

Paper size key from PAPER_SIZES.

span_columns bool

True for full-page width, False for single column.

height float

Axes height as a fraction of axes width (aspect ratio).

sharey bool

Share the y-axis: only the first column keeps its y-axis label and tick labels.

sharex bool

Share the x-axis: only the bottom row keeps its x-axis label and tick labels.

ylabel_margin float

Extra width (inches) reserved for the y-axis label and tick labels. When sharey=True it is reserved on the first column only (the rest have their y-axis stripped); when sharey=False it is reserved on every column. Must be >= xtick_overhang — the first column's left edge relies on ylabel_margin to also cover the first x-tick label's overhang.

xlabel_margin float

Extra height (inches) reserved for the x-axis label and tick labels on the bottom row (only used when sharex=True).

gutter float

Visible gap (inches) between adjacent figures. Distributed by \hfill in LaTeX.

bottom_pad float

Bottom margin (inches) including x-axis tick labels and label.

top_pad float

Top margin (inches) above the axes.

xtick_overhang float

Horizontal padding (inches) reserved inside the figure on either side of the axes so the first and last x-tick labels — which are centered on their ticks and extend past the axes frame — are not cropped by the saved bounding box. Applied to the right edge of every column and the left edge of every non-first column; the first column's left edge is already covered by ylabel_margin.

Source code in src/bessaplots/save_figure.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
@dataclass
class FigureGrid:
    """Size and save figures for a grid layout with shared axes.

    Computes figure dimensions so that every cell in the grid has an
    identical axes (plot area) size.  When ``sharey=True`` the first
    column is wider (it retains the y-axis label and tick labels) and
    the remaining columns are narrower.  When ``sharex=True`` the
    bottom row is taller (it retains the x-axis label and tick labels)
    and the upper rows are shorter.

    The ``gutter`` parameter controls the visible gap between adjacent
    figures.  This space lives *outside* the figures and should be
    distributed by ``\\hfill`` in the LaTeX subfigure environment.

    Attributes
    ----------
    n_figures : int
        Number of figures per row (columns in the grid, must be >= 1).
    paper_size : str
        Paper size key from ``PAPER_SIZES``.
    span_columns : bool
        ``True`` for full-page width, ``False`` for single column.
    height : float
        Axes height as a fraction of axes width (aspect ratio).
    sharey : bool
        Share the y-axis: only the first column keeps its y-axis
        label and tick labels.
    sharex : bool
        Share the x-axis: only the bottom row keeps its x-axis
        label and tick labels.
    ylabel_margin : float
        Extra width (inches) reserved for the y-axis label and tick
        labels.  When ``sharey=True`` it is reserved on the first
        column only (the rest have their y-axis stripped); when
        ``sharey=False`` it is reserved on every column.  Must be
        ``>= xtick_overhang`` — the first column's left edge relies
        on ``ylabel_margin`` to also cover the first x-tick label's
        overhang.
    xlabel_margin : float
        Extra height (inches) reserved for the x-axis label and tick
        labels on the bottom row (only used when ``sharex=True``).
    gutter : float
        Visible gap (inches) between adjacent figures.  Distributed
        by ``\\hfill`` in LaTeX.
    bottom_pad : float
        Bottom margin (inches) including x-axis tick labels and
        label.
    top_pad : float
        Top margin (inches) above the axes.
    xtick_overhang : float
        Horizontal padding (inches) reserved inside the figure on
        either side of the axes so the first and last x-tick labels
        — which are centered on their ticks and extend past the
        axes frame — are not cropped by the saved bounding box.
        Applied to the right edge of every column and the left edge
        of every non-first column; the first column's left edge is
        already covered by ``ylabel_margin``.
    """

    n_figures: int
    paper_size: str = "letter"
    span_columns: bool = False
    height: float = 0.8
    sharey: bool = True
    sharex: bool = False
    ylabel_margin: float = 0.45
    xlabel_margin: float = 0.33
    gutter: float = 0.06
    bottom_pad: float = 0.38
    top_pad: float = 0.05
    xtick_overhang: float = 0.1

    def __post_init__(self) -> None:
        if self.n_figures < 1:
            raise ValueError("n_figures must be >= 1")

        if self.xtick_overhang < 0:
            raise ValueError("xtick_overhang must be >= 0")

        if self.sharey and self.ylabel_margin < self.xtick_overhang:
            raise ValueError(
                f'ylabel_margin ({self.ylabel_margin}") must be '
                f'>= xtick_overhang ({self.xtick_overhang}") '
                f"when sharey=True"
            )

        try:
            sizes = PAPER_SIZES[self.paper_size.lower()]
        except KeyError:
            raise ValueError(
                f"Unknown paper size '{self.paper_size}'. "
                f"Available sizes: {list(PAPER_SIZES.keys())}"
            ) from None

        self.paper_size = self.paper_size.lower()

        col_key = (
            "double_col_width" if self.span_columns else "single_col_width"
        )
        self._total_textwidth: float = sizes[col_key]

        # Gutter space lives outside the figures (LaTeX \hfill)
        n_gutters = self.n_figures - 1
        W = self._total_textwidth - n_gutters * self.gutter

        # -- widths --------------------------------------------------

        # On the first column, ylabel_margin covers both the y-axis
        # label/tick labels *and* the first x-tick label's overhang,
        # so the "extra" width the first column gets over the rest
        # is ylabel_margin - xtick_overhang.  This keeps axes widths
        # identical across columns while preserving the textwidth
        # invariant.  When sharey=False every column keeps its own
        # y-axis, so widths are equal and each column reserves
        # ylabel_margin on its left side (see ``apply``).
        if self.sharey and self.n_figures > 1:
            effective_ylabel = self.ylabel_margin - self.xtick_overhang
            self._rest_width = (W - effective_ylabel) / self.n_figures
            self._first_width = self._rest_width + effective_ylabel
        else:
            self._first_width = W / self.n_figures
            self._rest_width = self._first_width

        if self._rest_width <= 0:
            raise ValueError(
                f'ylabel_margin ({self.ylabel_margin}") is too large '
                f'for {self.n_figures} figures in {W:.2f}" total width'
            )

        # Axes width is measured from a column whose left edge
        # reserves xtick_overhang (sharey=True non-first columns).
        # When sharey=False, every column's left edge reserves
        # ylabel_margin instead, so subtract that extra space.
        if self.sharey:
            self._axes_width = self._rest_width - 2 * (
                _FRAME_PAD + self.xtick_overhang
            )
        else:
            self._axes_width = self._rest_width - (
                2 * _FRAME_PAD + self.ylabel_margin + self.xtick_overhang
            )
        if self._axes_width <= 0:
            raise ValueError(
                "Computed axes width is non-positive; "
                "reduce margins, ylabel_margin, or xtick_overhang"
            )

        # -- heights -------------------------------------------------

        if self.sharex and self.xlabel_margin > self.bottom_pad:
            raise ValueError(
                f'xlabel_margin ({self.xlabel_margin}") exceeds '
                f'bottom_pad ({self.bottom_pad}")'
            )

        axes_height = self._axes_width * self.height
        self._fig_height = self.top_pad + axes_height + self.bottom_pad

        if self.sharex:
            self._inner_row_height = (
                self.top_pad
                + axes_height
                + (self.bottom_pad - self.xlabel_margin)
            )
        else:
            self._inner_row_height = self._fig_height

    # -- read-only properties ----------------------------------------

    @property
    def first_width(self) -> float:
        """Figure width (inches) for the first column."""
        return self._first_width

    @property
    def rest_width(self) -> float:
        """Figure width (inches) for non-first columns."""
        return self._rest_width

    @property
    def axes_width(self) -> float:
        """Axes (plot area) width (inches), identical for all."""
        return self._axes_width

    @property
    def fig_height(self) -> float:
        """Figure height (inches) for the bottom row."""
        return self._fig_height

    @property
    def inner_row_height(self) -> float:
        """Figure height (inches) for non-bottom rows.

        Equal to :attr:`fig_height` when ``sharex=False``.
        """
        return self._inner_row_height

    @property
    def textwidth_fractions(self) -> list[float]:
        r"""Figure widths as fractions of ``\textwidth``.

        Returns a list of length *n_figures*.  When ``sharey=True``
        the first entry is larger than the rest.  The remaining
        fraction is gutter space distributed by ``\hfill`` in LaTeX.
        """
        return [
            (self._first_width if i == 0 else self._rest_width)
            / self._total_textwidth
            for i in range(self.n_figures)
        ]

    # -- public methods ----------------------------------------------

    def apply(
        self,
        fig: plt.Figure,
        first_column: bool = True,
        last_row: bool = True,
    ) -> plt.Figure:
        """Apply grid sizing to *fig*.

        Sets figure dimensions and ``subplots_adjust`` margins so that
        the axes occupies the same absolute rectangle in every figure.
        Strips y-axis decorations when ``sharey=True`` and
        *first_column* is ``False``; strips x-axis decorations when
        ``sharex=True`` and *last_row* is ``False``.

        Parameters
        ----------
        fig : plt.Figure
            The matplotlib figure to resize.
        first_column : bool
            ``True`` for the leftmost figure in a row (keeps y-axis
            when ``sharey`` is enabled).
        last_row : bool
            ``True`` for figures in the bottom row (keeps x-axis
            when ``sharex`` is enabled).

        Returns
        -------
        plt.Figure
            The same figure, mutated in place.
        """
        w = self._first_width if first_column else self._rest_width
        h = self._fig_height if last_row else self._inner_row_height

        fig.set_size_inches(w, h)

        # Left margin: the first column (sharey=True) and every
        # column (sharey=False) keep their y-axis and need
        # ylabel_margin on the left.  Non-first columns with
        # sharey=True have their y-axis stripped and only need
        # xtick_overhang to cover the first x-tick label's overhang.
        if self.sharey and not first_column:
            left_frac = (_FRAME_PAD + self.xtick_overhang) / w
        else:
            left_frac = (self.ylabel_margin + _FRAME_PAD) / w

        # Bottom margin
        if self.sharex and not last_row:
            bottom_frac = (self.bottom_pad - self.xlabel_margin) / h
        else:
            bottom_frac = self.bottom_pad / h

        fig.subplots_adjust(
            left=left_frac,
            right=1.0 - (_FRAME_PAD + self.xtick_overhang) / w,
            bottom=bottom_frac,
            top=1.0 - self.top_pad / h,
        )

        # Strip y-axis on non-first columns
        if self.sharey and not first_column:
            for ax in fig.get_axes():
                ax.set_ylabel("")
                ax.tick_params(axis="y", labelleft=False)

        # Strip x-axis on non-bottom rows
        if self.sharex and not last_row:
            for ax in fig.get_axes():
                ax.set_xlabel("")
                ax.tick_params(axis="x", labelbottom=False)

        return fig

    def save(
        self,
        fig: plt.Figure,
        path: str,
        first_column: bool = True,
        last_row: bool = True,
        format: str = "pgf",
    ) -> None:
        """Apply sizing and save with deterministic bounding box.

        Calls :meth:`apply` then saves with ``bbox_inches`` set to
        the figure's full bbox so the output file has the exact
        computed dimensions.  The explicit bbox is required (rather
        than ``None``) because matplotlib maps
        ``bbox_inches=None`` to the ``savefig.bbox`` rcParam, which
        some styles set to ``"tight"`` — that would crop the output
        to content-dependent dimensions and break grid alignment.

        Parameters
        ----------
        fig : plt.Figure
            Figure to save.
        path : str
            Output path (extension replaced by *format*).
        first_column : bool
            ``True`` for the leftmost figure in a row.
        last_row : bool
            ``True`` for figures in the bottom row.
        format : str
            Output format (default ``"pgf"``).
        """
        self.apply(fig, first_column=first_column, last_row=last_row)
        _path = Path(path).with_suffix(f".{format}")
        fig.savefig(
            _path,
            format=format,
            bbox_inches=fig.bbox_inches,
            pad_inches=0.0,
            transparent=True,
        )
        logger.info(f"Saved figure: {_path}")
axes_width property ¤

Axes (plot area) width (inches), identical for all.

bottom_pad = 0.38 class-attribute ¤

Convert a string or number to a floating point number, if possible.

fig_height property ¤

Figure height (inches) for the bottom row.

first_width property ¤

Figure width (inches) for the first column.

gutter = 0.06 class-attribute ¤

Convert a string or number to a floating point number, if possible.

height = 0.8 class-attribute ¤

Convert a string or number to a floating point number, if possible.

inner_row_height property ¤

Figure height (inches) for non-bottom rows.

Equal to :attr:fig_height when sharex=False.

paper_size = 'letter' class-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

rest_width property ¤

Figure width (inches) for non-first columns.

sharex = False class-attribute ¤

bool(x) -> bool

Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

sharey = True class-attribute ¤

bool(x) -> bool

Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

span_columns = False class-attribute ¤

bool(x) -> bool

Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

textwidth_fractions property ¤

Figure widths as fractions of \textwidth.

Returns a list of length n_figures. When sharey=True the first entry is larger than the rest. The remaining fraction is gutter space distributed by \hfill in LaTeX.

top_pad = 0.05 class-attribute ¤

Convert a string or number to a floating point number, if possible.

xlabel_margin = 0.33 class-attribute ¤

Convert a string or number to a floating point number, if possible.

xtick_overhang = 0.1 class-attribute ¤

Convert a string or number to a floating point number, if possible.

ylabel_margin = 0.45 class-attribute ¤

Convert a string or number to a floating point number, if possible.

apply(fig: Figure, first_column: bool = True, last_row: bool = True) -> Figure ¤

Apply grid sizing to fig.

Sets figure dimensions and subplots_adjust margins so that the axes occupies the same absolute rectangle in every figure. Strips y-axis decorations when sharey=True and first_column is False; strips x-axis decorations when sharex=True and last_row is False.

Parameters:

Name Type Description Default
fig Figure

The matplotlib figure to resize.

required
first_column bool

True for the leftmost figure in a row (keeps y-axis when sharey is enabled).

True
last_row bool

True for figures in the bottom row (keeps x-axis when sharex is enabled).

True

Returns:

Type Description
Figure

The same figure, mutated in place.

Source code in src/bessaplots/save_figure.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
def apply(
    self,
    fig: plt.Figure,
    first_column: bool = True,
    last_row: bool = True,
) -> plt.Figure:
    """Apply grid sizing to *fig*.

    Sets figure dimensions and ``subplots_adjust`` margins so that
    the axes occupies the same absolute rectangle in every figure.
    Strips y-axis decorations when ``sharey=True`` and
    *first_column* is ``False``; strips x-axis decorations when
    ``sharex=True`` and *last_row* is ``False``.

    Parameters
    ----------
    fig : plt.Figure
        The matplotlib figure to resize.
    first_column : bool
        ``True`` for the leftmost figure in a row (keeps y-axis
        when ``sharey`` is enabled).
    last_row : bool
        ``True`` for figures in the bottom row (keeps x-axis
        when ``sharex`` is enabled).

    Returns
    -------
    plt.Figure
        The same figure, mutated in place.
    """
    w = self._first_width if first_column else self._rest_width
    h = self._fig_height if last_row else self._inner_row_height

    fig.set_size_inches(w, h)

    # Left margin: the first column (sharey=True) and every
    # column (sharey=False) keep their y-axis and need
    # ylabel_margin on the left.  Non-first columns with
    # sharey=True have their y-axis stripped and only need
    # xtick_overhang to cover the first x-tick label's overhang.
    if self.sharey and not first_column:
        left_frac = (_FRAME_PAD + self.xtick_overhang) / w
    else:
        left_frac = (self.ylabel_margin + _FRAME_PAD) / w

    # Bottom margin
    if self.sharex and not last_row:
        bottom_frac = (self.bottom_pad - self.xlabel_margin) / h
    else:
        bottom_frac = self.bottom_pad / h

    fig.subplots_adjust(
        left=left_frac,
        right=1.0 - (_FRAME_PAD + self.xtick_overhang) / w,
        bottom=bottom_frac,
        top=1.0 - self.top_pad / h,
    )

    # Strip y-axis on non-first columns
    if self.sharey and not first_column:
        for ax in fig.get_axes():
            ax.set_ylabel("")
            ax.tick_params(axis="y", labelleft=False)

    # Strip x-axis on non-bottom rows
    if self.sharex and not last_row:
        for ax in fig.get_axes():
            ax.set_xlabel("")
            ax.tick_params(axis="x", labelbottom=False)

    return fig
save(fig: Figure, path: str, first_column: bool = True, last_row: bool = True, format: str = 'pgf') -> None ¤

Apply sizing and save with deterministic bounding box.

Calls :meth:apply then saves with bbox_inches set to the figure's full bbox so the output file has the exact computed dimensions. The explicit bbox is required (rather than None) because matplotlib maps bbox_inches=None to the savefig.bbox rcParam, which some styles set to "tight" — that would crop the output to content-dependent dimensions and break grid alignment.

Parameters:

Name Type Description Default
fig Figure

Figure to save.

required
path str

Output path (extension replaced by format).

required
first_column bool

True for the leftmost figure in a row.

True
last_row bool

True for figures in the bottom row.

True
format str

Output format (default "pgf").

'pgf'
Source code in src/bessaplots/save_figure.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
def save(
    self,
    fig: plt.Figure,
    path: str,
    first_column: bool = True,
    last_row: bool = True,
    format: str = "pgf",
) -> None:
    """Apply sizing and save with deterministic bounding box.

    Calls :meth:`apply` then saves with ``bbox_inches`` set to
    the figure's full bbox so the output file has the exact
    computed dimensions.  The explicit bbox is required (rather
    than ``None``) because matplotlib maps
    ``bbox_inches=None`` to the ``savefig.bbox`` rcParam, which
    some styles set to ``"tight"`` — that would crop the output
    to content-dependent dimensions and break grid alignment.

    Parameters
    ----------
    fig : plt.Figure
        Figure to save.
    path : str
        Output path (extension replaced by *format*).
    first_column : bool
        ``True`` for the leftmost figure in a row.
    last_row : bool
        ``True`` for figures in the bottom row.
    format : str
        Output format (default ``"pgf"``).
    """
    self.apply(fig, first_column=first_column, last_row=last_row)
    _path = Path(path).with_suffix(f".{format}")
    fig.savefig(
        _path,
        format=format,
        bbox_inches=fig.bbox_inches,
        pad_inches=0.0,
        transparent=True,
    )
    logger.info(f"Saved figure: {_path}")
FigureSaver ¤
Source code in src/bessaplots/save_figure.py
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
class FigureSaver:
    SUPPORTED_FORMATS = ("pdf", "pgf")
    format: str = "pdf"

    @classmethod
    def set_format(cls, fmt: str) -> None:
        """Set the output format for store/load operations.

        Parameters
        ----------
        fmt : str
            File format. Must be one of ``"pdf"`` or ``"pgf"``.

        Raises
        ------
        ValueError
            If *fmt* is not a supported format.
        """
        fmt = fmt.lower()
        if fmt not in cls.SUPPORTED_FORMATS:
            raise ValueError(
                f"Unsupported format '{fmt}'. "
                f"Supported formats: {cls.SUPPORTED_FORMATS}"
            )
        cls.format = fmt

    @staticmethod
    def load(path: str) -> plt.Figure:
        """Load a figure from a file."""
        _path = Path(path).with_suffix(f".{FigureSaver.format}")
        return plt.imread(_path)

    @staticmethod
    def store(object: plt.Figure, path: str) -> str:
        """Store a figure at its exact ``set_size_inches`` dimensions.

        Passes the figure's full bbox explicitly so a
        ``savefig.bbox : tight`` rcParam in the active style cannot
        crop the output to content-dependent dimensions.
        """
        fmt = FigureSaver.format
        _path = Path(path)
        object.savefig(
            _path.with_suffix(f".{fmt}"),
            format=fmt,
            bbox_inches=object.bbox_inches,
            pad_inches=0.01,
            transparent=True,
            dpi=300,
        )
        return str(_path)
SUPPORTED_FORMATS = ('pdf', 'pgf') class-attribute ¤

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

format = 'pdf' class-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

load(path: str) -> Figure staticmethod ¤

Load a figure from a file.

Source code in src/bessaplots/save_figure.py
121
122
123
124
125
@staticmethod
def load(path: str) -> plt.Figure:
    """Load a figure from a file."""
    _path = Path(path).with_suffix(f".{FigureSaver.format}")
    return plt.imread(_path)
set_format(fmt: str) -> None classmethod ¤

Set the output format for store/load operations.

Parameters:

Name Type Description Default
fmt str

File format. Must be one of "pdf" or "pgf".

required

Raises:

Type Description
ValueError

If fmt is not a supported format.

Source code in src/bessaplots/save_figure.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@classmethod
def set_format(cls, fmt: str) -> None:
    """Set the output format for store/load operations.

    Parameters
    ----------
    fmt : str
        File format. Must be one of ``"pdf"`` or ``"pgf"``.

    Raises
    ------
    ValueError
        If *fmt* is not a supported format.
    """
    fmt = fmt.lower()
    if fmt not in cls.SUPPORTED_FORMATS:
        raise ValueError(
            f"Unsupported format '{fmt}'. "
            f"Supported formats: {cls.SUPPORTED_FORMATS}"
        )
    cls.format = fmt
store(object: Figure, path: str) -> str staticmethod ¤

Store a figure at its exact set_size_inches dimensions.

Passes the figure's full bbox explicitly so a savefig.bbox : tight rcParam in the active style cannot crop the output to content-dependent dimensions.

Source code in src/bessaplots/save_figure.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
@staticmethod
def store(object: plt.Figure, path: str) -> str:
    """Store a figure at its exact ``set_size_inches`` dimensions.

    Passes the figure's full bbox explicitly so a
    ``savefig.bbox : tight`` rcParam in the active style cannot
    crop the output to content-dependent dimensions.
    """
    fmt = FigureSaver.format
    _path = Path(path)
    object.savefig(
        _path.with_suffix(f".{fmt}"),
        format=fmt,
        bbox_inches=object.bbox_inches,
        pad_inches=0.01,
        transparent=True,
        dpi=300,
    )
    return str(_path)
TypstReport ¤

Builder for Typst-based PDF reports.

Accumulates paragraphs and figure grids, then renders them to a .typ source file and optionally compiles to PDF via the typst CLI.

Parameters:

Name Type Description Default
title str

Report title. Empty string omits the title block entirely. The default is "".

''
author str

Author name shown below the title. Ignored when title is empty. The default is "".

''
date str or None

Date string shown below the author. None uses today's ISO date; "" omits the date line. The default is None.

None
paper_size str

One of "letter", "a4", or "b5". The default is "a4".

'a4'

Examples:

>>> from bessaplots import TypstReport
>>> r = TypstReport(title="My Report", author="J. Doe")
>>> r.add_paragraph("Introduction.")
>>> r.add_figures(["fig1.pdf", "fig2.pdf"], columns=2)
>>> r.save("report")  # writes report.typ + compiles report.pdf
Source code in src/bessaplots/typst_report.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
@dataclass
class TypstReport:
    """
    Builder for Typst-based PDF reports.

    Accumulates paragraphs and figure grids, then renders them to a
    ``.typ`` source file and optionally compiles to PDF via the
    ``typst`` CLI.

    Parameters
    ----------
    title : str, optional
        Report title. Empty string omits the title block entirely.
        The default is ``""``.
    author : str, optional
        Author name shown below the title. Ignored when title is empty.
        The default is ``""``.
    date : str or None, optional
        Date string shown below the author.  ``None`` uses today's ISO
        date; ``""`` omits the date line.  The default is ``None``.
    paper_size : str, optional
        One of ``"letter"``, ``"a4"``, or ``"b5"``.
        The default is ``"a4"``.

    Examples
    --------
    >>> from bessaplots import TypstReport
    >>> r = TypstReport(title="My Report", author="J. Doe")
    >>> r.add_paragraph("Introduction.")
    >>> r.add_figures(["fig1.pdf", "fig2.pdf"], columns=2)
    >>> r.save("report")  # writes report.typ + compiles report.pdf
    """

    title: str = ""
    author: str = ""
    date: str | None = None
    paper_size: str = "a4"
    _blocks: list[str] = field(default_factory=list, init=False, repr=False)

    def __post_init__(self) -> None:
        """Validate *paper_size* and default *date* to today's ISO date.

        Raises
        ------
        ValueError
            If *paper_size* is not a recognised size in
            ``PAPER_SIZES``.
        """
        try:
            _ = PAPER_SIZES[self.paper_size.lower()]
        except KeyError:
            raise ValueError(
                f"Unknown paper size '{self.paper_size}'. "
                f"Available sizes: {list(PAPER_SIZES.keys())}"
            ) from None

        self.paper_size = self.paper_size.lower()
        if self.date is None:
            self.date = datetime.date.today().isoformat()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def add_paragraph(self, text: str) -> None:
        """
        Add a paragraph of text to the report.

        Parameters
        ----------
        text : str
            Plain text or Typst markup passed through verbatim.
            Special Typst characters (``#``, ``[``, ``]``) are not
            escaped; advanced users may include Typst markup directly.
        """
        self._blocks.append(text)

    def add_figures(
        self,
        paths: list[str | Path],
        columns: int | list[str] = 1,
        caption: str | None = None,
        gutter: str = "1em",
        subcaptions: list[str] | None = None,
        rows_per_page: int | None = None,
    ) -> None:
        """
        Add a grid of figures to the report.

        Parameters
        ----------
        paths : list of str or Path
            Paths to the figure files (e.g. PDF or PNG).  Each path is
            resolved to an absolute POSIX path so the generated ``.typ``
            file works regardless of where it is written.
        columns : int or list of str, optional
            Number of equal-width columns (``int``) or explicit Typst
            column-width strings such as ``["1fr", "2fr"]``.
            The default is ``1``.
        caption : str or None, optional
            When provided, wraps the grid in a Typst ``figure`` block
            with this caption, enabling automatic figure numbering.
            The default is ``None`` (no caption, no figure numbering).
        gutter : str, optional
            Typst ``column-gutter`` value (e.g. ``"1em"``, ``"5mm"``).
            The default is ``"1em"``.
        subcaptions : list of str or None, optional
            Per-figure captions.  When provided, each image is wrapped
            in its own ``figure()`` block inside the grid.  The list
            length must equal ``len(paths)``.  Can be combined with
            *caption* for an overall grid caption.
            The default is ``None``.
        rows_per_page : int or None, optional
            Maximum number of rows per page.  When provided, the
            figures are split into multiple grids separated by
            ``#pagebreak()``.  The caption (if any) is placed on the
            last chunk only.  The default is ``None`` (no splitting).

        Raises
        ------
        ValueError
            If ``paths`` is empty, ``columns`` is an integer <= 0,
            or ``subcaptions`` length does not match ``paths``.
        """
        if not paths:
            raise ValueError("paths must be non-empty")
        if isinstance(columns, int) and columns <= 0:
            raise ValueError("columns must be a positive integer")
        if isinstance(columns, list) and len(columns) == 0:
            raise ValueError("columns list must be non-empty")
        if subcaptions is not None and len(subcaptions) != len(paths):
            raise ValueError(
                f"subcaptions length ({len(subcaptions)}) must equal "
                f"paths length ({len(paths)})"
            )

        if rows_per_page is not None:
            self._blocks.append(
                _split_figure_blocks(
                    paths,
                    columns,
                    caption,
                    gutter,
                    subcaptions,
                    rows_per_page,
                )
            )
        else:
            self._blocks.append(
                _figure_block(paths, columns, caption, gutter, subcaptions)
            )

    def write(self, path: str | Path) -> Path:
        """
        Write the ``.typ`` source file.

        Parameters
        ----------
        path : str or Path
            Destination path.  The ``.typ`` extension is enforced
            regardless of what suffix is provided.

        Returns
        -------
        Path
            Absolute path to the written ``.typ`` file.
        """
        resolved = Path(path).with_suffix(".typ")
        resolved.write_text(
            _render(
                self._blocks,
                self.title,
                self.author,
                self.date,
                self.paper_size,
            ),
            encoding="utf-8",
        )
        logger.info(f"Wrote Typst source: {resolved}")
        return resolved

    def compile(
        self,
        typ_path: str | Path,
        pdf_path: str | Path | None = None,
    ) -> Path:
        """
        Compile a ``.typ`` file to PDF using the ``typst`` CLI.

        Parameters
        ----------
        typ_path : str or Path
            Path to the ``.typ`` source file.
        pdf_path : str or Path or None, optional
            Output PDF path.  ``None`` replaces the ``.typ`` suffix with
            ``.pdf``.  The default is ``None``.

        Returns
        -------
        Path
            Path to the compiled PDF.

        Raises
        ------
        RuntimeError
            If the ``typst`` executable is not found or compilation
            fails.
        """
        typ = Path(typ_path).with_suffix(".typ")
        pdf = (
            typ.with_suffix(".pdf")
            if pdf_path is None
            else Path(pdf_path).with_suffix(".pdf")
        )
        try:
            result = subprocess.run(
                ["typst", "compile", "--root", "/", str(typ), str(pdf)],
                capture_output=True,
                text=True,
            )
        except FileNotFoundError:
            raise RuntimeError(
                "typst CLI not found. Install via "
                "https://github.com/typst/typst#installation"
            ) from None

        if result.returncode != 0:
            raise RuntimeError(f"typst compile failed:\n{result.stderr}")
        if result.stderr:
            logger.warning(result.stderr)

        logger.info(f"Compiled PDF: {pdf}")
        return pdf

    def save(self, stem: str | Path) -> Path:
        """
        Write the ``.typ`` file and compile it to PDF.

        Parameters
        ----------
        stem : str or Path
            Base name (without extension) for the output files.
            Produces ``<stem>.typ`` and ``<stem>.pdf``.

        Returns
        -------
        Path
            Path to the compiled PDF.
        """
        stem = Path(stem)
        typ_path = self.write(stem.with_suffix(".typ"))
        return self.compile(typ_path)
author = '' class-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

paper_size = 'a4' class-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

title = '' class-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

add_figures(paths: list[str | pathlib.Path], columns: int | list[str] = 1, caption: str | None = None, gutter: str = '1em', subcaptions: list[str] | None = None, rows_per_page: int | None = None) -> None ¤

Add a grid of figures to the report.

Parameters:

Name Type Description Default
paths list of str or Path

Paths to the figure files (e.g. PDF or PNG). Each path is resolved to an absolute POSIX path so the generated .typ file works regardless of where it is written.

required
columns int or list of str

Number of equal-width columns (int) or explicit Typst column-width strings such as ["1fr", "2fr"]. The default is 1.

1
caption str or None

When provided, wraps the grid in a Typst figure block with this caption, enabling automatic figure numbering. The default is None (no caption, no figure numbering).

None
gutter str

Typst column-gutter value (e.g. "1em", "5mm"). The default is "1em".

'1em'
subcaptions list of str or None

Per-figure captions. When provided, each image is wrapped in its own figure() block inside the grid. The list length must equal len(paths). Can be combined with caption for an overall grid caption. The default is None.

None
rows_per_page int or None

Maximum number of rows per page. When provided, the figures are split into multiple grids separated by #pagebreak(). The caption (if any) is placed on the last chunk only. The default is None (no splitting).

None

Raises:

Type Description
ValueError

If paths is empty, columns is an integer <= 0, or subcaptions length does not match paths.

Source code in src/bessaplots/typst_report.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def add_figures(
    self,
    paths: list[str | Path],
    columns: int | list[str] = 1,
    caption: str | None = None,
    gutter: str = "1em",
    subcaptions: list[str] | None = None,
    rows_per_page: int | None = None,
) -> None:
    """
    Add a grid of figures to the report.

    Parameters
    ----------
    paths : list of str or Path
        Paths to the figure files (e.g. PDF or PNG).  Each path is
        resolved to an absolute POSIX path so the generated ``.typ``
        file works regardless of where it is written.
    columns : int or list of str, optional
        Number of equal-width columns (``int``) or explicit Typst
        column-width strings such as ``["1fr", "2fr"]``.
        The default is ``1``.
    caption : str or None, optional
        When provided, wraps the grid in a Typst ``figure`` block
        with this caption, enabling automatic figure numbering.
        The default is ``None`` (no caption, no figure numbering).
    gutter : str, optional
        Typst ``column-gutter`` value (e.g. ``"1em"``, ``"5mm"``).
        The default is ``"1em"``.
    subcaptions : list of str or None, optional
        Per-figure captions.  When provided, each image is wrapped
        in its own ``figure()`` block inside the grid.  The list
        length must equal ``len(paths)``.  Can be combined with
        *caption* for an overall grid caption.
        The default is ``None``.
    rows_per_page : int or None, optional
        Maximum number of rows per page.  When provided, the
        figures are split into multiple grids separated by
        ``#pagebreak()``.  The caption (if any) is placed on the
        last chunk only.  The default is ``None`` (no splitting).

    Raises
    ------
    ValueError
        If ``paths`` is empty, ``columns`` is an integer <= 0,
        or ``subcaptions`` length does not match ``paths``.
    """
    if not paths:
        raise ValueError("paths must be non-empty")
    if isinstance(columns, int) and columns <= 0:
        raise ValueError("columns must be a positive integer")
    if isinstance(columns, list) and len(columns) == 0:
        raise ValueError("columns list must be non-empty")
    if subcaptions is not None and len(subcaptions) != len(paths):
        raise ValueError(
            f"subcaptions length ({len(subcaptions)}) must equal "
            f"paths length ({len(paths)})"
        )

    if rows_per_page is not None:
        self._blocks.append(
            _split_figure_blocks(
                paths,
                columns,
                caption,
                gutter,
                subcaptions,
                rows_per_page,
            )
        )
    else:
        self._blocks.append(
            _figure_block(paths, columns, caption, gutter, subcaptions)
        )
add_paragraph(text: str) -> None ¤

Add a paragraph of text to the report.

Parameters:

Name Type Description Default
text str

Plain text or Typst markup passed through verbatim. Special Typst characters (#, [, ]) are not escaped; advanced users may include Typst markup directly.

required
Source code in src/bessaplots/typst_report.py
318
319
320
321
322
323
324
325
326
327
328
329
def add_paragraph(self, text: str) -> None:
    """
    Add a paragraph of text to the report.

    Parameters
    ----------
    text : str
        Plain text or Typst markup passed through verbatim.
        Special Typst characters (``#``, ``[``, ``]``) are not
        escaped; advanced users may include Typst markup directly.
    """
    self._blocks.append(text)
compile(typ_path: str | pathlib.Path, pdf_path: str | pathlib.Path | None = None) -> Path ¤

Compile a .typ file to PDF using the typst CLI.

Parameters:

Name Type Description Default
typ_path str or Path

Path to the .typ source file.

required
pdf_path str or Path or None

Output PDF path. None replaces the .typ suffix with .pdf. The default is None.

None

Returns:

Type Description
Path

Path to the compiled PDF.

Raises:

Type Description
RuntimeError

If the typst executable is not found or compilation fails.

Source code in src/bessaplots/typst_report.py
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
def compile(
    self,
    typ_path: str | Path,
    pdf_path: str | Path | None = None,
) -> Path:
    """
    Compile a ``.typ`` file to PDF using the ``typst`` CLI.

    Parameters
    ----------
    typ_path : str or Path
        Path to the ``.typ`` source file.
    pdf_path : str or Path or None, optional
        Output PDF path.  ``None`` replaces the ``.typ`` suffix with
        ``.pdf``.  The default is ``None``.

    Returns
    -------
    Path
        Path to the compiled PDF.

    Raises
    ------
    RuntimeError
        If the ``typst`` executable is not found or compilation
        fails.
    """
    typ = Path(typ_path).with_suffix(".typ")
    pdf = (
        typ.with_suffix(".pdf")
        if pdf_path is None
        else Path(pdf_path).with_suffix(".pdf")
    )
    try:
        result = subprocess.run(
            ["typst", "compile", "--root", "/", str(typ), str(pdf)],
            capture_output=True,
            text=True,
        )
    except FileNotFoundError:
        raise RuntimeError(
            "typst CLI not found. Install via "
            "https://github.com/typst/typst#installation"
        ) from None

    if result.returncode != 0:
        raise RuntimeError(f"typst compile failed:\n{result.stderr}")
    if result.stderr:
        logger.warning(result.stderr)

    logger.info(f"Compiled PDF: {pdf}")
    return pdf
save(stem: str | pathlib.Path) -> Path ¤

Write the .typ file and compile it to PDF.

Parameters:

Name Type Description Default
stem str or Path

Base name (without extension) for the output files. Produces <stem>.typ and <stem>.pdf.

required

Returns:

Type Description
Path

Path to the compiled PDF.

Source code in src/bessaplots/typst_report.py
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
def save(self, stem: str | Path) -> Path:
    """
    Write the ``.typ`` file and compile it to PDF.

    Parameters
    ----------
    stem : str or Path
        Base name (without extension) for the output files.
        Produces ``<stem>.typ`` and ``<stem>.pdf``.

    Returns
    -------
    Path
        Path to the compiled PDF.
    """
    stem = Path(stem)
    typ_path = self.write(stem.with_suffix(".typ"))
    return self.compile(typ_path)
write(path: str | pathlib.Path) -> Path ¤

Write the .typ source file.

Parameters:

Name Type Description Default
path str or Path

Destination path. The .typ extension is enforced regardless of what suffix is provided.

required

Returns:

Type Description
Path

Absolute path to the written .typ file.

Source code in src/bessaplots/typst_report.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def write(self, path: str | Path) -> Path:
    """
    Write the ``.typ`` source file.

    Parameters
    ----------
    path : str or Path
        Destination path.  The ``.typ`` extension is enforced
        regardless of what suffix is provided.

    Returns
    -------
    Path
        Absolute path to the written ``.typ`` file.
    """
    resolved = Path(path).with_suffix(".typ")
    resolved.write_text(
        _render(
            self._blocks,
            self.title,
            self.author,
            self.date,
            self.paper_size,
        ),
        encoding="utf-8",
    )
    logger.info(f"Wrote Typst source: {resolved}")
    return resolved
read_styles_in_folders(root_path) ¤

Reads all stylesheets in the given path and its subfolders.

Parameters:

Name Type Description Default
root_path str

Path to the root folder containing the stylesheets and other subfolders with stylesheets.

required

Returns:

Name Type Description
stylesheets dict

Dictionary of stylesheets in the form of {style_name: rcParams}. Should be compatible with matplotlib's plt.style.library dictionary.

Source code in src/bessaplots/styles_discovery.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def read_styles_in_folders(root_path):
    """
    Reads all stylesheets in the given path and its subfolders.

    Parameters
    ----------
    root_path : str
        Path to the root folder containing the stylesheets and other subfolders
        with stylesheets.

    Returns
    -------
    stylesheets : dict
        Dictionary of stylesheets in the form of {style_name: rcParams}.
        Should be compatible with matplotlib's plt.style.library dictionary.
    """
    # matplotlib >= 3.11 promoted `read_style_directory` to the public
    # `matplotlib.style` namespace and dropped the `matplotlib.style.core`
    # submodule; <= 3.10 only exposes it under `.core`. Prefer the public
    # name and fall back to `.core` for older matplotlib.
    read_style_directory = (
        getattr(plt.style, "read_style_directory", None)
        or plt.style.core.read_style_directory
    )

    stylesheets = {}  # plt.style.library is a dictionary
    for folder, _, _ in os.walk(root_path):
        new_stylesheets = read_style_directory(folder)
        stylesheets.update(new_stylesheets)
    return stylesheets
savefig(fig: Figure, path: str, n_side_by_side=1, span_columns=False, height=0.8, paper_size: str = 'letter', format: str = 'pdf') -> None ¤

Saves a figure scaled exactly for an IEEE subfigure slot.

Parameters:

Name Type Description Default
fig Figure

The matplotlib figure object.

required
path str

The storage location (including file name) for the figure. The .pdf suffix is added automatically.

required
n_side_by_side int

How many figures will sit horizontally (1, 2, or 3). The default is 1.

1
span_columns bool

False for single column (3.48"), True for full page (7.14"). The default is False.

False
height float

The height of the figure as a fraction of the width. The default is 0.8.

0.8
paper_size str

The paper size ("letter", "a4", "b5"). The default is "letter".

'letter'
format str

The file format to save (e.g., "pdf", "png"). The default is "pdf".

'pdf'
Source code in src/bessaplots/save_figure.py
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
def savefig(
    fig: plt.Figure,
    path: str,
    n_side_by_side=1,
    span_columns=False,
    height=0.8,
    paper_size: str = "letter",
    format: str = "pdf",
) -> None:
    """
    Saves a figure scaled exactly for an IEEE subfigure slot.

    Parameters
    ----------
    fig : plt.Figure
        The matplotlib figure object.
    path : str
        The storage location (including file name) for the figure.
        The .pdf suffix is added automatically.
    n_side_by_side : int, optional
        How many figures will sit horizontally (1, 2, or 3).
        The default is 1.
    span_columns : bool, optional
        False for single column (3.48"), True for full page (7.14").
        The default is False.
    height : float, optional
        The height of the figure as a fraction of the width.
        The default is 0.8.
    paper_size : str, optional
        The paper size ("letter", "a4", "b5"). The default is "letter".
    format : str, optional
        The file format to save (e.g., "pdf", "png"). The default is "pdf".
    """

    fig = set_size(
        fig,
        n_side_by_side=n_side_by_side,
        span_columns=span_columns,
        height=height,
        paper_size=paper_size,
    )

    _path = Path(path).with_suffix(f".{format}")

    # 5. Save with tight bounding box
    # pad_inches is tiny to ensure the figure maximizes the LaTeX slot
    fig.savefig(
        _path,
        format=format,
        bbox_inches=fig.bbox_inches,
        pad_inches=0.0,
        transparent=True,
    )
    logger.info(f"Saved figure: {_path}")
set_size(fig: Figure, n_side_by_side=1, span_columns=False, height: float = 0.8, paper_size: str = 'letter', subplot_adjust: dict | None = None) -> Figure ¤

Set figure size to fit in IEEE column layout.

Parameters:

Name Type Description Default
fig Figure

The matplotlib figure object.

required
n_side_by_side int

How many figures will sit horizontally (1, 2, or 3). The default is 1.

1
span_columns bool

False for single column (3.48"), True for full page (7.14"). The default is False.

False
height float

The height of the figure as a fraction of the width. The default is 0.8.

0.8
paper_size str

The paper size ("letter", "a4", "b5"). The default is "letter".

'letter'
subplot_adjust dict

Optional dictionary of subplot adjustments to pass to fig.subplots_adjust().

None

Returns:

Name Type Description
fig Figure

The matplotlib figure object with the new size.

Source code in src/bessaplots/save_figure.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def set_size(
    fig: plt.Figure,
    n_side_by_side=1,
    span_columns=False,
    height: float = 0.8,
    paper_size: str = "letter",
    subplot_adjust: dict | None = None,
) -> plt.Figure:
    """
    Set figure size to fit in IEEE column layout.

    Parameters
    ----------
    fig : plt.Figure
        The matplotlib figure object.
    n_side_by_side : int, optional
        How many figures will sit horizontally (1, 2, or 3). The default is 1.
    span_columns : bool, optional
        False for single column (3.48"), True for full page (7.14").
        The default is False.
    height : float, optional
        The height of the figure as a fraction of the width.
        The default is 0.8.
    paper_size : str, optional
        The paper size ("letter", "a4", "b5"). The default is "letter".
    subplot_adjust : dict, optional
        Optional dictionary of subplot adjustments to pass to
        `fig.subplots_adjust()`.

    Returns
    -------
    fig : plt.Figure
        The matplotlib figure object with the new size.
    """

    try:
        sizes = PAPER_SIZES[paper_size.lower()]
    except KeyError:
        raise ValueError(
            f"Unknown paper size '{paper_size}'. "
            f"Available sizes: {list(PAPER_SIZES.keys())}"
        ) from None

    total_width = (
        sizes["double_col_width"]
        if span_columns
        else sizes["single_col_width"]
    )

    # 2. Account for LaTeX margins/gutters between subfigures
    # We use a 2% "safety margin" so LaTeX doesn't force a line break
    available_fraction = 0.98 / n_side_by_side
    target_width = total_width * available_fraction

    # 3. Set Height (Golden ratio is standard, but 0.8 is great for ERTD)
    target_height = target_width * height

    # 4. Apply dimensions and force 8pt font
    fig.set_size_inches(target_width, target_height)
    if subplot_adjust is not None:
        fig.subplots_adjust(**subplot_adjust)

    return fig

FigureGrid¤

bessaplots.FigureGrid ¤

Size and save figures for a grid layout with shared axes.

Computes figure dimensions so that every cell in the grid has an identical axes (plot area) size. When sharey=True the first column is wider (it retains the y-axis label and tick labels) and the remaining columns are narrower. When sharex=True the bottom row is taller (it retains the x-axis label and tick labels) and the upper rows are shorter.

The gutter parameter controls the visible gap between adjacent figures. This space lives outside the figures and should be distributed by \hfill in the LaTeX subfigure environment.

Attributes:

Name Type Description
n_figures int

Number of figures per row (columns in the grid, must be >= 1).

paper_size str

Paper size key from PAPER_SIZES.

span_columns bool

True for full-page width, False for single column.

height float

Axes height as a fraction of axes width (aspect ratio).

sharey bool

Share the y-axis: only the first column keeps its y-axis label and tick labels.

sharex bool

Share the x-axis: only the bottom row keeps its x-axis label and tick labels.

ylabel_margin float

Extra width (inches) reserved for the y-axis label and tick labels. When sharey=True it is reserved on the first column only (the rest have their y-axis stripped); when sharey=False it is reserved on every column. Must be >= xtick_overhang — the first column's left edge relies on ylabel_margin to also cover the first x-tick label's overhang.

xlabel_margin float

Extra height (inches) reserved for the x-axis label and tick labels on the bottom row (only used when sharex=True).

gutter float

Visible gap (inches) between adjacent figures. Distributed by \hfill in LaTeX.

bottom_pad float

Bottom margin (inches) including x-axis tick labels and label.

top_pad float

Top margin (inches) above the axes.

xtick_overhang float

Horizontal padding (inches) reserved inside the figure on either side of the axes so the first and last x-tick labels — which are centered on their ticks and extend past the axes frame — are not cropped by the saved bounding box. Applied to the right edge of every column and the left edge of every non-first column; the first column's left edge is already covered by ylabel_margin.

Source code in src/bessaplots/save_figure.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
@dataclass
class FigureGrid:
    """Size and save figures for a grid layout with shared axes.

    Computes figure dimensions so that every cell in the grid has an
    identical axes (plot area) size.  When ``sharey=True`` the first
    column is wider (it retains the y-axis label and tick labels) and
    the remaining columns are narrower.  When ``sharex=True`` the
    bottom row is taller (it retains the x-axis label and tick labels)
    and the upper rows are shorter.

    The ``gutter`` parameter controls the visible gap between adjacent
    figures.  This space lives *outside* the figures and should be
    distributed by ``\\hfill`` in the LaTeX subfigure environment.

    Attributes
    ----------
    n_figures : int
        Number of figures per row (columns in the grid, must be >= 1).
    paper_size : str
        Paper size key from ``PAPER_SIZES``.
    span_columns : bool
        ``True`` for full-page width, ``False`` for single column.
    height : float
        Axes height as a fraction of axes width (aspect ratio).
    sharey : bool
        Share the y-axis: only the first column keeps its y-axis
        label and tick labels.
    sharex : bool
        Share the x-axis: only the bottom row keeps its x-axis
        label and tick labels.
    ylabel_margin : float
        Extra width (inches) reserved for the y-axis label and tick
        labels.  When ``sharey=True`` it is reserved on the first
        column only (the rest have their y-axis stripped); when
        ``sharey=False`` it is reserved on every column.  Must be
        ``>= xtick_overhang`` — the first column's left edge relies
        on ``ylabel_margin`` to also cover the first x-tick label's
        overhang.
    xlabel_margin : float
        Extra height (inches) reserved for the x-axis label and tick
        labels on the bottom row (only used when ``sharex=True``).
    gutter : float
        Visible gap (inches) between adjacent figures.  Distributed
        by ``\\hfill`` in LaTeX.
    bottom_pad : float
        Bottom margin (inches) including x-axis tick labels and
        label.
    top_pad : float
        Top margin (inches) above the axes.
    xtick_overhang : float
        Horizontal padding (inches) reserved inside the figure on
        either side of the axes so the first and last x-tick labels
        — which are centered on their ticks and extend past the
        axes frame — are not cropped by the saved bounding box.
        Applied to the right edge of every column and the left edge
        of every non-first column; the first column's left edge is
        already covered by ``ylabel_margin``.
    """

    n_figures: int
    paper_size: str = "letter"
    span_columns: bool = False
    height: float = 0.8
    sharey: bool = True
    sharex: bool = False
    ylabel_margin: float = 0.45
    xlabel_margin: float = 0.33
    gutter: float = 0.06
    bottom_pad: float = 0.38
    top_pad: float = 0.05
    xtick_overhang: float = 0.1

    def __post_init__(self) -> None:
        if self.n_figures < 1:
            raise ValueError("n_figures must be >= 1")

        if self.xtick_overhang < 0:
            raise ValueError("xtick_overhang must be >= 0")

        if self.sharey and self.ylabel_margin < self.xtick_overhang:
            raise ValueError(
                f'ylabel_margin ({self.ylabel_margin}") must be '
                f'>= xtick_overhang ({self.xtick_overhang}") '
                f"when sharey=True"
            )

        try:
            sizes = PAPER_SIZES[self.paper_size.lower()]
        except KeyError:
            raise ValueError(
                f"Unknown paper size '{self.paper_size}'. "
                f"Available sizes: {list(PAPER_SIZES.keys())}"
            ) from None

        self.paper_size = self.paper_size.lower()

        col_key = (
            "double_col_width" if self.span_columns else "single_col_width"
        )
        self._total_textwidth: float = sizes[col_key]

        # Gutter space lives outside the figures (LaTeX \hfill)
        n_gutters = self.n_figures - 1
        W = self._total_textwidth - n_gutters * self.gutter

        # -- widths --------------------------------------------------

        # On the first column, ylabel_margin covers both the y-axis
        # label/tick labels *and* the first x-tick label's overhang,
        # so the "extra" width the first column gets over the rest
        # is ylabel_margin - xtick_overhang.  This keeps axes widths
        # identical across columns while preserving the textwidth
        # invariant.  When sharey=False every column keeps its own
        # y-axis, so widths are equal and each column reserves
        # ylabel_margin on its left side (see ``apply``).
        if self.sharey and self.n_figures > 1:
            effective_ylabel = self.ylabel_margin - self.xtick_overhang
            self._rest_width = (W - effective_ylabel) / self.n_figures
            self._first_width = self._rest_width + effective_ylabel
        else:
            self._first_width = W / self.n_figures
            self._rest_width = self._first_width

        if self._rest_width <= 0:
            raise ValueError(
                f'ylabel_margin ({self.ylabel_margin}") is too large '
                f'for {self.n_figures} figures in {W:.2f}" total width'
            )

        # Axes width is measured from a column whose left edge
        # reserves xtick_overhang (sharey=True non-first columns).
        # When sharey=False, every column's left edge reserves
        # ylabel_margin instead, so subtract that extra space.
        if self.sharey:
            self._axes_width = self._rest_width - 2 * (
                _FRAME_PAD + self.xtick_overhang
            )
        else:
            self._axes_width = self._rest_width - (
                2 * _FRAME_PAD + self.ylabel_margin + self.xtick_overhang
            )
        if self._axes_width <= 0:
            raise ValueError(
                "Computed axes width is non-positive; "
                "reduce margins, ylabel_margin, or xtick_overhang"
            )

        # -- heights -------------------------------------------------

        if self.sharex and self.xlabel_margin > self.bottom_pad:
            raise ValueError(
                f'xlabel_margin ({self.xlabel_margin}") exceeds '
                f'bottom_pad ({self.bottom_pad}")'
            )

        axes_height = self._axes_width * self.height
        self._fig_height = self.top_pad + axes_height + self.bottom_pad

        if self.sharex:
            self._inner_row_height = (
                self.top_pad
                + axes_height
                + (self.bottom_pad - self.xlabel_margin)
            )
        else:
            self._inner_row_height = self._fig_height

    # -- read-only properties ----------------------------------------

    @property
    def first_width(self) -> float:
        """Figure width (inches) for the first column."""
        return self._first_width

    @property
    def rest_width(self) -> float:
        """Figure width (inches) for non-first columns."""
        return self._rest_width

    @property
    def axes_width(self) -> float:
        """Axes (plot area) width (inches), identical for all."""
        return self._axes_width

    @property
    def fig_height(self) -> float:
        """Figure height (inches) for the bottom row."""
        return self._fig_height

    @property
    def inner_row_height(self) -> float:
        """Figure height (inches) for non-bottom rows.

        Equal to :attr:`fig_height` when ``sharex=False``.
        """
        return self._inner_row_height

    @property
    def textwidth_fractions(self) -> list[float]:
        r"""Figure widths as fractions of ``\textwidth``.

        Returns a list of length *n_figures*.  When ``sharey=True``
        the first entry is larger than the rest.  The remaining
        fraction is gutter space distributed by ``\hfill`` in LaTeX.
        """
        return [
            (self._first_width if i == 0 else self._rest_width)
            / self._total_textwidth
            for i in range(self.n_figures)
        ]

    # -- public methods ----------------------------------------------

    def apply(
        self,
        fig: plt.Figure,
        first_column: bool = True,
        last_row: bool = True,
    ) -> plt.Figure:
        """Apply grid sizing to *fig*.

        Sets figure dimensions and ``subplots_adjust`` margins so that
        the axes occupies the same absolute rectangle in every figure.
        Strips y-axis decorations when ``sharey=True`` and
        *first_column* is ``False``; strips x-axis decorations when
        ``sharex=True`` and *last_row* is ``False``.

        Parameters
        ----------
        fig : plt.Figure
            The matplotlib figure to resize.
        first_column : bool
            ``True`` for the leftmost figure in a row (keeps y-axis
            when ``sharey`` is enabled).
        last_row : bool
            ``True`` for figures in the bottom row (keeps x-axis
            when ``sharex`` is enabled).

        Returns
        -------
        plt.Figure
            The same figure, mutated in place.
        """
        w = self._first_width if first_column else self._rest_width
        h = self._fig_height if last_row else self._inner_row_height

        fig.set_size_inches(w, h)

        # Left margin: the first column (sharey=True) and every
        # column (sharey=False) keep their y-axis and need
        # ylabel_margin on the left.  Non-first columns with
        # sharey=True have their y-axis stripped and only need
        # xtick_overhang to cover the first x-tick label's overhang.
        if self.sharey and not first_column:
            left_frac = (_FRAME_PAD + self.xtick_overhang) / w
        else:
            left_frac = (self.ylabel_margin + _FRAME_PAD) / w

        # Bottom margin
        if self.sharex and not last_row:
            bottom_frac = (self.bottom_pad - self.xlabel_margin) / h
        else:
            bottom_frac = self.bottom_pad / h

        fig.subplots_adjust(
            left=left_frac,
            right=1.0 - (_FRAME_PAD + self.xtick_overhang) / w,
            bottom=bottom_frac,
            top=1.0 - self.top_pad / h,
        )

        # Strip y-axis on non-first columns
        if self.sharey and not first_column:
            for ax in fig.get_axes():
                ax.set_ylabel("")
                ax.tick_params(axis="y", labelleft=False)

        # Strip x-axis on non-bottom rows
        if self.sharex and not last_row:
            for ax in fig.get_axes():
                ax.set_xlabel("")
                ax.tick_params(axis="x", labelbottom=False)

        return fig

    def save(
        self,
        fig: plt.Figure,
        path: str,
        first_column: bool = True,
        last_row: bool = True,
        format: str = "pgf",
    ) -> None:
        """Apply sizing and save with deterministic bounding box.

        Calls :meth:`apply` then saves with ``bbox_inches`` set to
        the figure's full bbox so the output file has the exact
        computed dimensions.  The explicit bbox is required (rather
        than ``None``) because matplotlib maps
        ``bbox_inches=None`` to the ``savefig.bbox`` rcParam, which
        some styles set to ``"tight"`` — that would crop the output
        to content-dependent dimensions and break grid alignment.

        Parameters
        ----------
        fig : plt.Figure
            Figure to save.
        path : str
            Output path (extension replaced by *format*).
        first_column : bool
            ``True`` for the leftmost figure in a row.
        last_row : bool
            ``True`` for figures in the bottom row.
        format : str
            Output format (default ``"pgf"``).
        """
        self.apply(fig, first_column=first_column, last_row=last_row)
        _path = Path(path).with_suffix(f".{format}")
        fig.savefig(
            _path,
            format=format,
            bbox_inches=fig.bbox_inches,
            pad_inches=0.0,
            transparent=True,
        )
        logger.info(f"Saved figure: {_path}")
axes_width property ¤

Axes (plot area) width (inches), identical for all.

bottom_pad = 0.38 class-attribute ¤

Convert a string or number to a floating point number, if possible.

fig_height property ¤

Figure height (inches) for the bottom row.

first_width property ¤

Figure width (inches) for the first column.

gutter = 0.06 class-attribute ¤

Convert a string or number to a floating point number, if possible.

height = 0.8 class-attribute ¤

Convert a string or number to a floating point number, if possible.

inner_row_height property ¤

Figure height (inches) for non-bottom rows.

Equal to :attr:fig_height when sharex=False.

paper_size = 'letter' class-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

rest_width property ¤

Figure width (inches) for non-first columns.

sharex = False class-attribute ¤

bool(x) -> bool

Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

sharey = True class-attribute ¤

bool(x) -> bool

Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

span_columns = False class-attribute ¤

bool(x) -> bool

Returns True when the argument x is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

textwidth_fractions property ¤

Figure widths as fractions of \textwidth.

Returns a list of length n_figures. When sharey=True the first entry is larger than the rest. The remaining fraction is gutter space distributed by \hfill in LaTeX.

top_pad = 0.05 class-attribute ¤

Convert a string or number to a floating point number, if possible.

xlabel_margin = 0.33 class-attribute ¤

Convert a string or number to a floating point number, if possible.

xtick_overhang = 0.1 class-attribute ¤

Convert a string or number to a floating point number, if possible.

ylabel_margin = 0.45 class-attribute ¤

Convert a string or number to a floating point number, if possible.

apply(fig: Figure, first_column: bool = True, last_row: bool = True) -> Figure ¤

Apply grid sizing to fig.

Sets figure dimensions and subplots_adjust margins so that the axes occupies the same absolute rectangle in every figure. Strips y-axis decorations when sharey=True and first_column is False; strips x-axis decorations when sharex=True and last_row is False.

Parameters:

Name Type Description Default
fig Figure

The matplotlib figure to resize.

required
first_column bool

True for the leftmost figure in a row (keeps y-axis when sharey is enabled).

True
last_row bool

True for figures in the bottom row (keeps x-axis when sharex is enabled).

True

Returns:

Type Description
Figure

The same figure, mutated in place.

Source code in src/bessaplots/save_figure.py
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
def apply(
    self,
    fig: plt.Figure,
    first_column: bool = True,
    last_row: bool = True,
) -> plt.Figure:
    """Apply grid sizing to *fig*.

    Sets figure dimensions and ``subplots_adjust`` margins so that
    the axes occupies the same absolute rectangle in every figure.
    Strips y-axis decorations when ``sharey=True`` and
    *first_column* is ``False``; strips x-axis decorations when
    ``sharex=True`` and *last_row* is ``False``.

    Parameters
    ----------
    fig : plt.Figure
        The matplotlib figure to resize.
    first_column : bool
        ``True`` for the leftmost figure in a row (keeps y-axis
        when ``sharey`` is enabled).
    last_row : bool
        ``True`` for figures in the bottom row (keeps x-axis
        when ``sharex`` is enabled).

    Returns
    -------
    plt.Figure
        The same figure, mutated in place.
    """
    w = self._first_width if first_column else self._rest_width
    h = self._fig_height if last_row else self._inner_row_height

    fig.set_size_inches(w, h)

    # Left margin: the first column (sharey=True) and every
    # column (sharey=False) keep their y-axis and need
    # ylabel_margin on the left.  Non-first columns with
    # sharey=True have their y-axis stripped and only need
    # xtick_overhang to cover the first x-tick label's overhang.
    if self.sharey and not first_column:
        left_frac = (_FRAME_PAD + self.xtick_overhang) / w
    else:
        left_frac = (self.ylabel_margin + _FRAME_PAD) / w

    # Bottom margin
    if self.sharex and not last_row:
        bottom_frac = (self.bottom_pad - self.xlabel_margin) / h
    else:
        bottom_frac = self.bottom_pad / h

    fig.subplots_adjust(
        left=left_frac,
        right=1.0 - (_FRAME_PAD + self.xtick_overhang) / w,
        bottom=bottom_frac,
        top=1.0 - self.top_pad / h,
    )

    # Strip y-axis on non-first columns
    if self.sharey and not first_column:
        for ax in fig.get_axes():
            ax.set_ylabel("")
            ax.tick_params(axis="y", labelleft=False)

    # Strip x-axis on non-bottom rows
    if self.sharex and not last_row:
        for ax in fig.get_axes():
            ax.set_xlabel("")
            ax.tick_params(axis="x", labelbottom=False)

    return fig
save(fig: Figure, path: str, first_column: bool = True, last_row: bool = True, format: str = 'pgf') -> None ¤

Apply sizing and save with deterministic bounding box.

Calls :meth:apply then saves with bbox_inches set to the figure's full bbox so the output file has the exact computed dimensions. The explicit bbox is required (rather than None) because matplotlib maps bbox_inches=None to the savefig.bbox rcParam, which some styles set to "tight" — that would crop the output to content-dependent dimensions and break grid alignment.

Parameters:

Name Type Description Default
fig Figure

Figure to save.

required
path str

Output path (extension replaced by format).

required
first_column bool

True for the leftmost figure in a row.

True
last_row bool

True for figures in the bottom row.

True
format str

Output format (default "pgf").

'pgf'
Source code in src/bessaplots/save_figure.py
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
def save(
    self,
    fig: plt.Figure,
    path: str,
    first_column: bool = True,
    last_row: bool = True,
    format: str = "pgf",
) -> None:
    """Apply sizing and save with deterministic bounding box.

    Calls :meth:`apply` then saves with ``bbox_inches`` set to
    the figure's full bbox so the output file has the exact
    computed dimensions.  The explicit bbox is required (rather
    than ``None``) because matplotlib maps
    ``bbox_inches=None`` to the ``savefig.bbox`` rcParam, which
    some styles set to ``"tight"`` — that would crop the output
    to content-dependent dimensions and break grid alignment.

    Parameters
    ----------
    fig : plt.Figure
        Figure to save.
    path : str
        Output path (extension replaced by *format*).
    first_column : bool
        ``True`` for the leftmost figure in a row.
    last_row : bool
        ``True`` for figures in the bottom row.
    format : str
        Output format (default ``"pgf"``).
    """
    self.apply(fig, first_column=first_column, last_row=last_row)
    _path = Path(path).with_suffix(f".{format}")
    fig.savefig(
        _path,
        format=format,
        bbox_inches=fig.bbox_inches,
        pad_inches=0.0,
        transparent=True,
    )
    logger.info(f"Saved figure: {_path}")

TypstReport¤

bessaplots.TypstReport ¤

Builder for Typst-based PDF reports.

Accumulates paragraphs and figure grids, then renders them to a .typ source file and optionally compiles to PDF via the typst CLI.

Parameters:

Name Type Description Default
title str

Report title. Empty string omits the title block entirely. The default is "".

''
author str

Author name shown below the title. Ignored when title is empty. The default is "".

''
date str or None

Date string shown below the author. None uses today's ISO date; "" omits the date line. The default is None.

None
paper_size str

One of "letter", "a4", or "b5". The default is "a4".

'a4'

Examples:

>>> from bessaplots import TypstReport
>>> r = TypstReport(title="My Report", author="J. Doe")
>>> r.add_paragraph("Introduction.")
>>> r.add_figures(["fig1.pdf", "fig2.pdf"], columns=2)
>>> r.save("report")  # writes report.typ + compiles report.pdf
Source code in src/bessaplots/typst_report.py
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
@dataclass
class TypstReport:
    """
    Builder for Typst-based PDF reports.

    Accumulates paragraphs and figure grids, then renders them to a
    ``.typ`` source file and optionally compiles to PDF via the
    ``typst`` CLI.

    Parameters
    ----------
    title : str, optional
        Report title. Empty string omits the title block entirely.
        The default is ``""``.
    author : str, optional
        Author name shown below the title. Ignored when title is empty.
        The default is ``""``.
    date : str or None, optional
        Date string shown below the author.  ``None`` uses today's ISO
        date; ``""`` omits the date line.  The default is ``None``.
    paper_size : str, optional
        One of ``"letter"``, ``"a4"``, or ``"b5"``.
        The default is ``"a4"``.

    Examples
    --------
    >>> from bessaplots import TypstReport
    >>> r = TypstReport(title="My Report", author="J. Doe")
    >>> r.add_paragraph("Introduction.")
    >>> r.add_figures(["fig1.pdf", "fig2.pdf"], columns=2)
    >>> r.save("report")  # writes report.typ + compiles report.pdf
    """

    title: str = ""
    author: str = ""
    date: str | None = None
    paper_size: str = "a4"
    _blocks: list[str] = field(default_factory=list, init=False, repr=False)

    def __post_init__(self) -> None:
        """Validate *paper_size* and default *date* to today's ISO date.

        Raises
        ------
        ValueError
            If *paper_size* is not a recognised size in
            ``PAPER_SIZES``.
        """
        try:
            _ = PAPER_SIZES[self.paper_size.lower()]
        except KeyError:
            raise ValueError(
                f"Unknown paper size '{self.paper_size}'. "
                f"Available sizes: {list(PAPER_SIZES.keys())}"
            ) from None

        self.paper_size = self.paper_size.lower()
        if self.date is None:
            self.date = datetime.date.today().isoformat()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------

    def add_paragraph(self, text: str) -> None:
        """
        Add a paragraph of text to the report.

        Parameters
        ----------
        text : str
            Plain text or Typst markup passed through verbatim.
            Special Typst characters (``#``, ``[``, ``]``) are not
            escaped; advanced users may include Typst markup directly.
        """
        self._blocks.append(text)

    def add_figures(
        self,
        paths: list[str | Path],
        columns: int | list[str] = 1,
        caption: str | None = None,
        gutter: str = "1em",
        subcaptions: list[str] | None = None,
        rows_per_page: int | None = None,
    ) -> None:
        """
        Add a grid of figures to the report.

        Parameters
        ----------
        paths : list of str or Path
            Paths to the figure files (e.g. PDF or PNG).  Each path is
            resolved to an absolute POSIX path so the generated ``.typ``
            file works regardless of where it is written.
        columns : int or list of str, optional
            Number of equal-width columns (``int``) or explicit Typst
            column-width strings such as ``["1fr", "2fr"]``.
            The default is ``1``.
        caption : str or None, optional
            When provided, wraps the grid in a Typst ``figure`` block
            with this caption, enabling automatic figure numbering.
            The default is ``None`` (no caption, no figure numbering).
        gutter : str, optional
            Typst ``column-gutter`` value (e.g. ``"1em"``, ``"5mm"``).
            The default is ``"1em"``.
        subcaptions : list of str or None, optional
            Per-figure captions.  When provided, each image is wrapped
            in its own ``figure()`` block inside the grid.  The list
            length must equal ``len(paths)``.  Can be combined with
            *caption* for an overall grid caption.
            The default is ``None``.
        rows_per_page : int or None, optional
            Maximum number of rows per page.  When provided, the
            figures are split into multiple grids separated by
            ``#pagebreak()``.  The caption (if any) is placed on the
            last chunk only.  The default is ``None`` (no splitting).

        Raises
        ------
        ValueError
            If ``paths`` is empty, ``columns`` is an integer <= 0,
            or ``subcaptions`` length does not match ``paths``.
        """
        if not paths:
            raise ValueError("paths must be non-empty")
        if isinstance(columns, int) and columns <= 0:
            raise ValueError("columns must be a positive integer")
        if isinstance(columns, list) and len(columns) == 0:
            raise ValueError("columns list must be non-empty")
        if subcaptions is not None and len(subcaptions) != len(paths):
            raise ValueError(
                f"subcaptions length ({len(subcaptions)}) must equal "
                f"paths length ({len(paths)})"
            )

        if rows_per_page is not None:
            self._blocks.append(
                _split_figure_blocks(
                    paths,
                    columns,
                    caption,
                    gutter,
                    subcaptions,
                    rows_per_page,
                )
            )
        else:
            self._blocks.append(
                _figure_block(paths, columns, caption, gutter, subcaptions)
            )

    def write(self, path: str | Path) -> Path:
        """
        Write the ``.typ`` source file.

        Parameters
        ----------
        path : str or Path
            Destination path.  The ``.typ`` extension is enforced
            regardless of what suffix is provided.

        Returns
        -------
        Path
            Absolute path to the written ``.typ`` file.
        """
        resolved = Path(path).with_suffix(".typ")
        resolved.write_text(
            _render(
                self._blocks,
                self.title,
                self.author,
                self.date,
                self.paper_size,
            ),
            encoding="utf-8",
        )
        logger.info(f"Wrote Typst source: {resolved}")
        return resolved

    def compile(
        self,
        typ_path: str | Path,
        pdf_path: str | Path | None = None,
    ) -> Path:
        """
        Compile a ``.typ`` file to PDF using the ``typst`` CLI.

        Parameters
        ----------
        typ_path : str or Path
            Path to the ``.typ`` source file.
        pdf_path : str or Path or None, optional
            Output PDF path.  ``None`` replaces the ``.typ`` suffix with
            ``.pdf``.  The default is ``None``.

        Returns
        -------
        Path
            Path to the compiled PDF.

        Raises
        ------
        RuntimeError
            If the ``typst`` executable is not found or compilation
            fails.
        """
        typ = Path(typ_path).with_suffix(".typ")
        pdf = (
            typ.with_suffix(".pdf")
            if pdf_path is None
            else Path(pdf_path).with_suffix(".pdf")
        )
        try:
            result = subprocess.run(
                ["typst", "compile", "--root", "/", str(typ), str(pdf)],
                capture_output=True,
                text=True,
            )
        except FileNotFoundError:
            raise RuntimeError(
                "typst CLI not found. Install via "
                "https://github.com/typst/typst#installation"
            ) from None

        if result.returncode != 0:
            raise RuntimeError(f"typst compile failed:\n{result.stderr}")
        if result.stderr:
            logger.warning(result.stderr)

        logger.info(f"Compiled PDF: {pdf}")
        return pdf

    def save(self, stem: str | Path) -> Path:
        """
        Write the ``.typ`` file and compile it to PDF.

        Parameters
        ----------
        stem : str or Path
            Base name (without extension) for the output files.
            Produces ``<stem>.typ`` and ``<stem>.pdf``.

        Returns
        -------
        Path
            Path to the compiled PDF.
        """
        stem = Path(stem)
        typ_path = self.write(stem.with_suffix(".typ"))
        return self.compile(typ_path)
author = '' class-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

paper_size = 'a4' class-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

title = '' class-attribute ¤

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.

add_figures(paths: list[str | pathlib.Path], columns: int | list[str] = 1, caption: str | None = None, gutter: str = '1em', subcaptions: list[str] | None = None, rows_per_page: int | None = None) -> None ¤

Add a grid of figures to the report.

Parameters:

Name Type Description Default
paths list of str or Path

Paths to the figure files (e.g. PDF or PNG). Each path is resolved to an absolute POSIX path so the generated .typ file works regardless of where it is written.

required
columns int or list of str

Number of equal-width columns (int) or explicit Typst column-width strings such as ["1fr", "2fr"]. The default is 1.

1
caption str or None

When provided, wraps the grid in a Typst figure block with this caption, enabling automatic figure numbering. The default is None (no caption, no figure numbering).

None
gutter str

Typst column-gutter value (e.g. "1em", "5mm"). The default is "1em".

'1em'
subcaptions list of str or None

Per-figure captions. When provided, each image is wrapped in its own figure() block inside the grid. The list length must equal len(paths). Can be combined with caption for an overall grid caption. The default is None.

None
rows_per_page int or None

Maximum number of rows per page. When provided, the figures are split into multiple grids separated by #pagebreak(). The caption (if any) is placed on the last chunk only. The default is None (no splitting).

None

Raises:

Type Description
ValueError

If paths is empty, columns is an integer <= 0, or subcaptions length does not match paths.

Source code in src/bessaplots/typst_report.py
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
def add_figures(
    self,
    paths: list[str | Path],
    columns: int | list[str] = 1,
    caption: str | None = None,
    gutter: str = "1em",
    subcaptions: list[str] | None = None,
    rows_per_page: int | None = None,
) -> None:
    """
    Add a grid of figures to the report.

    Parameters
    ----------
    paths : list of str or Path
        Paths to the figure files (e.g. PDF or PNG).  Each path is
        resolved to an absolute POSIX path so the generated ``.typ``
        file works regardless of where it is written.
    columns : int or list of str, optional
        Number of equal-width columns (``int``) or explicit Typst
        column-width strings such as ``["1fr", "2fr"]``.
        The default is ``1``.
    caption : str or None, optional
        When provided, wraps the grid in a Typst ``figure`` block
        with this caption, enabling automatic figure numbering.
        The default is ``None`` (no caption, no figure numbering).
    gutter : str, optional
        Typst ``column-gutter`` value (e.g. ``"1em"``, ``"5mm"``).
        The default is ``"1em"``.
    subcaptions : list of str or None, optional
        Per-figure captions.  When provided, each image is wrapped
        in its own ``figure()`` block inside the grid.  The list
        length must equal ``len(paths)``.  Can be combined with
        *caption* for an overall grid caption.
        The default is ``None``.
    rows_per_page : int or None, optional
        Maximum number of rows per page.  When provided, the
        figures are split into multiple grids separated by
        ``#pagebreak()``.  The caption (if any) is placed on the
        last chunk only.  The default is ``None`` (no splitting).

    Raises
    ------
    ValueError
        If ``paths`` is empty, ``columns`` is an integer <= 0,
        or ``subcaptions`` length does not match ``paths``.
    """
    if not paths:
        raise ValueError("paths must be non-empty")
    if isinstance(columns, int) and columns <= 0:
        raise ValueError("columns must be a positive integer")
    if isinstance(columns, list) and len(columns) == 0:
        raise ValueError("columns list must be non-empty")
    if subcaptions is not None and len(subcaptions) != len(paths):
        raise ValueError(
            f"subcaptions length ({len(subcaptions)}) must equal "
            f"paths length ({len(paths)})"
        )

    if rows_per_page is not None:
        self._blocks.append(
            _split_figure_blocks(
                paths,
                columns,
                caption,
                gutter,
                subcaptions,
                rows_per_page,
            )
        )
    else:
        self._blocks.append(
            _figure_block(paths, columns, caption, gutter, subcaptions)
        )
add_paragraph(text: str) -> None ¤

Add a paragraph of text to the report.

Parameters:

Name Type Description Default
text str

Plain text or Typst markup passed through verbatim. Special Typst characters (#, [, ]) are not escaped; advanced users may include Typst markup directly.

required
Source code in src/bessaplots/typst_report.py
318
319
320
321
322
323
324
325
326
327
328
329
def add_paragraph(self, text: str) -> None:
    """
    Add a paragraph of text to the report.

    Parameters
    ----------
    text : str
        Plain text or Typst markup passed through verbatim.
        Special Typst characters (``#``, ``[``, ``]``) are not
        escaped; advanced users may include Typst markup directly.
    """
    self._blocks.append(text)
compile(typ_path: str | pathlib.Path, pdf_path: str | pathlib.Path | None = None) -> Path ¤

Compile a .typ file to PDF using the typst CLI.

Parameters:

Name Type Description Default
typ_path str or Path

Path to the .typ source file.

required
pdf_path str or Path or None

Output PDF path. None replaces the .typ suffix with .pdf. The default is None.

None

Returns:

Type Description
Path

Path to the compiled PDF.

Raises:

Type Description
RuntimeError

If the typst executable is not found or compilation fails.

Source code in src/bessaplots/typst_report.py
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
def compile(
    self,
    typ_path: str | Path,
    pdf_path: str | Path | None = None,
) -> Path:
    """
    Compile a ``.typ`` file to PDF using the ``typst`` CLI.

    Parameters
    ----------
    typ_path : str or Path
        Path to the ``.typ`` source file.
    pdf_path : str or Path or None, optional
        Output PDF path.  ``None`` replaces the ``.typ`` suffix with
        ``.pdf``.  The default is ``None``.

    Returns
    -------
    Path
        Path to the compiled PDF.

    Raises
    ------
    RuntimeError
        If the ``typst`` executable is not found or compilation
        fails.
    """
    typ = Path(typ_path).with_suffix(".typ")
    pdf = (
        typ.with_suffix(".pdf")
        if pdf_path is None
        else Path(pdf_path).with_suffix(".pdf")
    )
    try:
        result = subprocess.run(
            ["typst", "compile", "--root", "/", str(typ), str(pdf)],
            capture_output=True,
            text=True,
        )
    except FileNotFoundError:
        raise RuntimeError(
            "typst CLI not found. Install via "
            "https://github.com/typst/typst#installation"
        ) from None

    if result.returncode != 0:
        raise RuntimeError(f"typst compile failed:\n{result.stderr}")
    if result.stderr:
        logger.warning(result.stderr)

    logger.info(f"Compiled PDF: {pdf}")
    return pdf
save(stem: str | pathlib.Path) -> Path ¤

Write the .typ file and compile it to PDF.

Parameters:

Name Type Description Default
stem str or Path

Base name (without extension) for the output files. Produces <stem>.typ and <stem>.pdf.

required

Returns:

Type Description
Path

Path to the compiled PDF.

Source code in src/bessaplots/typst_report.py
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
def save(self, stem: str | Path) -> Path:
    """
    Write the ``.typ`` file and compile it to PDF.

    Parameters
    ----------
    stem : str or Path
        Base name (without extension) for the output files.
        Produces ``<stem>.typ`` and ``<stem>.pdf``.

    Returns
    -------
    Path
        Path to the compiled PDF.
    """
    stem = Path(stem)
    typ_path = self.write(stem.with_suffix(".typ"))
    return self.compile(typ_path)
write(path: str | pathlib.Path) -> Path ¤

Write the .typ source file.

Parameters:

Name Type Description Default
path str or Path

Destination path. The .typ extension is enforced regardless of what suffix is provided.

required

Returns:

Type Description
Path

Absolute path to the written .typ file.

Source code in src/bessaplots/typst_report.py
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def write(self, path: str | Path) -> Path:
    """
    Write the ``.typ`` source file.

    Parameters
    ----------
    path : str or Path
        Destination path.  The ``.typ`` extension is enforced
        regardless of what suffix is provided.

    Returns
    -------
    Path
        Absolute path to the written ``.typ`` file.
    """
    resolved = Path(path).with_suffix(".typ")
    resolved.write_text(
        _render(
            self._blocks,
            self.title,
            self.author,
            self.date,
            self.paper_size,
        ),
        encoding="utf-8",
    )
    logger.info(f"Wrote Typst source: {resolved}")
    return resolved