Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0f83ee2

Browse files
authored
Merge pull request #18436 from QuLogic/errorbar3d
MNT: Sync 3D errorbar with 2D
2 parents b681abc + 0102e2b commit 0f83ee2

File tree

4 files changed

+124
-111
lines changed

4 files changed

+124
-111
lines changed

lib/matplotlib/axes/_axes.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3384,7 +3384,7 @@ def errorbar(self, x, y, yerr=None, xerr=None,
33843384
if key in kwargs:
33853385
eb_lines_style[key] = kwargs[key]
33863386

3387-
# Make the style dict for the caps.
3387+
# Make the style dict for caps (the "hats").
33883388
eb_cap_style = {**base_style, 'linestyle': 'none'}
33893389
if capsize is None:
33903390
capsize = rcParams["errorbar.capsize"]
@@ -3469,8 +3469,7 @@ def extract_err(name, err, data, lolims, uplims):
34693469
mlines.Line2D(lo, yo, marker='|', **eb_cap_style),
34703470
mlines.Line2D(ro, yo, marker='|', **eb_cap_style)])
34713471
if xlolims.any():
3472-
xo, yo, lo, ro = apply_mask([x, y, left, right],
3473-
xlolims & everymask)
3472+
xo, yo, ro = apply_mask([x, y, right], xlolims & everymask)
34743473
if self.xaxis_inverted():
34753474
marker = mlines.CARETLEFTBASE
34763475
else:
@@ -3481,8 +3480,7 @@ def extract_err(name, err, data, lolims, uplims):
34813480
caplines.append(mlines.Line2D(
34823481
xo, yo, marker='|', **eb_cap_style))
34833482
if xuplims.any():
3484-
xo, yo, lo, ro = apply_mask([x, y, left, right],
3485-
xuplims & everymask)
3483+
xo, yo, lo = apply_mask([x, y, left], xuplims & everymask)
34863484
if self.xaxis_inverted():
34873485
marker = mlines.CARETRIGHTBASE
34883486
else:
@@ -3506,8 +3504,7 @@ def extract_err(name, err, data, lolims, uplims):
35063504
mlines.Line2D(xo, lo, marker='_', **eb_cap_style),
35073505
mlines.Line2D(xo, uo, marker='_', **eb_cap_style)])
35083506
if lolims.any():
3509-
xo, yo, lo, uo = apply_mask([x, y, lower, upper],
3510-
lolims & everymask)
3507+
xo, yo, uo = apply_mask([x, y, upper], lolims & everymask)
35113508
if self.yaxis_inverted():
35123509
marker = mlines.CARETDOWNBASE
35133510
else:
@@ -3518,8 +3515,7 @@ def extract_err(name, err, data, lolims, uplims):
35183515
caplines.append(mlines.Line2D(
35193516
xo, yo, marker='_', **eb_cap_style))
35203517
if uplims.any():
3521-
xo, yo, lo, uo = apply_mask([x, y, lower, upper],
3522-
uplims & everymask)
3518+
xo, yo, lo = apply_mask([x, y, lower], uplims & everymask)
35233519
if self.yaxis_inverted():
35243520
marker = mlines.CARETUPBASE
35253521
else:

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 119 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import functools
1515
import itertools
1616
import math
17+
from numbers import Integral
1718
import textwrap
1819

1920
import numpy as np
@@ -22,6 +23,7 @@
2223
import matplotlib.axes as maxes
2324
import matplotlib.collections as mcoll
2425
import matplotlib.colors as mcolors
26+
import matplotlib.lines as mlines
2527
import matplotlib.scale as mscale
2628
import matplotlib.container as mcontainer
2729
import matplotlib.transforms as mtransforms
@@ -3019,7 +3021,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
30193021
barsabove=False, errorevery=1, ecolor=None, elinewidth=None,
30203022
capsize=None, capthick=None, xlolims=False, xuplims=False,
30213023
ylolims=False, yuplims=False, zlolims=False, zuplims=False,
3022-
arrow_length_ratio=.4, **kwargs):
3024+
**kwargs):
30233025
"""
30243026
Plot lines and/or markers with errorbars around them.
30253027
@@ -3096,10 +3098,6 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
30963098
Used to avoid overlapping error bars when two series share x-axis
30973099
values.
30983100
3099-
arrow_length_ratio : float, default: 0.4
3100-
Passed to :meth:`quiver`, the ratio of the arrow head with respect
3101-
to the quiver.
3102-
31033101
Returns
31043102
-------
31053103
errlines : list
@@ -3124,34 +3122,14 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
31243122
"""
31253123
had_data = self.has_data()
31263124

3127-
plot_line = (fmt.lower() != 'none')
3128-
label = kwargs.pop("label", None)
3125+
kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D)
3126+
# anything that comes in as 'None', drop so the default thing
3127+
# happens down stream
3128+
kwargs = {k: v for k, v in kwargs.items() if v is not None}
3129+
kwargs.setdefault('zorder', 2)
31293130

3130-
if fmt == '':
3131-
fmt_style_kwargs = {}
3132-
else:
3133-
fmt_style_kwargs = {k: v for k, v in
3134-
zip(('linestyle', 'marker', 'color'),
3135-
_process_plot_format(fmt))
3136-
if v is not None}
3137-
3138-
if fmt == 'none':
3139-
# Remove alpha=0 color that _process_plot_format returns
3140-
fmt_style_kwargs.pop('color')
3141-
3142-
if ('color' in kwargs or 'color' in fmt_style_kwargs):
3143-
base_style = {}
3144-
if 'color' in kwargs:
3145-
base_style['color'] = kwargs.pop('color')
3146-
else:
3147-
base_style = next(self._get_lines.prop_cycler)
3148-
3149-
base_style['label'] = '_nolegend_'
3150-
base_style.update(fmt_style_kwargs)
3151-
if 'color' not in base_style:
3152-
base_style['color'] = 'C0'
3153-
if ecolor is None:
3154-
ecolor = base_style['color']
3131+
self._process_unit_info([("x", x), ("y", y), ("z", z)], kwargs,
3132+
convert=False)
31553133

31563134
# make sure all the args are iterable; use lists not arrays to
31573135
# preserve units
@@ -3162,24 +3140,75 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
31623140
if not len(x) == len(y) == len(z):
31633141
raise ValueError("'x', 'y', and 'z' must have the same size")
31643142

3165-
# make the style dict for the 'normal' plot line
3166-
if 'zorder' not in kwargs:
3167-
kwargs['zorder'] = 2
3168-
plot_line_style = {
3169-
**base_style,
3170-
**kwargs,
3171-
'zorder': (kwargs['zorder'] - .1 if barsabove else
3172-
kwargs['zorder'] + .1),
3173-
}
3143+
if isinstance(errorevery, Integral):
3144+
errorevery = (0, errorevery)
3145+
if isinstance(errorevery, tuple):
3146+
if (len(errorevery) == 2 and
3147+
isinstance(errorevery[0], Integral) and
3148+
isinstance(errorevery[1], Integral)):
3149+
errorevery = slice(errorevery[0], None, errorevery[1])
3150+
else:
3151+
raise ValueError(
3152+
f'errorevery={errorevery!r} is a not a tuple of two '
3153+
f'integers')
3154+
3155+
elif isinstance(errorevery, slice):
3156+
pass
3157+
3158+
elif not isinstance(errorevery, str) and np.iterable(errorevery):
3159+
# fancy indexing
3160+
try:
3161+
x[errorevery]
3162+
except (ValueError, IndexError) as err:
3163+
raise ValueError(
3164+
f"errorevery={errorevery!r} is iterable but not a valid "
3165+
f"NumPy fancy index to match "
3166+
f"'xerr'/'yerr'/'zerr'") from err
3167+
else:
3168+
raise ValueError(
3169+
f"errorevery={errorevery!r} is not a recognized value")
31743170

3175-
# make the style dict for the line collections (the bars)
3176-
eb_lines_style = dict(base_style)
3177-
eb_lines_style.pop('marker', None)
3178-
eb_lines_style.pop('markerfacecolor', None)
3179-
eb_lines_style.pop('markeredgewidth', None)
3180-
eb_lines_style.pop('markeredgecolor', None)
3181-
eb_lines_style.pop('linestyle', None)
3182-
eb_lines_style['color'] = ecolor
3171+
label = kwargs.pop("label", None)
3172+
kwargs['label'] = '_nolegend_'
3173+
3174+
# Create the main line and determine overall kwargs for child artists.
3175+
# We avoid calling self.plot() directly, or self._get_lines(), because
3176+
# that would call self._process_unit_info again, and do other indirect
3177+
# data processing.
3178+
(data_line, base_style), = self._get_lines._plot_args(
3179+
(x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True)
3180+
art3d.line_2d_to_3d(data_line, zs=z)
3181+
3182+
# Do this after creating `data_line` to avoid modifying `base_style`.
3183+
if barsabove:
3184+
data_line.set_zorder(kwargs['zorder'] - .1)
3185+
else:
3186+
data_line.set_zorder(kwargs['zorder'] + .1)
3187+
3188+
# Add line to plot, or throw it away and use it to determine kwargs.
3189+
if fmt.lower() != 'none':
3190+
self.add_line(data_line)
3191+
else:
3192+
data_line = None
3193+
# Remove alpha=0 color that _process_plot_format returns.
3194+
base_style.pop('color')
3195+
3196+
if 'color' not in base_style:
3197+
base_style['color'] = 'C0'
3198+
if ecolor is None:
3199+
ecolor = base_style['color']
3200+
3201+
# Eject any marker information from line format string, as it's not
3202+
# needed for bars or caps.
3203+
base_style.pop('marker', None)
3204+
base_style.pop('markersize', None)
3205+
base_style.pop('markerfacecolor', None)
3206+
base_style.pop('markeredgewidth', None)
3207+
base_style.pop('markeredgecolor', None)
3208+
base_style.pop('linestyle', None)
3209+
3210+
# Make the style dict for the line collections (the bars).
3211+
eb_lines_style = {**base_style, 'color': ecolor}
31833212

31843213
if elinewidth:
31853214
eb_lines_style['linewidth'] = elinewidth
@@ -3190,37 +3219,18 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
31903219
if key in kwargs:
31913220
eb_lines_style[key] = kwargs[key]
31923221

3193-
# make the style dict for cap collections (the "hats")
3194-
eb_cap_style = dict(base_style)
3195-
# eject any marker information from format string
3196-
eb_cap_style.pop('marker', None)
3197-
eb_cap_style.pop('ls', None)
3198-
eb_cap_style['linestyle'] = 'none'
3222+
# Make the style dict for caps (the "hats").
3223+
eb_cap_style = {**base_style, 'linestyle': 'None'}
31993224
if capsize is None:
3200-
capsize = kwargs.pop('capsize', rcParams["errorbar.capsize"])
3225+
capsize = rcParams["errorbar.capsize"]
32013226
if capsize > 0:
32023227
eb_cap_style['markersize'] = 2. * capsize
32033228
if capthick is not None:
32043229
eb_cap_style['markeredgewidth'] = capthick
32053230
eb_cap_style['color'] = ecolor
32063231

3207-
if plot_line:
3208-
data_line = art3d.Line3D(x, y, z, **plot_line_style)
3209-
self.add_line(data_line)
3210-
3211-
try:
3212-
offset, errorevery = errorevery
3213-
except TypeError:
3214-
offset = 0
3215-
3216-
if errorevery < 1 or int(errorevery) != errorevery:
3217-
raise ValueError(
3218-
'errorevery must be positive integer or tuple of integers')
3219-
if int(offset) != offset:
3220-
raise ValueError("errorevery's starting index must be an integer")
3221-
32223232
everymask = np.zeros(len(x), bool)
3223-
everymask[offset::errorevery] = True
3233+
everymask[errorevery] = True
32243234

32253235
def _apply_mask(arrays, mask):
32263236
# Return, for each array in *arrays*, the elements for which *mask*
@@ -3234,14 +3244,8 @@ def _extract_errs(err, data, lomask, himask):
32343244
else:
32353245
low_err, high_err = err, err
32363246

3237-
# for compatibility with the 2d errorbar function, when both upper
3238-
# and lower limits specified, we need to draw the markers / line
3239-
common_mask = (lomask == himask) & everymask
3240-
_lomask = lomask | common_mask
3241-
_himask = himask | common_mask
3242-
3243-
lows = np.where(_lomask, data - low_err, data)
3244-
highs = np.where(_himask, data + high_err, data)
3247+
lows = np.where(lomask | ~everymask, data, data - low_err)
3248+
highs = np.where(himask | ~everymask, data, data + high_err)
32453249

32463250
return lows, highs
32473251

@@ -3256,6 +3260,33 @@ def _extract_errs(err, data, lomask, himask):
32563260
capmarker = {0: '|', 1: '|', 2: '_'}
32573261
i_xyz = {'x': 0, 'y': 1, 'z': 2}
32583262

3263+
# Calculate marker size from points to quiver length. Because these are
3264+
# not markers, and 3D Axes do not use the normal transform stack, this
3265+
# is a bit involved. Since the quiver arrows will change size as the
3266+
# scene is rotated, they are given a standard size based on viewing
3267+
# them directly in planar form.
3268+
quiversize = eb_cap_style.get('markersize',
3269+
rcParams['lines.markersize']) ** 2
3270+
quiversize *= self.figure.dpi / 72
3271+
quiversize = self.transAxes.inverted().transform([
3272+
(0, 0), (quiversize, quiversize)])
3273+
quiversize = np.mean(np.diff(quiversize, axis=0))
3274+
# quiversize is now in Axes coordinates, and to convert back to data
3275+
# coordinates, we need to run it through the inverse 3D transform. For
3276+
# consistency, this uses a fixed azimuth and elevation.
3277+
with cbook._setattr_cm(self, azim=0, elev=0):
3278+
invM = np.linalg.inv(self.get_proj())
3279+
# azim=elev=0 produces the Y-Z plane, so quiversize in 2D 'x' is 'y' in
3280+
# 3D, hence the 1 index.
3281+
quiversize = np.dot(invM, np.array([quiversize, 0, 0, 0]))[1]
3282+
# Quivers use a fixed 15-degree arrow head, so scale up the length so
3283+
# that the size corresponds to the base. In other words, this constant
3284+
# corresponds to the equation tan(15) = (base / 2) / (arrow length).
3285+
quiversize *= 1.8660254037844388
3286+
eb_quiver_style = {**eb_cap_style,
3287+
'length': quiversize, 'arrow_length_ratio': 1}
3288+
eb_quiver_style.pop('markersize', None)
3289+
32593290
# loop over x-, y-, and z-direction and draw relevant elements
32603291
for zdir, data, err, lolims, uplims in zip(
32613292
['x', 'y', 'z'], [x, y, z], [xerr, yerr, zerr],
@@ -3276,18 +3307,16 @@ def _extract_errs(err, data, lomask, himask):
32763307
lolims = np.broadcast_to(lolims, len(data)).astype(bool)
32773308
uplims = np.broadcast_to(uplims, len(data)).astype(bool)
32783309

3279-
nolims = ~(lolims | uplims)
3280-
32813310
# a nested list structure that expands to (xl,xh),(yl,yh),(zl,zh),
32823311
# where x/y/z and l/h correspond to dimensions and low/high
32833312
# positions of errorbars in a dimension we're looping over
32843313
coorderr = [
3285-
_extract_errs(err * dir_vector[i], coord,
3286-
~lolims & everymask, ~uplims & everymask)
3314+
_extract_errs(err * dir_vector[i], coord, lolims, uplims)
32873315
for i, coord in enumerate([x, y, z])]
32883316
(xl, xh), (yl, yh), (zl, zh) = coorderr
32893317

32903318
# draws capmarkers - flat caps orthogonal to the error bars
3319+
nolims = ~(lolims | uplims)
32913320
if nolims.any() and capsize > 0:
32923321
lo_caps_xyz = _apply_mask([xl, yl, zl], nolims & everymask)
32933322
hi_caps_xyz = _apply_mask([xh, yh, zh], nolims & everymask)
@@ -3305,24 +3334,12 @@ def _extract_errs(err, data, lomask, himask):
33053334
caplines.append(cap_lo)
33063335
caplines.append(cap_hi)
33073336

3308-
if (lolims | uplims).any():
3309-
limits = [
3310-
_extract_errs(err*dir_vector[i], coord, uplims, lolims)
3311-
for i, coord in enumerate([x, y, z])]
3312-
3313-
(xlo, xup), (ylo, yup), (zlo, zup) = limits
3314-
lomask = lolims & everymask
3315-
upmask = uplims & everymask
3316-
lolims_xyz = np.array(_apply_mask([xlo, ylo, zlo], upmask))
3317-
uplims_xyz = np.array(_apply_mask([xup, yup, zup], lomask))
3318-
lo_xyz = np.array(_apply_mask([x, y, z], upmask))
3319-
up_xyz = np.array(_apply_mask([x, y, z], lomask))
3320-
x0, y0, z0 = np.concatenate([lo_xyz, up_xyz], axis=-1)
3321-
dx, dy, dz = np.concatenate([lolims_xyz - lo_xyz,
3322-
uplims_xyz - up_xyz], axis=-1)
3323-
self.quiver(x0, y0, z0, dx, dy, dz,
3324-
arrow_length_ratio=arrow_length_ratio,
3325-
**eb_lines_style)
3337+
if lolims.any():
3338+
xh0, yh0, zh0 = _apply_mask([xh, yh, zh], lolims & everymask)
3339+
self.quiver(xh0, yh0, zh0, *dir_vector, **eb_quiver_style)
3340+
if uplims.any():
3341+
xl0, yl0, zl0 = _apply_mask([xl, yl, zl], uplims & everymask)
3342+
self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style)
33263343

33273344
errline = art3d.Line3DCollection(np.array(coorderr).T,
33283345
**eb_lines_style)

0 commit comments

Comments
 (0)