Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

NF - control the length of colorbar extensions #766

Merged
merged 1 commit into from

6 participants

@ajdawson

Added the ability to control the length of colorbar extensions using a new keyword argument "extendfrac" to matplotlib.pyplot.colorbar.

lib/matplotlib/colorbar.py
((9 lines not shown))
*extend* [ 'neither' | 'both' | 'min' | 'max' ]
If not 'neither', make pointed end(s) for out-of-
range values. These are set for a given colormap
using the colormap set_under and set_over methods.
+ *extendfrac* [ 'default' | None | 'auto' | length | lengths ]
@WeatherGod Collaborator

In mpl, we use None as synonymous with default (typically from an rcparam). I would get rid of the use of 'neither' and stick with None. Also, in documentation, surround None with asterisks.

@WeatherGod Collaborator
@efiring Owner
efiring added a note

I agree that it would be cleaner to eliminate "default" in favor of None in this case. @ajdawson is correct that "neither" must be left alone as an option to the original extend; I think that @WeatherGod actually meant "default".

@tonysyu
tonysyu added a note

@WeatherGod: This is really off-topic, but is there any chance that Matplotlib will migrate to Numpy's docstring standards?

@WeatherGod Collaborator

Correct, I meant "default', sorry for the confusion.

Tony, I have to read through their standard and see how comprehensive they are. Maybe mpl's standard might have to be a superset of it? I do have some minor issues with how they do things, but any standard is better than none!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/colorbar.py
((9 lines not shown))
*extend* [ 'neither' | 'both' | 'min' | 'max' ]
If not 'neither', make pointed end(s) for out-of-
range values. These are set for a given colormap
using the colormap set_under and set_over methods.
+ *extendfrac* [ 'default' | None | 'auto' | length | lengths ]
+ If 'auto', make the triangular colorbar extensions
@WeatherGod Collaborator

The default case should also be mentioned first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/colorbar.py
@@ -613,16 +630,26 @@ def _uniform_y(self, N):
if self.extend == 'neither':
y = np.linspace(0, 1, N)
else:
+ extendlength = 0.05
@WeatherGod Collaborator

Maybe this stuff should be refactored into a internal function (i.e., one that has a name that starts with an underscore) so that it is not repeated twice?

I've thought about refactoring this code, I'm not sure it would make the code easier to understand/maintain. What happens inside the conditional statements is really rather different in _uniform_y and _proportional_y. Using one method to resolve both these cases, I believe could result in longer and more difficult to understand code. Suggestions welcome of course.

@WeatherGod Collaborator

I took another look, and you are right, it really can't be refactored, I don't think. At the least, maybe add some comments in each spot pointing out the other's similar logic? There was a problem a year ago where someone made some fixes to the histogram function for one of the inputs, but then completely missed the almost identical logic for another input parameter elsewhere in the function. So I ended up adding comments so that future maintainers know that if they make a change in this one spot, then they will likely need to make a change in the other spot as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/colorbar.py
@@ -613,16 +635,31 @@ def _uniform_y(self, N):
if self.extend == 'neither':
y = np.linspace(0, 1, N)
else:
+ # Set the extension length as a fraction of the colorbar.
+ # We first check for automatic length setting, then try to
+ # use a user supplied value, and fall back to 0.05 if no
+ # valid option was specified. The same checks are performed
+ # in the method _proportional_y.
+ extendlength = 0.05
+ if isinstance(self.extendfrac, str):
+ if self.extendfrac.lower() == 'auto':
+ extendlength = 1. / N
+ elif self.extendfrac is not None:
+ extendlength = self.extendfrac
+ try:
@pelson Collaborator
pelson added a note

I wonder if the pythonic way would be to try to do the subtraction, and if it fails to raise the exception, rather than silently accepting a default value? Does anyone else have a feeling on this?

Additionally, if this try block coincides with a user pressing ctrl+C a KeyboardInterrupt is silently ignored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@mdboom
Owner

Can you add an example and a unit test?

@ajdawson

With regards to a unit test, I think need some advice.

With the revision of master that this PR is branched from, many tests are failing, I think due to problems comparing images. This is fixed in the current master. I am not able to write a functioning test in this branch without these fixes.

It says in the developer's guide that one should not merge the current master into a feature branch to keep history as clean as possible. What would be the best thing to do in this scenario?

@WeatherGod
Collaborator

You will need to rebase your branch against the current master anyway. When you rebase, you will need to address any merge conflicts and then push that result back up to your github repo. The changes should then be reflected here. You can then made additional commits for unit tests as needed.

@mdboom
Owner

@ajdawson: Agreed about not merging master into the feature branch. However, rebasing the feature branch on master is fine -- it basically replacing your branch's history with a new history built on top of the new master. I find this quite useful to revive out of date branches.

@ajdawson

I guess it is safe to ignore the usual warnings about not rebasing stuff that has been pushed to a public repository in this instance and force git to push the changes?

@mdboom
Owner

Yes. Force pushing is only usually a problem if you know other people have based a branch off of yours. So that definitely applies to the master and v1.1.x branches in the main matplotlib fork. For most pull requests that are sheperded along by one author, this doesn't apply.

@ajdawson

I've written a new test module for testing colorbars with extensions. I've also added to the existing colorbar example in the API examples, it seemed like the most appropriate place to put an example of this feature.

Let me know if there is something I have overlooked.

@pelson pelson commented on the diff
lib/matplotlib/colorbar.py
((12 lines not shown))
+ if isinstance(frac, str):
+ if frac.lower() == 'auto':
+ # Use the provided values when 'auto' is required.
+ extendlength[0] = automin
+ extendlength[1] = automax
+ else:
+ # Any other string is invalid.
+ raise ValueError('invalid value for extendfrac')
+ elif frac is not None:
+ try:
+ # Try to set min and max extension fractions directly.
+ extendlength[:] = frac
+ # If frac is a sequence contaning None then NaN may
+ # be encountered. This is an error.
+ if np.isnan(extendlength).any():
+ raise ValueError
@pelson Collaborator
pelson added a note

Stylistically this should be a ValueError instance. i.e. ValueError().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@pelson
Collaborator

I think this is a really well put together PR and, IMHO, is at a stage where this is good to be merged in. Thanks for all your work @ajdawson.

@pelson
Collaborator

+1 from me.

@WeatherGod
Collaborator

Agreed. The only thing missing that I can see is an addition to the "matplotlib/doc/users/whats_new.rst" and "matplotlib/doc/api/api_changes.rst". At least one of the two, if not both.

@ajdawson

I just followed the style of the other entries in the whats_new.rst and api_changes.rst files. I hope these are appropriate.

@WeatherGod
Collaborator

Your edits look good, just need a rebase

@ajdawson ajdawson NF - control the length of colorbar extensions
Added ability to control length of colorbar extensions
using a new keyword argument "extendfrac". Tests and examples
are also included.
c33a09f
@ajdawson

I rebased onto master and squashed my commits into a single commit.

@WeatherGod
Collaborator

Looks good. Thanks for your work. Merging...

@WeatherGod WeatherGod merged commit a2d44d5 into matplotlib:master
@ajdawson ajdawson deleted the ajdawson:colorbar-extensions branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 19, 2012
  1. @ajdawson

    NF - control the length of colorbar extensions

    ajdawson authored ajdawson committed
    Added ability to control length of colorbar extensions
    using a new keyword argument "extendfrac". Tests and examples
    are also included.
This page is out of date. Refresh to see the latest.
View
3  doc/api/api_changes.rst
@@ -51,6 +51,9 @@ Changes in 1.2.x
matplotlib axes by providing a ``_as_mpl_axes`` method. See
:ref:`adding-new-scales` for more detail.
+* A new keyword *extendfrac* in :meth:`~matplotlib.pyplot.colorbar` and
+ :class:`~matplotlib.colorbar.ColorbarBase` allows one to control the size of
+ the triangular minimum and maximum extensions on colorbars.
Changes in 1.1.x
================
View
31 doc/users/whats_new.rst
@@ -26,6 +26,37 @@ Damon McDougall added a new plotting method for the
.. plot:: mpl_examples/mplot3d/trisurf3d_demo.py
+Control the lengths of colorbar extensions
+------------------------------------------
+
+Andrew Dawson added a new keyword argument *extendfrac* to
+:meth:`~matplotlib.pyplot.colorbar` to control the length of
+minimum and maximum colorbar extensions.
+
+.. plot::
+
+ import matplotlib.pyplot as plt
+ import numpy as np
+
+ x = y = np.linspace(0., 2*np.pi, 100)
+ X, Y = np.meshgrid(x, y)
+ Z = np.cos(X) * np.sin(0.5*Y)
+
+ clevs = [-.75, -.5, -.25, 0., .25, .5, .75]
+ cmap = plt.cm.get_cmap(name='jet', lut=8)
+
+ ax1 = plt.subplot(211)
+ cs1 = plt.contourf(x, y, Z, clevs, cmap=cmap, extend='both')
+ cb1 = plt.colorbar(orientation='horizontal', extendfrac=None)
+ cb1.set_label('Default length colorbar extensions')
+
+ ax2 = plt.subplot(212)
+ cs2 = plt.contourf(x, y, Z, clevs, cmap=cmap, extend='both')
+ cb2 = plt.colorbar(orientation='horizontal', extendfrac='auto')
+ cb2.set_label('Custom length colorbar extensions')
+
+ plt.show()
+
.. _whats-new-1-1:
new in matplotlib-1.1
View
27 examples/api/colorbar_only.py
@@ -6,8 +6,9 @@
# Make a figure and axes with dimensions as desired.
fig = pyplot.figure(figsize=(8,3))
-ax1 = fig.add_axes([0.05, 0.65, 0.9, 0.15])
-ax2 = fig.add_axes([0.05, 0.25, 0.9, 0.15])
+ax1 = fig.add_axes([0.05, 0.80, 0.9, 0.15])
+ax2 = fig.add_axes([0.05, 0.475, 0.9, 0.15])
+ax3 = fig.add_axes([0.05, 0.15, 0.9, 0.15])
# Set the colormap and norm to correspond to the data for which
# the colorbar will be used.
@@ -47,5 +48,27 @@
orientation='horizontal')
cb2.set_label('Discrete intervals, some other units')
+# The third example illustrates the use of custom length colorbar
+# extensions, used on a colorbar with discrete intervals.
+cmap = mpl.colors.ListedColormap([[0., .4, 1.], [0., .8, 1.],
+ [1., .8, 0.], [1., .4, 0.]])
+cmap.set_over((1., 0., 0.))
+cmap.set_under((0., 0., 1.))
+
+bounds = [-1., -.5, 0., .5, 1.]
+norm = mpl.colors.BoundaryNorm(bounds, cmap.N)
+cb3 = mpl.colorbar.ColorbarBase(ax3, cmap=cmap,
+ norm=norm,
+ boundaries=[-10]+bounds+[10],
+ extend='both',
+ # Make the length of each extension
+ # the same as the length of the
+ # interior colors:
+ extendfrac='auto',
+ ticks=bounds,
+ spacing='uniform',
+ orientation='horizontal')
+cb3.set_label('Custom extension lengths, some other units')
+
pyplot.show()
View
3  lib/matplotlib/__init__.py
@@ -992,7 +992,8 @@ def tk_window_focus():
'matplotlib.tests.test_text',
'matplotlib.tests.test_tightlayout',
'matplotlib.tests.test_delaunay',
- 'matplotlib.tests.test_legend'
+ 'matplotlib.tests.test_legend',
+ 'matplotlib.tests.test_colorbar',
]
def test(verbosity=1):
View
89 lib/matplotlib/colorbar.py
@@ -59,13 +59,29 @@
colormap_kw_doc = '''
- =========== ====================================================
+ ============ ====================================================
Property Description
- =========== ====================================================
+ ============ ====================================================
*extend* [ 'neither' | 'both' | 'min' | 'max' ]
If not 'neither', make pointed end(s) for out-of-
range values. These are set for a given colormap
using the colormap set_under and set_over methods.
+ *extendfrac* [ *None* | 'auto' | length | lengths ]
+ If set to *None*, both the minimum and maximum
+ triangular colorbar extensions with have a length of
+ 5% of the interior colorbar length (this is the
+ default setting). If set to 'auto', makes the
+ triangular colorbar extensions the same lengths as
+ the interior boxes (when *spacing* is set to
+ 'uniform') or the same lengths as the respective
+ adjacent interior boxes (when *spacing* is set to
+ 'proportional'). If a scalar, indicates the length
+ of both the minimum and maximum triangular colorbar
+ extensions as a fraction of the interior colorbar
+ length. A two-element sequence of fractions may also
+ be given, indicating the lengths of the minimum and
+ maximum colorbar extensions respectively as a
+ fraction of the interior colorbar length.
*spacing* [ 'uniform' | 'proportional' ]
Uniform spacing gives each discrete color the same
space; proportional makes the space proportional to
@@ -82,7 +98,7 @@
given instead.
*drawedges* [ False | True ] If true, draw lines at color
boundaries.
- =========== ====================================================
+ ============ ====================================================
The following will probably be useful only in the context of
indexed colors (that is, when the mappable has norm=NoNorm()),
@@ -221,6 +237,7 @@ def __init__(self, ax, cmap=None,
format=None,
drawedges=False,
filled=True,
+ extendfrac=None,
):
self.ax = ax
self._patch_ax()
@@ -236,6 +253,7 @@ def __init__(self, ax, cmap=None,
self.orientation = orientation
self.drawedges = drawedges
self.filled = filled
+ self.extendfrac = extendfrac
self.solids = None
self.lines = None
self.outline = None
@@ -616,6 +634,35 @@ def _extended_N(self):
N += 1
return N
+ def _get_extension_lengths(self, frac, automin, automax, default=0.05):
+ '''
+ Get the lengths of colorbar extensions.
+
+ A helper method for _uniform_y and _proportional_y.
+ '''
+ # Set the default value.
+ extendlength = np.array([default, default])
+ if isinstance(frac, str):
+ if frac.lower() == 'auto':
+ # Use the provided values when 'auto' is required.
+ extendlength[0] = automin
+ extendlength[1] = automax
+ else:
+ # Any other string is invalid.
+ raise ValueError('invalid value for extendfrac')
+ elif frac is not None:
+ try:
+ # Try to set min and max extension fractions directly.
+ extendlength[:] = frac
+ # If frac is a sequence contaning None then NaN may
+ # be encountered. This is an error.
+ if np.isnan(extendlength).any():
+ raise ValueError()
@pelson Collaborator
pelson added a note

Stylistically this should be a ValueError instance. i.e. ValueError().

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ except (TypeError, ValueError):
+ # Raise an error on encountering an invalid value for frac.
+ raise ValueError('invalid value for extendfrac')
+ return extendlength
+
def _uniform_y(self, N):
'''
Return colorbar data coordinates for *N* uniformly
@@ -624,16 +671,19 @@ def _uniform_y(self, N):
if self.extend == 'neither':
y = np.linspace(0, 1, N)
else:
+ automin = automax = 1. / (N - 1.)
+ extendlength = self._get_extension_lengths(self.extendfrac,
+ automin, automax, default=0.05)
if self.extend == 'both':
y = np.zeros(N + 2, 'd')
- y[0] = -0.05
- y[-1] = 1.05
+ y[0] = 0. - extendlength[0]
+ y[-1] = 1. + extendlength[1]
elif self.extend == 'min':
y = np.zeros(N + 1, 'd')
- y[0] = -0.05
+ y[0] = 0. - extendlength[0]
else:
y = np.zeros(N + 1, 'd')
- y[-1] = 1.05
+ y[-1] = 1. + extendlength[1]
y[self._inside] = np.linspace(0, 1, N)
return y
@@ -648,10 +698,27 @@ def _proportional_y(self):
y = y / (self._boundaries[-1] - self._boundaries[0])
else:
y = self.norm(self._boundaries.copy())
- if self._extend_lower():
- y[0] = -0.05
- if self._extend_upper():
- y[-1] = 1.05
+ if self.extend == 'min':
+ # Exclude leftmost interval of y.
+ clen = y[-1] - y[1]
+ automin = (y[2] - y[1]) / clen
+ automax = (y[-1] - y[-2]) / clen
+ elif self.extend == 'max':
+ # Exclude rightmost interval in y.
+ clen = y[-2] - y[0]
+ automin = (y[1] - y[0]) / clen
+ automax = (y[-2] - y[-3]) / clen
+ else:
+ # Exclude leftmost and rightmost intervals in y.
+ clen = y[-2] - y[1]
+ automin = (y[2] - y[1]) / clen
+ automax = (y[-2] - y[-3]) / clen
+ extendlength = self._get_extension_lengths(self.extendfrac,
+ automin, automax, default=0.05)
+ if self.extend in ('both', 'min'):
+ y[0] = 0. - extendlength[0]
+ if self.extend in ('both', 'max'):
+ y[-1] = 1. + extendlength[1]
yi = y[self._inside]
norm = colors.Normalize(yi[0], yi[-1])
y[self._inside] = norm(yi)
View
BIN  lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_proportional.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
BIN  lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_uniform.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
61 lib/matplotlib/tests/test_colorbar.py
@@ -0,0 +1,61 @@
+from matplotlib import rcParams, rcParamsDefault
+from matplotlib.testing.decorators import image_comparison
+import matplotlib.pyplot as plt
+from matplotlib.colors import BoundaryNorm
+from matplotlib.cm import get_cmap
+from matplotlib.colorbar import ColorbarBase
+
+
+def _colorbar_extensions(spacing):
+
+ # Create a color map and specify the levels it represents.
+ cmap = get_cmap("RdBu", lut=5)
+ clevs = [-5., -2.5, -.5, .5, 1.5, 3.5]
+
+ # Define norms for the color maps.
+ norms = dict()
+ norms['neither'] = BoundaryNorm(clevs, len(clevs)-1)
+ norms['min'] = BoundaryNorm([-10]+clevs[1:], len(clevs)-1)
+ norms['max'] = BoundaryNorm(clevs[:-1]+[10], len(clevs)-1)
+ norms['both'] = BoundaryNorm([-10]+clevs[1:-1]+[10], len(clevs)-1)
+
+ # Create a figure and adjust whitespace for subplots.
+ fig = plt.figure()
+ fig.subplots_adjust(hspace=.6)
+
+ for i, extension_type in enumerate(('neither', 'min', 'max', 'both')):
+ # Get the appropriate norm and use it to get colorbar boundaries.
+ norm = norms[extension_type]
+ boundaries = values = norm.boundaries
+ for j, extendfrac in enumerate((None, 'auto', 0.1)):
+ # Create a subplot.
+ cax = fig.add_subplot(12, 1, i*3+j+1)
+ # Turn off text and ticks.
+ for item in cax.get_xticklabels() + cax.get_yticklabels() +\
+ cax.get_xticklines() + cax.get_yticklines():
+ item.set_visible(False)
+ # Generate the colorbar.
+ cb = ColorbarBase(cax, cmap=cmap, norm=norm,
+ boundaries=boundaries, values=values,
+ extend=extension_type, extendfrac=extendfrac,
+ orientation='horizontal', spacing=spacing)
+
+ # Return the figure to the caller.
+ return fig
+
+
+@image_comparison(
+ baseline_images=['colorbar_extensions_uniform', 'colorbar_extensions_proportional'],
+ extensions=['png'])
+def test_colorbar_extensions():
+ # Use default params so .matplotlibrc doesn't cause the test to fail.
+ rcParams.update(rcParamsDefault)
+ # Create figures for uniform and proportionally spaced colorbars.
+ fig1 = _colorbar_extensions('uniform')
+ fig2 = _colorbar_extensions('proportional')
+
+
+if __name__ == '__main__':
+ import nose
+ nose.runmodule(argv=['-s', '--with-doctest'], exit=False)
+
Something went wrong with that request. Please try again.