有一个具有不同斜率的多条对角线的图。我想用与线条斜率匹配的文本标签来注释这些线条。
像这样的东西:
有没有一种强大的方法来做到这一点?
我已经尝试了text
's 和annotate
's 的旋转参数,但它们是在屏幕坐标中,而不是数据坐标中(即,x
无论 xy 范围如何,它始终是屏幕上的度数)。Myx
和y
Ranges 相差几个数量级,并且明显的斜率受到视口大小以及其他变量的影响,因此固定度数的旋转并不能解决问题。还有其他想法吗?
有一个具有不同斜率的多条对角线的图。我想用与线条斜率匹配的文本标签来注释这些线条。
像这样的东西:
有没有一种强大的方法来做到这一点?
我已经尝试了text
's 和annotate
's 的旋转参数,但它们是在屏幕坐标中,而不是数据坐标中(即,x
无论 xy 范围如何,它始终是屏幕上的度数)。Myx
和y
Ranges 相差几个数量级,并且明显的斜率受到视口大小以及其他变量的影响,因此固定度数的旋转并不能解决问题。还有其他想法吗?
我想出了一些对我有用的东西。注意灰色虚线:
旋转必须手动设置,但这必须在draw()
布局之后完成。所以我的解决方案是将行与注释相关联,然后遍历它们并执行以下操作:
这并不完美,因为 matplotlib 对旋转文本的处理都是错误的。它通过边界框而不是文本基线对齐。
如果您对文本渲染感兴趣,可以了解一些字体基础知识:http: //docs.oracle.com/javase/tutorial/2d/text/fontconcepts.html
这个例子展示了 matplotlib 的作用: http: //matplotlib.org/examples/pylab_examples/text_rotation.html
我发现在线条旁边正确放置标签的唯一方法是在垂直和水平方向上按中心对齐。然后我将标签向左偏移 10 个点以使其不重叠。对我的应用来说已经足够了。
这是我的代码。我随心所欲地画线,然后绘制注释,然后用辅助函数将它们绑定:
line, = fig.plot(xdata, ydata, '--', color=color)
# x,y appear on the midpoint of the line
t = fig.annotate("text", xy=(x, y), xytext=(-10, 0), textcoords='offset points', horizontalalignment='left', verticalalignment='bottom', color=color)
text_slope_match_line(t, x, y, line)
然后在布局之后但之前调用另一个辅助函数savefig
(对于交互式图像,我认为您必须注册绘制事件并update_text_slopes
在处理程序中调用)
plt.tight_layout()
update_text_slopes()
帮手:
rotated_labels = []
def text_slope_match_line(text, x, y, line):
global rotated_labels
# find the slope
xdata, ydata = line.get_data()
x1 = xdata[0]
x2 = xdata[-1]
y1 = ydata[0]
y2 = ydata[-1]
rotated_labels.append({"text":text, "line":line, "p1":numpy.array((x1, y1)), "p2":numpy.array((x2, y2))})
def update_text_slopes():
global rotated_labels
for label in rotated_labels:
# slope_degrees is in data coordinates, the text() and annotate() functions need it in screen coordinates
text, line = label["text"], label["line"]
p1, p2 = label["p1"], label["p2"]
# get the line's data transform
ax = line.get_axes()
sp1 = ax.transData.transform_point(p1)
sp2 = ax.transData.transform_point(p2)
rise = (sp2[1] - sp1[1])
run = (sp2[0] - sp1[0])
slope_degrees = math.degrees(math.atan(rise/run))
text.set_rotation(slope_degrees)
这与@Adam 给出的过程和基本代码完全相同——它只是被重组为(希望)更方便一点。
def label_line(line, label, x, y, color='0.5', size=12):
"""Add a label to a line, at the proper angle.
Arguments
---------
line : matplotlib.lines.Line2D object,
label : str
x : float
x-position to place center of text (in data coordinated
y : float
y-position to place center of text (in data coordinates)
color : str
size : float
"""
xdata, ydata = line.get_data()
x1 = xdata[0]
x2 = xdata[-1]
y1 = ydata[0]
y2 = ydata[-1]
ax = line.get_axes()
text = ax.annotate(label, xy=(x, y), xytext=(-10, 0),
textcoords='offset points',
size=size, color=color,
horizontalalignment='left',
verticalalignment='bottom')
sp1 = ax.transData.transform_point((x1, y1))
sp2 = ax.transData.transform_point((x2, y2))
rise = (sp2[1] - sp1[1])
run = (sp2[0] - sp1[0])
slope_degrees = np.degrees(np.arctan2(rise, run))
text.set_rotation(slope_degrees)
return text
像这样使用:
import numpy as np
import matplotlib.pyplot as plt
...
fig, axes = plt.subplots()
color = 'blue'
line, = axes.plot(xdata, ydata, '--', color=color)
...
label_line(line, "Some Label", x, y, color=color)
编辑:注意这个方法仍然需要在图形布局完成后调用,否则会改变。
见:https ://gist.github.com/lzkelley/0de9e8bf2a4fe96d2018f1b1bd5a0d3c
尽管这个问题很老,但我一直遇到它并感到沮丧,因为它不太管用。我把它改造成一个类LineAnnotation
和助手line_annotate
,这样它
x
,x = np.linspace(np.pi, 2*np.pi)
line, = plt.plot(x, np.sin(x))
for x in [3.5, 4.0, 4.5, 5.0, 5.5, 6.0]:
line_annotate(str(x), line, x)
我最初把它放在public gist中,但@Adam 让我把它包括在这里。
import numpy as np
from matplotlib.text import Annotation
from matplotlib.transforms import Affine2D
class LineAnnotation(Annotation):
"""A sloped annotation to *line* at position *x* with *text*
Optionally an arrow pointing from the text to the graph at *x* can be drawn.
Usage
-----
fig, ax = subplots()
x = linspace(0, 2*pi)
line, = ax.plot(x, sin(x))
ax.add_artist(LineAnnotation("text", line, 1.5))
"""
def __init__(
self, text, line, x, xytext=(0, 5), textcoords="offset points", **kwargs
):
"""Annotate the point at *x* of the graph *line* with text *text*.
By default, the text is displayed with the same rotation as the slope of the
graph at a relative position *xytext* above it (perpendicularly above).
An arrow pointing from the text to the annotated point *xy* can
be added by defining *arrowprops*.
Parameters
----------
text : str
The text of the annotation.
line : Line2D
Matplotlib line object to annotate
x : float
The point *x* to annotate. y is calculated from the points on the line.
xytext : (float, float), default: (0, 5)
The position *(x, y)* relative to the point *x* on the *line* to place the
text at. The coordinate system is determined by *textcoords*.
**kwargs
Additional keyword arguments are passed on to `Annotation`.
See also
--------
`Annotation`
`line_annotate`
"""
assert textcoords.startswith(
"offset "
), "*textcoords* must be 'offset points' or 'offset pixels'"
self.line = line
self.xytext = xytext
# Determine points of line immediately to the left and right of x
xs, ys = line.get_data()
def neighbours(x, xs, ys, try_invert=True):
inds, = np.where((xs <= x)[:-1] & (xs > x)[1:])
if len(inds) == 0:
assert try_invert, "line must cross x"
return neighbours(x, xs[::-1], ys[::-1], try_invert=False)
i = inds[0]
return np.asarray([(xs[i], ys[i]), (xs[i+1], ys[i+1])])
self.neighbours = n1, n2 = neighbours(x, xs, ys)
# Calculate y by interpolating neighbouring points
y = n1[1] + ((x - n1[0]) * (n2[1] - n1[1]) / (n2[0] - n1[0]))
kwargs = {
"horizontalalignment": "center",
"rotation_mode": "anchor",
**kwargs,
}
super().__init__(text, (x, y), xytext=xytext, textcoords=textcoords, **kwargs)
def get_rotation(self):
"""Determines angle of the slope of the neighbours in display coordinate system
"""
transData = self.line.get_transform()
dx, dy = np.diff(transData.transform(self.neighbours), axis=0).squeeze()
return np.rad2deg(np.arctan2(dy, dx))
def update_positions(self, renderer):
"""Updates relative position of annotation text
Note
----
Called during annotation `draw` call
"""
xytext = Affine2D().rotate_deg(self.get_rotation()).transform(self.xytext)
self.set_position(xytext)
super().update_positions(renderer)
def line_annotate(text, line, x, *args, **kwargs):
"""Add a sloped annotation to *line* at position *x* with *text*
Optionally an arrow pointing from the text to the graph at *x* can be drawn.
Usage
-----
x = linspace(0, 2*pi)
line, = ax.plot(x, sin(x))
line_annotate("sin(x)", line, 1.5)
See also
--------
`LineAnnotation`
`plt.annotate`
"""
ax = line.axes
a = LineAnnotation(text, line, x, *args, **kwargs)
if "clip_on" in kwargs:
a.set_clip_path(ax.patch)
ax.add_artist(a)
return a
transform_rotates_text
现在有一个用于相对于一行旋转文本的内置参数:
要相对于一条线旋转文本,正确的角度不是该线在绘图坐标系中的角度,而是该线在屏幕坐标系中出现的角度。这个角度可以通过设置新参数自动确定
transform_rotates_text
。
所以现在我们可以将原始数据角度传递给plt.text
matplotlib 通过设置自动将其转换为正确的视角transform_rotates_text=True
:
# plot line from (1, 4) to (6, 10)
x = [1, 6]
y = [4, 10]
plt.plot(x, y, 'r.-')
# compute angle in raw data coordinates (no manual transforms)
dy = y[1] - y[0]
dx = x[1] - x[0]
angle = np.rad2deg(np.arctan2(dy, dx))
# annotate with transform_rotates_text to align text and line
plt.text(x[0], y[0], f'rotation={angle:.2f}', ha='left', va='bottom',
transform_rotates_text=True, rotation=angle, rotation_mode='anchor')
这种方法对图形和轴比例是稳健的。即使我们在放置文本之后figsize
修改or ,旋转仍保持正确对齐:xlim
# resizing the figure won't mess up the rotation
plt.gcf().set_size_inches(9, 4)
# rescaling the axes won't mess up the rotation
plt.xlim(0, 12)