6565 axes edges without explicitly setting the limits. Use `margin` to set both at once.
6666xbounds, ybounds : 2-tuple of float, optional
6767 The x and y axis data bounds within which to draw the spines. For example,
68- ``xlim=(0, 4)`` combined with ``xbounds=(1, 4)`` will prevent the spines
69- from meeting at the origin.
68+ ``xlim=(0, 4)`` combined with ``xbounds=(2, 4)`` will prevent the spines
69+ from meeting at the origin. This also applies ``xspineloc='bottom'`` and
70+ ``yspineloc='left'`` by default if both spines are currently visible.
7071xtickrange, ytickrange : 2-tuple of float, optional
7172 The x and y axis data ranges within which major tick marks are labelled.
7273 For example, ``xlim=(-5, 5)`` combined with ``xtickrange=(-1, 1)`` and a
@@ -435,6 +436,41 @@ def _dualy_scale(self):
435436 child .set_ylim (nlim , emit = False )
436437 child ._dualy_prevstate = (scale , * olim )
437438
439+ def _fix_ticks (self , x , fixticks = False ):
440+ """
441+ Ensure there are no out-of-bounds ticks. Mostly a brute-force version of
442+ `~matplotlib.axis.Axis.set_smart_bounds` (which I couldn't get to work).
443+ """
444+ # NOTE: Previously triggered this every time FixedFormatter was found
445+ # on axis but 1) that seems heavy-handed + strange and 2) internal
446+ # application of FixedFormatter by boxplot resulted in subsequent format()
447+ # successfully calling this and messing up the ticks for some reason.
448+ # So avoid using this when possible, and try to make behavior consistent
449+ # by cacheing the locators before we use them for ticks.
450+ axis = getattr (self , x + 'axis' )
451+ sides = ('bottom' , 'top' ) if x == 'x' else ('left' , 'right' )
452+ bounds = tuple (self .spines [side ].get_bounds () or (None , None ) for side in sides )
453+ skipticks = lambda xs : [
454+ x for x in xs if all ((l is None or x >= l ) and (h is None or x <= h ) for (l , h ) in bounds ) # noqa: E501
455+ ]
456+ if (
457+ fixticks
458+ or any (x is not None for b in bounds for x in b )
459+ or axis .get_scale () == 'cutoff'
460+ ):
461+ # Major locator
462+ locator = getattr (axis , '_major_locator_cached' , None )
463+ if locator is None :
464+ locator = axis ._major_locator_cached = axis .get_major_locator ()
465+ locator = constructor .Locator (skipticks (locator ()))
466+ axis .set_major_locator (locator )
467+ # Minor locator
468+ locator = getattr (axis , '_minor_locator_cached' , None )
469+ if locator is None :
470+ locator = axis ._minor_locator_cached = axis .get_minor_locator ()
471+ locator = constructor .Locator (skipticks (locator ()))
472+ axis .set_minor_locator (locator )
473+
438474 def _is_panel_group_member (self , other ):
439475 """
440476 Return whether the axes belong in a panel sharing stack..
@@ -540,35 +576,6 @@ def _sharey_setup(self, sharey, *, labels=True, limits=True):
540576 if level > 1 and limits :
541577 self ._sharey_limits (sharey )
542578
543- def _update_bounds (self , x , fixticks = False ):
544- """
545- Ensure there are no out-of-bounds labels. Mostly a brute-force version of
546- `~matplotlib.axis.Axis.set_smart_bounds` (which I couldn't get to work).
547- """
548- # NOTE: Previously triggered this every time FixedFormatter was found
549- # on axis but 1) that seems heavy-handed + strange and 2) internal
550- # application of FixedFormatter by boxplot resulted in subsequent format()
551- # successfully calling this and messing up the ticks for some reason.
552- # So avoid using this when possible, and try to make behavior consistent
553- # by cacheing the locators before we use them for ticks.
554- axis = getattr (self , x + 'axis' )
555- sides = ('bottom' , 'top' ) if x == 'x' else ('left' , 'right' )
556- bounds = tuple (self .spines [side ].get_bounds () is not None for side in sides )
557- if fixticks or any (bounds ) or axis .get_scale () == 'cutoff' :
558- # Major locator
559- lim = bounds [0 ] or bounds [1 ] or getattr (self , 'get_' + x + 'lim' )()
560- locator = getattr (axis , '_major_locator_cached' , None )
561- if locator is None :
562- locator = axis ._major_locator_cached = axis .get_major_locator ()
563- locator = constructor .Locator ([x for x in locator () if lim [0 ] <= x <= lim [1 ]]) # noqa: E501
564- axis .set_major_locator (locator )
565- # Minor locator
566- locator = getattr (axis , '_minor_locator_cached' , None )
567- if locator is None :
568- locator = axis ._minor_locator_cached = axis .get_minor_locator ()
569- locator = constructor .Locator ([x for x in locator () if lim [0 ] <= x <= lim [1 ]]) # noqa: E501
570- axis .set_minor_locator (locator )
571-
572579 def _update_formatter (
573580 self , x , formatter = None , * , formatter_kw = None ,
574581 tickrange = None , wraprange = None ,
@@ -736,14 +743,25 @@ def _update_spines(self, x, *, loc=None, bounds=None):
736743 """
737744 # Iterate over spines associated with this axis
738745 sides = ('bottom' , 'top' ) if x == 'x' else ('left' , 'right' )
746+ vboth = all (self .spines [side ].get_visible () for side in sides ) # both visible
747+ pside = sides [0 ] # side for spine.set_position()
748+ if np .iterable (loc ) and len (loc ) == 2 and loc [0 ] in ('axes' , 'data' ):
749+ if loc [0 ] == 'data' :
750+ lim = getattr (self , f'get_{ x } lim' )()
751+ center = lim [0 ] + 0.5 * (lim [1 ] - lim [0 ])
752+ else :
753+ center = 0.5
754+ pside = sides [int (loc [1 ] > center )]
739755 for side in sides :
740756 # Change default spine location from 'both' to the first relevant
741757 # side if the user passes 'bounds'.
742758 spine = self .spines [side ]
743- if loc is None and bounds is not None :
759+ if vboth and bounds is not None :
744760 loc = _not_none (loc , sides [0 ])
745761 # Eliminate sides
746- if loc == 'neither' :
762+ if loc is None :
763+ pass
764+ elif loc == 'neither' :
747765 spine .set_visible (False )
748766 elif loc == 'both' :
749767 spine .set_visible (True )
@@ -752,18 +770,17 @@ def _update_spines(self, x, *, loc=None, bounds=None):
752770 # Special spine location, usually 'zero', 'center', or tuple with
753771 # (units, location) where 'units' can be 'axes', 'data', or 'outward'.
754772 # Matplotlib internally represents these with 'bottom' and 'left'.
755- elif loc is not None :
756- if side == sides [1 ]:
757- spine .set_visible (False )
758- else :
759- spine .set_visible (True )
760- try :
761- spine .set_position (loc )
762- except ValueError :
763- raise ValueError (
764- f'Invalid { x } spine location { loc !r} . Options are: '
765- + ', ' .join (map (repr , (* sides , 'both' , 'neither' ))) + '.'
766- )
773+ elif pside != side :
774+ spine .set_visible (False )
775+ else :
776+ try :
777+ spine .set_position (loc )
778+ except ValueError :
779+ raise ValueError (
780+ f'Invalid { x } spine location { loc !r} . Options are: '
781+ + ', ' .join (map (repr , (* sides , 'both' , 'neither' ))) + '.'
782+ )
783+ spine .set_visible (True )
767784 # Apply spine bounds
768785 if bounds is not None :
769786 spine .set_bounds (* bounds )
@@ -1163,7 +1180,7 @@ def format(
11631180 )
11641181
11651182 # Ensure ticks are within axis bounds
1166- self ._update_bounds (x , fixticks = fixticks )
1183+ self ._fix_ticks (x , fixticks = fixticks )
11671184
11681185 # Parent format method
11691186 if aspect is not None :
0 commit comments