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.
- {mgplot-0.1.6 → mgplot-0.1.7}/CHANGELOG.md +8 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/PKG-INFO +1 -1
- {mgplot-0.1.6 → mgplot-0.1.7}/pyproject.toml +1 -1
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/__init__.py +6 -6
- mgplot-0.1.6/src/mgplot/date_utils.py → mgplot-0.1.7/src/mgplot/axis_utils.py +68 -8
- mgplot-0.1.7/src/mgplot/bar_plot.py +316 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/finalisers.py +55 -73
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/growth_plot.py +139 -115
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/kw_type_checking.py +13 -1
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/line_plot.py +78 -21
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/multi_plot.py +3 -3
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/revision_plot.py +19 -21
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/run_plot.py +3 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/settings.py +1 -1
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/summary_plot.py +2 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/utilities.py +64 -85
- {mgplot-0.1.6 → mgplot-0.1.7}/uv.lock +51 -51
- mgplot-0.1.6/docs/index.html +0 -7
- mgplot-0.1.6/docs/mgplot/bar_plot.html +0 -504
- mgplot-0.1.6/docs/mgplot/finalise_plot.html +0 -818
- mgplot-0.1.6/docs/mgplot/line_plot.html +0 -719
- mgplot-0.1.6/docs/mgplot/postcovid_plot.html +0 -594
- mgplot-0.1.6/docs/mgplot/revision_plot.html +0 -404
- mgplot-0.1.6/docs/mgplot/run_plot.html +0 -626
- mgplot-0.1.6/docs/mgplot/seastrend_plot.html +0 -535
- mgplot-0.1.6/docs/mgplot/summary_plot.html +0 -641
- mgplot-0.1.6/docs/mgplot.html +0 -2936
- mgplot-0.1.6/docs/search.js +0 -46
- mgplot-0.1.6/src/mgplot/bar_plot.py +0 -116
- {mgplot-0.1.6 → mgplot-0.1.7}/.gitignore +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/LICENSE +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/README.md +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/build-docs.sh +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/build-test.sh +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/colors.py +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/finalise_plot.py +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/lint-all.sh +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/postcovid_plot.py +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/py.typed +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/seastrend_plot.py +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/test.py +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/zz-test-data/ocr_rba.csv +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/zz-test-data/revisions.csv +0 -0
- {mgplot-0.1.6 → mgplot-0.1.7}/src/mgplot/zz-test-data/summary.csv +0 -0
|
@@ -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
|
-
|
|
24
|
+
growth_plot,
|
|
25
25
|
series_growth_plot,
|
|
26
26
|
SERIES_GROWTH_KW_TYPES,
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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
|