Hi folks,

First off, I apologize for the wall of text...

Spurred on by this Stack Overflow
question<http://stackoverflow.com/questions/4018860/text-box-in-matplotlib/4056853#4056853>,
and by an itch I've been wanting to scratch lately, I put together a a
callback function that (attempts, anyway) to auto-wrap text artists to the
boundaries of the axis they're in.

It is often useful to have text reflow/auto-wrap within the axis boundaries
during interactive use (resizing a plot with lots of labeled points, for
example...).  It doesn't really need to be precise, as long as it keeps
words from being fully off the figure.

A "full" gui toolkit would be a better way of handling this, but I've gotten
in the habit of slapping a few callback functions onto matplotlib figures to
make (simple) interactive scripts that I can share across platforms. (Lab
exercises, in my case.) Having a way to make text reflow within the axis
boundaries during resizing of plots makes things a bit easier.

I'm aware that this isn't really possible in a general, backend-independent
fashion due to the subtleties of text rendering in matplotlib (e.g. things
like latex, where the length of the raw string has nothing to do with it's
rendered size, and the general fact that the size of the text isn't known
until after it's drawn).    Even getting it approximately correct is still
useful, though.

I have it working about as well as the approach I'm taking can do, I think,
but I could use some help on a couple of points.

I have two specific questions, and one more general one...

First:
Is it possible to disconnect and then reconnect a callback function from an
event within the callback function, and without disconnecting all other
callback functions from the event?

I'm redrawing the canvas within a "draw_event" callback function, and I'm
currently avoiding recursion by disconnecting and reconnecting all callbacks
to the draw event. It would be nice to disconnect only the function I'm
inside, but I can't find any way of getting its cid...

Alternatively, is there a way to redraw a figure's canvas without triggering
a draw event?

Second:
Is there any way to determine the average aspect ratio of a font in
matplotlib?

I'm trying to approximate the length of a rendered text string based on it's
font size and the number of characters. Currently, I'm assuming that all
fonts have an average aspect ratio of 0.5, which works decently for most
non-monospaced fonts, but fails miserably with others.

Finally:
Is there a better way to do this? My current approach has tons of
limitations but works in most situations.  My biggest problem so far is that
vertical alignment in matplotlib (quite reasonably) refers to an
axis-aligned bounding box, rather than within a text aligned bounding box.
This makes reflowing rotated text more difficult, and I'm only half-way
dealing with rotated text in the code below.  Any suggestions on any points
are welcome!

Thanks!
-Joe

import matplotlib.pyplot as plt

def main():
    fig = plt.figure()
    plt.plot(range(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(7, 3, t, ha='center', rotation=30, va='center')
    plt.text(5, 7, t, fontsize=18, ha='center')
    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")
    fig.canvas.mpl_connect('draw_event', on_draw)
    plt.show()

def on_draw(event):
    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):
    import textwrap
    from math import sin, cos
    # 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()

    # Get the amount of space in the direction of rotation to the left and
    # right of x0, y0
    # (This doesn't try to correct for different vertical alignments, and
will
    # have issues with rotated text when va & ha are not 'center')
    dx1, dx2 = x0 - clip.x0, clip.x1 - x0
    dy1, dy2 = y0 - clip.y0, clip.y1 - y0
    rotation = textobj.get_rotation()
    left_space = min(abs(dx1 / cos(rotation)), abs(dy1 / sin(rotation)))
    right_space = min(abs(dx2 / cos(rotation)), abs(dy2 / sin(rotation)))

    # Determine the width (in pixels) of the new text
    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)

if __name__ == '__main__':
    main()
------------------------------------------------------------------------------
Nokia and AT&T present the 2010 Calling All Innovators-North America contest
Create new apps & games for the Nokia N8 for consumers in  U.S. and Canada
$10 million total in prizes - $4M cash, 500 devices, nearly $6M in marketing
Develop with Nokia Qt SDK, Web Runtime, or Java and Publish to Ovi Store 
http://p.sf.net/sfu/nokia-dev2dev
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-users

Reply via email to