Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

matplotlib autoscale axes to include annotations

Does anyone know of an easy way to expand the plot area to include annotations? I have a figure where some labels are long and/or multiline strings, and rather than clipping these to the axes, I want to expand the axes to include the annotations.

Autoscale_view doesn't do it, and ax.relim doesn't pick up the position of the annotations, so that doesn't seem to be an option.

I've tried to do something like the code below, which loops over all the annotations (assuming they are in data coordinates) to get their extents and then updates the axes accordingly, but ideally I don't want my annotations in data coordinates (they are offset from the actual data points).

xmin, xmax = plt.xlim()
ymin, ymax = plt.ylim()
# expand figure to include labels
for l in my_labels:
    # get box surrounding text, in data coordinates
    bbox = l.get_window_extent(renderer=plt.gcf().canvas.get_renderer())
    l_xmin, l_ymin, l_xmax, l_ymax = bbox.extents
    xmin = min(xmin, l_xmin); xmax = max(xmax, l_xmax); ymin = min(ymin, l_ymin); ymax = max(ymax, l_ymax)
plt.xlim(xmin, xmax)
plt.ylim(ymin, ymax)
like image 350
Tango Avatar asked Jul 18 '12 15:07

Tango


1 Answers

I struggled with this too. The key point is that matplotlib doesn't determine how big the text is going to be until it has actually drawn it. So you need to explicitly call plt.draw(), then adjust your bounds, and then draw it again.

The get_window_extent method is supposed to give an answer in display coordinates, not data coordinates, per the documentation. But if the canvas hasn't been drawn yet, it seems to respond in whatever coordinate system you specified in the textcoords keyword argument to annotate. That's why your code above works using textcoords='data', but not 'offset points'.

Here's an example:

x = np.linspace(0,360,101)
y = np.sin(np.radians(x))

line, = plt.plot(x, y)
label = plt.annotate('finish', (360,0),
                     xytext=(12, 0), textcoords='offset points',
                     ha='left', va='center')

bbox = label.get_window_extent(plt.gcf().canvas.get_renderer())
print(bbox.extents)

plot with annotation clipped

array([ 12.     ,  -5.     ,  42.84375,   5.     ])

We want to change the limits so that the text label is within the axes. The value of bbox given isn't much help: since it's in points relative to the labeled point: offset by 12 points in x, a string that evidently will be a little over 30 points long, in 10 point font (-5 to 5 in y). It's nontrivial to figure out how to get from there to a new set of axes bounds.

However, if we call the method again now that we've drawn it, we get a totally different bbox:

bbox = label.get_window_extent(plt.gcf().canvas.get_renderer())
print(bbox.extents)

Now we get

array([ 578.36666667,  216.66666667,  609.21041667,  226.66666667])

This is in display coordinates, which we can transform with ax.transData like we're used to. So to get our labels into the bounds, we can do:

x = np.linspace(0,360,101)
y = np.sin(np.radians(x))

line, = plt.plot(x, y)
label = plt.annotate('finish', (360,0),
                     xytext=(8, 0), textcoords='offset points',
                     ha='left', va='center')

plt.draw()
bbox = label.get_window_extent()

ax = plt.gca()
bbox_data = bbox.transformed(ax.transData.inverted())
ax.update_datalim(bbox_data.corners())
ax.autoscale_view()

fixed plot

Note it's no longer necessary to explicitly pass plt.gcf().canvas.get_renderer() to get_window_extent after the plot has been drawn once. Also, I'm using update_datalim instead of xlim and ylim directly, so that the autoscaling can notch itself up to a round number automatically.

I posted this answer in notebook format here.

like image 157
Tom Baldwin Avatar answered Nov 13 '22 22:11

Tom Baldwin