Skip to content

Commit 907f78d

Browse files
committed
Curved polar errorbars
- uses _interpolation_steps - prefers transform MarkerStyle in init over _transform property - adjusted what's new - added more tests for overlapping, asymmetric and long errorbars - combine all tests to a single figure - remove overlappnig since it does not work same on all platforms - rework test figure, add overlapping, might work by avoiding grid - update what's new with image and link to example
1 parent 269c0b9 commit 907f78d

File tree

5 files changed

+95
-9
lines changed

5 files changed

+95
-9
lines changed
Lines changed: 7 additions & 0 deletions
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
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""
2+
=================================
3+
Error bar rendering on polar axis
4+
=================================
5+
6+
Demo of error bar plot in polar coordinates.
7+
"""
8+
import numpy as np
9+
import matplotlib.pyplot as plt
10+
11+
fig = plt.figure(figsize=(10, 10))
12+
ax = plt.subplot(111, projection='polar')
13+
theta = np.arange(0, 2*np.pi, np.pi / 4)
14+
r = theta / np.pi / 2 + 0.5
15+
ax.errorbar(theta, r, xerr=0.25, yerr=0.1, capsize=7, fmt="o")
16+
plt.show()
17+
18+
#############################################################################
19+
#
20+
# .. admonition:: References
21+
#
22+
# The use of the following functions, methods, classes and modules is shown
23+
# in this example:
24+
#
25+
# - `matplotlib.axes.Axes.errorbar` / `matplotlib.pyplot.errorbar`
26+
# - `matplotlib.projections.polar`

lib/matplotlib/axes/_axes.py

Lines changed: 27 additions & 9 deletions
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),
81.6 KB
Loading

lib/matplotlib/tests/test_axes.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3688,6 +3688,41 @@ def test_errorbar():
36883688
ax.set_title("Simplest errorbars, 0.2 in x, 0.4 in y")
36893689

36903690

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

36933728
f, ax = plt.subplots()

0 commit comments

Comments
 (0)