Skip to content

Commit dbfc79c

Browse files
authored
Merge pull request #9903 from jklymak/enh-colorbar-ticks
ENH: adjustable colorbar ticks
2 parents 197bb59 + b74c0fd commit dbfc79c

File tree

10 files changed

+1039
-1173
lines changed

10 files changed

+1039
-1173
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

+169-47
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
2020
'''
2121

22+
import logging
2223
import warnings
2324

2425
import numpy as np
@@ -39,6 +40,8 @@
3940
import matplotlib._constrained_layout as constrained_layout
4041
from matplotlib import docstring
4142

43+
_log = logging.getLogger(__name__)
44+
4245
make_axes_kw_doc = '''
4346
4447
============= ====================================================
@@ -212,6 +215,63 @@ def _set_ticks_on_axis_warn(*args, **kw):
212215
warnings.warn("Use the colorbar set_ticks() method instead.")
213216

214217

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

404+
# sets self._boundaries and self._values in real data units.
405+
# takes into account extend values:
344406
self._process_values()
407+
# sets self.vmin and vmax in data units, but just for
408+
# the part of the colorbar that is not part of the extend
409+
# patch:
345410
self._find_range()
411+
# returns the X and Y mesh, *but* this was/is in normalized
412+
# units:
346413
X, Y = self._mesh()
347414
C = self._values[:, np.newaxis]
348415
self._config_axes(X, Y)
@@ -351,35 +418,105 @@ def draw_all(self):
351418

352419
def config_axis(self):
353420
ax = self.ax
421+
if (isinstance(self.norm, colors.LogNorm)
422+
and self._use_auto_colorbar_locator()):
423+
# *both* axes are made log so that determining the
424+
# mid point is easier.
425+
ax.set_xscale('log')
426+
ax.set_yscale('log')
427+
354428
if self.orientation == 'vertical':
355-
ax.xaxis.set_ticks([])
356-
# location is either one of 'bottom' or 'top'
357-
ax.yaxis.set_label_position(self.ticklocation)
358-
ax.yaxis.set_ticks_position(self.ticklocation)
429+
long_axis, short_axis = ax.yaxis, ax.xaxis
359430
else:
360-
ax.yaxis.set_ticks([])
361-
# location is either one of 'left' or 'right'
362-
ax.xaxis.set_label_position(self.ticklocation)
363-
ax.xaxis.set_ticks_position(self.ticklocation)
431+
long_axis, short_axis = ax.xaxis, ax.yaxis
432+
433+
long_axis.set_label_position(self.ticklocation)
434+
long_axis.set_ticks_position(self.ticklocation)
435+
short_axis.set_ticks([])
436+
short_axis.set_ticks([], minor=True)
364437

365438
self._set_label()
366439

440+
def _get_ticker_locator_formatter(self):
441+
"""
442+
This code looks at the norm being used by the colorbar
443+
and decides what locator and formatter to use. If ``locator`` has
444+
already been set by hand, it just returns
445+
``self.locator, self.formatter``.
446+
"""
447+
locator = self.locator
448+
formatter = self.formatter
449+
if locator is None:
450+
if self.boundaries is None:
451+
if isinstance(self.norm, colors.NoNorm):
452+
nv = len(self._values)
453+
base = 1 + int(nv / 10)
454+
locator = ticker.IndexLocator(base=base, offset=0)
455+
elif isinstance(self.norm, colors.BoundaryNorm):
456+
b = self.norm.boundaries
457+
locator = ticker.FixedLocator(b, nbins=10)
458+
elif isinstance(self.norm, colors.LogNorm):
459+
locator = _ColorbarLogLocator(self)
460+
elif isinstance(self.norm, colors.SymLogNorm):
461+
# The subs setting here should be replaced
462+
# by logic in the locator.
463+
locator = ticker.SymmetricalLogLocator(
464+
subs=np.arange(1, 10),
465+
linthresh=self.norm.linthresh,
466+
base=10)
467+
else:
468+
if mpl.rcParams['_internal.classic_mode']:
469+
locator = ticker.MaxNLocator()
470+
else:
471+
locator = _ColorbarAutoLocator(self)
472+
else:
473+
b = self._boundaries[self._inside]
474+
locator = ticker.FixedLocator(b, nbins=10)
475+
_log.debug('locator: %r', locator)
476+
return locator, formatter
477+
478+
def _use_auto_colorbar_locator(self):
479+
"""
480+
Return if we should use an adjustable tick locator or a fixed
481+
one. (check is used twice so factored out here...)
482+
"""
483+
return (self.boundaries is None
484+
and self.values is None
485+
and ((type(self.norm) == colors.Normalize)
486+
or (type(self.norm) == colors.LogNorm)))
487+
367488
def update_ticks(self):
368489
"""
369490
Force the update of the ticks and ticklabels. This must be
370491
called whenever the tick locator and/or tick formatter changes.
371492
"""
372493
ax = self.ax
373-
ticks, ticklabels, offset_string = self._ticker()
374-
if self.orientation == 'vertical':
375-
ax.yaxis.set_ticks(ticks)
376-
ax.set_yticklabels(ticklabels)
377-
ax.yaxis.get_major_formatter().set_offset_string(offset_string)
494+
# get the locator and formatter. Defaults to
495+
# self.locator if not None..
496+
locator, formatter = self._get_ticker_locator_formatter()
378497

498+
if self.orientation == 'vertical':
499+
long_axis, short_axis = ax.yaxis, ax.xaxis
379500
else:
380-
ax.xaxis.set_ticks(ticks)
381-
ax.set_xticklabels(ticklabels)
382-
ax.xaxis.get_major_formatter().set_offset_string(offset_string)
501+
long_axis, short_axis = ax.xaxis, ax.yaxis
502+
503+
if self._use_auto_colorbar_locator():
504+
_log.debug('Using auto colorbar locator on colorbar')
505+
_log.debug('locator: %r', locator)
506+
long_axis.set_major_locator(locator)
507+
long_axis.set_major_formatter(formatter)
508+
if type(self.norm) == colors.LogNorm:
509+
long_axis.set_minor_locator(_ColorbarLogLocator(self,
510+
base=10., subs='auto'))
511+
long_axis.set_minor_formatter(
512+
ticker.LogFormatter()
513+
)
514+
else:
515+
_log.debug('Using fixed locator on colorbar')
516+
ticks, ticklabels, offset_string = self._ticker(locator, formatter)
517+
long_axis.set_ticks(ticks)
518+
long_axis.set_ticklabels(ticklabels)
519+
long_axis.get_major_formatter().set_offset_string(offset_string)
383520

384521
def set_ticks(self, ticks, update_ticks=True):
385522
"""
@@ -515,6 +652,7 @@ def _add_solids(self, X, Y, C):
515652
# since the axes object should already have hold set.
516653
_hold = self.ax._hold
517654
self.ax._hold = True
655+
_log.debug('Setting pcolormesh')
518656
col = self.ax.pcolormesh(*args, **kw)
519657
self.ax._hold = _hold
520658
#self.add_observer(col) # We should observe, not be observed...
@@ -568,39 +706,11 @@ def add_lines(self, levels, colors, linewidths, erase=True):
568706
self.ax.add_collection(col)
569707
self.stale = True
570708

571-
def _ticker(self):
709+
def _ticker(self, locator, formatter):
572710
'''
573711
Return the sequence of ticks (colorbar data locations),
574712
ticklabels (strings), and the corresponding offset string.
575713
'''
576-
locator = self.locator
577-
formatter = self.formatter
578-
if locator is None:
579-
if self.boundaries is None:
580-
if isinstance(self.norm, colors.NoNorm):
581-
nv = len(self._values)
582-
base = 1 + int(nv / 10)
583-
locator = ticker.IndexLocator(base=base, offset=0)
584-
elif isinstance(self.norm, colors.BoundaryNorm):
585-
b = self.norm.boundaries
586-
locator = ticker.FixedLocator(b, nbins=10)
587-
elif isinstance(self.norm, colors.LogNorm):
588-
locator = ticker.LogLocator(subs='all')
589-
elif isinstance(self.norm, colors.SymLogNorm):
590-
# The subs setting here should be replaced
591-
# by logic in the locator.
592-
locator = ticker.SymmetricalLogLocator(
593-
subs=np.arange(1, 10),
594-
linthresh=self.norm.linthresh,
595-
base=10)
596-
else:
597-
if mpl.rcParams['_internal.classic_mode']:
598-
locator = ticker.MaxNLocator()
599-
else:
600-
locator = ticker.AutoLocator()
601-
else:
602-
b = self._boundaries[self._inside]
603-
locator = ticker.FixedLocator(b, nbins=10)
604714
if isinstance(self.norm, colors.NoNorm) and self.boundaries is None:
605715
intv = self._values[0], self._values[-1]
606716
else:
@@ -840,17 +950,29 @@ def _mesh(self):
840950
transposition for a horizontal colorbar are done outside
841951
this function.
842952
'''
953+
# if boundaries and values are None, then we can go ahead and
954+
# scale this up for Auto tick location. Otherwise we
955+
# want to keep normalized between 0 and 1 and use manual tick
956+
# locations.
957+
843958
x = np.array([0.0, 1.0])
844959
if self.spacing == 'uniform':
845960
y = self._uniform_y(self._central_N())
846961
else:
847962
y = self._proportional_y()
963+
if self._use_auto_colorbar_locator():
964+
y = self.norm.inverse(y)
965+
x = self.norm.inverse(x)
848966
self._y = y
849967
X, Y = np.meshgrid(x, y)
968+
if self._use_auto_colorbar_locator():
969+
xmid = self.norm.inverse(0.5)
970+
else:
971+
xmid = 0.5
850972
if self._extend_lower() and not self.extendrect:
851-
X[0, :] = 0.5
973+
X[0, :] = xmid
852974
if self._extend_upper() and not self.extendrect:
853-
X[-1, :] = 0.5
975+
X[-1, :] = xmid
854976
return X, Y
855977

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

0 commit comments

Comments
 (0)