8000 Merge pull request #23690 from stefmolin/bar-label-fmt · matplotlib/matplotlib@ae53649 · GitHub
[go: up one dir, main page]

Skip to content

Commit ae53649

Browse files
authored
Merge pull request #23690 from stefmolin/bar-label-fmt
Add new-style string formatting option and callable option to `fmt` in `Axes.bar_label()`.
2 parents f8cf0ee + 5448d57 commit ae53649

File tree

6 files changed

+123
-5
lines changed

6 files changed

+123
-5
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
Additional format string options in `~matplotlib.axes.Axes.bar_label`
2+
---------------------------------------------------------------------
3+
4+
The ``fmt`` argument of `~matplotlib.axes.Axes.bar_label` now accepts
5+
{}-style format strings:
6+
7+
.. code-block:: python
8+
9+
import matplotlib.pyplot as plt
10+
11+
fruit_names = ['Coffee', 'Salted Caramel', 'Pistachio']
12+
fruit_counts = [4000, 2000, 7000]
13+
14+
fig, ax = plt.subplots()
15+
bar_container = ax.bar(fruit_names, fruit_counts)
16+
ax.set(ylabel='pints sold', title='Gelato sales by flavor', ylim=(0, 8000))
17+
ax.bar_label(bar_container, fmt='{:,.0f}')
18+
19+
It also accepts callables:
20+
21+
.. code-block:: python
22+
23+
animal_names = ['Lion', 'Gazelle', 'Cheetah']
24+
mph_speed = [50, 60, 75]
25+
26+
fig, ax = plt.subplots()
27+
bar_container = ax.bar(animal_names, mph_speed)
28+
ax.set(ylabel='speed in MPH', title='Running speeds', ylim=(0, 80))
29+
ax.bar_label(
30+
bar_container, fmt=lambda x: '{:.1f} km/h'.format(x * 1.61)
31+
)

examples/lines_bars_and_markers/bar_label_demo.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,30 @@
9494

9595
plt.show()
9696

97+
###############################################################################
98+
# Bar labels using {}-style format string
99+
100+
fruit_names = ['Coffee', 'Salted Caramel', 'Pistachio']
101+
fruit_counts = [4000, 2000, 7000]
102+
103+
fig, ax = plt.subplots()
104+
bar_container = ax.bar(fruit_names, fruit_counts)
105+
ax.set(ylabel='pints sold', title='Gelato sales by flavor', ylim=(0, 8000))
106+
ax.bar_label(bar_container, fmt='{:,.0f}')
107+
108+
###############################################################################
109+
# Bar labels using a callable
110+
111+
animal_names = ['Lion', 'Gazelle', 'Cheetah']
112+
mph_speed = [50, 60, 75]
113+
114+
fig, ax = plt.subplots()
115+
bar_container = ax.bar(animal_names, mph_speed)
116+
ax.set(ylabel='speed in MPH', title='Running speeds', ylim=(0, 80))
117+
ax.bar_label(
118+
bar_container, fmt=lambda x: '{:.1f} km/h'.format(x * 1.61)
119+
)
120+
97121
#############################################################################
98122
#
99123
# .. admonition:: References

lib/matplotlib/axes/_axes.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2622,8 +2622,11 @@ def bar_label(self, container, labels=None, *, fmt="%g", label_type="edge",
26222622
A list of label texts, that should be displayed. If not given, the
26232623
label texts will be the data values formatted with *fmt*.
26242624
2625-
fmt : str, default: '%g'
2626-
A format string for the label.
2625+
fmt : str or callable, default: '%g'
2626+
An unnamed %-style or {}-style format string for the label or a
2627+
function to call with the value as the first argument.
2628+
When *fmt* is a string and can be interpreted in both formats,
2629+
%-style takes precedence over {}-style.
26272630
26282631
label_type : {'edge', 'center'}, default: 'edge'
26292632
The label type. Possible values:
@@ -2745,7 +2748,14 @@ def sign(x):
27452748
if np.isnan(dat):
27462749
lbl = ''
27472750

2748-
annotation = self.annotate(fmt % value if lbl is None else lbl,
2751+
if lbl is None:
2752+
if isinstance(fmt, str):
2753+
lbl = cbook._auto_format_str(fmt, value)
2754+
elif callable(fmt):
2755+
lbl = fmt(value)
2756+
else:
2757+
raise TypeError("fmt must be a str or callable")
2758+
annotation = self.annotate(lbl,
27492759
xy, xytext, textcoords="offset points",
27502760
ha=ha, va=va, **kwargs)
27512761
annotations.append(annotation)

lib/matplotlib/cbook/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2325,3 +2325,30 @@ def _unpack_to_numpy(x):
23252325
if isinstance(xtmp, np.ndarray):
23262326
return xtmp
23272327
return x
2328+
2329+
2330+
def _auto_format_str(fmt, value):
2331+
"""
2332+
Apply *value* to the format string *fmt*.
2333+
2334+
This works both with unnamed %-style formatting and
2335+
unnamed {}-style formatting. %-style formatting has priority.
2336+
If *fmt* is %-style formattable that will be used. Otherwise,
2337+
{}-formatting is applied. Strings without formatting placeholders
2338+
are passed through as is.
2339+
2340+
Examples
2341+
--------
2342+
>>> _auto_format_str('%.2f m', 0.2)
2343+
'0.20 m'
2344+
>>> _auto_format_str('{} m', 0.2)
2345+
'0.2 m'
2346+
>>> _auto_format_str('const', 0.2)
2347+
'const'
2348+
>>> _auto_format_str('%d or {}', 0.2)
2349+
'0 or {}'
2350+
"""
2351+
try:
2352+
return fmt % (value,)
2353+
except (TypeError, ValueError):
2354+
return fmt.format(value)

lib/matplotlib/tests/test_axes.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7813,14 +7813,24 @@ def test_bar_label_location_errorbars():
78137813
assert labels[1].get_va() == 'top'
78147814

78157815

7816-
def test_bar_label_fmt():
7816+
@pytest.mark.parametrize('fmt', [
7817+
'%.2f', '{:.2f}', '{:.2f}'.format
7818+
])
7819+
def test_bar_label_fmt(fmt):
78177820
ax = plt.gca()
78187821
rects = ax.bar([1, 2], [3, -4])
7819-
labels = ax.bar_label(rects, fmt='%.2f')
7822+
labels = ax.bar_label(rects, fmt=fmt)
78207823
assert labels[0].get_text() == '3.00'
78217824
assert labels[1].get_text() == '-4.00'
78227825

78237826

7827+
def test_bar_label_fmt_error():
7828+
ax = plt.gca()
7829+
rects = ax.bar([1, 2], [3, -4])
7830+
with pytest.raises(TypeError, match='str or callable'):
7831+
_ = ax.bar_label(rects, fmt=10)
7832+
7833+
78247834
def test_bar_label_labels():
78257835
ax = plt.gca()
78267836
rects = ax.bar([1, 2], [3, -4])

lib/matplotlib/tests/test_cbook.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,3 +895,19 @@ def test_safe_first_element_with_none():
895895
datetime_lst[0] = None
896896
actual = cbook._safe_first_non_none(datetime_lst)
897897
assert actual is not None and actual == datetime_lst[1]
898+
899+
900+
@pytest.mark.parametrize('fmt, value, result', [
901+
('%.2f m', 0.2, '0.20 m'),
902+
('{:.2f} m', 0.2, '0.20 m'),
903+
('{} m', 0.2, '0.2 m'),
904+
('const', 0.2, 'const'),
905+
('%d or {}', 0.2, '0 or {}'),
906+
('{{{:,.0f}}}', 2e5, '{200,000}'),
907+
('{:.2%}', 2/3, '66.67%'),
908+
('$%g', 2.54, '$2.54'),
909+
])
910+
def test_auto_format_str(fmt, value, result):
911+
"""Apply *value* to the format string *fmt*."""
912+
assert cbook._auto_format_str(fmt, value) == result
913+
assert cbook._auto_format_str(fmt, np.float64(value)) == result

0 commit comments

Comments
 (0)
0