Я пытаюсь создать круг, который отображает круг независимо от масштабирования оси, но помещается в координаты данных и радиус которого зависит от масштабирования оси Y. Основываясь на уроке по преобразованиям , а точнее, на части о построении графика в физических координатах, мне нужен конвейер, который выглядит следующим образом:
from matplotlib import pyplot as plt, patches as mpatch, transforms as mtrans
fig, ax = plt.subplots()
x, y = 5, 10
r = 3
transform = fig.dpi_scale_trans + fig_to_data_scaler + mtrans.ScaledTranslation(x, y, ax.transData)
ax.add_patch(mpatch.Circle((0, 0), r, edgecolor='k', linewidth=2, facecolor='w', transform=t))
Цель состоит в том, чтобы создать круг, который правильно масштабируется на уровне фигуры, масштабировать его до нужной высоты, а затем переместить его в координатах данных. fig.dpi_scale_trans
и mtrans.ScaledTranslation(x, y, ax.transData)
работают как положено. Однако я не могу дать адекватного определения fig_to_data_scaler
.
Совершенно очевидно, что мне нужно смешанное преобразование , которое берет шкалу Y из ax.transData
в сочетании с fig.dpi_scale_trans
(инвертирует?), а затем использует те же значения для x
, независимо от преобразования данных. Как мне это сделать?
Еще одна ссылка, которую я посмотрел: https://stackoverflow.com/a/56079290/2988730.
Вот график преобразования, который я безуспешно пытался построить:
vertical_scale_transform = mtrans.blended_transform_factory(mtrans.IdentityTransform(), fig.dpi_scale_trans.inverted() + mtrans.AffineDeltaTransform(ax.transData))
reflection = mtrans.Affine2D.from_values(0, 1, 1, 0, 0, 0)
fig_to_data_scaler = vertical_scale_transform + reflection + vertical_scale_transform # + reflection, though it's optional
Похоже, предыдущая попытка была немного слишком сложной. Не имеет значения, какое соотношение сторон фигуры. Преобразование данных осей буквально обрабатывает все это «из коробки». Следующая попытка почти сработала. Единственное, что он не обрабатывает, — это соотношение сторон пикселей:
vertical_scale_transform = mtrans.AffineDeltaTransform(ax.transData)
reflection = mtrans.Affine2D.from_values(0, 1, 1, 0, 0, 0)
uniform_scale_transform = mtrans.blended_transform_factory(reflection + vertical_scale_transform + reflection, vertical_scale_transform)
t = uniform_scale_transform + mtrans.ScaledTranslation(x, y, ax.transData)
ax.add_patch(mpatch.Circle((0, 0), r, edgecolor='k', linewidth=2, facecolor='w', transform=t))
Это позволит разместить идеальные круги в правильных местах. Панорамирование работает как положено. Единственная проблема заключается в том, что размер кругов не обновляется при масштабировании. Учитывая mtrans.AffineDeltaTransform(ax.transData)
на оси Y, я нахожу это удивительным.
Я думаю, тогда возникает обновленный вопрос: почему масштабирующая часть графика преобразования не обновляется полностью, когда я масштабирую оси?
Вычисление соотношения сторон фигуры и его применение к преобразованию гарантирует, что круги будут отображаться с правильным соотношением сторон независимо от размеров фигуры. Круги должны сохранять свою форму и размер одинаково в различных конфигурациях графиков.
aspect_ratio = fig.get_figheight() / fig.get_figwidth()
Соотношение сторон рассчитывается как высота, разделенная на ширину. Это соотношение используется для обеспечения того, чтобы преобразование масштабирования сохраняло правильные пропорции при применении к кругу.
transform = mtrans.Affine2D().scale(aspect_ratio, 1.0) + ax.transData
Преобразование корректирует радиус круга в координатах данных, поэтому он отображается в правильном масштабе при рендеринге на фигуре, что важно, поскольку соотношение сторон фигуры может искажать объекты, если его не учитывать, что приводит к тому, что круги выглядят как эллипсы .
Протестировано в python v3.12.3
, matplotlib v3.8.4
.
import matplotlib.pyplot as plt
import matplotlib.patches as mpatch
import matplotlib.transforms as mtrans
from typing import Tuple
def create_scaled_circle(ax: plt.Axes, center: Tuple[float, float], radius: float) -> mpatch.Circle:
fig = ax.figure
x, y = center
# Calculate aspect ratio of the figure
aspect_ratio = fig.get_figheight() / fig.get_figwidth()
# Create a transform that scales the radius correctly in data coordinates
transform = mtrans.Affine2D().scale(aspect_ratio, 1.0) + ax.transData
circle = mpatch.Circle(center, radius, transform=transform, facecolor='none', edgecolor='red')
return circle
fig, ax = plt.subplots()
circle1 = create_scaled_circle(ax, (5, 10), 3)
circle2 = create_scaled_circle(ax, (7, 15), 5)
ax.add_patch(circle1)
ax.add_patch(circle2)
ax.set_xlim(0, 20)
ax.set_ylim(0, 20)
plt.show()
# Define 10 test cases with different centers, radii, axis limits, and quadrants
test_cases = [
((5, 10), 3, (-20, 20), (-20, 20)), # Quadrant I
((-7, 15), 5, (-20, 20), (-20, 20)), # Quadrant II
((-10, -5), 2, (-20, 20), (-20, 20)), # Quadrant III
((15, -10), 4, (-20, 20), (-20, 20)), # Quadrant IV
((0.5, 0.5), 7, (-30, 30), (-30, 30)), # Near origin, all quadrants
((-12, -12), 6, (-20, 20), (-20, 20)), # Quadrant III
((3, -18), 1.5, (-20, 20), (-20, 20)), # Quadrant IV
((-6, 6), 2.5, (-15, 15), (-15, 15)), # Quadrant II
((9, -9), 4.5, (-25, 25), (-25, 25)), # Quadrant IV
((-11, 7), 3.5, (-20, 20), (-20, 20)), # Quadrant II
]
for center, radius, xlim, ylim in test_cases:
fig, ax = plt.subplots()
circle = create_scaled_circle(ax, center, radius)
ax.add_patch(circle)
ax.set_xlim(xlim)
ax.set_ylim(ylim)
plt.show()
def create_scaled_circle
неправильно устанавливает центр или аспект круга, если высота и ширина figsize
не равны.
Следующая функция правильно позиционирует и изменяет размеры кругов, даже если высота и ширина не совпадают. Кроме того, при увеличении размер кругов изменяется.
def create_scaled_circle(ax: plt.Axes, center: Tuple[float, float], radius: float) -> mpatch.Circle:
fig = ax.figure
x, y = center
# Calculate the aspect ratio
aspect_ratio = fig.get_figwidth() / fig.get_figheight()
# Adjust the radius according to the aspect ratio
adjusted_radius = radius / aspect_ratio
# Create the circle with the adjusted radius
circle = mpatch.Circle(center, adjusted_radius, transform=ax.transData, facecolor='none', edgecolor='violet')
return circle
# Define test cases with different figsize
test_cases = [
((5, 10), 3, (-20, 20), (-20, 20), (5, 5)), # square figure
((7, 15), 5, (-20, 20), (0, 25), (3, 5)), # tall figure
((-10, -5), 7, (-20, 20), (-20, 20), (5, 3)), # wide figure
]
for center, radius, xlim, ylim, figsize in test_cases:
fig = plt.figure(figsize=figsize)
ax = fig.add_subplot(111)
circle = create_scaled_circle(ax, center, radius)
ax.add_patch(circle)
ax.set_xlim(xlim)
ax.set_ylim(ylim)
ax.grid()
ax.set_aspect('equal')
plt.show()
Интерактивный сюжет
Интерактивный график увеличен
Интерактивный сюжет
Интерактивный график увеличен
Интерактивный сюжет
Интерактивный график увеличен
Я обновил вопрос, надеюсь, упростив и сузив его на основе моего последнего расследования/мысленного процесса.
Кстати, вы показали мне, как учитывать относительное масштабирование пикселей как часть преобразования. Думаю, я добавлю элемент в галерею matplotlib, как только проблема будет решена.
Решил не ждать: github.com/matplotlib/matplotlib/pull/28364
Признаюсь, я не очень удачно сформулировал требования. Моя проблема связана с интерактивным зумом, независимо от размера фигуры.
ОИК. Вы просто устанавливаете преобразование на ax.transData
после того, как вычислили начальный размер объекта. Это намного проще, чем то, что я делал, хотя я не уверен на 100%, будет ли это работать с чем-то вроде логарифмического графика.
Я пытаюсь заставить что-то работать конкретно без использования axis('equal')
. Это делает остальные части довольно простыми. Фактически, вычисление аспекта фигуры не является необходимым, поскольку оно выполняется один раз и не обновляется вместе с деревом преобразований.
Просто в продолжение: похоже, мой подход был правильным, а поведение на самом деле было ошибкой: github.com/matplotlib/matplotlib/issues/…. Я опубликую свой ответ в ближайшее время
Нисколько. Он не совсем отвечает на мой вопрос, но дает много полезной информации и помог мне направить меня на правильный путь. Я думаю, что в этом есть большая ценность для будущих читателей.
Похоже, что подход, который я предложил в вопросе, должен работать. Чтобы создать преобразование, которое масштабирует данные в направлении Y и такое же масштабирование независимо от данных в направлении X, мы можем сделать следующее:
ax.transData
.Affine2D
ScalesTranslation
, чтобы разместить объект в правильном месте данных.Вот полное решение:
from matplotlib import pyplot as plt, patches as mpatch, transforms as mtrans
fig, ax = plt.subplots()
x, y = 5, 10
r = 3
# AffineDeltaTransform returns just the scaling portion
vertical_scale_transform = mtrans.AffineDeltaTransform(ax.transData)
reflection = mtrans.Affine2D.from_values(0, 1, 1, 0, 0, 0)
# The first argument relies on the fact that `reflection` is its own inverse
uniform_scale_transform = mtrans.blended_transform_factory(reflection + vertical_scale_transform + reflection, vertical_scale_transform)
t = uniform_scale_transform + mtrans.ScaledTranslation(x, y, ax.transData)
# Create a circle at origin, and move it with the transform
ax.add_patch(mpatch.Circle((0, 0), r, edgecolor='k', linewidth=2, facecolor='w', transform=t))
Этот ответ инкапсулирован в предлагаемом примере галереи: https://github.com/matplotlib/matplotlib/pull/28364
Проблема с этим решением на момент написания заключается в том, что AffineDeltaTransform не обновляется правильно при масштабировании или изменении размера осей. Проблема зарегистрирована в matplotlib#28372 и решена в matplotlib#28375. Будущие версии matplotlib смогут запускать приведенный выше код в интерактивном режиме.
Похоже, что преобразование аспекта фигуры не будет обновляться при масштабировании фигуры.