Skip to content

Polar errcaps #23592

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 2 commits into from
Oct 7, 2022
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
7 changes: 7 additions & 0 deletions doc/users/next_whats_new/polar_errorbar_caps.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Fixed errorbars in polar plots
------------------------------
Caps and error lines are now drawn with respect to polar coordinates,
when plotting errorbars on polar plots.

.. figure:: /gallery/pie_and_polar_charts/images/sphx_glr_polar_error_caps_001.png
Copy link
Member

Choose a reason for hiding this comment

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

If this works, great! (not sure it will, but my ignorance of sphinx is relatively vast)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just mimicked it from this what's new message, but it does not seem to work properly in the built documentation, so I am not sure it is worth the try.

:target: ../../gallery/pie_and_polar_charts/polar_error_caps.html
53 changes: 53 additions & 0 deletions examples/pie_and_polar_charts/polar_error_caps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
=================================
Error bar rendering on polar axis
=================================

Demo of error bar plot in polar coordinates.
Theta error bars are curved lines ended with caps oriented towards the
center.
Radius error bars are straight lines oriented towards center with
perpendicular caps.
"""
import numpy as np
import matplotlib.pyplot as plt

theta = np.arange(0, 2 * np.pi, np.pi / 4)
r = theta / np.pi / 2 + 0.5

fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(projection='polar')
ax.errorbar(theta, r, xerr=0.25, yerr=0.1, capsize=7, fmt="o", c="seagreen")
ax.set_title("Pretty polar error bars")
plt.show()

#############################################################################
# Please acknowledge that large theta error bars will be overlapping.
# This may reduce readability of the output plot. See example figure below:

fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(projection='polar')
ax.errorbar(theta, r, xerr=5.25, yerr=0.1, capsize=7, fmt="o", c="darkred")
ax.set_title("Overlapping theta error bars")
plt.show()

#############################################################################
# On the other hand, large radius error bars will never overlap, they just
# lead to unwanted scale in the data, reducing the displayed range.

fig = plt.figure(figsize=(10, 10))
ax = fig.add_subplot(projection='polar')
ax.errorbar(theta, r, xerr=0.25, yerr=10.1, capsize=7, fmt="o", c="orangered")
ax.set_title("Large radius error bars")
plt.show()


#############################################################################
#
# .. admonition:: References
#
# The use of the following functions, methods, classes and modules is shown
# in this example:
#
# - `matplotlib.axes.Axes.errorbar` / `matplotlib.pyplot.errorbar`
# - `matplotlib.projections.polar`
36 changes: 27 additions & 9 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3573,10 +3573,11 @@ def _upcast_err(err):
eb_cap_style['color'] = ecolor

barcols = []
caplines = []
caplines = {'x': [], 'y': []}

# Vectorized fancy-indexer.
def apply_mask(arrays, mask): return [array[mask] for array in arrays]
def apply_mask(arrays, mask):
return [array[mask] for array in arrays]

# dep: dependent dataset, indep: independent dataset
for (dep_axis, dep, err, lolims, uplims, indep, lines_func,
Expand Down Expand Up @@ -3607,9 +3608,12 @@ def apply_mask(arrays, mask): return [array[mask] for array in arrays]
# return dep - elow * ~lolims, dep + ehigh * ~uplims
# except that broadcast_to would strip units.
low, high = dep + np.row_stack([-(1 - lolims), 1 - uplims]) * err

barcols.append(lines_func(
*apply_mask([indep, low, high], everymask), **eb_lines_style))
if self.name == "polar" and dep_axis == "x":
for b in barcols:
for p in b.get_paths():
p._interpolation_steps = 2
# Normal errorbars for points without upper/lower limits.
nolims = ~(lolims | uplims)
if nolims.any() and capsize > 0:
Expand All @@ -3622,7 +3626,7 @@ def apply_mask(arrays, mask): return [array[mask] for array in arrays]
line = mlines.Line2D(indep_masked, indep_masked,
marker=marker, **eb_cap_style)
line.set(**{f"{dep_axis}data": lh_masked})
caplines.append(line)
caplines[dep_axis].append(line)
for idx, (lims, hl) in enumerate([(lolims, high), (uplims, low)]):
if not lims.any():
continue
Expand All @@ -3636,15 +3640,29 @@ def apply_mask(arrays, mask): return [array[mask] for array in arrays]
line = mlines.Line2D(x_masked, y_masked,
marker=hlmarker, **eb_cap_style)
line.set(**{f"{dep_axis}data": hl_masked})
caplines.append(line)
caplines[dep_axis].append(line)
if capsize > 0:
caplines.append(mlines.Line2D(
caplines[dep_axis].append(mlines.Line2D(
x_masked, y_masked, marker=marker, **eb_cap_style))

for l in caplines:
self.add_line(l)
if self.name == 'polar':
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)
if axis == 'y':
rotation.rotate(-np.pi / 2)
ms = mmarkers.MarkerStyle(marker=marker,
transform=rotation)
self.add_line(mlines.Line2D([theta], [r], marker=ms,
**eb_cap_style))
else:
for axis in caplines:
for l in caplines[axis]:
self.add_line(l)

self._request_autoscale_view()
caplines = caplines['x'] + caplines['y']
errorbar_container = ErrorbarContainer(
(data_line, tuple(caplines), tuple(barcols)),
has_xerr=(xerr is not None), has_yerr=(yerr is not None),
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
35 changes: 35 additions & 0 deletions lib/matplotlib/tests/test_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -3688,6 +3688,41 @@ def test_errorbar():
ax.set_title("Simplest errorbars, 0.2 in x, 0.4 in y")


@image_comparison(['mixed_errorbar_polar_caps'], extensions=['png'],
remove_text=True)
def test_mixed_errorbar_polar_caps():
"""
Mix several polar errorbar use cases in a single test figure.

It is advisable to position individual points off the grid. If there are
problems with reproducibility of this test, consider removing grid.
"""
fig = plt.figure()
ax = plt.subplot(111, projection='polar')

# symmetric errorbars
th_sym = [1, 2, 3]
r_sym = [0.9]*3
ax.errorbar(th_sym, r_sym, xerr=0.35, yerr=0.2, fmt="o")

# long errorbars
th_long = [np.pi/2 + .1, np.pi + .1]
r_long = [1.8, 2.2]
ax.errorbar(th_long, r_long, xerr=0.8 * np.pi, yerr=0.15, fmt="o")

# asymmetric errorbars
th_asym = [4*np.pi/3 + .1, 5*np.pi/3 + .1, 2*np.pi-0.1]
r_asym = [1.1]*3
xerr = [[.3, .3, .2], [.2, .3, .3]]
yerr = [[.35, .5, .5], [.5, .35, .5]]
ax.errorbar(th_asym, r_asym, xerr=xerr, yerr=yerr, fmt="o")

# overlapping errorbar
th_over = [2.1]
r_over = [3.1]
ax.errorbar(th_over, r_over, xerr=10, yerr=.2, fmt="o")


def test_errorbar_colorcycle():

f, ax = plt.subplots()
Expand Down