Aligning a text box edge with an image corner - python

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

Related

How can I add "secondary" labels with different font sizes to axes in Matplotlib?

I am trying to generate the following figure in Matplotlib:
Code used to generate the axes (without the labels):
import matplotlib.pyplot as plt
fig,ax = plt.subplots(3,3,sharex=True,sharey=True,
constrained_layout=False)
I know how to add the labels "X-axis label here" and "Y-axis label here", but I am not sure how to place the labels "A","B","C","D","E", and "F" where they are shown in the figure above. These labels should also have different font sizes to "X-axis label here" and "Y-axis label here". Any suggestions?
The general approach would be to use ax.annotate() but for the x-axis, we can simply use the subplot title:
import matplotlib.pyplot as plt
fig, ax = plt.subplots(3,3,sharex=True,sharey=True,
constrained_layout=False, figsize=(10, 6))
x_titles = list("DEF")
y_titles = list("ABC")
for curr_title, curr_ax in zip(x_titles, ax[0, :]):
curr_ax.set_title(curr_title, fontsize=15)
for curr_title, curr_ax in zip(y_titles, ax[:, 0]):
#the xy coordinates are in % axes from the lower left corner (0,0) to the upper right corner (1,1)
#the xytext coordinates are offsets in pixel to prevent
#that the text moves in relation to the axis when resizing the window
curr_ax.annotate(curr_title, xy=(0, 0.5), xycoords="axes fraction",
xytext=(-80, 0), textcoords='offset pixels',
fontsize=15, rotation="vertical")
plt.show()
Sample output:

Matplotlib: Render Patch above Annotation

I have several patches (circles) which are labeled with a text as annotation inside. The two circles are draggable so it is possible, that these overlap. The problem is, that the all circles are always rendered before all annotations, so that all annotations are rendered above the circles. This means, if circle 1 overlaps circle 2, the annotations of both circles are visible. This looks kinda weird and wrong. I tried to solve this with the use of the zorder parameter, but this parameter had no effect. After further studying the documentation, this behaviour seems to be hardcoded into the renderer and I haven't found any solution to change this behaviour.
This is the current way how I draw a single circle with annotation:
def draw_circle_with_annotation(ax, position, label, point_size):
trans = ScaledTranslation(position[0], position[1], ax.transData)
point = Circle((0, 0), point_size, transform=trans, picker=True)
ax.add_patch(self.point)
label = ax.annotate(label, xy=(0.5, 0.5), xycoords=point, fontsize=10, ha='center', va='center_baseline')
This function is called successively for each circle, so that the draw calls of the patches and the annotations are executed alternately. Still all annotations are rendered on top of all patches.
However, is there any way to draw a patch above an annotation?
Matplotlib first draws elements with the lowest zorder, and then subsequently the higher zorders on top. When elements have the same zorder, patches are draw before texts. The solution is to give every annotation a new, higher zorder:
import matplotlib.pyplot as plt
from matplotlib.transforms import ScaledTranslation
from matplotlib.patches import Circle
import numpy as np
zorder = 2
def draw_circle_with_annotation(ax, position, label, point_size):
global zorder
point = Circle(position, point_size, picker=True, facecolor='tomato', edgecolor='k', zorder=zorder)
ax.add_patch(point)
ax.annotate(label, xy=position, fontsize=14, ha='center', va='center_baseline', zorder=zorder)
zorder += 1
fig, ax = plt.subplots()
for i in range(10):
draw_circle_with_annotation(ax, np.random.uniform(10, [50, 25], 2), f'label\n{i + 1}', 5)
ax.autoscale()
ax.set_aspect('equal')
plt.show()

Over-plot an equation curve over a png image

enter image description hereI'm having trouble overplotting a relation between radial velocity and offset(position). I've looked at various solutions, but it doesn't seem to work. I've converted the equation into numbers, with only one variable.It also doesn't display the picture to the required dimensions.
x = np.linspace(-0.8 ,0.8 , 1000)
y = 0.5*((1.334e+20/x)**0.5)
img = plt.imread('Pictures/PVdiagram1casaviewer.png')
fig, ax = plt.subplots(figsize=(16, 16), tight_layout=True)
ax.set_xlabel('Offset(arcsec)', fontsize=14)
ax.set_ylabel('Radial Velocity (Km/S)', fontsize=14)
ax.imshow(img, extent=[-0.8, 0.8, -5, 15])
ax.plot(x, y, linewidth=5, color='white')
plt.title('PV Diagram')
plt.show()
enter image description here
If I plot your image, you can see that the axis of the image and matplotlib don't match, because the image contains space between the plot and border of the pictures (axis titles, and so on...)
So, first you need to crop the image, so that it contains just the plot area.
Next, you can plot the image with the argument aspect=auto to scale it to your figsize:
ax.imshow(img, extent=[-0.8,0.8,-5,15], aspect='auto')
If you try to plot your y function over the image, you will see that the values of y are much larger, so the curve is above the image (notice the tiny image is at the bottom).
I don't know what the physical background of y is, but if you divide it by 10e9 it fits inside the image-range.
Full code:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(-0.8 ,0.8 , 1000)
y = 0.5*((1.334e+20/x)**0.5)/10e9 # Scale it here... but how?
img = plt.imread('hNMw82.png')
fig, ax = plt.subplots(figsize=(16, 16), tight_layout=True)
ax.set_xlabel('Offset(arcsec)', fontsize=14)
ax.set_ylabel('Radial Velocity (Km/S)', fontsize=14)
ax.imshow(img, extent=[-0.8,0.8,-5,15], aspect='auto')
ax.plot(x, y, linewidth=5, color='white')
ax.set_ylim([-5,15])
ax.set_xlim([-0.8,0.8])
plt.title('PV Diagram')
plt.show()
Result:
(I also set the axis limits.)

matplotlib - autosize of text according to shape size

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:

How to add a text into a Rectangle?

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)

Categories

Resources