mgplot 0.1.6__tar.gz → 0.1.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {mgplot-0.1.6 → mgplot-0.1.7}/CHANGELOG.md +8 -0
  2. {mgplot-0.1.6 → mgplot-0.1.7}/PKG-INFO +1 -1
  3. {mgplot-0.1.6 → mgplot-0.1.7}/pyproject.toml +1 -1
  4. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/__init__.py +6 -6
  5. mgplot-0.1.6/src/mgplot/date_utils.py → mgplot-0.1.7/src/mgplot/axis_utils.py +68 -8
  6. mgplot-0.1.7/src/mgplot/bar_plot.py +316 -0
  7. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/finalisers.py +55 -73
  8. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/growth_plot.py +139 -115
  9. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/kw_type_checking.py +13 -1
  10. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/line_plot.py +78 -21
  11. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/multi_plot.py +3 -3
  12. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/revision_plot.py +19 -21
  13. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/run_plot.py +3 -0
  14. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/settings.py +1 -1
  15. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/summary_plot.py +2 -0
  16. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/utilities.py +64 -85
  17. {mgplot-0.1.6 → mgplot-0.1.7}/uv.lock +51 -51
  18. mgplot-0.1.6/docs/index.html +0 -7
  19. mgplot-0.1.6/docs/mgplot/bar_plot.html +0 -504
  20. mgplot-0.1.6/docs/mgplot/finalise_plot.html +0 -818
  21. mgplot-0.1.6/docs/mgplot/line_plot.html +0 -719
  22. mgplot-0.1.6/docs/mgplot/postcovid_plot.html +0 -594
  23. mgplot-0.1.6/docs/mgplot/revision_plot.html +0 -404
  24. mgplot-0.1.6/docs/mgplot/run_plot.html +0 -626
  25. mgplot-0.1.6/docs/mgplot/seastrend_plot.html +0 -535
  26. mgplot-0.1.6/docs/mgplot/summary_plot.html +0 -641
  27. mgplot-0.1.6/docs/mgplot.html +0 -2936
  28. mgplot-0.1.6/docs/search.js +0 -46
  29. mgplot-0.1.6/src/mgplot/bar_plot.py +0 -116
  30. {mgplot-0.1.6 → mgplot-0.1.7}/.gitignore +0 -0
  31. {mgplot-0.1.6 → mgplot-0.1.7}/LICENSE +0 -0
  32. {mgplot-0.1.6 → mgplot-0.1.7}/README.md +0 -0
  33. {mgplot-0.1.6 → mgplot-0.1.7}/build-docs.sh +0 -0
  34. {mgplot-0.1.6 → mgplot-0.1.7}/build-test.sh +0 -0
  35. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/colors.py +0 -0
  36. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/finalise_plot.py +0 -0
  37. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/lint-all.sh +0 -0
  38. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/postcovid_plot.py +0 -0
  39. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/py.typed +0 -0
  40. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/seastrend_plot.py +0 -0
  41. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/test.py +0 -0
  42. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/zz-test-data/ocr_rba.csv +0 -0
  43. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/zz-test-data/revisions.csv +0 -0
  44. {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/zz-test-data/summary.csv +0 -0
@@ -1,3 +1,11 @@
1
+ Version 0.1.7 - released 06-Jun-2025 (Canberra, Australia)
2
+
3
+ * major changes
4
+ - reworked growth_plot so that it used the line_plot()
5
+ and bar_plot() functions.
6
+
7
+ ---
8
+
1
9
  Version 0.1.6 - released 03-Jun-2025 (Canberra, Australia)
2
10
 
3
11
  * minor changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mgplot
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: mgplot is a time-series/PeriodIndex frontend for matplotlib
5
5
  Project-URL: Repository, https://github.com/bpalmer4/mgplot
6
6
  Project-URL: Homepage, https://github.com/bpalmer4/mgplot
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "mgplot"
3
- version = "0.1.6"
3
+ version = "0.1.7"
4
4
  description = "mgplot is a time-series/PeriodIndex frontend for matplotlib"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -21,10 +21,10 @@ from mgplot.run_plot import run_plot, RUN_KW_TYPES
21
21
  from mgplot.summary_plot import summary_plot, SUMMARY_KW_TYPES
22
22
  from mgplot.growth_plot import (
23
23
  calc_growth,
24
- raw_growth_plot,
24
+ growth_plot,
25
25
  series_growth_plot,
26
26
  SERIES_GROWTH_KW_TYPES,
27
- RAW_GROWTH_KW_TYPES,
27
+ GROWTH_KW_TYPES,
28
28
  )
29
29
  from mgplot.multi_plot import (
30
30
  multi_start,
@@ -53,7 +53,7 @@ from mgplot.finalisers import (
53
53
  postcovid_plot_finalise,
54
54
  revision_plot_finalise,
55
55
  summary_plot_finalise,
56
- raw_growth_plot_finalise,
56
+ growth_plot_finalise,
57
57
  series_growth_plot_finalise,
58
58
  run_plot_finalise,
59
59
  )
@@ -102,7 +102,7 @@ __all__ = (
102
102
  "summary_plot",
103
103
  # --- growth_plot
104
104
  "calc_growth",
105
- "raw_growth_plot",
105
+ "growth_plot",
106
106
  "series_growth_plot",
107
107
  # --- multi_plot
108
108
  "multi_start",
@@ -115,7 +115,7 @@ __all__ = (
115
115
  "postcovid_plot_finalise",
116
116
  "revision_plot_finalise",
117
117
  "summary_plot_finalise",
118
- "raw_growth_plot_finalise",
118
+ "growth_plot_finalise",
119
119
  "series_growth_plot_finalise",
120
120
  "run_plot_finalise",
121
121
  # --- typing information
@@ -128,7 +128,7 @@ __all__ = (
128
128
  "RUN_KW_TYPES",
129
129
  "SUMMARY_KW_TYPES",
130
130
  "SERIES_GROWTH_KW_TYPES",
131
- "RAW_GROWTH_KW_TYPES",
131
+ "GROWTH_KW_TYPES",
132
132
  # --- The rest are internal use only
133
133
  )
134
134
  # __pdoc__: dict[str, Any] = {"test": False} # hide submodules from documentation
@@ -1,16 +1,66 @@
1
1
  """
2
- date_utils.py
3
- This module contains functions to work with date-like
4
- (i.e. not time-like) PeriodIndex frequencies in Pandas.
5
- It is used to label the x-axis of a plot with the
6
- appropriate date-like labels.
2
+ axis_utils.py
3
+
4
+ This module contains functions to work with categorical
5
+ axis in Matplotlib, specifically:
6
+ 1) integers
7
+ 2) date-like PeriodIndex frequencies
8
+ 3) strings
7
9
  """
8
10
 
9
11
  import calendar
10
12
  from enum import Enum
11
- from pandas import Period, PeriodIndex, period_range
13
+ from pandas import Series, Index, Period, PeriodIndex, period_range
14
+ from pandas.api.types import is_integer_dtype, is_string_dtype
12
15
  from matplotlib.pyplot import Axes
13
16
 
17
+ from mgplot.settings import DataT
18
+
19
+
20
+ def is_categorical(data: DataT) -> bool:
21
+ """
22
+ Check if the data.index is usefully categorical
23
+ (index needs to be complete, and unique).
24
+
25
+ Note: we plot categoricals using bar plots.
26
+ """
27
+
28
+ if data.index.has_duplicates or data.index.hasnans or data.index.empty:
29
+ return False
30
+ if is_string_dtype(data.index.dtype):
31
+ # unique strings are categoricals by default
32
+ return True
33
+ if (
34
+ not data.index.is_monotonic_increasing
35
+ and not data.index.is_monotonic_decreasing
36
+ ):
37
+ # these categoricals should be monotonic
38
+ return False
39
+ if is_integer_dtype(data.index.dtype):
40
+ # completeness check for integers
41
+ return data.index.max() - data.index.min() == len(data.index) - 1
42
+ if isinstance(data.index, PeriodIndex):
43
+ # completeness check for PeriodIndex
44
+ return (
45
+ data.index.max().ordinal - data.index.min().ordinal == len(data.index) - 1
46
+ )
47
+
48
+ return False
49
+
50
+
51
+ def map_periodindex(data: DataT) -> None | tuple[DataT, PeriodIndex]:
52
+ """
53
+ map a PeriodIndex to an integer index.
54
+ """
55
+ if not is_categorical(data):
56
+ return None
57
+ if not isinstance(data.index, PeriodIndex):
58
+ return None
59
+ original_index = data.index
60
+ ordinals = tuple(p.ordinal for p in PeriodIndex(data.index))
61
+ data.index = Index(ordinals)
62
+ return data, original_index
63
+
14
64
 
15
65
  class DateLike(Enum):
16
66
  """Recognised date-like PeriodIndex frequencies"""
@@ -285,8 +335,7 @@ def make_ilabels(p: PeriodIndex, max_ticks: int) -> tuple[list[int], list[str]]:
285
335
  """
286
336
 
287
337
  labels = make_labels(p, max_ticks)
288
- base = p.min().ordinal
289
- ticks = [x.ordinal - base for x in sorted(labels.keys())]
338
+ ticks = [x.ordinal for x in sorted(labels.keys())]
290
339
  ticklabels = [labels[x] for x in sorted(labels.keys())]
291
340
 
292
341
  return ticks, ticklabels
@@ -322,3 +371,14 @@ if __name__ == "__main__":
322
371
  print(f"Test {index + 1}")
323
372
  print("Labels:", make_labels(test, 10), "\n")
324
373
  print("========")
374
+
375
+ N = 4
376
+ int_test1: Series = Series(range(N), index=range(N))
377
+ int_test2: Series = Series(range(N), index=[1, 2, 3, 4])
378
+ str_test3: Series = Series(range(N), index=[f"Item {i}" for i in range(N)])
379
+ pi_test4: Series = Series(
380
+ range(N), index=period_range(start="2020-01", periods=N, freq="M")
381
+ )
382
+ for s_test in (int_test1, int_test2, str_test3, pi_test4):
383
+ print(f"Testing is_categorical {s_test.index}:", is_categorical(s_test))
384
+ print("========")
@@ -0,0 +1,316 @@
1
+ """
2
+ bar_plot.py
3
+ This module contains functions to create bar plots using Matplotlib.
4
+ Note: bar plots in Matplotlib are not the same as bar charts in other
5
+ libraries. Bar plots are used to represent categorical data with
6
+ rectangular bars. As a result, bar plots and line plots typically
7
+ cannot be plotted on the same axes.
8
+ """
9
+
10
+ # --- imports
11
+ from typing import Any, Final
12
+ from collections.abc import Sequence
13
+
14
+ import numpy as np
15
+ from pandas import Series, DataFrame, PeriodIndex
16
+ import matplotlib.pyplot as plt
17
+ from matplotlib.pyplot import Axes
18
+ import matplotlib.patheffects as pe
19
+
20
+
21
+ from mgplot.settings import DataT, get_setting
22
+ from mgplot.finalise_plot import make_legend
23
+ from mgplot.utilities import (
24
+ apply_defaults,
25
+ get_color_list,
26
+ get_axes,
27
+ constrain_data,
28
+ default_rounding,
29
+ )
30
+ from mgplot.kw_type_checking import (
31
+ ExpectedTypeDict,
32
+ validate_expected,
33
+ report_kwargs,
34
+ validate_kwargs,
35
+ )
36
+ from mgplot.axis_utils import set_labels, map_periodindex, is_categorical
37
+
38
+
39
+ # --- constants
40
+ # - plot and data constants
41
+ AXES = "ax" # used to control the axes to plot on
42
+ DROPNA = "dropna" # used to control dropping NaN values
43
+ STACKED = "stacked" # used to control if the bars are stacked or grouped
44
+ ROTATION = "rotation" # used to control the rotation of x-axis labels
45
+ MAX_TICKS = "max_ticks" # used to control the maximum number of ticks on the x-axis
46
+ PLOT_FROM = "plot_from" # used to control the starting point of the plot
47
+ # - bar plot constants
48
+ LEGEND = "legend" # used to control the legend display
49
+ COLOR = "color" # used to control the color of the bars
50
+ WIDTH = "width" # used to control the width of the bars
51
+ LABEL_SERIES = "label_series" # used to control the labeling of series in the legend
52
+ # - annoptation constants
53
+ ANNOTATE = "annotate" # used to control the annotation of bars
54
+ FONTSIZE = "fontsize" # used to control the font size of annotations
55
+ FONTNAME = "fontname" # used to control the font name of annotations
56
+ BAR_ROTATION = "bar_rotation" # used to control the rotation of bar labels
57
+ ANNO_COLOR = "annotate_color" # used to control the color of annotations
58
+ ROUNDING = "rounding" # used to control the rounding of annotations
59
+ ABOVE = "above" # used to control the position of annotations
60
+
61
+ BAR_KW_TYPES: Final[ExpectedTypeDict] = {
62
+ # --- options for the entire bar plot
63
+ AXES: (Axes, type(None)), # axes to plot on, or None for new axes
64
+ STACKED: bool, # if True, the bars will be stacked. If False, they will be grouped.
65
+ ROTATION: (int, float), # rotation of x-axis labels in degrees
66
+ MAX_TICKS: int,
67
+ PLOT_FROM: (int, PeriodIndex, type(None)),
68
+ LEGEND: (bool, dict, (str, object), type(None)),
69
+ # --- options for each bar ...
70
+ COLOR: (str, Sequence, (str,)),
71
+ LABEL_SERIES: (bool, Sequence, (bool,)),
72
+ WIDTH: (float, Sequence, (float,)),
73
+ # - options for bar annotations
74
+ ANNOTATE: (type(None), bool), # None, True
75
+ FONTSIZE: (int, float, str),
76
+ FONTNAME: (str),
77
+ BAR_ROTATION: (int, float), # rotation of bar labels
78
+ ROUNDING: int,
79
+ ANNO_COLOR: (str, type(None)), # color of annotations
80
+ ABOVE: bool, # if True, annotations are above the bar
81
+ # - other bar attributes
82
+ }
83
+ validate_expected(BAR_KW_TYPES, "bar_plot")
84
+
85
+
86
+ # --- functions
87
+ def annotate_bars(
88
+ series: Series,
89
+ offset: float,
90
+ base: np.ndarray[tuple[int, ...], np.dtype[Any]],
91
+ axes: Axes,
92
+ **kwargs,
93
+ ) -> None:
94
+ """Bar plot annotations."""
95
+
96
+ # --- only annotate in limited circumstances
97
+ if ANNOTATE not in kwargs or kwargs[ANNOTATE] is None or kwargs[ANNOTATE] is False:
98
+ return
99
+ max_annotations = 30
100
+ if len(series) > max_annotations:
101
+ return
102
+
103
+ # --- internal logic check
104
+ if len(base) != len(series):
105
+ print(
106
+ f"Warning: base array length {len(base)} does not match series length {len(series)}."
107
+ )
108
+ return
109
+
110
+ # --- assemble the annotation parameters
111
+ above: Final[bool | None] = kwargs.get(ABOVE, False) # None is also False-ish
112
+ annotate_style = {
113
+ "fontsize": kwargs.get(FONTSIZE),
114
+ "fontname": kwargs.get(FONTNAME),
115
+ "color": kwargs.get(ANNO_COLOR),
116
+ "rotation": kwargs.get(BAR_ROTATION),
117
+ }
118
+ rounding = default_rounding(series=series, provided=kwargs.get(ROUNDING, None))
119
+ adjustment = (series.max() - series.min()) * 0.01
120
+ rebase = series.index.min()
121
+
122
+ # --- annotate each bar
123
+ for index, value in zip(series.index.astype(int), series): # mypy syntactic sugar
124
+ position = base[index - rebase] + (adjustment if value >= 0 else -adjustment)
125
+ if above:
126
+ position += value
127
+ text = axes.text(
128
+ x=index + offset,
129
+ y=position,
130
+ s=f"{value:.{rounding}f}",
131
+ ha="center",
132
+ va="bottom" if value >= 0 else "top",
133
+ **annotate_style,
134
+ )
135
+ if not above and "foreground" in kwargs:
136
+ # apply a stroke-effect to within bar annotations
137
+ # to make them more readable with very small bars.
138
+ text.set_path_effects(
139
+ [pe.withStroke(linewidth=2, foreground=kwargs.get("foreground"))]
140
+ )
141
+
142
+
143
+ def grouped(axes, df: DataFrame, anno_args, **kwargs) -> None:
144
+ """
145
+ plot a grouped bar plot
146
+ """
147
+
148
+ series_count = len(df.columns)
149
+
150
+ for i, col in enumerate(df.columns):
151
+ series = df[col]
152
+ if series.isnull().all():
153
+ continue
154
+ width = kwargs["width"][i]
155
+ if width < 0 or width > 1:
156
+ width = 0.8
157
+ adjusted_width = width / series_count # 0.8
158
+ # far-left + margin + halfway through one grouped column
159
+ left = -0.5 + ((1 - width) / 2.0) + (adjusted_width / 2.0)
160
+ offset = left + (i * adjusted_width)
161
+ foreground = kwargs["color"][i]
162
+ axes.bar(
163
+ x=series.index + offset,
164
+ height=series,
165
+ color=foreground,
166
+ width=adjusted_width,
167
+ label=col if kwargs["label_series"][i] else "_not_in_legend_",
168
+ )
169
+ annotate_bars(
170
+ series,
171
+ offset,
172
+ np.zeros(len(series)),
173
+ axes,
174
+ foreground=foreground,
175
+ **anno_args,
176
+ )
177
+
178
+
179
+ def stacked(axes, df: DataFrame, anno_args, **kwargs) -> None:
180
+ """
181
+ plot a stacked bar plot
182
+ """
183
+
184
+ series_count = len(df)
185
+ base_plus: np.ndarray[tuple[int, ...], np.dtype[np.float64]] = np.zeros(
186
+ shape=series_count, dtype=np.float64
187
+ )
188
+ base_minus: np.ndarray[tuple[int, ...], np.dtype[np.float64]] = np.zeros(
189
+ shape=series_count, dtype=np.float64
190
+ )
191
+ for i, col in enumerate(df.columns):
192
+ series = df[col]
193
+ base = np.where(series >= 0, base_plus, base_minus)
194
+ foreground = kwargs["color"][i]
195
+ axes.bar(
196
+ x=series.index,
197
+ height=series,
198
+ bottom=base,
199
+ color=foreground,
200
+ width=kwargs["width"][i],
201
+ label=col if kwargs["label_series"][i] else "_not_in_legend_",
202
+ )
203
+ annotate_bars(series, 0, base, axes, foreground=foreground, **anno_args)
204
+ base_plus += np.where(series >= 0, series, 0)
205
+ base_minus += np.where(series < 0, series, 0)
206
+
207
+
208
+ def bar_plot(
209
+ data: DataT,
210
+ **kwargs,
211
+ ) -> Axes:
212
+ """
213
+ Create a bar plot from the given data. Each column in the DataFrame
214
+ will be stacked on top of each other, with positive values above
215
+ zero and negative values below zero.
216
+
217
+ Parameters
218
+ - data: Series - The data to plot. Can be a DataFrame or a Series.
219
+ - **kwargs: dict Additional keyword arguments for customization.
220
+ /* affects the entire bar plot */
221
+ - ax: Axes | None - The axes to plot on. If None, a new figure and axes will be created.
222
+ - stacked: bool - If True, the bars will be stacked. If False, they will be grouped.
223
+ - rotation: int | float - The rotation of the x-axis labels in degrees.
224
+ - max_ticks: int - The maximum number of ticks on the x-axis (for PeriodIndex only)
225
+ - plot_from: int | PeriodIndex | None - The starting point of the plot.
226
+ If None, the entire data will be plotted.
227
+ - legend: bool | dict | (str, object) | None - If True, a legend will be created.
228
+ /* affects the bars in the bar plot */
229
+ - color: str | Sequence[str] - The color of the bars. If a sequence is provided,
230
+ it should match the number of columns in the DataFrame.
231
+ - label_series: bool | Sequence[bool] - If True, the series will be labeled in
232
+ the legend. If a sequence is provided, it should match the number of columns
233
+ in the DataFrame.
234
+ /* options for bar annotations */
235
+ - annotate: None | bool - If True, the bars will be annotated with their values.
236
+ - fontsize: int | float | str - The font size of the annotations.
237
+ - fontname: str - The font name of the annotations.
238
+ - bar_rotation: int | float - The rotation of the bar labels in degrees.
239
+ - rounding: int | bool - The number of decimal places to round the annotations.
240
+ If True, a default between 0 and 2 is used.
241
+ - annotate_color: str - The color of the annotations.
242
+ - above: bool - If True, the annotations will be placed above the bars.
243
+
244
+ Note: This function does not assume all data is timeseries with a PeriodIndex,
245
+
246
+ Returns
247
+ - axes: Axes - The axes for the plot.
248
+ """
249
+
250
+ # --- check the kwargs
251
+ me = "bar_plot"
252
+ report_kwargs(called_from=me, **kwargs)
253
+ validate_kwargs(BAR_KW_TYPES, me, **kwargs)
254
+
255
+ # --- get the data
256
+ # no call to check_clean_timeseries here, as bar plots are not
257
+ # necessarily timeseries data. If the data is a Series, it will be
258
+ # converted to a DataFrame with a single column.
259
+ df = DataFrame(data) # really we are only plotting DataFrames
260
+ df, kwargs = constrain_data(df, **kwargs)
261
+ item_count = len(df.columns)
262
+
263
+ # --- deal with complete PeriodIdex indicies
264
+ if not is_categorical(df):
265
+ print(
266
+ "Warning: bar_plot is not designed for incomplete or non-categorical data indexes."
267
+ )
268
+ saved_pi = map_periodindex(df)
269
+ if saved_pi is not None:
270
+ df = saved_pi[0] # extract the reindexed DataFrame from the PeriodIndex
271
+
272
+ # --- set up the default arguments
273
+ bar_defaults: dict[str, Any] = {
274
+ "color": get_color_list(item_count),
275
+ "width": get_setting("bar_width"),
276
+ "label_series": (item_count > 1),
277
+ }
278
+ anno_args = {
279
+ ANNOTATE: kwargs.get(ANNOTATE, False),
280
+ FONTSIZE: kwargs.get(FONTSIZE, "small"),
281
+ FONTNAME: kwargs.get(FONTNAME, "Helvetica"),
282
+ BAR_ROTATION: kwargs.get(BAR_ROTATION, 0),
283
+ ROUNDING: kwargs.get(ROUNDING, True),
284
+ ANNO_COLOR: kwargs.get(ANNO_COLOR, "white"),
285
+ ABOVE: kwargs.get(ABOVE, False),
286
+ }
287
+ bar_args, remaining_kwargs = apply_defaults(item_count, bar_defaults, kwargs)
288
+ chart_defaults: dict[str, Any] = {
289
+ STACKED: False,
290
+ ROTATION: 90,
291
+ MAX_TICKS: 10,
292
+ LEGEND: item_count > 1,
293
+ }
294
+ chart_args = {k: kwargs.get(k, v) for k, v in chart_defaults.items()}
295
+
296
+ # --- plot the data
297
+ axes, _rkwargs = get_axes(**remaining_kwargs)
298
+ if chart_args["stacked"]:
299
+ stacked(axes, df, anno_args, **bar_args)
300
+ else:
301
+ grouped(axes, df, anno_args, **bar_args)
302
+
303
+ # --- handle complete periodIndex data and label rotation
304
+ rotate_labels = True
305
+ if saved_pi is not None:
306
+ set_labels(axes, saved_pi[1], chart_args["max_ticks"])
307
+ rotate_labels = False
308
+
309
+ if rotate_labels:
310
+ plt.xticks(rotation=chart_args["rotation"])
311
+
312
+ # --- add a legend if requested
313
+ if "LEGEND" in chart_args:
314
+ make_legend(axes, chart_args["LEGEND"])
315
+
316
+ return axes