How to find the right cartopy projection? - python

I need to plot some data in Germany with cartopy. The data part of my plot works fine (so I deleted it for now). Unfortunately, the shape of the country is deformed due to the projection.
I am currently using the PlateCarree projection, but changing it to Orthographic or others created the same plot.
How to improve the shape?
Code:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
from cartopy.io import shapereader
# get country borders
resolution = '10m'
category = 'cultural'
name = 'admin_0_countries'
shpfilename = shapereader.natural_earth(resolution, category, name)
df = geopandas.read_file(shpfilename)
poly = df.loc[df['ADMIN'] == 'Germany']['geometry'].values[0]
# plot
ax = plt.axes(projection=ccrs.PlateCarree())
ax.set_extent([5.8, 15.1, 47.25, 55.1],
crs=ccrs.PlateCarree())
ax.add_geometries(poly,
crs=ccrs.PlateCarree(),
facecolor='gainsboro',
edgecolor='slategray',
lw=0.1,
alpha=.8)
# save plot
save_path = 'germany.png'
plt.savefig(save_path, dpi=250, bbox_inches='tight', pad_inches=0.)
plt.close()

The solution is transforming the Geopandas Dataframe using the same projection as explained here
New output:
germany.png
New code:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import geopandas
from cartopy.io import shapereader
# get country borders
resolution = "10m"
category = "cultural"
name = "admin_0_countries"
shpfilename = shapereader.natural_earth(resolution, category, name)
df = geopandas.read_file(shpfilename)
df_de = df.loc[df["ADMIN"] == "Germany"]
extent = [6., 14.8, 47.1, 55.1]
# plot
crs = ccrs.Orthographic(
central_longitude=(0.5 * (extent[0] + extent[1])),
central_latitude=(0.5 * (extent[2] + extent[3])),
)
crs_proj4 = crs.proj4_init
df_de.crs = "EPSG:4326"
df_ae = df_de.to_crs(crs_proj4)
fig, ax = plt.subplots(subplot_kw={"projection": crs})
ax.set_extent(extent)
ax.add_geometries(
df_ae["geometry"],
crs=crs,
facecolor="gainsboro",
edgecolor="slategray",
lw=0.1,
alpha=0.8,
)
# save plot
save_path = "germany.png"
plt.savefig(save_path, dpi=250, bbox_inches="tight", pad_inches=0.0)
plt.close()

Related

How to plot the map correctly over the SST data in cartopy?

I am trying to plot L2 Sea Surface Temperature data and I want to plot it over the globe in a geostationary projection. I tried the following code:
import h5py
import sys
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
# First get data from HDF5 file with h5py:
fn = '/home/swadhin/project/insat/data/3RIMG_30MAR2018_0014_L2B_SST_V01R00.h5'
with h5py.File(fn) as f:
print(list(f.keys()))
image = 'SST'
img_arr = f[image][0,:,:]
# get _FillValue for data masking
img_arr_fill = f[image].attrs['_FillValue'][0]
# retrieve extent of plot from file attributes:
left_lon = f.attrs['left_longitude'][0]
right_lon = f.attrs['right_longitude'][0]
lower_lat = f.attrs['lower_latitude'][0]
upper_lat = f.attrs['upper_latitude'][0]
sat_long = f.attrs['Nominal_Central_Point_Coordinates(degrees)_Latitude_Longitude'][1]
sat_hght = f.attrs['Nominal_Altitude(km)'][0] * 1000.0 # (for meters)
print('Done reading HDF5 file')
## Use np.ma.masked_equal with integer values to
## mask '_FillValue' data in corners:
img_arr_m = np.ma.masked_equal(img_arr, img_arr_fill)
print(img_arr_fill)
print(np.max(img_arr_m))
print(np.min(img_arr_m))
#print(np.shape(img_arr_m))
# # Create Geostationary plot with cartopy and matplotlib
map_proj = ccrs.Geostationary(central_longitude=sat_long,satellite_height=sat_hght)
ax = plt.axes(projection=map_proj)
ax.coastlines(color='black',linewidth = 0.5)
#ax.add_feature(cfeature.BORDERS, edgecolor='white', linewidth=0.25)
#ax.add_feature(cfeature.STATES,edgecolor = 'red',linewidth = 0.5)
ax.gridlines(color='black', alpha=0.5, linestyle='--', linewidth=0.75, draw_labels=True)
#ax.add_geometries(ind_shapes,crs = map_proj, edgecolor = 'black', alpha = 0.5)
map_extend_geos = ax.get_extent(crs=map_proj)
plt.imshow(img_arr_m, interpolation='none',origin='upper',extent=map_extend_geos, cmap = 'jet')
plt.colorbar()
#plt.clim(-10,5)
plt.savefig('/home/swadhin/project/insat/data/l2_sst.png',format = 'png', dpi=1000)
The output I got is not very accurate. There are some SST values over some of the land areas which should not be the case.
I am adding the data here for people who wanna give it a try.
https://drive.google.com/file/d/126oW36JXua-zz3XMUcyZxwPj8UISDgUM/view?usp=sharing
I have checked your HDF5 file, and there are Longitude and Latitude variables in the file. So I think these WGS84 coordinates should be used.
First, the imshow method needs the image boundary information that cannot be obtained.
I also tried the pcolormesh method, but this method can not accept lon/lat array with NaN value.
In conclusion, the contourf seems to be the best choice, but this method still has the disadvantage that it is time-consuming to run.
import h5py
import sys
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
fn ='3RIMG_30MAR2018_0014_L2B_SST_V01R00.h5'
with h5py.File(fn) as f:
print(list(f.keys()))
image = 'SST'
img_arr = f[image][0,:,:]
lon = f['Longitude'][:]*0.01
lat = f['Latitude'][:]*0.01
# # get _FillValue for data masking
img_arr_fill = f[image].attrs['_FillValue'][0]
# # retrieve extent of plot from file attributes:
left_lon = f.attrs['left_longitude'][0]
right_lon = f.attrs['right_longitude'][0]
lower_lat = f.attrs['lower_latitude'][0]
upper_lat = f.attrs['upper_latitude'][0]
sat_long = f.attrs['Nominal_Central_Point_Coordinates(degrees)_Latitude_Longitude'][1]
sat_hght = f.attrs['Nominal_Altitude(km)'][0] * 1000.0 # (for meters)
print('Done reading HDF5 file')
## Use np.ma.masked_equal with integer values to
## mask '_FillValue' data in corners:
img_arr_m = np.ma.masked_equal(img_arr, img_arr_fill)
print(img_arr_fill)
print(np.max(img_arr_m))
print(np.min(img_arr_m))
lon_m = np.ma.masked_equal(lon, 327.67)
lat_m = np.ma.masked_equal(lat, 327.67)
# # Create Geostationary plot with cartopy and matplotlib
map_proj = ccrs.Geostationary(central_longitude=sat_long,satellite_height=sat_hght)
# or map_proj = ccrs.PlateCarree()
ax = plt.axes(projection=map_proj)
ax.set_global()
ax.coastlines(color='black',linewidth = 0.5)
ax.add_feature(cfeature.BORDERS, edgecolor='white', linewidth=0.25)
ax.add_feature(cfeature.STATES,edgecolor = 'red',linewidth = 0.5)
ax.gridlines(color='black', alpha=0.5, linestyle='--', linewidth=0.75, draw_labels=True)
cb = ax.contourf(lon_m,lat_m,img_arr_m, cmap = 'jet',transform = ccrs.PlateCarree())
plt.colorbar(cb)
plt.savefig('l2_sst1.png',format = 'png', dpi=300)
Here is the output figure.
or using a lon-lat projection.

Axis labels for LambertConformal in cartopy at wrong location

I want to plot some data in a LambertConformal projection and add labels to the axes. See the example code below. However, now the x-labels show up twice, and both times in the middle of the plot, instead of at its bottom. When instead I set gl.xlabels_bottom = False and gl.xlabels_top = True, no x-labels are plotted at all. With the y-labels, I do not get this problem; they are just nicely plotted either along the left or right boundary of the plot.
How can I get the x-labels at the right location (at the bottom of the figure)?
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
bounds_lon = [-45,-25]
bounds_lat = [55,65]
lon = np.arange(bounds_lon[0],bounds_lon[1]+0.1,0.1)
lat = np.arange(bounds_lat[0],bounds_lat[1]+0.1,0.1)
Lon, Lat = np.meshgrid(lon,lat)
data = np.ones(np.shape(Lon))
data_crs = ccrs.PlateCarree()
projection = ccrs.LambertConformal(central_longitude=np.mean(bounds_lon),central_latitude=np.mean(bounds_lat),cutoff=bounds_lat[0])
plt.figure(figsize=(4,4))
ax = plt.axes(projection=projection)
ax.coastlines()
ax.contourf(Lon, Lat, data, transform=data_crs)
gl = ax.gridlines(crs=ccrs.PlateCarree(), linewidth=2, color='gray', alpha=0.5, linestyle='--')
gl.xlabels_bottom = True
Manual repositioning of tick-labels are needed. To do that successfully, requires some other adjustments of the plot settings. Here is the code you can try.
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
bounds_lon = [-45,-25]
bounds_lat = [55,65]
# make-up data to plot on the map
inc = 0.5
lon = np.arange(bounds_lon[0],bounds_lon[1]+inc, inc)
lat = np.arange(bounds_lat[0],bounds_lat[1]+inc, inc)
Lon, Lat = np.meshgrid(lon,lat)
#data = np.ones(np.shape(Lon)) # original `boring` data
data = np.sin(Lon)+np.cos(Lat) # better data to use instead
data_crs = ccrs.PlateCarree()
projection = ccrs.LambertConformal(central_longitude=np.mean(bounds_lon), \
central_latitude=np.mean(bounds_lat), \
#cutoff=bounds_lat[0]
)
# Note: `cutoff` causes horizontal cut at lower edge
# init plot figure
plt.figure(figsize=(15,9))
ax = plt.axes(projection=projection)
ax.coastlines(lw=0.2)
ax.contourf(Lon, Lat, data, transform=data_crs, alpha=0.5)
# set gridlines specs
gl = ax.gridlines(crs=ccrs.PlateCarree(), linewidth=2, color='gray', alpha=0.5, linestyle='--')
gl.top_labels=True
gl.bottom_labels=True
gl.left_labels=True
gl.right_labels=True
plt.draw() #enable access to lables' positions
xs_ys = ax.get_extent() #(x0,x1, y0,y1)
#dx = xs_ys[1]-xs_ys[0]
dy = xs_ys[3]-xs_ys[2]
# The extent of `ax` must be adjusted
# Extents' below and above are increased
new_ext = [xs_ys[0], xs_ys[1], xs_ys[2]-dy/15., xs_ys[3]+dy/12.]
ax.set_extent(new_ext, crs=projection)
# find locations of the labels and reposition them as needed
xs, ys = [], []
for ix,ea in enumerate(gl.label_artists):
xy = ea[2].get_position()
xs.append(xy[0])
ys.append(xy[1])
# Targeted labels to manipulate has "W" in them
if "W" in ea[2].get_text():
x_y = ea[2].get_position()
# to check which are above/below mid latitude of the plot
# use 60 (valid only this special case)
if x_y[1]<60:
# labels at lower latitudes
curpos = ea[2].get_position()
newpos = (curpos[0], 54.7) # <- from inspection: 54.7
ea[2].set_position(newpos)
else:
curpos = ea[2].get_position()
newpos = (curpos[0], 65.3) # <- from inspection: 65.3
ea[2].set_position(newpos)
plt.show()
Edit1
If you want to move all the lat/long labels to the outside edges, try this code. It is much more concise than the above.
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
bounds_lon = [-45,-25]
bounds_lat = [55,65]
inc = 0.5
lon = np.arange(bounds_lon[0],bounds_lon[1]+inc, inc)
lat = np.arange(bounds_lat[0],bounds_lat[1]+inc, inc)
Lon, Lat = np.meshgrid(lon,lat)
#data = np.ones(np.shape(Lon)) # boring data
data = np.sin(Lon)+np.cos(Lat) # more interesting
data_crs = ccrs.PlateCarree()
projection = ccrs.LambertConformal(central_longitude=np.mean(bounds_lon), \
central_latitude=np.mean(bounds_lat), \
cutoff=bounds_lat[0]
)
# init plot
plt.figure(figsize=(15,9))
ax = plt.axes(projection=projection)
ax.coastlines(lw=0.2)
ax.contourf(Lon, Lat, data, transform=data_crs, alpha=0.3)
gl = ax.gridlines(draw_labels=True, x_inline=False, y_inline=False,
color='k', linestyle='dashed', linewidth=0.5)
gl.top_labels=True
gl.bottom_labels=True
gl.left_labels=True
gl.right_labels=True
plt.show()
If you want to get bottom edge as a straight line, you can achieve that by dropping the option cutoff=bounds_lat[0] from this line of code:-
projection = ccrs.LambertConformal(central_longitude=np.mean(bounds_lon), \
central_latitude=np.mean(bounds_lat), \
cutoff=bounds_lat[0]
)
so that it becomes
projection = ccrs.LambertConformal(central_longitude=np.mean(bounds_lon),
central_latitude=np.mean(bounds_lat))
and you will get the plot like this:-

Overlay/Fill country boundaries with image in

Is there a way to fill a country with an image similar to R solution using custom library here:
I have a solution where the face colour is filled for instance the below where Italy is blue. However, I would like to add the Italian flag. Is there a way in Python (I have not found much after searching) or is something like QGIS needed:
#create a map where I can load images in to fill the countries
import cartopy
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import cartopy.io.shapereader as shpreader
flag = "italy.png" #this is a locally saved png.
plt.figure(figsize=(15, 15)) #size of plot
ax = plt.axes(projection=cartopy.crs.TransverseMercator(25))
ax.add_feature(cartopy.feature.BORDERS, linestyle='-', alpha=1)
ax.coastlines(resolution='110m') #simplifies the border lines
ax.add_feature(cartopy.feature.OCEAN, facecolor="#40e0d0") #colour of ocean
# ax.gridlines() #adds global grid lines
ax.set_extent ((-7.5, 50, 34, 69), cartopy.crs.PlateCarree()) #makes it european
shpfilename = shpreader.natural_earth(resolution='110m',
category='cultural',
name='admin_0_countries')
for country in shpreader.Reader(shpfilename).records():
if country.attributes['NAME_LONG'] == "Italy":
ax.add_geometries(country.geometry, ccrs.PlateCarree(),
facecolor="blue",
#no attribute like this img= "fd",
label=country.attributes['NAME_LONG'])
plt.show()
Any help, much appreciated!
Here is a demo code that does what you need. As a matter of fact, cartopy logo uses this technique to create.
import cartopy
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import cartopy.io.shapereader as shpreader
import matplotlib.patches as mpatches
import numpy as np
imdat1 = plt.imread('flag-of-italy.jpg', format='jpg') # use your flag
plt.figure(figsize=(8, 8))
ax = plt.axes(projection=cartopy.crs.TransverseMercator(25))
ax.add_feature(cartopy.feature.BORDERS, linestyle='-', alpha=1)
ax.coastlines(resolution='110m')
ax.add_feature(cartopy.feature.OCEAN, facecolor="#40e0d0")
# ax.gridlines() #adds global grid lines
ax.set_extent ((-7.5, 50, 24, 69), cartopy.crs.PlateCarree())
shpfilename = shpreader.natural_earth(resolution='110m',
category='cultural',
name='admin_0_countries')
italy_ctry = None #use this to grab italy's
for country in shpreader.Reader(shpfilename).records():
if country.attributes['NAME_LONG'] == "Italy":
italy_ctry = country
ax.add_geometries(country.geometry, ccrs.PlateCarree(),
facecolor="none",
alpha=0.7,
zorder=2,
label=country.attributes['NAME_LONG'])
# create mpatch from `italy` geometry
cg = italy_ctry.geometry
cg2 = cg.simplify(0.02)
if cg2.geometryType()=='MultiPolygon':
# if == `Polygon`, dont need to loop
for ea in cg2.geoms:
cg2xy = ea.exterior.xy # tuple of (x,y)
xys = []
for ea in zip(cg2xy[0], cg2xy[1]):
#print(ea[0],ea[1])
xys.append([ea[0],ea[1]])
# add a patch
poly = mpatches.Polygon(xys, closed=True, ec='r', \
lw=2, fc='yellow', \
transform=ccrs.PlateCarree(), \
alpha=0.5, zorder=30)
plate_carree_transform = ccrs.PlateCarree()._as_mpl_transform(ax)
xtent1 = (6.519950, 17.122259, 35.783370, 47.962952)
imdat2 = ax.imshow(imdat1, origin='upper', extent=xtent1, \
transform=ccrs.PlateCarree(), \
zorder=15, alpha=.9)
##imdat2 = ax.stock_img() #for testing
imdat2.set_clip_path(mpatches.Path(xys), transform=plate_carree_transform)
pass
plt.show()
The sample plot (varies with the flag in use):

Python Basemap Heatmap

I am trying to copy the method that was done on this page: https://makersportal.com/blog/2018/7/20/geographic-mapping-from-a-csv-file-using-python-and-basemap under "Mapping Interesting Data" to have a color bar associated with my data.
Right now I just get a plain map of South America, which is what I want as my background but there is no data included.
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
m = Basemap(projection='mill',
llcrnrlat = -30, #bottom
llcrnrlon = -120, #left
urcrnrlat = 20, #top
urcrnrlon = -50, #right
resolution='c')
m.drawcoastlines()
m.drawcountries()
# format colors for elevation range
SST_min = np.min(df5.DaasgardSST)
SST_max = np.max(df5.DaasgardSST)
cmap = plt.get_cmap('gist_earth')
normalize = matplotlib.colors.Normalize(vmin=SST_min, vmax=SST_max)
# plot SST with different colors
for i in range(0,len(df5.DaasgardSST)):
x,y = m(lon,lat)
color_interp = np.interp(df5,[SST_min,SST_max],[0,30])
plt.plot(x,y,marker='o',markersize=6,color=cmap(int(color_interp)))
# format the colorbar
cax, _ = matplotlib.colorbar.make_axes(ax)
cbar = matplotlib.colorbar.ColorbarBase(cax, cmap=cmap,norm=normalize,label='Elevation')
plt.title('Title')
plt.show()

Contour will not plot over Python basemap

I am trying to plot a contour and quiver plot over a basemap. When I plot, I get no errors, but only the basemap will show. The netcdf file only has one point in it for lat and long, so I had to create a range of coordinates. Any ideas why this is happening?
import netCDF4
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import pylab
from mpl_toolkits.basemap import Basemap
from matplotlib.patches import Polygon
ncfile = netCDF4.Dataset('30JUNE2012_0400UTC.cdf', 'r')
dbZ = ncfile.variables['MAXDBZF']
u = ncfile.variables['UNEW']
v = ncfile.variables['VNEW']
#print u
#print v
#print dbZ
data = dbZ[0,0]
data.shape
#print data.shape
z_index = 0 # z-level you want to plot (0-19)
U = u[0,z_index, :,:] #[time,z,x,y]
V = v[0,z_index, :,:]
lats = np.linspace(35.0, 41.0, data.shape[0])
lons = np.linspace(-81.0,-73.0, data.shape[1])
# create the map
map = Basemap(llcrnrlat=36,urcrnrlat=40,\
llcrnrlon=-80,urcrnrlon=-74,lat_ts=20,resolution='c')
# load the shapefile, use the name 'states'
map.readshapefile('st99_d00', name='states', drawbounds=True)
# collect the state names from the shapefile attributes so we can
# look up the shape obect for a state by it's name
state_names = []
for shape_dict in map.states_info:
state_names.append(shape_dict['NAME'])
ax = plt.gca() # get current axes instance
x,y = map(*np.meshgrid(lats,lons))
levels = np.arange(5,60,3)
c = map.contourf(x,y,data, levels, cmap='jet')
plt.colorbar()
q=plt.quiver(U,V,width=0.002, scale_units='xy',scale=10)
qk= plt.quiverkey (q,0.95, 1.02, 20, '20m/s', labelpos='N')
plt.show()

Categories

Resources