Skip to content

Commit 604cb9f

Browse files
committed
Fix polar error bar cap orientation
1 parent fbf1611 commit 604cb9f

File tree

6 files changed

+73
-3
lines changed

6 files changed

+73
-3
lines changed

lib/matplotlib/axes/_axes.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
_AxesBase, _TransformedBoundsLocator, _process_plot_format)
3838
from matplotlib.axes._secondary_axes import SecondaryAxis
3939
from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer
40+
from matplotlib.transforms import ScaledRotation
4041

4142
_log = logging.getLogger(__name__)
4243

@@ -3784,13 +3785,14 @@ def apply_mask(arrays, mask):
37843785
caplines[dep_axis].append(mlines.Line2D(
37853786
x_masked, y_masked, marker=marker, **eb_cap_style))
37863787
if self.name == 'polar':
3788+
trans_shift = self.transShift
37873789
for axis in caplines:
37883790
for l in caplines[axis]:
37893791
# Rotate caps to be perpendicular to the error bars
37903792
for theta, r in zip(l.get_xdata(), l.get_ydata()):
3791-
rotation = mtransforms.Affine2D().rotate(theta)
3793+
rotation = ScaledRotation(theta=theta, trans_shift=trans_shift)
37923794
if axis == 'y':
3793-
rotation.rotate(-np.pi / 2)
3795+
rotation += mtransforms.Affine2D().rotate(np.pi / 2)
37943796
ms = mmarkers.MarkerStyle(marker=marker,
37953797
transform=rotation)
37963798
self.add_line(mlines.Line2D([theta], [r], marker=ms,

lib/matplotlib/tests/test_polar.py

+18
Original file line numberDiff line numberDiff line change
@@ -481,3 +481,21 @@ def test_polar_neg_theta_lims():
481481
ax.set_thetalim(-np.pi, np.pi)
482482
labels = [l.get_text() for l in ax.xaxis.get_ticklabels()]
483483
assert labels == ['-180°', '-135°', '-90°', '-45°', '0°', '45°', '90°', '135°']
484+
485+
486+
@pytest.mark.parametrize("order", ["before", "after"])
487+
@image_comparison(baseline_images=['polar_errorbar'], remove_text=True,
488+
extensions=['png'], style='mpl20')
489+
def test_polar_errorbar(order):
490+
theta = np.arange(0, 2 * np.pi, np.pi / 8)
491+
r = theta / np.pi / 2 + 0.5
492+
fig = plt.figure(figsize=(5, 5))
493+
ax = fig.add_subplot(projection='polar')
494+
if order == "before":
495+
ax.set_theta_zero_location("N")
496+
ax.set_theta_direction(-1)
497+
ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen")
498+
else:
499+
ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen")
500+
ax.set_theta_zero_location("N")
501+
ax.set_theta_direction(-1)

lib/matplotlib/tests/test_transforms.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
import matplotlib.pyplot as plt
1010
import matplotlib.patches as mpatches
1111
import matplotlib.transforms as mtransforms
12-
from matplotlib.transforms import Affine2D, Bbox, TransformedBbox
12+
from matplotlib.transforms import Affine2D, Bbox, TransformedBbox, ScaledRotation
1313
from matplotlib.path import Path
1414
from matplotlib.testing.decorators import image_comparison, check_figures_equal
15+
from unittest.mock import MagicMock
1516

1617

1718
class TestAffine2D:
@@ -1104,3 +1105,27 @@ def test_interval_contains_open():
11041105
assert not mtransforms.interval_contains_open((0, 1), -1)
11051106
assert not mtransforms.interval_contains_open((0, 1), 2)
11061107
assert mtransforms.interval_contains_open((1, 0), 0.5)
1108+
1109+
1110+
def test_scaledrotation_initialization():
1111+
"""Test that the ScaledRotation object is initialized correctly."""
1112+
theta = 1.0 # Arbitrary theta value for testing
1113+
trans_shift = MagicMock() # Mock the trans_shift transformation
1114+
scaled_rot = ScaledRotation(theta, trans_shift)
1115+
assert scaled_rot._theta == theta
1116+
assert scaled_rot._trans_shift == trans_shift
1117+
assert scaled_rot._mtx is None
1118+
1119+
1120+
def test_scaledrotation_get_matrix_invalid():
1121+
"""Test get_matrix when the matrix is invalid and needs recalculation."""
1122+
theta = np.pi / 2
1123+
trans_shift = MagicMock(transform=MagicMock(return_value=[[theta, 0]]))
1124+
scaled_rot = ScaledRotation(theta, trans_shift)
1125+
scaled_rot._invalid = True
1126+
matrix = scaled_rot.get_matrix()
1127+
trans_shift.transform.assert_called_once_with([[theta, 0]])
1128+
expected_rotation = np.array([[0, -1],
1129+
[1, 0]])
1130+
assert matrix is not None
1131+
assert_allclose(matrix[:2, :2], expected_rotation, atol=1e-15)

lib/matplotlib/transforms.py

+20
Original file line numberDiff line numberDiff line change
@@ -2685,6 +2685,26 @@ def get_matrix(self):
26852685
return self._mtx
26862686

26872687

2688+
class ScaledRotation(Affine2DBase):
2689+
"""
2690+
A transformation that applies offset and direction
2691+
based on *trans_shift*.
2692+
"""
2693+
def __init__(self, theta, trans_shift):
2694+
super().__init__()
2695+
self._theta = theta
2696+
self._trans_shift = trans_shift
2697+
self._mtx = None
2698+
2699+
def get_matrix(self):
2700+
if self._invalid:
2701+
transformed_coords = self._trans_shift.transform([[self._theta, 0]])[0]
2702+
adjusted_theta = transformed_coords[0]
2703+
rotation = Affine2D().rotate(adjusted_theta)
2704+
self._mtx = rotation.get_matrix()
2705+
return self._mtx
2706+
2707+
26882708
class AffineDeltaTransform(Affine2DBase):
26892709
r"""
26902710
A transform wrapper for transforming displacements between pairs of points.

lib/matplotlib/transforms.pyi

+5
Original file line numberDiff line numberDiff line change
@@ -334,3 +334,8 @@ def offset_copy(
334334
y: float = ...,
335335
units: Literal["inches", "points", "dots"] = ...,
336336
) -> Transform: ...
337+
338+
339+
class ScaledRotation(Affine2DBase):
340+
def __init__(self, theta: float, trans_shift: Transform) -> None: ...
341+
def get_matrix(self) -> np.ndarray: ...

0 commit comments

Comments
 (0)