I would like to draw annotation images in a matplotlib plot, and be able to move them after plotting.
To that end, I started from the demo:
https://matplotlib.org/3.2.1/gallery/text_labels_and_annotations/demo_annotation_box.html
with the notebook backend:
%matplotlib notebook
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Circle
from matplotlib.offsetbox import (TextArea, DrawingArea, OffsetImage,
AnnotationBbox)
from matplotlib.cbook import get_sample_data
fig, ax = plt.subplots()
# Define a 1st position to annotate (display it with a marker)
xy = (0.5, 0.7)
ax.plot(xy[0], xy[1], ".r")
# Annotate the 1st position with a text box ('Test 1')
offsetbox = TextArea("Test 1", minimumdescent=False)
ab = AnnotationBbox(offsetbox, xy,
xybox=(-20, 40),
xycoords='data',
boxcoords="offset points",
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab)
# Annotate the 1st position with another text box ('Test')
offsetbox = TextArea("Test", minimumdescent=False)
ab = AnnotationBbox(offsetbox, xy,
xybox=(1.02, xy[1]),
xycoords='data',
boxcoords=("axes fraction", "data"),
box_alignment=(0., 0.5),
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab)
# Define a 2nd position to annotate (don't display with a marker this time)
xy = [0.3, 0.55]
# Annotate the 2nd position with a circle patch
da = DrawingArea(20, 20, 0, 0)
p = Circle((10, 10), 10)
da.add_artist(p)
ab = AnnotationBbox(da, xy,
xybox=(1.02, xy[1]),
xycoords='data',
boxcoords=("axes fraction", "data"),
box_alignment=(0., 0.5),
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab)
# Annotate the 2nd position with an image (a generated array of pixels)
arr = np.arange(100).reshape((10, 10))
im = OffsetImage(arr, zoom=2)
im.image.axes = ax
ab = AnnotationBbox(im, xy,
xybox=(-50., 50.),
xycoords='data',
boxcoords="offset points",
pad=0.3,
arrowprops=dict(arrowstyle="->"))
ax.add_artist(ab)
# Annotate the 2nd position with another image (a Grace Hopper portrait)
with get_sample_data("grace_hopper.png") as file:
arr_img = plt.imread(file, format='png')
imagebox = OffsetImage(arr_img, zoom=0.2)
imagebox.image.axes = ax
ab = AnnotationBbox(imagebox, xy,
xybox=(120., -80.),
xycoords='data',
boxcoords="offset points",
pad=0.5,
arrowprops=dict(
arrowstyle="->",
connectionstyle="angle,angleA=0,angleB=90,rad=3")
)
ax.add_artist(ab)
# Fix the display limits to see everything
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
plt.show()
To test how to move stuff, I execute in the next cell:
rnd = fig.canvas.renderer
print(rnd)
print(im.get_window_extent(rnd))
print(im.get_window_extent(rnd).get_points())
print(ab.xybox)
ab.xybox = (130, -70)
im._offset = (135.46666666666667, 301.6355555555555)
im.set_zoom(.5)
ab.update_positions(rnd)
print(ab.xybox)
print(im.get_window_extent(rnd).get_points())
ab.draw(rnd)
im.draw(rnd)
fig.draw(rnd)
ax.plot([0, 1], [0, 1])
plt.show()
fig.canvas.draw_idle()
and what I see is that I do get a break in the plotted line, showing that the annotationbbox is shifted, but all its content isn't. (here the line has been plotted first (blue), then all the shifting has been executed, with another line plot (orange))
Any suggestions on how I can actually move the image of Grace Hopper are highly appreciated.
This is not a direct answer to your question, but I was wondering a similar thing (and the matplotlib documentation is lacking), so I hacked up this example, which appears to work. The key thing seems to be using fig.canvas.draw() after every change. Also, I'm using matplotlib version 3.1.3
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
def onpick(evt):
x = (evt.xdata, evt.ydata)
ab.xybox = x
print(ab.xybox)
fig.canvas.draw()
pix = np.linspace(0, 1, 20)
x, y = np.meshgrid(pix, pix)
p = np.cos(2*np.pi*x)*np.sin(8*np.pi*y)
fig = plt.figure()
ax = fig.add_subplot(111)
im = OffsetImage(p, zoom=1, cmap = 'gray')
ab = AnnotationBbox(im, (0.5, 0.5), xycoords='data', frameon=False)
ax.add_artist(ab)
fig.canvas.mpl_connect('button_press_event', onpick)
plt.show()
Related
from this code,
import matplotlib.pyplot as plt
import numpy as np
def f(x):
return x*x*np.sqrt(x+1)
# prepare coordinate vectors
x = np.linspace(-1, 1.5, 500)
y = f(x)
# create figure and axes
fig, ax = plt.subplots(1,1)
# format spines
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_position(('data', 0))
ax.spines['bottom'].set_position(('data', 0))
# plot and format
ax.plot(x,y, 'r-')
ax.plot([-1, -4/5, 0], [f(-1), f(-4/5), f(0)], linestyle="", marker='o', mfc='b', mec='r')
ax.set_xlabel(r'$x$'); ax.set_title(r'$y = x^2\sqrt{x+1}$')
ax.set_xlim([-1.5,1.5])
ax.annotate('local max', xy=(-4/5-0.05, f(-4/5)+0.1), xycoords='data', xytext=(-1.2, 1.5),
ha='right', va='top', arrowprops={'fc':'blue',}
)
# save the figure
fig.savefig('sketch.png')
plt.show()
What is the '$' sign mean in python?
thank you.I was trying to search on internet but cannot really understand it,thank you.
The $ doesn't mean anything in Python - it's just that character in a string. Matplotlib uses it for rendering LaTeX: https://matplotlib.org/stable/tutorials/text/usetex.html
I have a simple plot, where I want to insert image of UAV, but it doesn't show up. I believe that the annotation box is somewhere out of area of plot, but can't figure out where to move it. Currently I want to have it at [2,4], just to test.
Here is my code:
from mpl_toolkits import mplot3d
import numpy as np
import matplotlib.pyplot as plt
import random
from matplotlib.offsetbox import (OffsetImage, AnnotationBbox)
import matplotlib.image as image
fig = plt.figure()
ax = plt.axes(projection="3d")
num_bars = 3
x_pos = random.sample(range(20), num_bars)
y_pos = random.sample(range(20), num_bars)
z_pos = [0] * num_bars
x_size = np.ones(num_bars)
y_size = np.ones(num_bars)
z_size = random.sample(range(20), num_bars)
#ax.bar3d(x_pos, y_pos, z_pos, x_size, y_size, z_size, color='grey')
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_zlabel('z')
ax.set_xlim(0,20)
ax.set_ylim(0,20)
ax.set_zlim(0,30)
"""
ax.set_xticks([])
ax.set_yticks([])
ax.set_zticks([])
"""
img="./UAV.png"
uav = image.imread(img)
arr_img = plt.imread("./UAV.png", format='png')
imagebox = OffsetImage(arr_img, zoom = .15)
imagebox.image.axes = ax
#ab = AnnotationBbox(imagebox, (5, 10), xybox = (5.0, 10.0), box_alignment=(1, 0))
ab = AnnotationBbox(imagebox, [2., 4.],
xycoords='data',
boxcoords="offset points",
pad=0
)
ax.add_artist(ab)
ax.bar3d(0,0,0,4,4,25,color="grey")
ax.bar3d(16,16,0,4,4,27,color="grey")
ax.bar3d(0,16,0,4,4,23,color="grey")
plt.tight_layout()
plt.show()
I could not find the problem with annotation box, but I have managed to fix this by adding the image to the plot by imshow instead. See the code:
arr_img = plt.imread("./UAV.png", format='png')
newax = fig.add_axes([0.45,0.5,0.2,0.2], anchor='NE', zorder=1)
newax.imshow(arr_img)
newax.patch.set_alpha(0.01)
newax.get_xaxis().set_ticks([])
newax.get_yaxis().set_ticks([])
newax.spines['top'].set_visible(False)
newax.spines['right'].set_visible(False)
newax.spines['bottom'].set_visible(False)
newax.spines['left'].set_visible(False)
output:
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 would like to update the arrow position while in a loop of plots. I found this post that has an analogous question for the situation in which the patch is a rectangle. Below, the solution proposed in the mentioned post with the addition of the Arrow patch.
from matplotlib import pyplot as plt
from matplotlib.patches import Rectangle, Arrow
import numpy as np
nmax = 10
xdata = range(nmax)
ydata = np.random.random(nmax)
fig, ax = plt.subplots()
ax.plot(xdata, ydata, 'o-')
ax.xaxis.set_ticks(xdata)
plt.ion()
rect = plt.Rectangle((0, 0), nmax, 1, zorder=10)
ax.add_patch(rect)
arrow = Arrow(0,0,1,1)
ax.add_patch(arrow)
for i in range(nmax):
rect.set_x(i)
rect.set_width(nmax - i)
#arrow.what --> which method?
fig.canvas.draw()
plt.pause(0.1)
The problem with the Arrow patch is that apparently it does not have a set method related with its position as the Rectangle patch has. Any tip is welcome.
The matplotlib.patches.Arrow indeed does not have a method to update its position. While it would be possible to change its transform dynamically, I guess the easiest solution is to simply remove it and add a new Arrow in each step of the animation.
from matplotlib import pyplot as plt
from matplotlib.patches import Rectangle, Arrow
import numpy as np
nmax = 9
xdata = range(nmax)
ydata = np.random.random(nmax)
plt.ion()
fig, ax = plt.subplots()
ax.set_aspect("equal")
ax.plot(xdata, ydata, 'o-')
ax.set_xlim(-1,10)
ax.set_ylim(-1,4)
rect = Rectangle((0, 0), nmax, 1, zorder=10)
ax.add_patch(rect)
x0, y0 = 5, 3
arrow = Arrow(1,1,x0-1,y0-1, color="#aa0088")
a = ax.add_patch(arrow)
plt.draw()
for i in range(nmax):
rect.set_x(i)
rect.set_width(nmax - i)
a.remove()
arrow = Arrow(1+i,1,x0-i+1,y0-1, color="#aa0088")
a = ax.add_patch(arrow)
fig.canvas.draw_idle()
plt.pause(0.4)
plt.waitforbuttonpress()
plt.show()
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)