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