Skip to content

Commit 5972691

Browse files
committed
ENH: colorbar ticks adjustable to colorbar size
1 parent f96fac0 commit 5972691

File tree

11 files changed

+1055
-1167
lines changed

11 files changed

+1055
-1167
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/axis.py

+1
Original file line numberDiff line numberDiff line change
@@ -2364,6 +2364,7 @@ def set_view_interval(self, vmin, vmax, ignore=False):
23642364
:meth:`~matplotlib.axes.Axes.set_ylim`.
23652365
23662366
"""
2367+
23672368
if ignore:
23682369
self.axes.viewLim.intervaly = vmin, vmax
23692370
else:

lib/matplotlib/colorbar.py

+184-41
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
@@ -43,6 +44,8 @@
4344

4445
from matplotlib import docstring
4546

47+
_log = logging.getLogger(__name__)
48+
4649
make_axes_kw_doc = '''
4750
4851
============= ====================================================
@@ -216,6 +219,65 @@ def _set_ticks_on_axis_warn(*args, **kw):
216219
warnings.warn("Use the colorbar set_ticks() method instead.")
217220

218221

222+
class ColorbarAutoLocator(ticker.MaxNLocator):
223+
"""
224+
AutoLocator for Colorbar
225+
226+
This locator is just a `.MaxNLocator` except the min and max are
227+
clipped by the norm's min and max (i.e. vmin/vmax from the
228+
image/pcolor/contour object). This is necessary so ticks don't
229+
extrude into the "extend regions".
230+
"""
231+
232+
def __init__(self, colorbar):
233+
"""
234+
ColorbarAutoLocator(colorbar)
235+
236+
This ticker needs to know the *colorbar* so that it can access
237+
its *vmin* and *vmax*. Otherwise it is the same as
238+
`~.ticker.AutoLocator`.
239+
"""
240+
241+
self._colorbar = colorbar
242+
nbins = 'auto'
243+
steps = [1, 2, 2.5, 5, 10]
244+
ticker.MaxNLocator.__init__(self, nbins=nbins, steps=steps)
245+
246+
def tick_values(self, vmin, vmax):
247+
vmin = max(vmin, self._colorbar.norm.vmin)
248+
vmax = min(vmax, self._colorbar.norm.vmax)
249+
return ticker.MaxNLocator.tick_values(self, vmin, vmax)
250+
251+
252+
class ColorbarLogLocator(ticker.LogLocator):
253+
"""
254+
LogLocator for Colorbarbar
255+
256+
This locator is just a `.LogLocator` except the min and max are
257+
clipped by the norm's min and max (i.e. vmin/vmax from the
258+
image/pcolor/contour object). This is necessary so ticks don't
259+
extrude into the "extend regions".
260+
261+
"""
262+
def __init__(self, colorbar, *args, **kwargs):
263+
"""
264+
ColorbarLogLocator(colorbar, *args, **kwargs)
265+
266+
This ticker needs to know the *colorbar* so that it can access
267+
its *vmin* and *vmax*. Otherwise it is the same as
268+
`~.ticker.LogLocator`. The ``*args`` and ``**kwargs`` are the
269+
same as `~.ticker.LogLocator`.
270+
"""
271+
self._colorbar = colorbar
272+
ticker.LogLocator.__init__(self, *args, **kwargs)
273+
274+
def tick_values(self, vmin, vmax):
275+
vmin = self._colorbar.norm.vmin
276+
vmax = self._colorbar.norm.vmax
277+
ticks = ticker.LogLocator.tick_values(self, vmin, vmax)
278+
return ticks[(ticks >= vmin) & (ticks <= vmax)]
279+
280+
219281
class ColorbarBase(cm.ScalarMappable):
220282
'''
221283
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)
@@ -357,34 +426,119 @@ def draw_all(self):
357426
def config_axis(self):
358427
ax = self.ax
359428
if self.orientation == 'vertical':
360-
ax.xaxis.set_ticks([])
361429
# location is either one of 'bottom' or 'top'
362430
ax.yaxis.set_label_position(self.ticklocation)
363431
ax.yaxis.set_ticks_position(self.ticklocation)
432+
if (isinstance(self.norm, colors.LogNorm)
433+
and self._use_adjustable()):
434+
ax.set_xscale('log')
435+
ax.set_yscale('log')
436+
ax.xaxis.set_ticks([])
437+
ax.xaxis.set_ticks([], minor=True)
438+
364439
else:
365-
ax.yaxis.set_ticks([])
366440
# location is either one of 'left' or 'right'
367441
ax.xaxis.set_label_position(self.ticklocation)
368442
ax.xaxis.set_ticks_position(self.ticklocation)
443+
if (isinstance(self.norm, colors.LogNorm)
444+
and self._use_adjustable()):
445+
ax.set_xscale('log')
446+
ax.set_yscale('log')
447+
ax.yaxis.set_ticks([])
448+
ax.yaxis.set_ticks([], minor=True)
369449

370450
self._set_label()
371451

452+
def _get_ticker_locator_formatter(self):
453+
"""
454+
This code looks at the norm being used by the colorbar
455+
and decides what locator and formatter to use. If ``locator`` has
456+
already been set by hand, it just returns
457+
``self.locator, self.formatter``.
458+
"""
459+
locator = self.locator
460+
formatter = self.formatter
461+
if locator is None:
462+
if self.boundaries is None:
463+
if isinstance(self.norm, colors.NoNorm):
464+
nv = len(self._values)
465+
base = 1 + int(nv / 10)
466+
locator = ticker.IndexLocator(base=base, offset=0)
467+
elif isinstance(self.norm, colors.BoundaryNorm):
468+
b = self.norm.boundaries
469+
locator = ticker.FixedLocator(b, nbins=10)
470+
elif isinstance(self.norm, colors.LogNorm):
471+
locator = ColorbarLogLocator(self)
472+
elif isinstance(self.norm, colors.SymLogNorm):
473+
# The subs setting here should be replaced
474+
# by logic in the locator.
475+
locator = ticker.SymmetricalLogLocator(
476+
subs=np.arange(1, 10),
477+
linthresh=self.norm.linthresh,
478+
base=10)
479+
else:
480+
if mpl.rcParams['_internal.classic_mode']:
481+
locator = ticker.MaxNLocator()
482+
else:
483+
locator = ColorbarAutoLocator(self)
484+
else:
485+
b = self._boundaries[self._inside]
486+
locator = ticker.FixedLocator(b, nbins=10)
487+
_log.debug('locator: %r', locator)
488+
return locator, formatter
489+
490+
def _use_adjustable(self):
491+
"""
492+
Return if we should use an adjustable tick locator or a fixed
493+
one. (check is used twice so factored out here...)
494+
"""
495+
return (self.boundaries is None
496+
and self.values is None
497+
and ((type(self.norm) == colors.Normalize)
498+
or (type(self.norm) == colors.LogNorm)))
499+
372500
def update_ticks(self):
373501
"""
374502
Force the update of the ticks and ticklabels. This must be
375503
called whenever the tick locator and/or tick formatter changes.
376504
"""
377505
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)
506+
# get the locator and formatter. Defaults to
507+
# self.locator if not None..
508+
locator, formatter = self._get_ticker_locator_formatter()
509+
510+
if self._use_adjustable():
511+
_log.debug('Using adjustable locator on colorbar')
512+
_log.debug('locator: %r', locator)
513+
#self._find_range()
514+
if self.orientation == 'vertical':
515+
ax.yaxis.set_major_locator(locator)
516+
ax.yaxis.set_major_formatter(formatter)
517+
if type(self.norm) == colors.LogNorm:
518+
ax.yaxis.set_minor_locator(ColorbarLogLocator(self,
519+
base=10., subs='auto'))
520+
ax.yaxis.set_minor_formatter(ticker.NullFormatter())
521+
522+
else:
523+
ax.xaxis.set_major_locator(locator)
524+
ax.xaxis.set_major_formatter(formatter)
525+
if type(self.norm) == colors.LogNorm:
526+
ax.xaxis.set_minor_locator(ColorbarLogLocator(self,
527+
base=10., subs='auto'))
528+
ax.xaxis.set_minor_formatter(ticker.NullFormatter())
383529

384530
else:
385-
ax.xaxis.set_ticks(ticks)
386-
ax.set_xticklabels(ticklabels)
387-
ax.xaxis.get_major_formatter().set_offset_string(offset_string)
531+
_log.debug('Using fixed locator on colorbar')
532+
ticks, ticklabels, offset_string = self._ticker(locator, formatter)
533+
if self.orientation == 'vertical':
534+
ax.yaxis.set_ticks(ticks)
535+
ax.set_yticklabels(ticklabels)
536+
ax.yaxis.get_major_formatter().set_offset_string(offset_string)
537+
538+
else:
539+
ax.xaxis.set_ticks(ticks)
540+
ax.set_xticklabels(ticklabels)
541+
ax.xaxis.get_major_formatter().set_offset_string(offset_string)
388542

389543
def set_ticks(self, ticks, update_ticks=True):
390544
"""
@@ -520,6 +674,7 @@ def _add_solids(self, X, Y, C):
520674
# since the axes object should already have hold set.
521675
_hold = self.ax._hold
522676
self.ax._hold = True
677+
_log.debug('Setting pcolormesh')
523678
col = self.ax.pcolormesh(*args, **kw)
524679
self.ax._hold = _hold
525680
#self.add_observer(col) # We should observe, not be observed...
@@ -574,39 +729,11 @@ def add_lines(self, levels, colors, linewidths, erase=True):
574729
self.ax.add_collection(col)
575730
self.stale = True
576731

577-
def _ticker(self):
732+
def _ticker(self, locator, formatter):
578733
'''
579734
Return the sequence of ticks (colorbar data locations),
580735
ticklabels (strings), and the corresponding offset string.
581736
'''
582-
locator = self.locator
583-
formatter = self.formatter
584-
if locator is None:
585-
if self.boundaries is None:
586-
if isinstance(self.norm, colors.NoNorm):
587-
nv = len(self._values)
588-
base = 1 + int(nv / 10)
589-
locator = ticker.IndexLocator(base=base, offset=0)
590-
elif isinstance(self.norm, colors.BoundaryNorm):
591-
b = self.norm.boundaries
592-
locator = ticker.FixedLocator(b, nbins=10)
593-
elif isinstance(self.norm, colors.LogNorm):
594-
locator = ticker.LogLocator(subs='all')
595-
elif isinstance(self.norm, colors.SymLogNorm):
596-
# The subs setting here should be replaced
597-
# by logic in the locator.
598-
locator = ticker.SymmetricalLogLocator(
599-
subs=np.arange(1, 10),
600-
linthresh=self.norm.linthresh,
601-
base=10)
602-
else:
603-
if mpl.rcParams['_internal.classic_mode']:
604-
locator = ticker.MaxNLocator()
605-
else:
606-
locator = ticker.AutoLocator()
607-
else:
608-
b = self._boundaries[self._inside]
609-
locator = ticker.FixedLocator(b, nbins=10)
610737
if isinstance(self.norm, colors.NoNorm) and self.boundaries is None:
611738
intv = self._values[0], self._values[-1]
612739
else:
@@ -851,12 +978,28 @@ def _mesh(self):
851978
y = self._uniform_y(self._central_N())
852979
else:
853980
y = self._proportional_y()
981+
# if boundaries and values are None, then we can go ahead and
982+
# scale this up for Auto tick location. Otherwise we
983+
# want to keep normalized between 0 and 1 and use manual tick
984+
# locations.
985+
if self._use_adjustable():
986+
y = self.norm.inverse(y)
987+
x = self.norm.inverse(x)
988+
else:
989+
dy = 1.0
854990
self._y = y
991+
855992
X, Y = np.meshgrid(x, y)
856993
if self._extend_lower() and not self.extendrect:
857-
X[0, :] = 0.5
994+
if self._use_adjustable():
995+
X[0, :] = self.norm.inverse(0.5)
996+
else:
997+
X[0, :] = 0.5
858998
if self._extend_upper() and not self.extendrect:
859-
X[-1, :] = 0.5
999+
if self._use_adjustable():
1000+
X[-1, :] = self.norm.inverse(0.5)
1001+
else:
1002+
X[-1, :] = 0.5
8601003
return X, Y
8611004

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

0 commit comments

Comments
 (0)