mgplot.bar_plot

Create bar plots using Matplotlib.

Note: bar plots in Matplotlib are not the same as bar charts in other libraries. Bar plots are used to represent categorical data with rectangular bars. As a result, bar plots and line plots typically cannot be plotted on the same axes.

  1"""Create bar plots using Matplotlib.
  2
  3Note: bar plots in Matplotlib are not the same as bar charts in other
  4libraries. Bar plots are used to represent categorical data with
  5rectangular bars. As a result, bar plots and line plots typically
  6cannot be plotted on the same axes.
  7"""
  8
  9from collections.abc import Callable, Sequence
 10from typing import Any, Final, NotRequired, TypedDict, Unpack
 11
 12import matplotlib.patheffects as pe
 13import numpy as np
 14from matplotlib.axes import Axes
 15from pandas import DataFrame, Period, Series
 16
 17from mgplot.axis_utils import map_periodindex, map_stringindex, set_labels
 18from mgplot.keyword_checking import BaseKwargs, report_kwargs, validate_kwargs
 19from mgplot.settings import DataT, get_setting
 20from mgplot.utilities import (
 21    apply_defaults,
 22    constrain_data,
 23    default_rounding,
 24    get_axes,
 25    get_color_list,
 26)
 27
 28# --- constants
 29ME: Final[str] = "bar_plot"
 30MAX_ANNOTATIONS: Final[int] = 30
 31ADJUSTMENT_FACTOR: Final[float] = 0.02
 32MIN_BAR_WIDTH: Final[float] = 0.0
 33MAX_BAR_WIDTH: Final[float] = 1.0
 34DEFAULT_GROUPED_WIDTH: Final[float] = 0.8
 35DEFAULT_BAR_OFFSET: Final[float] = 0.5
 36DEFAULT_MAX_TICKS: Final[int] = 10
 37
 38
 39class BarKwargs(BaseKwargs):
 40    """Keyword arguments for the bar_plot function."""
 41
 42    # --- options for the entire bar plot
 43    ax: NotRequired[Axes | None]
 44    stacked: NotRequired[bool]
 45    horizontal: NotRequired[bool]
 46    max_ticks: NotRequired[int]
 47    tick_relabel: NotRequired[Callable[[str], str]]
 48    plot_from: NotRequired[int | Period]
 49    label_rotation: NotRequired[int | float]
 50    # --- options for each bar ...
 51    color: NotRequired[str | Sequence[str]]
 52    label_series: NotRequired[bool | Sequence[bool]]
 53    width: NotRequired[float | int | Sequence[float | int]]
 54    zorder: NotRequired[int | float | Sequence[int | float]]
 55    # --- options for bar annotations
 56    annotate: NotRequired[bool]
 57    fontsize: NotRequired[int | float | str]
 58    fontname: NotRequired[str]
 59    rounding: NotRequired[int]
 60    rotation: NotRequired[int | float]
 61    annotate_color: NotRequired[str]
 62    above: NotRequired[bool]
 63
 64
 65# --- functions
 66class AnnoKwargs(TypedDict, total=False):
 67    """TypedDict for the kwargs used in annotate_bars."""
 68
 69    annotate: bool
 70    fontsize: int | float | str
 71    fontname: str
 72    color: str
 73    rotation: int | float
 74    foreground: str | Sequence[str]  # used for stroke effect on text (per-bar if a sequence)
 75    above: bool
 76    rounding: bool | int  # if True, uses default rounding; if int, uses that value
 77
 78
 79def annotate_bars(
 80    series: Series,
 81    offset: float,
 82    base: np.ndarray,
 83    axes: Axes,
 84    *,
 85    horizontal: bool = False,
 86    **anno_kwargs: Unpack[AnnoKwargs],
 87) -> None:
 88    """Bar plot annotations.
 89
 90    Annotations are placed along the value axis: the y-axis for vertical
 91    bars, the x-axis when horizontal=True.
 92
 93    Note: "annotate", "fontsize", "fontname", "color", and "rotation" are expected in anno_kwargs.
 94    """
 95    # --- only annotate in limited circumstances
 96    if "annotate" not in anno_kwargs or not anno_kwargs["annotate"]:
 97        return
 98    max_annotations = MAX_ANNOTATIONS
 99    if len(series) > max_annotations:
100        return
101
102    # --- internal logic check
103    if len(base) != len(series):
104        print(f"Warning: base array length {len(base)} does not match series length {len(series)}.")
105        return
106
107    # --- assemble the annotation parameters
108    above: Final[bool | None] = anno_kwargs.get("above", False)  # None is also False-ish
109    annotate_style: dict[str, Any] = {
110        "fontsize": anno_kwargs.get("fontsize"),
111        "fontname": anno_kwargs.get("fontname"),
112        "color": anno_kwargs.get("color"),
113        "rotation": anno_kwargs.get("rotation"),
114    }
115    rounding = default_rounding(series=series, provided=anno_kwargs.get("rounding"))
116    adjustment = (series.max() - series.min()) * ADJUSTMENT_FACTOR
117    zero_correction = series.index.min()
118
119    # --- annotate each bar
120    for index, value in zip(series.index.astype(int), series, strict=True):
121        position = base[index - zero_correction] + (adjustment if value >= 0 else -adjustment)
122        if above:
123            position += value
124        if horizontal:
125            placement: dict[str, Any] = {
126                "x": position,
127                "y": index + offset,
128                "ha": "left" if value >= 0 else "right",
129                "va": "center",
130            }
131        else:
132            placement = {
133                "x": index + offset,
134                "y": position,
135                "ha": "center",
136                "va": "bottom" if value >= 0 else "top",
137            }
138        text = axes.text(
139            s=f"{value:.{rounding}f}",
140            **placement,
141            **annotate_style,
142        )
143        if not above and "foreground" in anno_kwargs:
144            # apply a stroke-effect to within bar annotations
145            # to make them more readable with very small bars.
146            foreground = anno_kwargs.get("foreground")
147            if isinstance(foreground, Sequence) and not isinstance(foreground, str):
148                foreground = foreground[index - zero_correction]  # per-bar colours
149            text.set_path_effects([pe.withStroke(linewidth=2, foreground=foreground)])
150
151
152class GroupedKwargs(TypedDict):
153    """TypedDict for the kwargs used in grouped."""
154
155    color: Sequence[str]
156    width: Sequence[float | int]
157    label_series: Sequence[bool]
158    zorder: Sequence[int | float | None]
159
160
161def grouped(
162    axes: Axes,
163    df: DataFrame,
164    anno_args: AnnoKwargs,
165    *,
166    horizontal: bool = False,
167    **kwargs: Unpack[GroupedKwargs],
168) -> None:
169    """Plot a grouped bar plot."""
170    series_count = len(df.columns)
171
172    for i, col in enumerate(df.columns):
173        series = df[col]
174        if series.isna().all():
175            continue
176        width = kwargs["width"][i]
177        if width < MIN_BAR_WIDTH or width > MAX_BAR_WIDTH:
178            width = DEFAULT_GROUPED_WIDTH
179        adjusted_width = width / series_count
180        # far-left + margin + halfway through one grouped column
181        left = -DEFAULT_BAR_OFFSET + ((1 - width) / 2.0) + (adjusted_width / 2.0)
182        offset = left + (i * adjusted_width)
183        foreground = kwargs["color"][i]
184        common: dict[str, Any] = {
185            "color": foreground,
186            "zorder": kwargs["zorder"][i],
187            "label": col if kwargs["label_series"][i] else f"_{col}_",
188        }
189        if horizontal:
190            axes.barh(y=series.index + offset, width=series, height=adjusted_width, **common)
191        else:
192            axes.bar(x=series.index + offset, height=series, width=adjusted_width, **common)
193        anno_args["foreground"] = foreground
194        annotate_bars(
195            series=series,
196            offset=offset,
197            base=np.zeros(len(series)),
198            axes=axes,
199            horizontal=horizontal,
200            **anno_args,
201        )
202
203
204class StackedKwargs(TypedDict):
205    """TypedDict for the kwargs used in stacked."""
206
207    color: Sequence[str]
208    width: Sequence[float | int]
209    label_series: Sequence[bool]
210    zorder: Sequence[int | float | None]
211
212
213def stacked(
214    axes: Axes,
215    df: DataFrame,
216    anno_args: AnnoKwargs,
217    *,
218    horizontal: bool = False,
219    **kwargs: Unpack[StackedKwargs],
220) -> None:
221    """Plot a stacked bar plot."""
222    row_count = len(df)
223    base_plus: np.ndarray = np.zeros(shape=row_count, dtype=np.float64)
224    base_minus: np.ndarray = np.zeros(shape=row_count, dtype=np.float64)
225    for i, col in enumerate(df.columns):
226        series = df[col]
227        base = np.where(series >= 0, base_plus, base_minus)
228        foreground = kwargs["color"][i]
229        common: dict[str, Any] = {
230            "color": foreground,
231            "zorder": kwargs["zorder"][i],
232            "label": col if kwargs["label_series"][i] else f"_{col}_",
233        }
234        if horizontal:
235            axes.barh(y=series.index, width=series, left=base, height=kwargs["width"][i], **common)
236        else:
237            axes.bar(x=series.index, height=series, bottom=base, width=kwargs["width"][i], **common)
238        anno_args["foreground"] = foreground
239        annotate_bars(
240            series=series,
241            offset=0,
242            base=base,
243            axes=axes,
244            horizontal=horizontal,
245            **anno_args,
246        )
247        base_plus += np.where(series >= 0, series, 0)
248        base_minus += np.where(series < 0, series, 0)
249
250
251def bar_plot(data: DataT, **kwargs: Unpack[BarKwargs]) -> Axes:
252    """Create a bar plot from the given data.
253
254    Each column in the DataFrame will be stacked on top of each other,
255    with positive values above zero and negative values below zero.
256
257    Args:
258        data: Series | DataFrame - The data to plot. Can be a DataFrame or a Series.
259        **kwargs: BarKwargs - Additional keyword arguments for customization.
260        (see BarKwargs for details)
261
262    Note: This function does not assume all data is timeseries with a PeriodIndex.
263
264    Returns:
265        axes: Axes - The axes for the plot.
266
267    """
268    # --- check the kwargs
269    report_kwargs(caller=ME, **kwargs)
270    validate_kwargs(schema=BarKwargs, caller=ME, **kwargs)
271
272    # --- get the data
273    # no call to check_clean_timeseries here, as bar plots are not
274    # necessarily timeseries data. If the data is a Series, it will be
275    # converted to a DataFrame with a single column.
276    df = DataFrame(data)  # really we are only plotting DataFrames
277    df, kwargs_d = constrain_data(df, **kwargs)
278    item_count = len(df.columns)
279
280    # --- deal with string indices
281    saved_strings = map_stringindex(df)
282    if saved_strings is not None:
283        df = saved_strings[0]
284
285    # --- deal with complete PeriodIndex indices
286    saved_pi = map_periodindex(df)
287    if saved_pi is not None:
288        df = saved_pi[0]  # extract the reindexed DataFrame from the PeriodIndex
289
290    # --- set up the default arguments
291    chart_defaults: dict[str, bool | int] = {
292        "stacked": False,
293        "horizontal": False,
294        "max_ticks": DEFAULT_MAX_TICKS,
295        "label_series": item_count > 1,
296        "label_rotation": 0,
297    }
298    chart_args = {k: kwargs_d.get(k, v) for k, v in chart_defaults.items()}
299
300    # --- horizontal bars are for categorical data, not PeriodIndex timeseries
301    horizontal = bool(chart_args["horizontal"])
302    if horizontal and saved_pi is not None:
303        print(f"Warning: horizontal=True is not supported with a PeriodIndex in {ME}(); plotting vertical.")
304        horizontal = False
305
306    # --- single series + one colour per bar => per-bar colours
307    user_color = kwargs_d.get("color")
308    if (
309        item_count == 1
310        and isinstance(user_color, Sequence)
311        and not isinstance(user_color, str)
312        and len(user_color) == len(df)
313    ):
314        kwargs_d["color"] = [list(user_color)]  # one series whose colour is a per-bar list
315
316    bar_defaults = {
317        "color": get_color_list(item_count),
318        "width": get_setting("bar_width"),
319        "label_series": item_count > 1,
320        "zorder": None,
321    }
322    above = kwargs_d.get("above", False)
323    anno_args: AnnoKwargs = {
324        "annotate": kwargs_d.get("annotate", False),
325        "fontsize": kwargs_d.get("fontsize", "small"),
326        "fontname": kwargs_d.get("fontname", "Helvetica"),
327        "rotation": kwargs_d.get("rotation", 0),
328        "rounding": kwargs_d.get("rounding", True),
329        "color": kwargs_d.get("annotate_color", "black" if above else "white"),
330        "above": above,
331    }
332    bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs_d)
333
334    # --- plot the data
335    axes, remaining_kwargs = get_axes(**dict(remaining_kwargs))
336    if chart_args["stacked"]:
337        stacked(axes, df, anno_args, horizontal=horizontal, **bar_args)
338    else:
339        grouped(axes, df, anno_args, horizontal=horizontal, **bar_args)
340
341    # --- handle index labels and rotation
342    if saved_strings is not None:
343        if horizontal:
344            axes.set_yticks(range(len(saved_strings[1])))
345            axes.set_yticklabels(saved_strings[1], rotation=chart_args["label_rotation"])
346        else:
347            axes.set_xticks(range(len(saved_strings[1])))
348            axes.set_xticklabels(saved_strings[1], rotation=chart_args["label_rotation"])
349    elif saved_pi is not None:
350        set_labels(
351            axes,
352            saved_pi[1],
353            chart_args["max_ticks"],
354            rotation=chart_args["label_rotation"],
355            tick_relabel=kwargs_d.get("tick_relabel"),
356        )
357
358    return axes
ME: Final[str] = 'bar_plot'
MAX_ANNOTATIONS: Final[int] = 30
ADJUSTMENT_FACTOR: Final[float] = 0.02
MIN_BAR_WIDTH: Final[float] = 0.0
MAX_BAR_WIDTH: Final[float] = 1.0
DEFAULT_GROUPED_WIDTH: Final[float] = 0.8
DEFAULT_BAR_OFFSET: Final[float] = 0.5
DEFAULT_MAX_TICKS: Final[int] = 10
class BarKwargs(mgplot.keyword_checking.BaseKwargs):
40class BarKwargs(BaseKwargs):
41    """Keyword arguments for the bar_plot function."""
42
43    # --- options for the entire bar plot
44    ax: NotRequired[Axes | None]
45    stacked: NotRequired[bool]
46    horizontal: NotRequired[bool]
47    max_ticks: NotRequired[int]
48    tick_relabel: NotRequired[Callable[[str], str]]
49    plot_from: NotRequired[int | Period]
50    label_rotation: NotRequired[int | float]
51    # --- options for each bar ...
52    color: NotRequired[str | Sequence[str]]
53    label_series: NotRequired[bool | Sequence[bool]]
54    width: NotRequired[float | int | Sequence[float | int]]
55    zorder: NotRequired[int | float | Sequence[int | float]]
56    # --- options for bar annotations
57    annotate: NotRequired[bool]
58    fontsize: NotRequired[int | float | str]
59    fontname: NotRequired[str]
60    rounding: NotRequired[int]
61    rotation: NotRequired[int | float]
62    annotate_color: NotRequired[str]
63    above: NotRequired[bool]

Keyword arguments for the bar_plot function.

ax: NotRequired[matplotlib.axes._axes.Axes | None]
stacked: NotRequired[bool]
horizontal: NotRequired[bool]
max_ticks: NotRequired[int]
tick_relabel: NotRequired[Callable[[str], str]]
plot_from: NotRequired[int | pandas.Period]
label_rotation: NotRequired[int | float]
color: NotRequired[str | Sequence[str]]
label_series: NotRequired[bool | Sequence[bool]]
width: NotRequired[float | int | Sequence[float | int]]
zorder: NotRequired[float | int | Sequence[float | int]]
annotate: NotRequired[bool]
fontsize: NotRequired[int | float | str]
fontname: NotRequired[str]
rounding: NotRequired[int]
rotation: NotRequired[int | float]
annotate_color: NotRequired[str]
above: NotRequired[bool]
class AnnoKwargs(typing.TypedDict):
67class AnnoKwargs(TypedDict, total=False):
68    """TypedDict for the kwargs used in annotate_bars."""
69
70    annotate: bool
71    fontsize: int | float | str
72    fontname: str
73    color: str
74    rotation: int | float
75    foreground: str | Sequence[str]  # used for stroke effect on text (per-bar if a sequence)
76    above: bool
77    rounding: bool | int  # if True, uses default rounding; if int, uses that value

TypedDict for the kwargs used in annotate_bars.

annotate: bool
fontsize: int | float | str
fontname: str
color: str
rotation: int | float
foreground: str | Sequence[str]
above: bool
rounding: bool | int
def annotate_bars( series: pandas.Series, offset: float, base: numpy.ndarray, axes: matplotlib.axes._axes.Axes, *, horizontal: bool = False, **anno_kwargs: Unpack[AnnoKwargs]) -> None:
 80def annotate_bars(
 81    series: Series,
 82    offset: float,
 83    base: np.ndarray,
 84    axes: Axes,
 85    *,
 86    horizontal: bool = False,
 87    **anno_kwargs: Unpack[AnnoKwargs],
 88) -> None:
 89    """Bar plot annotations.
 90
 91    Annotations are placed along the value axis: the y-axis for vertical
 92    bars, the x-axis when horizontal=True.
 93
 94    Note: "annotate", "fontsize", "fontname", "color", and "rotation" are expected in anno_kwargs.
 95    """
 96    # --- only annotate in limited circumstances
 97    if "annotate" not in anno_kwargs or not anno_kwargs["annotate"]:
 98        return
 99    max_annotations = MAX_ANNOTATIONS
100    if len(series) > max_annotations:
101        return
102
103    # --- internal logic check
104    if len(base) != len(series):
105        print(f"Warning: base array length {len(base)} does not match series length {len(series)}.")
106        return
107
108    # --- assemble the annotation parameters
109    above: Final[bool | None] = anno_kwargs.get("above", False)  # None is also False-ish
110    annotate_style: dict[str, Any] = {
111        "fontsize": anno_kwargs.get("fontsize"),
112        "fontname": anno_kwargs.get("fontname"),
113        "color": anno_kwargs.get("color"),
114        "rotation": anno_kwargs.get("rotation"),
115    }
116    rounding = default_rounding(series=series, provided=anno_kwargs.get("rounding"))
117    adjustment = (series.max() - series.min()) * ADJUSTMENT_FACTOR
118    zero_correction = series.index.min()
119
120    # --- annotate each bar
121    for index, value in zip(series.index.astype(int), series, strict=True):
122        position = base[index - zero_correction] + (adjustment if value >= 0 else -adjustment)
123        if above:
124            position += value
125        if horizontal:
126            placement: dict[str, Any] = {
127                "x": position,
128                "y": index + offset,
129                "ha": "left" if value >= 0 else "right",
130                "va": "center",
131            }
132        else:
133            placement = {
134                "x": index + offset,
135                "y": position,
136                "ha": "center",
137                "va": "bottom" if value >= 0 else "top",
138            }
139        text = axes.text(
140            s=f"{value:.{rounding}f}",
141            **placement,
142            **annotate_style,
143        )
144        if not above and "foreground" in anno_kwargs:
145            # apply a stroke-effect to within bar annotations
146            # to make them more readable with very small bars.
147            foreground = anno_kwargs.get("foreground")
148            if isinstance(foreground, Sequence) and not isinstance(foreground, str):
149                foreground = foreground[index - zero_correction]  # per-bar colours
150            text.set_path_effects([pe.withStroke(linewidth=2, foreground=foreground)])

Bar plot annotations.

Annotations are placed along the value axis: the y-axis for vertical bars, the x-axis when horizontal=True.

Note: "annotate", "fontsize", "fontname", "color", and "rotation" are expected in anno_kwargs.

class GroupedKwargs(typing.TypedDict):
153class GroupedKwargs(TypedDict):
154    """TypedDict for the kwargs used in grouped."""
155
156    color: Sequence[str]
157    width: Sequence[float | int]
158    label_series: Sequence[bool]
159    zorder: Sequence[int | float | None]

TypedDict for the kwargs used in grouped.

color: Sequence[str]
width: Sequence[float | int]
label_series: Sequence[bool]
zorder: Sequence[int | float | None]
def grouped( axes: matplotlib.axes._axes.Axes, df: pandas.DataFrame, anno_args: AnnoKwargs, *, horizontal: bool = False, **kwargs: Unpack[GroupedKwargs]) -> None:
162def grouped(
163    axes: Axes,
164    df: DataFrame,
165    anno_args: AnnoKwargs,
166    *,
167    horizontal: bool = False,
168    **kwargs: Unpack[GroupedKwargs],
169) -> None:
170    """Plot a grouped bar plot."""
171    series_count = len(df.columns)
172
173    for i, col in enumerate(df.columns):
174        series = df[col]
175        if series.isna().all():
176            continue
177        width = kwargs["width"][i]
178        if width < MIN_BAR_WIDTH or width > MAX_BAR_WIDTH:
179            width = DEFAULT_GROUPED_WIDTH
180        adjusted_width = width / series_count
181        # far-left + margin + halfway through one grouped column
182        left = -DEFAULT_BAR_OFFSET + ((1 - width) / 2.0) + (adjusted_width / 2.0)
183        offset = left + (i * adjusted_width)
184        foreground = kwargs["color"][i]
185        common: dict[str, Any] = {
186            "color": foreground,
187            "zorder": kwargs["zorder"][i],
188            "label": col if kwargs["label_series"][i] else f"_{col}_",
189        }
190        if horizontal:
191            axes.barh(y=series.index + offset, width=series, height=adjusted_width, **common)
192        else:
193            axes.bar(x=series.index + offset, height=series, width=adjusted_width, **common)
194        anno_args["foreground"] = foreground
195        annotate_bars(
196            series=series,
197            offset=offset,
198            base=np.zeros(len(series)),
199            axes=axes,
200            horizontal=horizontal,
201            **anno_args,
202        )

Plot a grouped bar plot.

class StackedKwargs(typing.TypedDict):
205class StackedKwargs(TypedDict):
206    """TypedDict for the kwargs used in stacked."""
207
208    color: Sequence[str]
209    width: Sequence[float | int]
210    label_series: Sequence[bool]
211    zorder: Sequence[int | float | None]

TypedDict for the kwargs used in stacked.

color: Sequence[str]
width: Sequence[float | int]
label_series: Sequence[bool]
zorder: Sequence[int | float | None]
def stacked( axes: matplotlib.axes._axes.Axes, df: pandas.DataFrame, anno_args: AnnoKwargs, *, horizontal: bool = False, **kwargs: Unpack[StackedKwargs]) -> None:
214def stacked(
215    axes: Axes,
216    df: DataFrame,
217    anno_args: AnnoKwargs,
218    *,
219    horizontal: bool = False,
220    **kwargs: Unpack[StackedKwargs],
221) -> None:
222    """Plot a stacked bar plot."""
223    row_count = len(df)
224    base_plus: np.ndarray = np.zeros(shape=row_count, dtype=np.float64)
225    base_minus: np.ndarray = np.zeros(shape=row_count, dtype=np.float64)
226    for i, col in enumerate(df.columns):
227        series = df[col]
228        base = np.where(series >= 0, base_plus, base_minus)
229        foreground = kwargs["color"][i]
230        common: dict[str, Any] = {
231            "color": foreground,
232            "zorder": kwargs["zorder"][i],
233            "label": col if kwargs["label_series"][i] else f"_{col}_",
234        }
235        if horizontal:
236            axes.barh(y=series.index, width=series, left=base, height=kwargs["width"][i], **common)
237        else:
238            axes.bar(x=series.index, height=series, bottom=base, width=kwargs["width"][i], **common)
239        anno_args["foreground"] = foreground
240        annotate_bars(
241            series=series,
242            offset=0,
243            base=base,
244            axes=axes,
245            horizontal=horizontal,
246            **anno_args,
247        )
248        base_plus += np.where(series >= 0, series, 0)
249        base_minus += np.where(series < 0, series, 0)

Plot a stacked bar plot.

def bar_plot( data: ~DataT, **kwargs: Unpack[BarKwargs]) -> matplotlib.axes._axes.Axes:
252def bar_plot(data: DataT, **kwargs: Unpack[BarKwargs]) -> Axes:
253    """Create a bar plot from the given data.
254
255    Each column in the DataFrame will be stacked on top of each other,
256    with positive values above zero and negative values below zero.
257
258    Args:
259        data: Series | DataFrame - The data to plot. Can be a DataFrame or a Series.
260        **kwargs: BarKwargs - Additional keyword arguments for customization.
261        (see BarKwargs for details)
262
263    Note: This function does not assume all data is timeseries with a PeriodIndex.
264
265    Returns:
266        axes: Axes - The axes for the plot.
267
268    """
269    # --- check the kwargs
270    report_kwargs(caller=ME, **kwargs)
271    validate_kwargs(schema=BarKwargs, caller=ME, **kwargs)
272
273    # --- get the data
274    # no call to check_clean_timeseries here, as bar plots are not
275    # necessarily timeseries data. If the data is a Series, it will be
276    # converted to a DataFrame with a single column.
277    df = DataFrame(data)  # really we are only plotting DataFrames
278    df, kwargs_d = constrain_data(df, **kwargs)
279    item_count = len(df.columns)
280
281    # --- deal with string indices
282    saved_strings = map_stringindex(df)
283    if saved_strings is not None:
284        df = saved_strings[0]
285
286    # --- deal with complete PeriodIndex indices
287    saved_pi = map_periodindex(df)
288    if saved_pi is not None:
289        df = saved_pi[0]  # extract the reindexed DataFrame from the PeriodIndex
290
291    # --- set up the default arguments
292    chart_defaults: dict[str, bool | int] = {
293        "stacked": False,
294        "horizontal": False,
295        "max_ticks": DEFAULT_MAX_TICKS,
296        "label_series": item_count > 1,
297        "label_rotation": 0,
298    }
299    chart_args = {k: kwargs_d.get(k, v) for k, v in chart_defaults.items()}
300
301    # --- horizontal bars are for categorical data, not PeriodIndex timeseries
302    horizontal = bool(chart_args["horizontal"])
303    if horizontal and saved_pi is not None:
304        print(f"Warning: horizontal=True is not supported with a PeriodIndex in {ME}(); plotting vertical.")
305        horizontal = False
306
307    # --- single series + one colour per bar => per-bar colours
308    user_color = kwargs_d.get("color")
309    if (
310        item_count == 1
311        and isinstance(user_color, Sequence)
312        and not isinstance(user_color, str)
313        and len(user_color) == len(df)
314    ):
315        kwargs_d["color"] = [list(user_color)]  # one series whose colour is a per-bar list
316
317    bar_defaults = {
318        "color": get_color_list(item_count),
319        "width": get_setting("bar_width"),
320        "label_series": item_count > 1,
321        "zorder": None,
322    }
323    above = kwargs_d.get("above", False)
324    anno_args: AnnoKwargs = {
325        "annotate": kwargs_d.get("annotate", False),
326        "fontsize": kwargs_d.get("fontsize", "small"),
327        "fontname": kwargs_d.get("fontname", "Helvetica"),
328        "rotation": kwargs_d.get("rotation", 0),
329        "rounding": kwargs_d.get("rounding", True),
330        "color": kwargs_d.get("annotate_color", "black" if above else "white"),
331        "above": above,
332    }
333    bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs_d)
334
335    # --- plot the data
336    axes, remaining_kwargs = get_axes(**dict(remaining_kwargs))
337    if chart_args["stacked"]:
338        stacked(axes, df, anno_args, horizontal=horizontal, **bar_args)
339    else:
340        grouped(axes, df, anno_args, horizontal=horizontal, **bar_args)
341
342    # --- handle index labels and rotation
343    if saved_strings is not None:
344        if horizontal:
345            axes.set_yticks(range(len(saved_strings[1])))
346            axes.set_yticklabels(saved_strings[1], rotation=chart_args["label_rotation"])
347        else:
348            axes.set_xticks(range(len(saved_strings[1])))
349            axes.set_xticklabels(saved_strings[1], rotation=chart_args["label_rotation"])
350    elif saved_pi is not None:
351        set_labels(
352            axes,
353            saved_pi[1],
354            chart_args["max_ticks"],
355            rotation=chart_args["label_rotation"],
356            tick_relabel=kwargs_d.get("tick_relabel"),
357        )
358
359    return axes

Create a bar plot from the given data.

Each column in the DataFrame will be stacked on top of each other, with positive values above zero and negative values below zero.

Args: data: Series | DataFrame - The data to plot. Can be a DataFrame or a Series. **kwargs: BarKwargs - Additional keyword arguments for customization. (see BarKwargs for details)

Note: This function does not assume all data is timeseries with a PeriodIndex.

Returns: axes: Axes - The axes for the plot.