text size

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

John,
It might be nice if we could add a "what if" type function to all the backends for text sizing. That way we could write much better auto-layout algorithms for the axis and legend code. A very simple improvement to the auto-date labeller would then work like this:

- Find min, max values of the time axis
- Convert to the label format using the date formatter
- Ask the backend what the size of the min/max labels would be
- Take the max of that as a reasonable guess for the date label size
- Divide up the total interval (in pixels) by the label size + pad
- Pick nice ticks based on the total time interval and number of ticks you can fit

This would be really handy for us in that we do a lot of date plotting with widely varying time ranges and it would be nice to have an "auto-ticker" that insures the labels don't overlap for a plot of a given size and range.

On a scale of 1-10, how difficult do you think this type of thing would be to do?

Ted

···

At 07:52 PM 3/17/2006, John Hunter wrote:

    > 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

-------------------------------------------------------
This SF.Net email is sponsored by xPML, a groundbreaking scripting language
that extends applications into web and mobile media. Attend the live webcast
and join the prime developer group breaking into this new coding territory!
http://sel.as-us.falkag.net/sel?cmd=lnk&kid=110944&bid=241720&dat=121642
_______________________________________________
Matplotlib-users mailing list
Matplotlib-users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-users