包装在matplotlib中的文本框?

是否有可能通过Matplotlib显示文本框, 并自动换行符 ? 通过使用pyplot.text() ,我只能打印超出窗口边界的多行文本,这很烦人。 线的大小是不知道的…任何想法将不胜感激!

这个答案的内容被合并到https://github.com/matplotlib/matplotlib/pull/4342中的; mpl master中,并将在下一个function发布中。


哇…这是一个棘手的问题…(这暴露​​了matplotlib的文本渲染的很多限制…)

这应该(imo)是matplotlib内置的东西,但不是。 在邮件列表上有一些关于它的线程 ,但没有find自动文本换行的解决scheme。

因此,首先,在matplotlib中绘制之前,无法确定呈现的文本string的大小(以像素为单位)。 这不是太大的问题,因为我们可以绘制它,获取大小,然后重新绘制包装的文本。 (这很贵,但不是太差)

接下来的问题是,字符没有固定的像素宽度,所以将文本string包装成给定数量的字符并不一定会在渲染时反映给定的宽度。 但这不是一个大问题。

除此之外,我们不能这样做一次…否则,它将在第一次绘制(例如在屏幕上)时被正确包装,但是如果再次绘制(当graphics被resize或保存为与屏幕不同的DPI的图像)。 这不是一个大问题,因为我们可以将一个callback函数连接到matplotlib绘制事件。

无论如何这个解决scheme是不完善的,但它应该在大多数情况下工作。 我不试图说明tex呈现的string,任何拉伸的字体或具有不寻常纵横比的字体。 但是,它现在应该正确处理旋转的文本。

然而,它应该尝试自动包装任何文本对象在多个子图中,无论您将on_drawcallback连接到…在许多情况下,这将是不完美的,但它是一个体面的工作。

 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() 

图与包裹的文本

它已经大约五年了,但似乎还没有一个好办法做到这一点。 这是我接受的解决scheme的版本。 我的目标是让像素完美的包装有select地应用到单个文本实例。 我还创build了一个简单的textBox()函数,将任何轴转换成自定义边距和alignment的文本框。

而不是假定一个特定的字体长宽比或平均宽度,我实际上一次绘制一个单词的string,一旦阈值被击中,插入换行符。 与近似值相比,这是非常慢的,但是对于<200字的string,仍然感觉很快。

 # 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 

用法:

在这里输入图像描述

 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) 

我放弃了一些对我来说不重要的function。 resize将失败,因为每个调用_wrap()插入额外的换行到string,但无法删除它们。 这可以通过删除_wrap函数中的所有\ n字符,或者将原始string存储在某个地方以及在重复之间“重置”文本实例来解决。