Circle object changes position depending on image save format in Matplotlib - python

I have to place a circle in a specific spot in an image. The problem is that the image is plotted in an semi-log scale, which distorts the circle unless I use some specific transform. However, when I do that, the circle changes position depending if I save the image as A PDF or PNG. Here's a MWE:
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Circle
import numpy as np
from matplotlib.text import OffsetFrom
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(11,5), squeeze=False,
gridspec_kw = {'width_ratios':[3, 1]}, subplot_kw=dict(aspect="auto"))
x=np.logspace(-2,1)
y=np.linspace(.5,0,x.size)
ax=axes[0,0]
ax.semilogx(x, y)
circ = Circle((.5, .5), .1, transform="none", facecolor="none", edgecolor="k")
ax.add_patch(circ)
ax.set(xlim=(1e-2, 1e1), ylim=(0, .6))
fig.savefig("circle.png")
And here are the two outputs depending on how I save the image:
I have also tried using transform=ax.transAxes and, while it preserves the location of the circle, it's not a circle anymore after the semilog transformation.
Any ideas?

I think this is a known issue. The problem is that pdf is always saved with a dpi of 72, while png will take the figure dpi into account.
However, instead of creating a circle directly in the figure or axes, I would recommend playing around with the Annotation BBox tools.
You may create an AnnotationBBox with a DrawingArea inside. The DrawingArea may contain the circle. The coordinates of the DrawingArea are points.
The AnnotationBbox can be placed anywhere on the axes or figure and its position may be specified in some other coordinate system like axes coordinates or data coordinates.
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Circle
import numpy as np
from matplotlib.offsetbox import DrawingArea, AnnotationBbox
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(11,5), squeeze=False,
gridspec_kw = {'width_ratios':[3, 1]}, subplot_kw=dict(aspect="auto"))
x=np.logspace(-2,1)
y=np.linspace(.5,0,x.size)
ax=axes[0,0]
ax.semilogx(x, y)
##################
# Axes Coordinates
# Create circle of 10 points radius
da = DrawingArea(1,1, 0, 0)
p = Circle((0, 0), 10)
da.add_artist(p)
# Place box in the middle ((.5,.5)) of the axes.
# Add circle inside drawing area to box
ab = AnnotationBbox(da, (.5,.5), xycoords='axes fraction',
box_alignment=(0, 0), frameon=False, pad=0.0)
ax.add_artist(ab)
###################
# Data Coordinates
# Create circle of 10 points radius
da = DrawingArea(1,1, 0, 0)
p = Circle((0, 0), 10, color="crimson")
da.add_artist(p)
# Place box at (0.1,0.3) in data coordinates.
# Add circle inside drawing area to box
ab = AnnotationBbox(da, (0.1,0.3), xycoords='data',
box_alignment=(0, 0), frameon=False, pad=0.0)
ax.add_artist(ab)
ax.set(xlim=(1e-2, 1e1), ylim=(0, .6))
fig.savefig("circle.png")
fig.savefig("circle.pdf")
plt.show()
The resulting pdf and png will now be identical

Related

How to place clip art behind plotted data in matplotlib

I wish to plot things on top of an image I insert into my figure. I'm not sure how to do that. Here is a simple example where I do my best to place scattered points in the foreground of mario: I specify the order with zorder and call the scatter command last. However, mario is in the foreground and the scattered points are in the background.
How can I make the scattered points appear in front of Mario?
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
# load up mario
vortexRF = plt.imread('./mario.png')
imagebox = OffsetImage(vortexRF, zoom=0.08, zorder=1)
# initiate plot
fig, ax = plt.subplots()
# place mario in plot
ab = AnnotationBbox(imagebox, (0, 0), frameon=False)
cbar_ax = fig.add_axes([0.7, .42, 0.1, 0.1])
cbar_ax.add_artist(ab)
cbar_ax.axis('off')
# add scatter plot
NPoints = 1000
ax.scatter(np.random.random(NPoints), np.random.normal(0, 1, NPoints), s=3, c='purple', zorder=2)
# comment that mario should be in the background
ax.set_title("we want the purple dots to be in front of Mario")
# save figure. Mario is behind the scattered points :(
plt.savefig('marioExample')
cbar_ax = fig.add_axes(..., zorder=-1) arranges the z-order between axes. And ax.set_facecolor('none') makes the background of the scatter plot fully transparent (the default is opaque white, hiding everything behind it).
Note that everything that uses an ax is combined into one layer. An ax is either completely in front or completely to the back of another ax. Inside each ax, the elements can have their own z-orders.
To avoid copy-right issues, and to create a standalone example, the code below uses Ada Lovelace's image that comes with matplotlib.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import matplotlib.cbook as cbook
np.random.seed(1234)
# load up Ada's image
with cbook.get_sample_data('ada.png') as image_file:
vortexRF = plt.imread(image_file)
imagebox = OffsetImage(vortexRF, zoom=0.2)
# initiate plot
fig, ax = plt.subplots()
# place Ada in plot
ab = AnnotationBbox(imagebox, (0, 0), frameon=False)
cbar_ax = fig.add_axes([0.6, .42, 0.3, 0.3], zorder=-1)
cbar_ax.add_artist(ab)
cbar_ax.axis('off')
# add scatter plot
ax.scatter(np.random.normal(np.tile(np.random.uniform(0, 1, 5), 1000), .1),
np.random.normal(np.tile(np.random.uniform(0, 1, 5), 1000), .1),
c=np.tile(['fuchsia', 'gold', 'coral', 'deepskyblue', 'chartreuse'], 1000),
s=3, alpha=0.2)
# comment that Ada should be in the background
ax.set_title("we want the dots to be in front of Ada")
# make the background of the scatter plot fully transparent
ax.set_facecolor('none')
plt.show()
PS: Note that you can also add the image on the same ax as the scatter using imshow with an extent. The extent is default expressed in the same data coordinates as the plot in the order (x0, x1, y0, y1). This makes things somewhat simpler. The method using fig.add_axes, however, nicely keeps the original aspect ratio of the image.
ax.imshow(vortexRF, extent=[0.0, 0.4, 0.7, 1.1])

How to create a custom blended_transform in matplotlib that acts on rotated directions?

I am developing a python GUI that plots many lines, arrows and rectangles on a matplotlib canvas.
The rectangles go aligned with the lines: Rotated rectangle above line
Here is the picture.
I want to set a transform on the Rectangle, so that the side's length perpendicular to the line are in axes coordinates units (transAxes), and the sides parallel to the line are in data coordinates units (transData).
I know that blended_transform is can be used to define to different transforms for x-axis and y-axis. This is similar, but the directions in which the transforms are applied are not neccessary the horizontal and vertical direction. Is there a way of defining a custom blended transform that works on rotated directions instead of x-y directions? The documentation on transforms is not very helpful when trying to create a custom one.
Thanks!
The questions in the comments weren't answered, so one needs to make some assumptions. Let's say the rotation is supposed to happen in display space and the axes coordinates are those in y-axis direction. Then a possible transform could look like
trans = ax.get_xaxis_transform() + mtrans.Affine2D().rotate_deg(angle)
In this case the first dimension are data coordinates, the second are axes coordinates.
Some example:
import matplotlib.pyplot as plt
import matplotlib.transforms as mtrans
fig, ax = plt.subplots()
angle = 38 # degrees
trans = ax.get_xaxis_transform() + mtrans.Affine2D().rotate_deg(angle)
ax.plot([5,9],[0,0], marker="o", transform=trans)
rect = plt.Rectangle((5,0), width=4, height=0.2, alpha=0.3,
transform=trans)
ax.add_patch(rect)
ax.set(xlim=(3,10))
plt.show()
If instead you want rotation about a point in data coordinates, a single transform is not doing the job. For example for a rotation about (5,5) in data space,
import matplotlib.pyplot as plt
import matplotlib.transforms as mtrans
fig, ax = plt.subplots()
ax.set(xlim=(3,10),ylim=(4,10))
fig.canvas.draw()
angle = 38 # degrees
x, y = ax.transData.transform((5,5))
_, yax = ax.transAxes.inverted().transform((0,y))
transblend = ax.get_xaxis_transform()
x, y = transblend.transform((5,yax))
trans = transblend + mtrans.Affine2D().rotate_deg_around(x,y, angle)
ax.plot([5,9],[yax,yax], marker="o", transform=trans)
rect = plt.Rectangle((5,yax), width=4, height=0.2, alpha=0.3,
transform=trans)
ax.add_patch(rect)
plt.show()
Note that this invalidates as soon as you change the limits or figure size.

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

Using Colormap with Annotate Arrow in Matplotlib

I have seen many examples of using annotate arrows in Matplotlib that have a single color specified. I was wondering if it is possible to instead set the color according to a colormap, so that the whole range of colors from a specified colormap is displayed on a single arrow. I know that it is possible to set the color of an arrow to a single color from a colormap, but I want to have a single arrow displaying all of the colors of a given colormap.
A simple example of using an annotate arrow is shown below. In the documentation, I have not found any method for specifying a colormap. If I naively specify a colormap, I get an error from an invalid RGBA argument.
import matplotlib.pyplot as plt
RdPu = plt.get_cmap('RdPu')
ax = plt.subplot(111)
ax.annotate("Test", xy=(0.2, 0.2), xycoords='data',
xytext=(0.8, 0.8), textcoords='data',
size=20, arrowprops=dict(color=RdPu),
)
plt.show()
Ok, let's produce The Rainbow Arrow. ;-)
There is of course no built-in way to colorize an arrow with a color gradient. Instead one needs to build the arrow manually. I can think of two options. (1) Create a color gradient and clip it with the circonference path of an arrow. (2) Produce a LineCollection with a colorgradient and then add an arrow head to it.
The following is the second option:
import matplotlib.pyplot as plt
import matplotlib.transforms
import matplotlib.path
import numpy as np
from matplotlib.collections import LineCollection
def rainbowarrow(ax, start, end, cmap="viridis", n=50,lw=3):
cmap = plt.get_cmap(cmap,n)
# Arrow shaft: LineCollection
x = np.linspace(start[0],end[0],n)
y = np.linspace(start[1],end[1],n)
points = np.array([x,y]).T.reshape(-1,1,2)
segments = np.concatenate([points[:-1],points[1:]], axis=1)
lc = LineCollection(segments, cmap=cmap, linewidth=lw)
lc.set_array(np.linspace(0,1,n))
ax.add_collection(lc)
# Arrow head: Triangle
tricoords = [(0,-0.4),(0.5,0),(0,0.4),(0,-0.4)]
angle = np.arctan2(end[1]-start[1],end[0]-start[0])
rot = matplotlib.transforms.Affine2D().rotate(angle)
tricoords2 = rot.transform(tricoords)
tri = matplotlib.path.Path(tricoords2, closed=True)
ax.scatter(end[0],end[1], c=1, s=(2*lw)**2, marker=tri, cmap=cmap,vmin=0)
ax.autoscale_view()
fig,ax = plt.subplots()
ax.axis([0,5,0,4])
ax.set_aspect("equal")
rainbowarrow(ax, (3,3), (2,2.5), cmap="viridis", n=100,lw=3)
rainbowarrow(ax, (1,1), (1.5,1.5), cmap="jet", n=50,lw=7)
rainbowarrow(ax, (4,1.3), (2.7,1.0), cmap="RdYlBu", n=23,lw=5)
plt.show()
The following is the old solution, caused by a misunderstanding
An annotation arrow is a single arrow. Hence you would need to draw any number of arrows individually. In order for each arrow to then obtain a color, you may use the arrowprops=dict(color="<some color>") argument.
To get colors from a colormap, you can call the colormap with a value. Here the length of the arrow can be taken as the quantity to encode as color.
import matplotlib.pyplot as plt
import numpy as np
RdPu = plt.get_cmap('RdPu')
ax = plt.subplot(111)
ax.axis([-6,2,-4.5,3.2])
ax.set_aspect("equal")
X = np.linspace(0,1,17, endpoint=False)
Xt =np.sin(2.5*X+3)
Yt = 3*np.cos(2.6*X+3.4)
Xh = np.linspace(-0.5,-5,17)
Yh = -1.3*Xh-5
#Distance
D = np.sqrt((Xh-Xt)**2+(Yh-Yt)**2)
norm = plt.Normalize(D.min(), D.max())
for xt, yt, xh, yh, d in zip(Xt,Yt,Xh,Yh,D):
ax.annotate("Test", xy=(xh,yh), xycoords='data',
xytext=(xt,yt), textcoords='data',
size=10, arrowprops=dict(color=RdPu(norm(d))))
plt.show()

How to plot heat map with matplotlib?

How to use python and matplotlib to plot a picture like following?
I know how to plot the 2D heat map, but it frustrated me a lot with plotting the bar on top of the heat map, and the bar between the color bar and heat map.
How to add those two bars on the picture, and show the number in x axis or y axis belongs to which group?
Thanks very much for all the responses.
A systematic and straightforward approach, although a bit more cumbersome at the start, is to use matplotlib.gridspec.GridSpec.
First set up the grid:
import matplotlib.pyplot as plt
from matplotlib.gridspec import GridSpec
fig = plt.figure()
gs = GridSpec(2, 3, width_ratios=[10, 1, 1], height_ratios=[1, 10])
This gives us a grid of 2 rows and 3 columns, where the lower left axis will be 10x10 and the other axes will be either 10x1 or 1x10 in relative sizes. These ratios can be tweaked to your liking. Note that the top center/right axes will be empty.
big_ax = fig.add_subplot(gs[1,0]) # bottom left
top_ax = fig.add_subplot(gs[0,0]) # top left
right_ax = fig.add_subplot(gs[1,1]) # bottom center
cbar_ax = fig.add_subplot(gs[1,2]) # bottom right
I will use a generic genome picture I found via google for the top and right image:
and will generate a random heatmap. I use imshow(aspect='auto') so that the image objects and heatmap take up the full space of their respective axes (otherwise they will override the height/width ratios set by gridspec).
im = plt.imread('/path/to/image.png')
# Plot your heatmap on big_ax and colorbar on cbar_ax
heatmap = big_ax.imshow(np.random.rand(10, 10), aspect='auto', origin='lower')
cbar = fig.colorbar(heatmap, cax=cbar_ax)
# Show your images on top_ax and right_ax
top_ax.imshow(im, aspect='auto')
# need to rotate my image.
# you may not have to if you have two different images
from scipy import ndimage
right_ax.imshow(ndimage.rotate(im, 90), aspect='auto')
# Clean up the image axes (remove ticks, etc.)
right_ax.set_axis_off()
top_ax.set_axis_off()
# remove spacing between axes
fig.subplots_adjust(wspace=0.05, hspace=0.05)
It's not super glamorous (especially with the default jet colormap), but you could easily use this to reproduce the figure your OP.
Edit: So if you want to generate that genome-like plot on the top and right, you could try something like this for the top bar:
from matplotlib.patches import Rectangle
from matplotlib.collections import PatchCollection
# draw the black line
top_ax.axhline(0, color='k', zorder=-1)
# box x-coords and text labels
boxes = zip(np.arange(0.1, 1, 0.2), np.arange(0.2, 1, 0.2))
box_text = ('A1', 'B1', 'B2', 'A2')
# color indicators for boxes
colors = (0, 1, 1, 0)
# construct Rects
patches = [Rectangle(xy=(x0, -1), width=(x1-x0), height=2) for x0,x1 in boxes]
p = PatchCollection(patches, cmap='jet')
# this maps the colors in [0,1] to the cmap above
p.set_array(np.array(colors))
top_ax.add_collection(p)
# add text
[top_ax.text((x0+x1)/2., 1.2, text, ha='center')
for (x0,x1), text in zip(boxes, box_text)]
# adjust ylims
top_ax.set_ylim(-2, 2)
For something the right axis, you can do the same thing but use axvline and swap the x-coords for y-coords.
right_ax.axvline(0, color='k', zorder=-1)
patches = [Rectangle(xy=(-1, y0), width=2, height=(y1-y0)) for y0, y1 in boxes]
p = PatchCollection(patches, cmap='jet')
p.set_array(np.array(colors))
right_ax.add_collection(p)
[right_ax.text(1.2, (y0+y1)/2., text, va='center')
for (y0, y1), text in zip(boxes, box_text)]
right_ax.set_xlim(-2,2)
These modifications lead to something like:

Categories