How to place clip art behind plotted data in matplotlib - python

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

Related

Circle object changes position depending on image save format in Matplotlib

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

Box gets truncated using zoomed_inset_axes

I want to use zoomed_inset_axes but the box gets truncated as soon as it passes the frame of the main figure. I could not get any better with
f.tight_layout()
f.subplots_adjust(bottom=...)
'figure.autolayout': True
not even with hidden (white) text using f.text somewhere outside.
Does anyone know how to do this properly?
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes, mark_inset
X = np.random.normal(.5,10,1000)
Y = np.random.normal(.5,10,1000)
f, ax = plt.subplots(1, figsize=(10,6))
ax.scatter(X,Y)
# # Setup zoom window
axins = zoomed_inset_axes(ax, 2, loc="center", bbox_to_anchor=(0,0))
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
axins.set_xlim([-15,0])
axins.set_ylim([-12,-3])
# # Plot zoom window
axins.scatter(X,Y)
f.tight_layout()
f.savefig('test.png', dpi=70)
Using subplots_adjust goes in the right direction. Don't use tight_layout afterwards as this would overwrite any settings done via subplots_adjust.
You may decide to opt for something like
fig.subplots_adjust(left=0.2, bottom=0.2)
to make some space for the inset in the lower left corner of the figure.
Then you need to position the inset. Since here you're working in the lower left corner, this is relatively easy. The loc parameter needs to be set to the lower left corner and you may stick to the bbox_to_anchor=(0,0) position. Then just add some padding via borderpad=3 (in units of font size), such that the inset axes' labels are still visible,
zoomed_inset_axes(ax, 2, loc='lower left', bbox_to_anchor=(0,0), borderpad=3)
Complete code:
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1.inset_locator import zoomed_inset_axes, mark_inset
X = np.random.normal(.5,10,1000)
Y = np.random.normal(.5,10,1000)
fig, ax = plt.subplots(1, figsize=(10,6))
fig.subplots_adjust(left=0.2, bottom=0.2)
ax.scatter(X,Y)
# # Setup zoom window
axins = zoomed_inset_axes(ax, 2, loc='lower left', bbox_to_anchor=(0,0), borderpad=3)
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
axins.set_xlim([-15,0])
axins.set_ylim([-12,-3])
# # Plot zoom window
axins.scatter(X,Y)
#fig.savefig('test.png', dpi=70)
plt.show()
In general, you have a lot of options to position and size the inset. I recently created a new example on the matplotlib page: Inset Locator Demo, which is currently only available in the devdocs, to show the interplay between the different parameters (in that case for inset_axes - but it totally applies to zoomed_inset_axes as well).

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:

Colour fill based on values?

I am looking for a way in Python/matplotlib/pandas to create a color fill for a graph similar to this (Source: http://www.scminc.com/resources/SCM_TIPSTRICKS_Petrel_Well_Sections_2013_July14.pdf):
It uses a color map for the fill (left of the image), and based on a specific interval on the x-axis assigns a color to it. Unfortunately, I haven't found a solution, and since I am pretty new to Python in general, I am unable to find a way to do that.
Many thanks
You can plot the fill as a background with imshow, then clip it. You can use fill_betweenx to make the mask.
Here's an example using random data:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import PathPatch
# Make a random x and a y to go with it.
np.random.seed(26)
x = np.random.normal(0, 1, 200).cumsum()
y = np.arange(x.size)
# Set up the figure.
fig, ax = plt.subplots(figsize=(2, 10))
# Make the background 'image'.
im = ax.imshow(x.reshape(-1, 1),
aspect='auto',
origin='lower',
extent=[x.min(), x.max(), y.min(), y.max()]
)
# Draw the path.
paths = ax.fill_betweenx(y, x, x.min(),
facecolor='none',
lw=2,
edgecolor='b',
)
# Make the 'fill' mask and clip the background image with it.
patch = PathPatch(paths._paths[0], visible=False)
ax.add_artist(patch)
im.set_clip_path(patch)
# Finish up.
ax.invert_yaxis()
plt.show()
This yields:

How to change color bar to align with main plot in Matplotlib?

When plotting matrix with imshow in Matplotlib, how to change colorbar legend bar size, location, font and other parameters?
Here I created an example code
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
def plot_matrix(mat, title='example', cmap=plt.cm.Blues):
plt.imshow(mat, interpolation='nearest', cmap=cmap)
plt.grid(False)
plt.title(title)
plt.colorbar()
data = np.random.random((20, 20))
plt.figure(figsize=(8,8))
plt.tick_params(axis='both', which='major', labelsize=12)
plot_matrix(data)
In a real use case, I got complex labels and the legend bar becomes much higher then the matrix itself. I want to change the legend bar to make the plot more efficiently use the space.
I found a documentation for the matplotlib.pyplot.colorbar, however have not figure out a good way to set the size, location and font size for the color legend bar.
imshow enforces a 1:1 aspect (by default, but you can change it with aspect parameter), which makes things a little trickier. To always get consistent result, I might suggest manually specify the size of axes:
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
def plot_matrix(mat, figsize, title='example', cmap=plt.cm.Blues):
f = plt.figure(figsize=figsize)
ax = plt.axes([0, 0.05, 0.9, 0.9 ]) #left, bottom, width, height
#note that we are forcing width:height=1:1 here,
#as 0.9*8 : 0.9*8 = 1:1, the figure size is (8,8)
#if the figure size changes, the width:height ratio here also need to be changed
im = ax.imshow(mat, interpolation='nearest', cmap=cmap)
ax.grid(False)
ax.set_title(title)
cax = plt.axes([0.95, 0.05, 0.05,0.9 ])
plt.colorbar(mappable=im, cax=cax)
return ax, cax
data = np.random.random((20, 20))
ax, cax = plot_matrix(data, (8,8))
Now you have the axis where the colorbar is plotted in, cax. You can do a lot of thing with that, say, rotate the labels, using plt.setp(cax.get_yticklabels(), rotation=45)

Categories