Problems with pcolormesh in Cartopy - python

I'm trying to adapt the Cartopy example plot for circular South Polar Stereographic plots to the North Pole and add data to it. I have a couple questions.
First, in the example code, the land feature is added before the ocean feature. When I did that, I got a map with only ocean. I reversed the order of the call in the code below and get a map with land and ocean. Why did the other order work with the South Polar example?
Second, and more importantly, I can't figure out why my pcolormesh call isn't having any effect.
I'm using Python 2.7.7, matplotlib 1.5.1, and Cartopy 0.15.1.
import matplotlib.path as mpath
import matplotlib.pyplot as plt
import numpy as np
import cartopy.crs as ccrs
import cartopy.feature
lats = np.linspace(60,90,30)
lons = np.linspace(0,360,200)
X,Y = np.meshgrid(lons,lats)
Z = np.random.normal(size = X.shape)
def main():
fig = plt.figure(figsize=[10, 5])
ax = plt.subplot(1, 1, 1, projection=ccrs.NorthPolarStereo())
fig.subplots_adjust(bottom=0.05, top=0.95,
left=0.04, right=0.95, wspace=0.02)
# Limit the map to -60 degrees latitude and below.
ax.set_extent([-180, 180, 60, 60], ccrs.PlateCarree())
ax.gridlines()
ax.add_feature(cartopy.feature.OCEAN)
ax.add_feature(cartopy.feature.LAND)
# Compute a circle in axes coordinates, which we can use as a boundary
# for the map. We can pan/zoom as much as we like - the boundary will be
# permanently circular.
theta = np.linspace(0, 2*np.pi, 100)
center, radius = [0.5, 0.5], 0.5
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
circle = mpath.Path(verts * radius + center)
ax.set_boundary(circle, transform=ax.transAxes)
ax.pcolormesh(X,Y,Z,transform=ccrs.PlateCarree())
plt.show()
if __name__ == '__main__':
main()

Your code leaves cartopy to dictate the order of feature plots on the map, as a result, some features can be hidden with no clues. It is possible to specify the order of plots explicitly.
The order of features plot is controlled by zorder, which can be specified with zorder=integer in most plotting statements. Here is a modified code that produces a better plot.
# your data
lats = np.linspace(60, 90, 30)
lons = np.linspace(0, 360, 160)
X,Y = np.meshgrid(lons, lats)
Z = np.random.normal(size = X.shape)
# new data for pcolormesh plot
latz = np.linspace(75, 90, 15)
lonz = np.linspace(0, 360, 160)
X1,Y1 = np.meshgrid(lonz, latz)
Z1 = np.random.normal(size = X1.shape)
def main():
fig = plt.figure(figsize=[10, 10])
ax = plt.subplot(1, 1, 1, projection=ccrs.NorthPolarStereo())
fig.subplots_adjust(bottom=0.05, top=0.95,
left=0.04, right=0.95, wspace=0.02)
# Limit the map to -60 degrees latitude and below.
ax.set_extent([-180, 180, 60, 60], ccrs.PlateCarree())
ax.gridlines()
# zorder can be used to arrange what is on top
ax.add_feature(cartopy.feature.LAND, zorder=4) # land is specified to plot above ...
ax.add_feature(cartopy.feature.OCEAN, zorder=1) # ... the ocean
# Compute a circle in axes coordinates, which we can use as a boundary
# for the map. We can pan/zoom as much as we like - the boundary will be
# permanently circular.
theta = np.linspace(0, 2*np.pi, 100)
center, radius = [0.5, 0.5], 0.5
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
circle = mpath.Path(verts * radius + center)
ax.set_boundary(circle, transform=ax.transAxes)
# pcolormesh is specified to plot on top of the ocean but below land
ax.pcolormesh(X1, Y1, Z1, transform=ccrs.PlateCarree(), zorder=3)
plt.show()
if __name__ == '__main__':
main()

Related

Placement of latitude labels in cartopy with polar stereographic projection

I am trying to figure out how to change the placement of gridline labels (more specifically, latitude labels) in cartopy when using a polar stereographic projection (NorthPolarStereo). My axis currently looks like this:
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import numpy as np
import cartopy.crs as ccrs
# Helper function
# from https://nordicesmhub.github.io/NEGI-Abisko-2019/training/example_NorthPolarStereo_projection.html
def polarCentral_set_latlim(lat_lims, ax):
ax.set_extent([-180, 180, lat_lims[0], lat_lims[1]], ccrs.PlateCarree())
theta = np.linspace(0, 2*np.pi, 100)
center, radius = [0.5, 0.5], 0.5
verts = np.vstack([np.sin(theta), np.cos(theta)]).T
circle = mpath.Path(verts * radius + center)
ax.set_boundary(circle, transform=ax.transAxes)
fig = plt.figure(figsize=(6,6))
ax = fig.add_subplot(1,1,1,projection=ccrs.NorthPolarStereo(central_longitude=0))
ax.coastlines(linewidth=0.5,color='k')
ax.gridlines(color='C7',lw=1,ls=':',draw_labels=True,rotate_labels=False,ylocs=[60,70,80])
polarCentral_set_latlim((50,90),ax)
Oddly, the latitude labels are always plotted at about 150E even if the central_longitude is set to a different value. Preferably, I'd like to align them with the 180th meridian (similar to the labels in this plot) but I cannot find an option in the documentation of the gridlines function to set their position. Did I overlook something or would I have to place them manually with plt.text()
After the gridlines is created, some labels can be accessed and moved to new positions.
I use alternate method to define the circular boundary of the plot in order not to interfere with gridlines' labels.
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import matplotlib.path as mpath
import numpy as np
# Value for r_extent is obtained by trial and error
# get it with `ax.get_ylim()` after running this code
r_extent = 4651194.319
r_extent *= 1.005 #increase a bit for better result
# Projection settings
lonlat_proj = ccrs.PlateCarree()
use_proj = ccrs.NorthPolarStereo(central_longitude=0)
fig = plt.figure(figsize=[7, 7])
ax = plt.subplot(1, 1, 1, projection=use_proj)
ax.set_extent([-180, 180, 50, 90], lonlat_proj)
#ax.stock_img() # add bluemarble image
ax.coastlines(lw=0.5, color="black", zorder=20) # add coastlines
# draw graticule (meridian and parallel lines)
gls = ax.gridlines(draw_labels=True, crs=lonlat_proj, lw=1, color="gray",
y_inline=True, xlocs=range(-180,180,30), ylocs=range(0,90,10))
# set the plot limits
ax.set_xlim(-r_extent, r_extent)
ax.set_ylim(-r_extent, r_extent)
# Prep circular boundary
circle_path = mpath.Path.unit_circle()
circle_path = mpath.Path(circle_path.vertices.copy() * r_extent,
circle_path.codes.copy())
#set circular boundary
#this method will not interfere with the gridlines' labels
ax.set_boundary(circle_path)
ax.set_frame_on(False) #hide the boundary frame
plt.draw() # Enable the use of `gl._labels`
# Reposition the tick labels
# Labels at 150d meridian will be moved to 180d
for ea in gls._labels:
# No _labels if not run `plt.draw()`
pos = ea[2].get_position()
#print("Position:", pos[0], pos[1])
if (pos[0]==150):
ea[2].set_position([180, pos[1]])
plt.show()
Might because of the cartopy version (0.21.1) I used but the the answer by #swatchai didn't work for me. Yet a slight change can be done and will work fine.
plt.draw()
for ea in gls.label_artists:
# return ea of mpl.text type, e.g. Text(135, 30, '30°N')
pos = ea.get_position()
if pos[0] == 135:
ea.set_position([180, pos[1]])

How to mask data that appears in the ocean using cartopy and matplotlib

Not at all sure what I'm doing wrong besides perhaps the order that I am plotting the ocean in. I am trying to get the ocean feature in to mask the data in the ocean. I am trying to get data to not appear in the ocean and to get the ax.add_feature(cfeature.OCEAN) to be on top of the temperature data I am plotting so I see ocean and no data. Similar to what is happening in the great lakes region where you see lakes and no temperature data.
proj_map = ccrs.Mercator(central_longitude=cLon)
proj_data = ccrs.PlateCarree()
fig = plt.figure(figsize=(30,20))
ax = fig.add_subplot(1,1,1, projection=proj_map)
ax.set_extent([-84,-66,37,47.5])
CT = ax.contourf(Tlat, Tlon, tempF, transform=temp.metpy.cartopy_crs, levels=clevs,
cmap=cmap)
ax.add_feature(cfeature.COASTLINE.with_scale('10m'), linewidth=0.5)
ax.add_feature(cfeature.OCEAN)
ax.add_feature(cfeature.LAKES)
ax.add_feature(cfeature.BORDERS, linewidth=0.5)
ax.add_feature(cfeature.STATES.with_scale('10m'), linewidth=0.5)
ax.add_feature(USCOUNTIES.with_scale('20m'), linewidth=0.25)
cbar = fig.colorbar(CT, orientation='horizontal', shrink=0.5, pad=0.05)
cbar.ax.tick_params(labelsize=14)
cbar.set_ticks([-50, -40, -30, -20, -10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100,
110, 120])
cbar.ax.set_xlabel("Temp ($^\circ$F)",fontsize=20)
Here is what the image looks like
You need to use zorder option to specify proper orders of the plot on the map. Features with largers values of zorder will be plotted on top of those with lower values. In your case, you need zorder of the OCEAN larger than the filled-contour.
Here is a runnable demo code and its sample plot. Read comments in the code for explanation.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import numpy as np
fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(projection=ccrs.PlateCarree()))
extent = [-84, -66, 37, 47.5]
# generate (x, y), centered at the middle of the `extent`
mean = [(extent[0]+extent[1])/2, (extent[2]+extent[3])/2] #mean
cov = [[7, 3.5], [3.5, 6]] #co-variance matrix
x, y = np.random.multivariate_normal(mean, cov, 4000).T
# make a 2D histogram
# set the edges of the bins in x and y directions
bin_size = 40
lonrange = np.linspace(extent[0], extent[1], bin_size)
latrange = np.linspace(extent[2], extent[3], bin_size)
# the cell sizes of the bins:
dx = (lonrange[1]- lonrange[0])/2
dy = (latrange[3]- latrange[2])/2
# compute array of center points of the bins' grid
# the dimensions of mesh-grid < the edges by 1
lonrange2 = np.linspace(extent[0]+dx, extent[1]-dx, bin_size-1)
latrange2 = np.linspace(extent[2]+dy, extent[3]-dy, bin_size-1)
x2d, y2d = np.meshgrid(lonrange2, latrange2)
# create 2d-histogram
# zorder is set = 10
h = ax.hist2d(x, y, bins=[lonrange, latrange], zorder=10, alpha=0.75)
#h: (counts, xedges, yedges, image)
ax.add_feature(cfeature.OCEAN, zorder=12) #zorder > 10
ax.add_feature(cfeature.BORDERS, linewidth=0.5)
ax.gridlines(draw_labels=True, xlocs=list(range(-85, -60, 5)), ylocs=list(range(35, 50, 5)),
linewidth=1.8, color='gray', linestyle='--', alpha=0.8, zorder=20)
# plot colorbar, using image from hist2d's result
plt.colorbar(h[3], ax=ax, shrink=0.45)
# finally, show the plot.
plt.show()
The output plot:
If zorder option is not specified:
ax.add_feature(cfeature.OCEAN)
the plot will be:

Griddata and Contourf produce artifacts with increasing steps/levels

I am using SciPy Griddata to interpolate data in its Cartesian form and then plot these data using contourf with a polar projection. When the Cartesian interpolated data is plotted with contourf there are no artifacts. However, when the projection is polar, artifacts develop with increasing "levels".
The artifacts are polygons or rays that form near regions of steep gradients. The code below plots the brightness of the sky with the moon. With graphlevels of "12" there isn't an issue. Artifacts develop with graphlevel of "25." My desired level is 80 or more - which shows terrible artifacts. The below is example real data from one night. These artifacts always occur. See images with Levels = 12 and Levels = 80
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import griddata
gridsize =150
graphlevels =12
plt.figure(figsize=(12,10))
ax = plt.subplot(111,projection='polar')
x = [72.90,68.00,59.14,44.38,29.63,63.94,59.68,51.92,38.98,26.03,47.34,44.20,38.46,28.89,19.31,23.40,20.40,15.34,10.28,-0.18,-0.14,-0.09,-0.04,0.02,-25.39,-23.66,-20.57,-15.40,-10.23,-47.56,-44.34,-38.54,-28.89,-19.22,-64.01,-59.68,-51.89,-38.90,-25.90,-72.77,-67.84,-58.98,-44.21,-29.44,-72.75,-67.83,-58.96,-44.18,-29.41,-59.63,-51.82,-38.83,-25.84,-47.42,-44.20,-38.40,-28.76,-19.12,-23.40,-20.32,-15.19,-10.08,0.27,0.25,0.23,0.20,23.92,20.80,15.63,10.46,47.93,44.67,38.86,29.17,19.48,64.40,60.03,52.20,39.18,26.15,73.08,68.12,59.26,44.47,29.68,-4.81]
y = [12.93,12.01,10.38,7.67,4.99,37.03,34.49,29.93,22.33,14.77,56.60,52.75,45.82,34.26,22.72,64.60,56.14,42.02,27.90,73.66,68.67,59.68,44.68,29.68,69.12,64.45,56.00,41.92,27.84,56.26,52.45,45.56,34.08,22.61,36.59,34.11,29.61,22.11,14.62,12.48,11.62,10.04,7.43,4.83,-13.33,-12.31,-10.78,-8.21,-5.58,-34.84,-30.36,-22.87,-15.36,-57.04,-53.20,-46.31,-34.83,-23.34,-65.20,-56.72,-42.62,-28.53,-69.33,-60.31,-45.31,-30.31,-65.09,-56.63,-42.55,-28.47,-56.81,-52.99,-46.13,-34.69,-23.23,-36.99,-34.53,-30.08,-22.66,-15.22,-12.73,-11.93,-10.44,-7.94,-5.40,-1.22,]
skybrightness = [19.26,19.31,19.21,19.65,19.40,19.26,19.23,19.43,19.57,19.52,19.19,19.31,19.33,19.68,19.50,19.29,19.45,19.50,19.23,18.98,19.28,19.46,19.54,19.22,19.03,19.18,19.35,19.37,19.08,18.99,18.98,19.26,19.36,19.08,18.79,18.85,19.13,19.17,19.05,18.51,18.64,18.88,18.92,18.93,18.12,18.34,18.72,18.82,18.74,18.22,18.46,18.76,18.26,18.13,18.24,18.46,18.58,17.30,18.38,18.08,18.24,17.68,18.34,18.46,18.65,18.23,18.70,18.52,18.79,18.83,18.18,18.51,19.01,19.08,19.08,18.99,19.02,19.07,19.20,19.27,19.06,19.01,19.28,19.46,19.30,18.94]
xgrid = np.linspace(min(x), max(x),gridsize)
ygrid = np.linspace(min(y), max(y),gridsize)
xgrid, ygrid = np.meshgrid(xgrid, ygrid, indexing='ij')
nsb_grid = griddata((x,y),skybrightness,(xgrid, ygrid), method='linear')
r = np.sqrt(xgrid**2 + ygrid**2)
theta = np.arctan2(ygrid, xgrid)
plt.rc('ytick', labelsize=16)
ax.set_facecolor('#eeddcc')
colors = plt.cm.get_cmap('RdYlBu')
levels,steps = np.linspace(min(skybrightness), max(skybrightness)+0.3,graphlevels, retstep=True)
ticks = np.linspace(min(skybrightness), max(skybrightness)+0.3,12)
cax = ax.contourf(theta, r, nsb_grid, levels=levels, cmap=colors)
cbar = plt.colorbar(cax, fraction=0.046, pad=0.04, ticks=ticks)
cbar.set_label(r'mag/arcsec$^2$')
ax.set_theta_zero_location('N')
ax.set_theta_direction(-1)
ax.set_rmax(75)
ax.set_yticks(range(10, 80, 20))
ax.set_xticklabels([r'N', r'NE', r'E', r'SE', r'S', r'SW', r'W', r'NW'])
ax.grid(alpha=0.3)
plt.savefig('StackOverflowHELP.png')
I am going to leave my question and this answer on StackOverflow... because I did get an answer from the developers of Matploblib. The problem is Contourf . In its attempt to project data in polar dimensions there are overlaps and extensions of polygons at the cyclic boundaries that cause problems. The only way to avoid this is to add points at the boundary. To quote the developer:
The workaround is a lot of effort and has to be tuned to each particular problem, so is a very long way from being ideal. We (Matplotlib) should do better in these situations. Inserting extra points into the triangulation isn't the right approach, we should instead correct the lines/polygons that traverse the discontinuity to provide a general solution.
See https://github.com/matplotlib/matplotlib/issues/20060 for the full discussion
The answer I settled on is to interpolate and render the result in Cartesian space. Then I format an empty polar plot with axes and labels to overlay on the top... and get on with my life!
import numpy as np
import matplotlib.pyplot as plt
from scipy.interpolate import griddata
gridsize =150
graphlevels = 200
fig = plt.figure(figsize=(12,10))
ax = fig.add_subplot(111, aspect='equal')
pax = fig.add_subplot(111,projection='polar')
pax.set_facecolor('none')
ax.set_axis_off()
ax.set_xlim([-75,75])
ax.set_ylim([-75,75])
x = [72.90,68.00,59.14,44.38,29.63,63.94,59.68,51.92,38.98,26.03,47.34,44.20,38.46,28.89,19.31,23.40,20.40,15.34,10.28,-0.18,-0.14,-0.09,-0.04,0.02,-25.39,-23.66,-20.57,-15.40,-10.23,-47.56,-44.34,-38.54,-28.89,-19.22,-64.01,-59.68,-51.89,-38.90,-25.90,-72.77,-67.84,-58.98,-44.21,-29.44,-72.75,-67.83,-58.96,-44.18,-29.41,-59.63,-51.82,-38.83,-25.84,-47.42,-44.20,-38.40,-28.76,-19.12,-23.40,-20.32,-15.19,-10.08,0.27,0.25,0.23,0.20,23.92,20.80,15.63,10.46,47.93,44.67,38.86,29.17,19.48,64.40,60.03,52.20,39.18,26.15,73.08,68.12,59.26,44.47,29.68,-4.81]
y = [12.93,12.01,10.38,7.67,4.99,37.03,34.49,29.93,22.33,14.77,56.60,52.75,45.82,34.26,22.72,64.60,56.14,42.02,27.90,73.66,68.67,59.68,44.68,29.68,69.12,64.45,56.00,41.92,27.84,56.26,52.45,45.56,34.08,22.61,36.59,34.11,29.61,22.11,14.62,12.48,11.62,10.04,7.43,4.83,-13.33,-12.31,-10.78,-8.21,-5.58,-34.84,-30.36,-22.87,-15.36,-57.04,-53.20,-46.31,-34.83,-23.34,-65.20,-56.72,-42.62,-28.53,-69.33,-60.31,-45.31,-30.31,-65.09,-56.63,-42.55,-28.47,-56.81,-52.99,-46.13,-34.69,-23.23,-36.99,-34.53,-30.08,-22.66,-15.22,-12.73,-11.93,-10.44,-7.94,-5.40,-1.22,]
skybrightness = [19.26,19.31,19.21,19.65,19.40,19.26,19.23,19.43,19.57,19.52,19.19,19.31,19.33,19.68,19.50,19.29,19.45,19.50,19.23,18.98,19.28,19.46,19.54,19.22,19.03,19.18,19.35,19.37,19.08,18.99,18.98,19.26,19.36,19.08,18.79,18.85,19.13,19.17,19.05,18.51,18.64,18.88,18.92,18.93,18.12,18.34,18.72,18.82,18.74,18.22,18.46,18.76,18.26,18.13,18.24,18.46,18.58,17.30,18.38,18.08,18.24,17.68,18.34,18.46,18.65,18.23,18.70,18.52,18.79,18.83,18.18,18.51,19.01,19.08,19.08,18.99,19.02,19.07,19.20,19.27,19.06,19.01,19.28,19.46,19.30,18.94]
xgrid = np.linspace(min(x), max(x),gridsize)
ygrid = np.linspace(min(y), max(y),gridsize)
xgrid, ygrid = np.meshgrid(xgrid, ygrid, indexing='ij')
nsb_grid = griddata((x,y),skybrightness,(xgrid, ygrid), method='linear')
plt.rc('ytick', labelsize=16) #colorbar font
colors = plt.cm.get_cmap('RdYlBu')
levels,steps = np.linspace(min(skybrightness), max(skybrightness)+0.3,graphlevels, retstep=True)
ticks = np.linspace(min(skybrightness), max(skybrightness)+0.3,12)
cax = ax.contourf(xgrid, ygrid, nsb_grid, levels=levels, cmap=colors)
cbar = plt.colorbar(cax, fraction=0.046, pad=0.04, ticks=ticks)
cbar.set_label(r'mag/arcsec$^2$')
pax.set_theta_zero_location('N')
pax.set_theta_direction(-1)
pax.set_rmax(75)
pax.set_yticks(range(10, 80, 20))
pax.set_xticklabels([r'N', r'NE', r'E', r'SE', r'S', r'SW', r'W', r'NW'])
pax.grid(alpha=0.3)

Aligning data (contourf) on Basemap

I've started working with Basemap, which seems potentially very useful.
If I plot some global data on a latitude/longitude grid as filled contours, it works great: Iff I leave the lat_0 and lon_0 as zero. Once I change the center location, the map moves but the data doesn't. I would be grateful for advice.
I've created a simple version of the code I'm using, with some simple sample data that illustrates the problem. The values should be (are) large at the equator but small at the poles. If you run the code with lat_0 and lon_0 = 0, it works fine. But if you change the center location to a different coordinate, the same pattern/data is presented even though the map has moved.
from mpl_toolkits.basemap import Basemap, cm
import matplotlib.pyplot as plt
import numpy as np
# create data
lat = np.linspace(-90,90,num=180)
lon = np.linspace(-180,180,num=361)
h2o_north = np.linspace(1,65,num=90)
h2o_south = np.flipud(h2o_north)
h2o = np.append(h2o_north,h2o_south)
data = np.transpose(np.tile(h2o,(len(lon),1)))
# create figure and axes instances
fig = plt.figure(figsize=(10,10))
ax = fig.add_axes([0.1,0.1,0.8,0.8])
# create map
m = Basemap(projection='ortho',lon_0=-50,lat_0=50,resolution='l')
# draw coastlines and country boundaries
m.drawcoastlines()
m.drawcountries()
# draw parallels
parallels = np.arange(-90.,90,10.)
m.drawparallels(parallels)
# draw meridians
meridians = np.arange(180.,360.,10.)
m.drawmeridians(meridians)
ny = data.shape[0]
nx = data.shape[1]
lons, lats = m.makegrid(nx, ny) # get lat/lons of ny by nx evenly space grid
x, y = m(lons, lats) # compute map projection coordinates
# draw filled contours.
clevs = np.linspace(0,70,num=281)
cs = m.contourf(x,y,data,clevs,cmap=plt.cm.jet)
# colorbar
cbar = m.colorbar(cs,location='bottom',pad="5%",ticks=np.linspace(0,70,15))
cbar.set_label('Scale of the data')
plt.title('Some global data', fontsize=14)
Use np.meshgrid() to create the meshgrid of lon-lat, then, convert it to projection coordinates, and the data are ready to generate contours and plot.
Here is the working code:
from mpl_toolkits.basemap import Basemap
import matplotlib.pyplot as plt
import numpy as np
# data for z (2D array)
h2o_north = np.linspace(1, 65, num=90)
h2o_south = np.flipud(h2o_north)
h2o = np.append(h2o_north, h2o_south)
data = np.transpose(np.tile(h2o, (len(h2o_north), 1)))
# create figure and axes instances
fig = plt.figure(figsize=(8, 8))
ax = fig.add_subplot()
# create basemap instance
m = Basemap(projection='ortho', lon_0=-50, lat_0=50, resolution='c', ax=ax)
# create meshgrid covering the whole globe with ...
# conforming dimensions of the `data`
lat = np.linspace(-90, 90, data.shape[0])
lon = np.linspace(-180, 180, data.shape[1])
xs, ys = np.meshgrid(lon, lat) # basic mesh in lon, lat (degrees)
x, y = m(xs, ys) # convert (lon,lat) to map (x,y)
# draw filled contours
clevs = np.linspace(0, np.max(data), 60)
cs = m.contourf(x, y, data, clevs, cmap=plt.cm.jet)
m.drawcoastlines()
m.drawcountries()
m.drawmeridians(range(-180, 180, 30))
m.drawparallels(range(-90, 90, 30))
# draw colorbar
cbar = m.colorbar(cs, location='bottom', pad="5%", ticks=np.linspace(0, np.max(data), 5))
cbar.set_label('Scale of the data')
plt.show()
The resulting plot:

Produce a RA vs DEC equatorial coordinates plot with python

I'm trying to generate an equatorial coordinates plot that should look more or less like this one:
(The figure is taken from this article, and it shows the position of the Large and Small MCs in equatorial coordinates)
Important things to notice about this plot:
The theta axis (ie: the right ascension) is in h:m:s (hours, minutes, seconds) as it is accustomed in astronomy, rather than in degrees as the default polar option does in matplotlib.
The r axis (ie: the declination) increases outward from -90º and the grid is centered in (0h, -90º).
The plot is clipped, meaning only a portion of it shows as opposed to the entire circle (as matplotlib does by default).
Using the polar=True option in matplotlib, the closest plot I've managed to produce is this (MWE below, data file here; some points are not present compared to the image above since the data file is a bit smaller):
I also need to add a third column of data to the plot, which is why I add a colorbar and color each point accordingly to a z array:
So what I mostly need right now is a way to clip the plot. Based mostly on this question and this example #cphlewis came quite close with his answer, but several things are still missing (mentioned in his answer).
Any help and/or pointers with this issue will be greatly appreciated.
MWE
(Notice I use gridspec to position the subplot because I need to generate several of these in the same output image file)
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
def skip_comments(f):
'''
Read lines that DO NOT start with a # symbol.
'''
for line in f:
if not line.strip().startswith('#'):
yield line
def get_data_bb():
'''RA, DEC data file.
'''
# Path to data file.
out_file = 'bb_cat.dat'
# Read data file
with open(out_file) as f:
ra, dec = [], []
for line in skip_comments(f):
ra.append(float(line.split()[0]))
dec.append(float(line.split()[1]))
return ra, dec
# Read RA, DEC data from file.
ra, dec = get_data_bb()
# Convert RA from decimal degrees to radians.
ra = [x / 180.0 * 3.141593 for x in ra]
# Make plot.
fig = plt.figure(figsize=(20, 20))
gs = gridspec.GridSpec(4, 2)
# Position plot in figure using gridspec.
ax = plt.subplot(gs[0], polar=True)
ax.set_ylim(-90, -55)
# Set x,y ticks
angs = np.array([330., 345., 0., 15., 30., 45., 60., 75., 90., 105., 120.])
plt.xticks(angs * np.pi / 180., fontsize=8)
plt.yticks(np.arange(-80, -59, 10), fontsize=8)
ax.set_rlabel_position(120)
ax.set_xticklabels(['$22^h$', '$23^h$', '$0^h$', '$1^h$', '$2^h$', '$3^h$',
'$4^h$', '$5^h$', '$6^h$', '$7^h$', '$8^h$'], fontsize=10)
ax.set_yticklabels(['$-80^{\circ}$', '$-70^{\circ}$', '$-60^{\circ}$'],
fontsize=10)
# Plot points.
ax.scatter(ra, dec, marker='o', c='k', s=1, lw=0.)
# Use this block to generate colored points with a colorbar.
#cm = plt.cm.get_cmap('RdYlBu_r')
#z = np.random.random((len(ra), 1)) # RGB values
#SC = ax.scatter(ra, dec, marker='o', c=z, s=10, lw=0., cmap=cm)
# Colorbar
#cbar = plt.colorbar(SC, shrink=1., pad=0.05)
#cbar.ax.tick_params(labelsize=8)
#cbar.set_label('colorbar', fontsize=8)
# Output png file.
fig.tight_layout()
plt.savefig(ra_dec_plot.png', dpi=300)
Getting the colorbar can be done with a merging of the OP code with #cphlewis's excellent answer. I've posted this as a turnkey solution on the request of the OP in chat. The first version of code simply adds a color bar, the final version (under EDIT 2) does an axes affine translation and corrects a few parameters / simplifies the code to suit OP spec exactly.
"""
An experimental support for curvilinear grid.
"""
import numpy as np
import mpl_toolkits.axisartist.angle_helper as angle_helper
import matplotlib.cm as cmap
from matplotlib.projections import PolarAxes
from matplotlib.transforms import Affine2D
from mpl_toolkits.axisartist import SubplotHost
from mpl_toolkits.axisartist import GridHelperCurveLinear
def curvelinear_test2(fig):
"""
polar projection, but in a rectangular box.
"""
global ax1
# see demo_curvelinear_grid.py for details
tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
extreme_finder = angle_helper.ExtremeFinderCycle(10, 60,
lon_cycle = 360,
lat_cycle = None,
lon_minmax = None,
lat_minmax = (0, np.inf),
)
grid_locator1 = angle_helper.LocatorHMS(12) #changes theta gridline count
tick_formatter1 = angle_helper.FormatterHMS()
grid_locator2 = angle_helper.LocatorDMS(6)
tick_formatter2 = angle_helper.FormatterDMS()
grid_helper = GridHelperCurveLinear(tr,
extreme_finder=extreme_finder,
grid_locator1=grid_locator1,
tick_formatter1=tick_formatter1,
grid_locator2=grid_locator2,
tick_formatter2=tick_formatter2
)
ax1 = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
# make ticklabels of right and top axis visible.
ax1.axis["right"].major_ticklabels.set_visible(True)
ax1.axis["top"].major_ticklabels.set_visible(True)
ax1.axis["bottom"].major_ticklabels.set_visible(True) #Turn off?
# let right and bottom axis show ticklabels for 1st coordinate (angle)
ax1.axis["right"].get_helper().nth_coord_ticks=0
ax1.axis["bottom"].get_helper().nth_coord_ticks=0
fig.add_subplot(ax1)
grid_helper = ax1.get_grid_helper()
ax1.set_aspect(1.)
ax1.set_xlim(-4,15) # moves the origin left-right in ax1
ax1.set_ylim(-3, 20) # moves the origin up-down
ax1.set_ylabel('90$^\circ$ + Declination')
ax1.set_xlabel('Ascension')
ax1.grid(True)
#ax1.grid(linestyle='--', which='x') # either keyword applies to both
#ax1.grid(linestyle=':', which='y') # sets of gridlines
return tr
import matplotlib.pyplot as plt
fig = plt.figure(1, figsize=(5, 5))
fig.clf()
tr = curvelinear_test2(fig) # tr.transform_point((x, 0)) is always (0,0)
# => (theta, r) in but (r, theta) out...
r_test = [0, 1.2, 2.8, 3.8, 5, 8, 10, 13.3, 17] # distance from origin
deg_test = [0, -7, 12, 28, 45, 70, 79, 90, 100] # degrees ascension
out_test = tr.transform(zip(deg_test, r_test))
sizes = [40, 30, 10, 30, 80, 33, 12, 48, 45]
#hues = [.9, .3, .2, .8, .6, .1, .4, .5,.7] # Oddly, floats-to-colormap worked for a while.
hues = np.random.random((9,3)) #RGB values
# Use this block to generate colored points with a colorbar.
cm = plt.cm.get_cmap('RdYlBu_r')
z = np.random.random((len(r_test), 1)) # RGB values
SC = ax1.scatter(out_test[:,0], #ax1 is a global
out_test[:,1],
s=sizes,
c=z,
cmap=cm,
zorder=9) #on top of gridlines
# Colorbar
cbar = plt.colorbar(SC, shrink=1., pad=0.05)
cbar.ax.tick_params(labelsize=8)
cbar.set_label('colorbar', fontsize=8)
plt.show()
EDIT
Bit of tidying parameters, adding in OP data, removing redundancy yields the following plot. Still need to centre the data on -90 instead of 0 - at the moment this is hacked, but I'm sure curvelinear_test2() can be changed to account for it...
EDIT 2
Following OP comment on intermediate version in this answer, a final version as below gives the plot at the very end of the post - with -90 on the dec axis and subplot demo
"""
An experimental support for curvilinear grid.
"""
import numpy as np
import mpl_toolkits.axisartist.angle_helper as angle_helper
import matplotlib.cm as cmap
from matplotlib.projections import PolarAxes
from matplotlib.transforms import Affine2D
from mpl_toolkits.axisartist import SubplotHost
from mpl_toolkits.axisartist import GridHelperCurveLinear
def curvelinear_test2(fig, rect=111):
"""
polar projection, but in a rectangular box.
"""
# see demo_curvelinear_grid.py for details
tr = Affine2D().translate(0,90) + Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
extreme_finder = angle_helper.ExtremeFinderCycle(10, 60,
lon_cycle = 360,
lat_cycle = None,
lon_minmax = None,
lat_minmax = (-90, np.inf),
)
grid_locator1 = angle_helper.LocatorHMS(12) #changes theta gridline count
tick_formatter1 = angle_helper.FormatterHMS()
grid_helper = GridHelperCurveLinear(tr,
extreme_finder=extreme_finder,
grid_locator1=grid_locator1,
tick_formatter1=tick_formatter1
)
ax1 = SubplotHost(fig, rect, grid_helper=grid_helper)
# make ticklabels of right and top axis visible.
ax1.axis["right"].major_ticklabels.set_visible(True)
ax1.axis["top"].major_ticklabels.set_visible(True)
ax1.axis["bottom"].major_ticklabels.set_visible(True) #Turn off?
# let right and bottom axis show ticklabels for 1st coordinate (angle)
ax1.axis["right"].get_helper().nth_coord_ticks=0
ax1.axis["bottom"].get_helper().nth_coord_ticks=0
fig.add_subplot(ax1)
grid_helper = ax1.get_grid_helper()
# You may or may not need these - they set the view window explicitly rather than using the
# default as determined by matplotlib with extreme finder.
ax1.set_aspect(1.)
ax1.set_xlim(-4,25) # moves the origin left-right in ax1
ax1.set_ylim(-3, 30) # moves the origin up-down
ax1.set_ylabel('Declination')
ax1.set_xlabel('Ascension')
ax1.grid(True)
#ax1.grid(linestyle='--', which='x') # either keyword applies to both
#ax1.grid(linestyle=':', which='y') # sets of gridlines
return ax1,tr
def skip_comments(f):
'''
Read lines that DO NOT start with a # symbol.
'''
for line in f:
if not line.strip().startswith('#'):
yield line
def get_data_bb():
'''RA, DEC data file.
'''
# Path to data file.
out_file = 'bb_cat.dat'
# Read data file
with open(out_file) as f:
ra, dec = [], []
for line in skip_comments(f):
ra.append(float(line.split()[0]))
dec.append(float(line.split()[1]))
return ra, dec
import matplotlib.pyplot as plt
fig = plt.figure(1, figsize=(5, 5))
fig.clf()
ax1, tr = curvelinear_test2(fig,121) # tr.transform_point((x, 0)) is always (0,0)
# => (theta, r) in but (r, theta) out...
# Read RA, DEC data from file.
ra, dec = get_data_bb()
out_test = tr.transform(zip(ra, dec))
# Use this block to generate colored points with a colorbar.
cm = plt.cm.get_cmap('RdYlBu_r')
z = np.random.random((len(ra), 1)) # RGB values
SC = ax1.scatter(out_test[:,0], #ax1 is a global
out_test[:,1],
marker = 'o',
c=z,
cmap=cm,
lw = 0.,
zorder=9) #on top of gridlines
# Colorbar
cbar = plt.colorbar(SC, shrink=1., pad=0.1)
cbar.ax.tick_params(labelsize=8)
cbar.set_label('colorbar', fontsize=8)
ax2, tr = curvelinear_test2(fig,122) # tr.transform_point((x, 0)) is always (0,0)
# => (theta, r) in but (r, theta) out...
# Read RA, DEC data from file.
ra, dec = get_data_bb()
out_test = tr.transform(zip(ra, dec))
# Use this block to generate colored points with a colorbar.
cm = plt.cm.get_cmap('RdYlBu_r')
z = np.random.random((len(ra), 1)) # RGB values
SC = ax2.scatter(out_test[:,0], #ax1 is a global
out_test[:,1],
marker = 'o',
c=z,
cmap=cm,
lw = 0.,
zorder=9) #on top of gridlines
# Colorbar
cbar = plt.colorbar(SC, shrink=1., pad=0.1)
cbar.ax.tick_params(labelsize=8)
cbar.set_label('colorbar', fontsize=8)
plt.show()
Final plot:
Chewing on the AxisArtist example is actually pretty promising (this combines two AxisArtist examples -- I wouldn't be surprised if AxisArtist was written with RA plots in mind):
Still to do:
Declination should run from -90 at the origin to 0
Be able to use
and add a colorbar
adjust limits if plotting outside them
aesthetic:
Serif font in axis labels
Dashed gridlines for ascension
anything else?
"""
An experimental support for curvilinear grid.
"""
import numpy as np
import mpl_toolkits.axisartist.angle_helper as angle_helper
import matplotlib.cm as cmap
from matplotlib.projections import PolarAxes
from matplotlib.transforms import Affine2D
from mpl_toolkits.axisartist import SubplotHost
from mpl_toolkits.axisartist import GridHelperCurveLinear
def curvelinear_test2(fig):
"""
polar projection, but in a rectangular box.
"""
global ax1
# see demo_curvelinear_grid.py for details
tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
extreme_finder = angle_helper.ExtremeFinderCycle(10, 60,
lon_cycle = 360,
lat_cycle = None,
lon_minmax = None,
lat_minmax = (0, np.inf),
)
grid_locator1 = angle_helper.LocatorHMS(12) #changes theta gridline count
tick_formatter1 = angle_helper.FormatterHMS()
grid_locator2 = angle_helper.LocatorDMS(6)
tick_formatter2 = angle_helper.FormatterDMS()
grid_helper = GridHelperCurveLinear(tr,
extreme_finder=extreme_finder,
grid_locator1=grid_locator1,
tick_formatter1=tick_formatter1,
grid_locator2=grid_locator2,
tick_formatter2=tick_formatter2
)
ax1 = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
# make ticklabels of right and top axis visible.
ax1.axis["right"].major_ticklabels.set_visible(True)
ax1.axis["top"].major_ticklabels.set_visible(True)
ax1.axis["bottom"].major_ticklabels.set_visible(True) #Turn off?
# let right and bottom axis show ticklabels for 1st coordinate (angle)
ax1.axis["right"].get_helper().nth_coord_ticks=0
ax1.axis["bottom"].get_helper().nth_coord_ticks=0
fig.add_subplot(ax1)
grid_helper = ax1.get_grid_helper()
ax1.set_aspect(1.)
ax1.set_xlim(-4,15) # moves the origin left-right in ax1
ax1.set_ylim(-3, 20) # moves the origin up-down
ax1.set_ylabel('90$^\circ$ + Declination')
ax1.set_xlabel('Ascension')
ax1.grid(True)
#ax1.grid(linestyle='--', which='x') # either keyword applies to both
#ax1.grid(linestyle=':', which='y') # sets of gridlines
return tr
import matplotlib.pyplot as plt
fig = plt.figure(1, figsize=(5, 5))
fig.clf()
tr = curvelinear_test2(fig) # tr.transform_point((x, 0)) is always (0,0)
# => (theta, r) in but (r, theta) out...
r_test = [0, 1.2, 2.8, 3.8, 5, 8, 10, 13.3, 17] # distance from origin
deg_test = [0, -7, 12, 28, 45, 70, 79, 90, 100] # degrees ascension
out_test = tr.transform(zip(deg_test, r_test))
sizes = [40, 30, 10, 30, 80, 33, 12, 48, 45]
#hues = [.9, .3, .2, .8, .6, .1, .4, .5,.7] # Oddly, floats-to-colormap worked for a while.
hues = np.random.random((9,3)) #RGB values
ax1.scatter(out_test[:,0], #ax1 is a global
out_test[:,1],
s=sizes,
c=hues,
#cmap=cmap.RdYlBu_r,
zorder=9) #on top of gridlines
plt.show()
I think it may be a problem with Python 3+ , now the line
out_test = tr.transform(zip(deg_test, r_test))
returns the error:
ValueError: cannot reshape array of size 1 into shape (2)
Changing the line to
out_test = tr.transform(list(zip(deg_test, r_test)))
fixes the problem and allows the plot to be generated correctly.

Categories

Resources