I have made a heatmap in seaborn, and I need to have text in the corners.
Have:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(2021)
data = pd.DataFrame(np.random.randint(0, 5, (3, 5)))\
.rename({0: "Supercalifragilisticexpialidocious",
1: "Humuhumunukunukuapua'a",
2: "Hippopotomonstrosesquipedaliophobia"
})
sns.heatmap(data)
plt.savefig("dave.png")
Want:
The plt.figtext command has worked for this in the past, but I am frustrated in formatting the right and left justification (and distance from the top and bottom), so I just want to have a standard distance from the edges, which sounds like justification. It sounds like that is a matter of changing the coordinates in figtext, which I have not figured out how to do, but I think that is not quite enough. Since the plot can extend very far to the left, I need the GHI and JKL to be to the left of the saved image, not just of the plotting area. The lengths of those words on the left can vary from plot to plot, and I want GHI and JKL left-justified no matter what, whether the long word is "Hippopotomonstrosesquipedaliophobia" or "Dave" (but it shouldn't be way to the left, beyond the left edge of the words, when those words are short).
What would be the way to execute this?
I suppose it would be nice to know how to have such an image appear in a Jupyter Notebook or pop up when I run my script from the command line, but I mostly care about saving the images with those ABC, DEF, GHI, and JKL comments.
One approach is to use a Gridspec. Define a grid with 4 rows and columns and create axes for each plot position in the grid.
In the center of the grid you plot the heatmap and in the borders of the grid you plot the labels.
The borders will define a 1x1 plot, you can adjust the labels position inside each individual plot with axis.text() or give more space to the labels by adding more rows and columns to the grid.
At the end hide the axis of each plot label with ax.axis('off') and you get the desire output
Full example
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(2021)
data = pd.DataFrame(np.random.randint(0, 5, (3, 5)))\
.rename(
{
0: "Supercalifragilisticexpialidocious",
1: "Humuhumunukunukuapua'a",
2: "Hippopotomonstrosesquipedaliophobia"
}
)
gs = gridspec.GridSpec(4, 4)
# Upper left label
ax1 = plt.subplot(gs[:1, :1])
ax1.text(0.5, 0.5, 'GHI', fontsize='xx-large')
ax1.axis('off')
# Lower left label
ax2 = plt.subplot(gs[3:4, :1])
ax2.text(0.5, 0.5, 'JKL', fontsize='xx-large')
ax2.axis('off')
# Upper right label
ax3 = plt.subplot(gs[:1, 3:4])
ax3.text(0, 0.5, 'ABC', fontsize='xx-large')
ax3.axis('off')
# Lower Right label
ax4 = plt.subplot(gs[3:4, 3:4])
ax4.text(0, 0.5, 'DEF', fontsize='xx-large')
ax4.axis('off')
ax5 = plt.subplot(gs[1:3, 1:3])
sns.heatmap(data, ax=ax5)
References
Center the third subplot in the middle of second row python
Hiding axis text in matplotlib plots
Putting text in top left corner of matplotlib plot
To put the text at the corners of your plot you need to
Make some room around your plot for the text
Get the extent (bounding box) of your total plot for the text positions.
For 1) you can specify a constrained layout with some pads large enough to accomodate the text. For 2) you need to get the bounding boxes of the heatmap itself and its colorbar in figure coordinates. Then you take the union of these two boxes and place your texts with the corresponding alignments at the four corners of this unified bounding box.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
np.random.seed(2021)
data = pd.DataFrame(np.random.randint(0, 5, (3, 5)))\
.rename({0: "Supercalifragilisticexpialidocious",
1: "Humuhumunukunukuapua'a",
2: "Hippopotomonstrosesquipedaliophobia"
})
fig, ax = plt.subplots(constrained_layout={'w_pad': .5, 'h_pad': .5})
sns.heatmap(data, ax=ax)
fig.draw_without_rendering()
cb = fig.axes[1]
ax_extent = fig.transFigure.inverted().transform_bbox(ax.get_tightbbox(fig.canvas.get_renderer()))
cb_extent = fig.transFigure.inverted().transform_bbox(cb.get_tightbbox(fig.canvas.get_renderer()))
total_extent = ax_extent.union([ax_extent, cb_extent])
# uncomment the following to show axes and colorbar extents
#from matplotlib.patches import Rectangle
#for extent in (ax_extent, cb_extent):
# fig.add_artist(Rectangle(extent.p0, extent.width, extent.height, ec='.6', ls='dotted', fill=False))
fig.text(total_extent.x1, total_extent.y1, 'ABC', ha='center', va='bottom', fontsize=20)
fig.text(total_extent.x1, total_extent.y0, 'DEF', ha='center', va='top', fontsize=20)
fig.text(total_extent.x0, total_extent.y1, 'GHI', ha='center', va='bottom', fontsize=20)
fig.text(total_extent.x0, total_extent.y0, 'KLM', ha='center', va='top', fontsize=20)
This also works without any change for short y labels, there's no need to fiddle with text positions:
Depending on your likings you can change the horizontal alignment of the texts from 'center' to 'right' and 'left' respectively.
To better understand how it works, you can uncomment the three lines for visualizing the bounding boxes. From here you'll see that we need the union of the two boxes to neatly put the texts at the same y position as the colorbar extends a bit beyond the heatmap:
Related
I'm trying to figure out how to align text with the far left of a graph in matplotlib. I can accomplish this by hard coding the value (in the example below x=-.98), however this takes a lot of trial and error.
Is there away to return the coordinates of the figure border (not the graph border) or to set the text to start at the far left of the figure?
# matplotlib example of text align
import matplotlib.pyplot as plt
# x and y axis data
y_axis_labels = ['y-label-1','y-label-2','y-label-3','y-label-4']
x_axis_labels = [1,2,3,4]
# create horizontal bar plot
fig, ax = plt.subplots()
ax.barh(y_axis_labels, x_axis_labels)
# Align the text with the far left of the y_axis labels
plt.text(x=-.98,y=4, s='start at far left', color='red')
plt.show()
Link to example image (I want the red text to align with blue line)
Thanks!
Replace
plt.text(x=-.98,y=4, s='start at far left', color='red')
with
ax.set_title('start at far left',loc='right',color='red' )
Instead of words or numbers being the tick labels of the x axis, I want to draw a simple drawing (made of lines and circles) as the label for each x tick. Is this possible? If so, what is the best way to go about it in matplotlib?
I would remove the tick labels and replace the text with patches. Here is a brief example of performing this task:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# define where to put symbols vertically
TICKYPOS = -.6
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(range(10))
# set ticks where your images will be
ax.get_xaxis().set_ticks([2,4,6,8])
# remove tick labels
ax.get_xaxis().set_ticklabels([])
# add a series of patches to serve as tick labels
ax.add_patch(patches.Circle((2,TICKYPOS),radius=.2,
fill=True,clip_on=False))
ax.add_patch(patches.Circle((4,TICKYPOS),radius=.2,
fill=False,clip_on=False))
ax.add_patch(patches.Rectangle((6-.1,TICKYPOS-.05),.2,.2,
fill=True,clip_on=False))
ax.add_patch(patches.Rectangle((8-.1,TICKYPOS-.05),.2,.2,
fill=False,clip_on=False))
This results in the following figure:
It is key to set clip_on to False, otherwise patches outside the axes will not be shown. The coordinates and sizes (radius, width, height, etc.) of the patches will depend on where your axes is in the figure. For example, if you are considering doing this with subplots, you will need to be sensitive of the patches placement so as to not overlap any other axes. It may be worth your time investigating Transformations, and defining the positions and sizes in an other unit (Axes, Figure or display).
If you have specific image files that you want to use for the symbols, you can use the BboxImage class to create artists to be added to the axes instead of patches. For example I made a simple icon with the following script:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(1,1),dpi=400)
ax = fig.add_axes([0,0,1,1],frameon=False)
ax.set_axis_off()
ax.plot(range(10),linewidth=32)
ax.plot(range(9,-1,-1),linewidth=32)
fig.savefig('thumb.png')
producing this image:
Then I created a BboxImage at the location I want the tick label and of the size I want:
lowerCorner = ax.transData.transform((.8,TICKYPOS-.2))
upperCorner = ax.transData.transform((1.2,TICKYPOS+.2))
bbox_image = BboxImage(Bbox([lowerCorner[0],
lowerCorner[1],
upperCorner[0],
upperCorner[1],
]),
norm = None,
origin=None,
clip_on=False,
)
Noticed how I used the transData transformation to convert from data units to display units, which are required in the definition of the Bbox.
Now I read in the image using the imread routine, and set it's results (a numpy array) to the data of bbox_image and add the artist to the axes:
bbox_image.set_data(imread('thumb.png'))
ax.add_artist(bbox_image)
This results in an updated figure:
If you do directly use images, make sure to import the required classes and methods:
from matplotlib.image import BboxImage,imread
from matplotlib.transforms import Bbox
The other answer has some drawbacks because it uses static coordinates. It will hence not work when changing the figure size or zooming and panning the plot.
A better option is to directly define the positions in the coordinate systems of choice. For the xaxis it makes sense to use data coordinates for the x position and axes coordinates for y position.
Using matplotlib.offsetboxes makes this rather simple. The following would position a box with a circle and a box with an image at coordinates (-5,0) and (5,0) respectively and offsets them a bit to the lower such that they'll look as if they were ticklabels.
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.offsetbox import (DrawingArea, OffsetImage,AnnotationBbox)
fig, ax = plt.subplots()
ax.plot([-10,10], [1,3])
# Annotate the 1st position with a circle patch
da = DrawingArea(20, 20, 10, 10)
p = mpatches.Circle((0, 0), 10)
da.add_artist(p)
ab = AnnotationBbox(da, (-5,0),
xybox=(0, -7),
xycoords=("data", "axes fraction"),
box_alignment=(.5, 1),
boxcoords="offset points",
bboxprops={"edgecolor" : "none"})
ax.add_artist(ab)
# Annotate the 2nd position with an image
arr_img = plt.imread("https://i.stack.imgur.com/FmX9n.png", format='png')
imagebox = OffsetImage(arr_img, zoom=0.2)
imagebox.image.axes = ax
ab = AnnotationBbox(imagebox, (5,0),
xybox=(0, -7),
xycoords=("data", "axes fraction"),
boxcoords="offset points",
box_alignment=(.5, 1),
bboxprops={"edgecolor" : "none"})
ax.add_artist(ab)
plt.show()
Note that many shapes exist as unicode symbols, such that one can simply set the ticklabels with those symbols. For such a solution, see How to use a colored shape as yticks in matplotlib or seaborn?
Instead of words or numbers being the tick labels of the x axis, I want to draw a simple drawing (made of lines and circles) as the label for each x tick. Is this possible? If so, what is the best way to go about it in matplotlib?
I would remove the tick labels and replace the text with patches. Here is a brief example of performing this task:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# define where to put symbols vertically
TICKYPOS = -.6
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(range(10))
# set ticks where your images will be
ax.get_xaxis().set_ticks([2,4,6,8])
# remove tick labels
ax.get_xaxis().set_ticklabels([])
# add a series of patches to serve as tick labels
ax.add_patch(patches.Circle((2,TICKYPOS),radius=.2,
fill=True,clip_on=False))
ax.add_patch(patches.Circle((4,TICKYPOS),radius=.2,
fill=False,clip_on=False))
ax.add_patch(patches.Rectangle((6-.1,TICKYPOS-.05),.2,.2,
fill=True,clip_on=False))
ax.add_patch(patches.Rectangle((8-.1,TICKYPOS-.05),.2,.2,
fill=False,clip_on=False))
This results in the following figure:
It is key to set clip_on to False, otherwise patches outside the axes will not be shown. The coordinates and sizes (radius, width, height, etc.) of the patches will depend on where your axes is in the figure. For example, if you are considering doing this with subplots, you will need to be sensitive of the patches placement so as to not overlap any other axes. It may be worth your time investigating Transformations, and defining the positions and sizes in an other unit (Axes, Figure or display).
If you have specific image files that you want to use for the symbols, you can use the BboxImage class to create artists to be added to the axes instead of patches. For example I made a simple icon with the following script:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(1,1),dpi=400)
ax = fig.add_axes([0,0,1,1],frameon=False)
ax.set_axis_off()
ax.plot(range(10),linewidth=32)
ax.plot(range(9,-1,-1),linewidth=32)
fig.savefig('thumb.png')
producing this image:
Then I created a BboxImage at the location I want the tick label and of the size I want:
lowerCorner = ax.transData.transform((.8,TICKYPOS-.2))
upperCorner = ax.transData.transform((1.2,TICKYPOS+.2))
bbox_image = BboxImage(Bbox([lowerCorner[0],
lowerCorner[1],
upperCorner[0],
upperCorner[1],
]),
norm = None,
origin=None,
clip_on=False,
)
Noticed how I used the transData transformation to convert from data units to display units, which are required in the definition of the Bbox.
Now I read in the image using the imread routine, and set it's results (a numpy array) to the data of bbox_image and add the artist to the axes:
bbox_image.set_data(imread('thumb.png'))
ax.add_artist(bbox_image)
This results in an updated figure:
If you do directly use images, make sure to import the required classes and methods:
from matplotlib.image import BboxImage,imread
from matplotlib.transforms import Bbox
The other answer has some drawbacks because it uses static coordinates. It will hence not work when changing the figure size or zooming and panning the plot.
A better option is to directly define the positions in the coordinate systems of choice. For the xaxis it makes sense to use data coordinates for the x position and axes coordinates for y position.
Using matplotlib.offsetboxes makes this rather simple. The following would position a box with a circle and a box with an image at coordinates (-5,0) and (5,0) respectively and offsets them a bit to the lower such that they'll look as if they were ticklabels.
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.offsetbox import (DrawingArea, OffsetImage,AnnotationBbox)
fig, ax = plt.subplots()
ax.plot([-10,10], [1,3])
# Annotate the 1st position with a circle patch
da = DrawingArea(20, 20, 10, 10)
p = mpatches.Circle((0, 0), 10)
da.add_artist(p)
ab = AnnotationBbox(da, (-5,0),
xybox=(0, -7),
xycoords=("data", "axes fraction"),
box_alignment=(.5, 1),
boxcoords="offset points",
bboxprops={"edgecolor" : "none"})
ax.add_artist(ab)
# Annotate the 2nd position with an image
arr_img = plt.imread("https://i.stack.imgur.com/FmX9n.png", format='png')
imagebox = OffsetImage(arr_img, zoom=0.2)
imagebox.image.axes = ax
ab = AnnotationBbox(imagebox, (5,0),
xybox=(0, -7),
xycoords=("data", "axes fraction"),
boxcoords="offset points",
box_alignment=(.5, 1),
bboxprops={"edgecolor" : "none"})
ax.add_artist(ab)
plt.show()
Note that many shapes exist as unicode symbols, such that one can simply set the ticklabels with those symbols. For such a solution, see How to use a colored shape as yticks in matplotlib or seaborn?
I plot a graph created by networkx with matplotlib. Now, I'd like to add annotation to a specific node with a circle around. For instance,
I use plt.annotate(*args, **kwargs) with the following code,
# add annotate text
pos = nx.get_node_attributes(G, 'pos')
pos_annotation_node = pos['Medici']
ax2.annotate('Midici',
xy=pos_annotation_node,
xytext=(i+0.2 for i in pos_annotation_node),
color='blue',
arrowprops=dict(facecolor='blue', shrink=0.01)
)
And I got this ugly graph,
I have two questions:
how do I draw a circle around the node 6 as shown in first figure.
to get a nice looking figure, I need manually set the value of xytext many times. Is there a better way?
If you use the fancyarrow arrowprops syntax as demonstrated in annotation_demo2, there is a shrinkA and shrinkB option that lets you shrink your arrow tail (shrinkA) and tip (shrinkB) independently, in points units.
Here's some arbitrary setup code:
import matplotlib.pyplot as plt
import numpy as np
# Some data:
dat = np.array([[5, 3, 4, 4, 6],
[1, 5, 3, 2, 2]])
# This is the point you want to point out
point = dat[:, 2]
# Make the figure
plt.figure(1, figsize=(4, 4))
plt.clf()
ax = plt.gca()
# Plot the data
ax.plot(dat[0], dat[1], 'o', ms=10, color='r')
ax.set_xlim([2, 8])
ax.set_ylim([0, 6])
And here is the code that puts a circle around one of these points and draws an arrow that is shrunk-back at the tip only:
circle_rad = 15 # This is the radius, in points
ax.plot(point[0], point[1], 'o',
ms=circle_rad * 2, mec='b', mfc='none', mew=2)
ax.annotate('Midici', xy=point, xytext=(60, 60),
textcoords='offset points',
color='b', size='large',
arrowprops=dict(
arrowstyle='simple,tail_width=0.3,head_width=0.8,head_length=0.8',
facecolor='b', shrinkB=circle_rad * 1.2)
)
Note here that:
1) I've made the marker face color of the circle transparent with mfc='none', and set the circle size (diameter) to twice the radius.
2) I've shrunk the arrow by 120% of the circle radius so that it backs off of the circle just a bit. Obviously you can play with circle_rad and the value of 1.2 until you get what you want.
3) I've used the "fancy" syntax that defines several of the arrow properties in a string, rather than in the dict. As far as I can tell the shrinkB option is not available if you don't use the fancy arrow syntax.
4) I've used the textcoords='offset points' so that I can specify the position of the text relative to the point, rather than absolute on the axes.
how do I draw a circle around the node 6 as shown in first figure.
You get a center of node #6 (tuple pos). Use this data to set the blue circle position.
to get a nice looking figure, I need manually set the value of xytext many times. Is there a better way?
Make a list of your labels and iterate in it and in tuples of coordinates of nodes to post annotate text. Look to comments of a code.
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from matplotlib.patches import Circle
import matplotlib.patches as patches
import numpy as np
from matplotlib.font_manager import FontProperties
font = FontProperties()
font.set_weight('bold')
font.set_size('medium')
labels = ["Midici","Firenze"]
image = mpimg.imread("g.png") # just a image of your graph
plt.imshow(image)
ax = plt.gca()
# set your own radius and centers of circles in loop, like here
r = 11; c = (157,177)
circ1 = patches.Circle(c,2*r,lw=3.,ec='b',fill=False)
ax.add_artist(circ1)
circ1.set_clip_box(ax.bbox)
# annotate circles
# I have one circle but for your array pos_annotation_node
# you need 'i' to extract proper position
for i,label in enumerate(labels):
annot_array_end = (c[0], c[1]+2*(-1)**i*r)
annot_text_pos = (c[0]+3,c[1]+5*(-1)**i*r)
ax.annotate(label,
xy= annot_array_end,
xytext=annot_text_pos,
color='b',
fontproperties=font,
arrowprops=dict(fc='b', shrink=.005)
)
plt.show()
Just an observation for other people finding this thread. Not everything has to be done in Matplotlib.
It might well be easier to use a drawing package, with your network chart saved as a PDF (or PNG) in the background...
Instead of words or numbers being the tick labels of the x axis, I want to draw a simple drawing (made of lines and circles) as the label for each x tick. Is this possible? If so, what is the best way to go about it in matplotlib?
I would remove the tick labels and replace the text with patches. Here is a brief example of performing this task:
import matplotlib.pyplot as plt
import matplotlib.patches as patches
# define where to put symbols vertically
TICKYPOS = -.6
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(range(10))
# set ticks where your images will be
ax.get_xaxis().set_ticks([2,4,6,8])
# remove tick labels
ax.get_xaxis().set_ticklabels([])
# add a series of patches to serve as tick labels
ax.add_patch(patches.Circle((2,TICKYPOS),radius=.2,
fill=True,clip_on=False))
ax.add_patch(patches.Circle((4,TICKYPOS),radius=.2,
fill=False,clip_on=False))
ax.add_patch(patches.Rectangle((6-.1,TICKYPOS-.05),.2,.2,
fill=True,clip_on=False))
ax.add_patch(patches.Rectangle((8-.1,TICKYPOS-.05),.2,.2,
fill=False,clip_on=False))
This results in the following figure:
It is key to set clip_on to False, otherwise patches outside the axes will not be shown. The coordinates and sizes (radius, width, height, etc.) of the patches will depend on where your axes is in the figure. For example, if you are considering doing this with subplots, you will need to be sensitive of the patches placement so as to not overlap any other axes. It may be worth your time investigating Transformations, and defining the positions and sizes in an other unit (Axes, Figure or display).
If you have specific image files that you want to use for the symbols, you can use the BboxImage class to create artists to be added to the axes instead of patches. For example I made a simple icon with the following script:
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(1,1),dpi=400)
ax = fig.add_axes([0,0,1,1],frameon=False)
ax.set_axis_off()
ax.plot(range(10),linewidth=32)
ax.plot(range(9,-1,-1),linewidth=32)
fig.savefig('thumb.png')
producing this image:
Then I created a BboxImage at the location I want the tick label and of the size I want:
lowerCorner = ax.transData.transform((.8,TICKYPOS-.2))
upperCorner = ax.transData.transform((1.2,TICKYPOS+.2))
bbox_image = BboxImage(Bbox([lowerCorner[0],
lowerCorner[1],
upperCorner[0],
upperCorner[1],
]),
norm = None,
origin=None,
clip_on=False,
)
Noticed how I used the transData transformation to convert from data units to display units, which are required in the definition of the Bbox.
Now I read in the image using the imread routine, and set it's results (a numpy array) to the data of bbox_image and add the artist to the axes:
bbox_image.set_data(imread('thumb.png'))
ax.add_artist(bbox_image)
This results in an updated figure:
If you do directly use images, make sure to import the required classes and methods:
from matplotlib.image import BboxImage,imread
from matplotlib.transforms import Bbox
The other answer has some drawbacks because it uses static coordinates. It will hence not work when changing the figure size or zooming and panning the plot.
A better option is to directly define the positions in the coordinate systems of choice. For the xaxis it makes sense to use data coordinates for the x position and axes coordinates for y position.
Using matplotlib.offsetboxes makes this rather simple. The following would position a box with a circle and a box with an image at coordinates (-5,0) and (5,0) respectively and offsets them a bit to the lower such that they'll look as if they were ticklabels.
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.offsetbox import (DrawingArea, OffsetImage,AnnotationBbox)
fig, ax = plt.subplots()
ax.plot([-10,10], [1,3])
# Annotate the 1st position with a circle patch
da = DrawingArea(20, 20, 10, 10)
p = mpatches.Circle((0, 0), 10)
da.add_artist(p)
ab = AnnotationBbox(da, (-5,0),
xybox=(0, -7),
xycoords=("data", "axes fraction"),
box_alignment=(.5, 1),
boxcoords="offset points",
bboxprops={"edgecolor" : "none"})
ax.add_artist(ab)
# Annotate the 2nd position with an image
arr_img = plt.imread("https://i.stack.imgur.com/FmX9n.png", format='png')
imagebox = OffsetImage(arr_img, zoom=0.2)
imagebox.image.axes = ax
ab = AnnotationBbox(imagebox, (5,0),
xybox=(0, -7),
xycoords=("data", "axes fraction"),
boxcoords="offset points",
box_alignment=(.5, 1),
bboxprops={"edgecolor" : "none"})
ax.add_artist(ab)
plt.show()
Note that many shapes exist as unicode symbols, such that one can simply set the ticklabels with those symbols. For such a solution, see How to use a colored shape as yticks in matplotlib or seaborn?