matplotlib - clip image using line(s) - python

Is it possible to clip an image generated by imshow() to the area under a line/multiple lines? I think Clip an image using several patches in matplotlib may have the solution, but I'm not sure how to apply it here.
I just want the coloring (from imshow()) under the lines in this plot:
Here is my plotting code:
from __future__ import division
from matplotlib.pyplot import *
from numpy import *
# wavelength array
lambd = logspace(-3.8, -7.2, 1000)
# temperatures
T_earth = 300
T_sun = 6000
# planck's law constants
h = 6.626069e-34
c = 2.997925e8
k = 1.380648e-23
# compute power using planck's law
power_earth = 2*h*c**2/lambd**5 * 1/(exp(h*c/(lambd*k*T_earth)) - 1)
power_sun = 2*h*c**2/lambd**5 * 1/(exp(h*c/(lambd*k*T_sun)) - 1)
# set up color array based on "spectrum" colormap
colors = zeros((1000,1000))
colors[:,:1000-764] = 0.03
for x,i in enumerate(range(701,765)):
colors[:,1000-i] = 1-x/(765-701)
colors[:,1000-701:] = 0.98
figure(1,(4,3),dpi=100)
# plot normalized planck's law graphs
semilogx(lambd, power_earth/max(power_earth), 'b-', lw=4, zorder=5); hold(True)
semilogx(lambd, power_sun/max(power_sun), 'r-', lw=4, zorder=5); hold(True)
# remove ticks (for now)
yticks([]); xticks([])
# set axis to contain lines nicely
axis([min(lambd), max(lambd), 0, 1.1])
# plot colors, shift extent to match graph
imshow(colors, cmap="spectral", extent=[min(lambd), max(lambd), 0, 1.1])
# reverse x-axis (longer wavelengths to the left)
ax = gca(); ax.set_xlim(ax.get_xlim()[::-1])
tight_layout()
show()

What you can do in this case is using the area under the curve as a Patch to apply set_clip_path. All you have to do is call fill_between and extract the corresponding path, like this:
semilogx(lambd, power_earth/max(power_earth), 'b-', lw=4, zorder=5)
# Area under the curve
fillb_earth = fill_between(lambd, power_earth/max(power_earth), color='none', lw=0)
# Get the path
path_earth, = fillb_earth.get_paths()
# Create a Patch
mask_earth = PathPatch(path_earth, fc='none')
# Add it to the current axes
gca().add_patch(mask_earth)
# Add the image
im_earth = imshow(colors, cmap="spectral", extent=[min(lambd), max(lambd), 0, 1.1])
# Clip the image with the Patch
im_earth.set_clip_path(mask_earth)
And then repeat the same lines for the Sun. Here is the result.

Related

How to transform data from one axis coordinates to another axis with matplotlib

I am trying to plot a line that overlays several axes using matplotlib and transforms, but I cannot seem to get the transforms right. Here is the code:
# Define figure
fig = plt.figure(figsize=(7,3))
# Define colors
Lcolor="grey"
Pcolor=[0.8, 0.47058823529411764, 0.7372549019607844, 1.0]
MAPcolor = [0.00784313725490196, 0.6196078431372549, 0.45098039215686275, 1.0]
# Add gridspec for legend
gs0 = fig.add_gridspec(nrows=1, ncols=1, left=0, right=0.15, bottom=0.01,top=.85)
ax0 = fig.add_subplot(gs0[0])
# Add grid spec for data
gs1 = fig.add_gridspec(nrows=1, ncols=1, left=0.25, right=.8,bottom=0.01,top=gs0.top)
ax1 = fig.add_subplot(gs1[0])
ax1.set_ylim(-.5,1.5)
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)
ax1.set_xlabel("Position",fontweight="bold")
ax1.set_ylabel("Position",fontweight="bold")
# Add gridspec for legend plot
gs2 = fig.add_gridspec(nrows=1, ncols=1, left=gs1.left, right=gs1.right, bottom=gs1.top+0.05,top=1)#,hspace=.25)
ax2 = fig.add_subplot(gs2[0])
# Define prior
xx = np.linspace(-.5, 1.5, 100)
prior = norm.pdf(xx, 0.5, .35)
ax0.plot(prior,xx,color="dodgerblue")
ax0.set_ylim(-.75,1.75)
ax0.xaxis.set_visible(False)
ax0.spines['bottom'].set_visible(False)
ax0.spines['top'].set_visible(False)
ax0.spines['right'].set_visible(False)
ax0.set_title("Prior belief",fontstyle="italic",color="dodgerblue",fontsize=10)
#### AXIS 2
plt.sca(ax1)
ax1.set_xlim(0,1.2)
nn=6
gs1_1 = fig.add_gridspec(nrows=1, ncols=nn, left=gs1.left, right=gs1.right, bottom=gs1.bottom,top=gs1.top)#,hspace=.25)
ax1_1 = []
xvals=np.linspace(0,1,nn)
for cnt,xval in enumerate(xvals):
# Store axis
ax1_1.append(fig.add_subplot(gs1_1[cnt],facecolor=None))
# Generate likelihood and posterior
_L = norm.pdf(xx, xval, 0.1)
_P = np.multiply(_L,prior)
# Plot distributions
ax1_1[cnt].plot(prior,xx,color="dodgerblue",zorder=3)
ax1_1[cnt].plot(_L,xx,color=Lcolor,zorder=3)
ax1_1[cnt].plot(_P,xx,color=Pcolor,zorder=4)
ax1_1[cnt].axvline(x=0,color="darkgrey",linestyle="--")
ax1_1[cnt].set_ylim(-.5,1.5)
ax1_1[cnt].set_axis_off()
# Plot MAP and adjust axis
ax1_1[cnt].scatter(_P.max(),xx[_P.argmax()],color=MAPcolor,s=30,clip_on=False)
xlims = ax1_1[cnt].get_xlim()
ax1_1[cnt].set_xlim(0,np.maximum(_P.max(),_L.max())+.25)
# Add axis on right side
ax1_r=ax1.twinx()
ax1_r.set_ylim(-.5,1.5)
ax1_r.set_ylabel("Error Predicted",color=MAPcolor,fontweight="bold",rotation=270)
ax1_r.tick_params(axis='y', colors=MAPcolor)
# Generate custom legend
colors=["dodgerblue",Lcolor,Pcolor,MAPcolor]
labels=["Prior","Likelihood","Posterior","Prediction (MAP)"]
for clr,lab in zip(colors,labels):
ax2.plot([], [], color=clr,alpha=1, label=lab)
leg = ax2.legend(loc="upper center",frameon=False,ncol=4,fontsize=10)
ax2.set_frame_on(False)
ax2.yaxis.set_visible(False)
ax2.xaxis.set_visible(False)
for (text,clr) in zip(leg.get_texts(),colors):
text.set_color(clr)
plt.show()
Here is the image: https://i.stack.imgur.com/dWGEP.png (I am unable to post inline)
Here is what I have tried so far:
Get the coordinates of the peak of the distribution and convert to display coordinates.
Define an inverse transform for the right axis
Apply transform to data and plot
So in the for loop:
for cnt,xval in enumerate(xvals):
...
temp=ax1_1[cnt].transData.transform((_P.max(),xx[_P.argmax()]))
invax = ax1_r.transData.inverted()
out = invax.transform(temp)
ax1_r.scatter(out[0],out[1],s=100)
...
In theory, I can then append the values to a list and then just fit a line to the points. I can't quite figure out the proper order of transformations to get from the subplots in ax1_1 to the bigger axis ax1_r
Thanks for the help.

Python matplotlib, adding single custom tickmark on axis

I am trying to label the intersection of two lines in a plot I have made. The code/MWE is:
import matplotlib.pyplot as plt
import numpy as np
#ignore my gross code, first time ever using Python :-)
#parameters
d = 0.02
s = 0.50 #absurd, but dynamics robust to 1>s>0
A = 0.90
u = 0.90
#variables
kt = np.arange(0, 50, 1)
invest = (1 - np.exp(-d*kt))*kt
output = A*u*kt
saving = s*output
#plot
plt.plot(kt, invest, 'r', label='Investment')
plt.plot(kt, output, 'b', label='Output')
plt.plot(kt, saving, label='Saving')
plt.xlabel('$K_t$')
plt.ylabel('$Y_t$, $S_t$, $I_t$')
plt.legend(loc="upper left")
#Steady State; changes with parameters
Kbar = np.log(1-s*A*u)/-d
x, y = [Kbar, Kbar], [0, s*A*u*Kbar]
plt.plot(x, y, 'k--')
#custom axes (no top and right)
ax = plt.gca()
right_side = ax.spines["right"]
right_side.set_visible(False)
top_side = ax.spines["top"]
top_side.set_visible(False)
#ax.grid(True) #uncomment for gridlines
plt.xlim(xmin=0) #no margins; preference
plt.ylim(ymin=0)
plt.show()
which creates:
I am trying to create a little label at the bottom of the dotted black line that says "$K^*$". I want it to coincide with Kbar so that, like the black line, it moves along with the parameters. Any tips or suggestions here?
I don't quite understand what you mean by "under the black dotted line", but you can already use the coordinate data of the dotted line to annotate it. I put it above the intersection point, but if you want to put it near the x-axis, you can set y=0.
plt.text(max(x), max(y)+1.5, '$K^*$', transform=ax.transData)
baseTicks=list(plt.xticks()[0]) #for better control, replace with a range or arange
ax.set_xticks(baseTicks+[np.log(1-A*u*s)/(-d)])
ax.set_xticklabels(baseTicks+['$K^*$'])

Defining a 2D object and using its area as Boolean

I have defined two space dimesions ( x and z ) and I was able to manually "draw" an object to use it as a boolen for solving an equation. I defined it as it follows:
A = np.zeros((nz,nx))
object = np.ones_like(A)
object[ int(5/dz):int(10/dz) , int(5/dx):int(10/dz) ] = 2
object = object == 2
By doing that I can define an square 5x10 in z dimesion and 5x10 in x dimesion , and apply the algorythim which understands this as an area , I think. But when it comes to draw complex areas it ends up being hard doing it by little squares and rectangles.
So I want to automatize an area generation by mouse clicking and I want to be able to use this area as a boolean.
I was able to draw a polygon using:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Polygon
fig, ax = plt.subplots()
object = np.array(plt.ginput(n=-100,mouse_stop=2))
p = Polygon(object, alpha=0.5)
plt.gca().add_artist(p)
plt.draw()
plt.show()
But this outputs z and x coordinates of the vertices, and I tried to use it as boleean but I could'nt write it so that python uderstands it as the area defined by those points.
Is this problem easy to solve?
If you just want to calculate the area of a general polygon, you can use for example the Shapely python package like this:
import numpy as np
import matplotlib.pyplot as plt
from shapely.ops import Polygon
from matplotlib.patches import Polygon as PltPolygon
# Get the coordinate input
canvas_size = np.array([1, 1])
canvas_lim = np.array([[0, canvas_size[0]], [0, canvas_size[1]]])
fig, ax = plt.subplots()
plt.xlim(canvas_lim[0])
plt.ylim(canvas_lim[1])
ax.set_aspect("equal")
coordinates = np.array(plt.ginput(n=-100, mouse_stop=2))
# Use shapely.ops.Polygon to calculate the area
poly = Polygon(coordinates)
area = poly.area
print("The area is {} units^2".format(area))
# Draw the polygon
p = PltPolygon(coordinates, alpha=0.5)
ax.add_artist(p)
plt.show()
If you definitely need the mask, here's one way to rasterize it using numpy and matplotlib.path. For details see the comments in the code:
import numpy as np
import matplotlib.path as mpltPath
import matplotlib.pyplot as plt
# Define the limits of our polygon
canvas_desired_size = np.array([110, 100])
# The pixel size with which we calculate (number of points to consider)
# The higher this number, the more we have to calculate, but the
# closer the approximation will be
pixel_size = 0.1
# Cacluate the actual size of the canvas
num_pxiels = np.ceil(canvas_desired_size / pixel_size).astype(int)
canvas_actual_size = num_pxiels * pixel_size
# Let's create a grid where each pixel's value is it's position in our 2d image
x_coords = np.linspace(
start=0,
stop=canvas_actual_size[0],
endpoint=False,
num=canvas_desired_size[0] / pixel_size,
)
y_coords = np.linspace(
start=0,
stop=canvas_actual_size[1],
endpoint=False,
num=canvas_desired_size[1] / pixel_size,
)
# Since it makes more sense to check if the middle of the pixel is in the
# polygion, we shift everything with half pixel size
pixel_offset = pixel_size / 2
x_centers = x_coords + pixel_offset
y_centers = y_coords + pixel_offset
xx, yy = np.meshgrid(x_centers, y_centers, indexing="ij")
# Flatten our xx and yy matrixes to an N * 2 array, which contains
# every point in our grid
pixel_centers = np.array(
list(zip(xx.flatten(), yy.flatten())), dtype=np.dtype("float64")
)
# Now prompt for the imput shape
canvas_lim = np.array([[0, canvas_actual_size[0]], [0, canvas_actual_size[1]]])
fig, ax = plt.subplots()
plt.xlim(canvas_lim[0])
plt.ylim(canvas_lim[1])
ax.set_aspect("equal")
shape_points = np.array(plt.ginput(n=-100, mouse_stop=2))
# Create a Path object
shape = mpltPath.Path(shape_points)
# Use Path.contains_points to calculate if each point is
# within our shape
shape_contains = shape.contains_points(pixel_centers)
# Reshape the result to be a matrix again
mask = np.reshape(shape_contains, num_pxiels)
# Calculate area
print(
"The shape area is roughly {} units^2".format(
np.sum(shape_contains) * pixel_size ** 2
)
)
# Show the rasterized shape to confirm it looks correct
plt.imshow(np.transpose(mask), aspect="equal", origin="lower")
plt.xlim([0, num_pxiels[0]])
plt.ylim([0, num_pxiels[1]])
plt.show()
Alternatively, a simpler solution would be using your plot as an image and thresholding it to get a boolean mask. There should be plent of examples of how to do this on google.

Displaying Wavenumber and Wavelength on One Plot

I currently work with an instrument that provides data in Wavenumber, but most of my community works in wavelength. Because of this I would like to create plots that display Wavenumber in cm^-1 along the bottom x-axis and wavelength in µm along the top. However the spacing doesn't quite match up between the two units of measurement to display a single spectrum. How do I create a different spacing for wavelength?
Here is an example of how a portion of one spectrum looks when plotted as a function of wavenumber against when it's plotted as a function of wavelength. Below is the code I'm currently implementing.
wn = wn_tot[425:3175] #range of 250 to 3000 cm-1
wl = 10000/wn #wavelength in microns
fig = plt.figure(1)
ax1 = plt.subplot(1,1,1)
ax2 = ax1.twiny()
ax1.plot(wn, spc[45], 'c', label='Wavenumber')
ax2.plot(wl, spc[45], 'm', label='Wavelength')
ax1.set_xlabel('Wavenumber (cm$^{-1}$)')
ax2.set_xlabel('Wavelength ($\mu$m)')
ax1.set_ylabel('Relative Intensity')
ax2.invert_xaxis()
fig.legend(loc=2, bbox_to_anchor=(0,1), bbox_transform=ax1.transAxes)
As said in the comment on the OP, both scales cannot be simultaneously linear, since one cannot be obtained from the other via a linear transformation. You must hence accept that one (or both) have ticks at non-regular intervals.
The correct way to do it
Apply a transformation to the scale, which causes matplotlib to have a non-homogeneous scale.
The doc for Axes.set_yscale leads to that example which demonstrate the syntax ax1.set_xscale('function', functions=(forward, inverse)). Here in that case, the transformation functions are simply
def forward(wn):
# cm^{-1} to μm
return 1.0e4 / wn
def reverse(lam):
# μm to cm^{-1}
return 1.0e4 / lam
However, my matplotlib is stuck on version 2.2.2 which does not have that feature, so I cannot give a working example.
The hacky way that works with older versions
Give tick positions and labels by hand, performing the calculations yourself.
# -*- coding: utf-8 -*-
import numpy as np
import matplotlib.pyplot as plt
def lambda_to_wave(lam):
# μm to cm^{-1}
return 1.0e4 / lam
x_wave = np.linspace(2000.0, 3000.0)
y_arb = np.linspace(0.0, 1.0e6)
ticks_wavelength_values = np.linspace(3.5, 5.5, num=5)
ticks_labels = [str(lam) for lam in ticks_wavelength_values]
ticks_wavenumber_positions = lambda_to_wave(ticks_wavelength_values)
print ticks_wavelength_values
print ticks_wavenumber_positions
fig = plt.figure(1)
ax1 = plt.subplot(1,1,1) # wavenumber
ax2 = ax1.twiny() # wavelength
ax2.get_shared_x_axes().join(ax1, ax2) # https://stackoverflow.com/questions/42973223/how-share-x-axis-of-two-subplots-after-they-are-created
ax1.plot(x_wave, y_arb, 'c', label='Data')
ax1.set_xlabel('Wavenumber (cm$^{-1}$)')
ax1.set_ylabel('Relative Intensity')
ax2.set_xticks(ticks_wavenumber_positions)
ax2.set_xticklabels(ticks_labels)
ax2.set_xlabel('Wavelength ($\mu$m)')
ax1.set_xlim(left=1800.0, right=3000.0)
fig.legend(loc=2, bbox_to_anchor=(0,1), bbox_transform=ax1.transAxes)
plt.show()
You can do without the second call to plot if you prefer: https://matplotlib.org/gallery/subplots_axes_and_figures/secondary_axis.html#sphx-glr-gallery-subplots-axes-and-figures-secondary-axis-py
wn = wn_tot[425:3175] #range of 250 to 3000 cm-1
fig = plt.figure(1)
ax1 = plt.subplot(1,1,1)
ax1.plot(wn, spc[45], 'c', label='Wavenumber')
def forward(x):
return 10000 / x
def inverse(x):
return 10000 / x
secax = ax.secondary_xaxis('top', functions=(forward, inverse))
ax1.set_xlabel('Wavenumber (cm$^{-1}$)')
secax.set_xlabel('Wavelength ($\mu$m)')
ax1.set_ylabel('Relative Intensity')

Python irregular x,y data to contour plot on original domain

I have file containing points under the columns "x-cord", "y-cord", "value". These are irregularly spaced. I am trying to make a contour plot of "value" and overlay this over the original domain. I gave up trying to do this in both pgfplots and matlab and thought I would give python a go. An answer in any of these scripts would be fine. The python script is as follows
import numpy as np
from scipy.interpolate import griddata
import matplotlib.pyplot as plt
import numpy.ma as ma
from numpy.random import uniform, seed
from scipy.spatial import ConvexHull
#
# Loading data
filename = "strain.dat"
coordinates = []
x_c = []
y_c = []
z_c = []
xyz = open(filename)
title = xyz.readline()
for line in xyz:
x,y,z = line.split()
coordinates.append([float(x), float(y), float(z)])
x_c.append([float(x)])
y_c.append([float(y)])
z_c.append([float(z)])
xyz.close()
#
# Rehaping and translating data
x_c=np.ravel(np.array(x_c))
y_c=np.ravel(np.array(y_c))
z_c=np.ravel(np.array(z_c))
x_c = x_c-100.0
y_c = y_c-100.0
#
# Checking the convex hull
points=np.column_stack((x_c,y_c))
hull = ConvexHull(points);
plt.plot(points[hull.vertices,0], points[hull.vertices,1], 'r--', lw=2)
plt.scatter(x_c, y_c, marker='o', s=5, zorder=10)
#
# Mapping the irregular data onto a regular grid and plotting
xic = np.linspace(min(x_c), max(x_c), 1000)
yic = np.linspace(min(y_c), max(y_c), 1000)
zic = griddata((x_c, y_c), z_c, (xic[None,:], yic[:,None]))
CS = plt.contour(xic,yic,zic,15,linewidths=0.5,colors='k')
CS = plt.contourf(xic,yic,zic,15,cmap=plt.cm.summer)
plt.colorbar() # draw colorbar
#
#plt.scatter(x_c, y_c, marker='o', s=5, zorder=10)
plt.axis('equal')
plt.savefig('foo.pdf', bbox_inches='tight')
plt.show()
and the output looks like
The problem is that griddata uses a convex hull and this convex hull exceeds the edges of the irregular data. Is there any way to set the values of the griddata points which are outside the edges of the boundary of the original points to zero?
Edit
In the end I threw in the towel and reverted back to Matlab. I'll have to export the data to pgfplots to get a nice plot. The code I came up with was
x = strain.x;
y = strain.y;
z = strain.eps;
% Get the alpha shape (couldn't do this in python easily)
shp = alphaShape(x,y,.001);
% Get the boundary nodes
[bi, xy] = boundaryFacets(shp);
no_grid = 500;
xb=xy(:,1);
yb=xy(:,2);
[X,Y] = ndgrid(linspace(min(x),max(x),no_grid),linspace(min(y),max(y),no_grid));
Z = griddata(x,y,z,X,Y,'v4');
% Got through the regular grid and set the values which are outside the boundary of the original domain to Nans
for j = 1:no_grid
[in,on] = inpolygon(X(:,j),Y(:,j),xb,yb);
Z(~in,j) = NaN;
end
contourf(X,Y,Z,10),axis equal
colorbar
hold on
plot(xb,yb)
axis equal
hold off
Here is the resulting image.
If someone can do something similar in Python I'll happily accept the answer.
I had to plot interpolated data on a complex geometry (see the blue points on figure) P(x,z) (z is the horizontal coordinate). I used mask operations and it worked well. Without mask, the whole square (x=0..1 ; z=0..17.28) is covered by contourf.
## limiting values for geometry
xmax1=0.408
zmin1=6.
xmax2=0.064
zmin2=13.12
xmin=0.
xmax=1.
zmin=0.
zmax=17.28
# Grid for points
x1 = np.arange(xmin,xmax+dx,dx)
z1 = np.arange(zmin,zmax+dz,dz)
zi2,xi2 = np.meshgrid(z1,x1)
mask = (((zi2 > zmin2) & (xi2 > xmax2)) | ((zi2 > zmin1) & (zi2 <= zmin2) & (xi2 > xmax1)))
zim=np.ma.masked_array(zi2,mask)
xim=np.ma.masked_array(xi2,mask)
# Grid for P values
# npz=z coordinates of data, npx is the x coordinates and npp is P values
grid_p = scipy.interpolate.griddata((npz, npx), npp, (zim,xim),method='nearest')
pm=np.ma.masked_array(grid_p,mask)
# plot
plt.contour(zim, xim, pm, 25, linewidths=0.5, colors='k',corner_mask=False)
plt.contourf(zim, xim, pm, 25,vmax=grid_p.max(), vmin=grid_p.min(),corner_mask=False)
plt.colorbar()
# Scatter plot to check
plt.scatter(npz,npr, marker='x', s=2)
plt.show()
enter image description here

Categories

Resources