Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Added the from_levels_and_colors function. #2050

Merged
merged 1 commit into from

3 participants

@pelson
Collaborator

This PR adds a function which simplifies the construction of a cmap and norm for discrete levels with specific colours.
The interface is intentionally similar to the contourf levels and colors arguments - though a little stricter in the fact that the correct number of colors for levels is required.

This has come up a couple of times with my colleagues and I always end up having to write a chunk of test code to make sure I've done it right. This function generally makes this easier - and will hopefully make doing quantized pcolormesh's less error prone.

@mdboom mdboom commented on the diff
lib/matplotlib/cbook.py
@@ -189,7 +189,7 @@ def deprecate(func, message=message, name=name, alternative=alternative,
name = func.__name__
message = _generate_deprecation_message(
- since, message, name, alternative, pending, 'function')
+ since, message, name, alternative, pending, obj_type)
@mdboom Owner
mdboom added a note

Thanks for catching that.

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

There is a python3 hashable problem which I need to deal with, but other than that, I'm pretty keen to get this in v1.3.x. For what it's worth, I've put together a gist which demonstrates the benefit: http://nbviewer.ipython.org/5628989

lib/matplotlib/colors.py
@@ -607,6 +612,11 @@ def __call__(self, X, alpha=None, bytes=False):
rgba = tuple(rgba[0, :])
return rgba
+ @property
@efiring Owner
efiring added a note

I like this use of properties; is there any reason not to extend it so that it can be used to set as well as get the value?

@pelson Collaborator
pelson added a note

Sure could do, but that would make two ways of setting the colours. There's no reason that we couldn't go down the deprecation path for set_under, set_over and set_bad (or just have two ways of setting them I suppose).

@efiring Owner
efiring added a note

It seems overly inconsistent to be able to read but not write bad_color etc, since writing is mostly what one wants to do from user code. For the purpose of your function, you don't need an external API--you could just use the private variables.

The rationale for keeping set_bad etc. would be that they include the alpha kwarg, although that is never really needed; one could always use, for example, bad_color = mpl.colors.colorConverter.to_rgba("r", alpha=0.5).

The use of setters and getters is a legacy. There are places where they serve a major function in the auto documentation system, but here in the colors module the are just fossils from early days. (I put them in.) I don't see any urgency to deprecate them, but I think that using them to flesh out the bad_color etc. properties would make sense, if you really want to proceed with bad_color--which I think makes for a much nicer API.

This brings up another point: to_rgba and friends are so deeply hidden that even I can never remember how to access them, but have to hunt around to find them. (A camelCase name? How did we end up with that?) Wouldn't it be nice, from the user standpoint, to have basic functionality like this in some more obvious place--either directly in colors as a function, or maybe even the base matplotlib namespace? Right now, that namespace (from import matplotlib) is full of completely useless junk--throwaways from the startup process. Ugly!

@pelson Collaborator
pelson added a note

I like your outlook @efiring :-)
If you and @mdboom are willing to review and merge it, I'm willing to submit the PRs :smile:

In the interests of keeping this PR as short as it can be, I'll add the property setters and hold back from deprecating the set_* methods.

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

@efiring - I've added a commit which does some of what you discussed (and more). If it is too extreme, then I'll take it back and be a little more conservative :wink:.

I'm also motivated to simplify the color conversion process in matplotlib and to provide a means to create hsv, hsl, hsva and hsvla interpolated color schemes easily. I'm tempted to hold fire on this and do it in v1.4 though (along with deprecating the use of Colormap.set_bad etc.,).

What do you think?

@mdboom
Owner

I think we should hold for 1.4 any further features at this point. (The feature freeze began almost two weeks ago now, and I've let a few things slide through, but we're mainly in the mode of polishing and bugfixing the features we already have at this point.)

@mdboom
Owner

I'll leave it to @efiring to give this one more look over and merge.

@efiring
Owner

@pelson, @mdboom, there are lots of good ideas in here, but also some radical changes that I think deserve discussion, so I am not willing to merge this as-is. Most of it may be more appropriate for 1.4 than for 1.3 at this late stage.

The biggest strategy change here is including peek() as a method of the Colormap. Having a peek method or function is a great idea, but making it a Colormap method, and yet including all the high level pyplot functionality while not even returning the figure object, seems to me like a questionable design. Everywhere else, we have tried to keep some high-level to low-level hierarchy. To be consistent with that logic, peek should be a pyplot function, perhaps view_colormap(cbar). (Longer term, it would be nice to have a colormap-editing widget instantiated by a pyplot function.) Having it as a pyplot function would also make it more discoverable.

The decorator gymnastics in the present PR are also getting a bit hard to follow, and I don't think they actually save any LOC or clarify anything, compared to simply repeating the phrase,

        if self._isinit:
            self._set_extremes()

three times, which amounts to 6 LOC, versus many more for the definition of the decorator plus its three invocations. With all the switching around of self and func, that decorator begins to resemble a textbook example of obfuscated code.

@pelson
Collaborator

I don't disagree with the analysis @efiring. Some of the things in here are a little contravertial and have no place in v1.3, but I do feel quite passionately about the underlying purpose for this PR making it in (the function from_levels_and_colors which avoids users, i.e. my colleagues, accidentally mis-representing their data by misconstructing Norms and Cmaps for quantised levels.). The first commit on this PR does that, without introducing any controversial changes - perhaps that is the way to go here.

@pelson
Collaborator

Ok. This has been reverted to the first commit (my original changes are in a branch https://github.com/pelson/matplotlib/tree/colormap_api_for_v1.4 if anybody is interested).

@efiring
Owner

@pelson, for the purpose of inclusion in 1.3, I recommend that you remove the new properties. At this stage, they would represent a commitment to a new API, which might be a good one (basically, I like it), but which is incomplete, and which contributes absolutely nothing to the purpose of the PR--no functionality, no reduction in LOC, no improvement in readability or performance.

You might even go so far as to put an "Experimental" tag in the docstring of your new function, to leave a little wiggle-room. I think this would be wise, given the very short review period that has been available.

@pelson
Collaborator

I've now reverted all of the API changes on Colormap.

@mdboom mdboom merged commit 48e1439 into matplotlib:master

1 check failed

Details default The Travis CI build failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on May 28, 2013
  1. @pelson
This page is out of date. Refresh to see the latest.
View
4 doc/api/api_changes.rst
@@ -99,6 +99,10 @@ Changes in 1.3.x
Deep copying a `Path` always creates an editable (i.e. non-readonly)
`Path`.
+* matplotlib.colors.normalize and matplotlib.colors.no_norm have been
+ deprecated in favour of matplotlib.colors.Normalize and
+ matplotlib.colors.NoNorm respectively.
+
* The `font.*` rcParams now affect only text objects created after the
rcParam has been set, and will not retroactively affect already
existing text objects. This brings their behavior in line with most
View
8 doc/users/whats_new.rst
@@ -68,6 +68,14 @@ rcParam has been set, and will not retroactively affect already
existing text objects. This brings their behavior in line with most
other rcParams.
+Easier creation of colormap and normalizer for levels with colors
+-----------------------------------------------------------------
+Phil Elson added the :func:`matplotlib.colors.from_levels_and_colors`
+function to easily create a colormap and normalizer for representation
+of discrete colors for plot types such as
+:func:`matplotlib.pyplot.pcolormesh`, with a similar interface to that of
+contourf.
+
Catch opening too many figures using pyplot
-------------------------------------------
Figures created through `pyplot.figure` are retained until they are
View
2  lib/matplotlib/cbook.py
@@ -189,7 +189,7 @@ def deprecate(func, message=message, name=name, alternative=alternative,
name = func.__name__
message = _generate_deprecation_message(
- since, message, name, alternative, pending, 'function')
+ since, message, name, alternative, pending, obj_type)
@mdboom Owner
mdboom added a note

Thanks for catching that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@functools.wraps(func)
def deprecated_func(*args, **kwargs):
View
7 lib/matplotlib/colorbar.py
@@ -853,8 +853,8 @@ def __init__(self, ax, mappable, **kw):
mappable.autoscale_None()
self.mappable = mappable
- kw['cmap'] = mappable.cmap
- kw['norm'] = mappable.norm
+ kw['cmap'] = cmap = mappable.cmap
+ kw['norm'] = norm = mappable.norm
if isinstance(mappable, contour.ContourSet):
CS = mappable
@@ -869,6 +869,9 @@ def __init__(self, ax, mappable, **kw):
if not CS.filled:
self.add_lines(CS)
else:
+ if getattr(cmap, 'colorbar_extend', False) is not False:
+ kw.setdefault('extend', cmap.colorbar_extend)
+
if isinstance(mappable, martist.Artist):
kw['alpha'] = mappable.get_alpha()
View
84 lib/matplotlib/colors.py
@@ -508,6 +508,12 @@ def __init__(self, name, N=256):
self._i_bad = N + 2
self._isinit = False
+ #: When this colormap exists on a scalar mappable and colorbar_extend
+ #: is not False, colorbar creation will pick up ``colorbar_extend`` as
+ #: the default value for the ``extend`` keyword in the
+ #: :class:`matplotlib.colorbar.Colorbar` constructor.
+ self.colorbar_extend = False
+
def __call__(self, X, alpha=None, bytes=False):
"""
Parameters
@@ -832,7 +838,7 @@ def _init(self):
class Normalize(object):
"""
A class which, when called, can normalize data into
- the ``[0, 1]`` interval.
+ the ``[0.0, 1.0]`` interval.
"""
def __init__(self, vmin=None, vmax=None, clip=False):
@@ -1212,8 +1218,12 @@ def inverse(self, value):
return value
# compatibility with earlier class names that violated convention:
-normalize = Normalize
-no_norm = NoNorm
+normalize = cbook.deprecated('1.3', alternative='Normalize',
+ name='normalize',
+ obj_type='class alias')(Normalize)
+no_norm = cbook.deprecated('1.3', alternative='NoNorm',
+ name='no_norm',
+ obj_type='class alias')(NoNorm)
def rgb_to_hsv(arr):
@@ -1405,3 +1415,71 @@ def shade_rgb(self, rgb, elevation, fraction=1.):
hsv[:, :, 1:] = np.where(hsv[:, :, 1:] > 1., 1, hsv[:, :, 1:])
# convert modified hsv back to rgb.
return hsv_to_rgb(hsv)
+
+
+def from_levels_and_colors(levels, colors, extend='neither'):
+ """
+ A helper routine to generate a cmap and a norm instance which
+ behave similar to contourf's levels and colors arguments.
+
+ Parameters
+ ----------
+ levels : sequence of numbers
+ The quantization levels used to construct the :class:`BoundaryNorm`.
+ Values ``v`` are quantizized to level ``i`` if
+ ``lev[i] <= v < lev[i+1]``.
+ colors : sequence of colors
+ The fill color to use for each level. If `extend` is "neither" there
+ must be ``n_level - 1`` colors. For an `extend` of "min" or "max" add
+ one extra color, and for an `extend` of "both" add two colors.
+ extend : {'neither', 'min', 'max', 'both'}, optional
+ The behaviour when a value falls out of range of the given levels.
+ See :func:`~matplotlib.pyplot.contourf` for details.
+
+ Returns
+ -------
+ (cmap, norm) : tuple containing a :class:`Colormap` and a \
+ :class:`Normalize` instance
+ """
+ colors_i0 = 0
+ colors_i1 = None
+
+ if extend == 'both':
+ colors_i0 = 1
+ colors_i1 = -1
+ extra_colors = 2
+ elif extend == 'min':
+ colors_i0 = 1
+ extra_colors = 1
+ elif extend == 'max':
+ colors_i1 = -1
+ extra_colors = 1
+ elif extend == 'neither':
+ extra_colors = 0
+ else:
+ raise ValueError('Unexpected value for extend: {0!r}'.format(extend))
+
+ n_data_colors = len(levels) - 1
+ n_expected_colors = n_data_colors + extra_colors
+ if len(colors) != n_expected_colors:
+ raise ValueError('With extend == {0!r} and n_levels == {1!r} expected'
+ ' n_colors == {2!r}. Got {3!r}.'
+ ''.format(extend, len(levels), n_expected_colors,
+ len(colors)))
+
+ cmap = ListedColormap(colors[colors_i0:colors_i1], N=n_data_colors)
+
+ if extend in ['min', 'both']:
+ cmap.set_under(colors[0])
+ else:
+ cmap.set_under('none')
+
+ if extend in ['max', 'both']:
+ cmap.set_over(colors[-1])
+ else:
+ cmap.set_over('none')
+
+ cmap.colorbar_extend = extend
+
+ norm = BoundaryNorm(levels, ncolors=n_data_colors)
+ return cmap, norm
View
BIN  lib/matplotlib/tests/baseline_images/test_colors/levels_and_colors.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
94 lib/matplotlib/tests/test_colors.py
@@ -1,12 +1,14 @@
-"""
-Tests for the colors module.
-"""
-
from __future__ import print_function
+from nose.tools import assert_raises
import numpy as np
from numpy.testing.utils import assert_array_equal, assert_array_almost_equal
+
+
import matplotlib.colors as mcolors
import matplotlib.cm as cm
+import matplotlib.pyplot as plt
+from matplotlib.testing.decorators import image_comparison
+
def test_colormap_endian():
"""
@@ -23,6 +25,7 @@ def test_colormap_endian():
#print(anative.dtype.isnative, aforeign.dtype.isnative)
assert_array_equal(cmap(anative), cmap(aforeign))
+
def test_BoundaryNorm():
"""
Github issue #1258: interpolation was failing with numpy
@@ -36,7 +39,8 @@ def test_BoundaryNorm():
ncolors = len(boundaries)
bn = mcolors.BoundaryNorm(boundaries, ncolors)
assert_array_equal(bn(vals), expected)
-
+
+
def test_LogNorm():
"""
LogNorm igornoed clip, now it has the same
@@ -46,6 +50,7 @@ def test_LogNorm():
ln = mcolors.LogNorm(clip=True, vmax=5)
assert_array_equal(ln([1, 6]), [0, 1.0])
+
def test_Normalize():
norm = mcolors.Normalize()
vals = np.arange(-10, 10, 1, dtype=np.float)
@@ -74,6 +79,7 @@ def _inverse_tester(norm_instance, vals):
"""
assert_array_almost_equal(norm_instance.inverse(norm_instance(vals)), vals)
+
def _scalar_tester(norm_instance, vals):
"""
Checks if scalars and arrays are handled the same way.
@@ -82,6 +88,7 @@ def _scalar_tester(norm_instance, vals):
scalar_result = [norm_instance(float(v)) for v in vals]
assert_array_almost_equal(scalar_result, norm_instance(vals))
+
def _mask_tester(norm_instance, vals):
"""
Checks mask handling
@@ -89,3 +96,80 @@ def _mask_tester(norm_instance, vals):
masked_array = np.ma.array(vals)
masked_array[0] = np.ma.masked
assert_array_equal(masked_array.mask, norm_instance(masked_array).mask)
+
+
+@image_comparison(baseline_images=['levels_and_colors'],
+ extensions=['png'])
+def test_cmap_and_norm_from_levels_and_colors():
+ data = np.linspace(-2, 4, 49).reshape(7, 7)
+ levels = [-1, 2, 2.5, 3]
+ colors = ['red', 'green', 'blue', 'yellow', 'black']
+ extend = 'both'
+ cmap, norm = mcolors.from_levels_and_colors(levels, colors, extend=extend)
+
+ ax = plt.axes()
+ m = plt.pcolormesh(data, cmap=cmap, norm=norm)
+ plt.colorbar(m)
+
+ # Hide the axes labels (but not the colorbar ones, as they are useful)
+ for lab in ax.get_xticklabels() + ax.get_yticklabels():
+ lab.set_visible(False)
+
+
+def test_cmap_and_norm_from_levels_and_colors2():
+ levels = [-1, 2, 2.5, 3]
+ colors = ['red', (0, 1, 0), 'blue', (0.5, 0.5, 0.5), (0.0, 0.0, 0.0, 1.0)]
+ clr = mcolors.colorConverter.to_rgba_array(colors)
+ bad = (0.1, 0.1, 0.1, 0.1)
+ no_color = (0.0, 0.0, 0.0, 0.0)
+
+ # Define the test values which are of interest.
+ # Note: levels are lev[i] <= v < lev[i+1]
+ tests = [('both', None, {-2: clr[0],
+ -1: clr[1],
+ 2: clr[2],
+ 2.25: clr[2],
+ 3: clr[4],
+ 3.5: clr[4],
+ np.ma.array(1, mask=True): bad}),
+
+ ('min', -1, {-2: clr[0],
+ -1: clr[1],
+ 2: clr[2],
+ 2.25: clr[2],
+ 3: no_color,
+ 3.5: no_color,
+ np.ma.array(1, mask=True): bad}),
+
+ ('max', -1, {-2: no_color,
+ -1: clr[0],
+ 2: clr[1],
+ 2.25: clr[1],
+ 3: clr[3],
+ 3.5: clr[3],
+ np.ma.array(1, mask=True): bad}),
+
+ ('neither', -2, {-2: no_color,
+ -1: clr[0],
+ 2: clr[1],
+ 2.25: clr[1],
+ 3: no_color,
+ 3.5: no_color,
+ np.ma.array(1, mask=True): bad}),
+ ]
+
+ for extend, i1, cases in tests:
+ cmap, norm = mcolors.from_levels_and_colors(levels, colors[0:i1],
+ extend=extend)
+ cmap.set_bad(bad)
+ for d_val, expected_color in sorted(cases.items()):
+ assert_array_equal(expected_color, cmap(norm([d_val]))[0],
+ 'Wih extend={0!r} and data '
+ 'value={1!r}'.format(extend, d_val))
+
+ assert_raises(ValueError, mcolors.from_levels_and_colors, levels, colors)
+
+
+if __name__ == '__main__':
+ import nose
+ nose.runmodule(argv=['-s', '--with-doctest'], exit=False)
Something went wrong with that request. Please try again.