Determining the size of the matplotlib plot (including labels) in axis coordinates

I need to find the size of the plot, including the artists associated with it (in this case only ticks and tags) in the axis coordinates (as defined in the matplotlib transformation ).

The basis of this is that I automatically create miniature graphs (as in this SO question ) for a large number of diagrams, only when I can put a thumbnail so that it does not hide the data in the original plot.

This is my current approach:

  • Create several candidate rectangles for testing, starting from the upper right of the original graph and working to the left, then in the lower right corner of the original graph and move left.
  • For each candidate rectangle:
    • Using the code from this SO question , convert the left and right side of the rectangle (in the axis coordinates) to the data coordinates to find which slice x is the data that the rectangle will cover.
    • Find the minimum / maximum y value for the piece of data the rectangle extends to.
    • Find the top and bottom of the rectangle in the data coordinates.
    • Using the above, determine if the rectangle overlaps with any data. If not, draw a thumbnail graph in the current rectangle, otherwise continue.

The problem with this approach is that the axis coordinates give you the axis size from (0,0) (lower left of the axes) to (1,1) (upper right) and do not contain ticks and marks (sketch) plots have no names axial inscriptions, legends or other artists).

All diagrams use the same font sizes, but diagrams have label marks of different lengths (for example, 1.5 or 1.2345 * 10^6 ), although they are known before the overlay was inserted. Is there a way to convert from font sizes / points to axis coordinates? Alternatively, there may be a better approach than higher (bounding fields?).

The following code implements the algorithm above:

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

And the result of this:

 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

+7
python matplotlib
source share
1 answer

My solution does not seem to detect the mark, but takes care of the label labels, axis labels, and shape names. I hope this is enough, since the fixed value of the pad should be good to take into account the tick marks.

Use axes.get_tightbbox to get a rectangle that fits around the axes, including labels.

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

While your original rectangle defines the bbox axis, inset.bbox . Find the rectangles in the axis coordinates for these two 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) 

Now calculate a new rectangle for the axis itself, so that the external hard bbox will be reduced in size to the old bbox axis:

 xmin_new = xmin + (xmin - xmin_tight) ymin_new = ymin + (ymin - ymin_tight) xmax_new = xmax - (xmax_tight - xmax) ymax_new = ymax - (ymax_tight - ymax) 

Now just go back to the curly coordinates and move the insert axis:

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

The axis_to_figure_transform function axis_to_figure_transform based on your transform function from add_inset_to_axis :

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

Note: this does not work with fig.show() , at least on my system; tight_layout.get_renderer(fig) throws an error. However, it works great if you only use savefig() and don't display the graph interactively.

Finally, here is your complete code with my changes and additions:

 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') 
+3
source share

All Articles