I have a code that draws hundreds of small rectangles on top of an image :
The rectangles are instances of
matplotlib.patches.Rectangle
I'd like to put a text (actually a number) into these rectangles, I don't see a way to do that. matplotlib.text.Text seems to allow one to insert text surrounded by a rectangle however I want the rectangle to be at a precise position and have a precise size and I don't think that can be done with text().
I think you need to use the annotate method of your axes object.
You can use properties of the rectangle to be smart about it. Here's a toy example:
import matplotlib.pyplot as plt
import matplotlib.patches as mpatch
fig, ax = plt.subplots()
rectangles = {'skinny' : mpatch.Rectangle((2,2), 8, 2),
'square' : mpatch.Rectangle((4,6), 6, 6)}
for r in rectangles:
ax.add_artist(rectangles[r])
rx, ry = rectangles[r].get_xy()
cx = rx + rectangles[r].get_width()/2.0
cy = ry + rectangles[r].get_height()/2.0
ax.annotate(r, (cx, cy), color='w', weight='bold',
fontsize=6, ha='center', va='center')
ax.set_xlim((0, 15))
ax.set_ylim((0, 15))
ax.set_aspect('equal')
plt.show()
This is an example.
import matplotlib.pyplot as plt
left, width = .25, .25
bottom, height = .25, .25
right = left + width
top = bottom + height
fig, ax = plt.subplots(figsize=(10, 10),sharex=True, sharey=True)
fig.patches.extend([plt.Rectangle((left, bottom), width, height,
facecolor='none',
edgecolor='red',
#fill=True,
#color='black',
#alpha=0.5,
#zorder=1000,
lw=5,
transform=fig.transFigure, figure=fig)])
fig.text(0.5*(left+right), 0.5*(bottom+top), 'Your text',
horizontalalignment='center',
verticalalignment='center',
fontsize=20, color='black',
transform=fig.transFigure)
Related
I created this tree map using Matplotlib and Squarify:
Now I would like to add a line plot on each rectangle in the tree map. Is that possible?
Squarify's plot is a convenience function to directly plot a treemap given values and labels. But, this process can also be executed step-by-step. One of the steps is to calculate the positions of the rectangles, for which we suppose a figure which has coordinates from 0,0 to 1,1 from lower left to upper right.
With these rectangles we can manually position axes to draw on. It is unclear whether ticks are needed. If needed, they can be placed inside. Or the axes could be moved completely to the center of each subplot. Or only have ticks without labels.
Here is some demonstrating code:
import numpy as np
import matplotlib.pyplot as plt
import squarify
values = [500, 433, 331, 254, 119]
values.sort(reverse=True) # values must be sorted descending (and positive)
# the sum of the values must equal the total area to be laid out; i.e., sum(values) == width * height
values = squarify.normalize_sizes(values, 1, 1)
rects = squarify.squarify(values, 0, 0, 1, 1)
fig = plt.figure(figsize=(7, 5))
axes = [fig.add_axes([rect['x'], rect['y'], rect['dx'], rect['dy'], ]) for rect in rects]
for ax, color in zip(axes, plt.cm.Pastel1.colors):
x = np.linspace(0, 10, 100)
y = np.random.normal(0.01, 0.1, 100).cumsum()
ax.plot(x, y)
ax.tick_params(axis="y", direction="in", pad=-15)
ax.tick_params(axis="x", direction="in", pad=-15)
plt.setp(ax.get_yticklabels(), ha="left")
ax.set_facecolor(color)
plt.show()
Here is another example resembling the image in the question, with a main plot and a colorbar. The default mplcursors gets confused with all these axes, but annotations while hovering can also be added manually.
import numpy as np
import matplotlib.pyplot as plt
import squarify
values = [4000, 1500, 1500, 1200, 1000, 500]
fig, mainax = plt.subplots(figsize=(6, 4))
mainax.set_xlim(0, 1000)
mainax.set_ylim(0, 1000)
mainax.grid(False)
cmap = plt.cm.get_cmap('Greens')
norm = plt.Normalize(vmin=0, vmax=max(values))
plt.colorbar(plt.cm.ScalarMappable(cmap=cmap, norm=norm))
pos = mainax.get_position()
values.sort(reverse=True)
normalized_values = squarify.normalize_sizes(values, pos.width, pos.height)
rects = squarify.squarify(normalized_values, pos.x0, pos.y0, pos.width, pos.height)
axes = [fig.add_axes([rect['x'], rect['y'], rect['dx'], rect['dy'], ]) for rect in rects]
for ax, val in zip(axes, values):
x = np.linspace(0, 10, 100)
y = np.random.normal(0.01, 0.1, 100).cumsum()
ax.plot(x, y)
ax.set_xticks([])
ax.set_yticks([])
ax.set_facecolor(cmap(norm(val)))
mainax.set_facecolor('none') # prevent that the mainax blocks the other axes
mainax.set_zorder(20) # high z-order because the annotations are drawn using this ax
labels = ['a', 'b', 'c', 'd', 'e', 'f']
sum_val = sum(values)
annotations = [mainax.annotate(f"'{lbl}': {val}\n{val * 100.0 / sum_val:.1f} %",
xy=(0, 0), xycoords='figure pixels',
xytext=(0, 0), textcoords='offset points',
bbox=dict(boxstyle='round', fc='lemonchiffon'),
ha='center', va='bottom')
for ax, val, lbl in zip(axes, values, labels)]
for annot in annotations:
annot.set_visible(False)
def hover(event):
for ax, annot in zip(axes, annotations):
if ax.bbox.contains(event.x, event.y):
annot.xy = (event.x, event.y)
annot.set_visible(True)
else:
annot.set_visible(False)
fig.canvas.draw_idle()
fig.canvas.mpl_connect("motion_notify_event", hover)
plt.show()
Yes it is possible. You will have to write the code to extract the exact positions where you want to place the new plot in.
You need to set the position of the new figure using f.canvas.manager.window.SetPosition
This answer will greatly help https://stackoverflow.com/a/37999370/4551984
I'm trying to plot the labels of some contours and an ellipse in a single legend. I'm almost there (code below), but I'd like the shape associated to the ellipse in the legend to be a straight line, instead of a rectangle as it is by default.
How can I change this?
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse
# Random data
ndim, nsamples = 2, 1000
samples = np.random.randn(ndim * nsamples).reshape([nsamples, ndim])
fig, ax = plt.subplots()
x, y = samples.T
H, X, Y = plt.hist2d(x, y, bins=20, cmap=plt.get_cmap('Greys'))[:-1]
# Plot two contours
contour = ax.contour(
H.T, levels=(5, 10), extent=[x.min(), x.max(), y.min(), y.max()])
# Plot ellipse
ellipse = Ellipse(xy=(0., 0.), width=3., height=2, angle=45, edgecolor='r', fc='None', label='ellipse')
ax.add_patch(ellipse)
# Get ellipse's handle and label
ellip, ellip_lbl = ax.get_legend_handles_labels()
# Plot legend
plt.legend(ellip + list(reversed(contour.collections)), ellip_lbl + ['1s', '2s'])
plt.show()
Below is the solution based on this answer. The main idea here is to use ls="-", by plotting an empty list and grabbing its handle. Store the ellipse's patch in ax1 and use it to get the label.
ellipse = Ellipse(xy=(0., 0.), width=3., height=2, angle=45, edgecolor='r', fc='None', label='ellipse')
ax1 = ax.add_patch(ellipse)
# Get ellipse's handle and label
ellip, ellip_lbl = ax.get_legend_handles_labels()
plt.legend(handles = [plt.plot([],ls="-", color='r')[0]] + list(reversed(contour.collections)),
labels=[ax1.get_label()] + ['1s', '2s'])
I'm adding a text inside a shape by:
ax.text(x,y,'text', ha='center', va='center',bbox=dict(boxstyle='circle', fc="w", ec="k"),fontsize=10) (ax is AxesSubplot)
The problem is that I couldn't make the circle size constant while changing the string length. I want the text size adjust to the circle size and not the other way around.
The circle is even completely gone if the string is an empty one.
The only bypass to the problem I had found is dynamically to set the fontsize param according to the len of the string, but that's too ugly and not still the circle size is not completely constant.
EDIT (adding a MVCE):
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_axes([0,0,1,1])
ax.text(0.5,0.5,'long_text', ha='center', va='center',bbox=dict(boxstyle='circle', fc="w", ec="k"),fontsize=10)
ax.text(0.3,0.7,'short', ha='center', va='center',bbox=dict(boxstyle='circle', fc="w", ec="k"),fontsize=10)
plt.show()
Trying to make both circles the same size although the string len is different. Currently looks like this:
I have a very dirty and hard-core solution which requires quite deep knowledge of matplotlib. It is not perfect but might give you some ideas how to start.
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
import numpy as np
plt.close('all')
fig, ax = plt.subplots(1, 1, figsize=(8, 8))
t1 = ax.text(0.5,0.5,'long_text', ha='center', va='center',fontsize=10)
t2 = ax.text(0.3,0.7,'short', ha='center', va='center', fontsize=10)
t3 = ax.text(0.1,0.7,'super-long-text-that-is-long', ha='center', va='center', fontsize=10)
fig.show()
def text_with_circle(text_obj, axis, color, border=1.5):
# Get the box containing the text
box1 = text_obj.get_window_extent()
# It turned out that what you get from the box is
# in screen pixels, so we need to transform them
# to "data"-coordinates. This is done with the
# transformer-function below
transformer = axis.transData.inverted().transform
# Now transform the corner coordinates of the box
# to data-coordinates
[x0, y0] = transformer([box1.x0, box1.y0])
[x1, y1] = transformer([box1.x1, box1.y1])
# Find the x and y center coordinate
x_center = (x0+x1)/2.
y_center = (y0+y1)/2.
# Find the radius, add some extra to make a nice border around it
r = np.max([(x1-x0)/2., (y1-y0)/2.])*border
# Plot the a circle at the center of the text, with radius r.
circle = Circle((x_center, y_center), r, color=color)
# Add the circle to the axis.
# Redraw the canvas.
return circle
circle1 = text_with_circle(t1, ax, 'g')
ax.add_artist(circle1)
circle2 = text_with_circle(t2, ax, 'r', 5)
ax.add_artist(circle2)
circle3 = text_with_circle(t3, ax, 'y', 1.1)
ax.add_artist(circle3)
fig.canvas.draw()
At the moment you have to run this in ipython, because the figure has to be drawn BEFORE you get_window_extent(). Therefore the fig.show() has to be called AFTER the text is added, but BEFORE the circle can be drawn! Then we can get the coordinates of the text, figures out where the middle is and add a circle around the text with a certain radius. When this is done we redraw the canvas to update with the new circle. Ofcourse you can customize the circle a lot more (edge color, face color, line width, etc), look into the Circle class.
Example of output plot:
I'm looking for a way of exactly aligning (overlaying) the corner edge of my image with corner and edge of a text box edge (bbox or other)
The code in question is:
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots(1)
ax.imshow(np.random.random((256,256)), cmap=plt.get_cmap("viridis"))
ax.axis("off")
ax.annotate(
s = 'image title',
xy=(0, 0),
xytext=(0, 0),
va='top',
ha='left',
fontsize = 15,
bbox=dict(facecolor='white', alpha=1),
)
plt.show()
As you can see, the edges of the text box is outside the image. For the life of me, I cannot find a consistent way of aligning the corner of the text box with the corner of the image. Ideally, I'd like the alignment to be independent of font size and image pixel size, but that might be asking a bit too much.
Finally, I'd like to achieve this with a grid of images, like the second example, below.
import numpy as np
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8, 8))
images = 4*[np.random.random((256,256))]
gs = gridspec.GridSpec(
nrows=2,
ncols=2,
top=1.,
bottom=0.,
right=1.,
left=0.,
hspace=0.,
wspace=0.,
)
for g, i in zip(gs, range(len(images))):
ax = plt.subplot(g)
im = ax.imshow(
images[i],
cmap=plt.get_cmap("viridis")
)
ax.set_xticks([])
ax.set_yticks([])
ax.annotate(
s = 'image title',
xy=(0, 0),
xytext=(0, 0),
va='top',
ha='left',
fontsize = 15,
bbox=dict(facecolor='white', alpha=1),
)
Thanks to P-robot for the solution. The key part of the solution is that the annotation text edge is offset x and y by one pixel from the xy coordinate. Any extra padding used increases the necessary amount to compensate for this offset. The second grouping of arguments given to ax.annotate, below, are the relevant ones.
fig, ax = plt.subplots(1)
ax.imshow(np.random.random((256,256)), cmap=plt.get_cmap("viridis"))
ax.axis("off")
padding = 5
ax.annotate(
s = 'image title',
fontsize = 12,
xy=(0, 0),
xytext=(padding-1, -(padding-1)),
textcoords = 'offset pixels',
bbox=dict(facecolor='white', alpha=1, pad=padding),
va='top',
ha='left',
)
plt.show()
Oddly, for the grid of four images, the offset in the x-direction did not require the subtraction of one pixel, which changes xytext to xytext=(padding, -(padding-1)).
The issue is caused from the padding of the bounding box. You can change the padding by passing the pad argument to the bounding box dictionary (for instance pad = 0 will keep the box inside the axes). I'm assuming you want some padding so it's probably best to set a padding argument and remove this from the position of the annotation (in units of pixels).
Consider that you have three artists in a matplotlib plot. What is the easiest way to not show the intermediate-level artist in the bbox of the top-level artist while retaining the low-level artist visible throughout the whole area?
Illustration of what I want to achieve:
It there was no requirement of being able to see the lowest level a non-transparent facecolor of the toplevel plot would be enough. This does not work with the three levels as then the both of the lower levels would be concealed.
See this IPython notebook for a solution using shapely. Here is a not yet completely functional pure matplotlib example, but I'm hoping there is a simpler way of getting the same result that I have not thought of yet.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import patches, cm
from matplotlib.path import Path
fig, ax = plt.subplots()
imdata = np.random.randn(10, 10)
ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto', cmap=cm.coolwarm)
text = ax.text(0.5, 0.5, 'Text', fontsize='xx-large', fontweight='bold',
color='k', ha='center', va='center')
renderer = fig.canvas.get_renderer()
bbox = text.get_window_extent(renderer).transformed(ax.transData.inverted())
bboxrect = patches.Rectangle((bbox.x0, bbox.y0), bbox.width, bbox.height)
bbpath = bboxrect.get_path().transformed(bboxrect.get_patch_transform())
patch = patches.Rectangle((0.3, 0.3), 0.4, 0.4)
path = patch.get_path().transformed(patch.get_patch_transform())
path = Path.make_compound_path(path, bbpath)
patch = patches.PathPatch(path, facecolor='none', hatch=r'//')
ax.add_patch(patch)
I came up with another answer that's a bit cleaner: it involves creating a clip mask for the hatched region that has a hole in it, so that you can see everything in the background behind it.
from matplotlib.path import Path
from matplotlib.patches import PathPatch
def DoubleRect(xy1, width1, height1,
xy2, width2, height2, **kwargs):
base = np.array([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
verts = np.vstack([xy1 + (width1, height1) * base,
xy2 + (width2, height2) * base[::-1],
xy1])
codes = 2 * ([Path.MOVETO] + 4 * [Path.LINETO]) + [Path.CLOSEPOLY]
return PathPatch(Path(verts, codes), **kwargs)
fig, ax = plt.subplots()
imdata = np.random.randn(10, 10)
# plot the image
im = ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto',
cmap='coolwarm', interpolation='nearest')
# plot the hatched rectangle
patch = plt.Rectangle((0.3, 0.3), 0.4, 0.4, facecolor='none',
hatch=r'//')
ax.add_patch(patch)
# add the text
text = ax.text(0.5, 0.5, 'Text', fontsize='xx-large', fontweight='bold',
color='k', ha='center', va='center')
# create a mask for the hatched rectangle
mask = DoubleRect((0, 0), 1, 1, (0.4, 0.45), 0.2, 0.1,
facecolor='none', edgecolor='black')
ax.add_patch(mask)
patch.set_clip_path(mask)
It's a bit of a hack, but I would probably accomplish this by showing the image twice, once in the background, and once with a custom clip path in the foreground. Here's an example:
fig, ax = plt.subplots()
imdata = np.random.randn(10, 10)
# plot the background image
im = ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto',
cmap=cm.coolwarm, zorder=1)
# plot the hatched rectangle
patch = patches.Rectangle((0.3, 0.3), 0.4, 0.4, facecolor='none',
hatch=r'//', zorder=2)
ax.add_patch(patch)
# plot the box around the text
minirect = patches.Rectangle((0.4, 0.45), 0.2, 0.1, facecolor='none',
edgecolor='black', zorder=4)
ax.add_patch(minirect)
# duplicate image and set a clip path
im2 = ax.imshow(imdata, extent=(0, 1, 0, 1), aspect='auto',
cmap=cm.coolwarm, zorder=3)
im2.set_clip_path(minirect)
# add the text on top
text = ax.text(0.5, 0.5, 'Text', fontsize='xx-large', fontweight='bold',
color='k', ha='center', va='center', zorder=5)