Skip to content

Commit a2396af

Browse files
committed
Fix issues with xbounds/ybounds/fixticks, improve default behavior
1 parent 92209e2 commit a2396af

1 file changed

Lines changed: 63 additions & 46 deletions

File tree

proplot/axes/cartesian.py

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,9 @@
6565
axes edges without explicitly setting the limits. Use `margin` to set both at once.
6666
xbounds, 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.
7071
xtickrange, 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

Comments
 (0)