longitude off by 180 degrees with cartopy Orthographic and RotatedPole - cartopy

I'm trying to place blue dots from the North pole down the prime meridian (longitude=0) but instead see the dots going down the dateline (longitude=180).
The code:
#!/usr/bin/env python
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
Lon = 0
ax = plt.axes( projection=ccrs.Orthographic( central_latitude=70,
central_longitude=Lon) )
ax.set_global()
vector_crs = ccrs.RotatedPole( pole_latitude=90, pole_longitude=Lon )
ax.plot([Lon, Lon, Lon, Lon, Lon, Lon], # longitude
[ 90, 80, 70, 60, 50, 40], # latitude
'bo', markersize=5, transform=vector_crs)
ax.stock_img()
plt.show()
Probably something related to the transforms but I haven't figured out what. Cartopy version 0.14.2, Python 3.6.

I think the problem is coming from the transform projection that you've defined:
ax = plt.axes(projection=vector_crs)
ax.coastlines()
plt.show()
Note that this simple coastlines plot in your transform projection looks like a Plate Carree plot with a central longitude of 180°. With this in mind, let's take a look at plotting your sample data points on a plot with a Plate Carree projection in order to also try simplifying the map you're plotting to:
ax = plt.axes(projection=ccrs.PlateCarree())
ax.plot([Lon, Lon, Lon, Lon, Lon, Lon],
[ 90, 80, 70, 60, 50, 40],
'bo', markersize=5, transform=vector_crs)
ax.stock_img()
plt.show()
As with your example, the points do not appear where we might expect. Finally, let's try using a Plate Carree projection as the transform for your points when plotting them onto an Orthographic map:
ax = plt.axes(projection=ccrs.Orthographic(central_latitude=70,
central_longitude=Lon))
ax.set_global()
ax.plot([Lon, Lon, Lon, Lon, Lon, Lon],
[ 90, 80, 70, 60, 50, 40],
'bo', markersize=5, transform=ccrs.PlateCarree())
ax.stock_img()
plt.show()
This seems to provide more the plot you were looking for.

Related

Cartopy, set_extent with NorthPoleStereo not working

Using set_extent on Polar Stereo Graphic maps doesn't seem to be working #in a predictable fashion. I am following this Answered StackOverflow example but neither rotation #yields a map. I have set ax1.set_global() to get the data displayed.
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, 8))
# first plot with default rotation. Global extent, works fine
ax1 = fig.add_subplot(221, projection=ccrs.NorthPolarStereo())
cs1 = ax1.contourf(x, y, z, 50, transform=ccrs.PlateCarree(),
cmap='gist_ncar')
ax1.set_title('Global')
#next plot setting extent to 0,360,40,90, no display
ax2 = fig.add_subplot(222,
projection=ccrs.NorthPolarStereo())
cs2 = ax2.contourf(x, y, z, 50,
transform=ccrs.PlateCarree(),
cmap='gist_ncar')
ax2.set_extent([0,360,40,90],crs=ccrs.PlateCarree())
ax2.coastlines()
ax2.set_title('Centred on 0$^\circ$ (default)')
#Now resetting set_extent to [-180,180,40,90] strangely works!
ax3 = fig.add_subplot(
223, projection=ccrs.NorthPolarStereo())
cs3 = ax3.contourf(x, y, z, 50, transform=ccrs.PlateCarree(),
cmap='gist_ncar')
ax3.set_extent([-180, 180, 40, 90], crs=ccrs.PlateCarree())
ax3.coastlines()
ax3.set_title('Using -180,180 $^\circ$W')
#but now rotating projection yields just a corner of the map
ax4 = fig.add_subplot(
224,projection=ccrs.NorthPolarStereo(central_longitude=-45))
cs4 = ax4.contourf(x, y, z, 50, transform=ccrs.PlateCarree(),
cmap='gist_ncar')
ax4.set_extent([-180, 180, 40, 90], crs=ccrs.PlateCarree())
ax4.coastlines()
ax4.set_title('Rotated on -45 $^\circ$W')
plt.show()
I was expecting the set_extent to work as documented but there seems to be #a strange interaction between the rotation and the extent
Output
It seems to be an artifact of how CartoPy calculates its bounding boxes. What it does, when given extents in lon/lat, is calculate the 4 corners of the box based on the extent, then project those to the native projection, then figure out the extent from that. The problem is that in the stereographic projection, some of those combinations end of up with no separation in x or y--due to the periodic nature of (especially) longitude.
I can reproduce the math issues with just PyProj, so it's not a problem with the projection math in CartoPy, just a limitation of how it calculates bounds. You can override by using projected coordinates in in set_extent:
ax.set_extent((0, 500000, 0, 500000), crs=ccrs.NorthPolarStereo())
I know that's not ideal, but I can't think of any good ways to calculate what the appropriate bounds is based on box in lon/lat space.

Matplotlib: plt.text with user-defined circle radii

Dear stackoverflow users,
I want to plot some data labels with its coordinates in a x,y-plot. Around the labels I want to put a circle with a user-defined radius as I want to symbolize the magnitude of the data property by the radius of the circle.
An example dataset could look like the following:
point1 = ["label1", 0.5, 0.25, 1e0] # equals [label, x, y, radius]
point2 = ["label2", 0.5, 0.75, 1e1] # equals [label, x, y, radius]
I want to use a code silimar to the following one:
import matplotlib.pyplot as plt
plt.text(point1[1], point1[2], point1[0], bbox = dict(boxstyle="circle")) # here I want to alter the radius by passing point1[3]
plt.text(point2[1], point2[2], point2[0], bbox = dict(boxstyle="circle")) # here I want to alter the radius by passing point2[3]
plt.show()
Is this possible somehow or is the plt.add_patch variant the only possible way?
Regards
In principle, you can use the boxes' pad parameter to define the circle size. However this is then relative to the label. I.e. a small label would have a smaller circle around it for the same value of pad than a larger label. Also the units of pad are fontsize (i.e. if you have a fontsize of 10pt, a padding of 1 would correspond to 10pt).
import numpy as np
import matplotlib.pyplot as plt
points = [["A", 0.2, 0.25, 0], # zero radius
["long label", 0.4, 0.25, 0], # zero radius
["label1", 0.6, 0.25, 1]] # one radius
for point in points:
plt.text(point[1], point[2], point[0], ha="center", va="center",
bbox = dict(boxstyle=f"circle,pad={point[3]}", fc="lightgrey"))
plt.show()
I don't know in how far this is desired.
I guess usually you would rather create a scatterplot at the same positions as the text
import numpy as np
import matplotlib.pyplot as plt
points = [["A", 0.2, 0.25, 100], # 5 pt radius
["long label", 0.4, 0.25, 100], # 5 pt radius
["label1", 0.6, 0.25, 1600]] # 20 pt radius
data = np.array([l[1:] for l in points])
plt.scatter(data[:,0], data[:,1], s=data[:,2], facecolor="gold")
for point in points:
plt.text(point[1], point[2], point[0], ha="center", va="center")
plt.show()

How to plot a tissot with cartopy and matplotlib?

For plotting skymaps I just switched from Basemap to cartopy, I like it a lot more
.
(The main reason was segfaulting of Basemap on some computers, which I could not fix).
The only thing I struggle with, is getting a tissot circle (used to show the view cone of our telescope.)
This is some example code plotting random stars (I use a catalogue for the real thing):
import matplotlib.pyplot as plt
from cartopy import crs
import numpy as np
# create some random stars:
n_stars = 100
azimuth = np.random.uniform(0, 360, n_stars)
altitude = np.random.uniform(75, 90, n_stars)
brightness = np.random.normal(8, 2, n_stars)
fig = plt.figure()
ax = fig.add_subplot(1,1,1, projection=crs.NorthPolarStereo())
ax.background_patch.set_facecolor('black')
ax.set_extent([-180, 180, 75, 90], crs.PlateCarree())
plot = ax.scatter(
azimuth,
altitude,
c=brightness,
s=0.5*(-brightness + brightness.max())**2,
transform=crs.PlateCarree(),
cmap='gray_r',
)
plt.show()
How would I add a tissot circle with a certain radius in degrees to that image?
https://en.wikipedia.org/wiki/Tissot%27s_indicatrix
I keep meaning to go back and add the two functions from GeographicLib which provide the forward and inverse geodesic calculations, with this it is simply a matter of computing a geodetic circle by sampling at appropriate azimuths for a given lat/lon/radius. Alas, I haven't yet done that, but there is a fairly primitive (but effective) wrapper in pyproj for the functionality.
To implement a tissot indicatrix then, the code might look something like:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy as np
from pyproj import Geod
import shapely.geometry as sgeom
def circle(geod, lon, lat, radius, n_samples=360):
"""
Return the coordinates of a geodetic circle of a given
radius about a lon/lat point.
Radius is in meters in the geodetic's coordinate system.
"""
lons, lats, back_azim = geod.fwd(np.repeat(lon, n_samples),
np.repeat(lat, n_samples),
np.linspace(360, 0, n_samples),
np.repeat(radius, n_samples),
radians=False,
)
return lons, lats
def main():
ax = plt.axes(projection=ccrs.Robinson())
ax.coastlines()
geod = Geod(ellps='WGS84')
radius_km = 500
n_samples = 80
geoms = []
for lat in np.linspace(-80, 80, 10):
for lon in np.linspace(-180, 180, 7, endpoint=False):
lons, lats = circle(geod, lon, lat, radius_km * 1e3, n_samples)
geoms.append(sgeom.Polygon(zip(lons, lats)))
ax.add_geometries(geoms, ccrs.Geodetic(), facecolor='blue', alpha=0.7)
plt.show()
if __name__ == '__main__':
main()

Straight line through the pole with Cartopy stereographic projection

I'm using cartopy to produce a map of the Arctic with stereographic projection and then plotting a line (to show the position of a cross-section) over the top. If I use the following code then the line doesn't go in a straight line through the pole but instead goes along a line of latitude.
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
x=[180,0]
y=[50,50]
ax = plt.axes(projection=ccrs.NorthPolarStereo())
ax.set_extent([0, 360, 50, 90], crs=ccrs.PlateCarree())
ax.plot(x,y,transform=ccrs.PlateCarree())
plt.gca().stock_img()
plt.gca().coastlines()
plt.show()
To get round this I have to change x and y to:
x=[180,180,0,0]
y=[50,90,90,50]
so that there are two data points at the North Pole. Is there a better solution for this?
Edit: Image attached
Thanks,
Tim
#ajdawson's answer is correct. Using the Geodetic transform, in this case, will do the trick.
To understand the reason the line wasn't as you expected it to look, we need to understand what the PlateCarree transform represents.
Firstly, lets observe that all lines drawn in the transform=<projection> form, using Cartopy, should pass through the same Geographic points irrespective of the projection that the line is being drawn on.
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
def main():
x=[180, 180, 0, 0]
y=[50, 90, 90, 50]
# plot2 - North Polar Stereographic
ax = plt.subplot(211, projection=ccrs.NorthPolarStereo())
ax.set_extent([0, 360, 50, 90], crs=ccrs.PlateCarree())
ax.plot(x, y, transform=ccrs.PlateCarree(), color='red', lw=2)
ax.stock_img()
ax.coastlines()
# plot2 - PlateCarree
ax = plt.subplot(212, projection=ccrs.PlateCarree(central_longitude=45))
ax.set_extent([0, 360, -45, 90], crs=ccrs.PlateCarree())
ax.plot(x, y, transform=ccrs.PlateCarree(), color='red', lw=2)
ax.stock_img()
ax.coastlines()
plt.show()
if __name__ == '__main__':
main()
So going back to drawing your original coordinates (which were in PlateCarree coordinates) on a PlateCarree map:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
def main():
x=[180, 0]
y=[50, 50]
ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=45))
ax.set_extent([0, 360, -45, 90], crs=ccrs.PlateCarree())
ax.plot(x, y, transform=ccrs.PlateCarree(), color='red', lw=2)
ax.stock_img()
ax.coastlines()
plt.tight_layout()
plt.show()
if __name__ == '__main__':
main()
You will find that the line passes through the same geographic points as your bad line in the original question.
This should satisfy you that Cartopy is behaving rationally and it is not a bug, but it doesn't answer the question about how you would go about drawing the line you desire.
#ajdawson has already said that, in your case, drawing the line:
plt.plot([180, 0], [50, 50] , transform=ccrs.Geodetic())
will result in the desired output.
That is because the Geodetic coordinate reference system draws the line of shortest distance on the globe between two points. However, there will be a latitude which, when crossed, passing through the north pole does not provide the shortest distance:
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
def main():
ax = plt.axes(projection=ccrs.PlateCarree(central_longitude=45))
ax.set_global()
ax.plot([180, 0], [20, 20], transform=ccrs.Geodetic(), color='red', lw=2, label='Latitude = 20')
ax.plot([180, 0], [0, 0], transform=ccrs.Geodetic(), color='blue', lw=2, label='Latitude = 0')
ax.plot([180, 0], [-20, -20], transform=ccrs.Geodetic(), color='yellow', lw=2, label='Latitude = -20')
ax.outline_patch.set_zorder(2)
plt.legend(loc=8, bbox_to_anchor=(0.65, -0.2), shadow=True, fancybox=True)
ax.stock_img()
ax.coastlines()
plt.tight_layout()
plt.show()
if __name__ == '__main__':
main()
Generally, if you wanted to draw a Geodetic line which always crosses the North Pole, then the north pole should be one of the coordinates of the line.
plt.plot([180, 0, 0], [-45, 90, -45] , transform=ccrs.Geodetic())
Finally, just to throw it into the mix, if you just wanted a vertical line in a North Polar Stereographic projection which crosses the North Pole, it is worth remembering that there exists a Cartesian coordinate system (in which it is worth remembering that the numbers are not latitude and longitudes), so simply doing:
ax = plt.axes(projection=ccrs.NorthPolarStereo())
plt.axvline()
Will also do the trick! (but is less transferable than the Geodetic approach)
Wow, my answer got long. I hope your still with me and that makes the whole PlateCarree thing clearer!
I think you need to use the Geodetic transform when plotting this section rather than Plate Carree:
<!-- language: lang-py -->
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
x=[180,0]
y=[50,50]
ax = plt.axes(projection=ccrs.NorthPolarStereo())
ax.set_extent([0, 360, 50, 90], crs=ccrs.PlateCarree())
ax.plot(x,y,transform=ccrs.Geodetic())
ax.stock_img()
ax.coastlines()
plt.show()
The result looks like this:
I think that is the correct way to handle this anyway!
Andrew

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..