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

Skip to content

Commit ce33de7

Browse files
authored
Merge pull request #13234 from jklymak/fix-colorbar-norm-changed
FIX: allow colorbar mappable norm to change and do right thing
2 parents b9c2b26 + 323b237 commit ce33de7

File tree

5 files changed

+186
-44
lines changed

5 files changed

+186
-44
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
`matplotlib.colorbar.ColorbarBase` is no longer a subclass of `.ScalarMappable`
2+
-------------------------------------------------------------------------------
3+
4+
This inheritance lead to a confusing situation where the
5+
`ScalarMappable` passed to `matplotlib.colorbar.Colorbar` (`~.Figure.colorbar`)
6+
had a ``set_norm`` method, as did the colorbar. The colorbar is now purely a
7+
slave to the `ScalarMappable` norm and colormap, and the old inherited methods
8+
`~matplotlib.colorbar.ColorbarBase.set_norm`,
9+
`~matplotlib.colorbar.ColorbarBase.set_cmap`,
10+
`~matplotlib.colorbar.ColorbarBase.set_clim` are deprecated, as are the
11+
getter versions of those calls. To set the norm associated with a colorbar do
12+
``colorbar.mappable.set_norm()`` etc.

lib/matplotlib/cm.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,13 @@ def set_norm(self, norm):
349349
Parameters
350350
----------
351351
norm : `.Normalize`
352+
353+
Notes
354+
-----
355+
If there are any colorbars using the mappable for this norm, setting
356+
the norm of the mappable will reset the norm, locator, and formatters
357+
on the colorbar to default.
358+
352359
"""
353360
if norm is None:
354361
norm = colors.Normalize()

lib/matplotlib/colorbar.py

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,51 @@ def tick_values(self, vmin, vmax):
304304
return ticks
305305

306306

307-
class ColorbarBase(cm.ScalarMappable):
307+
class _ColorbarMappableDummy(object):
308+
"""
309+
Private class to hold deprecated ColorbarBase methods that used to be
310+
inhereted from ScalarMappable.
311+
"""
312+
@cbook.deprecated("3.1", alternative="ScalarMappable.set_norm")
313+
def set_norm(self, norm):
314+
"""
315+
`.colorbar.Colorbar.set_norm` does nothing; set the norm on
316+
the mappable associated with this colorbar.
317+
"""
318+
pass
319+
320+
@cbook.deprecated("3.1", alternative="ScalarMappable.set_cmap")
321+
def set_cmap(self, cmap):
322+
"""
323+
`.colorbar.Colorbar.set_cmap` does nothing; set the norm on
324+
the mappable associated with this colorbar.
325+
"""
326+
pass
327+
328+
@cbook.deprecated("3.1", alternative="ScalarMappable.set_clim")
329+
def set_clim(self, cmap):
330+
"""
331+
`.colorbar.Colorbar.set_clim` does nothing; set the limits on
332+
the mappable associated with this colorbar.
333+
"""
334+
pass
335+
336+
@cbook.deprecated("3.1", alternative="ScalarMappable.get_cmap")
337+
def get_cmap(self):
338+
"""
339+
return the colormap
340+
"""
341+
return self.cmap
342+
343+
@cbook.deprecated("3.1", alternative="ScalarMappable.get_clim")
344+
def get_clim(self):
345+
"""
346+
return the min, max of the color limits for image scaling
347+
"""
348+
return self.norm.vmin, self.norm.vmax
349+
350+
351+
class ColorbarBase(_ColorbarMappableDummy):
308352
'''
309353
Draw a colorbar in an existing axes.
310354
@@ -371,7 +415,8 @@ def __init__(self, ax, cmap=None,
371415
if norm is None:
372416
norm = colors.Normalize()
373417
self.alpha = alpha
374-
cm.ScalarMappable.__init__(self, cmap=cmap, norm=norm)
418+
self.cmap = cmap
419+
self.norm = norm
375420
self.values = values
376421
self.boundaries = boundaries
377422
self.extend = extend
@@ -387,30 +432,26 @@ def __init__(self, ax, cmap=None,
387432
self.outline = None
388433
self.patch = None
389434
self.dividers = None
435+
self.locator = None
436+
self.formatter = None
390437
self._manual_tick_data_values = None
391438

392439
if ticklocation == 'auto':
393440
ticklocation = 'bottom' if orientation == 'horizontal' else 'right'
394441
self.ticklocation = ticklocation
395442

396443
self.set_label(label)
444+
self._reset_locator_formatter_scale()
445+
397446
if np.iterable(ticks):
398447
self.locator = ticker.FixedLocator(ticks, nbins=len(ticks))
399448
else:
400449
self.locator = ticks # Handle default in _ticker()
401-
if format is None:
402-
if isinstance(self.norm, colors.LogNorm):
403-
self.formatter = ticker.LogFormatterSciNotation()
404-
elif isinstance(self.norm, colors.SymLogNorm):
405-
self.formatter = ticker.LogFormatterSciNotation(
406-
linthresh=self.norm.linthresh)
407-
else:
408-
self.formatter = ticker.ScalarFormatter()
409-
elif isinstance(format, str):
450+
451+
if isinstance(format, str):
410452
self.formatter = ticker.FormatStrFormatter(format)
411453
else:
412-
self.formatter = format # Assume it is a Formatter
413-
# The rest is in a method so we can recalculate when clim changes.
454+
self.formatter = format # Assume it is a Formatter or None
414455
self.draw_all()
415456

416457
def _extend_lower(self):
@@ -432,7 +473,6 @@ def draw_all(self):
432473
Calculate any free parameters based on the current cmap and norm,
433474
and do all the drawing.
434475
'''
435-
436476
# sets self._boundaries and self._values in real data units.
437477
# takes into account extend values:
438478
self._process_values()
@@ -451,12 +491,6 @@ def draw_all(self):
451491

452492
def config_axis(self):
453493
ax = self.ax
454-
if (isinstance(self.norm, colors.LogNorm)
455-
and self._use_auto_colorbar_locator()):
456-
# *both* axes are made log so that determining the
457-
# mid point is easier.
458-
ax.set_xscale('log')
459-
ax.set_yscale('log')
460494

461495
if self.orientation == 'vertical':
462496
long_axis, short_axis = ax.yaxis, ax.xaxis
@@ -504,6 +538,20 @@ def _get_ticker_locator_formatter(self):
504538
else:
505539
b = self._boundaries[self._inside]
506540
locator = ticker.FixedLocator(b, nbins=10)
541+
542+
if formatter is None:
543+
if isinstance(self.norm, colors.LogNorm):
544+
formatter = ticker.LogFormatterSciNotation()
545+
elif isinstance(self.norm, colors.SymLogNorm):
546+
formatter = ticker.LogFormatterSciNotation(
547+
linthresh=self.norm.linthresh)
548+
else:
549+
formatter = ticker.ScalarFormatter()
550+
else:
551+
formatter = self.formatter
552+
553+
self.locator = locator
554+
self.formatter = formatter
507555
_log.debug('locator: %r', locator)
508556
return locator, formatter
509557

@@ -517,6 +565,24 @@ def _use_auto_colorbar_locator(self):
517565
and ((type(self.norm) == colors.Normalize)
518566
or (type(self.norm) == colors.LogNorm)))
519567

568+
def _reset_locator_formatter_scale(self):
569+
"""
570+
Reset the locator et al to defaults. Any user-hardcoded changes
571+
need to be re-entered if this gets called (either at init, or when
572+
the mappable normal gets changed: Colorbar.update_normal)
573+
"""
574+
self.locator = None
575+
self.formatter = None
576+
if (isinstance(self.norm, colors.LogNorm)
577+
and self._use_auto_colorbar_locator()):
578+
# *both* axes are made log so that determining the
579+
# mid point is easier.
580+
self.ax.set_xscale('log')
581+
self.ax.set_yscale('log')
582+
else:
583+
self.ax.set_xscale('linear')
584+
self.ax.set_yscale('linear')
585+
520586
def update_ticks(self):
521587
"""
522588
Force the update of the ticks and ticklabels. This must be
@@ -526,7 +592,6 @@ def update_ticks(self):
526592
# get the locator and formatter. Defaults to
527593
# self.locator if not None..
528594
locator, formatter = self._get_ticker_locator_formatter()
529-
530595
if self.orientation == 'vertical':
531596
long_axis, short_axis = ax.yaxis, ax.xaxis
532597
else:
@@ -1102,7 +1167,6 @@ def __init__(self, ax, mappable, **kw):
11021167
kw['boundaries'] = CS._levels
11031168
kw['values'] = CS.cvalues
11041169
kw['extend'] = CS.extend
1105-
#kw['ticks'] = CS._levels
11061170
kw.setdefault('ticks', ticker.FixedLocator(CS.levels, nbins=10))
11071171
kw['filled'] = CS.filled
11081172
ColorbarBase.__init__(self, ax, **kw)
@@ -1125,8 +1189,7 @@ def on_mappable_changed(self, mappable):
11251189
by :func:`colorbar_factory` and should not be called manually.
11261190
11271191
"""
1128-
self.set_cmap(mappable.get_cmap())
1129-
self.set_clim(mappable.get_clim())
1192+
_log.debug('colorbar mappable changed')
11301193
self.update_normal(mappable)
11311194

11321195
def add_lines(self, CS, erase=True):
@@ -1156,9 +1219,24 @@ def update_normal(self, mappable):
11561219
Update solid patches, lines, etc.
11571220
11581221
Unlike `.update_bruteforce`, this does not clear the axes. This is
1159-
meant to be called when the image or contour plot to which this
1160-
colorbar belongs changes.
1222+
meant to be called when the norm of the image or contour plot to which
1223+
this colorbar belongs changes.
1224+
1225+
If the norm on the mappable is different than before, this resets the
1226+
locator and formatter for the axis, so if these have been customized,
1227+
they will need to be customized again. However, if the norm only
1228+
changes values of *vmin*, *vmax* or *cmap* then the old formatter
1229+
and locator will be preserved.
11611230
"""
1231+
1232+
_log.debug('colorbar update normal %r %r', mappable.norm, self.norm)
1233+
self.mappable = mappable
1234+
self.set_alpha(mappable.get_alpha())
1235+
self.cmap = mappable.cmap
1236+
if mappable.norm != self.norm:
1237+
self.norm = mappable.norm
1238+
self._reset_locator_formatter_scale()
1239+
11621240
self.draw_all()
11631241
if isinstance(self.mappable, contour.ContourSet):
11641242
CS = self.mappable
@@ -1180,15 +1258,16 @@ def update_bruteforce(self, mappable):
11801258
# properties have been changed by methods other than the
11811259
# colorbar methods, those changes will be lost.
11821260
self.ax.cla()
1261+
self.locator = None
1262+
self.formatter = None
1263+
11831264
# clearing the axes will delete outline, patch, solids, and lines:
11841265
self.outline = None
11851266
self.patch = None
11861267
self.solids = None
11871268
self.lines = list()
11881269
self.dividers = None
1189-
self.set_alpha(mappable.get_alpha())
1190-
self.cmap = mappable.cmap
1191-
self.norm = mappable.norm
1270+
self.update_normal(mappable)
11921271
self.draw_all()
11931272
if isinstance(self.mappable, contour.ContourSet):
11941273
CS = self.mappable

lib/matplotlib/tests/test_colorbar.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
from matplotlib import rc_context
55
from matplotlib.testing.decorators import image_comparison
66
import matplotlib.pyplot as plt
7-
from matplotlib.colors import BoundaryNorm, LogNorm, PowerNorm
7+
from matplotlib.colors import BoundaryNorm, LogNorm, PowerNorm, Normalize
88
from matplotlib.cm import get_cmap
9-
from matplotlib.colorbar import ColorbarBase
10-
from matplotlib.ticker import LogLocator, LogFormatter
9+
from matplotlib.colorbar import ColorbarBase, _ColorbarLogLocator
10+
from matplotlib.ticker import LogLocator, LogFormatter, FixedLocator
1111

1212

1313
def _get_cmap_norms():
@@ -442,23 +442,69 @@ def test_colorbar_renorm():
442442
fig, ax = plt.subplots()
443443
im = ax.imshow(z)
444444
cbar = fig.colorbar(im)
445+
assert np.allclose(cbar.ax.yaxis.get_majorticklocs(),
446+
np.arange(0, 120000.1, 15000))
447+
448+
cbar.set_ticks([1, 2, 3])
449+
assert isinstance(cbar.locator, FixedLocator)
445450

446451
norm = LogNorm(z.min(), z.max())
447452
im.set_norm(norm)
448-
cbar.set_norm(norm)
449-
cbar.locator = LogLocator()
450-
cbar.formatter = LogFormatter()
451-
cbar.update_normal(im)
453+
assert isinstance(cbar.locator, _ColorbarLogLocator)
454+
assert np.allclose(cbar.ax.yaxis.get_majorticklocs(),
455+
np.logspace(-8, 5, 14))
456+
# note that set_norm removes the FixedLocator...
452457
assert np.isclose(cbar.vmin, z.min())
458+
cbar.set_ticks([1, 2, 3])
459+
assert isinstance(cbar.locator, FixedLocator)
460+
assert np.allclose(cbar.ax.yaxis.get_majorticklocs(),
461+
[1.0, 2.0, 3.0])
453462

454463
norm = LogNorm(z.min() * 1000, z.max() * 1000)
455464
im.set_norm(norm)
456-
cbar.set_norm(norm)
457-
cbar.update_normal(im)
458465
assert np.isclose(cbar.vmin, z.min() * 1000)
459466
assert np.isclose(cbar.vmax, z.max() * 1000)
460467

461468

469+
def test_colorbar_format():
470+
# make sure that format is passed properly
471+
x, y = np.ogrid[-4:4:31j, -4:4:31j]
472+
z = 120000*np.exp(-x**2 - y**2)
473+
474+
fig, ax = plt.subplots()
475+
im = ax.imshow(z)
476+
cbar = fig.colorbar(im, format='%4.2e')
477+
fig.canvas.draw()
478+
assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '6.00e+04'
479+
480+
# make sure that if we change the clim of the mappable that the
481+
# formatting is *not* lost:
482+
im.set_clim([4, 200])
483+
fig.canvas.draw()
484+
assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '8.00e+01'
485+
486+
# but if we change the norm:
487+
im.set_norm(LogNorm(vmin=0.1, vmax=10))
488+
fig.canvas.draw()
489+
assert (cbar.ax.yaxis.get_ticklabels()[0].get_text() ==
490+
r'$\mathdefault{10^{-1}}$')
491+
492+
493+
def test_colorbar_scale_reset():
494+
x, y = np.ogrid[-4:4:31j, -4:4:31j]
495+
z = 120000*np.exp(-x**2 - y**2)
496+
497+
fig, ax = plt.subplots()
498+
pcm = ax.pcolormesh(z, cmap='RdBu_r', rasterized=True)
499+
cbar = fig.colorbar(pcm, ax=ax)
500+
assert cbar.ax.yaxis.get_scale() == 'linear'
501+
502+
pcm.set_norm(LogNorm(vmin=1, vmax=100))
503+
assert cbar.ax.yaxis.get_scale() == 'log'
504+
pcm.set_norm(Normalize(vmin=-20, vmax=20))
505+
assert cbar.ax.yaxis.get_scale() == 'linear'
506+
507+
462508
def test_colorbar_get_ticks():
463509
with rc_context({'_internal.classic_mode': False}):
464510

tutorials/colors/colorbar_only.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,10 @@
88
Customized Colorbars
99
====================
1010
11-
:class:`~matplotlib.colorbar.ColorbarBase` derives from
12-
:mod:`~matplotlib.cm.ScalarMappable` and puts a colorbar in a specified axes,
13-
so it has everything needed for a standalone colorbar. It can be used as-is to
14-
make a colorbar for a given colormap; it does not need a mappable object like
15-
an image. In this tutorial we will explore what can be done with standalone
16-
colorbar.
11+
`~matplotlib.colorbar.ColorbarBase` puts a colorbar in a specified axes,
12+
and can make a colorbar for a given colormap; it does not need a mappable
13+
object like an image. In this tutorial we will explore what can be done with
14+
standalone colorbar.
1715
1816
Basic continuous colorbar
1917
-------------------------

0 commit comments

Comments
 (0)