Skip to content

Commit 2393866

Browse files
authored
Merge pull request #23592 from deep-jkl/polar-errcaps
Polar errcaps
2 parents 4965500 + b7c27d5 commit 2393866

File tree

5 files changed

+122
-9
lines changed

5 files changed

+122
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Fixed errorbars in polar plots
2+
------------------------------
3+
Caps and error lines are now drawn with respect to polar coordinates,
4+
when plotting errorbars on polar plots.
5+
6+
.. figure:: /gallery/pie_and_polar_charts/images/sphx_glr_polar_error_caps_001.png
7+
:target: ../../gallery/pie_and_polar_charts/polar_error_caps.html
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
=================================
3+
Error bar rendering on polar axis
4+
=================================
5+
6+
Demo of error bar plot in polar coordinates.
7+
Theta error bars are curved lines ended with caps oriented towards the
8+
center.
9+
Radius error bars are straight lines oriented towards center with
10+
perpendicular caps.
11+
"""
12+
import numpy as np
13+
import matplotlib.pyplot as plt
14+
15+
theta = np.arange(0, 2 * np.pi, np.pi / 4)
16+
r = theta / np.pi / 2 + 0.5
17+
18+
fig = plt.figure(figsize=(10, 10))
19+
ax = fig.add_subplot(projection='polar')
20+
ax.errorbar(theta, r, xerr=0.25, yerr=0.1, capsize=7, fmt="o", c="seagreen")
21+
ax.set_title("Pretty polar error bars")
22+
plt.show()
23+
24+
#############################################################################
25+
# Please acknowledge that large theta error bars will be overlapping.
26+
# This may reduce readability of the output plot. See example figure below:
27+
28+
fig = plt.figure(figsize=(10, 10))
29+
ax = fig.add_subplot(projection='polar')
30+
ax.errorbar(theta, r, xerr=5.25, yerr=0.1, capsize=7, fmt="o", c="darkred")
31+
ax.set_title("Overlapping theta error bars")
32+
plt.show()
33+
34+
#############################################################################
35+
# On the other hand, large radius error bars will never overlap, they just
36+
# lead to unwanted scale in the data, reducing the displayed range.
37+
38+
fig = plt.figure(figsize=(10, 10))
39+
ax = fig.add_subplot(projection='polar')
40+
ax.errorbar(theta, r, xerr=0.25, yerr=10.1, capsize=7, fmt="o", c="orangered")
41+
ax.set_title("Large radius error bars")
42+
plt.show()
43+
44+
45+
#############################################################################
46+
#
47+
# .. admonition:: References
48+
#
49+
# The use of the following functions, methods, classes and modules is shown
50+
# in this example:
51+
#
52+
# - `matplotlib.axes.Axes.errorbar` / `matplotlib.pyplot.errorbar`
53+
# - `matplotlib.projections.polar`

lib/matplotlib/axes/_axes.py

+27-9
Original file line numberDiff line numberDiff line change
@@ -3573,10 +3573,11 @@ def _upcast_err(err):
35733573
eb_cap_style['color'] = ecolor
35743574

35753575
barcols = []
3576-
caplines = []
3576+
caplines = {'x': [], 'y': []}
35773577

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

35813582
# dep: dependent dataset, indep: independent dataset
35823583
for (dep_axis, dep, err, lolims, uplims, indep, lines_func,
@@ -3607,9 +3608,12 @@ def apply_mask(arrays, mask): return [array[mask] for array in arrays]
36073608
# return dep - elow * ~lolims, dep + ehigh * ~uplims
36083609
# except that broadcast_to would strip units.
36093610
low, high = dep + np.row_stack([-(1 - lolims), 1 - uplims]) * err
3610-
36113611
barcols.append(lines_func(
36123612
*apply_mask([indep, low, high], everymask), **eb_lines_style))
3613+
if self.name == "polar" and dep_axis == "x":
3614+
for b in barcols:
3615+
for p in b.get_paths():
3616+
p._interpolation_steps = 2
36133617
# Normal errorbars for points without upper/lower limits.
36143618
nolims = ~(lolims | uplims)
36153619
if nolims.any() and capsize > 0:
@@ -3622,7 +3626,7 @@ def apply_mask(arrays, mask): return [array[mask] for array in arrays]
36223626
line = mlines.Line2D(indep_masked, indep_masked,
36233627
marker=marker, **eb_cap_style)
36243628
line.set(**{f"{dep_axis}data": lh_masked})
3625-
caplines.append(line)
3629+
caplines[dep_axis].append(line)
36263630
for idx, (lims, hl) in enumerate([(lolims, high), (uplims, low)]):
36273631
if not lims.any():
36283632
continue
@@ -3636,15 +3640,29 @@ def apply_mask(arrays, mask): return [array[mask] for array in arrays]
36363640
line = mlines.Line2D(x_masked, y_masked,
36373641
marker=hlmarker, **eb_cap_style)
36383642
line.set(**{f"{dep_axis}data": hl_masked})
3639-
caplines.append(line)
3643+
caplines[dep_axis].append(line)
36403644
if capsize > 0:
3641-
caplines.append(mlines.Line2D(
3645+
caplines[dep_axis].append(mlines.Line2D(
36423646
x_masked, y_masked, marker=marker, **eb_cap_style))
3643-
3644-
for l in caplines:
3645-
self.add_line(l)
3647+
if self.name == 'polar':
3648+
for axis in caplines:
3649+
for l in caplines[axis]:
3650+
# Rotate caps to be perpendicular to the error bars
3651+
for theta, r in zip(l.get_xdata(), l.get_ydata()):
3652+
rotation = mtransforms.Affine2D().rotate(theta)
3653+
if axis == 'y':
3654+
rotation.rotate(-np.pi / 2)
3655+
ms = mmarkers.MarkerStyle(marker=marker,
3656+
transform=rotation)
3657+
self.add_line(mlines.Line2D([theta], [r], marker=ms,
3658+
**eb_cap_style))
3659+
else:
3660+
for axis in caplines:
3661+
for l in caplines[axis]:
3662+
self.add_line(l)
36463663

36473664
self._request_autoscale_view()
3665+
caplines = caplines['x'] + caplines['y']
36483666
errorbar_container = ErrorbarContainer(
36493667
(data_line, tuple(caplines), tuple(barcols)),
36503668
has_xerr=(xerr is not None), has_yerr=(yerr is not None),

lib/matplotlib/tests/test_axes.py

+35
Original file line numberDiff line numberDiff line change
@@ -3687,6 +3687,41 @@ def test_errorbar():
36873687
ax.set_title("Simplest errorbars, 0.2 in x, 0.4 in y")
36883688

36893689

3690+
@image_comparison(['mixed_errorbar_polar_caps'], extensions=['png'],
3691+
remove_text=True)
3692+
def test_mixed_errorbar_polar_caps():
3693+
"""
3694+
Mix several polar errorbar use cases in a single test figure.
3695+
3696+
It is advisable to position individual points off the grid. If there are
3697+
problems with reproducibility of this test, consider removing grid.
3698+
"""
3699+
fig = plt.figure()
3700+
ax = plt.subplot(111, projection='polar')
3701+
3702+
# symmetric errorbars
3703+
th_sym = [1, 2, 3]
3704+
r_sym = [0.9]*3
3705+
ax.errorbar(th_sym, r_sym, xerr=0.35, yerr=0.2, fmt="o")
3706+
3707+
# long errorbars
3708+
th_long = [np.pi/2 + .1, np.pi + .1]
3709+
r_long = [1.8, 2.2]
3710+
ax.errorbar(th_long, r_long, xerr=0.8 * np.pi, yerr=0.15, fmt="o")
3711+
3712+
# asymmetric errorbars
3713+
th_asym = [4*np.pi/3 + .1, 5*np.pi/3 + .1, 2*np.pi-0.1]
3714+
r_asym = [1.1]*3
3715+
xerr = [[.3, .3, .2], [.2, .3, .3]]
3716+
yerr = [[.35, .5, .5], [.5, .35, .5]]
3717+
ax.errorbar(th_asym, r_asym, xerr=xerr, yerr=yerr, fmt="o")
3718+
3719+
# overlapping errorbar
3720+
th_over = [2.1]
3721+
r_over = [3.1]
3722+
ax.errorbar(th_over, r_over, xerr=10, yerr=.2, fmt="o")
3723+
3724+
36903725
def test_errorbar_colorcycle():
36913726

36923727
f, ax = plt.subplots()

0 commit comments

Comments
 (0)