Why is matplotlib.patches.Circle not a circle? - python

I am trying to plot a circle over a plot. Using the Anatomy of a Figure for inspiration, I've created a short test code :
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.patches import Circle
from matplotlib.patheffects import withStroke
fig = plt.figure()
x = np.arange(1,10,.1)
y3 = np.sin(x)
gs = fig.add_gridspec(1,1) # 2x2 grid
ax=fig.add_subplot(gs[0,0])
ax.plot(x,y3)
Xc = 6
Yc = 0.5
### This produces an ellipse
circle = Circle((Xc, Yc), 0.25, clip_on=False, zorder=10, linewidth=1,
edgecolor='black', facecolor=(0, 0, 0, .0125),
path_effects=[withStroke(linewidth=5, foreground='w')])
ax.add_artist(circle)
plt.show()
which generates the below plot
Question :
Why is the displayed 'circle' really an ellipse?
The help page for Circle, defines a resolution order, but it isn't obvious how this 'resolution' order is decided. Given that my circle is following the format of the above Anatomy of a Figure, I don't understand how this happens.

For your circle to look like a circle, you have to set the aspect ratio of your plot to 1.
In your link, that is done in this line:
ax = fig.add_subplot(1, 1, 1, aspect=1)
In your example:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib.patches import Circle
from matplotlib.patheffects import withStroke
fig = plt.figure()
x = np.arange(1,10,.1)
y3 = np.sin(x)
gs = fig.add_gridspec(1,1) # 2x2 grid
ax=fig.add_subplot(gs[0,0], aspect=1)
ax.plot(x,y3)
Xc = 6
Yc = 0.5
### This produces an ellipse
circle = Circle((Xc, Yc), 0.25, clip_on=False, zorder=10, linewidth=1,
edgecolor='black', facecolor=(0, 0, 0, .0125),
path_effects=[withStroke(linewidth=5, foreground='w')])
ax.add_artist(circle)
plt.show()

Related

How to draw a circle in a double y axis graph with matplotlib?

I am new to matplotlib. I am trying to draw an empty circle to specify the data points in a double y axis graph. I used the plt.Cirle to do the work. But it did not work. Could you kindly help me? Here is the code, and what I have got is a rectangle instead of a circle.
from matplotlib.patches import *
fig = plt.figure()
ax1 = plt.gca()
markers,stems,base = ax1.stem(x1,oscillator,linefmt='k-',markerfmt='ko')
for stem in stems:
stem.set_linewidth(1)
ax1.set_ylim(0,0.4)
ax1.set_xlim(250,500)
ax1.set_xlabel('Wavelength (nm)')
ax1.set_ylabel('Oscillator strength')
ax1.annotate('Oscillator strength', xy=(307,0.31), xytext=(325,0.35),
arrowprops=dict(arrowstyle= '-|>',connectionstyle='arc3,rad=0.5',lw = 1, color ='k'))
circ = plt.Circle((300,0.3), radius=20, edgecolor='g')
ax1.add_artist(circ)
ax2 = ax1.twinx()
ax2.plot(x2,absorbance,'r-',linewidth=1)
ax2.spines['right'].set_color('red')
ax2.tick_params(axis='y', colors='red')
ax2.yaxis.label.set_color('red')
ax2.set_ylabel('Absorbance',color='r')
ax2.annotate('', xy=(414,0.31), xytext=(450,0.33),
arrowprops=dict(arrowstyle= '-|>',connectionstyle='arc3,rad=0.5',lw = 1, color ='r'))
ax2.text(450,0.33,'Absorbance',color='red')
plt.show()
Here is the graph, the blue rectangle should be a circle:
The problem is that you create a circle in an axes of very unequal data limits. While the x axis ranges in the ranges of hundreds, the y axis ranges in the range below 1. What you observe as rectangle is hence a very distorted circle.
For such cases, where the limits are significantly different, or in any case where you want to have a true Circle on screen, the circle should rather be defined in axes coordinates or even better, in display coordinates.
An easy method to produce a circle in display coordinates is a scatter plot.
import matplotlib.pyplot as plt
import numpy as np
x = np.arange(303,320,3)
y = np.array([.25,.13,.09,.33,.16,.11])
fig, ax = plt.subplots()
ax.stem(x,y,linefmt='k-',markerfmt='ko')
ax.scatter([x[3]],[y[3]], s=40**2, edgecolor="green", facecolor="None", linewidth=3 )
ax.axis([250,500,0,.4])
plt.show()
This should solve your problem:
https://matplotlib.org/gallery/api/patch_collection.html
from matplotlib.patches import Circle, Wedge
from matplotlib.collections import PatchCollection
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
patches = []
circle = Circle((0.5, 0.8), 0.25) #Filled circle
patches.append(circle)
patches += [
Wedge((.3, .7), .1, 0, 360), # Full circle
Wedge((.7, .8), .2, 0, 360, width=0.05), # Full ring
Wedge((.8, .3), .2, 0, 45), # Full sector
Wedge((.8, .3), .2, 45, 90, width=0.10)] # Ring sector
p = PatchCollection(patches, alpha=0.4)
ax.add_collection(p)
plt.show()

How to easily add a sub_axes with proper position and size in matplotlib and cartopy?

I want to add a 2nd axes at the top right corner of a 1st axes. After googling, I found two ways to do things like this: fig.add_axes(), and mpl_toolkits.axes_grid.inset_locator.inset_axes. But the fig.add_axes() doesn't accept transform arg. So the following code throws an error. So the position can't be under the parent axes coordinates but the figure coordinates.
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
fig, ax = plt.subplots(1, 1, subplot_kw={'projection': ccrs.PlateCarree()})
ax2 = fig.add_axes([0.8, 0, 0.2, 0.2], transform=ax.transAxes, projection=ccrs.PlateCarree())
And inset_axes() doesn't accept the projection arg, so I can't add ax2 as a cartopy geo-axes.
from mpl_toolkits.axes_grid.inset_locator import inset_axes
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
fig, ax = plt.subplots(1, 1, subplot_kw={'projection': ccrs.PlateCarree()})
# The following line doesn't work
ax2 = inset_axes(ax, width='20%', height='20%', axes_kwargs={'projection': ccrs.PlateCarree()})
# Doesn't work neither:
ax2 = inset_axes(ax, width='20%', height='20%', projection=ccrs.PlateCarree())
I've asked the question at matplotlib issue. It seems the following code works well as long as it's not a cartopy axes.
import matplotlib as mpl
fig, ax = plt.subplots(1, 1)
box = mpl.transforms.Bbox.from_bounds(0.8, 0.8, 0.2, 0.2)
ax2 = fig.add_axes(fig.transFigure.inverted().transform_bbox(ax.transAxes.transform_bbox(box)))
Question:
How to easily add a sub_axes with proper position and size in matplotlib and cartopy?
As I understand, after ax.set_extend(), the size of axes will change. So maybe is there a way that some point of sub_axes (eg: top right corner of ax2) can be anchored at one fixed position of the parent_axes (eg: top right corner of ax1)?
As inset_axes() doesn't accept projection arg, the roundabout way is to use InsetPosition(). This way you can create an axes in the usual way (using projection), and then "link" both axes using InsetPosition(). The main advantage over using subplots or similar is that the inset position is fixed, you can resize the figure or change the main plot area and the inset will always be in the same place relative to the main axes. This was based on this answer: specific location for inset axes, just adding the cartopy way of doing things.
import numpy as np
import matplotlib.pyplot as plt
import cartopy.crs as ccrs
import cartopy.feature as cfeature
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
from shapely.geometry.polygon import LinearRing
extent = [-60, -30, -40, -10]
lonmin, lonmax, latmin, latmax = extent
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(1, 1, 1, projection=ccrs.PlateCarree())
ax.set_extent(extent, crs=ccrs.PlateCarree())
ax.add_feature(cfeature.LAND)
ax.add_feature(cfeature.OCEAN)
ax.add_feature(cfeature.COASTLINE)
# inset location relative to main plot (ax) in normalized units
inset_x = 1
inset_y = 1
inset_size = 0.2
ax2 = plt.axes([0, 0, 1, 1], projection=ccrs.Orthographic(
central_latitude=(latmin + latmax) / 2,
central_longitude=(lonmin + lonmax) / 2))
ax2.set_global()
ax2.add_feature(cfeature.LAND)
ax2.add_feature(cfeature.OCEAN)
ax2.add_feature(cfeature.COASTLINE)
ip = InsetPosition(ax, [inset_x - inset_size / 2,
inset_y - inset_size / 2,
inset_size,
inset_size])
ax2.set_axes_locator(ip)
nvert = 100
lons = np.r_[np.linspace(lonmin, lonmin, nvert),
np.linspace(lonmin, lonmax, nvert),
np.linspace(lonmax, lonmax, nvert)].tolist()
lats = np.r_[np.linspace(latmin, latmax, nvert),
np.linspace(latmax, latmax, nvert),
np.linspace(latmax, latmin, nvert)].tolist()
ring = LinearRing(list(zip(lons, lats)))
ax2.add_geometries([ring], ccrs.PlateCarree(),
facecolor='none', edgecolor='red', linewidth=0.75)
I may have figured something out.
According to the answer this question. I can get the position of both axes, then reposition the 2nd axes. The code was like:
import matplotlib.pyplot as plt
from cartopy import crs as ccrs
fig, ax = plt.subplots(1, 1, subplot_kw={'projection': ccrs.PlateCarree()})
ax2 = fig.add_axes([0.8, 0.8, 0.2, 0.2], projection=ccrs.PlateCarree())
ax.set_extent([100, 120, 20, 40])
ax.coastlines()
ax2.set_global()
ax2.coastlines()
ax2.stock_img()
def reposition():
plt.draw()
p1 = ax.get_position()
p2 = ax2.get_position()
ax2.set_position([p1.x1-p2.width, p1.y1-p2.height, p2.width, p2.height])
reposition()
plt.show()
The result is just what I want.

How can I embed an image on each of my subplots in matplotlib?

I'm trying to put a little arrow in the corner of each of my subplots. Below is the sample code I'm using:
import matplotlib.pyplot as plt
import matplotlib.image as image
from numpy import linspace
xs = linspace(0, 1, 100)
im = image.imread('arrow.png')
def multi_plot():
fig, axes = plt.subplots(4, 1)
x = 0
for axis in axes:
axis.plot(xs, xs**2)
axis.imshow(im, extent=(0.4, 0.6, .5, .7), zorder=-1, aspect='auto')
plt.show()
multi_plot()
Unfortunately, this produces 4 subplots that are entirely dominated by the arrows and the plots themselves are not seen.
Example output - Incorrect:
What do I need to do so that each individual subplot has a small image and the plot itself can be seen?
I think it's worthwhile thinking about putting the image in a box and place it similar to the legend, using a loc argument. The advantage is that you don't need to care about extents and data coordinates at all. You also wouldn't need to take care of what happens when zooming or panning the plot. Further it allows to keep the image in it's original resolution (zoom=1 in below code).
import matplotlib.pyplot as plt
import matplotlib.image as image
from numpy import linspace
from matplotlib.offsetbox import OffsetImage,AnchoredOffsetbox
xs = linspace(0, 1, 100)
im = image.imread('arrow.png')
def place_image(im, loc=3, ax=None, zoom=1, **kw):
if ax==None: ax=plt.gca()
imagebox = OffsetImage(im, zoom=zoom*0.72)
ab = AnchoredOffsetbox(loc=loc, child=imagebox, frameon=False, **kw)
ax.add_artist(ab)
def multi_plot():
fig, axes = plt.subplots(4, 1)
for axis in axes:
axis.plot(xs, xs**2)
place_image(im, loc=2, ax=axis, pad=0, zoom=1)
plt.show()
multi_plot()
You'll notice that the limits on the x and y axis have been set to the extent of the imshow, rather than 0-1, which your plot needs to see the line.
You can control this by using axis.set_xlim(0, 1) and axis.set_ylim(0, 1).
import matplotlib.pyplot as plt
import matplotlib.image as image
from numpy import linspace
xs = linspace(0, 1, 100)
im = image.imread('arrow.png')
def multi_plot():
fig, axes = plt.subplots(4, 1)
x = 0
for axis in axes:
axis.plot(xs, xs**2)
axis.imshow(im, extent=(0.4, 0.6, .5, .7), zorder=-1, aspect='auto')
axis.set_xlim(0, 1)
axis.set_ylim(0, 1)
plt.show()
multi_plot()
Alternatively, if you want to maintain the extra 5% margin around your data that matplotlib uses by default, you can move the imshow command to before the plot command, then the latter will control the axis limits.
import matplotlib.pyplot as plt
import matplotlib.image as image
from numpy import linspace
xs = linspace(0, 1, 100)
im = image.imread('arrow.png')
def multi_plot():
fig, axes = plt.subplots(4, 1)
x = 0
for axis in axes:
axis.imshow(im, extent=(0.4, 0.6, .5, .7), zorder=-1, aspect='auto')
axis.plot(xs, xs**2)
plt.show()
multi_plot()

Draw Marker in Image

I'm drawing a picture using Matplotlib:
plt.imshow(bild)
plt.show()
How do I add a Marker to this (eg. red dot / arrow) using the coordinates of the image?
You can also use plt.scatter to add a red dot to mark the point. Building on the previous answer's example code:
import matplotlib.pyplot as plt
import numpy as np
img = np.random.randn(100, 100)
plt.figure()
plt.imshow(img)
plt.annotate('25, 50', xy=(25, 50), xycoords='data',
xytext=(0.5, 0.5), textcoords='figure fraction',
arrowprops=dict(arrowstyle="->"))
plt.scatter(25, 50, s=500, c='red', marker='o')
plt.show()
You could use the module matplotlib.patches as shown below. Notice that in order to place a patch at the xth row and yth column of the image you need to reverse the order of the coordinates, i.e. y, x when instantiating the corresponding patch.
from skimage import io
import matplotlib.pyplot as plt
from matplotlib.patches import Arrow, Circle
maze = io.imread('https://i.stack.imgur.com/SQCy9.png')
ax, ay = 300, 25
dx, dy = 0, 75
cx, cy = 300, 750
patches = [Arrow(ay, ax, dy, dx, width=100., color='green'),
Circle((cy, cx), radius=25, color='red')]
fig, ax = plt.subplots(1)
ax.imshow(maze)
for p in patches:
ax.add_patch(p)
plt.show(fig)
You can use the function plt.annotate for this:
import matplotlib.pyplot as plt
import numpy as np
img = np.random.randn(100, 100)
plt.imshow(img)
plt.annotate('25, 50', xy=(25, 40), xycoords='data',
xytext=(0.5, 0.5), textcoords='figure fraction',
arrowprops=dict(arrowstyle="->"))
plt.show()

Python 2D plots as 3D (Matplotlib)

Python plot in Matplotlib: I have a number of samples taken daily at the same time which shows a change in measurement (of something). This may be shown as a 2D plot (below left), but as the sample number increases I'd like to display this data as a 3D plot which is stacked (below right image) - this image is for illustration only.
For a starting point my code is below, how may I achieve this?
import numpy as np
import pylab as plt
t = np.arange(1024)*1e-6
y1 = np.sin(t*2e3*np.pi)
y2 = 0.5*y1
y3 = 0.25*y1
plt.plot(t,y1,'k-', label='12/03/14')
plt.plot(t,y2,'r-', label='13/03/14')
plt.plot(t,y3,'b-', label='14/03/14')
plt.xlabel('Time/sample no.')
plt.ylabel('Pk-pk level (arbitrary units)')
plt.legend()
plt.grid()
plt.show()
Would it be something like this?
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.collections import PolyCollection
from matplotlib.colors import colorConverter
import matplotlib.pyplot as plt
import numpy as np
fig = plt.figure()
ax = fig.gca(projection='3d')
zs = [0.0, 1.0, 2.0]
t = np.arange(1024)*1e-6
ones = np.ones(1024)
y1 = np.sin(t*2e3*np.pi)
y2 = 0.5*y1
y3 = 0.25*y1
verts=[list(zip(t, y1)), list(zip(t, y2)), list(zip(t, y3))]
poly = PolyCollection(verts, facecolors = ['r','g','b'])
poly.set_alpha(0.7)
ax.add_collection3d(poly, zs=zs, zdir='y')
ax.set_xlabel('X')
ax.set_xlim3d(0, 1024e-6)
ax.set_ylabel('Y')
ax.set_ylim3d(-1, 3)
ax.set_zlabel('Z')
ax.set_zlim3d(-1, 1)
plt.show()

Categories