How to draw a circular heatmap within a rectangle in Python - python

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].

Related

Trouble using Insetposition for Inset Axes with Cartopy

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.

Can't draw circle with right proportions Matplotlib Python

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.

How to transpose from cartopy to axes coords

I have a cartopy GeoAxesSubplot with some points, and potentially lines or polygons. The projection could be any that is supported by cartopy, including orthographic.
I can plot using different transformations, as explained here:
from matplotlib import pyplot as plt
import cartopy.crs as ccrs
# Projection could be any, e.g. globe or Arctic Stereopolar...
ax = plt.axes(projection=ccrs.Mollweide())
ax.coastlines()
# Plot using the coordinate system of the Axes
a = ax.plot(0.45, 0.5, transform=ax.transAxes, marker='o', ms=10)
# Plot using the projected coordinates using cartopy.crs
b = ax.plot(0, 0, transform=ccrs.PlateCarree() , marker='o', ms=10)
I would like to transform geographical coordinates to get the cartesian coordinates of the object in the axis (e.g. a subplot). That is, the coordinates in the range [0,1] in the axes of the figure, with (0,0) in the lower-left corner, and (1,1) in the upper-right.
In the case above, b should be converted to (0.5, 0, 5) as it is in the center of the map.
Something similar can be done using transform_points, however, I have not been able to transpose to axes-coords.
A number of parameters are defined in matplotlib and cartopy to control where the object is on the map (extent, projection, center meridian, view elevation etc). Hence, introduce another library might be awkward.
Answer given e.g. here explains how the reverse is achievable, however, the example does not give the right answer for how to generate axes coords.
Keep in mind that "geographical coordinates" is not that well defined, since you're mixing two projections (Mollweide & PlateCarree) which both use "geographical coordinates". Also be careful with using the exact center, since that might accidentally look correct, even if you use incorrect coordinates.
So you might first need to convert your data to the projection of the map (projection).
Other than that the Matplotlib transformation tutorial you link to provides all the information necessary to do the transforms.
Setting up the inputs:
from matplotlib import pyplot as plt
import cartopy.crs as ccrs
# sample point coordinates in Plate-Carree
x_pc = -110.0 # longitude
y_pc = 45.0 # latitude
map_proj = ccrs.Mollweide()
data_proj = ccrs.PlateCarree()
The conversion depends on the xlim and ylim of the axes, so it's important to set use ax.set_global() first. That gives a proper mapping from the projection to the display coordinates (and subsequent axes coordinates).
fig, ax = plt.subplots(subplot_kw=dict(projection=map_proj), facecolor='w')
ax.set_global()
ax.coastlines()
b = ax.plot(x_pc, y_pc, 'go', transform=data_proj, ms=5)
# convert to map-coordinates (Mollweide)
x_mollw, y_mollw = ax.projection.transform_point(x_pc, y_pc, data_proj)
# convert to display coordinates
x_disp, y_disp = ax.transData.transform((x_mollw, y_mollw))
# convert to axes coordinates
x_axes, y_axes = ax.transAxes.inverted().transform((x_disp, y_disp))
# plot same point but using axes coordinates
ax.plot(x_axes, y_axes, 'ro', transform=ax.transAxes, ms=10, mfc='none', mew=2)

Matplotlib plot has slanted lines

I'm trying to plot projections of coordinates onto a line, but for some reason, Matplotlib is plotting the projections in a slightly slanted manner. Ideally, I would like the (blue) projections to be perpendicular to the (green) line. Here's an image of how it looks with sample data:
As you can see, the angles between the blue lines and the green line are slightly obtuse instead of right. I tried playing around with the rotation parameter to the annotate function, but this did not help. The code for this plot is below, although the data might look a bit different since the random generator is not seeded:
import numpy as np
import matplotlib.pyplot as plt
prefs = {'color':'purple','edgecolors':'black'}
X = np.dot(np.random.rand(2,2), np.random.rand(2,50)).T
pts = np.linspace(-1,1)
v1_m = 0.8076549717643662
plt.scatter(X[:,0],X[:,1],**prefs)
plt.plot(pts, [v1_m*x for x in pts], color='lightgreen')
for x,y in X:
# slope of connecting line
# y = mx+b
m = -np.reciprocal(v1_m)
b = y-m*x
# find intersecting point
zx = b/(v1_m-m)
zy = v1_m*zx
# draw line
plt.annotate('',(zx,zy),(x,y),arrowprops=dict(linewidth=2,arrowstyle='-',color='lightblue'))
plt.show()
The problem lies in the unequal axes which makes it look like they are not at a right angle. Use plt.axis('equal') to have equal axis spans on x- and y-axis and a square figure with equal height and width. plt.axis('scaled') works the same way. As pointed out by #CedricZoppolo, you should set the equal aspect ratios before plt.show(). As per docs, setting the aspect ratio to "equal" means
same scaling from data to plot units for x and y
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8,8))
# Your code here
plt.axis('equal')
plt.show()
Choosing a square figure is not necessary as it works also with rectangular figures as
fig = plt.figure(figsize=(8,6))
# Your code here
plt.axis('equal')
plt.show()
The blue lines not being perpendicular is due to axis not being equal.
You just need to add below line before plt.show()
plt.gca().set_aspect('equal')
Below you can see the resulted graph:

Matplotlib: Draw a vertical arrow in a log-log plot

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')

Categories