I want to create an inset map on a map using Cartopy. I'd like to specify the x & y inset location positions as a function of the parent axes so that the child axes are always inside the parent. For example, 0,0 aligns the inset axes at the bottom left and 1,1 aligns the inset axes at the top right, but in both cases the inset plot is inside the parent. I've achieved this using the following:
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
import cartopy.crs as ccrs
ax = plt.axes(projection=ccrs.PlateCarree(), label='1')
ax.coastlines()
iax = plt.axes([0, 0, 1, 1], projection=ccrs.PlateCarree(), label='2')
iax.coastlines()
size = .5
inset_x = 1
inset_y = 1
left = inset_x - inset_x*size
bottom = inset_y - inset_y*size
ip = InsetPosition(ax, [left, bottom, size, size]) #posx, posy, width, height
iax.set_axes_locator(ip)
working fine when extents not set
The problem is if I apply new extents to the inset map. Depending on the aspect ratio of the newly set extents, my x or y inset position translates to positions well inside the parent axes - a 0-y results in the inset being off the bottom and a 1-y results it being offset at the top. When this happens, the offset is symmetric and only occurs in one of the position axes, the other one behaves as desired. I've tried using get_position, but that doesn't seem intuitive when using Cartopy because the bbox returned does not reflect the aspect ratio of the plot extents. For example, adding this before applying the InsetPosition:
# extent=[-20,60,40,65] # this breaks y positioning
extent=[-20,60,0,65] # this breaks x positioning
iax.set_extent(extent, crs=ccrs.PlateCarree())
not working as expected
I can manually adjust them to where I want but the correction doesn't match differences in bbox height/width or any other value I've thought to check. Any suggestions?
If I change the left and bottom locations to:
left = inset_x - size/2
bottom = inset_y - size/2
That always works consistently, regardless of the extents set, but it puts the inset map overlapping the corner.
works consistently but not desired results
Additional note - the same behavior can be found if you use ordinary (non-GeoAxes) plots and change the aspect of the inset using set_aspect. I still haven't figured out the bbox size relationships (parent and inset) between the pre- and post-aspect change and how it impacts the specific inset placement.
#Hagbeard,I found the similar question the similar question, and I have further refined #gepcel's code so that it can better meet your needs.
Here is the code.
import matplotlib.pyplot as plt
#from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
import cartopy.crs as ccrs
ax = plt.axes(projection=ccrs.PlateCarree(), label='1')
ax.coastlines()
size = .2 # Figure Standardized coordinates (0~1)
#GeoAxes has a width/height ratio according to self's projection
#Therefore, Here only set a same width and height
iax = plt.axes([0, 0, size, size], projection=ccrs.PlateCarree(), label='2')
iax.coastlines()
extent=[-20,60,40,65]
#extent=[-20,60,0,65]
iax.set_extent(extent, crs=ccrs.PlateCarree())
def set_subplot2corner(ax,ax_sub,corner="bottomright"):
ax.get_figure().canvas.draw()
p1 = ax.get_position()
p2 = ax_sub.get_position()
if corner == "topright":
ax_sub.set_position([p1.x1-p2.width, p1.y1-p2.height, p2.width, p2.height])
if corner == "bottomright":
ax_sub.set_position([p1.x1-p2.width, p1.y0, p2.width, p2.height])
if corner == "bottomleft":
ax_sub.set_position([p1.x0, p1.y0, p2.width, p2.height])
if corner == "topleft":
ax_sub.set_position([p1.x0, p1.y1-p2.height, p2.width, p2.height])
set_subplot2corner(ax,iax,corner="topright")
# Do not support a interactive zoom in and out
# so plz save the plot as a static figure
plt.savefig("corner_subfig.png",bbox_inches="tight")
#plt.show()
And the final plot.
Related
In matplotlib, is there a way to specify arrow head sizes in figure units rather than in data units?
The use case is: I am making a multi-panel figure in which each panel has a different axis size (e.g., one goes from 0 to 1 on the X-axis, and the next goes from 0 to 10). I'd like the arrows to appear the same in each panel. I'd also like the arrows to appear the same independent of direction.
For axes with an aspect ratio not equal to 1, the width of the tail (and therefore the size of the head) varies with direction.
The closest I've come is, after drawing on the canvas:
dx = ax.get_xlim()[1] - ax.get_xlim()[0]
for arrow in ax.patches:
arrow.set_data(width=dx/50)
but this does not work; it results in images like this:
Just use ax.annotate() instead of ax.arrow():
import matplotlib.pyplot as plt
import numpy as np
xlim, ylim = (-.3, .8), (0, 5.8)
arrow_start, arrow_end = np.asarray([.1, 3]), np.asarray([.5, 5])
fig = plt.figure(figsize=(3, 2))
ax = plt.gca()
ax.set_title('using ax.arrow()')
ax.set_xlim(xlim)
ax.set_ylim(ylim)
ax.arrow(*arrow_start, *(arrow_end - arrow_start), width=1/50)
fig = plt.figure(figsize=(3, 2))
ax = plt.gca()
ax.set_title('using ax.annotate()')
ax.set_xlim(xlim)
ax.set_ylim(ylim)
ax.annotate('', arrow_end, arrow_start, arrowprops=dict(width=5, headwidth=10, headlength=5))
I want to draw Circle on my plot. For this purpose I decided to use patch.Circle class from matplotlib. Cirlce object uses radius argument to set a radius of a circle, but if the axes ratio is not 1 (see my plot), how to draw circle with right proportions?
My code for drawing circle is:
rect = patches.Circle(xy=(9, yaxes),radius= 2, linewidth=3, edgecolor='r', facecolor='red',alpha=0.5)
ax.add_patch(rect)
yaxes is equal 206 in this example (because I wanted to draw it upper left coner).
Here is a picture I got using this code:
But I want something like this:
You could use ax.transData to transform 1,1 vs 0,0 and obtain the deformation in x vs y direction. That ratio can be used to know the horizontal versus the vertical size of the circle.
If you just need to place a circle using coordinates relative to the axes, plt.scatter with transform=ax.transAxes can be used. Note that the size is an "area" measure based on "points" (a "point" is 1/72th of an inch).
The following example code uses the data coordinates to position the "circle" (using an ellipse) and the x-coordinates for the radius. A red circle is placed using axes coordinates.
from matplotlib import pyplot as plt
from matplotlib.patches import Ellipse
import pandas as pd
import numpy as np
# plot some random data
np.random.seed(2021)
df = pd.DataFrame({'y': np.random.normal(10, 100, 50).cumsum() + 2000},
index=np.arange(101, 151))
ax = df.plot(figsize=(12, 5))
# find an "interesting" point
max_ind = df['y'].argmax()
max_x = df.index[max_ind]
max_y = df.iloc[max_ind]['y']
# calculate the aspect ratio
xscale, yscale = ax.transData.transform([1, 1]) - ax.transData.transform([0, 0])
# draw the ellipse to be displayed as circle
radius_x = 4
radius_y = radius_x * xscale / yscale
ax.add_patch(Ellipse((max_x, max_y), radius_x, radius_y, color='purple', alpha=0.4))
# use ax.scatter to draw a red dot at the top left
ax.scatter(0.05, 0.9, marker='o', s=2000, color='red', transform=ax.transAxes)
plt.show()
Some remarks about drawing the ellipse:
this will only work for linear coordinates, not e.g. for logscale or polar coordinates
the code supposes nor the axis limits nor the axis position will change afterwards, as these will distort the aspect ratio
The issue seems to be that your X (passed to xy=) is not always the same as your Y, thus the oval instead of a perfect circle.
I am trying to reproduce a plot like this:
So the requirements are actually that the grid (that is to be present just on the left side) behaves just like a grid, that is, if we zoom in and out, it is always there present and not dependent on specific x-y limits for the actual data.
Unfortunately there is no diagonal version of axhline/axvline (open issue here) so I was thinking about using the grid from polar plots.
So for that I have two problems:
This answer shows how to overlay a polar axis on top of a rectangular one, but it does not match the origins and x-y values. How can I do that?
I also tried the suggestion from this answer for having polar plots using ax.set_thetamin/max but I get an AttributeError: 'AxesSubplot' object has no attribute 'set_thetamin' How can I use these functions?
This is the code I used to try to add a polar grid to an already existing rectangular plot on ax axis:
ax_polar = fig.add_axes(ax, polar=True, frameon=False)
ax_polar.set_thetamin(90)
ax_polar.set_thetamax(270)
ax_polar.grid(True)
I was hoping I could get some help from you guys. Thanks!
The mpl_toolkits.axisartist has the option to plot a plot similar to the desired one. The following is a slightly modified version of the example from the mpl_toolkits.axisartist tutorial:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cbook as cbook
from mpl_toolkits.axisartist import SubplotHost, ParasiteAxesAuxTrans
from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear
import mpl_toolkits.axisartist.angle_helper as angle_helper
from matplotlib.projections import PolarAxes
from matplotlib.transforms import Affine2D
# PolarAxes.PolarTransform takes radian. However, we want our coordinate
# system in degree
tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform()
# polar projection, which involves cycle, and also has limits in
# its coordinates, needs a special method to find the extremes
# (min, max of the coordinate within the view).
# 20, 20 : number of sampling points along x, y direction
extreme_finder = angle_helper.ExtremeFinderCycle(20, 20,
lon_cycle=360,
lat_cycle=None,
lon_minmax=None,
lat_minmax=(0, np.inf),)
grid_locator1 = angle_helper.LocatorDMS(36)
tick_formatter1 = angle_helper.FormatterDMS()
grid_helper = GridHelperCurveLinear(tr,
extreme_finder=extreme_finder,
grid_locator1=grid_locator1,
tick_formatter1=tick_formatter1
)
fig = plt.figure(1, figsize=(7, 4))
fig.clf()
ax = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper)
# make ticklabels of right invisible, and top axis visible.
ax.axis["right"].major_ticklabels.set_visible(False)
ax.axis["right"].major_ticks.set_visible(False)
ax.axis["top"].major_ticklabels.set_visible(True)
# let left axis shows ticklabels for 1st coordinate (angle)
ax.axis["left"].get_helper().nth_coord_ticks = 0
# let bottom axis shows ticklabels for 2nd coordinate (radius)
ax.axis["bottom"].get_helper().nth_coord_ticks = 1
fig.add_subplot(ax)
## A parasite axes with given transform
## This is the axes to plot the data to.
ax2 = ParasiteAxesAuxTrans(ax, tr)
## note that ax2.transData == tr + ax1.transData
## Anything you draw in ax2 will match the ticks and grids of ax1.
ax.parasites.append(ax2)
intp = cbook.simple_linear_interpolation
ax2.plot(intp(np.array([150, 230]), 50),
intp(np.array([9., 3]), 50),
linewidth=2.0)
ax.set_aspect(1.)
ax.set_xlim(-12, 1)
ax.set_ylim(-5, 5)
ax.grid(True, zorder=0)
wp = plt.Rectangle((0,-5),width=1,height=10, facecolor="w", edgecolor="none")
ax.add_patch(wp)
ax.axvline(0, color="grey", lw=1)
plt.show()
Say I have an image and I have a bounding box on a part of the image. How can I draw a circular heatmap within this rectangle?
You need to create a new Axes in the desired position, and use a polar pcolor plot to construct a "heatmap":
import matplotlib.pyplot as plt
import numpy as np
fig,ax1 = plt.subplots()
# plot dummy image
ax1.imshow(np.random.rand(200,200),cmap='viridis')
# create new Axes, position is in figure relative coordinates!
relpos = [0.6, 0.6, 0.2, 0.2]
ax2 = fig.add_axes(relpos, polar=True)
ax2.axis('off')
phi = np.linspace(0,2*np.pi,50)
r = np.linspace(0,1,50)
gradient = np.tile(np.linspace(0,1,r.size)[:,None],phi.size)
ax2.pcolor(gradient,cmap='hot_r')
The result:
The color gradient samples linearly from the colormap, in the above example named hot_r. You can play around with both the colormap and with the transition of the gradient variable, the result will always be radially dependent.
The only thing you need to take care of is to transform your rectangle (given in units which only you can tell) to relative figure units (where (0,0) is the bottom left corner of the figure, and (1,1) is the top left). The axis positioning works in the way which is usual for box-shaped objects: [left,bottom,width,height].
I am not able to draw a simple, vertical arrow in the following log-log plot:
#!/usr/bin/python2
import matplotlib.pyplot as plt
import matplotlib as mpl
plt.yscale('log')
plt.xscale('log')
plt.ylim((1e-20,1e-10))
plt.xlim((1e-12,1))
plt.arrow(0.00006666, 1e-20, 0, 1e-8 - 1e-20, length_includes_head=True)
plt.savefig('test.pdf')
It just doesn't show. From the documentation it appears as if all the arguments, like width, height and so on relate to the scale of the axis. This is very counter-intuitive. I tried using twin() of the axisartist package to define an axis on top of mine with limits (0,1), (0,1) to have more control over the arrow's parameters, but I couldn't figure out how to have a completely independent axis on top of the primary one.
Any ideas?
I was looking for an answer to this question, and found a useful answer! You can specify any "mathtext" character (matplotlib's version of LaTeX) as a marker. Try:
plt.plot(x,y, 'ko', marker=r'$\downarrow$', markersize=20)
This will plot a downward pointing, black arrow at position (x,y) that looks good on any plot (even log-log).
See: matplotlib.org/users/mathtext.html#mathtext-tutorial for more symbols you can use.
Subplots approach
After creating the subplots do the following
Align the positions
Use set_axis_off() to turn the axis off (ticks, labels, etc)
Draw the arrow!
So a few lines gets whats you want!
E.g.
#!/usr/bin/python2
import matplotlib.pyplot as plt
hax = plt.subplot(1,2,1)
plt.yscale('log')
plt.xscale('log')
plt.ylim((1e-20,1e-10))
plt.xlim((1e-12,1))
hax2 = plt.subplot(1,2,2)
plt.arrow(0.1, 1, 0, 1, length_includes_head=True)
hax.set_position([0.1, 0.1, 0.8, 0.8])
hax2.set_position([0.1, 0.1, 0.8, 0.8])
hax2.set_axis_off()
plt.savefig('test.pdf')
Rescale data
Alternatively a possibly easier approach, though the axis labels may be tricky, is to rescale the data.
i.e.
import numpy
# Other import commands and data input
plt.plot(numpy.log10(x), numpy.log10(y)))
Not a great solution, but a decent result if you can handle the tick labels!
I know this thread has been dead for a long time now, but I figure posting my solution might be helpful for anyone else trying to figure out how to draw arrows on log-scale plots efficiently.
As an alternative to what others have already posted, you could use a transformation object to input the arrow coordinates not in the scale of the original axes but in the (linear) scale of the "axes coordinates". What I mean by axes coordinates are those that are normalized to [0,1] (horizontal range) by [0,1] (vertical range), where the point (0,0) would be the bottom-left corner and the point (1,1) would be the top-right, and so on. Then you could simply include an arrow by:
plt.arrow(0.1, 0.1, 0.9, 0.9, transform=plot1.transAxes, length_includes_head=True)
This gives an arrow that spans diagonally over 4/5 of the plot's horizontal and vertical range, from the bottom-left to the top-right (where plot1 is the subplot name).
If you want to do this in general, where exact coordinates (x0,y0) and (x1,y1) in the log-space can be specified for the arrow, this is not too difficult if you write two functions fx(x) and fy(y) that transform from the original coordinates to these "axes" coordinates. I've given an example of how the original code posted by the OP could be modified to implement this below (apologies for not including the images the code produces, I don't have the required reputation yet).
#!/usr/bin/python3
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
# functions fx and fy take log-scale coordinates to 'axes' coordinates
ax = 1E-12 # [ax,bx] is range of horizontal axis
bx = 1E0
def fx(x):
return (np.log(x) - np.log(ax))/(np.log(bx) - np.log(ax))
ay = 1E-20 # [ay,by] is range of vertical axis
by = 1E-10
def fy(y):
return (np.log(y) - np.log(ay))/(np.log(by) - np.log(ay))
plot1 = plt.subplot(111)
plt.xscale('log')
plt.yscale('log')
plt.xlim(ax, bx)
plt.ylim(ay, by)
# transformed coordinates for arrow from (1E-10,1E-18) to (1E-4,1E-16)
x0 = fx(1E-10)
y0 = fy(1E-18)
x1 = fx(1E-4) - fx(1E-10)
y1 = fy(1E-16) - fy(1E-18)
plt.arrow(
x0, y0, x1, y1, # input transformed arrow coordinates
transform = plot1.transAxes, # tell matplotlib to use axes coordinates
facecolor = 'black',
length_includes_head=True
)
plt.grid(True)
plt.savefig('test.pdf')