Skip to content

Commit 60ea7c6

Browse files
authored
Merge branch 'matplotlib:main' into logo_guide
2 parents a446629 + 40a47e5 commit 60ea7c6

File tree

9 files changed

+109
-27
lines changed

9 files changed

+109
-27
lines changed

doc/_static/switcher.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[
22
{
33
"name": "3.10 (stable)",
4-
"version": "3.10.3",
4+
"version": "3.10.5",
55
"url": "https://matplotlib.org/stable/",
66
"preferred": true
77
},
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
``BarContainer`` properties
2+
---------------------------
3+
`.BarContainer` gained new properties to easily access coordinates of the bars:
4+
5+
- `~.BarContainer.bottoms`
6+
- `~.BarContainer.tops`
7+
- `~.BarContainer.position_centers`
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Callable *valfmt* for ``Slider`` and ``RangeSlider``
2+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3+
4+
In addition to the existing %-format string, the *valfmt* parameter of
5+
`~.matplotlib.widgets.Slider` and `~.matplotlib.widgets.RangeSlider` now
6+
also accepts a callable of the form ``valfmt(val: float) -> str``.

lib/matplotlib/container.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,48 @@ def __init__(self, patches, errorbar=None, *, datavalues=None,
7373
self.orientation = orientation
7474
super().__init__(patches, **kwargs)
7575

76+
@property
77+
def bottoms(self):
78+
"""
79+
Return the values at the lower end of the bars.
80+
81+
.. versionadded:: 3.11
82+
"""
83+
if self.orientation == 'vertical':
84+
return [p.get_y() for p in self.patches]
85+
elif self.orientation == 'horizontal':
86+
return [p.get_x() for p in self.patches]
87+
else:
88+
raise ValueError("orientation must be 'vertical' or 'horizontal'.")
89+
90+
@property
91+
def tops(self):
92+
"""
93+
Return the values at the upper end of the bars.
94+
95+
.. versionadded:: 3.11
96+
"""
97+
if self.orientation == 'vertical':
98+
return [p.get_y() + p.get_height() for p in self.patches]
99+
elif self.orientation == 'horizontal':
100+
return [p.get_x() + p.get_width() for p in self.patches]
101+
else:
102+
raise ValueError("orientation must be 'vertical' or 'horizontal'.")
103+
104+
@property
105+
def position_centers(self):
106+
"""
107+
Return the centers of bar positions.
108+
109+
.. versionadded:: 3.11
110+
"""
111+
if self.orientation == 'vertical':
112+
return [p.get_x() + p.get_width() / 2 for p in self.patches]
113+
elif self.orientation == 'horizontal':
114+
return [p.get_y() + p.get_height() / 2 for p in self.patches]
115+
else:
116+
raise ValueError("orientation must be 'vertical' or 'horizontal'.")
117+
76118

77119
class ErrorbarContainer(Container):
78120
"""

lib/matplotlib/container.pyi

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ class BarContainer(Container):
3232
orientation: Literal["vertical", "horizontal"] | None = ...,
3333
**kwargs
3434
) -> None: ...
35+
@property
36+
def bottoms(self) -> list[float]: ...
37+
@property
38+
def tops(self) -> list[float]: ...
39+
@property
40+
def position_centers(self) -> list[float]: ...
3541

3642
class ErrorbarContainer(Container):
3743
lines: tuple[Line2D, tuple[Line2D, ...], tuple[LineCollection, ...]]

lib/matplotlib/tests/test_backend_qt.py

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from matplotlib import _c_internal_utils
1616

1717
try:
18+
from matplotlib.backends.qt_compat import QtCore # type: ignore[attr-defined]
1819
from matplotlib.backends.qt_compat import QtGui # type: ignore[attr-defined] # noqa: E501, F401
1920
from matplotlib.backends.qt_compat import QtWidgets # type: ignore[attr-defined]
2021
from matplotlib.backends.qt_editor import _formlayout
@@ -25,12 +26,6 @@
2526
_test_timeout = 60 # A reasonably safe value for slower architectures.
2627

2728

28-
@pytest.fixture
29-
def qt_core(request):
30-
from matplotlib.backends.qt_compat import QtCore
31-
return QtCore
32-
33-
3429
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
3530
def test_fig_close():
3631

@@ -101,7 +96,7 @@ def test_fig_close():
10196
'QtAgg',
10297
marks=pytest.mark.backend('QtAgg', skip_on_importerror=True)),
10398
])
104-
def test_correct_key(backend, qt_core, qt_key, qt_mods, answer, monkeypatch):
99+
def test_correct_key(backend, qt_key, qt_mods, answer, monkeypatch):
105100
"""
106101
Make a figure.
107102
Send a key_press_event event (using non-public, qtX backend specific api).
@@ -137,7 +132,7 @@ def on_key_press(event):
137132

138133

139134
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
140-
def test_device_pixel_ratio_change(qt_core):
135+
def test_device_pixel_ratio_change():
141136
"""
142137
Make sure that if the pixel ratio changes, the figure dpi changes but the
143138
widget remains the same logical size.
@@ -155,12 +150,11 @@ def set_device_pixel_ratio(ratio):
155150
p.return_value = ratio
156151

157152
window = qt_canvas.window().windowHandle()
158-
current_version = tuple(
159-
int(x) for x in qt_core.qVersion().split('.', 2)[:2])
153+
current_version = tuple(int(x) for x in QtCore.qVersion().split('.', 2)[:2])
160154
if current_version >= (6, 6):
161-
qt_core.QCoreApplication.sendEvent(
155+
QtCore.QCoreApplication.sendEvent(
162156
window,
163-
qt_core.QEvent(qt_core.QEvent.Type.DevicePixelRatioChange))
157+
QtCore.QEvent(QtCore.QEvent.Type.DevicePixelRatioChange))
164158
else:
165159
# The value here doesn't matter, as we can't mock the C++ QScreen
166160
# object, but can override the functional wrapper around it.
@@ -342,7 +336,7 @@ def _get_testable_qt_backends():
342336

343337

344338
@pytest.mark.backend('QtAgg', skip_on_importerror=True)
345-
def test_fig_sigint_override(qt_core):
339+
def test_fig_sigint_override():
346340
from matplotlib.backends.backend_qt5 import _BackendQT5
347341
# Create a figure
348342
plt.figure()
@@ -357,10 +351,10 @@ def fire_signal_and_quit():
357351
event_loop_handler = signal.getsignal(signal.SIGINT)
358352

359353
# Request event loop exit
360-
qt_core.QCoreApplication.exit()
354+
QtCore.QCoreApplication.exit()
361355

362356
# Timer to exit event loop
363-
qt_core.QTimer.singleShot(0, fire_signal_and_quit)
357+
QtCore.QTimer.singleShot(0, fire_signal_and_quit)
364358

365359
# Save original SIGINT handler
366360
original_handler = signal.getsignal(signal.SIGINT)
@@ -385,7 +379,7 @@ def custom_handler(signum, frame):
385379

386380
# Repeat again to test that SIG_DFL and SIG_IGN will not be overridden
387381
for custom_handler in (signal.SIG_DFL, signal.SIG_IGN):
388-
qt_core.QTimer.singleShot(0, fire_signal_and_quit)
382+
QtCore.QTimer.singleShot(0, fire_signal_and_quit)
389383
signal.signal(signal.SIGINT, custom_handler)
390384

391385
_BackendQT5.mainloop()

lib/matplotlib/tests/test_container.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
from numpy.testing import assert_array_equal
23
import matplotlib.pyplot as plt
34

45

@@ -35,3 +36,20 @@ def test_nonstring_label():
3536
# Test for #26824
3637
plt.bar(np.arange(10), np.random.rand(10), label=1)
3738
plt.legend()
39+
40+
41+
def test_barcontainer_position_centers__bottoms__tops():
42+
fig, ax = plt.subplots()
43+
pos = [1, 2, 4]
44+
bottoms = np.array([1, 5, 3])
45+
heights = np.array([2, 3, 4])
46+
47+
container = ax.bar(pos, heights, bottom=bottoms)
48+
assert_array_equal(container.position_centers, pos)
49+
assert_array_equal(container.bottoms, bottoms)
50+
assert_array_equal(container.tops, bottoms + heights)
51+
52+
container = ax.barh(pos, heights, left=bottoms)
53+
assert_array_equal(container.position_centers, pos)
54+
assert_array_equal(container.bottoms, bottoms)
55+
assert_array_equal(container.tops, bottoms + heights)

lib/matplotlib/widgets.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -370,8 +370,9 @@ def __init__(self, ax, label, valmin, valmax, *, valinit=0.5, valfmt=None,
370370
The slider initial position.
371371
372372
valfmt : str, default: None
373-
%-format string used to format the slider value. If None, a
374-
`.ScalarFormatter` is used instead.
373+
The way to format the slider value. If a string, it must be in %-format.
374+
If a callable, it must have the signature ``valfmt(val: float) -> str``.
375+
If None, a `.ScalarFormatter` is used.
375376
376377
closedmin : bool, default: True
377378
Whether the slider interval is closed on the bottom.
@@ -553,7 +554,10 @@ def _update(self, event):
553554
def _format(self, val):
554555
"""Pretty-print *val*."""
555556
if self.valfmt is not None:
556-
return self.valfmt % val
557+
if callable(self.valfmt):
558+
return self.valfmt(val)
559+
else:
560+
return self.valfmt % val
557561
else:
558562
_, s, _ = self._fmt.format_ticks([self.valmin, val, self.valmax])
559563
# fmt.get_offset is actually the multiplicative factor, if any.
@@ -650,9 +654,11 @@ def __init__(
650654
The initial positions of the slider. If None the initial positions
651655
will be at the 25th and 75th percentiles of the range.
652656
653-
valfmt : str, default: None
654-
%-format string used to format the slider values. If None, a
655-
`.ScalarFormatter` is used instead.
657+
valfmt : str or callable, default: None
658+
The way to format the range's minimal and maximal values. If a
659+
string, it must be in %-format. If a callable, it must have the
660+
signature ``valfmt(val: float) -> str``. If None, a
661+
`.ScalarFormatter` is used.
656662
657663
closedmin : bool, default: True
658664
Whether the slider interval is closed on the bottom.
@@ -896,7 +902,10 @@ def _update(self, event):
896902
def _format(self, val):
897903
"""Pretty-print *val*."""
898904
if self.valfmt is not None:
899-
return f"({self.valfmt % val[0]}, {self.valfmt % val[1]})"
905+
if callable(self.valfmt):
906+
return f"({self.valfmt(val[0])}, {self.valfmt(val[1])})"
907+
else:
908+
return f"({self.valfmt % val[0]}, {self.valfmt % val[1]})"
900909
else:
901910
_, s1, s2, _ = self._fmt.format_ticks(
902911
[self.valmin, *val, self.valmax]

lib/matplotlib/widgets.pyi

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ class SliderBase(AxesWidget):
6464
valmax: float
6565
valstep: float | ArrayLike | None
6666
drag_active: bool
67-
valfmt: str
67+
valfmt: str | Callable[[float], str] | None
6868
def __init__(
6969
self,
7070
ax: Axes,
@@ -73,7 +73,7 @@ class SliderBase(AxesWidget):
7373
closedmax: bool,
7474
valmin: float,
7575
valmax: float,
76-
valfmt: str,
76+
valfmt: str | Callable[[float], str] | None,
7777
dragging: Slider | None,
7878
valstep: float | ArrayLike | None,
7979
) -> None: ...
@@ -130,7 +130,7 @@ class RangeSlider(SliderBase):
130130
valmax: float,
131131
*,
132132
valinit: tuple[float, float] | None = ...,
133-
valfmt: str | None = ...,
133+
valfmt: str | Callable[[float], str] | None = ...,
134134
closedmin: bool = ...,
135135
closedmax: bool = ...,
136136
dragging: bool = ...,

0 commit comments

Comments
 (0)