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.
Related
I'm trying to draw a couple of vectors with pyplot.
My approach is to define first the plot size (or axis values), as this will change depending on the job run.
pretty much what i need to do is to draw a straight, upwards-facing vector.
So for the test i am using the same values for x-axis, and two different ones for Y (as that would give me an vertical vector)
problem I face is - whatever i am doing, i can't seem to make the vector straight. It always gets rotated.
Does anyone know what reason there might be for it ?
my current code
import matplotlib.pyplot as plt
import math
start = (162305,299400,162890,299400) #define coordinates for vector
#define plot axis values
top = 163440
bottom = 161910
left = 299595
right = 300510
fig, ax = plt.subplots()
ax.quiver(start[0],start[1],start[2],start[3])
plt.xlim(left,right)
plt.ylim(bottom,top)
plt.autoscale()
plt.show()
and the resulting plot
Hope this would help!
import matplotlib.pyplot as plt
import math
start = (162305,299400,162890,299400) #define coordinates for vector
#define plot axis values
top = 163440
bottom = 161910
left = 299595
right = 300510
fig, ax = plt.subplots()
ax.quiver(start[0],start[1],1,0)
#ax.quiver(x_pos, y_pos, x_dir, y_dir, color)
#give the x_pos, y_pos value
#x_dir=1, y_dir=1, meaning 45 degree angle
#x_dir=1, y_dir=0, meaning 0 degree angle
#x_dir=0, y_dir=1, meaning 90 degree angle
# you put x_dir=start[2] and y_dir=start[3], making an arbitary angle of atan
#(y_dir/x_dir) which makes the angled arrow
plt.xlim(left,right)
plt.ylim(bottom,top)
plt.autoscale()
plt.show()
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.
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')
I'm currently implementing something with Python and matplotlib. I know how to draw polygons and also how to fill them, but how do I fill everything except the interior of a polygon? To be clearer, I'd like to modify the result below, obtained using axhspan's and axvspan's, by clipping the horizontal and vertical red lines so as to obtain a red rectangle (outside which everything is hatched as it is now):
This post asks (and answers) essentially this question. Look at 'Edit 2' in the accepted answer. It describes how to create a vector polygon the size of your plot bounds and then how to create a hole in it to match the shape you want to complement. It does this by assigning line codes that define whether or not the pen draws when it moves.
Here is the portion of the above-referenced post that is relevant to this question:
import numpy as np
import matplotlib.pyplot as plt
def main():
# Contour some regular (fake) data
grid = np.arange(100).reshape((10,10))
plt.contourf(grid)
# Verticies of the clipping polygon in counter-clockwise order
# (A triange, in this case)
poly_verts = [(2, 2), (5, 2.5), (6, 8), (2, 2)]
mask_outside_polygon(poly_verts)
plt.show()
def mask_outside_polygon(poly_verts, ax=None):
"""
Plots a mask on the specified axis ("ax", defaults to plt.gca()) such that
all areas outside of the polygon specified by "poly_verts" are masked.
"poly_verts" must be a list of tuples of the verticies in the polygon in
counter-clockwise order.
Returns the matplotlib.patches.PathPatch instance plotted on the figure.
"""
import matplotlib.patches as mpatches
import matplotlib.path as mpath
if ax is None:
ax = plt.gca()
# Get current plot limits
xlim = ax.get_xlim()
ylim = ax.get_ylim()
# Verticies of the plot boundaries in clockwise order
bound_verts = [(xlim[0], ylim[0]), (xlim[0], ylim[1]),
(xlim[1], ylim[1]), (xlim[1], ylim[0]),
(xlim[0], ylim[0])]
# A series of codes (1 and 2) to tell matplotlib whether to draw a line or
# move the "pen" (So that there's no connecting line)
bound_codes = [mpath.Path.MOVETO] + (len(bound_verts) - 1) * [mpath.Path.LINETO]
poly_codes = [mpath.Path.MOVETO] + (len(poly_verts) - 1) * [mpath.Path.LINETO]
# Plot the masking patch
path = mpath.Path(bound_verts + poly_verts, bound_codes + poly_codes)
patch = mpatches.PathPatch(path, facecolor='white', edgecolor='none')
patch = ax.add_patch(patch)
# Reset the plot limits to their original extents
ax.set_xlim(xlim)
ax.set_ylim(ylim)
return patch
if __name__ == '__main__':
main()
If you only need the complement of a rectangle, you could instead draw 4 rectangles around it (like the 4 rectangles that are visible in your example image). The coordinates of the plot edges can be obtained with xlim() and ylim().
I am not sure that Matplotlib offers a way of painting the outside of a polygon…