2

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

FraWa
  • 63
  • 10
  • It'll work [as expected](https://i.stack.imgur.com/egAgq.png) when using more colors per decade to define the colormap, e.g. `mapColS = list(plt.cm.gray(np.linspace(0,1,100)))`. However you will notice that the lowest is not completely black and the highest not completely white. This is due to the problem shown in [this question](https://stackoverflow.com/questions/46768028/how-does-pyplot-contourf-choose-colors-from-a-colormap/46777627#46777627). But one would need to look deeper in how the the solution there is applicable to a lognorm. – ImportanceOfBeingErnest Sep 25 '18 at 14:24
  • Thank you for the reply, but in fact I _want_ the colors to repeat within the colormap. (A decade change should always be the same black/white interface.) – FraWa Sep 25 '18 at 15:40
  • That's what it does, right? Except it's a verydarkgrey/almostwhite change. ;-) Now how important is it to have the exact color? – ImportanceOfBeingErnest Sep 25 '18 at 15:42
  • The decade change is ok. No problem for the nearlyblack to nearlywhite. But the thing I don't like is that the 0.6-0.8 interval has the same shade as the 0.8-1 interval.... And this is probably the case because the second color in the cmap is never used. (I replaced one by one _gray_ by _jet_ for each of the five elementary entries, and color appeared for all except the second.) – FraWa Sep 25 '18 at 15:48
  • Just to make sure we're talking about the same thing here: The very first sentence in my first comment links to an image. This image does use 5 different colors per decade. You will get this image with the code shown (replacing the first occurance of `mapColS`, not the second. It's a bit unlucky to name both the same.). – ImportanceOfBeingErnest Sep 25 '18 at 15:56
  • Sorry, you're right. Your solution gives the wanted result (with the small change from black to dark grey)...even though I still dont understand why we need 100 colours to use 5... whatsoever, if you transform your first comment to an answer, I'll happily tick it as solution. Thanks again – FraWa Sep 25 '18 at 16:14

2 Answers2

2

The problem is that you were repeating the same color levels mapColS 3 times by using mapColS = mapColS + mapColS + mapColS. The straight forward solution is to create a single continuous grayscale by dividing linearly the scale between plt.cm.gray(0) and plt.cm.gray(0.99) into 15 equal levels as

mapColS = [plt.cm.gray(i) for i in np.linspace(0, 0.99, 15)]

MyCmap=colors.ListedColormap(mapColS) # make color map

Output

enter image description here

Sheldore
  • 37,862
  • 7
  • 57
  • 71
  • Thank you for the reply, but in fact I want the colors to repeat within the colormap. (A decade change should always be the same black/white interface.) (See the discussion above.) – FraWa Sep 25 '18 at 16:16
  • Aah I see. In that case, the solution by @ImportanceOfBeingErnest works fine as `mapColS = [plt.cm.gray(i) for i in np.linspace(0,1,100)]` and then `mapColS = mapColS + mapColS + mapColS`. Sorry that you had to retract your vote from my answer – Sheldore Sep 25 '18 at 16:38
0

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

ImportanceOfBeingErnest
  • 321,279
  • 53
  • 665
  • 712