问题描述
在尝试回答未解决的老问题时,我遇到了一个与matplotlib
中的文本注释有关的小问题:将旋转的文本添加到图形的某个位置时,文本是相对于文本的边界框对齐的,而不是相对于保存文本本身的(虚构的)旋转框.最好用一个小例子来解释一下:
该图显示了具有不同旋转角度和不同对齐选项的文本.对于每个文本对象,红点表示赋予ax.text()
函数的坐标.蓝色框是围绕文本的旋转框架,黑色框是文本的近似边界框(有点太大,但是应该可以理解).显而易见,对于对齐位于边缘(左,右,顶部,底部)的情况,红点位于边界框的侧面或边缘,而不是文本框.唯一以直观方式对齐文本的对齐方式是将水平和垂直对齐方式都设置为居中".现在,这不是错误,而是此处中概述的预期行为.但是,在某些情况下,它不是很实用,因为必须手动"调整位置以使文本位于所需位置,并且如果旋转角度发生变化或图形重新缩放,此调整也会发生变化. /p>
问题是,是否有一种健壮的方法来生成与文本框架而不是边界框对齐的文本.我已经有解决该问题的方法,但是要弄清楚这很繁琐,所以我想与您分享.
新解决方案rotation_mode="anchor"
matplotlib.text.Text
实际上有一个参数rotation_mode
,该参数精确地控制了所请求的功能.默认值是rotation_mode="default"
,它会从问题中重新创建不需要的行为,而 rotation_mode="anchor"
会根据文本本身而不是其边界框来固定旋转点.
ax.text(x,y,'test', rotation = deg, rotation_mode="anchor")
另请参见 demo_text_rotation_mode示例.
这样,可以轻松创建问题的示例,而无需子类Text
.
from matplotlib import pyplot as plt
import numpy as np
fig, axes = plt.subplots(3,3, figsize=(10,10),dpi=100)
aligns = [ (va,ha) for va in ('top', 'center', 'bottom')
for ha in ('left', 'center', 'right')]
xys = [[i,j] for j in np.linspace(0.9,0.1,5) for i in np.linspace(0.1,0.9,5)]
degs = np.linspace(0,360,25)
for ax, align in zip(axes.reshape(-1), aligns):
ax.set_xlim([-0.1,1.1])
ax.set_ylim([-0.1,1.1])
for deg,xy in zip(degs,xys):
x,y = xy
ax.plot(x,y,'r.')
text = ax.text(x,y,'test',
rotation = deg,
rotation_mode="anchor", ### <--- this is the key
va = align[0],
ha = align[1],
bbox=dict(facecolor='none', edgecolor='blue', pad=0.0),
)
ax.set_title('alignment = {}'.format(align))
fig.tight_layout()
plt.show()
旧解决方案,将Text
子类化如果仍然有人感兴趣,@ThomasKühn提供的解决方案当然可以很好地工作,但是有一些缺点在非笛卡尔系统中使用文本时,因为它会计算数据坐标中所需的偏移量.
以下是通过使用转换在显示坐标中偏移文本的代码版本,该转换在绘制文本时临时附加.因此,例如,也可以使用它.在极地图中.
from matplotlib import pyplot as plt
from matplotlib import patches, text
import matplotlib.transforms
import numpy as np
class TextTrueAlign(text.Text):
"""
A Text object that always aligns relative to the text, not
to the bounding box; also when the text is rotated.
"""
def __init__(self, x, y, text, **kwargs):
super(TextTrueAlign, self).__init__(x,y,text, **kwargs)
self.__Ha = self.get_ha()
self.__Va = self.get_va()
def draw(self, renderer, *args, **kwargs):
"""
Overload of the Text.draw() function
"""
trans = self.get_transform()
offset = self.update_position()
# while drawing, set a transform which is offset
self.set_transform(trans + offset)
super(TextTrueAlign, self).draw(renderer, *args, **kwargs)
# reset to original transform
self.set_transform(trans)
def update_position(self):
"""
As the (center/center) alignment always aligns to the center of the
text, even upon rotation, we make use of this here. The algorithm
first computes the (x,y) offset for the un-rotated text between
centered alignment and the alignment requested by the user. This offset
is then rotated by the given rotation angle.
Finally a translation of the negative offset is returned.
"""
#resetting to the original state:
rotation = self.get_rotation()
self.set_rotation(0)
self.set_va(self.__Va)
self.set_ha(self.__Ha)
##from https://stackoverflow.com/questions/5320205/matplotlib-text-dimensions
##getting the current renderer, so that
##get_window_extent() works
renderer = self.axes.figure.canvas.get_renderer()
##computing the bounding box for the un-rotated text
##aligned as requested by the user
bbox1 = self.get_window_extent(renderer=renderer)
##re-aligning text to (center,center) as here rotations
##do what is intuitively expected
self.set_va('center')
self.set_ha('center')
##computing the bounding box for the un-rotated text
##aligned to (center,center)
bbox2 = self.get_window_extent(renderer=renderer)
##computing the difference vector between the two alignments
dr = np.array(bbox2.get_points()[0]-bbox1.get_points()[0])
##computing the rotation matrix, which also accounts for
##the aspect ratio of the figure, to stretch squeeze
##dimensions as needed
rad = np.deg2rad(rotation)
rot_mat = np.array([
[np.cos(rad), np.sin(rad)],
[-np.sin(rad), np.cos(rad)]
])
##computing the offset vector
drp = np.dot(dr,rot_mat)
# transform to translate by the negative offset vector
offset = matplotlib.transforms.Affine2D().translate(-drp[0],-drp[1])
##setting rotation value back to the one requested by the user
self.set_rotation(rotation)
return offset
if __name__ == '__main__':
fig, axes = plt.subplots(3,3, figsize=(10,10),dpi=100)
aligns = [ (va,ha) for va in ('top', 'center', 'bottom')
for ha in ('left', 'center', 'right')]
xys = [[i,j] for j in np.linspace(0.9,0.1,5) for i in np.linspace(0.1,0.9,5)]
degs = np.linspace(0,360,25)
for ax, align in zip(axes.reshape(-1), aligns):
ax.set_xlim([-0.1,1.1])
ax.set_ylim([-0.1,1.1])
for deg,xy in zip(degs,xys):
x,y = xy
ax.plot(x,y,'r.')
text = TextTrueAlign(
x = x,
y = y,
text='test',
axes = ax,
rotation = deg,
va = align[0],
ha = align[1],
bbox=dict(facecolor='none', edgecolor='blue', pad=0.0),
)
ax.add_artist(text)
ax.set_title('alignment = {}'.format(align))
fig.tight_layout()
plt.show()
While trying to answer an old, unanswered question, I encountered a little problem concerning text annotations in matplotlib
: When adding rotated text to a figure at a certain position, the text is aligned relative to the bounding box of the text, not the (imaginary) rotated box that holds the text itself. This is maybe best explained with a little example:
The figure shows pieces of text with different rotation angles and different alignment options. For each text object, the red point denotes the coordinate given to the ax.text()
function. The blue box is the rotated frame around the text, and the black box is the approximate bounding box of the text (it's a bit too big, but one should get the idea). It's easily visible that, for the cases where alignment is at the edges (left, right, top, bottom), the red dot is on the sides or edges of the bounding box, not the text frame. The only alignment option, where the text is aligned in an intuitive way is if both horizontal and vertical alignments are set to 'center'. Now, this is not a bug, but intended behaviour as outlined here. However, in some situations, it's not very practical, as the position has to be adjusted 'manually' for the text to be in the desired place, and this adjustment changes if the rotation angle changes or if the figure is re-scaled.
The question is, is there a robust way to generate text that is aligned with the text frame rather with the bounding box. I already have a solution to the problem, but it was quite tedious to figure out, so I thought I'd share it.
New solution rotation_mode="anchor"
There is actually an argument rotation_mode
to matplotlib.text.Text
, which steers exactly the requested functionality. The default is rotation_mode="default"
which recreates the unwanted behaviour from the question, while rotation_mode="anchor"
anchors the point of revolution according to the text itself and not its bounding box.
ax.text(x,y,'test', rotation = deg, rotation_mode="anchor")
Also see the demo_text_rotation_mode example.
With this, the example from the question can be created easily without the need to subclass Text
.
from matplotlib import pyplot as plt
import numpy as np
fig, axes = plt.subplots(3,3, figsize=(10,10),dpi=100)
aligns = [ (va,ha) for va in ('top', 'center', 'bottom')
for ha in ('left', 'center', 'right')]
xys = [[i,j] for j in np.linspace(0.9,0.1,5) for i in np.linspace(0.1,0.9,5)]
degs = np.linspace(0,360,25)
for ax, align in zip(axes.reshape(-1), aligns):
ax.set_xlim([-0.1,1.1])
ax.set_ylim([-0.1,1.1])
for deg,xy in zip(degs,xys):
x,y = xy
ax.plot(x,y,'r.')
text = ax.text(x,y,'test',
rotation = deg,
rotation_mode="anchor", ### <--- this is the key
va = align[0],
ha = align[1],
bbox=dict(facecolor='none', edgecolor='blue', pad=0.0),
)
ax.set_title('alignment = {}'.format(align))
fig.tight_layout()
plt.show()
old solution, subclassing Text
In case one is still interested, the solution given by @ThomasKühn is of course working fine, but has some drawbacks when text is used in a non-cartesian system, because it calculates the offset needed in Data coordinates.
The following would be a version of the code which offsets the text in display coordinates by using a transformation, which is temporarily attached while drawing the text. It can therefore also be used e.g. in polar plots.
from matplotlib import pyplot as plt
from matplotlib import patches, text
import matplotlib.transforms
import numpy as np
class TextTrueAlign(text.Text):
"""
A Text object that always aligns relative to the text, not
to the bounding box; also when the text is rotated.
"""
def __init__(self, x, y, text, **kwargs):
super(TextTrueAlign, self).__init__(x,y,text, **kwargs)
self.__Ha = self.get_ha()
self.__Va = self.get_va()
def draw(self, renderer, *args, **kwargs):
"""
Overload of the Text.draw() function
"""
trans = self.get_transform()
offset = self.update_position()
# while drawing, set a transform which is offset
self.set_transform(trans + offset)
super(TextTrueAlign, self).draw(renderer, *args, **kwargs)
# reset to original transform
self.set_transform(trans)
def update_position(self):
"""
As the (center/center) alignment always aligns to the center of the
text, even upon rotation, we make use of this here. The algorithm
first computes the (x,y) offset for the un-rotated text between
centered alignment and the alignment requested by the user. This offset
is then rotated by the given rotation angle.
Finally a translation of the negative offset is returned.
"""
#resetting to the original state:
rotation = self.get_rotation()
self.set_rotation(0)
self.set_va(self.__Va)
self.set_ha(self.__Ha)
##from https://stackoverflow.com/questions/5320205/matplotlib-text-dimensions
##getting the current renderer, so that
##get_window_extent() works
renderer = self.axes.figure.canvas.get_renderer()
##computing the bounding box for the un-rotated text
##aligned as requested by the user
bbox1 = self.get_window_extent(renderer=renderer)
##re-aligning text to (center,center) as here rotations
##do what is intuitively expected
self.set_va('center')
self.set_ha('center')
##computing the bounding box for the un-rotated text
##aligned to (center,center)
bbox2 = self.get_window_extent(renderer=renderer)
##computing the difference vector between the two alignments
dr = np.array(bbox2.get_points()[0]-bbox1.get_points()[0])
##computing the rotation matrix, which also accounts for
##the aspect ratio of the figure, to stretch squeeze
##dimensions as needed
rad = np.deg2rad(rotation)
rot_mat = np.array([
[np.cos(rad), np.sin(rad)],
[-np.sin(rad), np.cos(rad)]
])
##computing the offset vector
drp = np.dot(dr,rot_mat)
# transform to translate by the negative offset vector
offset = matplotlib.transforms.Affine2D().translate(-drp[0],-drp[1])
##setting rotation value back to the one requested by the user
self.set_rotation(rotation)
return offset
if __name__ == '__main__':
fig, axes = plt.subplots(3,3, figsize=(10,10),dpi=100)
aligns = [ (va,ha) for va in ('top', 'center', 'bottom')
for ha in ('left', 'center', 'right')]
xys = [[i,j] for j in np.linspace(0.9,0.1,5) for i in np.linspace(0.1,0.9,5)]
degs = np.linspace(0,360,25)
for ax, align in zip(axes.reshape(-1), aligns):
ax.set_xlim([-0.1,1.1])
ax.set_ylim([-0.1,1.1])
for deg,xy in zip(degs,xys):
x,y = xy
ax.plot(x,y,'r.')
text = TextTrueAlign(
x = x,
y = y,
text='test',
axes = ax,
rotation = deg,
va = align[0],
ha = align[1],
bbox=dict(facecolor='none', edgecolor='blue', pad=0.0),
)
ax.add_artist(text)
ax.set_title('alignment = {}'.format(align))
fig.tight_layout()
plt.show()
这篇关于相对于文本而不是边界框对齐任意旋转的文本注释的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!