簡體   English   中英

在軸坐標中查找 matplotlib 圖(包括刻度標簽)的范圍

[英]Finding the extent of a matplotlib plot (including ticklabels) in axis coordinates

我需要在軸坐標(如matplotlib 轉換教程中定義)中找到繪圖的范圍,包括其相關藝術家(在本例中為刻度和刻度標簽)。

這的背景是我自動為大量圖表創建縮略圖圖(如在這個 SO question中),只有當我可以定位縮略圖以便它不會掩蓋原始圖中的數據時。

這是我目前的做法:

  1. 創建一些候選矩形進行測試,從原始圖的右上角開始向左移動,然后從原始圖的右下角向左移動。
  2. 對於每個候選矩形:
    1. 使用此 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

腳本輸出

我的解決方案似乎沒有檢測到刻度線,但確實處理了刻度標簽、軸標簽和圖形標題。 但希望它足夠了,因為固定的填充值應該可以很好地解釋刻度線。

使用 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是基於您的transform功能add_inset_to_axis

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

要在圖形坐標中獲得軸的緊密 bbox,請使用

def tight_bbox(ax):
    fig = ax.get_figure()
    tight_bbox_raw = ax.get_tightbbox(fig.canvas.get_renderer())
    from matplotlib.transforms import TransformedBbox
    tight_bbox_fig = TransformedBbox(tight_bbox_raw, fig.transFigure.inverted())
    return tight_bbox_fig

例如,這可以用於將標簽相對於圖形坐標中的軸放置在緊密邊界框之外。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM