How to plot heat map with matplotlib? - python

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:

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.

Matplotlib: expand legend vertically

I have a plot whose legend is anchored to the top-right corner: how can I expand the legend to fit the height of the chart?
borderaxespad=0. would expand it horizontally, but I could not find an equivalent to expand it vertically.
I am using matplotlib 2.0
Sample Code:
import numpy as np
x = np.linspace(0, 2*np.pi, 100)
data = [np.sin(x * np.pi/float(el)) for el in range(1, 5)]
fig, ax = plt.subplots(1)
for key, el in enumerate(data):
ax.plot(x, el, label=str(key))
ax.legend(bbox_to_anchor=(1.04,1), loc="upper left", borderaxespad=0., mode='expand')
plt.tight_layout(rect=[0,0,0.8,1])
Which produces:
First to explain the output from the question: When using the 2-tuple notation for bbox_to_anchor, a bounding box without extent is created. The mode="expand" will expand the legend horizontally into this bounding box, which has zero extend, effectively shrinking it to zero size.
The problem is that mode="expand" will expand the legend only horizontally.
From the documentation:
mode : {“expand”, None}
If mode is set to "expand" the legend will be horizontally expanded to fill the axes area (or bbox_to_anchor if defines the legend’s size).
For a solution you need to dig deep into the legend internals. First off you need to set the bbox-to-anchor with a 4-tuple, specifying also width and height of the bbox, bbox_to_anchor=(x0,y0,width,height), where all numbers are in normalized axes coordinates. Then you need to calculate the height of of the legend's _legend_box. Since there is some padding being set, you need to subtract that padding from the bounding box's height. In order to calculate the padding the current legend's fontsize must be known. All of this has to take place after the axes' position is last changed.
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2*np.pi, 100)
data = [np.sin(x * np.pi/float(el)) for el in range(1, 5)]
fig, ax = plt.subplots(1)
for key, el in enumerate(data):
ax.plot(x, el, label=str(key))
# legend:
leg = ax.legend(bbox_to_anchor=(1.04,0.0,0.2,1), loc="lower left",
borderaxespad=0, mode='expand')
plt.tight_layout(rect=[0,0,0.8,1])
# do this after calling tight layout or changing axes positions in any way:
fontsize = fig.canvas.get_renderer().points_to_pixels(leg._fontsize)
pad = 2 * (leg.borderaxespad + leg.borderpad) * fontsize
leg._legend_box.set_height(leg.get_bbox_to_anchor().height-pad)
plt.show()
labelspacing may be what your looking for ?
fig, ax = plt.subplots(1)
for key, el in enumerate(data):
ax.plot(x, el, label=str(key))
ax.legend(labelspacing=8, loc=6, bbox_to_anchor=(1, 0.5))
plt.tight_layout(rect=[0, 0, 0.9, 1])
It is not automatic but you might find some relation with figsize (which is also 8 here).
loc=6, bbox_to_anchor=(1, 0.5) will center you legend on the right hand side of your plot.
Which gives:

matplotlib axesgrid - additional colorbar?

I want to add another colorbar to a plot where I use AxesGrid toolkit. For example, I add a colorbar axes using ImageGrid on the left, and then I add another one on the right manually. Here is a simple example:
f = plt.figure(1)
grid = ImageGrid(f, 111, # similar to subplot(111)
nrows_ncols=(2, 2),
axes_pad=0.01,
add_all=True,
cbar_location="left",
label_mode='L',
cbar_mode="edge",
cbar_size="3%",
cbar_pad="2%",
)
for i in range(3):
m = grid[i].matshow(np.arange(100).reshape((10, 10)))
plt.colorbar(m, grid.cbar_axes[0])
m = grid[3].matshow(np.arange(100).reshape((10, 10)), cmap='plasma')
plt.colorbar(m, shrink=0.5, anchor=(0, 0))
plt.show()
How do I make the new colorbar match the position of one of the subplots in the grid exactly? I at least managed to fix the size and y-position using shrink and anchor... But it also gets a bit complicated if I try to account for the padding between subplots, and if they are rectangular rather than square...
One option is to manually place the colorbar axes according to the position of one of the axes. To this end one first needs to draw the canvas, such that the positions are known. One can then create a new axes according to coordinates of image plot. This new axes will serve as the colorbar axes.
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.axes_grid1 import ImageGrid
fig = plt.figure(1)
grid = ImageGrid(fig, 111, # similar to subplot(111)
nrows_ncols=(2, 2),
axes_pad=0.01,
add_all=True,
cbar_location="left",
label_mode='L',
cbar_mode="edge",
cbar_size="3%",
cbar_pad="2%",
)
for i in range(3):
m = grid[i].matshow(np.arange(100).reshape((10, 10)))
plt.colorbar(m, grid.cbar_axes[0])
m = grid[3].matshow(np.arange(100).reshape((10, 10)), cmap='plasma')
# first draw the figure, such that the axes are positionned
fig.canvas.draw()
#create new axes according to coordinates of image plot
trans = fig.transFigure.inverted()
g3 =grid[3].bbox.transformed(trans)
pos = [g3.x1 + g3.width*0.02, g3.y0, g3.width*0.03, g3.height ]
cax = fig.add_axes(pos) #l,b,w,h
# add colorbar to new axes
plt.colorbar(m, cax=cax)
plt.show()
This method depends on the position of the axes in the figure, once that changes, e.g. because the figure is rezised, unforseable things might happen.
A different method, which does not rely on the drawn coordinates, is to (mis)use inset axes and place the inset outside the axes. In this way the coordinates by which the inset is located are axes coordinates, so the colorbar will change its position according to the axes.
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
cax = inset_axes(grid[3], "3%", "100%", loc=3, bbox_to_anchor=(1.02,0,1,1),
bbox_transform=grid[3].transAxes, borderpad=0.0)
plt.colorbar(m, cax=cax)

Draw Circles on Top Level of Figure

I'm working on a figure where I'm trying to draw a circle on top of a combination colormap and contour plot. The circle keeps getting drawn under the contours instead of on top of them (see the figure below). I've tried reordering how I call imshow, contour, and Circle to see if I can get the circle to display on top, but I haven't had any luck. Is there a way to force Circle to be on the top most level of the figure? Thanks for your help!
Use the zorder kwarg. That controls which elements go on top of each other. So, in this case, you want to increase the zorder of the circle. You may need to experiment to find a zorder that gives you the result you require, but the rule is that higher zorder objects appear on top of lower zorder objects.
Its hard to know exactly without any of your code, but assuming you've used pcolormesh, contour and a Circle patch, this example shows the effect of not setting a zorder (white circle), and setting zorder=10 (red circle).
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
# Fake data
x = np.arange(100)
y = np.arange(100)
X, Y = np.meshgrid(x, y)
z = X**0.5 * Y**0.5
fig, ax = plt.subplots(1)
ax.set_aspect('equal')
ax.pcolormesh(X, Y, z, cmap='viridis')
ax.contour(X, Y, z, colors='k', linewidths=3)
circ1 = Circle((65, 65), 30, facecolor='None', edgecolor='w', lw=5)
circ2 = Circle((35, 35), 30, facecolor='None', edgecolor='r', lw=5, zorder=10)
ax.add_patch(circ1)
ax.add_patch(circ2)
plt.show()
Note that the white circle lies beneath the black contour lines, but by increasing the zorder to 10, the red circle lies on top of the contour lines.
You can set the zorder property of the plot object to force it to be on top of other plots within the same axes. A higher zorder value will appear on top of a lower zorder value.
plt.plot([1, 2], [1, 2], zorder=100)
By default, patches have a zorder of 1, 2D line objects have a zorder of 2 and text has a zorder of 3.

Categories