8

I'm using matplotlib 1.3.0 and I have the following:

import matplotlib.pyplot as plt
cmap = plt.cm.jet
plt.contourf([[.12, .2], [.8, 2]], levels=[0, .1, .3, .5, 1, 3], cmap=cmap, vmin=0, vmax=3)
plt.colorbar()

which produces:

enter image description here

The bit that I don't understand is where did all of the other colors go? As I understand, by specifying vmin=0, vmax=3 then the color bar should use the full range of cmap like in this image:

enter image description here

which is produced without giving the vmin, vmax and levels arguments. So... what am I missing here?

EDIT 1

In response to tom10 & tcaswell. I would have expected it to be as you say, but... unfortunately it's not. Take a look at this:

plt.contourf([[.12, .2], [.8, 3.2]], levels=[0, .1, .3, .5, 1, 3], cmap=cmap, vmin=0, vmax=3)
plt.colorbar()

with:

enter image description here

Maybe to clarify this a bit: say I have data and the important features of it are around 0.1, but there are some around 3 let's say. So I give it a levels=[0, 0.005, 0.075, 0.1, 0.125, 0.15, 0.2, 1, 2.5, 2.75, 3, 3.25] and vmin=0, vmax=3.25. Now I would expect to see the full range of colors, but instead all of the important data-points 0.005 to 0.125 end up in the blue region (by using the standard plt.cm.jet color map). What I'm saying I guess is... if I give levels=[0, 1, 2, 3], vmin=0, vmax=3 for some data that goes from 0 to 3 I expect to see all the colors in the given color map, but if I give levels=[0, 0.9, 0.1, 0.11, 1, 3], vmi=0, vmax=3 I would expect the same, to see all the colors in the given color map, except mapped to the right intervals, instead I see the bunch of blues coloring the 0-0.11 region and some green / yellow coloring the other part of the region. Hope this makes it... a bit clear.

EDIT 2

The same happens even if I don't give any norm or vmin, vmax.

EDIT 3

Referring to tcaswell's comment, behaving the way it is... for me at least is counter-intuitive. I expected that the color would be independent of the data-points in a way. I would expect that the full range of colors from the colormap would be used all the time (except when the vmin, vmax are larger/smaller than the levels min, max values). In other words, looking at this code I did a while back (Python 3):

import matplotlib.colors as mc
def addNorm(cmapData):
    cmapData['norm'] = mc.BoundaryNorm(cmapData['bounds'], cmapData['cmap'].N)
    return True
def discretize(cmap, bounds):
    resCmap = {}
    resCmap['cmap'] = mc.ListedColormap( \
        [cmap(i/len(bounds[1:])) for i in range(len(bounds[1:]))]
    )
    resCmap['bounds'] = bounds
    addNorm(resCmap)
    return resCmap

then use it as:

levels = [0, .1, .3, .5, 1, 3]
cmapData = discretize(plt.cm.jet, bounds=levels)
plt.contourf([[.12, .2], [.8, 3.2]], levels=levels, cmap=cmapData['cmap'], norm=cmapData['norm'])
plt.colorbar()

which gives the plot where you can actually distinguish the features (0.1-0.5), i.e. they are no longer in the blue region by using the above method with plt.cm.jet:

enter image description here

I mean, I know I solved this, and a while back too... but my question I guess is... how come the default in matplotlib is not this? I would have expected it to be this way... or maybe is it just a configuration / argument / something to enable this by default that I'm missing?

razvanc
  • 930
  • 2
  • 11
  • 18
  • Your update behaves _exactly_ as I expect. The yellow is still 2, your top line (which you aren't drawing) is red, and above the 3 contour is white because it is outside of your levels. – tacaswell Sep 04 '13 at 04:32
  • OK, I think I got the idea how this default colormaps are used (and I'm not bothered by that white), but... then is there a way to make the colors behave as _I should expect_? ie. using a **colormap** with colors from **c0** to **c8** let's say, for simplicity, but I still am referring to the _default_ colormaps): when I set `levels=[0, 0.09, 0.1, 0.11, 3]` I expect it to use **[c0 (for 0-0.09), c2 (0.09-0.1), c4 (0.09-0.1), c6 (0.1-0.11), c8 (0.11 - 0.3)]** instead of using **[c0 (0-0.09), c1 (0.09-0.1), c2 (0.1-0.11), c6 (0.11-3)]**. Hope this makes sense... – razvanc Sep 04 '13 at 05:07
  • fine, but that is not what you did. The color map knows nothing about your levels, all it knows is how to convert a scalar -> a color linearly between `vmin` and `vmax`. Look in to listed color maps. – tacaswell Sep 04 '13 at 05:31
  • http://matplotlib.org/api/colors_api.html#matplotlib.colors.ListedColormap – tacaswell Sep 04 '13 at 05:37
  • I would argue what you _want_ is incredibly _dependent_ on the data as the mapping is _wildly_ non-linear. You are not mapping the colors by the _value_ of the data, but by the _index_ of your bounds. I think it is very miss-leading to call that function `descritize`. – tacaswell Sep 04 '13 at 05:42
  • Yes... it is dependent on the data, that's why we give `levels` as a non-linear monotonically increasing sequence. And about the name of the function, it doesn't really matter (I just copy-pasted...). I think I just have it in my head that the colors should match the levels instead of the data itself... but I guess then this would defeat have a linear normalization on the data using `plt.cm.colors.Normalize`. – razvanc Sep 04 '13 at 05:46
  • 1
    See comment here: https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/contour.py#L1223 for how the colors are set – tacaswell Sep 04 '13 at 05:53
  • see my edit. A bit of monkey patching will make it behave in the way you want (even if I think it is a really strange way to _want_ it to behave) ;) – tacaswell Sep 04 '13 at 06:00

4 Answers4

3

After playing around a bit it seems that the answer to this question is way easier than I ever thought. Just some explanation first. While reading the documentation on the normalizing classes from matplotlib.colors I figured... well, matplotlib.colors.BoundaryNorm should be used here! but something is wrong as you can see in the following example:

import matplotlib.pyplot as plt
import matplotlib.colors as mc
levels = [0, .1, .3, .5, 1, 3]
norm = mc.BoundaryNorm(levels, len(levels)-1)
plt.contourf([[.12, .2], [.8, 2]], levels=levels, norm=norm)
plt.colorbar()
plt.show()

which gives this: enter image description here and this is obviously something we don't want! And I was thinking... why would you have to give to the constructor of BoundaryNorm the number of colors to use?... Shouldn't BoundaryNorm use the full extent of the colormap? And then it struck me, with just a little change to the code above:

# use here 256 instead of len(levels)-1 becuase
# as it's mentioned in the documentation for the
# colormaps, the default colormaps use 256 colors in their
# definition: print(plt.cm.jet.N) for example
norm = mc.BoundaryNorm(levels, 256)

and we get: enter image description here which is exactly what we want!

Or you we can do:

cmap = # user define cmap
norm = mc.BoundaryNorm(levels, cmap.N)
# which is I guess a little bit more programatically (is this a word?!) correct
razvanc
  • 930
  • 2
  • 11
  • 18
  • It's a pity this isn't well documented... it took a lot of time just to figure this simple thing. – razvanc Oct 21 '13 at 11:41
  • Sense you seem to understand you use case and boundry norm, can you write some documentation for this? It would be helpful to either improve the doc-strings or add an example to the gallery. – tacaswell Oct 21 '13 at 14:54
  • Yes, I will write some documentation on this, hopefully sooner than later. – razvanc Oct 22 '13 at 10:20
2

The color of the filled region is picked by mid point of the two lines it is filling between (iirc). The yellow you are seeing is the mapping of 2 under the color map and limits you set.

If you want to map the color by region index, do a bit of monkey patching:

def _process_colors_by_index(self):
    """
    Color argument processing for contouring.

    The color is based in the index in the level set, not
    the actual value of the level.

    """
    self.monochrome = self.cmap.monochrome
    if self.colors is not None:
        # Generate integers for direct indexing.
        i0, i1 = 0, len(self.levels)
        if self.filled:
            i1 -= 1
        # Out of range indices for over and under:
        if self.extend in ('both', 'min'):
            i0 = -1
        if self.extend in ('both', 'max'):
            i1 += 1
        self.cvalues = list(range(i0, i1))
        self.set_norm(colors.NoNorm())
    else:
        self.cvalues = range(len(self.levels))
    self.set_array(range(len(self.levels)))
    self.autoscale_None()
    if self.extend in ('both', 'max', 'min'):
        self.norm.clip = False

    # self.tcolors are set by the "changed" method


orig = matplotlib.contour.ContourSet._process_colors
matplotlib.contour.ContourSet._process_colors = _process_colors_by_index
cmap = plt.cm.jet
figure()
out = plt.contourf([[.12, .2], [.8, 2]], levels=[0, .1, .3, .5, 1, 3], cmap=cmap)
plt.colorbar()
# fix what we have done
matplotlib.contour.ContourSet._process_colors = orig

output

You can probably do better and remove the shift by 1/2 as well.

You can also reach in and just change the color of existing contours. It looks like you need to change the values of out.cvalues and then call out.changed() on the object.

A less destructive version would be to write a custom norm by sub-classing matplotlib.colors.Normalize, see colors.py for a template.

tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • Now that I think about what you said, this makes perfect sense. But still... this is not how I would have expected the colormap to behave (please see my comment above) – razvanc Sep 04 '13 at 05:11
  • Yes, this is cool :D... I didn't even knew you can do that... assign your own _home grown_ functions to change the core functionality like that. Yes, I would have expected it to be based on the index of the levels rather than the data :)... to be able to better visualize the regions of interest (ie. when the data is highly non-linear). Thanks! – razvanc Sep 04 '13 at 06:07
  • @razvanc also look into `LogNorm` or writing custom norm functions. That might be less disruptive in the long run. – tacaswell Sep 04 '13 at 06:08
  • I didn't know you can write custom norm functions... another new thing... this days is full of surprises :) – razvanc Sep 04 '13 at 06:12
  • 1
    as for example, using this makes your scripts basically unsharable as it changes how `mpl` behaves. You can be clever, keep a reference to the original and restore it later. – tacaswell Sep 04 '13 at 06:12
0

The maximum value of your data is 2. In the plot in question you set vmax=3.

In more detail, vmax sets the range of colors used in the mapping. Since this is much bigger than your data range, when you plot the data, you don't see the full range of colors. This is further confused by the small number of levels that you chose, which isn't showing you all the colors that are available, since the colorbar only shows a single color for the whole 1 to 3 range, again, obscuring colors available beyond 2.

tom10
  • 67,082
  • 10
  • 127
  • 137
0

Actually I think the best solution yet is located at this place:

http://protracted-matter.blogspot.ie/2012/08/nonlinear-colormap-in-matplotlib.html

It defines this little class which solves all the problems:

class nlcmap(mc.LinearSegmentedColormap):
    """A nonlinear colormap"""

    name = 'nlcmap'

    def __init__(self, cmap, levels):
        self.cmap = cmap
        # @MRR: Need to add N for backend
        self.N = cmap.N
        self.monochrome = self.cmap.monochrome
        self.levels = np.asarray(levels, dtype='float64')
        self._x = self.levels / self.levels.max()
        self._y = np.linspace(0.0, 1.0, len(self.levels))

    #@MRR Need to add **kw for 'bytes'
    def __call__(self, xi, alpha=1.0, **kw):
        yi = np.interp(xi, self._x, self._y)
        return self.cmap(yi, alpha)

The script was originally developed by a guy named Robert Hetland. All the details are in the link above.

razvanc
  • 930
  • 2
  • 11
  • 18
  • This breaks the normalization/color map model `matplotlib` uses. The correct place to put the non-linear behavior is in the `Normalize` functions. – tacaswell Sep 30 '13 at 15:50
  • Yes, you're right, for example with this I noticed that `set_over` for example doesn't work. But still, I don't have time to go into the details of the normalization class (since it doesn't have any examples on how to extend it) and this is just more easier for what I need. – razvanc Oct 02 '13 at 15:34
  • I unaccepted this answer as I finally found a satisfactory alternative using ``BoundaryNorm`` which I documented in the accepted new answer. – razvanc Oct 21 '13 at 11:40