my matplotlib title gets cropped

后端 未结 3 1127
孤独总比滥情好
孤独总比滥情好 2020-12-28 08:38

SOLVED - see comment below on combining wraptext.wrap and plt.tightlayout.

PROBLEM: Here\'s the code:

import matplotlib.pyp         


        
相关标签:
3条回答
  • 2020-12-28 09:13

    You can try the solution found here.

    It's quite a bit of code, but it seems to handle text wrapping for any sort of text on the plot.

    Here's the code from the solution, modified to fit your example:

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

    This produces the following plot: enter image description here

    UPDATE:

    Use the following line to create more space between the top of the figure and the top of the actual plot:

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

    For example, if you use:

    plt.subplots_adjust(top=0.5)
    

    The output will look like: enter image description here

    0 讨论(0)
  • 2020-12-28 09:18

    You could wrap the text with newline characters (\n) automatically using textwrap:

    >>> 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'
    

    In this case, 100 is the number of characters per line (to the nearest whitespace - textwrap tries not to break up words)


    Another option is to reduce the size of the font:

    matplotlib.rcParams.update({'font.size': 12})
    
    0 讨论(0)
  • 2020-12-28 09:29

    You can include newline characters \n in the title to a break a title over a number of lines.

    0 讨论(0)
提交回复
热议问题