Skip to content

Commit 651d609

Browse files
committed
ENH: colorbar ticks adjustable to colorbar size
1 parent fc73593 commit 651d609

File tree

10 files changed

+1045
-1174
lines changed

10 files changed

+1045
-1174
lines changed
+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
The ticks for colorbar now adjust for the size of the colorbar
2+
--------------------------------------------------------------
3+
4+
Colorbar ticks now adjust for the size of the colorbar if the
5+
colorbar is made from a mappable that is not a contour or
6+
doesn't have a BoundaryNorm, or boundaries are not specified.
7+
If boundaries, etc are specified, the colorbar maintains the
8+
original behaviour.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Colorbar ticks can now be automatic
2+
-----------------------------------
3+
4+
The number of ticks on colorbars was appropriate for a large colorbar, but
5+
looked bad if the colorbar was made smaller (i.e. via the ``shrink`` kwarg).
6+
This has been changed so that the number of ticks is now responsive to how
7+
large the colorbar is.

lib/matplotlib/colorbar.py

+175-48
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import six
2525
from six.moves import xrange, zip
2626

27+
import logging
2728
import warnings
2829

2930
import numpy as np
@@ -44,6 +45,8 @@
4445
import matplotlib._constrained_layout as constrained_layout
4546
from matplotlib import docstring
4647

48+
_log = logging.getLogger(__name__)
49+
4750
make_axes_kw_doc = '''
4851
4952
============= ====================================================
@@ -217,6 +220,65 @@ def _set_ticks_on_axis_warn(*args, **kw):
217220
warnings.warn("Use the colorbar set_ticks() method instead.")
218221

219222

223+
class _ColorbarAutoLocator(ticker.MaxNLocator):
224+
"""
225+
AutoLocator for Colorbar
226+
227+
This locator is just a `.MaxNLocator` except the min and max are
228+
clipped by the norm's min and max (i.e. vmin/vmax from the
229+
image/pcolor/contour object). This is necessary so ticks don't
230+
extrude into the "extend regions".
231+
"""
232+
233+
def __init__(self, colorbar):
234+
"""
235+
_ColorbarAutoLocator(colorbar)
236+
237+
This ticker needs to know the *colorbar* so that it can access
238+
its *vmin* and *vmax*. Otherwise it is the same as
239+
`~.ticker.AutoLocator`.
240+
"""
241+
242+
self._colorbar = colorbar
243+
nbins = 'auto'
244+
steps = [1, 2, 2.5, 5, 10]
245+
ticker.MaxNLocator.__init__(self, nbins=nbins, steps=steps)
246+
247+
def tick_values(self, vmin, vmax):
248+
vmin = max(vmin, self._colorbar.norm.vmin)
249+
vmax = min(vmax, self._colorbar.norm.vmax)
250+
return ticker.MaxNLocator.tick_values(self, vmin, vmax)
251+
252+
253+
class _ColorbarLogLocator(ticker.LogLocator):
254+
"""
255+
LogLocator for Colorbarbar
256+
257+
This locator is just a `.LogLocator` except the min and max are
258+
clipped by the norm's min and max (i.e. vmin/vmax from the
259+
image/pcolor/contour object). This is necessary so ticks don't
260+
extrude into the "extend regions".
261+
262+
"""
263+
def __init__(self, colorbar, *args, **kwargs):
264+
"""
265+
_ColorbarLogLocator(colorbar, *args, **kwargs)
266+
267+
This ticker needs to know the *colorbar* so that it can access
268+
its *vmin* and *vmax*. Otherwise it is the same as
269+
`~.ticker.LogLocator`. The ``*args`` and ``**kwargs`` are the
270+
same as `~.ticker.LogLocator`.
271+
"""
272+
self._colorbar = colorbar
273+
ticker.LogLocator.__init__(self, *args, **kwargs)
274+
275+
def tick_values(self, vmin, vmax):
276+
vmin = self._colorbar.norm.vmin
277+
vmax = self._colorbar.norm.vmax
278+
ticks = ticker.LogLocator.tick_values(self, vmin, vmax)
279+
return ticks[(ticks >= vmin) & (ticks <= vmax)]
280+
281+
220282
class ColorbarBase(cm.ScalarMappable):
221283
'''
222284
Draw a colorbar in an existing axes.
@@ -346,8 +408,15 @@ def draw_all(self):
346408
and do all the drawing.
347409
'''
348410

411+
# sets self._boundaries and self._values in real data units.
412+
# takes into account extend values:
349413
self._process_values()
414+
# sets self.vmin and vmax in data units, but just for
415+
# the part of the colorbar that is not part of the extend
416+
# patch:
350417
self._find_range()
418+
# returns the X and Y mesh, *but* this was/is in normalized
419+
# units:
351420
X, Y = self._mesh()
352421
C = self._values[:, np.newaxis]
353422
self._config_axes(X, Y)
@@ -356,35 +425,108 @@ def draw_all(self):
356425

357426
def config_axis(self):
358427
ax = self.ax
428+
if (isinstance(self.norm, colors.LogNorm)
429+
and self._use_auto_colorbar_locator()):
430+
# *both* axes are made log so that determining the
431+
# mid point is easier.
432+
ax.set_xscale('log')
433+
ax.set_yscale('log')
434+
359435
if self.orientation == 'vertical':
360-
ax.xaxis.set_ticks([])
361-
# location is either one of 'bottom' or 'top'
362-
ax.yaxis.set_label_position(self.ticklocation)
363-
ax.yaxis.set_ticks_position(self.ticklocation)
436+
long_axis, short_axis = ax.yaxis, ax.xaxis
364437
else:
365-
ax.yaxis.set_ticks([])
366-
# location is either one of 'left' or 'right'
367-
ax.xaxis.set_label_position(self.ticklocation)
368-
ax.xaxis.set_ticks_position(self.ticklocation)
438+
long_axis, short_axis = ax.xaxis, ax.yaxis
439+
440+
long_axis.set_label_position(self.ticklocation)
441+
long_axis.set_ticks_position(self.ticklocation)
442+
short_axis.set_ticks([])
443+
short_axis.set_ticks([], minor=True)
369444

370445
self._set_label()
371446

447+
def _get_ticker_locator_formatter(self):
448+
"""
449+
This code looks at the norm being used by the colorbar
450+
and decides what locator and formatter to use. If ``locator`` has
451+
already been set by hand, it just returns
452+
``self.locator, self.formatter``.
453+
"""
454+
locator = self.locator
455+
formatter = self.formatter
456+
if locator is None:
457+
if self.boundaries is None:
458+
if isinstance(self.norm, colors.NoNorm):
459+
nv = len(self._values)
460+
base = 1 + int(nv / 10)
461+
locator = ticker.IndexLocator(base=base, offset=0)
462+
elif isinstance(self.norm, colors.BoundaryNorm):
463+
b = self.norm.boundaries
464+
locator = ticker.FixedLocator(b, nbins=10)
465+
elif isinstance(self.norm, colors.LogNorm):
466+
locator = _ColorbarLogLocator(self)
467+
elif isinstance(self.norm, colors.SymLogNorm):
468+
# The subs setting here should be replaced
469+
# by logic in the locator.
470+
locator = ticker.SymmetricalLogLocator(
471+
subs=np.arange(1, 10),
472+
linthresh=self.norm.linthresh,
473+
base=10)
474+
else:
475+
if mpl.rcParams['_internal.classic_mode']:
476+
locator = ticker.MaxNLocator()
477+
else:
478+
locator = _ColorbarAutoLocator(self)
479+
else:
480+
b = self._boundaries[self._inside]
481+
locator = ticker.FixedLocator(b, nbins=10)
482+
_log.debug('locator: %r', locator)
483+
return locator, formatter
484+
485+
def _use_auto_colorbar_locator(self):
486+
"""
487+
Return if we should use an adjustable tick locator or a fixed
488+
one. (check is used twice so factored out here...)
489+
"""
490+
return (self.boundaries is None
491+
and self.values is None
492+
and ((type(self.norm) == colors.Normalize)
493+
or (type(self.norm) == colors.LogNorm)))
494+
372495
def update_ticks(self):
373496
"""
374497
Force the update of the ticks and ticklabels. This must be
375498
called whenever the tick locator and/or tick formatter changes.
376499
"""
377500
ax = self.ax
378-
ticks, ticklabels, offset_string = self._ticker()
379-
if self.orientation == 'vertical':
380-
ax.yaxis.set_ticks(ticks)
381-
ax.set_yticklabels(ticklabels)
382-
ax.yaxis.get_major_formatter().set_offset_string(offset_string)
383-
501+
# get the locator and formatter. Defaults to
502+
# self.locator if not None..
503+
locator, formatter = self._get_ticker_locator_formatter()
504+
505+
if self._use_auto_colorbar_locator():
506+
_log.debug('Using auto colorbar locator on colorbar')
507+
_log.debug('locator: %r', locator)
508+
if self.orientation == 'vertical':
509+
long_axis, short_axis = ax.yaxis, ax.xaxis
510+
else:
511+
long_axis, short_axis = ax.xaxis, ax.yaxis
512+
long_axis.set_major_locator(locator)
513+
long_axis.set_major_formatter(formatter)
514+
if type(self.norm) == colors.LogNorm:
515+
long_axis.set_minor_locator(_ColorbarLogLocator(self,
516+
base=10., subs='auto'))
517+
long_axis.set_minor_formatter(ticker.NullFormatter())
384518
else:
385-
ax.xaxis.set_ticks(ticks)
386-
ax.set_xticklabels(ticklabels)
387-
ax.xaxis.get_major_formatter().set_offset_string(offset_string)
519+
_log.debug('Using fixed locator on colorbar')
520+
ticks, ticklabels, offset_string = self._ticker(locator, formatter)
521+
if self.orientation == 'vertical':
522+
ax.yaxis.set_ticks(ticks)
523+
ax.set_yticklabels(ticklabels)
524+
ax.yaxis.get_major_formatter().set_offset_string(offset_string)
525+
526+
else:
527+
ax.xaxis.set_ticks(ticks)
528+
ax.set_xticklabels(ticklabels)
529+
ax.xaxis.get_major_formatter().set_offset_string(offset_string)
388530

389531
def set_ticks(self, ticks, update_ticks=True):
390532
"""
@@ -520,6 +662,7 @@ def _add_solids(self, X, Y, C):
520662
# since the axes object should already have hold set.
521663
_hold = self.ax._hold
522664
self.ax._hold = True
665+
_log.debug('Setting pcolormesh')
523666
col = self.ax.pcolormesh(*args, **kw)
524667
self.ax._hold = _hold
525668
#self.add_observer(col) # We should observe, not be observed...
@@ -573,39 +716,11 @@ def add_lines(self, levels, colors, linewidths, erase=True):
573716
self.ax.add_collection(col)
574717
self.stale = True
575718

576-
def _ticker(self):
719+
def _ticker(self, locator, formatter):
577720
'''
578721
Return the sequence of ticks (colorbar data locations),
579722
ticklabels (strings), and the corresponding offset string.
580723
'''
581-
locator = self.locator
582-
formatter = self.formatter
583-
if locator is None:
584-
if self.boundaries is None:
585-
if isinstance(self.norm, colors.NoNorm):
586-
nv = len(self._values)
587-
base = 1 + int(nv / 10)
588-
locator = ticker.IndexLocator(base=base, offset=0)
589-
elif isinstance(self.norm, colors.BoundaryNorm):
590-
b = self.norm.boundaries
591-
locator = ticker.FixedLocator(b, nbins=10)
592-
elif isinstance(self.norm, colors.LogNorm):
593-
locator = ticker.LogLocator(subs='all')
594-
elif isinstance(self.norm, colors.SymLogNorm):
595-
# The subs setting here should be replaced
596-
# by logic in the locator.
597-
locator = ticker.SymmetricalLogLocator(
598-
subs=np.arange(1, 10),
599-
linthresh=self.norm.linthresh,
600-
base=10)
601-
else:
602-
if mpl.rcParams['_internal.classic_mode']:
603-
locator = ticker.MaxNLocator()
604-
else:
605-
locator = ticker.AutoLocator()
606-
else:
607-
b = self._boundaries[self._inside]
608-
locator = ticker.FixedLocator(b, nbins=10)
609724
if isinstance(self.norm, colors.NoNorm) and self.boundaries is None:
610725
intv = self._values[0], self._values[-1]
611726
else:
@@ -845,17 +960,29 @@ def _mesh(self):
845960
transposition for a horizontal colorbar are done outside
846961
this function.
847962
'''
963+
# if boundaries and values are None, then we can go ahead and
964+
# scale this up for Auto tick location. Otherwise we
965+
# want to keep normalized between 0 and 1 and use manual tick
966+
# locations.
967+
848968
x = np.array([0.0, 1.0])
849969
if self.spacing == 'uniform':
850970
y = self._uniform_y(self._central_N())
851971
else:
852972
y = self._proportional_y()
973+
if self._use_auto_colorbar_locator():
974+
y = self.norm.inverse(y)
975+
x = self.norm.inverse(x)
853976
self._y = y
854977
X, Y = np.meshgrid(x, y)
978+
if self._use_auto_colorbar_locator():
979+
xmid = self.norm.inverse(0.5)
980+
else:
981+
xmid = 0.5
855982
if self._extend_lower() and not self.extendrect:
856-
X[0, :] = 0.5
983+
X[0, :] = xmid
857984
if self._extend_upper() and not self.extendrect:
858-
X[-1, :] = 0.5
985+
X[-1, :] = xmid
859986
return X, Y
860987

861988
def _locate(self, x):
Binary file not shown.

0 commit comments

Comments
 (0)