pythonpython-3.xmatplotlibcolormapcontourf

contourf with custom colormap repeats color instead of changing


I want to plot some data with a logarithmic color code where a decade limit is indicated by a white/black interface. Grey levels are used to show some subdivisions of a decade. My problem is that there are two white neighboring regions in each decade even though the color map has the right number of entries (at least I think). Could someone help please?

In the meantime I made some tests and I found that it's the second color of the repeating pattern that is not used (the gray(0.25)), but I still have no idea why.

Here is the short version of the code:

import numpy as np
import matplotlib.pyplot as plt
from matplotlib import colors

# generate data
x = y = np.linspace(-3, 3, 200)
im = 1800*np.exp(-(np.outer(x,x) + y**2))
im = im / im.max() # normalize

# set logarithic levels (with small steps)
levS = np.array([1e-3,2e-3,4e-3,6e-3,8e-3,
                 1e-2,2e-2,4e-2,6e-2,8e-2,
                 0.1,0.2,0.4,0.6,0.8,
                 1])
# (5 intervals in one decade )

# avoid white patches by filling up to lowest level
im[ im < levS.min() ] = levS.min()

# make a list of 5 colors to create a colormap from
mapColS = [plt.cm.gray(0),plt.cm.gray(0.25),plt.cm.gray(0.50), 
           plt.cm.gray(0.7),plt.cm.gray(0.99)]
# repeat 3 times for the three decades
mapColS = mapColS + mapColS + mapColS
MyCmap=colors.ListedColormap(mapColS) # make color map

fig13g = plt.figure(1000) #create figure
ax13g = fig13g.add_subplot(111)

# plot lines
cax = plt.contour(im, levS, linewidths = 0.5,
                   norm=colors.LogNorm(), colors = 'k')
# fill with colors
cax = plt.contourf(im, levS, norm=colors.LogNorm(),
                   cmap=MyCmap)  # plt.cm.jet  OR  MyCmap

# show log color bar
cbar = fig13g.colorbar(cax, orientation='vertical',
                       spacing='regular',ticks= levS)

Here are the results:

The plot with the problem

For comparisson, using 'jet' there is no problem:
using 'jet' there is no problem


Solution

  • The problem is that different values end up producing the same color. This is due to the non-linear norm in use.
    For a linear norm, the colors for the layers of the contourf plot would be taken at the arithmetic mean between the levels. While this may also cause problems when comparing images and contour plots (as shown in How does pyplot.contourf choose colors from a colormap?), it would still leed to N unique colors being used for N+1 levels.

    For a LogNorm, the geometric mean is used instead of the arithmetic mean.

    In the following the values used to produce the colors from the colormap are shown. As can be seen several end up in the same bin.

    enter image description here

    Increasing the number of colors will allow each value to be in its own colorbin.

    enter image description here

    This is in principle exactly why the use of the 'jet' colormap works fine, because you have 256 different colors.

    Hence a possible solution is to use more colors for the colormap creation,

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib import colors
    
    # generate data
    x = y = np.linspace(-3, 3, 200)
    im = 1800*np.exp(-(np.outer(x,x) + y**2))
    im = im / im.max() # normalize
    
    # set logarithic levels (with small steps)
    levS = np.array([1e-3,2e-3,4e-3,6e-3,8e-3,
                     1e-2,2e-2,4e-2,6e-2,8e-2,
                     0.1,0.2,0.4,0.6,0.8,
                     1])
    # (5 intervals in one decade )
    
    # avoid white patches by filling up to lowest level
    im[ im < levS.min() ] = levS.min()
    
    # make a list of N colors to create a colormap from
    N = 20
    mapColS = list(plt.cm.gray(np.linspace(0,1,N)))
    # repeat 3 times for the three decades
    mapColR = mapColS + mapColS + mapColS
    MyCmap=colors.ListedColormap(mapColR) # make color map
    
    fig13g = plt.figure(1000) #create figure
    ax13g = fig13g.add_subplot(111)
    
    # plot lines
    c = plt.contour(im, levS, linewidths = 0.5,
                       norm=colors.LogNorm(), colors = 'k')
    # fill with colors
    cf = plt.contourf(im, levS, norm=colors.LogNorm(),
                       cmap=MyCmap)  # plt.cm.jet  OR  MyCmap
    
    cbar = fig13g.colorbar(cf, orientation='vertical',
                           spacing='regular',ticks= levS)                 
    plt.show()
    

    enter image description here

    The drawback of this is that you loose dynamic range because the lowest color is not black but dark grey.

    A different option would hence be to calculate those layer values and create a colormap with the respective colors at exactly those positions.

    enter image description here

    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib import colors
    
    # generate data
    x = y = np.linspace(-3, 3, 200)
    im = 1800*np.exp(-(np.outer(x,x) + y**2))
    im = im / im.max() # normalize
    
    # set logarithic levels (with small steps)
    levS = np.array([1e-3,2e-3,4e-3,6e-3,8e-3,
                     1e-2,2e-2,4e-2,6e-2,8e-2,
                     0.1,0.2,0.4,0.6,0.8,
                     1])
    # (5 intervals in one decade )
    
    # avoid white patches by filling up to lowest level
    im[ im < levS.min() ] = levS.min()
    
    # make a list of N colors to create a colormap from
    N = 5
    mapColS = list(plt.cm.gray(np.linspace(0,1,N)))
    # repeat 3 times for the three decades
    mapColR = mapColS + mapColS + mapColS
    
    #calculate layer values for lognorm
    layers = np.sqrt(levS[:-1]) * np.sqrt(levS[1:])
    norm = colors.LogNorm(levS.min(), levS.max())
    #add outmost values and colors
    lvals = np.concatenate(([0.], norm(layers), [1.]))
    cvals = [mapColR[0]] + mapColR + [mapColR[-1]]
    
    # make the colormap from values and colors
    MyCmap=colors.LinearSegmentedColormap.from_list("", list(zip(lvals,cvals)))
    
    fig13g = plt.figure(1000) #create figure
    ax13g = fig13g.add_subplot(111)
    
    # plot lines
    c = plt.contour(im, levS, linewidths = 0.5,
                       norm=norm, colors = 'k')
    # fill with colors
    cf = plt.contourf(im, levS, norm=norm,
                       cmap=MyCmap)
    
    cbar = fig13g.colorbar(cf, orientation='vertical',
                           spacing='regular',ticks= levS)                 
    plt.show()
    

    enter image description here