matplotlib autoscale axes to include annotations

女生的网名这么多〃 提交于 2019-11-30 15:34:34

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)

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()

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.

For me tight_layout usually solved the problem, but in some cases I had to use 'manual' adjustments with subplots_adjust, like this:

fig = plt.figure()
fig.subplots_adjust(bottom=0.2, top=0.12, left=0.12, right=0.1)

The numbers do not usually change dramatically, so you can fix them rather then try to calculate from the actual plot.

BTW, setting xlim as you do in your example changes only the range of the data you plot and not the white area around all your labels.

In matplotlib1.1 the tight_layout is introduced to solve some of the layout problems. There is a nice tutorial here.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!