Implementation

This page describes the internal flow of histogram and the order in which validation, bin selection, and plotting additions are applied. Unlike Overview, the goal here is not to repeat the parameter list, but to show how the function actually builds the histogram.

The following snippets are taken from the current src/mespy/plot_utils.py implementation. Private helpers are mentioned only to clarify the flow; complete details are documented in _as_float_vector, _validate_axis_limits, _validate_figsize, _validate_decimals, _style_context, and standard_deviation.

Execution sequence

The implementation follows this sequence:

  1. Converts x into a one-dimensional, finite float64 vector with _as_float_vector("x", x).

  2. Validates decimals as a readable non-negative integer.

  3. Enters _style_context(style) to apply the requested style and make behaviour consistent across notebooks.

  4. If xlim or ylim are provided, it validates them as pairs of finite values.

  5. If ax is None, it creates a new figure with plt.subplots(...); otherwise it reuses the figure associated with the provided axis.

  6. Builds hist_range_tuple if hist_range is provided, and checks that it satisfies xmin < xmax.

  7. If bin_width is provided, it checks that it is positive, enforces bins == "auto", and explicitly builds the bin edges with np.arange(...).

  8. Prepares hist_kwargs, adding bar_color and edgecolor only when they were passed explicitly.

  9. Calls ax.hist(...) to draw the histogram and retrieve bin_edges.

  10. If requested, it uses bin_edges to set x-axis ticks and labels, then applies any requested rotation.

  11. Computes the arithmetic mean and standard deviation of the sample.

  12. If enabled, it adds the mean line and the +- 1 sigma band.

  13. Sets labels, title, grid, legend, axis limits, optional saving, and finally returns (fig, ax).

  14. In IPython/Jupyter environments, _style_context explicitly displays the new figures before leaving the context manager.

Input validation, style, and figure setup

values = _as_float_vector("x", x)
decimals = _validate_decimals(decimals)

with _style_context(style):
    if xlim is not None:
        xlim = _validate_axis_limits(
            xlim,
            name="xlim",
            min_label="xmin",
            max_label="xmax",
        )

    if ylim is not None:
        ylim = _validate_axis_limits(
            ylim,
            name="ylim",
            min_label="ymin",
            max_label="ymax",
        )

    if ax is None:
        import matplotlib.pyplot as plt

        subplots_kwargs: dict = {"constrained_layout": True}
        if figsize is not None:
            subplots_kwargs["figsize"] = _validate_figsize(figsize)
        if dpi is not None:
            subplots_kwargs["dpi"] = dpi
        fig, ax = plt.subplots(**subplots_kwargs)
    else:
        fig = ax.get_figure()

This first block sets the function’s input contract.

  • x is not passed directly to Matplotlib: it is first normalized into values, which must be a one-dimensional, non-empty numeric array without non-finite values.

  • decimals is validated immediately and remains available both for x-axis ticks and for the textual labels of the mean and band.

  • _style_context(style) centralizes three distinct cases: style=None uses the current rcParams, style="mespy" loads the mespy.mplstyle file, and any other string is passed to plt.style.context(...).

  • xlim and ylim are checked before any drawing happens. If they are present, they must be valid pairs of finite numbers.

  • figsize and dpi are passed to plt.subplots(...) only when they are explicitly provided. If the user passes ax, the function does not create a new figure and reuses ax.get_figure().

  • constrained_layout=True is part of the standard behaviour when histogram creates the figure on its own.

  • In IPython/Jupyter notebooks, the same context manager also handles explicit display of new figures to avoid awkward interactions with plt.style.context(...).

Range and bin construction

hist_range_tuple: tuple[float, float] | None = None
if hist_range is not None:
    hist_range_tuple = _validate_axis_limits(
        hist_range,
        name="hist_range",
        min_label="xmin",
        max_label="xmax",
    )
    xmin, xmax = hist_range_tuple
    if xmin >= xmax:
        raise ValueError("Serve xmin < xmax in 'hist_range'")
else:
    xmin = float(np.min(values))
    xmax = float(np.max(values))

if bin_width is not None:
    if bin_width <= 0:
        raise ValueError("'bin_width' deve essere > 0.")

    if bins != "auto":
        raise ValueError(
            "'bins' e 'bin_width' sono mutualmente esclusivi. Usane uno solo"
        )

    start = np.floor(xmin / bin_width) * bin_width
    stop = np.ceil(xmax / bin_width) * bin_width
    bins = np.arange(start, stop + bin_width, bin_width)

Qui la funzione decide il dominio numerico su cui costruire l’istogramma.

  • Se hist_range e presente, viene validato con lo stesso helper dei limiti degli assi, ma con un controllo aggiuntivo: deve valere strettamente xmin < xmax.

  • Se hist_range non e presente, xmin e xmax vengono ricavati direttamente dai dati validati in values.

  • bin_width non modifica solo la larghezza dei bin: cambia proprio la strategia di costruzione, perche i bordi vengono generati esplicitamente con np.arange(...).

  • Il calcolo di start e stop usa floor e ceil, quindi i bin vengono allineati a multipli interi di bin_width che coprano tutto l’intervallo considerato.

Calling ax.hist(...) and formatting the x axis

hist_kwargs: dict = {
    "bins": bins,
    "range": hist_range_tuple if bin_width is None else None,
    "density": False,
    "alpha": hist_alpha,
    "label": label,
}
if bar_color is not None:
    hist_kwargs["color"] = bar_color
if edgecolor is not None:
    hist_kwargs["edgecolor"] = edgecolor

_, bin_edges, _ = ax.hist(values, **hist_kwargs)

fmt = f".{decimals}f"
if show_bin_ticks:
    ax.set_xticks(bin_edges)
    ax.set_xticklabels([f"{edge:{fmt}}" for edge in bin_edges])

if tick_rotation != 0:
    ax.tick_params(axis="x", rotation=tick_rotation)

Questo e il punto in cui il grafico viene davvero disegnato.

  • La funzione usa density=False, quindi l’istogramma e sempre a conteggi, non a densita normalizzata.

  • bar_color ed edgecolor non hanno un default hardcoded nella funzione: vengono inoltrati a Matplotlib solo se l’utente li passa. Altrimenti decide lo stile attivo.

  • ax.hist(...) restituisce bin_edges, che diventano la sorgente ufficiale per i tick dell’asse x quando show_bin_ticks=True.

  • Le etichette dei bordi non vengono calcolate in modo indipendente: vengono formattate direttamente dai bin_edges prodotti da Matplotlib.

  • decimals controlla la rappresentazione testuale dei bordi tramite fmt = f".{decimals}f", ma la stessa formattazione verra riusata anche per media e banda.

  • tick_rotation e indipendente da show_bin_ticks: se diverso da zero, la rotazione viene comunque applicata all’asse x.

Mean, band, and plot finalization

mu = float(np.mean(values))
sigma = standard_deviation(values, ddof=ddof)

if show_mean:
    ax.axvline(
        mu,
        color=mean_color,
        linestyle="--",
        linewidth=1.2,
        label=rf"${mean_symbol} = {mu:{fmt}}$",
    )

if show_band:
    ax.axvspan(
        mu - sigma,
        mu + sigma,
        color=band_color,
        alpha=band_alpha,
        label=rf"$\pm 1\sigma = {sigma:{fmt}}$",
    )

ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel if ylabel is not None else "Conteggi")

title_kwargs: dict = {}
if title_fontsize is not None:
    title_kwargs["fontsize"] = title_fontsize
if title_pad is not None:
    title_kwargs["pad"] = title_pad
ax.set_title(title, **title_kwargs)

if not show_grid:
    ax.grid(False)
elif grid_alpha is not None:
    ax.grid(True, axis="y", alpha=grid_alpha)

if show_legend:
    legend_kwargs: dict = {}
    if legend_fontsize is not None:
        legend_kwargs["fontsize"] = legend_fontsize
    if legend_loc is not None:
        legend_kwargs["loc"] = legend_loc
    ax.legend(**legend_kwargs)

if xlim is not None:
    ax.set_xlim(xlim)

if ylim is not None:
    ax.set_ylim(ylim)

if save_path is not None:
    savefig_kwargs: dict = {"bbox_inches": "tight"}
    if dpi is not None:
        savefig_kwargs["dpi"] = dpi
    fig.savefig(save_path, **savefig_kwargs)

return fig, ax

L’ultima parte aggiunge gli elementi statistici e completa la configurazione dell’asse.

  • La media mu e calcolata con np.mean(values), mentre sigma viene delegata a standard_deviation e quindi dipende da ddof.

  • show_mean e show_band sono indipendenti: si puo mostrare solo la linea, solo la banda, entrambe oppure nessuna delle due.

  • mean_color and band_color always remain explicit per-call overrides; they are not recovered from rcParams.

  • La legenda della media usa mean_symbol, mentre la banda viene etichettata come +- 1 sigma in forma matematica.

  • Se ylabel e None, la funzione usa il default esplicito "Conteggi".

  • Title and legend receive extra keyword arguments only when the user has provided them; in all other cases, the active style configuration remains in effect.

  • The grid follows three distinct branches: show_grid=False turns it off explicitly, show_grid=True with grid_alpha is None leaves it to the active style, and show_grid=True with an explicit grid_alpha applies a grid on the y axis with that opacity.

  • Saving happens at the end with bbox_inches="tight"; dpi is passed to savefig(...) only if the user explicitly requested it.

Important interactions between parameters

Alcune combinazioni di parametri definiscono il comportamento pratico piu importante della funzione.

  • bin_width e bins sono mutualmente esclusivi, salvo il caso default in cui bins == "auto".

  • hist_range ha due ruoli diversi: se bin_width e assente, viene passato a ax.hist(..., range=...); se bin_width e presente, viene usato per determinare xmin e xmax da cui costruire i bordi dei bin.

  • style=None uses the current rcParams, style="mespy" uses the package style, and any other string is passed straight through Matplotlib.

  • bar_color, edgecolor, title_fontsize, title_pad, legend_fontsize, legend_loc, and grid_alpha override the style only when they are not None.

  • show_bin_ticks=True usa i bin_edges restituiti da Matplotlib, non una ricostruzione separata fatta da mespy.

  • ddof influisce solo sul calcolo di sigma e quindi sulla banda +- 1 sigma; non cambia i conteggi dell’istogramma.

  • ylabel=None non lascia l’asse senza etichetta: produce sempre "Conteggi".

  • figsize only has an effect when ax is None; dpi can instead affect both figure creation and saving, but always only when it is explicitly provided.

  • show_grid non attiva una griglia completa sul piano, ma solo una griglia orizzontale legata all’asse y quando la funzione interviene direttamente.

  • save_path salva sempre la figura finale, anche quando ax e stato passato dall’esterno e la figura non e stata creata dentro histogram.

  • In notebooks, _style_context shows only the new figures created inside the block and then closes them, to avoid double automatic display.

Commented example

from mespy import histogram

fig, ax = histogram(
    x,
    bin_width=0.5,
    hist_range=(-2, 3),
    style="mespy",
    show_mean=True,
    show_band=True,
    decimals=2,
)

In questo esempio:

  • hist_range definisce l’intervallo numerico da cui ricavare xmin e xmax

  • bin_width=0.5 forza la costruzione esplicita dei bordi dei bin e quindi esclude l’uso di bins diverso da "auto"

  • style="mespy" explicitly requests the package style for that single call

  • la linea della media e la banda +- 1 sigma vengono aggiunte dopo il disegno dell’istogramma

  • if x-axis ticks are enabled, they are shown using the bin edges formatted with 2 decimal places