-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
FIX: Generalize Colorbar Scale Handling #18887
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5378151
Start
jklymak fd853ea
More progress
jklymak c5f5064
FIX: update minor ticks
jklymak 95cab6d
FIX: update minor ticks
jklymak 7d8d3c9
FIX: update minor ticks
jklymak d52abb8
FIX: fix symlognorm
jklymak 8e070e8
FIX: fix symlognorm
jklymak 6180df7
TST: update constrained_layout images
jklymak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -222,96 +222,41 @@ def _set_ticks_on_axis_warn(*args, **kw): | |
cbook._warn_external("Use the colorbar set_ticks() method instead.") | ||
|
||
|
||
class _ColorbarAutoLocator(ticker.MaxNLocator): | ||
""" | ||
AutoLocator for Colorbar | ||
|
||
This locator is just a `.MaxNLocator` except the min and max are | ||
clipped by the norm's min and max (i.e. vmin/vmax from the | ||
image/pcolor/contour object). This is necessary so ticks don't | ||
extrude into the "extend regions". | ||
""" | ||
|
||
def __init__(self, colorbar): | ||
""" | ||
This ticker needs to know the *colorbar* so that it can access | ||
its *vmin* and *vmax*. Otherwise it is the same as | ||
`~.ticker.AutoLocator`. | ||
""" | ||
|
||
self._colorbar = colorbar | ||
nbins = 'auto' | ||
steps = [1, 2, 2.5, 5, 10] | ||
super().__init__(nbins=nbins, steps=steps) | ||
|
||
def tick_values(self, vmin, vmax): | ||
# flip if needed: | ||
if vmin > vmax: | ||
vmin, vmax = vmax, vmin | ||
vmin = max(vmin, self._colorbar.norm.vmin) | ||
vmax = min(vmax, self._colorbar.norm.vmax) | ||
ticks = super().tick_values(vmin, vmax) | ||
rtol = (vmax - vmin) * 1e-10 | ||
return ticks[(ticks >= vmin - rtol) & (ticks <= vmax + rtol)] | ||
|
||
|
||
class _ColorbarAutoMinorLocator(ticker.AutoMinorLocator): | ||
""" | ||
AutoMinorLocator for Colorbar | ||
|
||
This locator is just a `.AutoMinorLocator` except the min and max are | ||
clipped by the norm's min and max (i.e. vmin/vmax from the | ||
image/pcolor/contour object). This is necessary so that the minorticks | ||
don't extrude into the "extend regions". | ||
""" | ||
|
||
def __init__(self, colorbar, n=None): | ||
""" | ||
This ticker needs to know the *colorbar* so that it can access | ||
its *vmin* and *vmax*. | ||
""" | ||
class _LocatorWrapper(): | ||
def __init__(self, parentLocator, colorbar=None): | ||
self.__class__ = type(parentLocator.__class__.__name__, | ||
(self.__class__, parentLocator.__class__), | ||
{}) | ||
self.__dict__ = parentLocator.__dict__ | ||
self._colorbar = colorbar | ||
self.ndivs = n | ||
super().__init__(n=None) | ||
|
||
def __call__(self): | ||
def _trim_ticks(self, ticks): | ||
if len(ticks) == 0: | ||
return ticks | ||
vmin = self._colorbar.norm.vmin | ||
vmax = self._colorbar.norm.vmax | ||
ticks = super().__call__() | ||
rtol = (vmax - vmin) * 1e-10 | ||
return ticks[(ticks >= vmin - rtol) & (ticks <= vmax + rtol)] | ||
|
||
|
||
class _ColorbarLogLocator(ticker.LogLocator): | ||
""" | ||
LogLocator for Colorbarbar | ||
|
||
This locator is just a `.LogLocator` except the min and max are | ||
clipped by the norm's min and max (i.e. vmin/vmax from the | ||
image/pcolor/contour object). This is necessary so ticks don't | ||
extrude into the "extend regions". | ||
if hasattr(self._colorbar.norm, '_scale'): | ||
trans = self._colorbar.norm._scale._transform.transform | ||
else: | ||
trans = mtransforms.IdentityTransform().transform | ||
rtol = (trans(vmax) - trans(vmin)) * 1e-10 | ||
good = ((trans(ticks) >= trans(vmin) - rtol) & | ||
(trans(ticks) <= trans(vmax) + rtol)) | ||
return ticks[good] | ||
|
||
""" | ||
def __init__(self, colorbar, *args, **kwargs): | ||
""" | ||
This ticker needs to know the *colorbar* so that it can access | ||
its *vmin* and *vmax*. Otherwise it is the same as | ||
`~.ticker.LogLocator`. The ``*args`` and ``**kwargs`` are the | ||
same as `~.ticker.LogLocator`. | ||
""" | ||
self._colorbar = colorbar | ||
super().__init__(*args, **kwargs) | ||
def __call__(self): | ||
ticks = super().__call__() | ||
return self._trim_ticks(np.asarray(ticks)) | ||
|
||
def tick_values(self, vmin, vmax): | ||
if vmin > vmax: | ||
vmin, vmax = vmax, vmin | ||
vmin = max(vmin, self._colorbar.norm.vmin) | ||
vmax = min(vmax, self._colorbar.norm.vmax) | ||
if vmin is not None and vmax is not None and vmin > vmax: | ||
vmax, vmin = vmin, vmax | ||
if vmin is not None: | ||
vmin = max(vmin, self._colorbar.norm.vmin) | ||
if vmax is not None: | ||
vmax = min(vmax, self._colorbar.norm.vmax) | ||
ticks = super().tick_values(vmin, vmax) | ||
rtol = (np.log10(vmax) - np.log10(vmin)) * 1e-10 | ||
ticks = ticks[(np.log10(ticks) >= np.log10(vmin) - rtol) & | ||
(np.log10(ticks) <= np.log10(vmax) + rtol)] | ||
return ticks | ||
return self._trim_ticks(np.asarray(ticks)) | ||
|
||
|
||
class _ColorbarSpine(mspines.Spine): | ||
|
@@ -488,7 +433,9 @@ def __init__(self, ax, cmap=None, | |
self.ax.add_collection(self.dividers) | ||
|
||
self.locator = None | ||
self.minorlocator = None | ||
self.formatter = None | ||
self.minorformatter = None | ||
self._manual_tick_data_values = None | ||
self.__scale = None # linear, log10 for now. Hopefully more? | ||
|
||
|
@@ -510,6 +457,18 @@ def __init__(self, ax, cmap=None, | |
self.formatter = format # Assume it is a Formatter or None | ||
self.draw_all() | ||
|
||
def _long_axis(self): | ||
if self.orientation == 'vertical': | ||
return self.ax.yaxis | ||
else: | ||
return self.ax.xaxis | ||
|
||
def _short_axis(self): | ||
if self.orientation == 'vertical': | ||
return self.ax.xaxis | ||
else: | ||
return self.ax.yaxis | ||
|
||
def _extend_lower(self): | ||
"""Return whether the lower limit is open ended.""" | ||
return self.extend in ('both', 'min') | ||
|
@@ -552,19 +511,16 @@ def config_axis(self): | |
|
||
def _config_axis(self): | ||
"""Set up long and short axis.""" | ||
ax = self.ax | ||
if self.orientation == 'vertical': | ||
long_axis, short_axis = ax.yaxis, ax.xaxis | ||
if mpl.rcParams['ytick.minor.visible']: | ||
self.minorticks_on() | ||
else: | ||
long_axis, short_axis = ax.xaxis, ax.yaxis | ||
if mpl.rcParams['xtick.minor.visible']: | ||
self.minorticks_on() | ||
long_axis.set(label_position=self.ticklocation, | ||
ticks_position=self.ticklocation) | ||
short_axis.set_ticks([]) | ||
short_axis.set_ticks([], minor=True) | ||
self._long_axis().set(label_position=self.ticklocation, | ||
ticks_position=self.ticklocation) | ||
self._short_axis().set_ticks([]) | ||
self._short_axis().set_ticks([], minor=True) | ||
self.stale = True | ||
|
||
def _get_ticker_locator_formatter(self): | ||
|
@@ -577,30 +533,19 @@ def _get_ticker_locator_formatter(self): | |
""" | ||
locator = self.locator | ||
formatter = self.formatter | ||
if locator is None: | ||
if self.boundaries is None: | ||
if isinstance(self.norm, colors.NoNorm): | ||
nv = len(self._values) | ||
base = 1 + int(nv / 10) | ||
locator = ticker.IndexLocator(base=base, offset=0) | ||
elif isinstance(self.norm, colors.BoundaryNorm): | ||
b = self.norm.boundaries | ||
locator = ticker.FixedLocator(b, nbins=10) | ||
elif isinstance(self.norm, colors.LogNorm): | ||
locator = _ColorbarLogLocator(self) | ||
elif isinstance(self.norm, colors.SymLogNorm): | ||
# The subs setting here should be replaced | ||
# by logic in the locator. | ||
locator = ticker.SymmetricalLogLocator( | ||
subs=np.arange(1, 10), | ||
linthresh=self.norm.linthresh, | ||
base=10) | ||
else: | ||
if mpl.rcParams['_internal.classic_mode']: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm biting the bullet and just removing this. This changes 10 of the image tests below and a few of the fixed values in |
||
locator = ticker.MaxNLocator() | ||
else: | ||
locator = _ColorbarAutoLocator(self) | ||
else: | ||
minorlocator = self.minorlocator | ||
minorformatter = self.minorformatter | ||
|
||
if (self.boundaries is None and | ||
not isinstance(self.norm, colors.BoundaryNorm)): | ||
if locator is None: | ||
locator = _LocatorWrapper( | ||
self._long_axis().get_major_locator(), colorbar=self) | ||
if minorlocator is None: | ||
minorlocator = _LocatorWrapper( | ||
self._long_axis().get_minor_locator(), colorbar=self) | ||
else: | ||
if locator is None: | ||
b = self._boundaries[self._inside] | ||
locator = ticker.FixedLocator(b, nbins=10) | ||
|
||
|
@@ -616,18 +561,17 @@ def _get_ticker_locator_formatter(self): | |
formatter = self.formatter | ||
|
||
self.locator = locator | ||
self.minorlocator = minorlocator | ||
self.formatter = formatter | ||
_log.debug('locator: %r', locator) | ||
return locator, formatter | ||
self.minorformatter = minorformatter | ||
|
||
def _use_auto_colorbar_locator(self): | ||
""" | ||
Return if we should use an adjustable tick locator or a fixed | ||
one. (check is used twice so factored out here...) | ||
""" | ||
contouring = self.boundaries is not None and self.spacing == 'uniform' | ||
return (type(self.norm) in [colors.Normalize, colors.LogNorm] and | ||
not contouring) | ||
return (hasattr(self.norm, '_scale') and not contouring) | ||
|
||
def _reset_locator_formatter_scale(self): | ||
""" | ||
|
@@ -636,14 +580,16 @@ def _reset_locator_formatter_scale(self): | |
the mappable normal gets changed: Colorbar.update_normal) | ||
""" | ||
self.locator = None | ||
self.minorlocator = None | ||
self.formatter = None | ||
if isinstance(self.norm, colors.LogNorm): | ||
# *both* axes are made log so that determining the | ||
# mid point is easier. | ||
self.ax.set_xscale('log') | ||
self.ax.set_yscale('log') | ||
self.minorticks_on() | ||
self.__scale = 'log' | ||
if hasattr(self.norm, '_scale'): | ||
self.ax.set_xscale(self.norm._scale.name, | ||
**self.norm._scale._kwargs) | ||
self.ax.set_yscale(self.norm._scale.name, | ||
**self.norm._scale._kwargs) | ||
self.ax.xaxis._scale = self.norm._scale | ||
self.ax.yaxis._scale = self.norm._scale | ||
self.__scale = self.norm._scale.name | ||
else: | ||
self.ax.set_xscale('linear') | ||
self.ax.set_yscale('linear') | ||
|
@@ -659,18 +605,22 @@ def update_ticks(self): | |
""" | ||
ax = self.ax | ||
# Get the locator and formatter; defaults to self.locator if not None. | ||
locator, formatter = self._get_ticker_locator_formatter() | ||
long_axis = ax.yaxis if self.orientation == 'vertical' else ax.xaxis | ||
self._get_ticker_locator_formatter() | ||
if self._use_auto_colorbar_locator(): | ||
_log.debug('Using auto colorbar locator %r on colorbar', locator) | ||
long_axis.set_major_locator(locator) | ||
long_axis.set_major_formatter(formatter) | ||
_log.debug('Using auto colorbar locator %r on colorbar', | ||
self.locator) | ||
self._long_axis().set_major_locator(self.locator) | ||
if self.minorlocator is not None: | ||
self._long_axis().set_minor_locator(self.minorlocator) | ||
self._long_axis().set_major_formatter(self.formatter) | ||
else: | ||
_log.debug('Using fixed locator on colorbar') | ||
ticks, ticklabels, offset_string = self._ticker(locator, formatter) | ||
long_axis.set_ticks(ticks) | ||
long_axis.set_ticklabels(ticklabels) | ||
long_axis.get_major_formatter().set_offset_string(offset_string) | ||
ticks, ticklabels, offset_string = self._ticker(self.locator, | ||
self.formatter) | ||
self._long_axis().set_ticks(ticks) | ||
self._long_axis().set_ticklabels(ticklabels) | ||
fmt = self._long_axis().get_major_formatter() | ||
fmt.set_offset_string(offset_string) | ||
|
||
def set_ticks(self, ticks, update_ticks=True): | ||
""" | ||
|
@@ -700,10 +650,7 @@ def set_ticks(self, ticks, update_ticks=True): | |
def get_ticks(self, minor=False): | ||
"""Return the x ticks as a list of locations.""" | ||
if self._manual_tick_data_values is None: | ||
ax = self.ax | ||
long_axis = ( | ||
ax.yaxis if self.orientation == 'vertical' else ax.xaxis) | ||
return long_axis.get_majorticklocs() | ||
return self._long_axis().get_majorticklocs() | ||
else: | ||
# We made the axes manually, the old way, and the ylim is 0-1, | ||
# so the majorticklocs are in those units, not data units. | ||
|
@@ -729,21 +676,23 @@ def minorticks_on(self): | |
Turn the minor ticks of the colorbar on without extruding | ||
into the "extend regions". | ||
""" | ||
ax = self.ax | ||
long_axis = ax.yaxis if self.orientation == 'vertical' else ax.xaxis | ||
|
||
if long_axis.get_scale() == 'log': | ||
long_axis.set_minor_locator(_ColorbarLogLocator(self, base=10., | ||
subs='auto')) | ||
long_axis.set_minor_formatter(ticker.LogFormatterSciNotation()) | ||
else: | ||
long_axis.set_minor_locator(_ColorbarAutoMinorLocator(self)) | ||
# get the default from the parent so we don't duplicate logic. | ||
self.ax.minorticks_on() | ||
self._short_axis().set_minor_locator(ticker.NullLocator()) | ||
|
||
# now wrap the default: | ||
loc = _LocatorWrapper(self._long_axis().get_minor_locator(), | ||
colorbar=self) | ||
|
||
self.minorlocator = loc | ||
self._long_axis().set_minor_locator(loc) | ||
|
||
def minorticks_off(self): | ||
"""Turn the minor ticks of the colorbar off.""" | ||
ax = self.ax | ||
long_axis = ax.yaxis if self.orientation == 'vertical' else ax.xaxis | ||
long_axis.set_minor_locator(ticker.NullLocator()) | ||
self.minorlocator = ticker.NullLocator() | ||
self._long_axis().set_minor_locator(self.minorlocator) | ||
|
||
def set_label(self, label, *, loc=None, **kwargs): | ||
""" | ||
|
@@ -1270,6 +1219,7 @@ def update_bruteforce(self, mappable): | |
# colorbar methods, those changes will be lost. | ||
self.ax.cla() | ||
self.locator = None | ||
self.minorlocator = None | ||
self.formatter = None | ||
|
||
# clearing the axes will delete outline, patch, solids, and lines: | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I seem to have managed to drop
NoNorm
which I guess means this section of code had no tests. Anyone know what this is supposed to do?