Skip to content

Fix polar error bar cap orientation #28966

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
_AxesBase, _TransformedBoundsLocator, _process_plot_format)
from matplotlib.axes._secondary_axes import SecondaryAxis
from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer
from matplotlib.transforms import _ScaledRotation

_log = logging.getLogger(__name__)

Expand Down Expand Up @@ -3784,13 +3785,14 @@ def apply_mask(arrays, mask):
caplines[dep_axis].append(mlines.Line2D(
x_masked, y_masked, marker=marker, **eb_cap_style))
if self.name == 'polar':
trans_shift = self.transShift
for axis in caplines:
for l in caplines[axis]:
# Rotate caps to be perpendicular to the error bars
for theta, r in zip(l.get_xdata(), l.get_ydata()):
rotation = mtransforms.Affine2D().rotate(theta)
rotation = _ScaledRotation(theta=theta, trans_shift=trans_shift)
if axis == 'y':
rotation.rotate(-np.pi / 2)
rotation += mtransforms.Affine2D().rotate(np.pi / 2)
ms = mmarkers.MarkerStyle(marker=marker,
transform=rotation)
self.add_line(mlines.Line2D([theta], [r], marker=ms,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions lib/matplotlib/tests/test_polar.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,3 +481,21 @@ def test_polar_neg_theta_lims():
ax.set_thetalim(-np.pi, np.pi)
labels = [l.get_text() for l in ax.xaxis.get_ticklabels()]
assert labels == ['-180°', '-135°', '-90°', '-45°', '0°', '45°', '90°', '135°']


@pytest.mark.parametrize("order", ["before", "after"])
@image_comparison(baseline_images=['polar_errorbar'], remove_text=True,
extensions=['png'], style='mpl20')
def test_polar_errorbar(order):
theta = np.arange(0, 2 * np.pi, np.pi / 8)
r = theta / np.pi / 2 + 0.5
fig = plt.figure(figsize=(5, 5))
ax = fig.add_subplot(projection='polar')
if order == "before":
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen")
else:
ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen")
ax.set_theta_zero_location("N")
ax.set_theta_direction(-1)
27 changes: 26 additions & 1 deletion lib/matplotlib/tests/test_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.transforms as mtransforms
from matplotlib.transforms import Affine2D, Bbox, TransformedBbox
from matplotlib.transforms import Affine2D, Bbox, TransformedBbox, _ScaledRotation
from matplotlib.path import Path
from matplotlib.testing.decorators import image_comparison, check_figures_equal
from unittest.mock import MagicMock


class TestAffine2D:
Expand Down Expand Up @@ -1104,3 +1105,27 @@ def test_interval_contains_open():
assert not mtransforms.interval_contains_open((0, 1), -1)
assert not mtransforms.interval_contains_open((0, 1), 2)
assert mtransforms.interval_contains_open((1, 0), 0.5)


def test_scaledrotation_initialization():
"""Test that the ScaledRotation object is initialized correctly."""
theta = 1.0 # Arbitrary theta value for testing
trans_shift = MagicMock() # Mock the trans_shift transformation
scaled_rot = _ScaledRotation(theta, trans_shift)
assert scaled_rot._theta == theta
assert scaled_rot._trans_shift == trans_shift
assert scaled_rot._mtx is None


def test_scaledrotation_get_matrix_invalid():
"""Test get_matrix when the matrix is invalid and needs recalculation."""
theta = np.pi / 2
trans_shift = MagicMock(transform=MagicMock(return_value=[[theta, 0]]))
scaled_rot = _ScaledRotation(theta, trans_shift)
scaled_rot._invalid = True
matrix = scaled_rot.get_matrix()
trans_shift.transform.assert_called_once_with([[theta, 0]])
expected_rotation = np.array([[0, -1],
[1, 0]])
assert matrix is not None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
assert matrix is not None

Can be left out. According to typing, this is always a nd.arrray. And even if we don't trust that completely, matrix[:2, :2] in the next line would fail the test.

assert_allclose(matrix[:2, :2], expected_rotation, atol=1e-15)
19 changes: 19 additions & 0 deletions lib/matplotlib/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -2685,6 +2685,25 @@ def get_matrix(self):
return self._mtx


class _ScaledRotation(Affine2DBase):
"""
A transformation that applies rotation by *theta*, after transform by *trans_shift*.
"""
def __init__(self, theta, trans_shift):
super().__init__()
self._theta = theta
self._trans_shift = trans_shift
self._mtx = None

def get_matrix(self):
if self._invalid:
transformed_coords = self._trans_shift.transform([[self._theta, 0]])[0]
adjusted_theta = transformed_coords[0]
rotation = Affine2D().rotate(adjusted_theta)
self._mtx = rotation.get_matrix()
return self._mtx


class AffineDeltaTransform(Affine2DBase):
r"""
A transform wrapper for transforming displacements between pairs of points.
Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/transforms.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,8 @@ def offset_copy(
y: float = ...,
units: Literal["inches", "points", "dots"] = ...,
) -> Transform: ...


class _ScaledRotation(Affine2DBase):
def __init__(self, theta: float, trans_shift: Transform) -> None: ...
def get_matrix(self) -> np.ndarray: ...
Loading