2017-01-04 4 views
7

軸座標(matplotlib transformations tutorialで定義されている)で関連するアーティスト(この場合はティックとチックラベルのみ)を含むプロットの範囲を見つける必要があります。軸座標でのマットプロット図(ティックラベルを含む)の範囲の検索

バックグラウンドは、元のプロットのデータが不明瞭にならないようにサムネイルを配置できる場合にのみ、多数のチャートのサムネイルプロット(this SO questionなど)を自動的に作成することです。

これは私の現在のアプローチです:

  1. は、元のプロットの右上に始まり、元のプロットや移動の左、そして右下の作業、テストするために候補矩形の数を作成します。左。各候補矩形の
  2. this SO questionからコードを使用
    1. 矩形がカバーするX-データのどのスライスを見つけるために、データの座標に(軸座標で)四角形の左側と右側に変換します。
    2. 矩形がカバーするデータスライスの最小/最大y値を検索します。
    3. 矩形の上と下をデータ座標で検索します。
    4. 上記を使用して、矩形がどのデータとも重なっているかどうかを判断します。そうでない場合は、現在の矩形にサムネイルプロットを描画し、それ以外の場合は続行します。

このアプローチの問題点は、軸座標が(1,1)(0,0)(左下軸)からあなたの軸の程度を与えることである(右上)およびマダニとticklabelsを(含まれていませんサムネイルプロットには、タイトル、軸ラベル、凡例または他のアーティストはありません)。

すべてのグラフは同じフォントサイズを使用しますが、チャートの長さはさまざまです(例: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') 

そして、これの出力は次のとおりです。

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 

script output

答えて

3

私のソリューションは、目盛りを検出していないようですが、ティックラベル、軸ラベル、図のタイトルを処理します。うまくいけば、固定パッドの値が目盛りを考慮に入れてうまくいかなければならないので、これで十分です。

axes.get_tightbboxを使用して、ラベルを含む軸を囲む矩形を取得します。元の矩形が軸バウンディングボックス、inset.bboxを設定するのに対し

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

。軸における長方形は、これら2つのbboxes座標検索:

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) 

今軸自体のための新たな矩形を計算し、アウタータイトなバウンディングボックスが古い軸のバウンディングボックスのサイズに縮小されるように:

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からction:

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

感謝:

最後に、ここに私の変更や追加を使用して完全なコードです。私が元のコード(ここの例がベースになっている)に移植したとき、 'show()'は正常に動作していたようでした。私はPDFレンダラを使用していると思われます。ちなみに、 'inset_bbox = inset.bbox.inverse_transformed(axis.transAxes)'の行は、 'inset_bbox'が決して読み込まれないので、冗長かもしれません。 – snim2

+0

あなたはそうです、その行は以前の反復から残されました。そして、わかりやすくそれを削除しました。 'renderer'のものを検索すると、バックエンドに応じてヒットしたりミスしたりするようです。特に問題があるのは、mac osです。 'tight_layout'や[この質問]への回答をインポートするのではなく、' renderer = fig.canvas.get_renderer() 'を使ってみることもできます(http://stackoverflow.com/questions/22667224/matplotlib-get-text-bounding -box-independent-back-endend)を指定します。とにかく、すでに動作していれば、それは素晴らしいです。お役に立てて嬉しいです! –

関連する問題