setting cartopy set_extent for pacific ocean (160E ~ -90W or 160 ~ 270) not working - cartopy

I'm trying to project the map only in the tropical pacific region which ranges from 160E to -90W (or 160 to 270) like the following example
paco_region = plt.axes(projection=ccrs.PlateCarree())
paco_region.coastlines()
paco_region.set_extent([160,-90,-20,20],crs=ccrs.PlateCarree())
paco_region.gridlines(crs=ccrs.PlateCarree(), draw_labels=True)
plt.show()
The problem is that cartopy is refusing to show the map from 160E (as set to left boundary) to -90W (as set to right boundary). It just shows the map from -90W to 160W (like figure below)
How do I fix this?

You need some coordinate transformation and a little trick to get it done.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
# define CRS's for our use case
crs0 = ccrs.PlateCarree(central_longitude=0) #for coding data, same as ccrs.PlateCarree()
crs180 = ccrs.PlateCarree(central_longitude=180) #for plotting map in pacific area
# For all plotting, use `crs180`
fig, paco_region = plt.subplots(figsize=(9,5), subplot_kw={'projection': crs180})
#paco_region.stock_img() # background image check-plot
paco_region.coastlines()
paco_region.set_extent([160, 270, -20, 20], crs=crs0)
# Sample plot of users' data
# Just use regular long/lat
lons = [175, 185, 195, 220, 250]
lats = [15, 0, -15, 12, 18]
# ... but specify `transform = crs0` when plot the data.
paco_region.scatter(lons, lats, transform=crs0, color="r")
# For grid-line labelling, use `crs0`
paco_region.gridlines(crs=crs0, draw_labels=True)
plt.show()

Related

How to apply Earth Features and Land/Ocean masks in high resolution coastlines in Cartopy?

I am using the coastlines of the GSHHS dataset in Cartopy. This has a high resolution for coastlines. But I want to not only plot the high resolution coastline but also apply a mask for the ocean.
import matplotlib.pyplot as plt
import cartopy
fig = plt.figure(figsize=(20,12))
ax = plt.axes(projection=cartopy.crs.PlateCarree())
coast = cartopy.feature.GSHHSFeature(scale="full")
ax.add_feature(coast, linewidth=2)
ax.add_feature(cartopy.feature.NaturalEarthFeature("physical", "land", "10m"))
ax.set_extent([-17, -16, 27.9, 28.7])
Executing the code there're differences in the images, since I guess that ax.add_feature(cartopy.feature.NaturalEarthFeature("physical", "land", "10m")) is using the "10m" resolution, while GSHHS has a higher resolution.
How can I mask using GSHHS higher resolution? Thx.
Before one can answer the question how to apply a mask to hide features in the main plot, we need to investigate the available masks first.
In our case, the main plot is Natural_Earth 10m resolution Physical Land features, and various resolutions of GSHHSFeature as the available masks.
The code and the output plot below reveals the insight.
# Code adapted from:-
# Src: https://ctroupin.github.io/posts/2019-09-02-fine-coast/
import matplotlib.pyplot as plt
import cartopy
import cartopy.crs as ccrs
import cartopy.feature as cfeature
resolutions = {"c": "crude",
"l": "low",
"i": "intermediate",
"h": "high",
"f": "full"}
coordinates = (8.7, 8.81, 42.55, 42.60)
myproj = ccrs.PlateCarree()
fig = plt.figure(figsize=(8, 4))
for i, res in enumerate(resolutions):
ax = plt.subplot(2, 3, i+1, projection=myproj)
coast = cfeature.GSHHSFeature(scale=res)
ax.add_feature(coast, facecolor="lightgray")
ax.add_feature(cartopy.feature.NaturalEarthFeature("physical", "land", "10m"),
ec="red", fc="yellow", lw=2, alpha=0.4)
ax.set_xlim(coordinates[0], coordinates[1])
ax.set_ylim(coordinates[2], coordinates[3])
plt.title(resolutions[res])
plt.suptitle("GSHHS: gray Versus 10m_Physical_Land: yellow/red")
plt.show()
Suppose we need a plot at this zoom level. It is clearly that the outlines from 2 data sources do not fit well enough to the eyes of the viewers. We may conclude that none of the available masks is fit for the target plot.
But if the plot extents is wider, or smaller scale plots, coupled with some cartographic techniques, e.g. using thicker coastlines, one may get acceptable plots. The process is trial-and-error approach.
Edit1
With (Global_land_mask) added, more choices can be plotted for
comparison.
from global_land_mask import globe
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import numpy as np
# Extent of map in degrees
minlon,maxlon,minlat,maxlat = (8.7, 8.81, 42.55, 42.60)
# Lat/lon points to get for `global_land_mask` uses
# Finer than 500x250 has no improvement
lons = np.linspace(minlon,maxlon, 500)
lats = np.linspace(minlat,maxlat, 250)
# Make a grid
lon_grid, lat_grid = np.meshgrid(lons,lats)
# Get whether the points are on land.
z = globe.is_land(lat_grid, lon_grid)
# GSHHS ...
resolutions = {"c": "crude",
"l": "low",
"i": "intermediate",
"h": "high",
"f": "full"}
myproj = ccrs.PlateCarree()
fig = plt.figure(figsize=(8, 4))
for i, res in enumerate(resolutions):
ax = plt.subplot(2, 3, i+1, projection=myproj)
# GSHHSFeature
coast = cfeature.GSHHSFeature(scale=res)
ax.add_feature(coast, facecolor="brown", alpha=0.5)
# 10m physical_land
ax.add_feature(cfeature.NaturalEarthFeature("physical", "land", "10m"),
ec="red", fc="yellow", lw=2, alpha=0.4)
# Global_land_mask data is used to create fillcontour
# The fillcontour with proper (colormap, zorder, alpha) can be used as land `mask`
ax.contourf(lon_grid, lat_grid, z, cmap="Greys_r", alpha=0.4)
ax.set_xlim(minlon, maxlon)
ax.set_ylim(minlat, maxlat)
plt.title(resolutions[res])
plt.suptitle("GSHHS:brown/black | 10m_Land:yellow/red | Global_land_mask:light_gray")
plt.show()
# The best resolutuion from `Global_land_mask` is plotted in `lightgray` covering the sea areas

Unexpected behavior from Cartopy

I'm trying to draw a 'straight' line on the surface of the Earth (a great circle), which should appear curved on an orthographic projection that isn't looking straight down on the curve. However, when I try to connect two points with a geodetic line in cartopy I get a line with a kink in it. Where is this kink coming from? And how can I get a correctly rendered great circle segment?
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
lats = [0, 36]
lons = [15, 76]
ax = plt.axes(projection = ccrs.Orthographic(central_longitude=0, central_latitude=45))
ax.plot(lons, lats, transform=ccrs.Geodetic())
ax.set_global()
ax.gridlines()
From the option transform=ccrs.Geodetic(), the implication is that you need great-circle arc as a result of your ax.plot() statement.
Without proper setting of projection._threshold you will get the kinked line as you experienced.
Here is the modified code and the expected result.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
lats = [0, 36]
lons = [15, 76]
myProj = ccrs.Orthographic(central_longitude=0, central_latitude=45)
myProj._threshold = myProj._threshold/20.
ax = plt.axes(projection = myProj)
ax.plot(lons, lats, transform=ccrs.Geodetic())
ax.set_global()
ax.gridlines()
Smaller values of the threshold will cause the plotted lines to have denser vertices along the lines. Additional vertices are not obtained by simple interpolation when great-circle arcs are required in this case.

How to plot a map of a semi-sphere (eg northern hemisphere) using matplotlib cartopy

How to plot a map of a semi-sphere (eg northern hemisphere) using cartopy.
I'm trying to plot a map of the northern hemisphere using cartopy. But I don't understand how should I define the extent of the map so that only this region of interest is plotted. I would like the map to be cut off at 0° latitude. I would like to have code where I could easily define any subset of the glob using the ccrs.NearsidePerspective projection, or the ccrs.Orthographic projection.
Below I leave a code for reproduction.
import numpy as np
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
# Creating fake data
x = np.linspace(-180, 180, 361)
y = np.linspace(-90, 90, 181)
lon, lat = np.meshgrid(x, y)
values = np.random.random(lon.shape)*20
fig = plt.figure(figsize=(15, 10))
proj = ccrs.NearsidePerspective(central_longitude=-45, central_latitude=21)
ax = fig.add_subplot(121, projection=proj)
ax.set_extent([-120, 40, 0, 60])
ax.pcolormesh(lon, lat, values, transform=ccrs.PlateCarree())
ax.coastlines(linewidth=2)
gl = ax.gridlines(draw_labels=True, linestyle='--')
The code generates the following figure:
Thank you very much in advance.
Robson
To plot only the upper hemisphere part of the map projection, a polygon of that part is needed to use as the projection boundary.
That polygon is created as a matplotlib-path object. It vertices' coordinates are data coordinates in my code, so that, no transformation is required when applied to the final plot.
This is a complete code:-
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import matplotlib.path as mpath
import numpy as np
from geographiclib.geodesic import Geodesic
fig = plt.figure(figsize=[12, 12])
proj = ccrs.NearsidePerspective(central_longitude=-45, central_latitude=21, satellite_height=35785831)
ax = plt.subplot(projection=proj)
# The value of r is obtained by previous run of this code ...
# with the line .. #print(ax.get_xlim()) uncommented
r = 5476336.098
ax.set_xlim(-r, r)
ax.set_ylim(-r, r)
ax.stock_img()
ax.coastlines(lw=1, color="darkblue")
# Find the locations of points along the equatorial arc
# start location
lon_fr, lat_fr = 30, 0
# end location
lon_to, lat_to = -120, 0
# This gets geodesic between the two points, WGS84 ellipsoid is used
geodl = Geodesic.WGS84.InverseLine(lat_fr, lon_fr, lat_to, lon_to)
lonlist, latlist = [], []
num_points = 32 #for series of points on geodesic/equator
for ea in np.linspace(0, geodl.s13, num_points):
g = geodl.Position(ea, Geodesic.STANDARD | Geodesic.LONG_UNROLL)
#print("{:.0f} {:.5f} {:.5f} {:.5f}".format(g['s12'], g['lat2'], g['lon2'], g['azi2']))
lon2, lat2 = g['lon2'], g['lat2']
lonlist.append( g['lon2'] )
latlist.append( g['lat2'] )
# Get data-coords from (lonlist, latlist)
# .. as points along equatorial arc
dataxy = proj.transform_points(ccrs.PlateCarree(), np.array(lonlist), np.array(latlist))
# (Uncomment to) Plot equator line
#ax.plot(dataxy[:, 0:1], dataxy[:, 1:2], "go-", linewidth=2, markersize=5, zorder=10)
# Top semi-circle arc for map extent
theta = np.linspace(-0.5*np.pi, 0.5*np.pi, 64)
center, radius = [0, 0], r
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
# Combine vertices of the semi-circle and equatorial arcs
# These points are in data coordinates, ready to plot on the axes.
verts = np.vstack([verts*r, dataxy[:, 0:2]])
polygon = mpath.Path(verts + center)
ax.set_boundary(polygon) #This masks-out unwanted part of the plot
gl = ax.gridlines(draw_labels=True, xlocs=range(-150,180,30), ylocs=range(0, 90, 15),
y_inline=True, linestyle='--', lw= 5, color= "w", )
# Get limits, the values are the radius of the circular map extent
# The values is then used as r = 5476336.09797 on top of the code
#print(ax.get_xlim())
#print(ax.get_ylim())
plt.show()

PyPlot ConnectionPatch between CartoPy GeoAxes

The ConnectionPatch is a useful way to draw a line between two points on two different axes (demo). Is it possible to use this class when one (or both) of the axes is of Cartopy GeoAxes type? A related answer suggests a work-around but I would prefer to avoid this.
I can not answer your question about the use of that class thing. But, if you are interested in plotting the lines between 2 different Cartopy geoaxes, or between matplotlib axes and a geoaxe, that can be achieved with some coordinate transformation. Here is a runnable code and the output plot. I have written some comments within the code to help explain the important steps.
For further information about coordinate system and tranformation:
Cartopy https://scitools.org.uk/cartopy/docs/latest/tutorials/understanding_transform.html
Since Cartopy is built on top of Matplotlib, you need to look into the related subject in Matplotlib.
Matplotlib https://matplotlib.org/3.2.1/tutorials/advanced/transforms_tutorial.html
import cartopy
import cartopy.mpl.geoaxes
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
fig, ax = plt.subplots()
fig.set_size_inches([8,8]) # 9,6; 8,9; 8,3 all OK
# Plot simple line on main axes
ax.plot([4,5,3,1,2])
p1 = [0.5,3.0] # Bangkok text location
p2 = [0.5,2.75] # Himalaya text location
# Plot texts (Bangkok, Himalaya) on the main axes
ax.text(*p1, "Bangkok", ha='right')
ax.text(*p2, "Himalaya", ha='right')
# Ploting on UR inset map (cartopy) on the main axes (ax)
bkk_lon, bkk_lat = 100, 13 # Bangkok
hml_lon, hml_lat = 83.32, 29.22 # Everest peak
# Create cartopy geoaxes inset axes as part of the main axes 'ax'
axins = inset_axes(ax, width="40%", height="30%", loc="upper right",
axes_class = cartopy.mpl.geoaxes.GeoAxes,
axes_kwargs = dict(map_projection = cartopy.crs.PlateCarree()))
# Set map limits on that axes (for Thailand)
llx, lly = 95, 0
urx, ury = 110, 25
axins.set_xlim((llx, urx))
axins.set_ylim((lly, ury))
# Plot coastlines
axins.add_feature(cartopy.feature.COASTLINE)
# Plot line across the inset mao, LL to UR; OK
#ll_p, ur_p = [llx,urx], [lly,ury]
#axins.plot(ll_p, ur_p, "r--")
axins.plot(bkk_lon, bkk_lat, 'ro', transform=cartopy.crs.PlateCarree()) # OK!
# Create another inset map on the main axes (ax)
axins2 = inset_axes(ax, width="40%", height="30%", loc="lower left",
axes_class = cartopy.mpl.geoaxes.GeoAxes,
axes_kwargs = dict(map_projection = cartopy.crs.PlateCarree()))
# Set map limits on that axes (second inset map)
llx2, lly2 = -60, -20
urx2, ury2 = 120, 90
axins2.set_xlim((llx2, urx2))
axins2.set_ylim((lly2, ury2))
axins2.add_feature(cartopy.feature.COASTLINE)
# Plot line from UK to BKK, OK
#p21, p22 = [0, 100], [40, 13]
#axins2.plot(p21, p22, "r--")
# Plot blue dot at Himalaya
axins2.plot(hml_lon, hml_lat, "bo")
plt.draw() # Do this to get updated position
# Do coordinate transformation to get BKK, HML locations in display coordinates
# from axins_data_xy to dp_xy
dpxy_bkk_axins = axins.transData.transform((bkk_lon, bkk_lat)) # get display coordinates
# from axins2_data_xy to dp_xy
dpxy_bkk_axins2 = axins2.transData.transform((hml_lon, hml_lat)) # get display coordinates
# Do coordinate transformation to get BKK, HML locations in data coordinates of the main axes 'ax'
# from both dp_xy to main_ax_data
ur_bkk = ax.transData.inverted().transform( dpxy_bkk_axins )
ll_hml = ax.transData.inverted().transform( dpxy_bkk_axins2 )
# Prep coordinates for line connecting BKK to HML
xs = ur_bkk[0], ll_hml[0]
ys = ur_bkk[1], ll_hml[1]
xs = ur_bkk[0], ll_hml[0]
ys = ur_bkk[1], ll_hml[1]
ax.plot(xs, ys, 'g--') # from Bkk to Himalaya of different inset maps
# Plot lines from texts (on main axes) to locations on maps
ax.plot([p1[0], ur_bkk[0]], [p1[1], ur_bkk[1]], 'y--')
ax.plot([p2[0], ll_hml[0]], [p2[1], ll_hml[1]], 'y--')
# Set cartopy inset background invisible
axins.background_patch.set_visible(False)
axins2.background_patch.set_visible(False)
plt.show()
The output plot:-

central longitude for NorthPolarStereo

I'd like to plot a polar stereographic plot of the Northern Hemisphere with 180 at the bottom of the plot so I can emphasize the Pacific region. I'm using the latest cartopy from git, and can make a polar stereographic plot no problem, but I can't work out how to change which longitude is at the bottom of the plot. I tried setting the longitude extent to [-180, 180] but this doesn't help, and the NorthPolarStereo() doesn't accept any keyword arguments like central_longitude. Is this possible currently?
This feature has now been implemented in Cartopy (v0.6.x). The following example produces two subplots in Northern Hemisphere polar stereographic projections, one with the default settings and one with the central longitude changed:
"""Stereographic plot with adjusted central longitude."""
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
from cartopy.examples.waves import sample_data
# read sample data
x, y, z = sample_data(shape=(73, 145))
fig = plt.figure(figsize=(8, 4))
# first plot with default settings
ax1 = fig.add_subplot(121, projection=ccrs.NorthPolarStereo())
cs1 = ax1.contourf(x, y, z, 50, transform=ccrs.PlateCarree(),
cmap='gist_ncar')
ax1.set_extent([0, 360, 0, 90], crs=ccrs.PlateCarree())
ax1.coastlines()
ax1.set_title('Centred on 0$^\circ$ (default)')
# second plot with 90W at the bottom of the plot
ax2 = fig.add_subplot(
122, projection=ccrs.NorthPolarStereo(central_longitude=-90))
cs2 = ax2.contourf(x, y, z, 50, transform=ccrs.PlateCarree(),
cmap='gist_ncar')
ax2.set_extent([0, 360, 0, 90], crs=ccrs.PlateCarree())
ax2.coastlines()
ax2.set_title('Centred on 90$^\circ$W')
plt.show()
The output of this script is:
For Cartopy 0.17 and matplotlib 3.1.1 (Python 3.7), I got an error in set_extent() with the above solution.
It seems that set_extent() only works this way:
ax1.set_extent([-180, 180, 0, 90], crs=ccrs.PlateCarree())
ax2.set_extent([-179, 179, 0, 90], crs=ccrs.PlateCarree())
So, the rotated image needs some weird longitude boundaries..