Is there a way to find out in terms of pixels, points, or
> fraction of figure, how large a text string is? In other
> words if I generate "Some label" as the x-axis label, can
> I find out how tall and wide it is?
It can be done -- it's not terribly elegant. The basic problem is
that text size depends on the backend and configuration settings.
Since we have arbitrary fonts, multiline text with arbitrary rotation,
possible using TeX to render, with multiple output targets that might
handle text differently, you can see how this seemingly simple
question is actually tough.
All text instances have a method called "get_window_extent" which
returns a matplotlib.transforms.BBox that bounds the text instance.
This bbox is in pixel coords.
bbox = sometext.get_window_extent()
l,b,w,h = bbox.get_bounds() # left, bottom, width, height
Here's the rub: we can't realistically know the text size until we
have a renderer, because the same text may be different in different
renderers. matplotlib sets the figure renderer at draw time, and so
by default we don't know the text size until draw size. This is
sometimes cumbersome.
One solution is to do something like
plot(something)
sometext = text(x,y,s)
draw() # force a draw to set the renderer
l,b,w,h = sometext.get_window_extent().get_bounds()
..adjust the postion of some objects based on this info
...draw again
not terribly elegant because we must redraw. If you did not first
force the draw command before calling get_window_extent, you would get
an exception about needing to first set the renderer.
Another approach if you want to avoid the duplicate drawing is to
create a proxy renderer and pass that off to the text instance.
l,b,w,h = fig.bbox.get_bounds()
renderer = RendererAgg(w, h, fig.dpi)
l,b,w,h = sometext.get_window_extent(renderer).get_bounds()
This is not ideal either because you have a duplicate renderer.
If you are using a *Agg backend and have a figure instance, you can do
the following, which requires no duplicate drawing and no duplicate
renderer (the get_renderer method below caches the return value so
repeated calls do not create duplicate renderers)
Here is a complete example
from matplotlib.patches import Rectangle
from matplotlib.transforms import identity_transform
from pylab import figure, show
fig = figure()
ax = fig.add_subplot(111)
ax.plot([1,2,3,4], [1,2,3,4])
t = ax.text(2,2,'Look Ma!\nNo hands', fontsize=40, rotation=-45)
renderer = fig.canvas.get_renderer()
bbox = t.get_window_extent(renderer)
l,b,w,h = bbox.get_bounds()
# no transformation necessary, already in pixel coords
r = Rectangle((l,b),w,h, transform=identity_transform())
ax.add_patch(r)
show()
If we made the get_renderer method standard across backends, we could
probably hide the get_renderer call from the user and make this a
little more friendly, but the above should suffice. Note if you have
some data in pixel coords, you can transform it into another
coordinate system (eg data coords) using the inverse transform
methods. The l,b,w,h bounding box of the text bbox in "data"
coordinates (ie, the coords of the [1,2,3,4] plot) can be obtained
with
from matplotlib.transforms import inverse_transform_bbox
databbox = inverse_transform_bbox(ax.transData, bbox)
print databbox.get_bounds()
In general, different coordinate systems in matplotlib communicate
with one another by transforming a data point to pixel space and then
inverse transforming that point into a different own space. There are
two inverse methods to help with these tasks. The first is a
transform method to handle single points:
# apply the inverse transformation to tuple xy
xi, yi = trans.inverse_xy_tup(xy)
and the second is a stand-alone function defined in
matplotlib.transforms to handle bboxes
# apply the inverse transformation of a bbox
bboxi = inverse_transform_bbox(trans, bbox)
You might be thinking: why aren't we simply using an affine
transformation matrix with a standard matrix inverse? All I can offer
in response is that this architecture supports nonlinear
transformations, ie, an affine plus a nonlinear transformation, for
polar, log, etc.... There is probably a better way, but this is what
we've got.
JDH