I want to embed subplots canvas inside a cartopy projected map. I wrote this code to show the expected result by using rectangles:
#%%
import numpy as np
import cartopy as cr
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from cartopy.io import shapereader
import geopandas
resolution = '10m'
category = 'cultural'
name = 'admin_0_countries'
shpfilename = shapereader.natural_earth(resolution, category, name)
# read the shapefile using geopandas
df = geopandas.read_file(shpfilename)
# read the country borders
usa = df.loc[df['ADMIN'] == 'United States of America']['geometry'].values[0]
can = df.loc[df['ADMIN'] == 'Canada']['geometry'].values[0]
central_lon, central_lat = -80, 60
extent = [-85, -55, 40, 62]
# ax = plt.axes(projection=ccrs.Orthographic(central_lon, central_lat))
#Golden ratio
phi = 1.618033987
h = 7
w = phi*h
fig = plt.figure(figsize=(w,h))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
#Set map extent
ax.set_extent(extent)
ax.set_xticks(np.linspace(extent[0],extent[1],11))
ax.set_yticks(np.linspace(extent[2],extent[3],6))
ax.add_geometries(usa, crs=ccrs.PlateCarree(), facecolor='none',
edgecolor='k')
# ax.gridlines()
ax.coastlines(resolution='50m')
nx, ny = 7,6
#Begin firts rectangle
xi = extent[0] + 0.5
yi = extent[2] + 0.5
x, y = xi, yi
#Loop for create the plots grid
for i in range(nx):
for j in range(ny):
#Inner rect height
in_h = 2.8
#Draw the rect
rect = ax.add_patch(mpatches.Rectangle(xy=[x, y], width=phi*in_h, height=in_h,
facecolor='blue',
alpha=0.2,
transform=ccrs.PlateCarree()))
#Get vertex of the drawn rectangle
verts = rect.get_path().vertices
trans = rect.get_patch_transform()
points = trans.transform(verts)
#Refresh rectangle coordinates
x += (points[1,0]-points[0,0]) + 0.2
if j == ny-1:
x = xi
y += (points[2,1]-points[1,1]) + 0.2
# print(points)
fig.tight_layout()
fig.savefig('Figure.pdf',format='pdf',dpi=90)
plt.show()
This routine prints this figure
What I am looking for is a way to embed plots that match every single rectangle in the figure. I tried with fig.add_axes, but I couldn't get that mini-canvas match with the actual rectangles.
Since you want to embed the axes inside the parent axes is recommend using inset_axes, see the documentation here.
I wrote simple code to demonstrate how it works. Clearly there will be some tweaking of the inset_axes positions and sizes necessary for your desired output, but I think my trivial implementation already does decent.
All created axes instances are stored in a list so that they can be accessed later.
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
axis = []
x = np.linspace(-85, -55)
y = np.linspace(40, 62)
ax.plot(x, y)
offset_l = 0.05
offset_h = 0.12
num_x = 6
num_y = 7
xs = np.linspace(offset_l, 1-offset_h, num_x)
ys = np.linspace(offset_l, 1-offset_h, num_y)
for k in range(num_x):
for j in range(num_y):
ax_ins = ax.inset_axes([xs[k], ys[j], 0.1, 0.1])
ax_ins.axhspan(0, 1, color='tab:blue', alpha=0.2)
axis.append(ax_ins)
Alternatively, you can also specify the inset_axes positions using data coordinates, for this you have to set the kwarg transform in the method to transform=ax.transData, see also my code below.
import matplotlib.pyplot as plt
import numpy as np
#Golden ratio
phi = 1.618033987
h = 7
w = phi*h
fig, ax = plt.subplots(figsize=(w, h))
axis = []
x = np.linspace(-85, -55)
y = np.linspace(40, 62)
ax.plot(x, y)
offset_l = 0.05
offset_h = 0.12
num_x = 6
num_y = 7
fig.tight_layout()
extent = [-85, -55, 40, 62]
xi = extent[0] + 0.5
yi = extent[2] + 0.5
in_h = 2.8
in_w = phi * 2.8
spacing = 0.4
for k in range(num_x):
for j in range(num_y):
ax_ins = ax.inset_axes([xi+k*(in_w + phi*spacing), yi+j*(in_h + spacing),
in_w, in_h], transform=ax.transData)
ax_ins.axhspan(0, 1, color='tab:blue', alpha=0.2)
axis.append(ax_ins)
Related
I'm trying to place a plane on the surface of a sphere, although I think the math is correct, the resulting figure displays the plane at some point else.
Here is the code to compute and visualize it;
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
plt.style.use('dark_background')
# point and the unit vector
point = np.array([ 349370.39178182, 5570903.05977037, 3085958.36621096])
unit_vector = point/np.linalg.norm(point)
# the math
print(f'plane equation:\nAx + By + Cz + D = 0')
D = np.sum(unit_vector * point)
print(f'D=-(point * unit_vector) = {D:.2f}')
print(f'plane equation:\n{unit_vector[0]:1.4f}x + {unit_vector[1]:1.4f}y + {unit_vector[2]:1.4f}z + {D:.1f} = 0')
print(f'{-1*unit_vector[2]:1.4f}z = {unit_vector[0]:1.4f}x + {unit_vector[1]:1.4f}y + {D:.1f}')
print(f'z = ({unit_vector[0]:1.4f}x + {unit_vector[1]:1.4f}y + {D:.1f}) / {-1*unit_vector[2]:1.4f}')
x = np.linspace(-3e6,+3e6,100)
y = np.linspace(-3e6,+3e6,100)
X,Y = np.meshgrid(x,y)
Z = (0.05477656*X +0.87344241*Y + 6378100.0)/-0.48383662
# plotting stuff
def set_axis_equal_scale(ax, ticks_off=True):
xl = ax.set_xlim()
yl = ax.set_ylim()
zl = ax.set_zlim()
maxx=max(max(xl), max(yl), max(zl))
minn=min(min(xl), min(yl), min(zl))
ax.set_xlim(minn, maxx)
ax.set_ylim(minn, maxx)
ax.set_zlim(minn, maxx)
if ticks_off:
ax.set_xticks([])
ax.set_yticks([])
ax.set_zticks([])
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot(111, projection='3d')
# plot wireframe
radius = 6.3781e6 # in units m
uu, vv = np.mgrid[0:2*np.pi:200j, 0:np.pi:100j]
xE = radius * np.cos(uu)*np.sin(vv)
yE = radius * np.sin(uu)*np.sin(vv)
zE = radius * np.cos(vv)
ax.plot_wireframe(xE,yE,zE, color='w', alpha=0.1)
ax.scatter(point[0], point[1], point[2], s=500, color='r')
ax.plot([0,point[0]], [0,point[1]], [0,point[2]], color='w', lw=2)
surf = ax.plot_surface(X, Y, Z)
ax.scatter(0,0,0, marker='o', s=900, color='b')
ax.view_init(25, -190)
ax.axis('off')
set_axis_equal_scale(ax)
I expect the plane to be on where the red marker is and perpendicular to the white line connecting the center and the red marker.
[here is the image][1]
[1]: https://i.stack.imgur.com/LHPzW.png
I have a script for plotting a cylinder using the function data_for_cylinder_along_z. How to remove the grid on the cylinder? Is it possible to set two colours for it?
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.patches import FancyArrowPatch
# Coordinate for the cylinder
def data_for_cylinder_along_z(center_x,center_y,radius,height_z):
z = np.linspace(0, height_z, 200)
theta = np.linspace(0, 2*np.pi, 200)
theta_grid, z_grid=np.meshgrid(theta, z)
x_grid = radius*np.cos(theta_grid) + center_x
y_grid = radius*np.sin(theta_grid) + center_y
return x_grid,y_grid,z_grid
figsize=[5,5]
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot(111, projection='3d')
ax.azim = -65
ax.elev = 11
ax.set_xlim(0.03, 0.049)
ax.set_ylim(0.02, 0.04)
ax.set_zlim(0.009, 0.02)
y_shift = 0.1
# Cylinder
Xc,Zc,Yc = data_for_cylinder_along_z(0,0,0.05,0.14) # center_x,center_y,radius,height_z
ax.plot_surface(Xc, Yc+y_shift, Zc, alpha=0.4, color = 'blue')
# Setting
ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([0.2, 1, 0.13, 1]))
# Hide axes
ax._axis3don = False
plt.tight_layout(pad=0)
plt.show()
I would like to 4D plot over the cube (x,y,z) vs. q, using the colormap on the 3 surfaces of the cubes, where the color and contour are determined depending on the q variable. Basically, I am looking for a similar image like this:
Any help is appreciated.
See my example of 3D ABC feild
import pyvista as pv
import numpy as np
from numpy import mgrid
import matplotlib.pyplot as plt
print('initializing domain')
xmin = -800.
xmax = 800.
Lx = xmax-xmin
B0 = 1
k = 1
alpha = 2.0*np.pi*k/Lx
x, y, z = Lx*mgrid[0:1:51j, 0:1:51j, 0:1:51j]
print('initializing 3D B field')
Bx = B0*(np.sin(alpha*z) + np.cos(alpha*y))
By = B0*(np.sin(alpha*x) + np.cos(alpha*z))
Bz = B0*(np.sin(alpha*y) + np.cos(alpha*x))
B = np.column_stack((Bx.ravel(), By.ravel(), Bz.ravel()))
grid = pv.StructuredGrid(x, y, z)
grid["ABC field magnitude"] = np.linalg.norm(B, axis=1)
grid["ABC field vectors"] = B
grid.set_active_vectors("ABC field vectors")
#contours = grid.contour(8, scalars="ABC field magnitude")
#arrows = contours.glyph(orient="ABC field vectors", factor=50.0)
print('plotting')
pv.set_plot_theme('document')
p = pv.Plotter(notebook=0, shape=(1,1))
#p.background_color='white'
#p.window_size
cmap = plt.cm.get_cmap("viridis", 4)
p.add_mesh(grid, cmap=cmap)
p.show_grid()
#p.add_mesh(arrows)
#p.subplot(0,1)
#slices = grid.slice_orthogonal(x=20, y=20, z=30)
#slices = grid.slice_orthogonal()
#p.add_mesh(slices, cmap=cmap)
##p.subplot(1,0)
#p.add_mesh(contours, opacity=1)
#p.subplot(1,1)
#p.add_mesh(arrows)
#single_slice = arrows.slice(normal=[1, 1, 0])
#slices = arrows.slice_orthogonal(x=20, y=20, z=30)
#slices = grid.slice_orthogonal()
#p.add_mesh(single_slice, cmap=cmap)
p.show_grid()
p.link_views()
p.view_isometric()
p.show(screenshot='abc3d_slicing.png')
A simple answer is
import numpy as np
import matplotlib.pyplot as plt
length = 10
data = length*np.mgrid[0:1:51j, 0:1:51j, 0:1:51j].reshape(3,-1).T
contour = np.random.rand(data.shape[0])
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
data_plot = ax.scatter(data[:,0], data[:,1], data[:,2], c=contour)
fig.colorbar(data_plot)
To optimize to only boundary points
length = 10
vol_data = length*np.mgrid[0:1:51j, 0:1:51j, 0:1:51j].reshape(3,-1).T
bound_data = np.array([data_i for data_i in vol_data
if any([coord in [0, length] for coord in data_i])])
contour = np.random.rand(bound_data.shape[0])
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
data_plot = ax.scatter(bound_data[:,0], bound_data[:,1], bound_data[:,2], c=contour)
fig.colorbar(data_plot)
Is there a way to align python basemaps like this figure below?
Here's some sample basemap code to produce a map:
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8, 4.5))
plt.subplots_adjust(left=0.02, right=0.98, top=0.98, bottom=0.00)
m = Basemap(projection='robin',lon_0=0,resolution='c')
m.fillcontinents(color='gray',lake_color='white')
m.drawcoastlines()
plt.savefig('world.png',dpi=75)
I am not an expert with Matplotlib, but I found a way to get a similar result by using the data files included in the source folder of basemap. They can be combined into a meshgrid to plot some data, in the example below we plot the altitude at every point.
One of the tricks I used is to set matplotlib to an orthogonal projection so that there is no distortion in the vertical spacing of the maps.
I have put the parameters at the beginning of the code as you may find it useful to adjust.
One thing I couldn't get my head around is the shadow under the maps.
from mpl_toolkits.mplot3d import proj3d
from mpl_toolkits.basemap import Basemap
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
import numpy as np
import matplotlib.pyplot as plt
# Parameters
n_maps = 5 # Number of maps
z_spacing = 4. # Spacing of maps along z
z_reduction = 1E-8 # Reduction factor for Z data, makes the map look flat
view_angles = (14., -100.) # Set view port angles
colbar_bottom = 0.2 # Space at the bottom of colorbar column
colbar_spacing = .132 # Space between colorbars
colbar_height = 0.1 # Height of colorbars
# Set orthogonal projection
def orthogonal_proj(zfront, zback):
a = (zfront+zback)/(zfront-zback)
b = -2*(zfront*zback)/(zfront-zback)
return np.array([[1,0,0,0],
[0,1,0,0],
[0,0,a,b],
[0,0,-0.0001,zback]])
proj3d.persp_transformation = orthogonal_proj
fig = plt.figure(figsize=[30, 10*n_maps])
ax = fig.gca(projection='3d')
etopo = np.loadtxt('etopo20data.gz')
lons = np.loadtxt('etopo20lons.gz')
lats = np.loadtxt('etopo20lats.gz')
# Create Basemap instance for Robinson projection.
m = Basemap(projection='robin', lon_0=0.5*(lons[0]+lons[-1]))
# Compute map projection coordinates for lat/lon grid.
X, Y = m(*np.meshgrid(lons,lats))
# Exclude the oceans
Z = etopo.clip(-1)
# Set the colormap
cmap = plt.cm.get_cmap("terrain")
cmap.set_under("grey")
for i in range(n_maps):
c = ax.contourf(X, Y, z_spacing*i + z_reduction*Z, 30, cmap=cmap, vmin=z_spacing*i, extend='neither')
cax = inset_axes(ax,
width="5%",
height="100%",
loc=3,
bbox_to_anchor=(.85, colbar_spacing*i+colbar_bottom, .2, colbar_height),
bbox_transform=ax.transAxes,
borderpad=0
)
cb = fig.colorbar(c, cax=cax)
cb.set_label("Altitude")
# Reset the ticks of the color bar to match initial data
cb.set_ticks([z_spacing * i + j/10. * z_reduction * Z.max() for j in range(11)])
cb.set_ticklabels([str(int(j/10. * Z.max())) for j in range(11)])
ax.set_axis_off()
ax.view_init(*view_angles)
ax.set_xlim3d(X.min(), X.max())
ax.set_ylim3d(Y.min(), Y.max())
ax.set_zlim3d(-1E-2, (n_maps-1)*z_spacing)
plt.savefig('world.png',dpi=75)
Edit:
If you want shadows and don't mind the extra compute time you can change the beginning of the for loop with something along the lines of:
shadow_Z = np.empty(Z.shape)
for i in range(n_maps):
c = ax.contourf(X, Y, z_spacing*i + z_reduction*Z, 30, cmap=cmaps[i], vmin=z_spacing*i, extend='neither')
for j in range(10):
shadow_Z.fill(z_spacing*i - 1E-2 * j)
s = ax.contourf((X - X.mean()) * (1 + 8E-3 * j) + X.mean() + 2E5,
(Y - Y.mean()) * (1 + 8E-3 * j) + Y.mean() - 2E5,
shadow_Z, colors='black', alpha=0.1 - j * 1E-2)
I would like to position a colorbar inside a scatter plot by specifying the position in data coordinates.
Here is an example of how it works when specifying figure coordinates:
import numpy as np
import matplotlib.pyplot as plt
#Generate some random data:
a = -2
b = 2
x = (b - a) * np.random.random(50) + a
y = (b - a) * np.random.random(50) + a
z = (b) * np.random.random(50)
#Do a scatter plot
fig = plt.figure()
hdl = plt.scatter(x,y,s=20,c=z,marker='o',vmin=0,vmax=2)
ax = plt.gca()
ax.set_xlim([-2,2])
ax.set_ylim([-2,2])
#Specifying figure coordinates works fine:
fig_coord = [0.2,0.8,0.25,0.05]
cbar_ax = fig.add_axes(fig_coord)
clevs = [0, 1 , 2]
cb1 = plt.colorbar(hdl, cax=cbar_ax, orientation='horizontal', ticks=clevs)
plt.show()
...Ok, can't include an image of the plot here because I am lacking reputation. But the above code will give you an impression....
Now the question is, how could I position the colorbar at data coordinates, to appear at e.g.:
left, bottom, width, height: -1.5, 1.5, 1, 0.25
I have experimented with a few things, like determining the axes position within the figure and transforming it to data coordinates but didn't succeed.
Many thanks for ideas or pointing me to already answered similar questions!
Here is what I did (not particularly beautiful but it helps). Thanks tcaswell !
#[lower left x, lower left y, upper right x, upper right y] of the desired colorbar:
dat_coord = [-1.5,1.5,-0.5,1.75]
#transform the two points from data coordinates to display coordinates:
tr1 = ax.transData.transform([(dat_coord[0],dat_coord[1]),(dat_coord[2],dat_coord[3])])
#create an inverse transversion from display to figure coordinates:
inv = fig.transFigure.inverted()
tr2 = inv.transform(tr1)
#left, bottom, width, height are obtained like this:
datco = [tr2[0,0], tr2[0,1], tr2[1,0]-tr2[0,0],tr2[1,1]-tr2[0,1]]
#and finally the new colorabar axes at the right position!
cbar_ax = fig.add_axes(datco)
#the rest stays the same:
clevs = [0, 1 , 2]
cb1 = plt.colorbar(hdl, cax=cbar_ax, orientation='horizontal', ticks=clevs)
plt.show()
Here is what I did, based on the comments to my original question:
import numpy as np
import matplotlib.pyplot as plt
a = -2
b = 2
x = (b - a) * np.random.random(50) + a
y = (b - a) * np.random.random(50) + a
z = (b) * np.random.random(50)
fig = plt.figure()
hdl = plt.scatter(x,y,s=20,c=z,marker='o',vmin=0,vmax=2)
ax = plt.gca()
ax.set_xlim([-2,2])
ax.set_ylim([-2,2])
#[(lower left x, lower left y), (upper right x, upper right y)] of the desired colorbar:
dat_coord = [(-1.5,1.5),(-0.5,1.75)]
#transform the two points from data coordinates to display coordinates:
tr1 = ax.transData.transform(dat_coord)
#create an inverse transversion from display to figure coordinates:
inv = fig.transFigure.inverted()
tr2 = inv.transform(tr1)
#left, bottom, width, height are obtained like this:
datco = [tr2[0,0], tr2[0,1], tr2[1,0]-tr2[0,0],tr2[1,1]-tr2[0,1]]
#and finally the new colorabar axes at the right position!
cbar_ax = fig.add_axes(datco)
#the rest stays the same:
clevs = [0, 1 , 2]
cb1 = plt.colorbar(hdl, cax=cbar_ax, orientation='horizontal', ticks=clevs)
plt.show()
Two step to specify the position in data coordinates of an Axes:
use Axes.set_axes_locator() to set a function that return a Bbox object in figure coordinate.
set the clip box of all children in the Axes by set_clip_box() method:
Here is the full code:
import numpy as np
import matplotlib.pyplot as plt
#Generate some random data:
a = -2
b = 2
x = (b - a) * np.random.random(50) + a
y = (b - a) * np.random.random(50) + a
z = (b) * np.random.random(50)
#Do a scatter plot
fig = plt.figure()
hdl = plt.scatter(x,y,s=20,c=z,marker='o',vmin=0,vmax=2)
ax = plt.gca()
ax.set_xlim([-2,2])
ax.set_ylim([-2,2])
#Specifying figure coordinates works fine:
fig_coord = [0.2,0.8,0.25,0.05]
cbar_ax = fig.add_axes(fig_coord)
def get_ax_loc(cbar_ax, render):
from matplotlib.transforms import Bbox
tr = ax.transData + fig.transFigure.inverted()
bbox = Bbox(tr.transform([[1, -0.5], [1.8, 0]]))
return bbox
clevs = [0, 1 , 2]
cb1 = plt.colorbar(hdl, cax=cbar_ax, orientation='horizontal', ticks=clevs)
def get_ax_loc(cbar_ax, render):
from matplotlib.transforms import Bbox
tr = ax.transData + fig.transFigure.inverted()
bbox = Bbox(tr.transform([[1, -0.5], [1.8, 0]]))
return bbox
def set_children_clip_box(artist, box):
for c in artist.get_children():
c.set_clip_box(box)
set_children_clip_box(c, box)
cbar_ax.set_axes_locator(get_ax_loc)
set_children_clip_box(cbar_ax, hdl.get_clip_box())
plt.show()
And here is the output: