Почему matplotlib рисует мои круги как овалы?

Есть ли способ получить matplotlib для построения идеального круга? Они больше похожи на овалы.

Ответ 1

Просто для правильного ответа DSM. По умолчанию графики имеют больше пикселей вдоль одной оси над другой. Когда вы добавляете круг, он традиционно добавляется в единицы данных. Если ваши оси имеют симметричный диапазон, это означает, что один шаг вдоль оси x будет включать в себя другое количество пикселей, чем один шаг вдоль вашей оси y. Таким образом, симметричный круг в единицах данных является асимметричным в единицах пикселя (что вы на самом деле видите).

Как правильно указал DSM, вы можете заставить оси x и y иметь равное количество пикселей на единицу данных. Это делается с использованием методов plt.axis("equal") или ax.axis("equal") (где ax является экземпляром Axes).

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

import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse, Circle


fig = plt.figure()
ax1 = fig.add_subplot(211)
# calculate asymmetry of x and y axes:
x0, y0 = ax1.transAxes.transform((0, 0)) # lower left in pixels
x1, y1 = ax1.transAxes.transform((1, 1)) # upper right in pixes
dx = x1 - x0
dy = y1 - y0
maxd = max(dx, dy)
width = .15 * maxd / dx
height = .15 * maxd / dy

# a circle you expect to be a circle, but it is not
ax1.add_artist(Circle((.5, .5), .15))
# an ellipse you expect to be an ellipse, but it a circle
ax1.add_artist(Ellipse((.75, .75), width, height))
ax2 = fig.add_subplot(212)

ax2.axis('equal')
# a circle you expect to be a circle, and it is
ax2.add_artist(Circle((.5, .5), .15))
# an ellipse you expect to be an ellipse, and it is
ax2.add_artist(Ellipse((.75, .75), width, height))

fig.savefig('perfectCircle1.png')

что приводит к этой цифре:

enter image description here

В качестве альтернативы вы можете настроить фигуру так, чтобы Axes были квадратными:

# calculate dimensions of axes 1 in figure units
x0, y0, dx, dy = ax1.get_position().bounds
maxd = max(dx, dy)
width = 6 * maxd / dx
height = 6 * maxd / dy

fig.set_size_inches((width, height))

fig.savefig('perfectCircle2.png')

в результате:

enter image description here

Обратите внимание, что теперь у вторых осей, имеющих опцию axis("equal"), тот же диапазон для осей x и y. Эта цифра была масштабирована так, чтобы единицы даты каждого были представлены одинаковым количеством пикселей.

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

Ответ 2

Сегодня я столкнулся с такой же проблемой, и думаю, что у меня может быть более гибкое решение. Перед предыдущим ответом остаются две основные проблемы (если вы не используете равную функцию аспекта). Сначала, если вы измените размер всего графика, пропорция не будет одинаковой, так как количество пикселей изменится. Второй момент, этот трюк не работает, если у вас нет одинакового lim для xaxis и yaxis.

Это решение трюки mpl с использованием настраиваемого объекта. В самом деле, всякий раз, когда вы меняете один из своих пределов оси или ваш размер графика, mpl вызовет внутреннюю функцию, которая примет значение ширины и высоты эллипса, умноженное на значение функции преобразования. Поскольку значение ширины и высоты сохраняется в объекте эллипса, одним из способов является создание настраиваемого объекта со значением, обновляемым при вызове функции, на основе текущих свойств ax:

import matplotlib.pyplot as plt
from matplotlib.patches import Ellipse

class GraphDist() :
    def __init__(self, size, ax, x=True) :
        self.size = size
        self.ax = ax
        self.x = x

    @property
    def dist_real(self) :
        x0, y0 = self.ax.transAxes.transform((0, 0)) # lower left in pixels
        x1, y1 = self.ax.transAxes.transform((1, 1)) # upper right in pixes
        value = x1 - x0 if self.x else y1 - y0
        return value

    @property
    def dist_abs(self) :
        bounds = self.ax.get_xlim() if self.x else self.ax.get_ylim()
        return bounds[0] - bounds[1]

    @property
    def value(self) :
        return (self.size / self.dist_real) * self.dist_abs

    def __mul__(self, obj) :
        return self.value * obj

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_xlim((0,10))
ax.set_ylim((0,5))
width = GraphDist(10, ax, True)
height = GraphDist(10, ax, False)
ax.add_artist(Ellipse((1, 3), width, height))
plt.show()