Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 97 additions & 147 deletions lib/matplotlib/colorbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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?

Expand All @@ -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')
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Copy link
Member Author

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?

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']:
Copy link
Member Author

Choose a reason for hiding this comment

The 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 test_colorbar, but otherwise has no ill effect. Its hard to wrap this as a special case just for linear scales/norms, and I don't see any reason to do this.

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)

Expand All @@ -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):
"""
Expand All @@ -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')
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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.
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,7 @@ def __init__(self, vmin=None, vmax=None, clip=False):
self.vmin = _sanitize_extrema(vmin)
self.vmax = _sanitize_extrema(vmax)
self.clip = clip
self._scale = scale.LinearScale(axis=None)

@staticmethod
def process_value(value):
Expand Down Expand Up @@ -1250,6 +1251,7 @@ def __init__(self, vcenter, vmin=None, vmax=None):
if vcenter is not None and vmin is not None and vcenter <= vmin:
raise ValueError('vmin, vcenter, and vmax must be in '
'ascending order')
self._scale = scale.LinearScale(axis=None)

def autoscale_None(self, A):
"""
Expand Down Expand Up @@ -1316,6 +1318,7 @@ def __init__(self, vcenter=0, halfrange=None, clip=False):
# calling the halfrange setter to set vmin and vmax
self.halfrange = halfrange
self.clip = clip
self._scale = scale.LinearScale(axis=None)

def _set_vmin_vmax(self):
"""
Expand Down
Loading