14
14
import functools
15
15
import itertools
16
16
import math
17
+ from numbers import Integral
17
18
import textwrap
18
19
19
20
import numpy as np
22
23
import matplotlib .axes as maxes
23
24
import matplotlib .collections as mcoll
24
25
import matplotlib .colors as mcolors
26
+ import matplotlib .lines as mlines
25
27
import matplotlib .scale as mscale
26
28
import matplotlib .container as mcontainer
27
29
import matplotlib .transforms as mtransforms
@@ -3019,7 +3021,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
3019
3021
barsabove = False , errorevery = 1 , ecolor = None , elinewidth = None ,
3020
3022
capsize = None , capthick = None , xlolims = False , xuplims = False ,
3021
3023
ylolims = False , yuplims = False , zlolims = False , zuplims = False ,
3022
- arrow_length_ratio = .4 , ** kwargs ):
3024
+ ** kwargs ):
3023
3025
"""
3024
3026
Plot lines and/or markers with errorbars around them.
3025
3027
@@ -3096,10 +3098,6 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
3096
3098
Used to avoid overlapping error bars when two series share x-axis
3097
3099
values.
3098
3100
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
-
3103
3101
Returns
3104
3102
-------
3105
3103
errlines : list
@@ -3124,34 +3122,14 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
3124
3122
"""
3125
3123
had_data = self .has_data ()
3126
3124
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 )
3129
3130
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 )
3155
3133
3156
3134
# make sure all the args are iterable; use lists not arrays to
3157
3135
# preserve units
@@ -3162,24 +3140,75 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
3162
3140
if not len (x ) == len (y ) == len (z ):
3163
3141
raise ValueError ("'x', 'y', and 'z' must have the same size" )
3164
3142
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" )
3174
3170
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 }
3183
3212
3184
3213
if elinewidth :
3185
3214
eb_lines_style ['linewidth' ] = elinewidth
@@ -3190,37 +3219,18 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
3190
3219
if key in kwargs :
3191
3220
eb_lines_style [key ] = kwargs [key ]
3192
3221
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' }
3199
3224
if capsize is None :
3200
- capsize = kwargs . pop ( 'capsize' , rcParams ["errorbar.capsize" ])
3225
+ capsize = rcParams ["errorbar.capsize" ]
3201
3226
if capsize > 0 :
3202
3227
eb_cap_style ['markersize' ] = 2. * capsize
3203
3228
if capthick is not None :
3204
3229
eb_cap_style ['markeredgewidth' ] = capthick
3205
3230
eb_cap_style ['color' ] = ecolor
3206
3231
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
-
3222
3232
everymask = np .zeros (len (x ), bool )
3223
- everymask [offset :: errorevery ] = True
3233
+ everymask [errorevery ] = True
3224
3234
3225
3235
def _apply_mask (arrays , mask ):
3226
3236
# Return, for each array in *arrays*, the elements for which *mask*
@@ -3234,14 +3244,8 @@ def _extract_errs(err, data, lomask, himask):
3234
3244
else :
3235
3245
low_err , high_err = err , err
3236
3246
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 )
3245
3249
3246
3250
return lows , highs
3247
3251
@@ -3256,6 +3260,33 @@ def _extract_errs(err, data, lomask, himask):
3256
3260
capmarker = {0 : '|' , 1 : '|' , 2 : '_' }
3257
3261
i_xyz = {'x' : 0 , 'y' : 1 , 'z' : 2 }
3258
3262
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
+
3259
3290
# loop over x-, y-, and z-direction and draw relevant elements
3260
3291
for zdir , data , err , lolims , uplims in zip (
3261
3292
['x' , 'y' , 'z' ], [x , y , z ], [xerr , yerr , zerr ],
@@ -3276,18 +3307,16 @@ def _extract_errs(err, data, lomask, himask):
3276
3307
lolims = np .broadcast_to (lolims , len (data )).astype (bool )
3277
3308
uplims = np .broadcast_to (uplims , len (data )).astype (bool )
3278
3309
3279
- nolims = ~ (lolims | uplims )
3280
-
3281
3310
# a nested list structure that expands to (xl,xh),(yl,yh),(zl,zh),
3282
3311
# where x/y/z and l/h correspond to dimensions and low/high
3283
3312
# positions of errorbars in a dimension we're looping over
3284
3313
coorderr = [
3285
- _extract_errs (err * dir_vector [i ], coord ,
3286
- ~ lolims & everymask , ~ uplims & everymask )
3314
+ _extract_errs (err * dir_vector [i ], coord , lolims , uplims )
3287
3315
for i , coord in enumerate ([x , y , z ])]
3288
3316
(xl , xh ), (yl , yh ), (zl , zh ) = coorderr
3289
3317
3290
3318
# draws capmarkers - flat caps orthogonal to the error bars
3319
+ nolims = ~ (lolims | uplims )
3291
3320
if nolims .any () and capsize > 0 :
3292
3321
lo_caps_xyz = _apply_mask ([xl , yl , zl ], nolims & everymask )
3293
3322
hi_caps_xyz = _apply_mask ([xh , yh , zh ], nolims & everymask )
@@ -3305,24 +3334,12 @@ def _extract_errs(err, data, lomask, himask):
3305
3334
caplines .append (cap_lo )
3306
3335
caplines .append (cap_hi )
3307
3336
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 )
3326
3343
3327
3344
errline = art3d .Line3DCollection (np .array (coorderr ).T ,
3328
3345
** eb_lines_style )
0 commit comments