How can I have straight contourlines in matplotlib? - python

I am plotting values with imshow, and I want to have one contourline at a certain value. However, pyplot.contour() uses some kind of interpolation which causes the contourlines to be diagonal around the point. How can I make sure that the lines are exactly lined up with my rectangular boxes (so only horizontal and vertical lines)?
(Anyone who wants to reproduce the picture I've got, the values are uploaded here)
A picture of the data looks like this:
produced with this code:
pyplot.imshow(KS_imshow, extent = [5. ,8., 0., 22., ], origin='lower', interpolation='nearest', aspect='auto', cmap = 'Blues', vmin = 0., vmax = 1.)
cbar = pyplot.colorbar()
CS2 = pyplot.contour(ri,phii,KS_imshow,levels=[0.5], colors='r')
cbar.add_lines(CS2)
pyplot.show()
The variables ri, phii and KS_imshow are in the linked document.

The problem is that imshow creates "pixels", but the underlying data are just points (at the centers). Thus contour does not know anything about the image which imshow creates. However, you can create a similar image by upscaling the original data and then use contour on that. It is certainly a hack, but it achieves what you want. There remains a problem at the edges though and I'm not sure how to solve that.
import matplotlib.pyplot as plt
import numpy as np
import scipy.ndimage
# data ranges
xr = [5., 8.]
yr = [0., 22.]
# pixel widths
x_pw = np.diff(xr) / (KS_imshow.shape[1])
y_pw = np.diff(yr) / (KS_imshow.shape[0])
# plot the image
plt.imshow(KS_imshow, extent=xr+yr, origin='lower', interpolation='nearest',
aspect='auto', cmap='Blues', vmin=0., vmax=1.)
cbar = plt.colorbar()
# upscale by a factor of 50 (might be an issue for large arrays)
highres = scipy.ndimage.zoom(KS_imshow, 50, order=0, mode='nearest')
# correct the extent by the pixel widths
extent = np.array(xr+yr) + np.array([x_pw, -x_pw, y_pw, -y_pw]).flatten()/2
# create the contours
CS2 = plt.contour(highres, levels=[0.5], extent=extent, origin='lower',
colors='r', linewidths=2)
cbar.add_lines(CS2)
plt.show()
Result:
However, just to show a threshold of 0.5, I would suggest to customize the colormap instead of using a contour line:
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.colors as mcolors
blues = plt.cm.Blues(np.linspace(0,1,200))
reds = plt.cm.Reds(np.linspace(0,1,200))
colors = np.vstack((blues[0:128,:], reds[-129:,:]))
i = np.linspace(0,1,256)
r = np.column_stack((i, colors[:-1,0], colors[1:,0]))
g = np.column_stack((i, colors[:-1,1], colors[1:,1]))
b = np.column_stack((i, colors[:-1,2], colors[1:,2]))
d = dict(red=r, green=g, blue=b)
mycmap = mcolors.LinearSegmentedColormap('mymap', d, N=256)
plt.imshow(KS_imshow, extent=[5, 8, 0, 22], origin='lower',
interpolation='nearest', aspect='auto', cmap=mycmap,
vmin=0., vmax=1.)
cbar = plt.colorbar()
plt.show()
Result:

As an addition to the nice answer by #hitzig I present some code that makes it simpler to draw straight contour lines. However, the underlying principle is exactly the same.
All we need are
the extent of the data ...
and np.kron
Then we can scale up our data using big_data = np.kron(data, np.ones((factor, factor))) and draw the contour lines using the big_data array. We make sure that the size of the image stays the same by passing the extent of the original data.
Example:
# Make up some data
data = np.zeros((10, 20))
data[2:4, 2:8] = 1 + np.random.random((2,6))
# Extent of the data into x and y directions
# (left, right, bottom, top)
extent = [0, 20, 0, 10]
# Plot the data a few times. Each time, the contours
# get drawn based on "enlarged" data to some factor
enlargement_factors = [1, 2, 10]
fig, axs = plt.subplots(len(enlargement_factors), 1)
for i, fac in enumerate(enlargement_factors):
# Draw the data
im = axs[i].imshow(data, origin='lower', aspect='auto', extent=extent)
# Scale the data up (enlarge) ... or leave equal if fac==1
big_data = np.kron(data, np.ones((fac, fac)))
# Draw the contour lines of the data
axs[i].contour(big_data, levels=[0.5], extent=extent, colors='w')
axs[i].set_title('Enlargement factor: {}'.format(fac))
fig.tight_layout()

Related

Overwrite points from pcolormesh if they aren't contained in a polygon

I'm trying to plot a map whereby a spatial pattern is plotted over the land using pcolormesh or contourf. A shapefile/polygon that describes the border of the UK is then overlayed onto this plot. My problem is how to directly access the points that fall outside the polygon to set them as 0 or directly colour them a single colour e.g. white. See the following minimal working example
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
# Load polygon
world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
UK = world[world.iso_a3 == "GBR"]
UK.boundary.plot()
# Simulate a spatial pattern
xlims = (-8, 3)
ylims = (49, 60)
resolution = 0.05
y, x = np.mgrid[slice(ylims[0], ylims[1] + resolution, resolution),
slice(xlims[0], xlims[1] + resolution, resolution)]
z = 0.5*x+np.sin(x)**2+ np.cos(y)
# Plot
fig, ax=plt.subplots(figsize=(6, 10))
im = ax.pcolormesh(x, y, z, cmap='viridis')
fig.colorbar(im, ax=ax)
UK.boundary.plot(ax=ax, color='black')
I have tried excluding any points in the original dataset and then generating the pcolormesh. However, pcolormesh interpolates between points. This results in a series of points being generated from Northern Ireland down to Cornwall. Just to be clear, what I would like is to fill outside the polygon. Thanks for any help.
Rather than what you request (modifying the values of z), I plot another layer of pcolormesh() on top to get the desired effect. In this process, a new_z array is created with individual values obtained from point-within_polygon operation. A custom colormap, new_binary is created to use with new_z to plot this layer and get the final plot.
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import matplotlib as mpl
from shapely.geometry import Point
# Load polygon
world = gpd.read_file(gpd.datasets.get_path("naturalearth_lowres"))
UK = world[world.iso_a3 == "GBR"]
# plot 1
#UK.boundary.plot()
# Simulate a spatial pattern
xlims = (-8, 3)
ylims = (49, 60)
resolution = 0.05 # 0.05
# slice()
y, x = np.mgrid[slice(ylims[0], ylims[1] + resolution, resolution),
slice(xlims[0], xlims[1] + resolution, resolution)]
z = 0.5*x+np.sin(x)**2+ np.cos(y)
# Target geometry, for point inside/outside polygon operation
ukgeom = UK['geometry'].values[0]
def prep_z(Xs,Ys,Zs):
# Xs,Ys: result of np.meshgrid(lon, lat)
# Zs: function of(Xs,Ys) to be manipulated; here hard-coded as `new_z`
for ro,(arow,acol) in enumerate(zip(Xs,Ys)):
# need 2 level loop to handle each grid point
for co,xiyi in enumerate(zip(arow,acol)):
pnt1 = Point(xiyi)
if pnt1.within(ukgeom):
new_z[ro][co]=1 #0:white, 1:black with cm='binary'
else:
new_z[ro][co]=0
pass
pass
# Usage of the function above: prep_z(x,y,z)
# Result: new_z is modified.
# New z for locations in/outside-polygon operation
new_z = np.zeros(z.shape)
prep_z(x,y,z)
# create custom colormap to use later
new_binary = mpl.colors.ListedColormap(np.array([[1., 1., 1., 1.],
[0., 0., 0., 0.]]))
# 0:white, 1:transparent with cm='new_binary'
# do not use alpha option to get the intended result
# Plot 2
fig, ax = plt.subplots(figsize=(6, 10))
im = ax.pcolormesh(x, y, z, cmap='viridis', zorder=1)
im2 = ax.pcolormesh(x, y, new_z, cmap=new_binary, zorder=2) # do not use alpha to get transparent mask
UK.boundary.plot(ax=ax, color='black', zorder=10)
fig.colorbar(im, ax=ax, shrink=0.5)
plt.show()

Add horizontal line with conditional coloring

I make a contourf plot using matplotlib.pyplot. Now I want to have a horizontal line (or something like ax.vspan would work too) with conditional coloring at y = 0. I will show you what I have and what I would like to get. I want to do this with an array, let's say landsurface that represents either land, ocean or ice. This array is filled with 1 (land), 2 (ocean) or 3 (ice) and has the len(locs) (so the x-axis).
This is the plot code:
plt.figure()
ax=plt.axes()
clev=np.arange(0.,50.,.5)
plt.contourf(locs,height-surfaceheight,var,clev,extend='max')
plt.xlabel('Location')
plt.ylabel('Height above ground level [m]')
cbar = plt.colorbar()
cbar.ax.set_ylabel('o3 mixing ratio [ppb]')
plt.show()
This is what I have so far:
This is what I want:
Many thanks in advance!
Intro
I'm going to use a line collection .
Because I have not your original data, I faked some data using a simple sine curve and plotting on the baseline the color codes corresponding to small, middle and high values of the curve
Code
Usual boilerplate, we need to explicitly import LineCollection
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection
Just to plot something, a sine curve (x r
x = np.linspace(0, 50, 101)
y = np.sin(0.3*x)
The color coding from the curve values (corresponding to your surface types) to the LineCollection colors, note that LineCollection requires that the colors are specified as RGBA tuples but I have seen examples using color strings, bah!
# 1 when near min, 2 when near 0, 3 when near max
z = np.where(y<-0.5, 1, np.where(y<+0.5, 2, 3))
col_d = {1:(0.4, 0.4, 1.0, 1), # blue, near min
2:(0.4, 1.0, 0.4, 1), # green, near zero
3:(1.0, 0.4, 0.4, 1)} # red, near max
# prepare the list of colors
colors = [col_d[n] for n in z]
In a line collection we need a sequence of segments, here I have decided to place my coded line at y=0 but you can just add a constant to s to move it up and down.
I admit that forming the sequence of segments is a bit tricky...
# build the sequence of segments
s = np.zeros(101)
segments=np.array(list(zip(zip(x,x[1:]),zip(s,s[1:])))).transpose((0,2,1))
# and fill the LineCollection
lc = LineCollection(segments, colors=colors, linewidths=5,
antialiaseds=0, # to prevent artifacts between lines
zorder=3 # to force drawing over the curve) lc = LineCollection(segments, colors=colors, linewidths=5) # possibly add zorder=...
Finally, we put everything on the canvas
# plot the function and the line collection
fig, ax = plt.subplots()
ax.plot(x,y)
ax.add_collection(lc)
I would suggest adding an imshow() with proper extent, e.g.:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colorbar as colorbar
import matplotlib.colors as colors
### generate some data
np.random.seed(19680801)
npts = 50
x = np.random.uniform(0, 1, npts)
y = np.random.uniform(0, 1, npts)
X,Y=np.meshgrid(x,y)
z = x * np.exp(-X**2 - Y**2)*100
### create a colormap of three distinct colors for each landmass
landmass_cmap=colors.ListedColormap(["b","r","g"])
x_land=np.linspace(0,1,len(x)) ## this should be scaled to your "location"
## generate some fake landmass types (either 0, 1, or 2) with probabilites
y_land=np.random.choice(3, len(x), p=[0.1, 0.6, 0.3])
print(y_land)
fig=plt.figure()
ax=plt.axes()
clev=np.arange(0.,50.,.5)
## adjust the "height" of the landmass
x0,x1=0,1
y0,y1=0,0.05 ## y1 is the "height" of the landmass
## make sure that you're passing sensible zorder here and in your .contourf()
im = ax.imshow(y_land.reshape((-1,len(x))),cmap=landmass_cmap,zorder=2,extent=(x0,x1,y0,y1))
plt.contourf(x,y,z,clev,extend='max',zorder=1)
ax.set_xlim(0,1)
ax.set_ylim(0,1)
ax.plot()
ax.set_xlabel('Location')
ax.set_ylabel('Height above ground level [m]')
cbar = plt.colorbar()
cbar.ax.set_ylabel('o3 mixing ratio [ppb]')
## add a colorbar for your listed colormap
cax = fig.add_axes([0.2, 0.95, 0.5, 0.02]) # x-position, y-position, x-width, y-height
bounds = [0,1,2,3]
norm = colors.BoundaryNorm(bounds, landmass_cmap.N)
cb2 = colorbar.ColorbarBase(cax, cmap=landmass_cmap,
norm=norm,
boundaries=bounds,
ticks=[0.5,1.5,2.5],
spacing='proportional',
orientation='horizontal')
cb2.ax.set_xticklabels(['sea','land','ice'])
plt.show()
yields:

Overlaying plots on a single graph

I have two heatmaps which are based on 2d histograms that I am trying to overlay on a single graph. The limits of their axes (extent_L and extent_H) do not necessarily coincide exactly. I can make the individual plots satisfactorily if needed, but when trying to show both heatmaps on a single graph nicely, only the most recent one is displayed.
import numpy as np
import numpy.random
import matplotlib.pyplot as plt
# Generate some test data
x_L = np.random.randn(8873)
y_L = np.random.randn(8873)
x_H = np.random.randn(1000)
y_H = np.random.randn(1000)
heatmap_L, xedges_L, yedges_L = np.histogram2d(x_L, y_L, bins=50)
extent_L = [xedges_L[0], xedges_L[-1], yedges_L[0], yedges_L[-1]]
heatmap_H, xedges_H, yedges_H = np.histogram2d(x_H, y_H, bins=50)
extent_H = [xedges_H[0], xedges_H[-1], yedges_H[0], yedges_H[-1]]
plt.clf()
im1 = plt.imshow(heatmap_L.T, extent=extent_L, origin='lower', cmap='Blues')
im2 = plt.imshow(heatmap_H.T, extent=extent_H, origin='lower', cmap='Greens')
plt.show()
Edit: If I'm not mistaken, all points are not in exactly the proper location
import numpy as np
import numpy.random
import matplotlib.pyplot as plt
# Generate some test data
x_L = np.random.randn(8873)
y_L = np.random.randn(8873)
x_H = np.random.randn(1000)
y_H = np.random.randn(1000)
heatmap_L, xedges_L, yedges_L = np.histogram2d(x_L, y_L, bins=50)
extent_L = np.array([xedges_L[0], xedges_L[-1], yedges_L[0], yedges_L[-1]])
heatmap_H, xedges_H, yedges_H = np.histogram2d(x_H, y_H, bins=50)
extent_H = np.array([xedges_H[0], xedges_H[-1], yedges_H[0], yedges_H[-1]])
plt.clf()
im1 = plt.imshow(heatmap_L.T, extent=extent_L, origin='lower', cmap='Blues')
im2 = plt.imshow(heatmap_H.T, extent=extent_H, origin='lower', cmap='Greens')
plt.autoscale()
plt.show()
flatHMH = np.reshape(heatmap_H, 2500) # flatten the 2D arrays
flatHML = np.reshape(heatmap_L, 2500)
maxHMH = flatHMH.max() # Find the maximum in each
maxHML = flatHML.max()
# Now for each value in the flat array build an RGBA tuple using
# 1 for the colour we want - either green or blue, and then scaling
# the value by the maximum, finally reshaping back to a 50x50 array
augHMH = np.array([(0, 1, 0, x/maxHMH) for x in flatHMH]).reshape((50, 50, 4))
augHML = np.array([(0, 0, 1, x/maxHML) for x in flatHML]).reshape((50, 50, 4))
plt.clf()
# Plot without cmap as colours are now part of the data array passed.
im1 = plt.imshow(augHML, extent=extent_L, origin='lower')
im2 = plt.imshow(augHMH, extent=extent_H, origin='lower')
plt.autoscale()
plt.show()
If you look closely at the points in the last plot, for example the clustering of points at the edge, you'll notice they are not the same as in the plot above.
You are displaying both plots, the problem is that you are drawing one on top of the other. To see this in action you can shift one of the plots as in:
import numpy as np
import numpy.random
import matplotlib.pyplot as plt
# Generate some test data
x_L = np.random.randn(8873)
y_L = np.random.randn(8873)
x_H = np.random.randn(1000)
y_H = np.random.randn(1000)
heatmap_L, xedges_L, yedges_L = np.histogram2d(x_L, y_L, bins=50)
extent_L = np.array([xedges_L[0], xedges_L[-1], yedges_L[0], yedges_L[-1]])
heatmap_H, xedges_H, yedges_H = np.histogram2d(x_H, y_H, bins=50)
extent_H = np.array([xedges_H[0], xedges_H[-1], yedges_H[0], yedges_H[-1]])
plt.clf()
im1 = plt.imshow(heatmap_L.T, extent=extent_L, origin='lower', cmap='Blues')
im2 = plt.imshow(heatmap_H.T+2, extent=extent_H+2, origin='lower', cmap='Greens')
plt.autoscale()
plt.show()
You also need the plt.autoscale() call in there as otherwise the limits are not adjusted correctly.
One way to show the two plots on top of each other is to use the argument alpha=X to the imshow call (where 0 < X < 1) in order to set transparency on the plot call. Another, possibly clearer way is to transform each value from the histogram2D to an RGBA value. See the imshow docs for both alternatives to displaying the plots on top of each other.
One way of transforming the values would be to flatten the data, and augment it with the colours you want.
# imports and test data generation as before, removed for clarity...
flatHMH = np.reshape(heatmap_H, 2500) # flatten the 2D arrays
flatHML = np.reshape(heatmap_L, 2500)
maxHMH = flatHMH.max() # Find the maximum in each
maxHML = flatHML.max()
# Now for each value in the flat array build an RGBA tuple using
# 1 for the colour we want - either green or blue, and then scaling
# the value by the maximum, finally reshaping back to a 50x50 array
augHMH = np.array([(0, 1, 0, x/maxHMH) for x in flatHMH]).reshape((50, 50, 4))
augHML = np.array([(0, 0, 1, x/maxHML) for x in flatHML]).reshape((50, 50, 4))
plt.clf()
# Plot without cmap as colours are now part of the data array passed.
im1 = plt.imshow(augHML, extent=extent_L, origin='lower')
im2 = plt.imshow(augHMH, extent=extent_H, origin='lower')
plt.autoscale()
plt.show()
You can call
plt.autoscale()
such that the limits are adjusted to the content of the axes.
Example:
import numpy as np
import matplotlib.pyplot as plt
def get(offs=0):
# Generate some test data
x = np.random.randn(8873)+offs
y = np.random.randn(8873)+offs
heatmap, xedges, yedges = np.histogram2d(x, y, bins=50)
extent = [xedges[0], xedges[-1], yedges[0], yedges[-1]]
heatmap, xedges, yedges = np.histogram2d(x, y, bins=50)
extent = [xedges[0], xedges[-1], yedges[0], yedges[-1]]
return heatmap, extent
h1,e1 = get(-3)
h2,e2 = get(+3)
plt.imshow(h1, extent=e1, origin='lower', cmap="RdBu")
plt.imshow(h2, extent=e2, origin='lower', cmap="YlGnBu")
plt.autoscale()
plt.show()

Create with imshow the same plot as pcolormesh [duplicate]

I am creating a histogram for my data. Interestingly, when I plot my raw data and their histogram together on one plot, they are a "y-flipped" version of each other as follows:
I failed to find out the reason and fix it. My code snippet is as follows:
import math as mt
import numpy as np
import matplotlib.pylab as plt
x = np.random.randn(50)
y = np.random.randn(50)
w = np.random.randn(50)
leftBound, rightBound, topBound, bottomBound = min(x), max(x), max(y), min(y)
# parameters for histogram
x_edges = np.linspace(int(mt.floor(leftBound)), int(mt.ceil(rightBound)), int(mt.ceil(rightBound))-int(mt.floor(leftBound))+1)
y_edges = np.linspace(int(mt.floor(bottomBound)), int(mt.ceil(topBound)), int(mt.ceil(topBound))-int(mt.floor(bottomBound))+1)
# construct the histogram
wcounts = np.histogram2d(x, y, bins=(x_edges, y_edges), normed=False, weights=w)[0]
# wcounts is a 2D array, with each element representing the weighted count in a bins
# show histogram
extent = x_edges[0], x_edges[-1], y_edges[0], y_edges[-1]
fig = plt.figure()
axes = fig.add_axes([0.1, 0.1, 0.8, 0.8]) # left, bottom, width, height (range 0 to 1)
axes.set_xlabel('x (m)')
axes.set_ylabel('y (m)')
histogram = axes.imshow(np.transpose(wcounts), extent=extent, alpha=1, vmin=0.5, vmax=5, cmap=cm.binary) # alpha controls the transparency
fig.colorbar(histogram)
# show data
axes.plot(x, y, color = '#99ffff')
Since the data here are generated randomly for demonstration, I don't think it helps much, if the problem is with that particular data set. But anyway, if it is something wrong with the code, it still helps.
By default, axes.imshow(z) places array element z[0,0] in the top left corner of the axes (or the extent in this case). You probably want to either add the origin="bottom" argument to your imshow() call or pass a flipped data array, i.e., z[:,::-1].

Data and histogram do not collide in matplotlib?

I am creating a histogram for my data. Interestingly, when I plot my raw data and their histogram together on one plot, they are a "y-flipped" version of each other as follows:
I failed to find out the reason and fix it. My code snippet is as follows:
import math as mt
import numpy as np
import matplotlib.pylab as plt
x = np.random.randn(50)
y = np.random.randn(50)
w = np.random.randn(50)
leftBound, rightBound, topBound, bottomBound = min(x), max(x), max(y), min(y)
# parameters for histogram
x_edges = np.linspace(int(mt.floor(leftBound)), int(mt.ceil(rightBound)), int(mt.ceil(rightBound))-int(mt.floor(leftBound))+1)
y_edges = np.linspace(int(mt.floor(bottomBound)), int(mt.ceil(topBound)), int(mt.ceil(topBound))-int(mt.floor(bottomBound))+1)
# construct the histogram
wcounts = np.histogram2d(x, y, bins=(x_edges, y_edges), normed=False, weights=w)[0]
# wcounts is a 2D array, with each element representing the weighted count in a bins
# show histogram
extent = x_edges[0], x_edges[-1], y_edges[0], y_edges[-1]
fig = plt.figure()
axes = fig.add_axes([0.1, 0.1, 0.8, 0.8]) # left, bottom, width, height (range 0 to 1)
axes.set_xlabel('x (m)')
axes.set_ylabel('y (m)')
histogram = axes.imshow(np.transpose(wcounts), extent=extent, alpha=1, vmin=0.5, vmax=5, cmap=cm.binary) # alpha controls the transparency
fig.colorbar(histogram)
# show data
axes.plot(x, y, color = '#99ffff')
Since the data here are generated randomly for demonstration, I don't think it helps much, if the problem is with that particular data set. But anyway, if it is something wrong with the code, it still helps.
By default, axes.imshow(z) places array element z[0,0] in the top left corner of the axes (or the extent in this case). You probably want to either add the origin="bottom" argument to your imshow() call or pass a flipped data array, i.e., z[:,::-1].

Categories

Resources