1414import functools
1515import itertools
1616import math
17+ from numbers import Integral
1718import textwrap
1819
1920import numpy as np
2223import matplotlib .axes as maxes
2324import matplotlib .collections as mcoll
2425import matplotlib .colors as mcolors
26+ import matplotlib .lines as mlines
2527import matplotlib .scale as mscale
2628import matplotlib .container as mcontainer
2729import 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