Определение размера графика matplotlib (включая метки) в координатах оси

Мне нужно найти размер сюжета, включая связанных с ним художников (в данном случае только галочки и метки) в координатах осей (как определено в учебнике трансформации matplotlib ).

В основе этого лежит то, что я автоматически создаю миниатюрные графики (как в этом вопросе SO ) для большого количества диаграмм, только когда я могу расположить миниатюру, чтобы она не заслоняла данные в исходном сюжете.

Это мой нынешний подход:

  1. Создайте несколько прямоугольников-кандидатов для тестирования, начиная с верхнего правого исходного участка и работая слева, затем в правом нижнем углу исходного графика и двигайтесь влево.
  2. Для каждого прямоугольника кандидата:
    1. Используя код из этого вопроса SO, преобразуйте левую и правую стороны прямоугольника (в координатах оси) в координаты данных, чтобы найти, какой фрагмент x-данных будет охватывать прямоугольник.
    2. Найдите минимальное / максимальное значение y для фрагмента данных, на которые накладываются прямоугольники.
    3. Найдите верх и низ прямоугольника в координатах данных.
    4. Используя вышеизложенное, определите, перекрывается ли прямоугольник с любыми данными. Если нет, нарисуйте график миниатюр в текущем прямоугольнике, в противном случае продолжите.

Проблема с этим подходом заключается в том, что координаты оси дают вам степень оси от (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') 

И результат этого:

 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 

выход скрипта

One Solution collect form web for “Определение размера графика matplotlib (включая метки) в координатах оси”

Мое решение, похоже, не обнаруживает отметки, но заботится о ярлыках меток, меток осей и названия фигур. Надеюсь, этого достаточно, так как фиксированное значение пэда должно быть в порядке, чтобы учитывать отметки меток.

Используйте 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') 
  • Постройте две гистограммы на одном и том же графике и суммируйте их столбцы до 100
  • Ошибка памяти при работе с огромными данными
  • Является ли график рассеяния матплотлиба медленным для большого количества данных?
  • Импорт пистолета в блокнот Jupyter
  • Emacs Python-нижняя оболочка не отображает приглашение после команды matplotlib show ()
  • График Matplotlib - это не-шоу
  • Как сопоставить координаты в AxesImage с координатами в сохраненном файле изображения?
  • Построение 3d-поверхности из списка кортежей в matplotlib
  • Python - лучший язык программирования в мире.