Создание смешанного преобразования с идентичным масштабированием в зависимости от данных

Я пытаюсь создать круг, который отображает круг независимо от масштабирования оси, но помещается в координаты данных и радиус которого зависит от масштабирования оси 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, я нахожу это удивительным.

Я думаю, тогда возникает обновленный вопрос: почему масштабирующая часть графика преобразования не обновляется полностью, когда я масштабирую оси?

Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
2
0
152
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Вычислить соотношение сторон фигуры

aspect_ratio = fig.get_figheight() / fig.get_figwidth()
  • fig.get_figheight(): Эта функция возвращает высоту фигуры в дюймах.
  • fig.get_figwidth(): Эта функция возвращает ширину фигуры в дюймах.

Соотношение сторон рассчитывается как высота, разделенная на ширину. Это соотношение используется для обеспечения того, чтобы преобразование масштабирования сохраняло правильные пропорции при применении к кругу.

Создайте преобразование для правильного масштабирования радиуса в координатах данных.

transform = mtrans.Affine2D().scale(aspect_ratio, 1.0) + ax.transData
  • mtrans.Affine2D(): создается новое аффинное преобразование, которое представляет собой комбинацию преобразований перемещения, масштабирования, вращения и сдвига.
  • scale(aspect_ratio, 1.0) : этот метод масштабирует преобразование по указанным коэффициентам по осям x и y. Масштабируйте ось X по соотношению сторон и ось Y на 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()

Интерактивный сюжет

Интерактивный график увеличен

Интерактивный сюжет

Интерактивный график увеличен

Интерактивный сюжет

Интерактивный график увеличен

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

Mad Physicist 10.06.2024 04:10

Я обновил вопрос, надеюсь, упростив и сузив его на основе моего последнего расследования/мысленного процесса.

Mad Physicist 10.06.2024 04:49

Кстати, вы показали мне, как учитывать относительное масштабирование пикселей как часть преобразования. Думаю, я добавлю элемент в галерею matplotlib, как только проблема будет решена.

Mad Physicist 10.06.2024 04:56

Решил не ждать: github.com/matplotlib/matplotlib/pull/28364

Mad Physicist 10.06.2024 19:55

Признаюсь, я не очень удачно сформулировал требования. Моя проблема связана с интерактивным зумом, независимо от размера фигуры.

Mad Physicist 11.06.2024 01:30

ОИК. Вы просто устанавливаете преобразование на ax.transData после того, как вычислили начальный размер объекта. Это намного проще, чем то, что я делал, хотя я не уверен на 100%, будет ли это работать с чем-то вроде логарифмического графика.

Mad Physicist 11.06.2024 01:46

Я пытаюсь заставить что-то работать конкретно без использования axis('equal'). Это делает остальные части довольно простыми. Фактически, вычисление аспекта фигуры не является необходимым, поскольку оно выполняется один раз и не обновляется вместе с деревом преобразований.

Mad Physicist 11.06.2024 02:04

Просто в продолжение: похоже, мой подход был правильным, а поведение на самом деле было ошибкой: github.com/matplotlib/matplotlib/issues/…. Я опубликую свой ответ в ближайшее время

Mad Physicist 11.06.2024 15:32

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

Mad Physicist 11.06.2024 15:35
Ответ принят как подходящий

Похоже, что подход, который я предложил в вопросе, должен работать. Чтобы создать преобразование, которое масштабирует данные в направлении Y и такое же масштабирование независимо от данных в направлении X, мы можем сделать следующее:

  1. Создайте преобразование, которое масштабируется по вертикали с помощью ax.transData.
  2. Создайте простое преобразование отражения, используя Affine2D
  3. Отразив, применив преобразование на шаге 1 и отразив обратно, мы можем создать преобразование, которое масштабирует ось X так же, как y.
  4. Наконец, мы добавляем 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 смогут запускать приведенный выше код в интерактивном режиме.

Другие вопросы по теме