【问题标题】:Finding the extent of a matplotlib plot (including ticklabels) in axis coordinates在轴坐标中查找 matplotlib 图(包括刻度标签)的范围
【发布时间】:2017-05-18 16:15:42
【问题描述】:

我需要在轴坐标(如matplotlib transformations tutorial 中定义)中找到包含其相关艺术家(在本例中为刻度和刻度标签)的绘图范围。

这样做的背景是,我会自动为大量图表创建缩略图图(如this SO question),只有当我可以放置缩略图以使其不会模糊原始图中的数据时。

这是我目前的做法:

  1. 创建多个候选矩形进行测试,从原始图的右上角开始向左移动,然后从原始图的右下角向左移动。
  2. 对于每个候选矩形:
    1. 使用来自this SO question 的代码将矩形的左侧和右侧(在轴坐标中)转换为数据坐标,以找到矩形将覆盖的 x 数据切片。
    2. 找出矩形覆盖的数据切片的最小/最大 y 值。
    3. 在数据坐标中查找矩形的顶部和底部。
    4. 使用上述方法,确定矩形是否与任何数据重叠。如果不是,则在当前矩形中绘制缩略图,否则继续。

这种方法的问题是轴坐标为您提供从(0,0)(轴的左下角)到(1,1)(右上角)的轴范围,并且不包括刻度和刻度标签(缩略图图没有标题、轴标签、图例或其他艺术家)。

所有图表都使用相同的字体大小,但图表有不同长度的刻度标签(例如1.51.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')

这个的输出是:

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

【问题讨论】:

    标签: python matplotlib


    【解决方案1】:

    我的解决方案似乎没有检测到刻度线,但确实处理了刻度标签、轴标签和图形标题。但希望它足够了,因为固定的填充值应该可以很好地解释刻度线。

    使用 axes.get_tightbbox 获得一个适合轴的矩形,包括标签。

    from matplotlib import tight_layout
    renderer = tight_layout.get_renderer(fig)
    inset_tight_bbox = inset.get_tightbbox(renderer)
    

    而您的原始矩形设置轴 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)
    

    现在为轴本身计算一个新的矩形,这样外部紧的 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)     
    

    现在,只需切换回图形坐标并重新定位插入轴:

    [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])
    

    axis_to_figure_transform 函数基于 add_inset_to_axis 中的 transform 函数:

    def axis_to_figure_transform(coord, axis):
        return fig.transFigure.inverted().transform(
            axis.transAxes.transform(coord))
    

    注意:这不适用于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')
    

    【讨论】:

    • 非常感谢。当我将它移植到我们的原始代码(此处的示例基于该代码)时,show() 似乎工作正常。我怀疑是因为我们使用的是 PDF 渲染器。顺便说一句,我认为inset_bbox = inset.bbox.inverse_transformed(axis.transAxes) 行可能是多余的,因为inset_bbox 从未被阅读过。
    • 你说得对,那条线是上一次迭代遗留下来的,为了清楚起见,我现在删除了它。如果您搜索有关renderer 的东西,它似乎会根据您的后端而被击中或错过,而mac os 尤其成问题。也可以尝试使用renderer = fig.canvas.get_renderer() 而不是导入tight_layoutthis question 的答案。无论如何,如果它已经有效,那就太好了。很高兴我能帮上忙!
    猜你喜欢
    • 2019-08-25
    • 2016-10-09
    • 2012-03-14
    • 2012-12-19
    • 1970-01-01
    • 2013-03-24
    • 1970-01-01
    • 1970-01-01
    • 2020-08-29
    相关资源
    最近更新 更多