我需要在轴坐标中找到包括其相关艺术家(在这种情况下只是刻度和刻度标签)的绘图范围(如matplotlib转换教程中所定义).
这样做的背景是我自动为大量图表创建缩略图(如此SO问题),只有当我可以定位缩略图以使其不会模糊原始图中的数据时.
这是我目前的做法:
这种方法的问题在于轴坐标为您提供从(0,0)(轴的左下角)到(1,1)(右上角)的轴的范围,并且不包括刻度线和刻度标签(缩略图没有标题,轴标签,传说或其他艺术家).
所有图表都使用相同的字体大小,但图表具有不同长度的刻度标签(例如1.5或者1.2345 * 10^6),尽管这些在绘制插图之前是已知的.有没有办法将字体大小/点转换为轴坐标?或者,也许有比上面更好的方法(边界框?).
以下代码实现了上述算法:
import math
from matplotlib import pyplot, rcParams
rcParams['xtick.direction'] = 'out'
rcParams['ytick.direction'] = 'out'
INSET_DEFAULT_WIDTH = 0.35
INSET_DEFAULT_HEIGHT = 0.25
INSET_PADDING = 0.05
INSET_TICK_FONTSIZE = 8
def axis_data_transform(axis, xin, yin, inverse=False):
"""Translate between axis and data coordinates.
If 'inverse' is True, data coordinates are translated to axis coordinates,
otherwise the transformation is reversed.
Code by Covich, from: https://stackoverflow.com/questions/29107800/
"""
xlim, ylim = axis.get_xlim(), axis.get_ylim()
xdelta, ydelta = xlim[1] - xlim[0], ylim[1] - ylim[0]
if not inverse:
xout, yout = xlim[0] + xin * xdelta, ylim[0] + yin * ydelta
else:
xdelta2, ydelta2 = xin - xlim[0], yin - ylim[0]
xout, yout = xdelta2 / xdelta, ydelta2 / ydelta
return xout, yout
def add_inset_to_axis(fig, axis, rect):
left, bottom, width, height = rect
def transform(coord):
return fig.transFigure.inverted().transform(
axis.transAxes.transform(coord))
fig_left, fig_bottom = transform((left, bottom))
fig_width, fig_height = transform([width, height]) - transform([0, 0])
return fig.add_axes([fig_left, fig_bottom, fig_width, fig_height])
def collide_rect((left, bottom, width, height), fig, axis, data):
# Find the values on the x-axis of left and right edges of the rect.
x_left_float, _ = axis_data_transform(axis, left, 0, inverse=False)
x_right_float, _ = axis_data_transform(axis, left + width, 0, inverse=False)
x_left = int(math.floor(x_left_float))
x_right = int(math.ceil(x_right_float))
# Find the highest and lowest y-value in that segment of data.
minimum_y = min(data[int(x_left):int(x_right)])
maximum_y = max(data[int(x_left):int(x_right)])
# Convert the bottom and top of the rect to data coordinates.
_, inset_top = axis_data_transform(axis, 0, bottom + height, inverse=False)
_, inset_bottom = axis_data_transform(axis, 0, bottom, inverse=False)
# Detect collision.
if ((bottom > 0.5 and maximum_y > inset_bottom) or # inset at top of chart
(bottom < 0.5 and minimum_y < inset_top)): # inset at bottom
return True
return False
if __name__ == '__main__':
x_data, y_data = range(0, 100), [-1.0] * 50 + [1.0] * 50 # Square wave.
y_min, y_max = min(y_data), max(y_data)
fig = pyplot.figure()
axis = fig.add_subplot(111)
axis.set_ylim(y_min - 0.1, y_max + 0.1)
axis.plot(x_data, y_data)
# Find a rectangle that does not collide with data. Start top-right
# and work left, then try bottom-right and work left.
inset_collides = False
left_offsets = [x / 10.0 for x in xrange(6)] * 2
bottom_values = (([1.0 - INSET_DEFAULT_HEIGHT - INSET_PADDING] * (len(left_offsets) / 2))
+ ([INSET_PADDING * 2] * (len(left_offsets) / 2)))
for left_offset, bottom in zip(left_offsets, bottom_values):
# rect: (left, bottom, width, height)
rect = (1.0 - INSET_DEFAULT_WIDTH - left_offset - INSET_PADDING,
bottom, INSET_DEFAULT_WIDTH, INSET_DEFAULT_HEIGHT)
inset_collides = collide_rect(rect, fig, axis, y_data)
print 'TRYING:', rect, 'RESULT:', inset_collides
if not inset_collides:
break
if not inset_collides:
inset = add_inset_to_axis(fig, axis, rect)
inset.set_ylim(axis.get_ylim())
inset.set_yticks([y_min, y_min + ((y_max - y_min) / 2.0), y_max])
inset.xaxis.set_tick_params(labelsize=INSET_TICK_FONTSIZE)
inset.yaxis.set_tick_params(labelsize=INSET_TICK_FONTSIZE)
inset_xlimit = (0, int(len(y_data) / 100.0 * 2.5)) # First 2.5% of data.
inset.set_xlim(inset_xlimit[0], inset_xlimit[1], auto=False)
inset.plot(x_data[inset_xlimit[0]:inset_xlimit[1] + 1],
y_data[inset_xlimit[0]:inset_xlimit[1] + 1])
fig.savefig('so_example.png')
Run Code Online (Sandbox Code Playgroud)
这个输出是:
TRYING: (0.6, 0.7, 0.35, 0.25) RESULT: True
TRYING: (0.5, 0.7, 0.35, 0.25) RESULT: True
TRYING: (0.4, 0.7, 0.35, 0.25) RESULT: True
TRYING: (0.30000000000000004, 0.7, 0.35, 0.25) RESULT: True
TRYING: (0.2, 0.7, 0.35, 0.25) RESULT: True
TRYING: (0.10000000000000002, 0.7, 0.35, 0.25) RESULT: False
Run Code Online (Sandbox Code Playgroud)
我的解决方案似乎没有检测到刻度线,但会处理刻度标签、轴标签和图形标题。但希望它足够了,因为固定的焊盘值应该可以很好地说明刻度线。
使用axes.get_tightbbox 获得一个包含标签在内的围绕轴的矩形。
from matplotlib import tight_layout
renderer = tight_layout.get_renderer(fig)
inset_tight_bbox = inset.get_tightbbox(renderer)
Run Code Online (Sandbox Code Playgroud)
而您的原始矩形设置轴 bbox,inset.bbox. 找到这两个 bbox 的轴坐标中的矩形:
inv_transform = axis.transAxes.inverted()
xmin, ymin = inv_transform.transform(inset.bbox.min)
xmin_tight, ymin_tight = inv_transform.transform(inset_tight_bbox.min)
xmax, ymax = inv_transform.transform(inset.bbox.max)
xmax_tight, ymax_tight = inv_transform.transform(inset_tight_bbox.max)
Run Code Online (Sandbox Code Playgroud)
现在为轴本身计算一个新的矩形,这样外部紧密 bbox 的大小将减小到旧轴 bbox:
xmin_new = xmin + (xmin - xmin_tight)
ymin_new = ymin + (ymin - ymin_tight)
xmax_new = xmax - (xmax_tight - xmax)
ymax_new = ymax - (ymax_tight - ymax)
Run Code Online (Sandbox Code Playgroud)
现在,只需切换回图形坐标并重新定位插入轴:
[x_fig,y_fig] = axis_to_figure_transform([xmin_new, ymin_new])
[x2_fig,y2_fig] = axis_to_figure_transform([xmax_new, ymax_new])
inset.set_position ([x_fig, y_fig, x2_fig - x_fig, y2_fig - y_fig])
Run Code Online (Sandbox Code Playgroud)
该功能axis_to_figure_transform基于您的transform功能add_inset_to_axis:
def axis_to_figure_transform(coord, axis):
return fig.transFigure.inverted().transform(
axis.transAxes.transform(coord))
Run Code Online (Sandbox Code Playgroud)
注意:这不适用于fig.show(),至少在我的系统上;tight_layout.get_renderer(fig)导致错误。但是,如果您只使用savefig()而不以交互方式显示绘图,它就可以正常工作。
最后,这是我的更改和添加的完整代码:
import math
from matplotlib import pyplot, rcParams, tight_layout
rcParams['xtick.direction'] = 'out'
rcParams['ytick.direction'] = 'out'
INSET_DEFAULT_WIDTH = 0.35
INSET_DEFAULT_HEIGHT = 0.25
INSET_PADDING = 0.05
INSET_TICK_FONTSIZE = 8
def axis_data_transform(axis, xin, yin, inverse=False):
"""Translate between axis and data coordinates.
If 'inverse' is True, data coordinates are translated to axis coordinates,
otherwise the transformation is reversed.
Code by Covich, from: http://stackoverflow.com/questions/29107800/
"""
xlim, ylim = axis.get_xlim(), axis.get_ylim()
xdelta, ydelta = xlim[1] - xlim[0], ylim[1] - ylim[0]
if not inverse:
xout, yout = xlim[0] + xin * xdelta, ylim[0] + yin * ydelta
else:
xdelta2, ydelta2 = xin - xlim[0], yin - ylim[0]
xout, yout = xdelta2 / xdelta, ydelta2 / ydelta
return xout, yout
def axis_to_figure_transform(coord, axis):
return fig.transFigure.inverted().transform(
axis.transAxes.transform(coord))
def add_inset_to_axis(fig, axis, rect):
left, bottom, width, height = rect
fig_left, fig_bottom = axis_to_figure_transform((left, bottom), axis)
fig_width, fig_height = axis_to_figure_transform([width, height], axis) \
- axis_to_figure_transform([0, 0], axis)
return fig.add_axes([fig_left, fig_bottom, fig_width, fig_height], frameon=True)
def collide_rect((left, bottom, width, height), fig, axis, data):
# Find the values on the x-axis of left and right edges of the rect.
x_left_float, _ = axis_data_transform(axis, left, 0, inverse=False)
x_right_float, _ = axis_data_transform(axis, left + width, 0, inverse=False)
x_left = int(math.floor(x_left_float))
x_right = int(math.ceil(x_right_float))
# Find the highest and lowest y-value in that segment of data.
minimum_y = min(data[int(x_left):int(x_right)])
maximum_y = max(data[int(x_left):int(x_right)])
# Convert the bottom and top of the rect to data coordinates.
_, inset_top = axis_data_transform(axis, 0, bottom + height, inverse=False)
_, inset_bottom = axis_data_transform(axis, 0, bottom, inverse=False)
# Detect collision.
if ((bottom > 0.5 and maximum_y > inset_bottom) or # inset at top of chart
(bottom < 0.5 and minimum_y < inset_top)): # inset at bottom
return True
return False
if __name__ == '__main__':
x_data, y_data = range(0, 100), [-1.0] * 50 + [1.0] * 50 # Square wave.
y_min, y_max = min(y_data), max(y_data)
fig = pyplot.figure()
axis = fig.add_subplot(111)
axis.set_ylim(y_min - 0.1, y_max + 0.1)
axis.plot(x_data, y_data)
# Find a rectangle that does not collide with data. Start top-right
# and work left, then try bottom-right and work left.
inset_collides = False
left_offsets = [x / 10.0 for x in xrange(6)] * 2
bottom_values = (([1.0 - INSET_DEFAULT_HEIGHT - INSET_PADDING] * (len(left_offsets) / 2))
+ ([INSET_PADDING * 2] * (len(left_offsets) / 2)))
for left_offset, bottom in zip(left_offsets, bottom_values):
# rect: (left, bottom, width, height)
rect = (1.0 - INSET_DEFAULT_WIDTH - left_offset - INSET_PADDING,
bottom, INSET_DEFAULT_WIDTH, INSET_DEFAULT_HEIGHT)
inset_collides = collide_rect(rect, fig, axis, y_data)
print 'TRYING:', rect, 'RESULT:', inset_collides
if not inset_collides:
break
if not inset_collides:
inset = add_inset_to_axis(fig, axis, rect)
inset.set_ylim(axis.get_ylim())
inset.set_yticks([y_min, y_min + ((y_max - y_min) / 2.0), y_max])
inset.xaxis.set_tick_params(labelsize=INSET_TICK_FONTSIZE)
inset.yaxis.set_tick_params(labelsize=INSET_TICK_FONTSIZE)
inset_xlimit = (0, int(len(y_data) / 100.0 * 2.5)) # First 2.5% of data.
inset.set_xlim(inset_xlimit[0], inset_xlimit[1], auto=False)
inset.plot(x_data[inset_xlimit[0]:inset_xlimit[1] + 1],
y_data[inset_xlimit[0]:inset_xlimit[1] + 1])
# borrow this function from tight_layout
renderer = tight_layout.get_renderer(fig)
inset_tight_bbox = inset.get_tightbbox(renderer)
# uncomment this to show where the two bboxes are
# def show_bbox_on_plot(ax, bbox, color='b'):
# inv_transform = ax.transAxes.inverted()
# xmin, ymin = inv_transform.transform(bbox.min)
# xmax, ymax = inv_transform.transform(bbox.max)
# axis.add_patch(pyplot.Rectangle([xmin, ymin], xmax-xmin, ymax-ymin, transform=axis.transAxes, color = color))
#
# show_bbox_on_plot(axis, inset_tight_bbox)
# show_bbox_on_plot(axis, inset.bbox, color = 'g')
inv_transform = axis.transAxes.inverted()
xmin, ymin = inv_transform.transform(inset.bbox.min)
xmin_tight, ymin_tight = inv_transform.transform(inset_tight_bbox.min)
xmax, ymax = inv_transform.transform(inset.bbox.max)
xmax_tight, ymax_tight = inv_transform.transform(inset_tight_bbox.max)
# shift actual axis bounds inwards by "margin" so that new size + margin
# is original axis bounds
xmin_new = xmin + (xmin - xmin_tight)
ymin_new = ymin + (ymin - ymin_tight)
xmax_new = xmax - (xmax_tight - xmax)
ymax_new = ymax - (ymax_tight - ymax)
[x_fig,y_fig] = axis_to_figure_transform([xmin_new, ymin_new], axis)
[x2_fig,y2_fig] = axis_to_figure_transform([xmax_new, ymax_new], axis)
inset.set_position ([x_fig, y_fig, x2_fig - x_fig, y2_fig - y_fig])
fig.savefig('so_example.png')
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
733 次 |
| 最近记录: |