长的matplotlib标题被裁剪

18 投票
3 回答
24996 浏览
提问于 2025-04-17 09:55

问题:

这是代码:

import matplotlib.pyplot as plt
plt.bar([1,2],[5,4])
plt.title('this is a very long title and therefore it gets cropped which is an unthinkable behaviour as it loses the information in the title')
plt.show()

运行这段代码后,会生成一个看起来像这样的图:

image

但是标题被截断了,我该怎么做才能让标题完整显示出来呢?

我想要的解决方案是让图的大小能够适应标题和坐标轴标签的文字,而不是那种通过换行来解决的问题,因为那样的办法并不总是有效:

from textwrap import wrap
import matplotlib.pyplot as plt
title = 'this is a very long title and therefore it gets cropped which is an unthinkable behaviour as it loses the information in the title'*5
plt.bar([1,2],[5,4])
plt.title('\n'.join(wrap(title,60)))
plt.show()` 

看看结果:

cropped title

3 个回答

2

你可以在标题中加入换行符 \n,这样就可以把标题分成多行显示。

20

你可以使用 textwrap 自动在文本中加上换行符(\n)来换行:

>>> longstring = "this is a very long title and therefore it gets cropped which is an unthinkable behaviour as it loses the information in the title"
>>> "\n".join(textwrap.wrap(longstring, 100))
'this is a very long title and therefore it gets cropped which is an unthinkable behaviour as it\nloses the information in the title'

在这个例子中,100 是每行的字符数(尽量在空格处换行,textwrap 会尽量不把单词拆开)


另一个选择是减小字体的大小:

matplotlib.rcParams.update({'font.size': 12})
19

你可以试试在这里找到的解决方案。

这段代码有点多,但它似乎可以处理图表上任何文本的换行问题。

下面是根据你的例子修改过的解决方案代码:

import matplotlib.pyplot as plt

def main():
    fig = plt.figure()
    plt.subplots_adjust(top=0.85) # use a lower number to make more vertical space
    plt.bar([1,2],[5,4])
    fig.canvas.mpl_connect('draw_event', on_draw)
    plt.title('this is a very long title and therefore it gets cropped which is an unthinkable behaviour as it loses the information in the title')
    plt.savefig('./test.png')

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

运行后会生成如下图表:

enter image description here

更新

可以使用以下代码来增加图表顶部和实际绘图之间的空间:

plt.subplots_adjust(top=0.85) # use a lower number to make more vertical space

比如,如果你使用:

plt.subplots_adjust(top=0.5)

输出结果会是:

enter image description here

撰写回答