Skip to content

Bar Plot

Bar and grouped bar plots from a DataFrame or Series.

Grouped bars come from a list of value_col columns; x_col only sets the x-axis categories. A Series is plotted against its index — x_col cannot be combined with one. Use index.plot for relative performance against a baseline and line.plot for sequential x-values.

plot(df, value_col=None, x_col=None, title=None, eyebrow=None, subtitle=None, x_label=None, y_label=None, legend_title=None, legend_labels=None, ax=None, source_text=None, move_legend_outside=False, orientation='vertical', sort_order=None, data_label_format=None, use_hatch=False, num_digits=3, **kwargs)

Create a bar plot (vertical or horizontal) from a DataFrame or Series.

A list of value_col columns creates grouped bars; x_col is the x-axis category column. When df is a Series, it is plotted against its index, value_col may be None, and x_col must be None. For a DataFrame, value_col=None becomes ["Value"] and raises KeyError unless a column literally named "Value" exists. A UserWarning is emitted when a percentage data_label_format is combined with negative values.

Parameters:

Name Type Description Default
df DataFrame | Series

The input DataFrame or Series containing the data to be plotted.

required
value_col str | list[str]

The column(s) containing values to plot as bars. Multiple columns create grouped bars.

None
x_col str

The x-axis category column (e.g., products, regions).

None
title str

The title of the plot.

None
eyebrow str

Small uppercase label rendered above the title.

None
subtitle str

Supporting copy rendered below the title.

None
x_label str

X-axis label; omitted means no label.

None
y_label str

Y-axis label; omitted means no label.

None
legend_title str

The title for the legend.

None
legend_labels list[str]

Override the legend labels read from the plotted series (e.g. swap column ids for human-readable names). Length must match the number of legend entries or a ValueError is raised.

None
ax Axes

The Matplotlib Axes object to plot on.

None
source_text str

Text to be displayed as a source at the bottom of the plot.

None
move_legend_outside bool

Whether to move the legend outside the plot area.

False
orientation Literal['horizontal', 'h', 'vertical', 'v']

Orientation of the bars. Accepts short or long forms, case-insensitive.

'vertical'
sort_order Literal['asc', 'ascending', 'desc', 'descending'] | None

Sorting order for the bars, by the first value_col only. Accepts short or long forms, case-insensitive.

None
data_label_format Literal['absolute', 'percentage_by_bar_group', 'percentage_by_series'] | None

Format for the data labels. "absolute" shows the raw value of each bar; "percentage_by_bar_group" shows each bar's share of its x-axis group; "percentage_by_series" shows each bar's share of its value-column series.

None
use_hatch bool

Whether to apply hatch patterns to the bars.

False
num_digits int

The number of digits to display in the data labels.

3
**kwargs Any

Additional keyword arguments for pandas' plot function. width defaults to 0.8, color to a per-column palette, and legend to True for grouped bars only.

{}

Returns:

Name Type Description
SubplotBase SubplotBase

The Matplotlib Axes object with the generated plot.

Raises:

Type Description
ValueError

If df is empty.

ValueError

If df is a Series and x_col is provided.

ValueError

If orientation, sort_order, or data_label_format is not an accepted value.

ValueError

If legend_labels is provided and its length does not match the number of legend entries.

Source code in openretailscience/plots/bar.py
def plot(  # noqa: PLR0913, PLR0917
    df: pd.DataFrame | pd.Series,
    value_col: str | list[str] | None = None,
    x_col: str | None = None,
    title: str | None = None,
    eyebrow: str | None = None,
    subtitle: str | None = None,
    x_label: str | None = None,
    y_label: str | None = None,
    legend_title: str | None = None,
    legend_labels: list[str] | None = None,
    ax: Axes | None = None,
    source_text: str | None = None,
    move_legend_outside: bool = False,
    orientation: Literal["horizontal", "h", "vertical", "v"] = "vertical",
    sort_order: Literal["asc", "ascending", "desc", "descending"] | None = None,
    data_label_format: Literal["absolute", "percentage_by_bar_group", "percentage_by_series"] | None = None,
    use_hatch: bool = False,
    num_digits: int = 3,
    **kwargs: Any,  # noqa: ANN401
) -> SubplotBase:
    """Create a bar plot (vertical or horizontal) from a DataFrame or Series.

    A list of ``value_col`` columns creates grouped bars; ``x_col`` is the x-axis category
    column. When ``df`` is a Series, it is plotted against its index, ``value_col`` may be None,
    and ``x_col`` must be None. For a DataFrame, ``value_col=None`` becomes ``["Value"]`` and
    raises ``KeyError`` unless a column literally named "Value" exists. A ``UserWarning`` is
    emitted when a percentage ``data_label_format`` is combined with negative values.

    Args:
        df (pd.DataFrame | pd.Series): The input DataFrame or Series containing the data to be plotted.
        value_col (str | list[str], optional): The column(s) containing values to plot as bars. Multiple
            columns create grouped bars.
        x_col (str, optional): The x-axis category column (e.g., products, regions).
        title (str, optional): The title of the plot.
        eyebrow (str, optional): Small uppercase label rendered above the title.
        subtitle (str, optional): Supporting copy rendered below the title.
        x_label (str, optional): X-axis label; omitted means no label.
        y_label (str, optional): Y-axis label; omitted means no label.
        legend_title (str, optional): The title for the legend.
        legend_labels (list[str], optional): Override the legend labels read from the plotted series
            (e.g. swap column ids for human-readable names). Length must match the number of legend
            entries or a ``ValueError`` is raised.
        ax (Axes, optional): The Matplotlib Axes object to plot on.
        source_text (str, optional): Text to be displayed as a source at the bottom of the plot.
        move_legend_outside (bool, optional): Whether to move the legend outside the plot area.
        orientation (Literal["horizontal", "h", "vertical", "v"], optional): Orientation of the bars.
            Accepts short or long forms, case-insensitive.
        sort_order (Literal["asc", "ascending", "desc", "descending"] | None, optional): Sorting order
            for the bars, by the first ``value_col`` only. Accepts short or long forms, case-insensitive.
        data_label_format (Literal["absolute", "percentage_by_bar_group", "percentage_by_series"] | None, optional):
            Format for the data labels. "absolute" shows the raw value of each bar;
            "percentage_by_bar_group" shows each bar's share of its x-axis group;
            "percentage_by_series" shows each bar's share of its value-column series.
        use_hatch (bool, optional): Whether to apply hatch patterns to the bars.
        num_digits (int, optional): The number of digits to display in the data labels.
        **kwargs: Additional keyword arguments for pandas' ``plot`` function. ``width`` defaults to 0.8,
            ``color`` to a per-column palette, and ``legend`` to True for grouped bars only.

    Returns:
        SubplotBase: The Matplotlib Axes object with the generated plot.

    Raises:
        ValueError: If df is empty.
        ValueError: If df is a Series and x_col is provided.
        ValueError: If orientation, sort_order, or data_label_format is not an accepted value.
        ValueError: If legend_labels is provided and its length does not match the number of legend entries.
    """
    orientation, sort_order, data_label_format = _validate_bar_inputs(
        df,
        x_col,
        orientation,
        sort_order,
        data_label_format,
    )

    width = kwargs.pop("width", DEFAULT_BAR_WIDTH)

    if value_col is None:
        value_col = ["Value"]
    elif isinstance(value_col, str):
        value_col = [value_col]

    df = df.to_frame(name=value_col[0]) if isinstance(df, pd.Series) else df

    if data_label_format in ["percentage_by_bar_group", "percentage_by_series"] and (df[value_col] < 0).any().any():
        warnings.warn(
            f"Negative values detected in {value_col}. This may lead to unexpected behavior in terms of the data "
            f"label format '{data_label_format}'.",
            UserWarning,
            stacklevel=2,
        )

    df = df.sort_values(by=value_col[0], ascending=sort_order in ("asc", "ascending")) if sort_order is not None else df

    default_colors = get_plot_colors(len(value_col))

    plot_kind = "bar" if orientation in ["vertical", "v"] else "barh"
    color = kwargs.pop("color", default_colors)
    legend = kwargs.pop("legend", (len(value_col) > 1))

    ax = df.plot(
        kind=plot_kind,
        y=value_col,
        x=x_col,
        ax=ax,
        width=width,
        color=color,
        legend=legend,
        **kwargs,
    )

    if use_hatch:
        ax = gu.apply_hatches(ax=ax, num_segments=len(value_col))

    if data_label_format is not None:
        _generate_bar_labels(
            ax=ax,
            plot_kind=plot_kind,
            value_col=value_col,
            df=df,
            data_label_format=data_label_format,
            is_stacked=kwargs.get("stacked", False),
            num_digits=num_digits,
        )

    return standard_graph_styles(
        ax=ax,
        title=title,
        eyebrow=eyebrow,
        subtitle=subtitle,
        x_label=x_label,
        y_label=y_label,
        legend_title=legend_title,
        legend_labels=legend_labels,
        move_legend_outside=move_legend_outside,
        source_text=source_text,
        # Gridlines on the value axis only — vertical bars read off y, horizontal off x.
        grid_axis="x" if plot_kind == "barh" else "y",
    )