Python matplotlib中是否有换行的文本框?

Python matplotlib中是否有换行的文本框?,python,textbox,matplotlib,Python,Textbox,Matplotlib,是否可以通过Matplotlib显示框中的文本,并自动换行?通过使用pyplot.text(),我只能打印超出窗口边界的多行文本,这很烦人。线的大小是不知道提前…任何想法将不胜感激 此答案的内容已于年合并到mpl master中,并将在下一个功能版本中发布 哇。。。这是一个棘手的问题。。。(它暴露了matplotlib文本呈现的许多限制…) 这应该(i.m.o.)是matplotlib内置的,但它没有。邮件列表上有一些,但我找不到自动文本包装的解决方案 因此,首先,在matplotlib中绘制

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

此答案的内容已于年合并到mpl master中,并将在下一个功能版本中发布


哇。。。这是一个棘手的问题。。。(它暴露了matplotlib文本呈现的许多限制…)

这应该(i.m.o.)是matplotlib内置的,但它没有。邮件列表上有一些,但我找不到自动文本包装的解决方案

因此,首先,在matplotlib中绘制渲染文本字符串之前,无法确定其大小(以像素为单位)。这不是一个太大的问题,因为我们可以绘制它,获得大小,然后重新绘制包装的文本。(很贵,但也不算太糟)

下一个问题是字符没有固定的像素宽度,因此将文本字符串包装为给定数量的字符在渲染时不一定反映给定的宽度。不过,这不是什么大问题

除此之外,我们不能只做一次。。。否则,在第一次绘制时(例如在屏幕上),它将正确包装,但如果再次绘制,则不会正确包装(当图形调整大小或保存为具有不同于屏幕的DPI的图像时)。这不是什么大问题,因为我们可以将回调函数连接到matplotlib draw事件

无论如何,这个解决方案是不完美的,但在大多数情况下都应该有效。我不尝试解释tex渲染字符串、任何拉伸字体或具有异常纵横比的字体。但是,它现在应该可以正确处理旋转的文本

但是,它应该尝试自动将任何文本对象包装在多个子图中,无论您将
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()
导入matplotlib.pyplot作为plt
def main():
图=plt.图()
plt.轴([0,10,0,10])
t=“这是一条非常长的字符串,我宁愿将其包装起来,以便\
不会超出图中的范围,但如果足够长,则会超出图中的范围\
“从顶部或底部!”
plt.text(4,1,t,ha='left',旋转=15)
plt.text(5,3.5,t,ha='right',旋转=-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(“这是一个很长的标题,我想把它包装起来”\
“不超出图形边界”,ha='center')
#现在让文本自动换行。。。
图canvas.mpl\u connect('draw\u event',on\u draw)
plt.show()
def on_draw(事件):
“”“在绘图时自动换行图形中的所有文本对象”“”
将matplotlib导入为mpl
图=event.canvas.figure
#在图形中所有轴上的所有美工人员之间循环
对于图轴中的ax:
对于ax中的艺术家。get_children():
#如果是文字艺术家,请将其包装。。。
如果isinstance(艺术家,mpl.text.text):
自动换行_文本(艺术家、事件渲染器)
#暂时断开对绘图事件的任何回调。。。
#(避免递归)
func_handles=fig.canvas.callbacks.callbacks[event.name]
图canvas.callbacks.callbacks[event.name]={}
#重新画这个数字。。
图canvas.draw()
#重置draw事件回调
图canvas.callbacks.callbacks[event.name]=func\u句柄
def自动换行_文本(textobj,渲染器):
“”“包装给定的matplotlib文本对象,使其超出边界
它所绘制的轴的方向
导入文本包装
#以像素为单位获取文本的起始位置。。。
x0,y0=textobj.get_transform().transform(textobj.get_position())
#以像素为单位获取当前轴的范围。。。
clip=textobj.get_axes().get_window_extent()
#将文本设置为围绕左边缘旋转(否则没有意义)
textobj.设置旋转模式(“锚定”)
#获取向左和向右旋转方向上的空间量
#x0、y0的右侧(左侧和右侧也与旋转相关)
rotation=textobj.get_rotation()
右空间=内部最小距离((x0,y0),旋转,剪辑)
左空间=内部最小距离((x0,y0),旋转-180,剪辑)
#根据水平对齐,使用左侧或右侧距离。
对齐=textobj.get_horizontalalignment()
如果对齐方式为“左”:
新的\u宽度=右\u空间
elif对齐是“正确的”:
新的\u宽度=左\u空间
其他:
新的\u宽度=2*min(左\u空间,右\u空间)
#估计以字符为单位的新大小的宽度。。。
纵横比=0.5#这随字体而异!!
fontsize=textobj.get_size()
像素每字符=纵横比*渲染器。点到像素(fontsize)
#如果包裹宽度小于1,只需将其设置为1个字符
换行宽度=最大值(1,新宽度//每字符像素)
尝试:
wrapped_text=textwrap.fill(textobj.get_text(),wrapp_width)
除类型错误外:
#这似乎是一个单词
wrapped_text=textobj.get_text()
textobj.set_text(包装的_text)
def最小距离(点、旋转、方框):
“”“获取给定方向上从“点”到
“长方体”(其中长方体是具有x0、y0、x1和y1属性的对象,点是
x、y和旋转的元组是角度(以度为单位)”
从数学输入sin,cos,radians
x0,y0=点
旋转=弧度(旋转)
距离=[]
阈值=0.0001
如果cos(旋转)>阈值:
#与右轴相交
距离。附加((box.x1-x0)/cos(旋转))
如果cos(旋转)<-阈值:
#与左轴相交
距离。追加((
# 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)
plt.text(5, 5, t, ha='right', rotation=-15, wrap=True)