Python matplotlib 中带换行的文本框?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/4018860/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Text box with line wrapping in matplotlib?
提问by Eric O Lebigot
Is it possible to display text in a box through Matplotlib, with automatic line breaks? By using pyplot.text(), I was only able to print multi-line text that flows beyond the boundaries of the window, which is annoying. The size of the lines is not known in advance… Any idea would be much appreciated!
是否可以通过 Matplotlib 在框中显示文本并自动换行?通过使用pyplot.text(),我只能打印超出窗口边界的多行文本,这很烦人。线的大小事先不知道......任何想法将不胜感激!
采纳答案by Joe Kington
The contents of this answer were merged into mpl master in https://github.com/matplotlib/matplotlib/pull/4342and will be in the next feature release.
此答案的内容已合并到https://github.com/matplotlib/matplotlib/pull/4342 中的mpl master 中,并将在下一个功能版本中发布。
Wow... This is a thorny problem... (And it exposes a lot of limitations in matplotlib's text rendering...)
哇...这是一个棘手的问题...(它暴露了matplotlib的文本渲染的很多限制...)
This should (i.m.o.) be something that matplotlib has built-in, but it doesn't. There have been a few threads about iton the mailing list, but no solution that I could find to automatic text wrapping.
这应该 (imo) 是 matplotlib 内置的东西,但它没有。邮件列表上有一些关于它的主题,但我找不到自动文本换行的解决方案。
So, first off, there's no way to determine the size (in pixels) of the rendered text string before it's drawn in matplotlib. This isn't too large of a problem, as we can just draw it, get the size, and then redraw the wrapped text. (It's expensive, but not too excessively bad)
因此,首先,无法确定渲染文本字符串在 matplotlib 中绘制之前的大小(以像素为单位)。这不是什么大问题,因为我们可以绘制它,获取大小,然后重新绘制换行的文本。(它很贵,但不是太糟糕)
The next problem is that characters don't have a fixed width in pixels, so wrapping a text string to a given number of characters won't necessarily reflect a given width when rendered. This isn't a huge problem, though.
下一个问题是字符没有以像素为单位的固定宽度,因此将文本字符串包装到给定数量的字符在呈现时不一定反映给定宽度。不过,这不是一个大问题。
Beyond that, we can't just do this once... Otherwise, it will be wrapped correctly when drawn the first time (on the screen, for example), but not if drawn again (when the figure is resized or saved as an image with a different DPI than the screen). This isn't a huge problem, as we can just connect a callback function to the matplotlib draw event.
除此之外,我们不能只做一次......否则,它会在第一次绘制时(例如在屏幕上)被正确包裹,但如果再次绘制(当图形被调整大小或另存为DPI 与屏幕不同的图像)。这不是一个大问题,因为我们可以将回调函数连接到 matplotlib 绘制事件。
At any rate this solution is imperfect, but it should work in most situations. I don't try to account for tex-rendered strings, any stretched fonts, or fonts with an unusual aspect ratio. However, it should now properly handle rotated text.
无论如何,这个解决方案是不完美的,但它应该适用于大多数情况。我不会尝试考虑 tex 渲染的字符串、任何拉伸字体或具有不寻常纵横比的字体。但是,它现在应该可以正确处理旋转的文本。
However, It should attempt automatically wrap any text objects in multiple subplots in whichever figures you connect the on_drawcallback to... It will be imperfect in many cases, but it does a decent job.
但是,它应该尝试在您将on_draw回调连接到的任何图形中自动将任何文本对象包装在多个子图中......在许多情况下它是不完美的,但它做得不错。
import matplotlib.pyplot as plt
def main():
fig = plt.figure()
plt.axis([0, 10, 0, 10])
t = "This is a really long string that I'd rather have wrapped so that it"\
" doesn't go outside of the figure, but if it's long enough it will go"\
" off the top or bottom!"
plt.text(4, 1, t, ha='left', rotation=15)
plt.text(5, 3.5, t, ha='right', rotation=-15)
plt.text(5, 10, t, fontsize=18, ha='center', va='top')
plt.text(3, 0, t, family='serif', style='italic', ha='right')
plt.title("This is a really long title that I want to have wrapped so it"\
" does not go outside the figure boundaries", ha='center')
# Now make the text auto-wrap...
fig.canvas.mpl_connect('draw_event', on_draw)
plt.show()
def on_draw(event):
"""Auto-wraps all text objects in a figure at draw-time"""
import matplotlib as mpl
fig = event.canvas.figure
# Cycle through all artists in all the axes in the figure
for ax in fig.axes:
for artist in ax.get_children():
# If it's a text artist, wrap it...
if isinstance(artist, mpl.text.Text):
autowrap_text(artist, event.renderer)
# Temporarily disconnect any callbacks to the draw event...
# (To avoid recursion)
func_handles = fig.canvas.callbacks.callbacks[event.name]
fig.canvas.callbacks.callbacks[event.name] = {}
# Re-draw the figure..
fig.canvas.draw()
# Reset the draw event callbacks
fig.canvas.callbacks.callbacks[event.name] = func_handles
def autowrap_text(textobj, renderer):
"""Wraps the given matplotlib text object so that it exceed the boundaries
of the axis it is plotted in."""
import textwrap
# Get the starting position of the text in pixels...
x0, y0 = textobj.get_transform().transform(textobj.get_position())
# Get the extents of the current axis in pixels...
clip = textobj.get_axes().get_window_extent()
# Set the text to rotate about the left edge (doesn't make sense otherwise)
textobj.set_rotation_mode('anchor')
# Get the amount of space in the direction of rotation to the left and
# right of x0, y0 (left and right are relative to the rotation, as well)
rotation = textobj.get_rotation()
right_space = min_dist_inside((x0, y0), rotation, clip)
left_space = min_dist_inside((x0, y0), rotation - 180, clip)
# Use either the left or right distance depending on the horiz alignment.
alignment = textobj.get_horizontalalignment()
if alignment is 'left':
new_width = right_space
elif alignment is 'right':
new_width = left_space
else:
new_width = 2 * min(left_space, right_space)
# Estimate the width of the new size in characters...
aspect_ratio = 0.5 # This varies with the font!!
fontsize = textobj.get_size()
pixels_per_char = aspect_ratio * renderer.points_to_pixels(fontsize)
# If wrap_width is < 1, just make it 1 character
wrap_width = max(1, new_width // pixels_per_char)
try:
wrapped_text = textwrap.fill(textobj.get_text(), wrap_width)
except TypeError:
# This appears to be a single word
wrapped_text = textobj.get_text()
textobj.set_text(wrapped_text)
def min_dist_inside(point, rotation, box):
"""Gets the space in a given direction from "point" to the boundaries of
"box" (where box is an object with x0, y0, x1, & y1 attributes, point is a
tuple of x,y, and rotation is the angle in degrees)"""
from math import sin, cos, radians
x0, y0 = point
rotation = radians(rotation)
distances = []
threshold = 0.0001
if cos(rotation) > threshold:
# Intersects the right axis
distances.append((box.x1 - x0) / cos(rotation))
if cos(rotation) < -threshold:
# Intersects the left axis
distances.append((box.x0 - x0) / cos(rotation))
if sin(rotation) > threshold:
# Intersects the top axis
distances.append((box.y1 - y0) / sin(rotation))
if sin(rotation) < -threshold:
# Intersects the bottom axis
distances.append((box.y0 - y0) / sin(rotation))
return min(distances)
if __name__ == '__main__':
main()


回答by user65
Its been roughly five years but there still doesn't seem to be a great way to do this. Here is my version of the accepted solution. My goal was to allow pixel-perfect wrapping to be selectively applied to individual text instances. I have also created a simple textBox() function which will convert any axes into a text box with custom margins and alignment.
它已经大约五年了,但似乎仍然没有一个很好的方法来做到这一点。这是我接受的解决方案的版本。我的目标是允许有选择地将像素完美的环绕应用于单个文本实例。我还创建了一个简单的 textBox() 函数,它将任何轴转换为具有自定义边距和对齐方式的文本框。
Instead of assuming a particular font aspect ratio or average width, I actually draw the string one word at a time and insert newlines once the threshold is hit. This is horrendously slow compared to the approximations, but still feels quite snappy for strings of <200 words.
我没有假设特定的字体纵横比或平均宽度,而是一次一个单词地绘制字符串,并在达到阈值时插入换行符。与近似值相比,这非常慢,但对于 <200 个单词的字符串仍然感觉非常活泼。
# Text Wrapping
# Defines wrapText which will attach an event to a given mpl.text object,
# wrapping it within the parent axes object. Also defines a the convenience
# function textBox() which effectively converts an axes to a text box.
def wrapText(text, margin=4):
""" Attaches an on-draw event to a given mpl.text object which will
automatically wrap its string wthin the parent axes object.
The margin argument controls the gap between the text and axes frame
in points.
"""
ax = text.get_axes()
margin = margin / 72 * ax.figure.get_dpi()
def _wrap(event):
"""Wraps text within its parent axes."""
def _width(s):
"""Gets the length of a string in pixels."""
text.set_text(s)
return text.get_window_extent().width
# Find available space
clip = ax.get_window_extent()
x0, y0 = text.get_transform().transform(text.get_position())
if text.get_horizontalalignment() == 'left':
width = clip.x1 - x0 - margin
elif text.get_horizontalalignment() == 'right':
width = x0 - clip.x0 - margin
else:
width = (min(clip.x1 - x0, x0 - clip.x0) - margin) * 2
# Wrap the text string
words = [''] + _splitText(text.get_text())[::-1]
wrapped = []
line = words.pop()
while words:
line = line if line else words.pop()
lastLine = line
while _width(line) <= width:
if words:
lastLine = line
line += words.pop()
# Add in any whitespace since it will not affect redraw width
while words and (words[-1].strip() == ''):
line += words.pop()
else:
lastLine = line
break
wrapped.append(lastLine)
line = line[len(lastLine):]
if not words and line:
wrapped.append(line)
text.set_text('\n'.join(wrapped))
# Draw wrapped string after disabling events to prevent recursion
handles = ax.figure.canvas.callbacks.callbacks[event.name]
ax.figure.canvas.callbacks.callbacks[event.name] = {}
ax.figure.canvas.draw()
ax.figure.canvas.callbacks.callbacks[event.name] = handles
ax.figure.canvas.mpl_connect('draw_event', _wrap)
def _splitText(text):
""" Splits a string into its underlying chucks for wordwrapping. This
mostly relies on the textwrap library but has some additional logic to
avoid splitting latex/mathtext segments.
"""
import textwrap
import re
math_re = re.compile(r'(?<!\)$')
textWrapper = textwrap.TextWrapper()
if len(math_re.findall(text)) <= 1:
return textWrapper._split(text)
else:
chunks = []
for n, segment in enumerate(math_re.split(text)):
if segment and (n % 2):
# Mathtext
chunks.append('${}$'.format(segment))
else:
chunks += textWrapper._split(segment)
return chunks
def textBox(text, axes, ha='left', fontsize=12, margin=None, frame=True, **kwargs):
""" Converts an axes to a text box by removing its ticks and creating a
wrapped annotation.
"""
if margin is None:
margin = 6 if frame else 0
axes.set_xticks([])
axes.set_yticks([])
axes.set_frame_on(frame)
an = axes.annotate(text, fontsize=fontsize, xy=({'left':0, 'right':1, 'center':0.5}[ha], 1), ha=ha, va='top',
xytext=(margin, -margin), xycoords='axes fraction', textcoords='offset points', **kwargs)
wrapText(an, margin=margin)
return an
Usage:
用法:
ax = plot.plt.figure(figsize=(6, 6)).add_subplot(111)
an = ax.annotate(t, fontsize=12, xy=(0.5, 1), ha='center', va='top', xytext=(0, -6),
xycoords='axes fraction', textcoords='offset points')
wrapText(an)
I dropped a few features which weren't as important to me. Resizing will fail as each call to _wrap() inserts additional newlines into the string but has no way of removing them. This can be solved by either stripping out all \n characters in the _wrap function, or storing the original string somewhere and "resetting" the text instance between wraps.
我删除了一些对我来说并不重要的功能。每次调用 _wrap() 都会在字符串中插入额外的换行符,但无法删除它们,因此调整大小将失败。这可以通过删除 _wrap 函数中的所有 \n 字符或将原始字符串存储在某处并在换行之间“重置”文本实例来解决。
回答by Daniel.Bourne
By setting wrap = Truewhen creating the text box, as in the below example. This may have the desired effect.
通过wrap = True在创建文本框时进行设置,如下例所示。这可能会产生预期的效果。
plt.text(5, 5, t, ha='right', rotation=-15, wrap=True)

