Limiting latitudinal extend of a cartopy orthographic projection - python

I am trying to plot a map of a sphere with an orthographic projection of the Northern (0-40N) and Southern (0-40S) hemispheres, and a Mollweide projection of the central latitudes (60N-60S). I get the following plot:
which shows a problem: there is a square bounding box with cut corners around the hemispherical plots. Note that the extent of the colours is the same for all three plots (-90 to 90).
When I plot a hemisphere without limiting its extent, however, I get a round bounding box, as expected from an orthographic projection:
Using plt.xlim(-90,-50) results in a vertical stripe, and plt.ylim(-90,-50) in a horizontal stripe, so that is no solution either.
How can I limit the latitudinal extent of my orthographic projection, whilst maintaining the circular bounding box?
The code to produce above graphs:
import numpy as np
from matplotlib import pyplot as plt
import cartopy.crs as ccrs
# Create dummy data, latitude from -90(S) to 90 (N), lon from -180 to 180
theta, phi = np.meshgrid(np.arange(0,180),np.arange(0,360));
theta = -1*(theta.ravel()-90)
phi = phi.ravel()-180
radii = theta
# Make masks for hemispheres and central
mask_central = np.abs(theta) < 60
mask_north = theta > 40
mask_south = theta < -40
data_crs= ccrs.PlateCarree() # Data CRS
# Grab map projections for various plots
map_proj = ccrs.Mollweide(central_longitude=0)
map_proj_N = ccrs.Orthographic(central_longitude=0, central_latitude=90)
map_proj_S = ccrs.Orthographic(central_longitude=0, central_latitude=-90)
fig = plt.figure()
ax1 = fig.add_subplot(2, 1, 2,projection=map_proj)
im1 = ax1.scatter(phi[mask_central],
theta[mask_central],
c = radii[mask_central],
transform=data_crs,
vmin = -90,
vmax = 90,
)
ax1.set_title('Central latitudes')
ax_N = fig.add_subplot(2, 2, 1, projection=map_proj_N)
ax_N.scatter(phi[mask_north],
theta[mask_north],
c = radii[mask_north],
transform=data_crs,
vmin = -90,
vmax = 90,
)
ax_N.set_title('Northern hemisphere')
ax_S = fig.add_subplot(2, 2, 2, projection=map_proj_S)
ax_S.scatter(phi[mask_south],
theta[mask_south],
c = radii[mask_south],
transform=data_crs,
vmin = -90,
vmax = 90,
)
ax_S.set_title('Southern hemisphere')
fig = plt.figure()
ax = fig.add_subplot(111,projection = map_proj_N)
ax.scatter(phi,
theta,
c = radii,
transform=data_crs,
vmin = -90,
vmax = 90,
)
ax.set_title('Northern hemisphere')
plt.show()

(1). In all of your plots, when you use scatter(), the size of the scatter points should be defined with proper s=value, otherwise the default value is used. I use s=0.2 and the resulting plots look better.
(2). For 'Central latitudes' case, you need to specify correct y-limits with set_ylim(). This involves the computation of them. The use of transform_point() is applied here.
(3). For the remaining plots that require elimination of unneeded features, proper circular clip paths can be used. Their perimeters are also used to plot as map boundaries in both cases. Their existence may cause trouble plotting other map features (such as coastlines) as I demonstrate with the code and its output.
# original is modified and extended
import numpy as np
from matplotlib import pyplot as plt
import cartopy.crs as ccrs
import matplotlib.patches as mpatches # need it to create clip-path
# Create dummy data, latitude from -90(S) to 90 (N), lon from -180 to 180
theta, phi = np.meshgrid(np.arange(0,180),np.arange(0,360));
theta = -1*(theta.ravel()-90)
phi = phi.ravel()-180
radii = theta
# Make masks for hemispheres and central
mask_central = np.abs(theta) < 60
mask_north = theta > 40
mask_south = theta < -40
data_crs= ccrs.PlateCarree() # Data CRS
# Grab map projections for various plots
map_proj = ccrs.Mollweide(central_longitude=0)
map_proj_N = ccrs.Orthographic(central_longitude=0, central_latitude=90)
map_proj_S = ccrs.Orthographic(central_longitude=0, central_latitude=-90)
# 'Central latitudes' plot
fig = plt.figure()
ax1 = fig.add_subplot(2, 1, 2, projection=map_proj)
# Note: Limits of plot depends on plotting data, but not exact!
im1 = ax1.scatter(phi[mask_central],
theta[mask_central],
s = 0.2,
c = radii[mask_central],
transform=data_crs,
vmin = -90,
vmax = 90,
)
# compute y limits
_, y_btm = map_proj.transform_point(0, -60, ccrs.Geodetic())
_, y_top = map_proj.transform_point(0, 60, ccrs.Geodetic())
# apply y limits
ax1.set_ylim(y_btm, y_top)
ax1.coastlines(color='k', lw=0.35)
ax1.set_title('Central latitudes')
ax_N = fig.add_subplot(2, 2, 1, projection=map_proj_N)
ax_N.scatter(phi[mask_north],
theta[mask_north],
s = 0.1, # not mandatory
c = radii[mask_north],
transform=data_crs,
vmin = -90,
vmax = 90,
)
# use a circular path as map boundary
clip_circle = mpatches.Circle(xy=[0,0], radius=4950000, facecolor='none', edgecolor='k')
ax_N.add_patch(clip_circle)
ax_N.set_boundary(clip_circle.get_path(), transform=None, use_as_clip_path=True)
# with `use_as_clip_path=True` the coastlines do not appear
ax_N.coastlines(color='k', lw=0.75, zorder=13) # not plotted!
ax_N.set_title('Northern hemisphere1')
# 'Southern hemisphere' plot
ax_S = fig.add_subplot(2, 2, 2, projection=map_proj_S)
ax_S.scatter(phi[mask_south],
theta[mask_south],
s = 0.02,
c = radii[mask_south],
transform=data_crs,
vmin = -90,
vmax = 90,
)
clip_circle = mpatches.Circle(xy=[0,0], radius=4950000, facecolor='none', edgecolor='k')
ax_S.add_patch(clip_circle)
# applying the clip-circle as boundary, but not use as clip-path
ax_S.set_boundary(clip_circle.get_path(), transform=None, use_as_clip_path=False)
# with `use_as_clip_path=False` the coastlines is plotted, but goes beyond clip-path
ax_S.coastlines(color='k', lw=0.75, zorder=13)
ax_S.set_title('Southern hemisphere')
# 'Northern hemisphere2' plot, has nice circular limit
fig = plt.figure()
ax = fig.add_subplot(111,projection = map_proj_N)
ax.scatter(phi,
theta,
s = 0.2,
c = radii,
transform=data_crs,
vmin = -90,
vmax = 90,
)
ax.coastlines(color='k', lw=0.5, zorder=13)
ax.set_title('Northern hemisphere2')
ax.set_global()
plt.show()
The output plot:

The usual axes in matplotlib are rectangular. For some projections in cartopy however, it does not make sense to show a rectangle where part of it isn't even defined. Those regions are encircled. This way it is ensured that the axes content always stays within the border.
If you do not want this, but instead use a circular border, even if part of the plot would potentially lie outside the circle, you would define that circle manually:
import numpy as np
from matplotlib import pyplot as plt
import cartopy.crs as ccrs
# Create dummy data, latitude from -90(S) to 90 (N), lon from -180 to 180
theta, phi = np.meshgrid(np.arange(0,180),np.arange(0,360));
theta = -1*(theta.ravel()-90)
phi = phi.ravel()-180
# Make mask for hemisphere
mask_north = theta > 40
data_crs= ccrs.PlateCarree() # Data CRS
# Grab map projections for various plots
map_proj_N = ccrs.Orthographic(central_longitude=0, central_latitude=90)
fig = plt.figure()
ax_N = fig.add_subplot(121, projection=map_proj_N)
ax_N.scatter(phi[mask_north], theta[mask_north],
c = theta[mask_north], transform=data_crs,
vmin = -90, vmax = 90)
ax_N.set_title('Northern hemisphere')
### Remove undesired patch
ax_N.patches[0].remove()
### Create new circle around the axes:
circ = plt.Circle((.5,.5), .5, edgecolor="k", facecolor="none",
transform=ax_N.transAxes, clip_on=False)
ax_N.add_patch(circ)
#### For comparisson, plot the full data in the right subplot:
ax = fig.add_subplot(122,projection = map_proj_N)
ax.scatter(phi, theta, c = theta,
transform=data_crs, vmin = -90, vmax = 90)
ax.set_title('Northern hemisphere')
plt.show()

Related

polar pcolormesh plot projected onto cartopy map

To simplify, as much as possible, a question I already asked, how would you OVERLAY or PROJECT a polar plot onto a cartopy map.
phis = np.linspace(1e-5,10,10) # SV half cone ang, measured up from nadir
thetas = np.linspace(0,2*np.pi,361)# SV azimuth, 0 coincides with the vel vector
X,Y = np.meshgrid(thetas,phis)
Z = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X)
fig, ax = plt.subplots(figsize=(4,4),subplot_kw=dict(projection='polar'))
im = ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto')
ax.set_theta_direction(-1)
ax.set_theta_offset(np.pi / 2.0)
ax.grid(True)
that results in
Over a cartopy map like this...
flatMap = ccrs.PlateCarree()
resolution = '110m'
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto')
gc.collect()
I'd like to project this polar plot over an arbitrary lon/lat... I can convert the polar theta/phi into lon/lat, but lon/lat coords (used on the map) are more 'cartesian like' than polar, hence you cannot just substitute lon/lat for theta/phi ... This is a conceptual problem. How would you tackle it?
Firstly, the data must be prepared/transformed into certain projection coordinates for use as input. And the instruction/option of the data's CRS must be specified correctly when used in the plot statement.
In your specific case, you need to transform your data into (long,lat) values.
XX = X/np.pi*180 # wrap around data in EW direction
YY = Y*9 # spread across N hemisphere
And plot it with an instruction transform=ccrs.PlateCarree().
ax.pcolormesh(XX,YY,Z, cmap=mpl.cm.jet_r,shading='auto',
transform=ccrs.PlateCarree())
The same (XX,YY,Z) data set can be plotted on orthographic projection.
Edit1
Update of the code and plots.
Part 1 (Data)
import matplotlib.colors
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import numpy as np
import matplotlib.pyplot as mpl
import cartopy.feature as cfeature
#
# Part 1
#
phis = np.linspace(1e-5,10,10) # SV half cone ang, measured up from nadir
thetas = np.linspace(0,2*np.pi,361)# SV azimuth, 0 coincides with the vel vector
X,Y = np.meshgrid(thetas,phis)
Z = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X)
fig, ax = plt.subplots(figsize=(4,4),subplot_kw=dict(projection='polar'))
im = ax.pcolormesh(X,Y,Z, cmap=mpl.cm.jet_r,shading='auto')
ax.set_theta_direction(-1)
ax.set_theta_offset(np.pi / 2.0)
ax.grid(True)
Part 2 The required code and output.
#
# Part 2
#
flatMap = ccrs.PlateCarree()
resolution = '110m'
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land',
resolution, edgecolor='black', alpha=0.7,
facecolor=cfeature.COLORS['land']))
ax.set_extent([-180, 180, -90, 90], crs=ccrs.PlateCarree())
def scale_position(lat_deg, lon_deg, rad_deg):
# Two operations:
# 1. manipulates X,Y data and get (XX,YY)
# 2. create proper projection of (XX,YY), `rotpole_proj`
# Returns: XX,YY,rotpole_proj
# For X data
XX = X/np.pi*180 #always wrap around EW direction
# For Y data
# The cone data: min=0, max=10 --> (90-rad),90
# rad_deg = radius of the display area
top = 90
btm = top-rad_deg
YY = btm + (Y/Y.max())*rad_deg
# The proper coordinate system
rotpole_proj = ccrs.RotatedPole(pole_latitude=lat_deg, pole_longitude=lon_deg)
# Finally,
return XX,YY,rotpole_proj
# Location 1 (Asia)
XX1, YY1, rotpole_proj1 = scale_position(20, 100, 20)
ax.pcolormesh(XX1, YY1, Z, cmap=mpl.cm.jet_r,
transform=rotpole_proj1)
# Location 2 (Europe)
XX2, YY2, rotpole_proj2 = scale_position(62, -6, 8)
ax.pcolormesh(XX2, YY2, Z, cmap=mpl.cm.jet_r,
transform=rotpole_proj2)
# Location 3 (N America)
XX3, YY3, rotpole_proj3 = scale_position(29, -75, 30)
ax.pcolormesh(XX3, YY3, Z, cmap=mpl.cm.jet_r,shading='auto',
transform=rotpole_proj3)
#gc.collect()
plt.show()
This solution does NOT account for the projection point being at some altitude above the globe... I can do that part, so I really have trouble mapping the meshgrid to lon/lat so the work with the PREVIOUSLY GENERATES values of Z.
Here's a simple mapping directly from polar to cart:
X_cart = np.array([[p*np.sin(t) for p in phis] for t in thetas]).T
Y_cart = np.array([[p*np.cos(t) for p in phis] for t in thetas]).T
# Need to map cartesian XY to Z that is compatbile with above...
Z_cart = np.sin(X)**10 + np.cos(10 + Y*X) * np.cos(X) # This Z does NOT map to cartesian X,Y
print(X_cart.shape,Y_cart.shape,Z_cart.shape)
flatMap = ccrs.PlateCarree()
resolution = '110m'
fig = plt.figure(figsize=(12,6), dpi=96)
ax = fig.add_subplot(111, projection=flatMap)
ax.imshow(np.tile(np.array([[cfeature.COLORS['water'] * 255]], dtype=np.uint8), [2, 2, 1]), origin='upper', transform=ccrs.PlateCarree(), extent=[-180, 180, -180, 180])
ax.add_feature(cfeature.NaturalEarthFeature('physical', 'land', resolution, edgecolor='black', facecolor=cfeature.COLORS['land']))
im = ax.pcolormesh(X_cart*2,Y_cart*2, Z_cart, cmap=mpl.cm.jet_r, shading='auto') # c=mapper.to_rgba(Z_cart), cmap=mpl.cm.jet_r)
gc.collect()
Which maps the polar plot center to lon/lat (0,0):
I'm close... I somehow need to move my cartesian coords to the proper lon/lat (the satellite sub-point) and then scale it appropriately. Have the set of lon/lat but I'm screwing up the meshgrid somehow... ???
The sphere_intersect() routine returns lon/lat for projection of theta/phi on the globe (that works)... The bit that doesn't work is building the meshgrid that replaces X,Y:
lons = np.array([orbits.sphere_intersect(SV_pos_vec, SV_vel_vec, az << u.deg, el << u.deg,
lonlat=True)[0] for az in thetas for el in phis], dtype='object')
lats = np.array([orbits.sphere_intersect(SV_pos_vec, SV_vel_vec, az << u.deg, el << u.deg,
lonlat=True)[1] for az in thetas for el in phis], dtype='object')
long, latg = np.meshgrid(lons,lats) # THIS IS A PROBLEM I BELIEVE...
and the pcolormesh makes a mess...

How nonlinearly re-normalize Matplotlib colorbar?

I am trying to obtain a 2D pseudocolor plot with a nonlinear color map (cmap). Independently I want to have a colobar that uses a similar cmap but differently scaled/stretched to avoid overlapping of the colorbar yticks.
The first one I can obtain using some nonlinear norm as an argument of pcolormesh.
But how to get the second part in an efficient way?
Finally, I was able to obtain the desired effect (see the bottom right corner in the below figure) but I am pretty sure that this is not the best/easiest/desired/Pythonic way of doing it.
Is there an easier way of obtaining such an effect?
Figure:
Here is the code that reproduces the above figure:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as colors
import matplotlib.cbook as cbook
from matplotlib import cm
import copy
def transform_cmap(cmap_basic,
new_cmap_name = None,
cmap_trans_fun = None,
how_many_levels = 256,
ticks = None,
ticks_trans_fun = None):
'''returns new cmap and new ticks locators for transformed ticks.
If ticks_trans_fun is None then ticks locators are linearly transformed
to the [0,1] interval, as a result, cmap is stretched, but when used with
colorbar than linearly spaced ticks are linearly spaced along the colorbar'''
# be sure that cmap is really a cmap
if not isinstance(cmap_basic,colors.Colormap):
try:
cmap_basic = cm.get_cmap(name=cmap_basic)
except:
print('basic_cmap is not a valid cmap or cmap name!')
if cmap_trans_fun is None:
cmap_trans_fun = colors.Normalize()
if new_cmap_name is None:
new_cmap_name = cmap_basic.name+'_new'
c_coords_linear = np.linspace(0,1, how_many_levels)
cmap_trans_fun_copy = copy.deepcopy(cmap_trans_fun)
# deppcopy to avoid overwritting the vmin, vmax values
cmap_trans_fun_copy.autoscale([0,1])
c_coords_after = cmap_trans_fun_copy(c_coords_linear)
c_list_after = cmap_basic(c_coords_after)
new_cmap = colors.LinearSegmentedColormap.from_list(new_cmap_name,
c_list_after,
N=how_many_levels)
if ticks_trans_fun is None:
ticks_trans_fun = colors.Normalize()
ticks_trans_fun_copy = copy.deepcopy(ticks_trans_fun)
ticks_trans_fun_copy.vmin = cmap_trans_fun.vmin
ticks_trans_fun_copy.vmax = cmap_trans_fun.vmax
new_ticks_locators = ticks_trans_fun_copy(ticks)
return new_cmap, new_ticks_locators
###########################################
# Prepare some data
# based on https://matplotlib.org/stable/gallery/userdemo/colormap_normalizations.html#sphx-glr-gallery-userdemo-colormap-normalizations-py
N = 100
X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]
# A low hump with a spike coming out of the top right. Needs to have
# z/colour axis on a log scale so we see both hump and spike. linear
# scale only shows the spike.
Z1 = np.exp(-X**2 - Y**2)
Z1b = 2*np.exp(-10*(X-0.5)**2 - 20*(Y-0.5)**2)
Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2)
Z = Z1 + 50 * Z2+ Z1b
Z = Z*10
# data prepared!
###########################################
cbar_ticks = [0,1,10,50,100,200,300,400]
# prepare basic 'linear' cmap from matplotlib avaliable cmap
cmap = 'inferno'
lin_cmap = cm.get_cmap(cmap)
############################################
# prepare nonlinear norm (good for data viz)
gamma = 0.1
nonlin_norm = colors.PowerNorm(gamma=gamma)
nonlin_norm.autoscale(Z)
# prepare nonlinear norm b (better for colorbar)
gamma_b = 0.40
nonlin_norm_b = colors.PowerNorm(gamma=gamma_b)
nonlin_norm.autoscale(Z)
################# PLOT ####################
# create 4 plots in 2x2 grid
fig, axs = plt.subplots(nrows = 2,
ncols = 2,
figsize=(6,5.5),
dpi=108,
squeeze = False)
fs = 8 # the same plot title fontsize
LLL_ax = axs[0,0]
LNL_ax = axs[0,1]
LNN_ax = axs[1,0]
LNNb_ax = axs[1,1]
#------------- Top left plot --------------
LLL_ax.set_title('linear cmap\nlinear norm\nlinear cbar',
fontsize = fs)
LLL_pcm = LLL_ax.pcolormesh(X, Y, Z,
cmap = lin_cmap,
shading='auto')
# colorbar takes LLL_pcm object to figure out colormap and scale
fig.colorbar(LLL_pcm,
ax=LLL_ax,
extend='both',
ticks=cbar_ticks)
#------------- Top right plot -------------
# an easy way of obtaining good color-scaling
# the colorbar shows cmap in a linear way
# the cbar yticks are nonlinearly scaled but
# they are overlapping
LNL_ax.set_title('linear cmap\nnonlinear norm\nlinear cbar cmap (nonlinear ticks)',
fontsize = fs)
nonlin_norm.autoscale(Z)
LNL_pcm = LNL_ax.pcolormesh(X, Y, Z,
cmap = lin_cmap,
norm = nonlin_norm,
shading='auto')
fig.colorbar(LNL_pcm,
ax=LNL_ax,
extend='both',
ticks=cbar_ticks)
#------------- Bottom left plot -----------
# the colorbar cmap is nonlinear
# the cbar yticks are linearly scaled but
# the overall effect is not good
# the cbar yticks are overlapping again
LNN_ax.set_title('linear cmap\nnonlinear norm\nnonlinear cbar cmap (linear ticks)',
fontsize = fs)
LNN_pcm = LNN_ax.pcolormesh(X, Y, Z,
cmap = lin_cmap,
norm = nonlin_norm,
shading='auto')
# create new, nonlinear cmap
nonlin_cmap, new_ticks_coords = transform_cmap(cmap_basic = lin_cmap ,
cmap_trans_fun = nonlin_norm,
how_many_levels = 256,
ticks = cbar_ticks,
ticks_trans_fun = None,
new_cmap_name = 'nonlinear_cmap')
# create object based on new cmap for colorbar
scalar_mappable = cm.ScalarMappable(cmap=nonlin_cmap)
LNN_cbar = fig.colorbar(scalar_mappable,
ax=LNN_ax,
extend='both',
ticks=new_ticks_coords)
# ticks are in correct places but they are normalized to [0,1] interval
# we need to overwrite them with desired labels
LNN_cbar.ax.set_yticklabels(cbar_ticks)
#------------- Bottom right plot ----------
# the colorbar shows cmap in a nonlinear way
# this is different nonlinear scaling than before (nonlin_norm_b)
# the cbar yticks are also nonlinearly scaled
# this is A GOOD LOOKING PLOT
LNNb_ax.set_title('linear cmap\nnonlinear norm\n2nd nonlinear cbar cmap (nonlinear ticks)',
fontsize = fs)
LNNb_pcm = LNNb_ax.pcolormesh(X, Y, Z,
cmap = lin_cmap,
norm = nonlin_norm,
shading='auto')
# this time as the cbar cmap is with different norm than data cmap
# we also need to recalculate positions of the cbar ticks using second norm
nonlin_cmap_b, new_ticks_coords_b = transform_cmap(cmap_basic = lin_cmap ,
cmap_trans_fun = nonlin_norm_b,
how_many_levels = 256,
ticks = cbar_ticks,
ticks_trans_fun = nonlin_norm_b,
new_cmap_name = 'nonlinear_cmap_v2')
scalar_mappable_b = cm.ScalarMappable(cmap=nonlin_cmap_b)
LNNb_cbar = fig.colorbar(scalar_mappable_b,
ax=LNNb_ax,
extend='both',
ticks=new_ticks_coords_b)
LNNb_cbar.ax.set_yticklabels(cbar_ticks)
#------------------------------
fig.tight_layout()
plt.show()
I was using this answer as a base:
Uniform tick labels for non-linear colorbar in Matplotlib
These answers may be useful but were looking too complicated:
Arbirtrary non-linear colorbar using Matplotlib
nonlinear colormap, matplotlib
I have a feeling that wiser usage of norm parameter in pcolor and perhaps in cbar should give me the desired result. Unfortunately, I was not able to obtain it in this way.

scatterplot and combined polar histogram in matplotlib

I am attempting to produce a plot like this which combines a cartesian scatter plot and a polar histogram. (Radial lines optional)
A similar solution (by Nicolas Legrand) exists for looking at differences in x and y (code here), but we need to look at ratios (i.e. x/y).
More specifically, this is useful when we want to look at the relative risk measure which is the ratio of two probabilities.
The scatter plot on it's own is obviously not a problem, but the polar histogram is more advanced.
The most promising lead I have found is this central example from the matplotlib gallery here
I have attempted to do this, but have run up against the limits of my matplotlib skills. Any efforts moving towards this goal would be great.
I'm sure that others will have better suggestions, but one method that gets something like you want (without the need for extra axes artists) is to use a polar projection with a scatter and bar chart together. Something like
import matplotlib.pyplot as plt
import numpy as np
x = np.random.uniform(size=100)
y = np.random.uniform(size=100)
r = np.sqrt(x**2 + y**2)
phi = np.arctan2(y, x)
h, b = np.histogram(phi, bins=np.linspace(0, np.pi/2, 21), density=True)
colors = plt.cm.Spectral(h / h.max())
ax = plt.subplot(111, projection='polar')
ax.scatter(phi, r, marker='.')
ax.bar(b[:-1], h, width=b[1:] - b[:-1],
align='edge', bottom=np.max(r) + 0.2, color=colors)
# Cut off at 90 degrees
ax.set_thetamax(90)
# Set the r grid to cover the scatter plot
ax.set_rgrids([0, 0.5, 1])
# Let's put a line at 1 assuming we want a ratio of some sort
ax.set_thetagrids([45], [1])
which will give
It is missing axes labels and some beautification, but it might be a place to start. I hope it is helpful.
You can use two axes on top of each other:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(6,6))
ax1 = fig.add_axes([0.1,0.1,.8,.8], label="cartesian")
ax2 = fig.add_axes([0.1,0.1,.8,.8], projection="polar", label="polar")
ax2.set_rorigin(-1)
ax2.set_thetamax(90)
plt.show()
Ok. Thanks to the answer from Nicolas, and the answer from tomjn I have a working solution :)
import numpy as np
import matplotlib.pyplot as plt
# Scatter data
n = 50
x = 0.3 + np.random.randn(n)*0.1
y = 0.4 + np.random.randn(n)*0.02
def radial_corner_plot(x, y, n_hist_bins=51):
"""Scatter plot with radial histogram of x/y ratios"""
# Axis setup
fig = plt.figure(figsize=(6,6))
ax1 = fig.add_axes([0.1,0.1,.6,.6], label="cartesian")
ax2 = fig.add_axes([0.1,0.1,.8,.8], projection="polar", label="polar")
ax2.set_rorigin(-20)
ax2.set_thetamax(90)
# define useful constant
offset_in_radians = np.pi/4
def rotate_hist_axis(ax):
"""rotate so that 0 degrees is pointing up and right"""
ax.set_theta_offset(offset_in_radians)
ax.set_thetamin(-45)
ax.set_thetamax(45)
return ax
# Convert scatter data to histogram data
r = np.sqrt(x**2 + y**2)
phi = np.arctan2(y, x)
h, b = np.histogram(phi,
bins=np.linspace(0, np.pi/2, n_hist_bins),
density=True)
# SCATTER PLOT -------------------------------------------------------
ax1.scatter(x,y)
ax1.set(xlim=[0, 1], ylim=[0, 1], xlabel="x", ylabel="y")
ax1.spines['right'].set_visible(False)
ax1.spines['top'].set_visible(False)
# HISTOGRAM ----------------------------------------------------------
ax2 = rotate_hist_axis(ax2)
# rotation of axis requires rotation in bin positions
b = b - offset_in_radians
# plot the histogram
bars = ax2.bar(b[:-1], h, width=b[1:] - b[:-1], align='edge')
def update_hist_ticks(ax, desired_ratios):
"""Update tick positions and corresponding tick labels"""
x = np.ones(len(desired_ratios))
y = 1/desired_ratios
phi = np.arctan2(y,x) - offset_in_radians
# define ticklabels
xticklabels = [str(round(float(label), 2)) for label in desired_ratios]
# apply updates
ax2.set(xticks=phi, xticklabels=xticklabels)
return ax
ax2 = update_hist_ticks(ax2, np.array([1/8, 1/4, 1/2, 1, 2, 4, 8]))
# just have radial grid lines
ax2.grid(which="major", axis="y")
# remove bin count labels
ax2.set_yticks([])
return (fig, [ax1, ax2])
fig, ax = radial_corner_plot(x, y)
Thanks for the pointers!

Matplotlib Circle patch does not have smooth edges

I'm trying to display Matplotlib patches using the Circle function on a map plot using cartopy geographical projections. Apparently this is supposed to give a smooth, near scale-free circular patch, however the edges are very polygonal. Strangely, CirclePolygon, the polygonal approximation counterpart of Circle, produces a smoother circle, albeit still not as smooth as I would like.
This is pretty much all the code as it pertains to adding the plot and the patches:
fig = plt.figure(figsize=(8,6))
img_extent = [340, 348, -35.5, -31]
ax = fig.add_subplot(1, 1, 1, projection = ccrs.Mollweide(), extent = img_extent)
patch_coords = [[342.5833, -34.5639],[343.4042, -34.3353],[343.8500, -33.8728],
[344.4917, -33.7636],[344.9250, -33.3108],[345.1333, -32.6811],
[344.9233, -32.1583]]
for pair in patch_coords:
ax.add_patch(mpatches.Circle(xy = pair, radius = 0.5,
color = 'r', alpha = 0.3, rasterized = None,
transform = ccrs.Geodetic()))
ax.scatter(ra1, dec1, transform = ccrs.Geodetic(), rasterized = True, s = 1,
marker = ".", c = 'g', label = 'z < 0.025')
ax.scatter(ra2, dec2, transform = ccrs.Geodetic(), rasterized = True, s = 2,
marker = ".", c = 'b', label = '0.25 < z < 0.034')
ax.scatter(ra3, dec3, transform = ccrs.Geodetic(), rasterized = True, s = 0.75,
marker = ".", c = 'grey', label = '0.034 < z < 0.05')
Which produces this
I've tried looking through the available arguments but none seem to fix it. Is there a reason why it comes out like this and is there any way to make it smoother?
I believe plotting Tissot's Indicatrices is more appropriate in your case. An Indicatrix represents a ground circle on a map projection. In many cases, the Indicatrices are rendered as ellipses as map projections do not always preserve shapes. The following is the working code that plots all the ground circles of radius = 55 km on the map projection that you desire. Read the comments in the code for some useful information.
import matplotlib.pyplot as plt
# import matplotlib.patches as mpatches
import cartopy.crs as ccrs
import numpy as np
fig = plt.figure(figsize=(12,8))
img_extent = [340, 348, -35.5, -31]
ax = fig.add_subplot(1, 1, 1, projection = ccrs.Mollweide(), extent = img_extent)
patch_coords = [[342.5833, -34.5639],[343.4042, -34.3353],[343.8500, -33.8728],
[344.4917, -33.7636],[344.9250, -33.3108],[345.1333, -32.6811],
[344.9233, -32.1583]]
for ix,pair in enumerate(patch_coords):
# plot tissot indicatrix at each location
# n_samples = number of points forming indicatrix' perimeter
# rad_km = 55 km. is about the angular distance 0.5 degree
ax.tissot(rad_km=55, lons=np.array(patch_coords)[:,0][ix], \
lats=np.array(patch_coords)[:,1][ix], n_samples=36, \
facecolor='red', edgecolor='black', linewidth=0.15, alpha = 0.3)
gl = ax.gridlines(draw_labels=False, linewidth=1, color='blue', alpha=0.3, linestyle='--')
plt.show()
The resulting plot:
Edit
Since the first version of the code is not optimal.
Code update is offered as follows:
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
fig = plt.figure(figsize=(12,8))
img_extent = [340, 348, -35.5, -31]
ax = fig.add_subplot(1, 1, 1, projection = ccrs.Mollweide(), extent = img_extent)
patch_coords = [[342.5833, -34.5639],[343.4042, -34.3353],[343.8500, -33.8728],
[344.4917, -33.7636],[344.9250, -33.3108],[345.1333, -32.6811],
[344.9233, -32.1583]]
for pair in patch_coords:
# plot tissot indicatrix at each location
# n_samples = number of points forming indicatrix' perimeter
# rad_km = 55 km. is about the angular distance 0.5 degree at equator
ax.tissot(rad_km=55, lons=pair[0], lats=pair[1], n_samples=36, \
facecolor='red', edgecolor='black', linewidth=0.15, alpha = 0.3)
gl = ax.gridlines(draw_labels=False, linewidth=1, color='blue', alpha=0.3, linestyle='--')
plt.show()
I believe that Cartopy does line projections with an arbitrary fixed accuracy, rather than a dynamic line-split calculation.
See e.g. :
https://github.com/SciTools/cartopy/issues/825
https://github.com/SciTools/cartopy/issues/363
I also think work is ongoing right now to address that.
In the meantime, to solve specific problems you can hack the CRS.threshold property,
as explained here : https://github.com/SciTools/cartopy/issues/8
That is, you can make it use finer steps by reprogramming the fixed value.
I think this would also fix this circle-drawing problem, though I'm not 100%

Plot square Cartopy map

I need to plot a square map using Cartopy. I currently use the following code for my map:
plt.figure(figsize = (15, 15))
img = cimgt.GoogleTiles()
ax = plt.axes(projection = img.crs)
ax.set_extent((d['longitude'].min() - 0.05, d['longitude'].max() + 0.05,
d['latitude'].min() - 0.05, d['latitude'].max() + 0.05))
ax.add_image(img, 10, interpolation = 'bicubic')
plt.scatter(d['longitude'], d['latitude'], transform = ccrs.PlateCarree(),
c = '#E8175D', s = 14)
This works fine, except for the fact that the map isn't square. Instead, it's just fitted into the (15, 15) square of the plot.
I would like to add a bit more map to the left and to the right to make the plot perfectly square without distorting it. Simply setting the extent to the same difference on latitude and longitude doesn't do the job, because latitude and longitude cover different distances in Google's (and most other) map projections. I also found this post, but from what I get, the intent here is to distort the map.
I hope someone has an idea how to do this. It seems that Cartopy is not very intuitive in this regard.
To get square extent you need to specify it in map projection coordinates. That involves some coordinate transformation. Here is the code snippet that you need.
# crs of your choice
crg = cimgt.StamenTerrain().crs # or cimgt.GoogleTiles().crs
# set map limits, in degrees
lonmin, lonmax = -22, -15
latmin, latmax = 63, 65
# do coordinate transformation
LL = crg.transform_point(lonmin, latmin, ccrs.Geodetic())
UR = crg.transform_point(lonmax, latmax, ccrs.Geodetic())
EW = UR[0] - LL[0]
SN = UR[1] - LL[1]
# get side of the square extent (in map units, usually meters)
side = max(EW, SN) # larger value is in effect
mid_x, mid_y = LL[0]+EW/2.0, LL[1]+SN/2.0 # center location
# the extent preserves the center location
extent = [mid_x-side/2.0, mid_x+side/2.0, mid_y-side/2.0, mid_y+side/2.0]
# this sets square extent
# crs=crg signifies that projection coordinates is used in extent
ax.set_extent(extent, crs=crg)
Hope it helps.
Edit
Here is a complete working code and its resulting map.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import cartopy.io.img_tiles as cimgt
def make_map(projection=ccrs.PlateCarree()):
fig, ax = plt.subplots(figsize=(10, 10),
subplot_kw=dict(projection=projection))
gl = ax.gridlines(draw_labels=True)
gl.xlabels_top = gl.ylabels_right = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
return fig, ax
request = cimgt.StamenTerrain() # very responsive
crg = request.crs #crs of the projection
fig, ax = make_map(projection = crg)
# specify map extent here
lonmin, lonmax = -22, -15
latmin, latmax = 63, 65
LL = crg.transform_point(lonmin, latmin, ccrs.Geodetic())
UR = crg.transform_point(lonmax, latmax, ccrs.Geodetic())
EW = UR[0] - LL[0]
SN = UR[1] - LL[1]
side = max(EW,SN)
mid_x, mid_y = LL[0]+EW/2.0, LL[1]+SN/2.0 #center location
extent = [mid_x-side/2.0, mid_x+side/2.0, mid_y-side/2.0, mid_y+side/2.0] # map coordinates, meters
ax.set_extent(extent, crs=crg)
ax.add_image(request, 8)
# add a marker at center of the map
plt.plot(mid_x, mid_y, marker='o', \
color='red', markersize=10, \
alpha=0.7, transform = crg)
plt.show()

Categories