Skip to content

Commit cf7ca12

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
1 parent ecab6de commit cf7ca12

File tree

7 files changed

+77
-9
lines changed

7 files changed

+77
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
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.

lib/matplotlib/axes/_axes.py

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

35243524
barcols = []
3525-
caplines = []
3525+
caplines = {'x': [], 'y': []}
35263526

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

35303531
# dep: dependent dataset, indep: independent dataset
35313532
for (dep_axis, dep, err, lolims, uplims, indep, lines_func,
@@ -3556,9 +3557,12 @@ def apply_mask(arrays, mask): return [array[mask] for array in arrays]
35563557
# return dep - elow * ~lolims, dep + ehigh * ~uplims
35573558
# except that broadcast_to would strip units.
35583559
low, high = dep + np.row_stack([-(1 - lolims), 1 - uplims]) * err
3559-
35603560
barcols.append(lines_func(
35613561
*apply_mask([indep, low, high], everymask), **eb_lines_style))
3562+
if self.name == "polar" and dep_axis == "x":
3563+
for b in barcols:
3564+
for p in b.get_paths():
3565+
p._interpolation_steps = 2
35623566
# Normal errorbars for points without upper/lower limits.
35633567
nolims = ~(lolims | uplims)
35643568
if nolims.any() and capsize > 0:
@@ -3571,7 +3575,7 @@ def apply_mask(arrays, mask): return [array[mask] for array in arrays]
35713575
line = mlines.Line2D(indep_masked, indep_masked,
35723576
marker=marker, **eb_cap_style)
35733577
line.set(**{f"{dep_axis}data": lh_masked})
3574-
caplines.append(line)
3578+
caplines[dep_axis].append(line)
35753579
for idx, (lims, hl) in enumerate([(lolims, high), (uplims, low)]):
35763580
if not lims.any():
35773581
continue
@@ -3585,15 +3589,29 @@ def apply_mask(arrays, mask): return [array[mask] for array in arrays]
35853589
line = mlines.Line2D(x_masked, y_masked,
35863590
marker=hlmarker, **eb_cap_style)
35873591
line.set(**{f"{dep_axis}data": hl_masked})
3588-
caplines.append(line)
3592+
caplines[dep_axis].append(line)
35893593
if capsize > 0:
3590-
caplines.append(mlines.Line2D(
3594+
caplines[dep_axis].append(mlines.Line2D(
35913595
x_masked, y_masked, marker=marker, **eb_cap_style))
3592-
3593-
for l in caplines:
3594-
self.add_line(l)
3596+
if self.name == 'polar':
3597+
for axis in caplines:
3598+
for l in caplines[axis]:
3599+
# Rotate caps to be perpendicular to the error bars
3600+
for theta, r in zip(l.get_xdata(), l.get_ydata()):
3601+
rotation = mtransforms.Affine2D().rotate(theta)
3602+
if axis == 'y':
3603+
rotation.rotate(-np.pi / 2)
3604+
ms = mmarkers.MarkerStyle(marker=marker,
3605+
transform=rotation)
3606+
self.add_line(mlines.Line2D([theta], [r], marker=ms,
3607+
**eb_cap_style))
3608+
else:
3609+
for axis in caplines:
3610+
for l in caplines[axis]:
3611+
self.add_line(l)
35953612

35963613
self._request_autoscale_view()
3614+
caplines = caplines['x'] + caplines['y']
35973615
errorbar_container = ErrorbarContainer(
35983616
(data_line, tuple(caplines), tuple(barcols)),
35993617
has_xerr=(xerr is not None), has_yerr=(yerr is not None),

lib/matplotlib/tests/test_axes.py

+46
Original file line numberDiff line numberDiff line change
@@ -3600,6 +3600,52 @@ def test_errorbar():
36003600
ax.set_title("Simplest errorbars, 0.2 in x, 0.4 in y")
36013601

36023602

3603+
@image_comparison(['even_errorbar_polar_caps'], extensions=['png'],
3604+
remove_text=True)
3605+
def test_even_errorbar_polar_caps():
3606+
fig = plt.figure()
3607+
ax = plt.subplot(111, projection='polar')
3608+
theta = np.arange(0, 2*np.pi, np.pi / 4)
3609+
r = theta / np.pi / 2 + 0.5
3610+
ax.errorbar(theta, r, xerr=0.15, yerr=0.1, fmt="o")
3611+
ax.set_rlim(0, 2)
3612+
3613+
3614+
@image_comparison(['long_errorbar_polar_caps'], extensions=['png'],
3615+
remove_text=True)
3616+
def test_long_errorbar_polar_caps():
3617+
fig = plt.figure()
3618+
ax = plt.subplot(111, projection='polar')
3619+
theta = np.linspace(0, 2 * np.pi, 2, False)
3620+
r = theta / np.pi / 2 + 0.5
3621+
ax.errorbar(theta, r, xerr=0.8 * np.pi, yerr=0.4, fmt="o")
3622+
ax.set_rlim(0, 2)
3623+
3624+
3625+
@image_comparison(['asymmetric_polar_caps'], extensions=['png'],
3626+
remove_text=True)
3627+
def test_asymmetric_polar_caps():
3628+
fig = plt.figure()
3629+
ax = plt.subplot(111, projection='polar')
3630+
theta = np.linspace(0, 2 * np.pi, 5, False)
3631+
r = theta / np.pi / 2 + 0.5
3632+
xerr = np.vstack([r * 0.5, r * 0.25])
3633+
yerr = np.vstack([theta * 0.1, r * 0.2]) + 0.2
3634+
ax.errorbar(theta, r, xerr=xerr, yerr=yerr, fmt="o")
3635+
ax.set_rlim(0, 2)
3636+
3637+
3638+
@image_comparison(['overlapping_polar_caps'], extensions=['png'],
3639+
remove_text=True)
3640+
def test_overlapping_polar_caps():
3641+
fig = plt.figure()
3642+
ax = plt.subplot(111, projection='polar')
3643+
theta = np.linspace(0, 2 * np.pi, 3, False)
3644+
r = theta / np.pi / 2 + 0.5
3645+
ax.errorbar(theta, r, xerr=8.25 * np.pi, yerr=0.4, fmt="o")
3646+
ax.set_rlim(0, 2)
3647+
3648+
36033649
def test_errorbar_colorcycle():
36043650

36053651
f, ax = plt.subplots()

0 commit comments

Comments
 (0)