Skip to content

Plotting & Visualization

The tit.plotting module provides matplotlib-based visualization functions used by the analysis, optimization, and reporting pipelines. All functions are headless-safe and work in Docker/CI environments without a display server.

graph LR
    ANALYZER([Field Arrays]) --> PLOTS[tit.plotting]
    STATS([Cluster Data]) --> PLOTS
    REPORTING([Report Data]) --> PLOTS
    PLOTS --> PDF([PDF Figures])
    PLOTS --> PNG([PNG / base64 Images])
    style ANALYZER fill:#1a3a5c,stroke:#48a,color:#fff
    style STATS fill:#1a3a5c,stroke:#48a,color:#fff
    style REPORTING fill:#1a3a5c,stroke:#48a,color:#fff
    style PLOTS fill:#2d5a27,stroke:#4a8,color:#fff
    style PDF fill:#1a5c4a,stroke:#4a8,color:#fff
    style PNG fill:#1a5c4a,stroke:#4a8,color:#fff

Focality Histograms

save_histogram

Writes the whole-head field distribution with each bin coloured by its ROI contribution, focality cutoff lines, an optional ROI mean marker, and summary statistics.

from pathlib import Path
from tit.analyzer.visualizer import save_histogram

output_path = save_histogram(
    whole_head_values=whole_head_values,
    roi_values=roi_values,
    output_dir=Path("/data/project/derivatives/ti-toolbox/analysis/sub-001"),
    whole_head_weights=wh_sizes,  # optional per-node areas or per-voxel volumes
    roi_weights=roi_sizes,       # supply both weight arrays or neither
    roi_mean=0.152,
    region_name="M1",
    unit_label="Volume (mm³)",
    n_bins=100,
    dpi=150,
)

Returns the path to histogram.png, or None if either input is empty. This replaces the removed tit.plotting.plot_whole_head_roi_histogram API.

TI Metric Distributions

plot_montage_distributions

Creates three side-by-side histograms showing TImax, TImean, and Focality distributions across montages.

from tit.plotting import plot_montage_distributions

output_path = plot_montage_distributions(
    timax_values=[0.21, 0.34, 0.18],
    timean_values=[0.12, 0.19, 0.09],
    focality_values=[0.85, 0.72, 0.91],
    output_file="/data/output/montage_distributions.png",
    dpi=300,
)

plot_intensity_vs_focality

Scatter plot of intensity versus focality, optionally colored by a composite index.

from tit.plotting import plot_intensity_vs_focality

output_path = plot_intensity_vs_focality(
    intensity=[0.12, 0.19, 0.09, 0.25],
    focality=[0.85, 0.72, 0.91, 0.68],
    composite=[0.48, 0.55, 0.41, 0.60],  # or None
    output_file="/data/output/intensity_vs_focality.png",
    dpi=300,
)

Statistical Plots

plot_permutation_null_distribution

Plots a permutation null distribution histogram with a significance threshold line and markers for observed clusters. Uses seaborn for styling.

from tit.plotting import plot_permutation_null_distribution

output_path = plot_permutation_null_distribution(
    null_distribution=null_dist_array,       # np.ndarray
    threshold=42.0,                          # significance threshold
    observed_clusters=[                      # list of dicts
        {"stat_value": 55.0, "p_value": 0.01},
        {"stat_value": 30.0, "p_value": 0.12},
    ],
    output_file="/data/output/null_distribution.pdf",
    alpha=0.05,
    cluster_stat="size",                     # "size" or "mass"
    dpi=300,
)

plot_cluster_size_mass_correlation

Scatter plot with regression line showing the correlation between cluster size and cluster mass from permutation testing. Annotates Pearson r and p-value.

from tit.plotting import plot_cluster_size_mass_correlation

output_path = plot_cluster_size_mass_correlation(
    cluster_sizes=sizes_array,     # np.ndarray
    cluster_masses=masses_array,   # np.ndarray
    output_file="/data/output/size_mass_correlation.pdf",
    dpi=300,
)

Returns None if fewer than 2 non-zero data points are available.

Helpers

The tit.plotting._common module provides shared utilities used by all plotting functions.

SaveFigOptions

Frozen dataclass controlling figure save parameters.

from tit.plotting import SaveFigOptions

opts = SaveFigOptions(
    dpi=600,                # default: 600
    bbox_inches="tight",    # default: "tight"
    facecolor="white",      # default: "white"
    edgecolor="none",       # default: "none"
)

ensure_headless_matplotlib_backend

Sets the matplotlib backend to "Agg" (or a specified backend) for headless environments. Should be called before importing matplotlib.pyplot. No-ops if a backend is already active.

from tit.plotting import ensure_headless_matplotlib_backend

ensure_headless_matplotlib_backend()          # defaults to "Agg"
ensure_headless_matplotlib_backend("Cairo")   # or specify another backend

savefig_close

Saves a matplotlib Figure to disk and closes it. Uses fig.savefig (not plt.savefig) to avoid global pyplot state issues.

from tit.plotting import savefig_close, SaveFigOptions

path = savefig_close(
    fig,
    "/data/output/figure.pdf",
    fmt="pdf",                           # optional explicit format
    opts=SaveFigOptions(dpi=300),        # optional overrides
)

Lazy Imports

The tit.plotting package uses lazy imports throughout. Importing tit.plotting does not pull in matplotlib, nibabel, seaborn, or scipy. These dependencies are only loaded when a plot function is actually called.

API Reference

Focality

tit.analyzer.visualizer.save_histogram

save_histogram(whole_head_values: ndarray, roi_values: ndarray, output_dir: Path, whole_head_weights: ndarray | None = None, roi_weights: ndarray | None = None, roi_mean: float | None = None, region_name: str | None = None, unit_label: str = 'Area (mm²)', n_bins: int = 100, dpi: int = 150) -> Path | None

Write histogram.png: the whole-head field distribution with the ROI's contribution.

One weighted histogram of the whole grey matter (area- or volume-weighted), each bar coloured by the fraction of it that lies inside the ROI (rainbow, blue -> red, with a colour bar), the ROI mean and the focality cutoffs (50/75/90/95 % of the GM 99.9th percentile) as vertical lines, and a stats box. One PNG at dpi.

Parameters

whole_head_values : numpy.ndarray Field values over the whole grey matter surface / volume. roi_values : numpy.ndarray Field values inside the ROI. output_dir : pathlib.Path Directory the PNG is written to. whole_head_weights, roi_weights : numpy.ndarray or None, optional Per-node areas (mm^2) or per-voxel volumes (mm^3). Both or neither; without them the histogram counts elements. roi_mean : float or None, optional Drawn as a vertical line. region_name : str or None, optional Named in the title. unit_label : str, optional The y-axis label when weights are given. n_bins : int, optional Bins over the whole-GM range (default 100). dpi : int, optional Output resolution (default 150).

Returns

pathlib.Path or None <output_dir>/histogram.png, or None when either input is empty.

Source code in tit/analyzer/visualizer.py
def save_histogram(
    whole_head_values: np.ndarray,
    roi_values: np.ndarray,
    output_dir: Path,
    whole_head_weights: np.ndarray | None = None,
    roi_weights: np.ndarray | None = None,
    roi_mean: float | None = None,
    region_name: str | None = None,
    unit_label: str = "Area (mm\u00b2)",
    n_bins: int = 100,
    dpi: int = 150,
) -> Path | None:
    """Write ``histogram.png``: the whole-head field distribution with the ROI's contribution.

    One weighted histogram of the whole grey matter (area- or volume-weighted),
    each bar coloured by the fraction of it that lies inside the ROI (rainbow,
    blue -> red, with a colour bar), the ROI mean and the focality cutoffs
    (50/75/90/95 % of the GM 99.9th percentile) as vertical lines, and a stats
    box.  One PNG at *dpi*.

    Parameters
    ----------
    whole_head_values : numpy.ndarray
        Field values over the whole grey matter surface / volume.
    roi_values : numpy.ndarray
        Field values inside the ROI.
    output_dir : pathlib.Path
        Directory the PNG is written to.
    whole_head_weights, roi_weights : numpy.ndarray or None, optional
        Per-node areas (mm^2) or per-voxel volumes (mm^3).  Both or neither;
        without them the histogram counts elements.
    roi_mean : float or None, optional
        Drawn as a vertical line.
    region_name : str or None, optional
        Named in the title.
    unit_label : str, optional
        The y-axis label when weights are given.
    n_bins : int, optional
        Bins over the whole-GM range (default 100).
    dpi : int, optional
        Output resolution (default 150).

    Returns
    -------
    pathlib.Path or None
        ``<output_dir>/histogram.png``, or ``None`` when either input is empty.
    """
    from tit.plotting._common import (
        SaveFigOptions,
        ensure_headless_matplotlib_backend,
        savefig_close,
    )

    gm = np.asarray(whole_head_values, dtype=float).ravel()
    roi = np.asarray(roi_values, dtype=float).ravel()
    gm_ok = np.isfinite(gm)
    roi_ok = np.isfinite(roi)
    gm_w = roi_w = None
    if whole_head_weights is not None and roi_weights is not None:
        gm_w = np.broadcast_to(np.asarray(whole_head_weights, float), gm.shape)[gm_ok]
        roi_w = np.broadcast_to(np.asarray(roi_weights, float), roi.shape)[roi_ok]
    gm = gm[gm_ok]
    roi = roi[roi_ok]
    if gm.size == 0 or roi.size == 0:
        logger.warning("Histogram skipped: empty ROI or grey-matter distribution")
        return None

    ensure_headless_matplotlib_backend()
    import matplotlib.pyplot as plt

    weighted = gm_w is not None
    y_label = unit_label if weighted else "Elements"
    gm_hist, edges = np.histogram(gm, bins=n_bins, weights=gm_w)
    roi_hist, _ = np.histogram(roi, bins=edges, weights=roi_w)
    centers = (edges[:-1] + edges[1:]) / 2
    width = float(edges[1] - edges[0])

    # Fraction of each bar that lies in the ROI; the colour scale tops out at the
    # 95th percentile of the non-zero fractions so a few pure-ROI bins do not
    # wash the rest out.
    roi_fraction = np.divide(
        roi_hist, gm_hist, out=np.zeros_like(gm_hist, dtype=float), where=gm_hist > 0
    )
    non_zero = roi_fraction[roi_fraction > 0]
    max_fraction = float(max(np.percentile(non_zero, 95), 0.01)) if non_zero.size else 0.01
    normalized = np.clip(roi_fraction / max_fraction, 0, 1)

    rc = {
        "font.family": "sans-serif",
        "font.sans-serif": ["DejaVu Sans", "Liberation Sans", "sans-serif"],
        "text.usetex": False,
    }
    with plt.rc_context(rc):
        fig, ax = plt.subplots(figsize=(14, 10))
        cmap = plt.get_cmap("rainbow")
        colors = cmap(normalized)
        colors[:, 3] = 0.7
        ax.bar(centers, gm_hist, width=width, color=colors, edgecolor="black")

        # Focality cutoffs: one legend entry each, the threshold and the number of
        # elements at or above it. The "of GM 99.9th pct" is said once, in the
        # legend title, not on every line.
        p999 = float(np.percentile(gm, 99.9))
        for frac, color in zip(
            (0.5, 0.75, 0.9, 0.95), ("red", "darkred", "crimson", "maroon")
        ):
            t = frac * p999
            if edges[0] <= t <= edges[-1]:
                count = int(np.count_nonzero(gm >= t))
                ax.axvline(
                    t,
                    color=color,
                    linestyle="--",
                    linewidth=2,
                    label=f"{int(frac * 100)}%: {t:.2f} V/m, {count:,} elements",
                )
        if roi_mean is not None and edges[0] <= float(roi_mean) <= edges[-1]:
            ax.axvline(
                float(roi_mean),
                color="green",
                linewidth=3,
                label=f"ROI mean: {float(roi_mean):.2f} V/m",
            )
        if ax.get_legend_handles_labels()[0]:
            ax.legend(
                loc="upper left",
                bbox_to_anchor=(0.02, 0.98),
                frameon=True,
                fontsize=11,
                title="Cutoffs (% of GM 99.9th percentile)",
                title_fontsize=11,
            )

        ax.set_xlabel("Field Strength (V/m)", fontsize=14)
        ax.set_ylabel(y_label, fontsize=14)
        ax.tick_params(axis="both", which="major", labelsize=12)
        title = "Whole-Head Field Distribution with ROI Contribution"
        if region_name:
            title += f"\nROI: {region_name}"
        ax.set_title(title, fontsize=14)
        ax.grid(True, alpha=0.3)

        sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(0, 1))
        sm.set_array([])
        cbar = fig.colorbar(sm, ax=ax, shrink=0.7, pad=0.02, aspect=25)
        cbar.set_label(
            f"ROI Contribution Fraction\n(Blue->Green->Red, max={max_fraction:.3f})",
            fontsize=12,
        )

        stats = (
            "Whole Head:\n"
            f"Max: {float(gm.max()):.2f} V/m\n"
            f"Mean: {float(np.average(gm, weights=gm_w)):.2f} V/m\n"
            f"99.9%ile: {p999:.2f} V/m\n"
            f"Elements: {gm.size:,}\n\n"
            "ROI:\n"
            f"Mean: {float(np.average(roi, weights=roi_w)):.2f} V/m\n"
            f"Max: {float(roi.max()):.2f} V/m\n"
            f"Elements: {roi.size:,}"
        )
        ax.text(
            0.98,
            0.98,
            stats,
            transform=ax.transAxes,
            fontsize=11,
            verticalalignment="top",
            horizontalalignment="right",
            bbox=dict(boxstyle="square", facecolor="lightyellow"),
        )

        output_dir = Path(output_dir)
        output_dir.mkdir(parents=True, exist_ok=True)
        out_path = output_dir / "histogram.png"
        fig.tight_layout()
        savefig_close(fig, str(out_path), fmt="png", opts=SaveFigOptions(dpi=dpi))

    logger.info("Saved histogram: %s", out_path)
    return out_path

TI Metrics

tit.plotting.ti_metrics.plot_montage_distributions

plot_montage_distributions(*, timax_values: Sequence[float], timean_values: Sequence[float], focality_values: Sequence[float], output_file: str, dpi: int = 300) -> str | None

Create 3 side-by-side histograms for TImax, TImean and Focality distributions.

Source code in tit/plotting/ti_metrics.py
def plot_montage_distributions(
    *,
    timax_values: Sequence[float],
    timean_values: Sequence[float],
    focality_values: Sequence[float],
    output_file: str,
    dpi: int = 300,
) -> str | None:
    """
    Create 3 side-by-side histograms for TImax, TImean and Focality distributions.
    """
    if (not timax_values) and (not timean_values) and (not focality_values):
        return None

    ensure_headless_matplotlib_backend()
    import matplotlib.pyplot as plt

    fig, axes = plt.subplots(1, 3, figsize=(15, 4))
    configs = [
        (timax_values, axes[0], "TImax (V/m)", "TImax Distribution", "#2196F3"),
        (timean_values, axes[1], "TImean (V/m)", "TImean Distribution", "#4CAF50"),
        (focality_values, axes[2], "Focality", "Focality Distribution", "#FF9800"),
    ]

    for values, ax, xlabel, title, color in configs:
        if values:
            ax.hist(values, bins=20, color=color, edgecolor="black", alpha=0.7)
            ax.set_xlabel(xlabel, fontsize=12)
            ax.set_ylabel("Frequency", fontsize=12)
            ax.set_title(title, fontsize=14, fontweight="bold")
            ax.grid(axis="y", alpha=0.3)

    fig.tight_layout()
    return savefig_close(fig, output_file, opts=SaveFigOptions(dpi=dpi))

tit.plotting.ti_metrics.plot_intensity_vs_focality

plot_intensity_vs_focality(*, intensity: Sequence[float], focality: Sequence[float], composite: Sequence[float] | None, output_file: str, dpi: int = 300) -> str | None

Scatter plot of intensity vs focality, optionally colored by composite index.

Source code in tit/plotting/ti_metrics.py
def plot_intensity_vs_focality(
    *,
    intensity: Sequence[float],
    focality: Sequence[float],
    composite: Sequence[float] | None,
    output_file: str,
    dpi: int = 300,
) -> str | None:
    """
    Scatter plot of intensity vs focality, optionally colored by composite index.
    """
    if (not intensity) or (not focality):
        return None

    ensure_headless_matplotlib_backend()
    import matplotlib.pyplot as plt

    fig, ax = plt.subplots(figsize=(6, 5))
    if composite and any(c is not None for c in composite):
        sc = ax.scatter(
            intensity,
            focality,
            c=composite,
            cmap="viridis",
            s=40,
            edgecolor="black",
            alpha=0.7,
        )
        fig.colorbar(sc, ax=ax).set_label("Composite Index", fontsize=12)
    else:
        ax.scatter(intensity, focality, s=40, edgecolor="black", alpha=0.7)

    ax.set_xlabel("TImean_ROI (V/m)", fontsize=12)
    ax.set_ylabel("Focality", fontsize=12)
    ax.set_title("Intensity vs Focality", fontsize=14, fontweight="bold")
    ax.grid(alpha=0.3)
    fig.tight_layout()
    return savefig_close(fig, output_file, opts=SaveFigOptions(dpi=dpi))

Statistical Plots

tit.plotting.stats.plot_permutation_null_distribution

plot_permutation_null_distribution(null_distribution: ndarray, threshold: float, observed_clusters: Sequence[Mapping[str, float]], output_file: str, *, alpha: float = 0.05, cluster_stat: str = 'size', dpi: int = 300) -> str

Plot permutation null distribution with threshold and observed clusters.

Source code in tit/plotting/stats.py
def plot_permutation_null_distribution(
    null_distribution: np.ndarray,
    threshold: float,
    observed_clusters: Sequence[Mapping[str, float]],
    output_file: str,
    *,
    alpha: float = 0.05,
    cluster_stat: str = "size",
    dpi: int = 300,
) -> str:
    """
    Plot permutation null distribution with threshold and observed clusters.
    """
    ensure_headless_matplotlib_backend()
    import matplotlib.pyplot as plt
    import seaborn as sns

    sns.set_style("whitegrid")
    sns.set_context("notebook", font_scale=1.0)

    fig, ax = plt.subplots(figsize=(10, 6))

    # Labels based on cluster statistic
    if cluster_stat == "size":
        x_label = "Maximum Cluster Size (voxels)"
        title = "Permutation Null Distribution of Maximum Cluster Sizes"
        threshold_label = f"Discrete Threshold (p<{alpha}): {threshold:.1f} voxels"
    else:
        x_label = "Maximum Cluster Mass (sum of t-statistics)"
        title = "Permutation Null Distribution of Maximum Cluster Mass"
        threshold_label = f"Discrete Threshold (p<{alpha}): {threshold:.2f}"

    # Histogram
    if sns is not None:
        sns.histplot(
            null_distribution,
            bins=200,
            alpha=0.7,
            color="gray",
            edgecolor="black",
            label="Null Distribution",
            ax=ax,
        )
    else:
        ax.hist(
            null_distribution,
            bins=200,
            alpha=0.7,
            color="gray",
            edgecolor="black",
            label="Null Distribution",
        )

    # Threshold line
    ax.axvline(
        threshold, color="red", linestyle="--", linewidth=2, label=threshold_label
    )

    # Observed clusters
    sig_plotted = False
    nonsig_plotted = False
    for cluster in observed_clusters:
        stat_value = float(cluster["stat_value"])
        p_value = cluster.get("p_value", None)
        if p_value is not None:
            is_significant = float(p_value) < 0.05
        else:
            is_significant = stat_value > threshold

        color = "green" if is_significant else "orange"
        label = None
        if is_significant and not sig_plotted:
            label = "Significant Clusters (p<0.05)"
            sig_plotted = True
        elif (not is_significant) and (not nonsig_plotted):
            label = "Non-significant Clusters (p≥0.05)"
            nonsig_plotted = True

        ax.axvline(
            stat_value, color=color, linestyle="-", linewidth=2, alpha=0.7, label=label
        )

    ax.set_xlabel(x_label, fontsize=12)
    ax.set_ylabel("Frequency", fontsize=12)
    ax.set_title(title, fontsize=14, fontweight="bold")
    ax.legend(loc="upper right", fontsize=10)
    ax.grid(True, alpha=0.3)
    fig.tight_layout()

    return savefig_close(fig, output_file, fmt="pdf", opts=SaveFigOptions(dpi=dpi))

tit.plotting.stats.plot_cluster_size_mass_correlation

plot_cluster_size_mass_correlation(cluster_sizes: ndarray, cluster_masses: ndarray, output_file: str, *, dpi: int = 300) -> str | None

Plot correlation between cluster size and cluster mass from permutation null distribution.

Source code in tit/plotting/stats.py
def plot_cluster_size_mass_correlation(
    cluster_sizes: np.ndarray,
    cluster_masses: np.ndarray,
    output_file: str,
    *,
    dpi: int = 300,
) -> str | None:
    """
    Plot correlation between cluster size and cluster mass from permutation null distribution.
    """
    from scipy.stats import pearsonr

    ensure_headless_matplotlib_backend()
    import matplotlib.pyplot as plt

    import seaborn as sns

    sns.set_style("whitegrid")
    sns.set_context("notebook", font_scale=1.0)

    # Remove zeros
    mask = (cluster_sizes > 0) & (cluster_masses > 0)
    sizes_nonzero = cluster_sizes[mask]
    masses_nonzero = cluster_masses[mask]
    if len(sizes_nonzero) < 2:
        return None

    r_value, p_value = pearsonr(sizes_nonzero, masses_nonzero)

    fig, ax = plt.subplots(figsize=(10, 8))

    if sns is not None:
        sns.regplot(
            x=sizes_nonzero,
            y=masses_nonzero,
            ax=ax,
            scatter_kws={
                "alpha": 0.6,
                "s": 50,
                "color": "steelblue",
                "edgecolors": "black",
                "linewidths": 0.5,
            },
            line_kws={"color": "red", "linewidth": 2},
        )
    else:
        ax.scatter(
            sizes_nonzero,
            masses_nonzero,
            alpha=0.6,
            s=50,
            c="steelblue",
            edgecolors="black",
            linewidths=0.5,
        )
        z = np.polyfit(sizes_nonzero, masses_nonzero, 1)
        xs = np.linspace(
            float(np.min(sizes_nonzero)), float(np.max(sizes_nonzero)), 100
        )
        ax.plot(xs, z[0] * xs + z[1], color="red", linewidth=2)

    z = np.polyfit(sizes_nonzero, masses_nonzero, 1)
    ax.set_xlabel("Maximum Cluster Size (voxels)", fontsize=12, fontweight="bold")
    ax.set_ylabel(
        "Maximum Cluster Mass (sum of t-statistics)", fontsize=12, fontweight="bold"
    )
    ax.set_title(
        f"Cluster Size vs Cluster Mass Correlation\nPearson r = {r_value:.3f} (p = {p_value:.2e})",
        fontsize=14,
        fontweight="bold",
    )

    textstr = (
        f"n = {len(sizes_nonzero)} permutations\n"
        f"r = {r_value:.3f}\n"
        f"p = {p_value:.2e}\n"
        f"Linear fit: y = {z[0]:.2f}x + {z[1]:.2f}"
    )
    props = dict(boxstyle="round", facecolor="wheat", alpha=0.8)
    ax.text(
        0.05,
        0.95,
        textstr,
        transform=ax.transAxes,
        fontsize=11,
        verticalalignment="top",
        bbox=props,
    )

    ax.grid(True, alpha=0.3)
    fig.tight_layout()

    return savefig_close(fig, output_file, fmt="pdf", opts=SaveFigOptions(dpi=dpi))

Common Utilities

tit.plotting._common.SaveFigOptions dataclass

SaveFigOptions(dpi: int = 600, bbox_inches: str = 'tight', facecolor: str = 'white', edgecolor: str = 'none')

Options forwarded to Figure.savefig.

Attributes

dpi : int Resolution in dots per inch (default 600). bbox_inches : str Bounding-box mode passed to savefig (default "tight"). facecolor : str Background colour of the saved figure (default "white"). edgecolor : str Edge colour of the saved figure (default "none").

tit.plotting._common.ensure_headless_matplotlib_backend

ensure_headless_matplotlib_backend(backend: str = 'Agg') -> None

Best-effort backend setup for headless environments.

Important: - This should be called BEFORE importing matplotlib.pyplot. - If a backend is already active, we do not force-change it.

Source code in tit/plotting/_common.py
def ensure_headless_matplotlib_backend(backend: str = "Agg") -> None:
    """
    Best-effort backend setup for headless environments.

    Important:
    - This should be called BEFORE importing matplotlib.pyplot.
    - If a backend is already active, we do not force-change it.
    """
    import os
    import matplotlib

    os.environ.setdefault("MPLBACKEND", backend)

    # Silence noisy `findfont:` chatter (safe even if pyplot was already imported).
    suppress_matplotlib_findfont_noise()

    current = str(matplotlib.get_backend() or "")
    if current and current.lower() != backend.lower():
        # Backend already selected; don't override.
        return

    matplotlib.use(backend)  # type: ignore[attr-defined]

tit.plotting._common.savefig_close

savefig_close(fig: Any, output_file: str, *, fmt: str | None = None, opts: SaveFigOptions = SaveFigOptions()) -> str

Save a matplotlib Figure and close it.

Uses fig.savefig (not plt.savefig) to avoid relying on global pyplot state.

Source code in tit/plotting/_common.py
def savefig_close(
    fig: Any,
    output_file: str,
    *,
    fmt: str | None = None,
    opts: SaveFigOptions = SaveFigOptions(),
) -> str:
    """
    Save a matplotlib Figure and close it.

    Uses fig.savefig (not plt.savefig) to avoid relying on global pyplot state.
    """
    fig.savefig(
        output_file,
        dpi=opts.dpi,
        bbox_inches=opts.bbox_inches,
        facecolor=opts.facecolor,
        edgecolor=opts.edgecolor,
        format=fmt,
    )
    import matplotlib.pyplot as plt

    plt.close(fig)

    return output_file