matplotlib 中带换行的文本框? [英] Text box with line wrapping in matplotlib?

查看:46
本文介绍了matplotlib 中带换行的文本框?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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!

推荐答案

本回答内容合并到mpl master https://github.com/matplotlib/matplotlib/pull/4342 并将在下一个功能版本中发布.

The contents of this answer were merged into mpl master in https://github.com/matplotlib/matplotlib/pull/4342 and will be in the next feature release.

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

Wow... This is a thorny problem... (And it exposes a lot of limitations in matplotlib's text rendering...)

这应该 (i.m.o.) 是 matplotlib 内置的东西,但它没有.有几个关于它的主题邮件列表,但我找不到自动换行的解决方案.

This should (i.m.o.) be something that matplotlib has built-in, but it doesn't. There have been a few threads about it on the mailing list, but no solution that I could find to automatic text wrapping.

因此,首先,无法确定渲染文本字符串在 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)

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

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.

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

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.

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

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.

但是,它应该尝试自动将任何文本对象包装在多个子图中,无论您将 on_draw 回调连接到哪个图形中...在许多情况下它是不完美的,但它做得不错.

However, It should attempt automatically wrap any text objects in multiple subplots in whichever figures you connect the on_draw callback to... It will be imperfect in many cases, but it does a decent job.

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 中带换行的文本框?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆