Tho*_*ühn 2 python matplotlib python-3.x
在尝试回答一个旧的未解决的问题时,我遇到了一个有关文本批注的小问题matplotlib:在将旋转的文本添加到图形的某个位置时,文本相对于文本的边界框对齐,而不是相对于(虚构的)对齐包含文本本身的旋转框。最好用一个小例子来解释一下:

该图显示了具有不同旋转角度和不同对齐选项的文本。对于每个文本对象,红点表示赋予该ax.text()功能的坐标。蓝色框是围绕文本的旋转框架,黑色框是文本的近似边界框(有点太大,但是应该可以理解)。很容易看出,对于对齐位于边缘(左,右,顶部,底部)的情况,红点位于边界框的侧面或边缘,而不是文本框。唯一以直观方式对齐文本的对齐方式是将水平和垂直对齐方式都设置为“居中”。现在,这不是错误,而是此处概述的预期行为。但是,在某些情况下,它不是很实用,因为必须“手动”调整位置以使文本位于所需位置,并且如果旋转角度发生更改或图形重新缩放,此调整也会更改。
问题是,是否存在一种可靠的方式来生成与文本框架而不是边界框对齐的文本。我已经有解决该问题的方法,但是要弄清楚这很繁琐,所以我想与您分享。
在对matplotlib代码本身进行了一些搜索和挖掘之后,并从这里和这里得到了一些启发,我提出了以下解决方案:
from matplotlib import pyplot as plt
from matplotlib import patches, text
import numpy as np
import math
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().__init__(x,y,text, **kwargs)
self.__Ha = self.get_ha()
self.__Va = self.get_va()
self.__Rotation = self.get_rotation()
self.__Position = self.get_position()
def draw(self, renderer, *args, **kwargs):
"""
Overload of the Text.draw() function
"""
self.update_position()
super().draw(renderer, *args, **kwargs)
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 transformed according to the requested rotation angle and the
aspect ratio of the graph. Finally the transformed offset is used to
shift the text such that the alignment point coincides with the
requested coordinate also when the text is rotated.
"""
#resetting to the original state:
self.set_rotation(0)
self.set_va(self.__Va)
self.set_ha(self.__Ha)
self.set_position(self.__Position)
ax = self.axes
xy = self.__Position
##determining the aspect ratio:
##from /sf/ask/2911802421/
##data limits
xlim = ax.get_xlim()
ylim = ax.get_ylim()
## Axis size on figure
figW, figH = ax.get_figure().get_size_inches()
## Ratio of display units
_, _, w, h = ax.get_position().bounds
##final aspect ratio
aspect = ((figW * w)/(figH * h))*(ylim[1]-ylim[0])/(xlim[1]-xlim[0])
##from /sf/ask/372414381/
##getting the current renderer, so that
##get_window_extent() works
renderer = ax.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)
bbox1d = ax.transData.inverted().transform(bbox1)
width = bbox1d[1,0]-bbox1d[0,0]
height = bbox1d[1,1]-bbox1d[0,1]
##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)
bbox2d = ax.transData.inverted().transform(bbox2)
##computing the difference vector between the two
##alignments
dr = np.array(bbox2d[0]-bbox1d[0])
##computing the rotation matrix, which also accounts for
##the aspect ratio of the figure, to stretch squeeze
##dimensions as needed
rad = np.deg2rad(self.__Rotation)
rot_mat = np.array([
[math.cos(rad), math.sin(rad)*aspect],
[-math.sin(rad)/aspect, math.cos(rad)]
])
##computing the offset vector
drp = np.dot(dr,rot_mat)
##setting new position
self.set_position((xy[0]-drp[0],xy[1]-drp[1]))
##setting rotation value back to the one requested by the user
self.set_rotation(self.__Rotation)
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):
ax.plot(*xy,'r.')
text = TextTrueAlign(
x = xy[0],
y = xy[1],
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()
Run Code Online (Sandbox Code Playgroud)
该示例有些冗长,因为我必须编写一个派生自matplotlib.text.Text该类的类,以便在重绘时正确更新文本对象(例如,如果图形被重新缩放)。如果水平和垂直对齐方式都设置为“中心”,则该代码依赖于始终与文本中心对齐的文本。它使用具有中心对齐方式和请求的对齐方式的文本的边界框之间的差值来预测偏移量,旋转后文本需要偏移此偏移量。该示例的输出如下所示:
为一体的纵横比graph,axes以及figure考虑到,这种做法也是鲁棒于该图的尺寸调整。
我认为,通过治疗方法set_ha(),set_va(),set_rotation()和set_position()我的方式,我可能会打破一些原有的功能matplotlib.text.Text,但应该通过重载这些函数和更换一些比较容易解决self用super()。
任何意见或建议如何改善这一点将不胜感激。此外,如果您碰巧要对此进行测试并发现任何错误或缺陷,请告诉我,我们将尝试对其进行修复。希望这对某人有用:)
rotation_mode="anchor"其实有一种说法rotation_mode来matplotlib.text.Text,这正是操纵所请求的功能。默认是rotation_mode="default"从问题中重新创建不需要的行为,同时rotation_mode="anchor"根据文本本身而不是其边界框来锚定旋转点。
ax.text(x,y,'test', rotation = deg, rotation_mode="anchor")
Run Code Online (Sandbox Code Playgroud)
另请参阅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()
Run Code Online (Sandbox Code Playgroud)
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 /sf/ask/372414381/
##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()
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1016 次 |
| 最近记录: |