For whatever it's worth, after a lot of wrangling, I think I solved most of
my problems (though perhaps not in the most efficient way).

In case anyone else is looking for similar functionality, here's a callback
function that will autowrap text objects to the inside of the axis they're
plotted in, and should handle any font, rotation, etc that you throw at it.
(The previous version had a lot of bugs).

Hope someone finds it useful, at any rate...
-Joe
[image: E2G1j.png]

import matplotlib.pyplot as plt

def main():
    """Draw some very long strings on a figure and have the auto-wrapped
    to the axis boundaries. Try resizing the figure!!"""

    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!"

    t2 = r"Furthermore, if I put mathtext in here, it won't mutilate it,"\
        " but will treat it like a really long word. For example: "\
        r"$\frac{\sigma}{\gamma} - e^{\theta \pm 5}$ won't be mangled!"

    plt.text(5, 10, t2, size=14, ha='center', va='top', family='monospace')
    plt.text(3, 0, t, family='serif', style='italic', ha='right')

    plt.text(4, 1, t, ha='left', family='Times New Roman', rotation=15)
    plt.text(5, 3.5, t, ha='right', rotation=-15)

    plt.title("This is a really long title that I want to have wrapped so"\
             r" it does not go outside the axis boundaries", ha='center')

    # All we do to autowrap everything  is connect a callback function...
    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 doesn't exceed the
    boundaries of the axis it is plotted in."""
    # 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 (nonsensical 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)
    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 h-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)

    # Convert to characters with a minimum width of 1 character
    wrap_width = max(1, new_width // pixels_per_char(textobj))
    try:
        wrapped_text = safewrap(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)

def pixels_per_char(textobj):
    """Determines the average width of a character of the given textobj
    by drawing a test string and calculating it's length"""
    test_text = 'Try something like a test'
    orig_text = textobj.get_text()
    textobj.set_text(test_text)
    width = textobj.get_window_extent().width
    textobj.set_text(orig_text)
    return width / len(test_text)

def safewrap(text, width):
    """Wraps text, but avoids putting linebreaks in tex strings"""
    import textwrap
    # If it's not a tex string, just wrap it as usual...
    if '$' not in text:
        return textwrap.fill(text, width)

    # Tex segments will be inside two "$"'s, so we want the odd items
    segments = text.split('$')
    tex = segments[1::2]

    # Temporarily replace spaces and dashes inside tex segments so that
    # they will be treated as long words by textwrap...
    segments[1::2] = [x.replace(' ','').replace('-','') for x in tex]
    # Rejoin the temp tex strings with the rest of the text and wrap it
    temp_text = '$'.join(segments)
    wrapped = textwrap.fill(temp_text, width, break_long_words=False)

    # Put the original tex strings back in between $'s
    segments = wrapped.split('$')
    segments[1::2] = tex
    return '$'.join(segments)

if __name__ == '__main__':
    main()
------------------------------------------------------------------------------
Achieve Improved Network Security with IP and DNS Reputation.
Defend against bad network traffic, including botnets, malware, 
phishing sites, and compromised hosts - saving your company time, 
money, and embarrassment.   Learn More! 
http://p.sf.net/sfu/hpdev2dev-nov
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-users

Reply via email to