Adapting coordinates in cartopy depending on the projection of a plot - python

In the following example, I am losing my point (i.e., I don't understand the change in coordinates) if I am using the ccrs.Mercator() projection instead of the ccrs.PlateCarree():
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
mypt = (6, 56)
ax0 = plt.subplot(221, projection=ccrs.PlateCarree()) # OK
ax1 = plt.subplot(222, projection=ccrs.Mercator()) # NOT OK
ax2 = plt.subplot(224, projection=ccrs.Mercator()) # NOT OK
def plotpt(ax, extent=(-15,15,46,62)):
ax.plot(mypt[0], mypt[1], 'r*', ms=20)
ax.set_extent(extent)
ax.coastlines(resolution='50m')
ax.gridlines(draw_labels=True)
plotpt(ax0)
plotpt(ax1)
plotpt(ax2, extent=(-89,89,-89,89))
plt.show()
It looks like the coordinates of my point go from (6,56) to (0,0)
What am I missing?
Why is the behaviour correct with ccrs.PlateCarree() and not with ccrs.Mercator()? Should I add any transform somewhere?
[EDIT with the solution]
My initial confusion came from the fact that projection applies to the plot, while transform applies to the data, meaning they should be set different when they do not share the same system - my first attempts with transform where wrong as in ax1 below, ax1bis is the solution.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
mypt = (6, 56)
ax0 = plt.subplot(221, projection=ccrs.PlateCarree())
ax1 = plt.subplot(222, projection=ccrs.Mercator())
ax1bis = plt.subplot(223, projection=ccrs.Mercator())
ax2 = plt.subplot(224, projection=ccrs.Mercator())
def plotpt(ax, extent=(-15,15,46,62), **kwargs):
ax.plot(mypt[0], mypt[1], 'r*', ms=20, **kwargs)
ax.set_extent(extent)
ax.coastlines(resolution='50m')
ax.gridlines(draw_labels=True)
ax.xaxis.set_ticks_position('bottom')
ax.yaxis.set_ticks_position('left')
plotpt(ax0) # correct because projection and data share the same system
plotpt(ax1, transform=ccrs.Mercator()) # WRONG
plotpt(ax1bis, transform=ccrs.PlateCarree()) # Correct, projection and transform are different!
plotpt(ax2, extent=(-89,89,-89,89), transform=ccrs.Mercator()) # WRONG
plt.show()

Yes, you should add the transform keyword to the plot call. You should also specify the coordinate system you want to set the extents in:
def plotpt(ax, extent=(-15,15,46,62)):
ax.plot(mypt[0], mypt[1], 'r*', ms=20, transform=ccrs.PlateCarree())
ax.set_extent(extent, crs=ccrs.PlateCarree())
ax.coastlines(resolution='50m')
ax.gridlines(draw_labels=True)
A basic guide on transforms and projections is now available in the cartopy documentation http://scitools.org.uk/cartopy/docs/latest/tutorials/understanding_transform.html. To avoid surprises, you should always specify a transform when plotting data on a map.

Related

Colorbar for sns.jointplot "kde"-style on the side

I'm trying to plot a colorbar next to my density plot with marginal axes.
It does plot the colorbar, but unfortunately not on the side.
That's what a tried so far:
sns.jointplot(x,y, data=df3, kind="kde", color="skyblue", legend=True, cbar=True,
xlim=[-10,40], ylim=[900,1040])
It looks like this:
I also tried this:
from matplotlib import pyplot as plt
import seaborn as sns
import numpy as np
kdeplot = sns.jointplot(x=tumg, y=pumg, kind="kde")
plt.subplots_adjust(left=0.2, right=0.8, top=0.8, bottom=0.2)
cbar_ax = kdeplot.fig.add_axes([.85, .25, .05, .4])
plt.colorbar(cax=cbar_ax)
plt.show()
But with the second option I'm getting a runtime error:
No mappable was found to use for colorbar creation.
First define a mappable such as an image (with imshow) or a contour set (with contourf).
Does anyone have an idea how to solve the problem?
There only seems to be information for a colorbar when effectively creating the colorbar.
So, an idea is to combine both approaches: add a colorbar via kdeplot, and then move it to the desired location. This will leave the main joint plot with insufficient width, so its width also should be adapted:
from matplotlib import pyplot as plt
import seaborn as sns
import numpy as np
# create some dummy data: gaussian multivariate with 10 centers with each 1000 points
tumg = np.random.normal(np.tile(np.random.uniform(10, 20, 10), 1000), 2)
pumg = np.random.normal(np.tile(np.random.uniform(10, 20, 10), 1000), 2)
kdeplot = sns.jointplot(x=tumg, y=pumg, kind="kde", cbar=True)
plt.subplots_adjust(left=0.1, right=0.8, top=0.9, bottom=0.1)
# get the current positions of the joint ax and the ax for the marginal x
pos_joint_ax = kdeplot.ax_joint.get_position()
pos_marg_x_ax = kdeplot.ax_marg_x.get_position()
# reposition the joint ax so it has the same width as the marginal x ax
kdeplot.ax_joint.set_position([pos_joint_ax.x0, pos_joint_ax.y0, pos_marg_x_ax.width, pos_joint_ax.height])
# reposition the colorbar using new x positions and y positions of the joint ax
kdeplot.fig.axes[-1].set_position([.83, pos_joint_ax.y0, .07, pos_joint_ax.height])
plt.show()

Getting display coordinated from cartopy generated image plot

I have need to plot point on the map, produce PNG image on that map and, output display coordinated of the plotted point.
Using cartopy I could get the map I wanted and plot a point in given lon/lat coordinates.
I cannot figure out how to get the pixel coordinates out. I tried to follow simple matplotlib tutorial https://matplotlib.org/users/transforms_tutorial.html But it does not work as expected in this situation
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
# Create Mercator projection with dateline in the middle:
from matplotlib import lines
fig = plt.figure(figsize=(10, 10))
ax = plt.axes(projection=ccrs.Mercator(central_longitude=26,))
ax.set_extent([19, 33, 59.5, 70.5], crs=ccrs.PlateCarree())
LAND = cfeature.NaturalEarthFeature('physical', 'land', '50m',
edgecolor='face',
facecolor=cfeature.COLORS['land'], zorder=-1)
ax.add_feature(LAND)
ax.coastlines(resolution='50m')
ax.add_feature(cfeature.NaturalEarthFeature('cultural', 'admin_0_boundary_lines_land',
'50m', edgecolor='black', facecolor='none'))
plt.plot([26.7042], [60.8679], color='blue', linewidth=2, marker='o',
transform=ccrs.PlateCarree(),
)
fig.canvas.draw()
# print image x y coordinates of point 60.8679° N, 26.7042° E here
plt.show()

How to use twinx with ax created with make_axes_locatable

I want to plot an image and colorbar with its associated histogram below. The two axes of the image and the histogram must have the same width.
Furthermore, the colorbar should be the same height as the image.
The part that is (and should not) be complicated is to superpose a plot of a cumulative histogram with the percentage of each bin in respect to the size of the data.
For the moment, I obtained something like this:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
data = np.random.normal(0,2,size=(100,100))
fig = plt.figure()
ax = fig.add_subplot(2,1,1)
im = ax.imshow(data,cmap="bone")
divider = make_axes_locatable(ax)
ax1 = divider.append_axes("right", size="5%", pad=0.05)
plt.colorbar(im,cax=ax1)
ax2 = divider.append_axes("bottom",size="100%",pad = 0.3)
n,bins,patches = ax2.hist(data.flatten(),bins=20)
ax3 = ax2.twinx()
ax3.plot(bins[:-1],np.cumsum(n*100/np.size(data)),lw=2)
plt.show()
Everything is going smoothly until I try to use twinx on ax2 (in order to plot my cumulative distribution on ax3 with a different y-scale). The resulting axis, instead of being with ax2, is wrapping all the axes of the figure.
I don't understand what is wrong and how I can fix this.
This is a hard one. The problem is that the axes_grid1 toolkit is designed to position the axes at the time of drawing. Apparently it draws the twin axis first and only after that relocates the axes according to the divider.
What makes things worse is that you want to have an axes with equal aspect ratio bound to an axes with unequal aspect, which makes it impossible to use AxisGrid.
While any two-fold combination of equal+unequal or equal+twin or unequal+twin would work in one way or the other, all three are just too much.
So the solution is probably to start from scratch, just putting the axes to the canvas and only at the very end reposition/resize them. This can be done using an event listener connected to a function which gets the position of the axes with equal aspect and resizes the other two axes accordingly.
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
data = np.random.normal(0,2,size=(100,100))
fig = plt.figure()
ax = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
im = ax.imshow(data,cmap="bone")
n,bins,patches = ax2.hist(data.flatten(),bins=20)
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size="5%", pad=0.05)
plt.colorbar(im, cax=cax)
ax3 = ax2.twinx()
ax3.plot(bins[:-1],np.cumsum(n*100/np.size(data)),lw=2, c=plt.cm.bone(0.4))
def resize(event):
axpos = ax.get_position()
axpos2 = ax2.get_position()
newpos = [axpos.x0, axpos2.y0, axpos.width, axpos2.height]
ax2.set_position(newpos)
ax3.set_position(newpos)
cid = fig.canvas.mpl_connect('draw_event', resize)
cid2 = fig.canvas.mpl_connect('resize_event', resize)
#if you want to save the figure, trigger the event manually
save=False
if save:
fig.canvas.draw()
resize()
plt.savefig(__file__+".png")
plt.show()

How do I change matplotlib's subplot projection of an existing axis?

I'm trying to construct a simple function that takes a subplot instance (matplotlib.axes._subplots.AxesSubplot) and transforms its projection to another projection, for example, to one of the cartopy.crs.CRS projections.
The idea looks something like this
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
def make_ax_map(ax, projection=ccrs.PlateCarree()):
# set ax projection to the specified projection
...
# other fancy formatting
ax2.coastlines()
...
# Create a grid of plots
fig, (ax1, ax2) = plt.subplots(ncols=2)
# the first subplot remains unchanged
ax1.plot(np.random.rand(10))
# the second one gets another projection
make_ax_map(ax2)
Of course, I can just use fig.add_subplot() function:
fig = plt.figure(figsize=(10,5))
ax1 = fig.add_subplot(121)
ax1.plot(np.random.rand(10))
ax2 = fig.add_subplot(122,projection=ccrs.PlateCarree())
ax2.coastlines()
but I was wondering if there is a proper matplotlib method to change a subplot axis projection after it was defined. Reading matplotlib API didn't help unfortunately.
You can't change the projection of an existing axes, the reason is given below. However the solution to your underlying problem is simply to use the subplot_kw argument to plt.subplots() described in the matplotlib documentation here. For example, if you wanted all your subplots to have the cartopy.crs.PlateCarree projection you could do
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
# Create a grid of plots
fig, (ax1, ax2) = plt.subplots(ncols=2, subplot_kw={'projection': ccrs.PlateCarree()})
Regarding the actual question, specifying a projection when you create an axes set determines the axes class you get, which is different for each projection type. For example
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
ax1 = plt.subplot(311)
ax2 = plt.subplot(312, projection='polar')
ax3 = plt.subplot(313, projection=ccrs.PlateCarree())
print(type(ax1))
print(type(ax2))
print(type(ax3))
This code will print the following
<class 'matplotlib.axes._subplots.AxesSubplot'>
<class 'matplotlib.axes._subplots.PolarAxesSubplot'>
<class 'cartopy.mpl.geoaxes.GeoAxesSubplot'>
Notice how each axes is actually an instance of a different class.
Assuming there are multiple axes being used for 2D plotting, like...
fig = matplotlib.pyplot.Figure()
axs = fig.subplots(3, 4) # prepare for multiple subplots
# (some plotting here)
axs[0,0].plot([1,2,3])
... one can simply destroy one of them and replace it with a new one having the 3D projection:
axs[2,3].remove()
ax = fig.add_subplot(3, 4, 12, projection='3d')
ax.plot_surface(...)
Just note that unlike rest of Python, the add_subplot uses row-column indexing starting from 1 (not from 0).
EDIT: Changed my typo about indexing.
following the answer to this question :
In python, how can I inherit and override a method on a class instance, assigning this new version to the same name as the old one?
I found a hack to change the projection of an axe after creating it which seems to work at least in the simple example below, but I have no idea if this solution is the best way
from matplotlib.axes import Axes
from matplotlib.projections import register_projection
class CustomAxe(Axes):
name = 'customaxe'
def plotko(self, x):
self.plot(x, 'ko')
self.set_title('CustomAxe')
register_projection(CustomAxe)
if __name__ == '__main__':
import matplotlib.pyplot as plt
fig = plt.figure()
## use this syntax to create a customaxe directly
# ax = fig.add_subplot(111, projection="customaxe")
## change the projection after creation
ax = plt.gca()
ax.__class__ = CustomAxe
ax.plotko(range(10))
plt.show()
You can use the following function, which removes the axis and generates the axis in the specified projection, similar to dominecf answer, with the advantage that the specific subplot parameters (row, col, and index) are retrieved automatically.
import matplotlib.pyplot as plt
def update_projection(ax, axi, projection='3d', fig=None):
if fig is None:
fig = plt.gcf()
rows, cols, start, stop = axi.get_subplotspec().get_geometry()
ax.flat[start].remove()
ax.flat[start] = fig.add_subplot(rows, cols, start+1, projection=projection)
and generate a plot with all available projections
import matplotlib.projections
import numpy as np
# test data
x = np.linspace(-np.pi, np.pi, 10)
# plot all projections available
projections = matplotlib.projections.get_projection_names()
fig, ax = plt.subplots(nrows=1, ncols=len(projections), figsize=[3.5*len(projections), 4], squeeze=False)
for i, pro_i in enumerate(projections):
update_projection(ax, ax.flat[i], pro_i)
ax.flat[i].set_title(pro_i)
try:
ax.flat[i].grid(True)
ax.flat[i].plot(x, x)
except Exception as a:
print(pro_i, a)
plt.tight_layout(pad=.5)

Adding Radial Axis Label in matplotlib

I am making a polar scatter plot for a college project with matplotlib and I can't find out how to add a label to the radial axis. Here is my code ( I left out the data because it was read out of a csv)
import matplotlib.pyplot as plt
ax = plt.subplot(111, polar=True)
ax.set_rmax(1)
c = plt.scatter(theta, radii)
ax.set_title("Spread of Abell Cluster Supernova Events as a Function of Fractional Radius", va='bottom')
ax.legend(['Supernova'])
plt.show()
(My plot looks like this. I can't seem to find any straight forward method to do it. Has anyone dealt with this before and have any suggestions?
I don't know of a built in way to do it, but you could use ax.text to make your own. You can get the position of the radial tick labels using ax.get_rlabel_position(), and the mid point of the radial axis using ax.get_rmax()/2.
For example, here's your code (with some random data):
import matplotlib.pyplot as plt
import numpy as np
theta=np.random.rand(40)*np.pi*2.
radii=np.random.rand(40)
ax = plt.subplot(111, polar=True)
ax.set_rmax(1)
c = plt.scatter(theta, radii)
ax.set_title("Spread of Abell Cluster Supernova Events as a Function of Fractional Radius", va='bottom')
ax.legend(['Supernova'])
label_position=ax.get_rlabel_position()
ax.text(np.radians(label_position+10),ax.get_rmax()/2.,'My label',
rotation=label_position,ha='center',va='center')
plt.show()
And here's the output:
I'd be interested to see if there's a more elegant solution, but hopefully this helps you.
from pylab import *
N = 150
r = 2*rand(N)
theta = 2*pi*rand(N)
area = 200*r**2*rand(N)
colors = theta
ax = subplot(111, polar=True)
c = scatter(theta, r, c=colors, s=area, cmap=cm.hsv)
c.set_alpha(0.75)
ax.set_ylabel('Radius', rotation=45, size=11)
show()
A slightly different method from #tom. This uses directly the plt.legend option.
Example:
import matplotlib.pyplot as plt
import numpy as np
theta=np.random.rand(40)*np.pi*2.
radii=np.random.rand(40)
ax = plt.subplot(111, polar=True)
ax.set_rmax(1)
c = plt.scatter(theta, radii,label='Supernova')
ax.set_title("Spread of Abell Cluster Supernova Events as a Function of Fractional Radius", va='bottom')
ax.legend(loc='lower right', scatterpoints=1)
plt.show()
You can change lower right to upper right or even to best to leave the alignment of the legend to matplotlib.

Categories