Skip to content
This repository

Added support for providing 1 or 2 extra colours to the contour routines to easily specify the under and over colors. #2002

Merged
merged 3 commits into from 11 months ago

3 participants

Phil Elson Eric Firing Michael Droettboom
Phil Elson
Collaborator
pelson commented May 14, 2013

I had a question from a colleague the other day about being able to specify the under and over colors of a contour.

The solution was:

import matplotlib.pyplot as plt
import numpy as np

data = np.arange(12).reshape(3, 4)

colors = ['red', 'yellow', 'pink', 'blue', 'black']
levels = [2, 4, 8, 10]

cs = plt.contourf(data, colors=colors[1:-1], levels=levels, extend='both')
plt.colorbar()
cs.cmap.set_under(colors[0])
cs.cmap.set_over(colors[-1])
cs.changed()

plt.show()

This PR makes the lines:

cs.cmap.set_under(colors[0])
cs.cmap.set_over(colors[-1])
cs.changed()

redundant.

If the user specifies n_colors = n_levels + n_extends then the appropriate set_under and set_over behaviour kicks in.

This change also fixes a bug with contour (i.e. not contourf) and its handling of extend:

import matplotlib.pyplot as plt
import numpy as np

data = np.arange(12).reshape(3, 4)
colors = ['red', 'yellow', 'pink', 'blue', 'black']
levels = [2, 4, 8]
cs = plt.contour(data, colors=colors, levels=levels, extend='both')
plt.colorbar()
plt.show()

Results in:

  ...
  File "matplotlib/colorbar.py", line 777, in _mesh
    y = self._uniform_y(self._central_N())
  File "matplotlib/colorbar.py", line 714, in _uniform_y
    automin = automax = 1. / (N - 1.)
ZeroDivisionError: float division by zero

For the record, this PR is a functional change. The test results image before the code changes in this PR look like:

before

Compare this with the result image in this PR: https://github.com/matplotlib/matplotlib/pull/2002/files#diff-1

Phil Elson
Collaborator
pelson commented May 15, 2013

@efiring - I would really value your input on this given how much knowledge you have of the contour code.

lib/matplotlib/contour.py
((4 lines not shown))
867  
-            cmap = colors.ListedColormap(self.colors, N=ncolors)
  867
+
  868
+            # Handle the case where colors are given for the extended
  869
+            # parts of the contour.
  870
+            given_colors = self.colors
  871
+            extend_min = self.extend in ['min', 'both']
  872
+            extend_max = self.extend in ['max', 'both']
  873
+            use_set_under_over = False
  874
+            # if we are extending the lower end, and we've been given enough colors
  875
+            # then skip the first color in the resulting cmap.
  876
+            total_levels = ncolors + int(extend_min) + int(extend_max)
  877
+            if len(self.colors) == total_levels and any([extend_min, extend_max]): 
  878
+                use_set_under_over = True
  879
+                if extend_min:
  880
+                    given_colors = given_colors[1:]
  881
+
2
Eric Firing Owner
efiring added a note May 15, 2013

It is not clear why you don't need to clip the end off of given_colors in the extend_max case--I think you do, but I haven't looked closely.
Instead of making a new variable, given_colors, and then chopping off one or both ends, you could generate slice indices:

i0, i1 = 0, None

Then inside the "extended" block:

if extend_min:
    i0 = 1
if extend_max:
    i1 = -1

Then in ListedColormap, use self.colors[i0:i1]

Phil Elson Collaborator
pelson added a note May 17, 2013

Nice suggestion.

We don't need to index the colors off the top because passing N through to ListedColormap truncates the colors for us.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eric Firing
Owner
efiring commented May 15, 2013

Overall, this seems like a nice change, making the interface more intuitive.

Phil Elson
Collaborator
pelson commented May 17, 2013

Thanks @efiring - I've added that suggestion.

lib/matplotlib/contour.py
... ...
@@ -864,7 +864,29 @@ def __init__(self, ax, *args, **kwargs):
864 864
             ncolors = len(self.levels)
865 865
             if self.filled:
866 866
                 ncolors -= 1
867  
-            cmap = colors.ListedColormap(self.colors, N=ncolors)
  867
+            i0, i1 = 0, None
  868
+            # Handle the case where colors are given for the extended
  869
+            # parts of the contour.
  870
+            extend_min = self.extend in ['min', 'both']
  871
+            extend_max = self.extend in ['max', 'both']
  872
+            use_set_under_over = False
  873
+            # if we are extending the lower end, and we've been given enough
  874
+            # colors then skip the first color in the resulting cmap.
  875
+            total_levels = ncolors + int(extend_min) + int(extend_max)
  876
+            if len(self.colors) == total_levels and \
1
Eric Firing Owner
efiring added a note May 17, 2013

Style police citation: no trailing backslash, please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/matplotlib/contour.py
((5 lines not shown))
  867
+            i0, i1 = 0, None
  868
+            # Handle the case where colors are given for the extended
  869
+            # parts of the contour.
  870
+            extend_min = self.extend in ['min', 'both']
  871
+            extend_max = self.extend in ['max', 'both']
  872
+            use_set_under_over = False
  873
+            # if we are extending the lower end, and we've been given enough
  874
+            # colors then skip the first color in the resulting cmap.
  875
+            total_levels = ncolors + int(extend_min) + int(extend_max)
  876
+            if len(self.colors) == total_levels and \
  877
+                    any([extend_min, extend_max]):
  878
+                use_set_under_over = True
  879
+                if extend_min:
  880
+                    i0 = 1
  881
+
  882
+            cmap = colors.ListedColormap(self.colors[i0:i1], N=ncolors)
2
Eric Firing Owner
efiring added a note May 17, 2013

So, you really don't need i1 after all. I suggest eliminating it in favor of None, and adding to the comment above the reason why you don't need to chop the top.

Phil Elson Collaborator
pelson added a note May 17, 2013

Have done. Thanks @efiring.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Phil Elson
Collaborator
pelson commented May 20, 2013

I think this is now good to go.

Eric Firing
Owner
efiring commented May 20, 2013

Doesn't it need a change to the contourf docstring and some mention in api changes? Sorry I didn't mention these before--I assumed you were just holding off on them because you were unsure as to whether the actual change would be acceptable.

Phil Elson
Collaborator
pelson commented May 21, 2013

There's a python3.3 test failiure on travis (not a result of this change). Other than that, the tests are passing, so when you're happy @efiring, please press merge :wink:

Michael Droettboom mdboom merged commit 355b056 into from May 21, 2013
Michael Droettboom mdboom closed this May 21, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 3 unique commits by 1 author.

May 14, 2013
Phil Elson Added support for providing 1 or 2 extra colours to the contour routi…
…nes to easily specify the under and over colors.
a736819
May 17, 2013
Phil Elson @efiring's suggestion. d582cec
May 21, 2013
Phil Elson Added an api_changes entry for contour levels & colours with the exte…
…nd keyword set.
dac1530
This page is out of date. Refresh to see the latest.
21  doc/api/api_changes.rst
Source Rendered
@@ -24,16 +24,25 @@ Changes in 1.3.x
24 24
   value of this kwarg is ``head_width = 20 * width``.
25 25
 
26 26
 * Removed call of :meth:`~matplotlib.axes.Axes.grid` in
27  
-  :meth:`~matplotlib.pyplot.plotfile`. To draw the axes grid, set to *True*
28  
-  matplotlib.rcParams['axes.grid'] or ``axes.grid`` in ``.matplotlibrc`` or
29  
-  explicitly call :meth:`~matplotlib.axes.Axes.grid`
  27
+  :meth:`~matplotlib.pyplot.plotfile`. To draw the axes grid, set the
  28
+  ``axes.grid`` rcParam to *True*, or explicitly call
  29
+  :meth:`~matplotlib.axes.Axes.grid`.
  30
+
  31
+* It is now posible to provide ``number of levels + 1`` colors in the case of
  32
+  `extend='both'` for contourf (or just ``number of levels`` colors for an
  33
+  extend value ``min`` or ``max``) such that the resulting colormap's
  34
+  ``set_under`` and ``set_over`` are defined appropriately. Any other number
  35
+  of colors will continue to behave as before (if more colors are provided
  36
+  than levels, the colors will be unused). A similar change has been applied
  37
+  to contour, where ``extend='both'`` would expect ``number of levels + 2``
  38
+  colors.
30 39
 
31 40
 * A new keyword *extendrect* in :meth:`~matplotlib.pyplot.colorbar` and
32 41
   :class:`~matplotlib.colorbar.ColorbarBase` allows one to control the shape
33 42
   of colorbar extensions.
34 43
 
35 44
 * The `~matplotlib.mpl` module is now deprecated. Those who relied on this
36  
-  module should transition to simply using `import matplotlib as mpl`.
  45
+  module should transition to simply using ``import matplotlib as mpl``.
37 46
 
38 47
 * The extension of :class:`~matplotlib.widgets.MultiCursor` to both vertical
39 48
   (default) and/or horizontal cursor implied that ``self.line`` is replaced
@@ -44,8 +53,8 @@ Changes in 1.3.x
44 53
   raises :class:`NotImplementedError` instead of :class:`OSError` if the
45 54
   :command:`ps` command cannot be run.
46 55
 
47  
-* The :func:`~matplotlib.cbook.check_output` function has been moved to
48  
-  `~matplotlib.compat.subprocess`.
  56
+* The :func:`matplotlib.cbook.check_output` function has been moved to
  57
+  :func:`matplotlib.compat.subprocess`.
49 58
 
50 59
 * :class:`~matplotlib.patches.Patch` now fully supports using RGBA values for
51 60
   its ``facecolor`` and ``edgecolor`` attributes, which enables faces and
37  lib/matplotlib/contour.py
@@ -864,7 +864,32 @@ def __init__(self, ax, *args, **kwargs):
864 864
             ncolors = len(self.levels)
865 865
             if self.filled:
866 866
                 ncolors -= 1
867  
-            cmap = colors.ListedColormap(self.colors, N=ncolors)
  867
+            i0 = 0
  868
+
  869
+            # Handle the case where colors are given for the extended
  870
+            # parts of the contour.
  871
+            extend_min = self.extend in ['min', 'both']
  872
+            extend_max = self.extend in ['max', 'both']
  873
+            use_set_under_over = False
  874
+            # if we are extending the lower end, and we've been given enough
  875
+            # colors then skip the first color in the resulting cmap. For the
  876
+            # extend_max case we don't need to worry about passing more colors
  877
+            # than ncolors as ListedColormap will clip.
  878
+            total_levels = ncolors + int(extend_min) + int(extend_max)
  879
+            if (len(self.colors) == total_levels and
  880
+                    any([extend_min, extend_max])):
  881
+                use_set_under_over = True
  882
+                if extend_min:
  883
+                    i0 = 1
  884
+
  885
+            cmap = colors.ListedColormap(self.colors[i0:None], N=ncolors)
  886
+
  887
+            if use_set_under_over:
  888
+                if extend_min:
  889
+                    cmap.set_under(self.colors[0])
  890
+                if extend_max:
  891
+                    cmap.set_over(self.colors[-1])
  892
+
868 893
         if self.filled:
869 894
             self.collections = cbook.silent_list('mcoll.PathCollection')
870 895
         else:
@@ -1172,16 +1197,16 @@ def _process_levels(self):
1172 1197
         # (Colorbar needs this even for line contours.)
1173 1198
         self._levels = list(self.levels)
1174 1199
 
1175  
-        if not self.filled:
1176  
-            self.layers = self.levels
1177  
-            return
1178  
-
1179 1200
         if self.extend in ('both', 'min'):
1180 1201
             self._levels.insert(0, min(self.levels[0], self.zmin) - 1)
1181 1202
         if self.extend in ('both', 'max'):
1182 1203
             self._levels.append(max(self.levels[-1], self.zmax) + 1)
1183 1204
         self._levels = np.asarray(self._levels)
1184 1205
 
  1206
+        if not self.filled:
  1207
+            self.layers = self.levels
  1208
+            return
  1209
+
1185 1210
         # layer values are mid-way between levels
1186 1211
         self.layers = 0.5 * (self._levels[:-1] + self._levels[1:])
1187 1212
         # ...except that extended layers must be outside the
@@ -1526,9 +1551,7 @@ def _check_xyz(self, args, kwargs):
1526 1551
             if y.shape != z.shape:
1527 1552
                 raise TypeError("Shape of y does not match that of z: found "
1528 1553
                                 "{0} instead of {1}.".format(y.shape, z.shape))
1529  
-
1530 1554
         else:
1531  
-
1532 1555
             raise TypeError("Inputs x and y must be 1D or 2D.")
1533 1556
 
1534 1557
         return x, y, z
BIN  lib/matplotlib/tests/baseline_images/test_contour/contour_manual_colors_and_levels.png
31  lib/matplotlib/tests/test_contour.py
@@ -149,3 +149,34 @@ def test_contour_manual_labels():
149 149
     cs = plt.contour(x,y,z)
150 150
     pts = np.array([(1.5, 3.0), (1.5, 4.4), (1.5, 6.0)])
151 151
     plt.clabel(cs, manual=pts)
  152
+
  153
+
  154
+@image_comparison(baseline_images=['contour_manual_colors_and_levels'],
  155
+                  extensions=['png'], remove_text=True)
  156
+def test_given_colors_levels_and_extends():
  157
+    _, axes = plt.subplots(2, 4)
  158
+
  159
+    data = np.arange(12).reshape(3, 4)
  160
+    
  161
+    colors = ['red', 'yellow', 'pink', 'blue', 'black']
  162
+    levels = [2, 4, 8, 10]
  163
+    
  164
+    for i, ax in enumerate(axes.flatten()):
  165
+        plt.sca(ax)
  166
+        
  167
+        filled = i % 2 == 0.
  168
+        extend = ['neither', 'min', 'max', 'both'][i // 2]
  169
+        
  170
+        if filled:
  171
+            last_color = -1 if extend in ['min', 'max'] else None
  172
+            plt.contourf(data, colors=colors[:last_color], levels=levels, extend=extend)
  173
+        else:
  174
+            last_level = -1 if extend == 'both' else None
  175
+            plt.contour(data, colors=colors, levels=levels[:last_level], extend=extend)
  176
+    
  177
+        plt.colorbar()
  178
+
  179
+
  180
+if __name__ == '__main__':
  181
+    import nose
  182
+    nose.runmodule(argv=['-s', '--with-doctest'], exit=False)
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.