Modify matplotlib colormap - python

I'm trying to produce a similar version of this image using Python:
I'm close but can't quite figure out how to modify a matplotlib colormap to make values <0.4 go to white. I tried masking those values and using set_bad but I ended up with a real blocky appearance, losing the nice smooth contours seen in the original image.
Result with continuous colormap (problem: no white):
Result with set_bad (problem: no smooth transition to white):
Code so far:
from netCDF4 import Dataset as NetCDFFile
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.basemap import Basemap
nc = NetCDFFile('C:/myfile1.nc')
nc1 = NetCDFFile('C:/myfile2.nc')
lat = nc.variables['lat'][:]
lon = nc.variables['lon'][:]
time = nc.variables['time'][:]
uwnd = nc.variables['uwnd'][:]
vwnd = nc1.variables['vwnd'][:]
map = Basemap(llcrnrlon=180.,llcrnrlat=0.,urcrnrlon=340.,urcrnrlat=80.)
lons,lats = np.meshgrid(lon,lat)
x,y = map(lons,lats)
speed = np.sqrt(uwnd*uwnd+vwnd*vwnd)
#speed = np.ma.masked_where(speed < 0.4, speed)
#cmap = plt.cm.jet
#cmap.set_bad(color='white')
levels = np.arange(0.0,3.0,0.1)
ticks = np.arange(0.0,3.0,0.2)
cs = map.contourf(x,y,speed[0],levels, cmap='jet')
vw = plt.quiver(x,y,speed)
cbar = plt.colorbar(cs, orientation='horizontal', cmap='jet', spacing='proportional',ticks=ticks)
cbar.set_label('850 mb Vector Wind Anomalies (m/s)')
map.drawcoastlines()
map.drawparallels(np.arange(20,80,20),labels=[1,1,0,0], linewidth=0.5)
map.drawmeridians(np.arange(200,340,20),labels=[0,0,0,1], linewidth=0.5)
#plt.show()
plt.savefig('phase8_850wind_anom.png',dpi=600)

The answer to get the result smooth lies in constructing your own colormap. To do this one has to create an RGBA-matrix: a matrix with on each row the amount (between 0 and 1) of Red, Green, Blue, and Alpha (transparency; 0 means that the pixel does not have any coverage information and is transparent).
As an example the distance to some point is plotted in two dimensions. Then:
For any distance higher than some critical value, the colors will be taken from a standard colormap.
For any distance lower than some critical value, the colors will linearly go from white to the first color of the previously mentioned map.
The choices depend fully on what you want to show. The colormaps and their sizes depend on your problem. For example, you can choose different types of interpolation: linear, exponential, ...; single- or multi-color colormaps; etc..
The code:
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
# create colormap
# ---------------
# create a colormap that consists of
# - 1/5 : custom colormap, ranging from white to the first color of the colormap
# - 4/5 : existing colormap
# set upper part: 4 * 256/4 entries
upper = mpl.cm.jet(np.arange(256))
# set lower part: 1 * 256/4 entries
# - initialize all entries to 1 to make sure that the alpha channel (4th column) is 1
lower = np.ones((int(256/4),4))
# - modify the first three columns (RGB):
# range linearly between white (1,1,1) and the first color of the upper colormap
for i in range(3):
lower[:,i] = np.linspace(1, upper[0,i], lower.shape[0])
# combine parts of colormap
cmap = np.vstack(( lower, upper ))
# convert to matplotlib colormap
cmap = mpl.colors.ListedColormap(cmap, name='myColorMap', N=cmap.shape[0])
# show some example
# -----------------
# open a new figure
fig, ax = plt.subplots()
# some data to plot: distance to point at (50,50)
x,y = np.meshgrid(np.linspace(0,99,100),np.linspace(0,99,100))
z = (x-50)**2. + (y-50)**2.
# plot data, apply colormap, set limit such that our interpretation is correct
im = ax.imshow(z, interpolation='nearest', cmap=cmap, clim=(0,5000))
# add a colorbar to the bottom of the image
div = make_axes_locatable(ax)
cax = div.append_axes('bottom', size='5%', pad=0.4)
cbar = plt.colorbar(im, cax=cax, orientation='horizontal')
# save/show the image
plt.savefig('so.png')
plt.show()
The result:

Related

Matplotlib contour: set range of colorbar (not range of colormap)

I have a problem with the colorbar for contour plots with user-defined levels. As you can see in the figure below, the color for the highest level (red) is not shown well in the colorbar. To solve this, I would like to change the range of the colorbar (e.g. from 0 to 1), while not changing the contour levels or range of the colormap.
So far, I have tried to manually set the ticks of the colorbar in the colorbar-call, but that doesn't change the range. (That is, ticks outside 0.3-0.8 will just not show up)
Minimum working example:
import numpy as np
import matplotlib.pyplot as plt
z = np.random.rand(10,10)
im = plt.contour(z, levels = [0.3,0.5,0.8], cmap="rainbow")
plt.colorbar(im)
I would like to change the range of the colorbar (e.g. from 0 to 1)
Save the colorbar object, change its boundaries and the ticks' position, finally update the plot to reflect our wishes.
import numpy as np
import matplotlib.pyplot as plt
z = np.random.rand(10,10)
im = plt.contour(z, levels = [0.3,0.5,0.8], cmap="rainbow")
cb = plt.colorbar(im)
cb.boundaries = np.array((0.0, 0.5, 1.0))
cb.set_ticks((0, 0.3, 0.5, 0.8, 1))
cb.update_normal(im)

How to plot different shades of a color for each point in a scatter plot in matplotlib?

I am trying to plot a scatter plot where each point in the scatter plot should correspond to a particular shade of a given color of my choice. The mpl documentation states that if I set something like:
color = '0.7'
it gives me a shade of grey with that scaled intensity of 0.7. I am reading the intensity of the colours from an array with values between 0 and 1 and each value corresponds to the intensity of that point in the scatter plot. My code below is as follows:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import math
tsne_embeddings = np.load("tsne_embeddings.npy")
labels = np.load('labels.npy')
weights = np.load('weights.npy')
# Scale the weights from 0 to 1
max_weight = max(weights)
min_weight = min(weights)
weights = (weights - min_weight)/(max_weight - min_weight)
print(tsne_embeddings.shape)
x = list(tsne_embeddings[:,0])
y = list(tsne_embeddings[:,1])
labels = list(labels)
weights = np.round(weights,decimals=2)
weights = (np.exp(weights) - 1)/(np.exp(1) - 1)
weights = list(weights)
print(min(weights),max(weights))
for i, shade in enumerate(weights):
plt.scatter(x[i],y[i],color=shade,marker = '+')
plt.show()
I am scaling those weights exponentially hoping for a better variation.
So, essentially, my questions are:
How do I change the color to say shades of blue or red or green as opposed to just greyscale?
Is the approach which I follow for greyscale correct?
Thank you!
To make your approach work for shades of grey, you need to convert the value to a string, so plt.scatter(..., color=str(shade)).
The more standard way of working with matplotlib would be to directly use the weights, without rescaling them to the range 0 to 1, use a colormap, and calling scatter directly with the arrays. The weights go into the c= parameter. For grey values this would be plt.scatter(x, y, c=weights, cmap='Greys', marker='+'). An additional feature of matplotlib is that with this information it can automatically create a colorbar mapping the grey values to the corresponding weight. If only one scatter plot is created, plt.colorbar() without parameters will show this colorbar.
Similar colormaps exist of 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', ... See the official doc with a complete list. If the range between light and dark goes the wrong way, appending '_r' to the name will use the opposite color range (so, 'Greys' goes from white to black, while 'Greys_r' goes from black to white).
Here is a working example using the values from 1 to 10 for the three arrays:
from matplotlib import pyplot as plt
import numpy as np
x = np.arange(1, 11)
y = np.arange(1, 11)
weights = np.arange(1, 11)
plt.scatter(x, y, c=weights, cmap='Greys', marker='+')
plt.colorbar()
plt.show()
You can use colormaps in python to generate different shades of blue green etc.
https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html
I am using Blues color map here
import matplotlib.pyplot as plt
import matplotlib as mpl
norm = mpl.colors.Normalize(vmin=min(weights), vmax=max(weights))
cmap = mpl.cm.ScalarMappable(norm=norm, cmap=mpl.cm.Blues)
for i, xi in enumerate(x):
plt.scatter(x[i],y[i],color=cmap.to_rgba(i+1),marker = '+')
plt.show()

Python Matplotlib: Create normal colorbar with white interval at specific values

I have Sea Ice Concentration data around Antarctica that range between -40 and +40 (see attached Figure) but I would like for the values between +10 and -10 to appear white in my map and colorbar because they do not represent sea ice concentration (they appear light green and light blue in the current Figure).
I do not want to exclude them from my array but rather want to assign them a specific color (in this case, white) and keep the jet colorscale for the other values.
I have looked at multiple other questions (i.e. How to change colorbar's color (in some particular value interval)?; Python matplotlib change default color for values exceeding colorbar range; **Reset default matplotlib colormap values after using 'set_under' or 'set_over')
but have not managed to apply those to my case.
I have tried to use 'set under','set over', 'set.bad' but have not managed to get what I would like. I have also tried to create my own colormap but haven't been successful either.
Would anyone be able to help?
Thanks.
UPDATE:
I have adapted the code from 'stackoverflow.com/a/41815114/5103802' (see Updated Code below) but my colors are not white for values [+10 -10] and 'jet'
for above +10 and below -10 (see Figure below). The 'tmap' object seems to have messed up the colorscale. Any idea on how I could get the jet colorscale and leave the interval between -10 and +10 white?
Thanks for your help.
Updated code:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors
from mpl_toolkits.basemap import Basemap, cm, rcParams
HIGH_86 = a.variables['high_86'][:]
n=40
x = 10
lower = plt.cm.seismic(np.linspace(1-x, 0, n))
upper = plt.cm.seismic(np.linspace(0, x, n))
white1 = plt.cm.seismic(np.ones(10)*x)
white2 = plt.cm.seismic(np.ones(10)*-(x))
colors = np.vstack((lower, white1, white2, upper))
tmap = matplotlib.colors.LinearSegmentedColormap.from_list('terrain_map_white', colors)
x = HIGH_86
lats = a.variables['latitude'][:]
lons = a.variables['longitude'][:]
lons, lats = np.meshgrid(lons,lats)
fig, ax = plt.subplots()
m2 = Basemap(projection='spstere',boundinglat=-50,lon_0=180,resolution='l')
CS2 = m2.contourf(lons,lats,x,cmap=tmap,latlon=True)
cbar = m2.colorbar(CS2,location='right', pad="12%")
m2.drawparallels(np.arange(-90.,99.,60.),labels=[False,False,False,False])
m2.drawmeridians(np.arange(-180.,180.,90.),labels=[True,False,False,True])
m2.drawcoastlines()
m1.fillcontinents(color='grey')
plt.title('SIE Anomaly')
plt.show()
I assume you have read the linked question and it's answer.
It clearly states
Colormaps are always ranged between 0 and 1.
and further explains how to arrive at a suitable colormap. It thus makes no sense to supply values of 10 or -9 to the colormap.
While you could directly copy the code from the answer here and would receive a decent result, you may of course also refine it to match this specific case, where one may go for 80 colors, 30 from the lower part of the colormap, 30 from the upper part and the remaining 20 in the middle to be white.
n=30
x = 0.5
lower = plt.cm.seismic(np.linspace(0, x, n))
white = plt.cm.seismic(np.ones(80-2*n)*x)
upper = plt.cm.seismic(np.linspace(1-x, 1, n))
colors = np.vstack((lower, white, upper))
tmap = matplotlib.colors.LinearSegmentedColormap.from_list('map_white', colors)
In order to get the jet colormap, which does not have white in it, an array of ones may be used in the middle
n=30
x = 0.5
cmap = plt.cm.jet
lower = cmap(np.linspace(0, x, n))
white = np.ones((80-2*n,4))
upper = cmap(np.linspace(1-x, 1, n))
colors = np.vstack((lower, white, upper))
tmap = matplotlib.colors.LinearSegmentedColormap.from_list('map_white', colors)
Complete code to reproduce the image above:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors
n=30
x = 0.5
cmap = plt.cm.jet
lower = cmap(np.linspace(0, x, n))
white = np.ones((80-2*n,4))
upper = cmap(np.linspace(1-x, 1, n))
colors = np.vstack((lower, white, upper))
tmap = matplotlib.colors.LinearSegmentedColormap.from_list('map_white', colors)
x = np.linspace(0,10)
X,Y = np.meshgrid(x,x)
z = np.sin(X) * np.cos(Y*0.4)*40
fig, ax = plt.subplots()
im = ax.contourf(z, cmap=tmap, vmin=-40, vmax=40)
plt.colorbar(im)
plt.show()

Modifying python colormaps to single value beyond a specific point

How do I change a colormap color scheme to show the same color beyond a point.
E.g. here's my colormap:
import palettable
cmap = palettable.colorbrewer.sequential.YlGn_9.mpl_colormap
If I use this colormap to plot a range from 0 to 100, how can I modify the color map such that beyond 50, it changes to the color red?
You could create the colormap for the given range (0 →100) by stacking two different colormaps on top of each other as shown:
Illustration:
import numpy as np
import matplotlib.pyplot as plt
import palettable
import matplotlib.colors as mcolors
# Set random seed
np.random.seed(42)
# Create random values of shape 10x10
data = np.random.rand(10,10) * 100
# Given colormap which takes values from 0→50
colors1 = palettable.colorbrewer.sequential.YlGn_9.mpl_colormap(np.linspace(0, 1, 256))
# Red colormap which takes values from 50→100
colors2 = plt.cm.Reds(np.linspace(0, 1, 256))
# stacking the 2 arrays row-wise
colors = np.vstack((colors1, colors2))
# generating a smoothly-varying LinearSegmentedColormap
cmap = mcolors.LinearSegmentedColormap.from_list('colormap', colors)
plt.pcolor(data, cmap=cmap)
plt.colorbar()
# setting the lower and upper limits of the colorbar
plt.clim(0, 100)
plt.show()
Incase you want the upper portion to be of the same color and not spread over the length of the colormap, you could make the following modification:
colors2 = plt.cm.Reds(np.linspace(1, 1, 256))
cmap.set_over("red")
And you may wanna use one of the norm functions to set your specific bounds. If using imshow, you can also set the parameter vmin=50 to make that your top value.
You can create a new colormap from an existing colormap using:
newcmap = cmap.from_list('newcmap',list(map(cmap,range(50))), N=50)
This new map uses the last value from the colormap for colors over 50. To make the last color red, we can just append red to the last color in the list that defines the colormap.
newcmap = cmap.from_list('newcmap',list(map(cmap,range(50)))+[(1,0,0,1)], N=51)
import palettable
from matplotlib import pyplot as plt
cmap = palettable.colorbrewer.sequential.YlGn_9.mpl_colormap
newcmap = cmap.from_list('newcmap',list(map(cmap,range(50))), N=50)
for x in range(80):
plt.bar(x,1, width=1, edgecolor='none',facecolor=newcmap(x))
plt.show()
newcmap = cmap.from_list('newcmap',list(map(cmap,range(50)))+[(1,0,0,1)], N=51)
for x in range(80):
plt.bar(x,1, width=1, edgecolor='none',facecolor=newcmap(x))
plt.show()
You can access the colors with:
cmap_dict = cmap._segmentdata
which yields a dictionary. By indexing it with:
red = cmap_dict["red"]
green= cmap_dict["green"]
blue = cmap_dict["blue"]
alpha = cmap_dict["alpha"]
Now you can add a color from the list like this:
red .append(red [1])
recombine them into a dictionary with the 4 keys like:
cmap_dict_new["red"] = red
and create a new colormap with:
new_cmap = palettable.palette.ListedColormap(cmap_dict_new)
I don't think you should change the colormap, but rather the object using the colormap. I asked a similar question not so long ago: change color for first level of contourf, and I took the answer from here: Python matplotlib change default color for values exceeding colorbar range
If you use contours in your plot for example, you should do something like this:
cs = pyplot.contourf(x,y,z, cmap=your_cmap)
cs.cmap.set_over('r') # Change color to red
cs.set_clim(0, 50) # Set the limit beyond which everything is red
cb = pyplot.colorbar(cs) # Plot the colorbar (if needed)

Plot semi transparent contour plot over image file using matplotlib

I'd like to plot a transparent contour plot over an image file in matplotlib/pyplot.
Here's what I got so far...
I have a 600x600 pixel square image file test.png that looks like so:
I would like to plot a contour plot over this image (having the image file be 'below' and a semi-transparent version of the contour plot overlaid) using matplotlib and pyplot. As a bonus, the image would be automatically scaled to fit within the current plotting boundaries. My example plotting script is as follows:
from matplotlib import pyplot
from matplotlib.ticker import MultipleLocator, FormatStrFormatter
from matplotlib.colors import BoundaryNorm
from matplotlib.ticker import MaxNLocator
from pylab import *
import numpy as np
import random
# ----------------------------- #
dx, dy = 500.0, 500.0
y, x = np.mgrid[slice(-2500.0, 2500.0 + dy, dy),slice(-2500.0, 2500.0 + dx, dx)]
z = []
for i in x:
z.append([])
for j in y:
z[-1].append(random.uniform(80.0,100.0))
# ----------------------------- #
plot_aspect = 1.2
plot_height = 10.0
plot_width = int(plot_height*plot_aspect)
# ----------------------------- #
pyplot.figure(figsize=(plot_width, plot_height), dpi=100)
pyplot.subplots_adjust(left=0.10, right=1.00, top=0.90, bottom=0.06, hspace=0.30)
subplot1 = pyplot.subplot(111)
# ----------------------------- #
cbar_max = 100.0
cbar_min = 80.0
cbar_step = 1.0
cbar_num_colors = 200
cbar_num_format = "%d"
# ----------
levels = MaxNLocator(nbins=cbar_num_colors).tick_values(cbar_min, cbar_max)
cmap = pyplot.get_cmap('jet')
norm = BoundaryNorm(levels, ncolors=cmap.N, clip=True)
pp = pyplot.contourf(x,y,z,levels=levels,cmap=cmap)
cbar = pyplot.colorbar(pp, orientation='vertical', ticks=np.arange(cbar_min, cbar_max+cbar_step, cbar_step), format=cbar_num_format)
cbar.ax.set_ylabel('Color Scale [unit]', fontsize = 16, weight="bold")
# ----------
CS = pyplot.contour(x,y,z, alpha=0.5)
# ----------
majorLocator1 = MultipleLocator(500)
majorFormatter1 = FormatStrFormatter('%d')
minorLocator1 = MultipleLocator(250)
subplot1.xaxis.set_major_locator(majorLocator1)
subplot1.xaxis.set_major_formatter(majorFormatter1)
subplot1.xaxis.set_minor_locator(minorLocator1)
pyplot.xticks(fontsize = 16)
pyplot.xlim(-2500.0,2500.0)
# ----------
majorLocator2 = MultipleLocator(500)
majorFormatter2 = FormatStrFormatter('%d')
minorLocator2 = MultipleLocator(250)
subplot1.yaxis.set_major_locator(majorLocator2)
subplot1.yaxis.set_major_formatter(majorFormatter2)
subplot1.yaxis.set_minor_locator(minorLocator2)
pyplot.yticks(fontsize = 16)
pyplot.ylim(-2500.0,2500.0)
# ----------
subplot1.xaxis.grid()
subplot1.yaxis.grid()
# ----------
subplot1.axes.set_aspect('equal')
# ----------
pyplot.suptitle('Main Title', fontsize = 24, weight="bold")
# ----------
pyplot.xlabel('X [m]', fontsize=16, weight="bold")
pyplot.ylabel('Y [m]', fontsize=16, weight="bold")
# ----------
implot = subplot1.imshow( pyplot.imread('test.png') , interpolation='nearest', alpha=0.5)
# ----------
pyplot.show()
#pyplot.savefig("tmp.png", dpi=100)
pyplot.close()
...but I'm not getting the result I want... instead I just see the contour plot part. Something like:
What should I do in my code to get what I want?
You basically need to do two things, set the extent of the image you want in the background. If you dont, the coordinates are assumed to be pixel coordinates, in this case 0 till 600 for both x and y. So adjust you imshow command to:
implot = subplot1.imshow(pyplot.imread(r'test.png'), interpolation='nearest',
alpha=0.5, extent=[-2500.0,2500.0,-2500.0,2500.0])
If you want to stretch the image to the limits of the plot automatically, you can grab the extent with:
extent = subplot1.get_xlim()+ subplot1.get_ylim()
And pass it to imshow as extent=extent.
Since its the background image, setting the alpha to 0.5 makes it very faint, i would set it to 1.0.
Secondly, you set the alpha of the contour lines, but you probably also (or especially) want to set the alpha of the filled contours. And when you use alpha with filled contours, enabling anti-aliasing reduces artifacts. So change your contourf command to:
pp = pyplot.contourf(x,y,z,levels=levels,cmap=cmap, alpha=.5, antialiased=True)
And since you already create the subplot object yourself, i would advice also using it to do the plotting instead of the pyplot interface, which operates on the currently active axes.
So:
subplot1.contourf()
etc
Instead of:
pyplot.contourf()
With the two changes mentioned above, my result looks like:
I personally used the multiple contour plot answer for a while with great results. However, I had to output my figures to PostScript, which does not support opacity (alpha option). I found this answer useful since it does not require the use of opacity.
The reason these lines show up is due to the edge color of the faces that make up the contour plot. The linked solution avoids this by changing the edge color to the face color.
cf = plt.contourf(x, y, z, levels=100)
# This is the fix for the white lines between contour levels
for c in cf.collections:
c.set_edgecolor("face")

Categories

Resources