在尝试回答 old, unanswered question 时, 我在 matplotlib
中遇到了一个关于文本注释的小问题:在某个位置向图形添加旋转文本时,文本相对于文本的边界框对齐,而不是(假想的)旋转包含文本本身的框。这可能最好用一个小例子来解释:
图中显示了具有不同旋转角度和不同对齐选项的文本片段。对于每个文本对象,红点表示提供给 ax.text()
函数的坐标。蓝色框是文本周围的旋转框,黑色框是文本的近似边界框(有点太大了,但应该明白了)。很容易看出,对于在边缘(左、右、上、下)对齐的情况,红点位于边界框的侧面或边缘,而不是文本框。如果水平和垂直对齐方式都设置为“居中”,则唯一的对齐选项(文本以直观的方式对齐)。现在,这不是错误,而是概述的预期行为 here .然而,在某些情况下,这不是很实用,因为必须“手动”调整位置才能使文本位于所需位置,并且如果旋转角度发生变化或图形重新缩放,则此调整也会发生变化。
问题是,是否有一种可靠的方法来生成与文本框而不是边界框对齐的文本。我已经找到了问题的解决方案,但弄清楚它非常乏味,所以我想我会分享它。
最佳答案
新解决方案rotation_mode="anchor"
matplotlib.text.Text
实际上有一个参数 rotation_mode
,它精确地引导请求的功能。默认值是 rotation_mode="default"
,它从问题中重新创建不需要的行为,而 rotation_mode="anchor"
锚定旋转点根据到文本本身而不是它的边界框。
ax.text(x,y,'test', rotation = deg, rotation_mode="anchor")
另见 the demo_text_rotation_mode example .
有了这个,问题中的例子可以很容易地创建,而不需要继承 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
如果仍然有兴趣,solution given by @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()
关于python - 相对于文本而不是边界框对齐任意旋转的文本注释,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44143395/