matplotlib (comprimento de unidade igual): com proporção de aspecto 'igual' o eixo z não é igual a x- e y-

87

Quando eu configuro a proporção de aspecto igual para o gráfico 3D, o eixo z não muda para 'igual'. Então, é isso:

fig = pylab.figure()
mesFig = fig.gca(projection='3d', adjustable='box')
mesFig.axis('equal')
mesFig.plot(xC, yC, zC, 'r.')
mesFig.plot(xO, yO, zO, 'b.')
pyplot.show()

me dá o seguinte: insira a descrição da imagem aqui

onde obviamente o comprimento da unidade do eixo z não é igual às unidades xey.

Como posso igualar o comprimento da unidade de todos os três eixos? Todas as soluções que encontrei não funcionaram. Obrigado.


fonte

Respostas:

69

Acredito que matplotlib ainda não defina eixos iguais corretamente em 3D ... Mas eu encontrei um truque algumas vezes atrás (não me lembro onde) que adaptei para usá-lo. O conceito é criar uma caixa delimitadora cúbica falsa em torno de seus dados. Você pode testá-lo com o seguinte código:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_aspect('equal')

X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25

scat = ax.scatter(X, Y, Z)

# Create cubic bounding box to simulate equal aspect ratio
max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max()
Xb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][0].flatten() + 0.5*(X.max()+X.min())
Yb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][1].flatten() + 0.5*(Y.max()+Y.min())
Zb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][2].flatten() + 0.5*(Z.max()+Z.min())
# Comment or uncomment following both lines to test the fake bounding box:
for xb, yb, zb in zip(Xb, Yb, Zb):
   ax.plot([xb], [yb], [zb], 'w')

plt.grid()
plt.show()

Os dados z têm uma ordem de magnitude maior do que xey, mas mesmo com a opção de eixo igual, matplotlib autoscale eixo z:

mau

Mas se você adicionar a caixa delimitadora, obterá uma escala correta:

insira a descrição da imagem aqui

Remy F
fonte
Nesse caso, você nem precisa da equalinstrução - ela será sempre igual.
1
Isso funciona bem se você estiver plotando apenas um conjunto de dados, mas e quando houver mais conjuntos de dados todos no mesmo gráfico 3D? Em questão, havia 2 conjuntos de dados, então é uma coisa simples combiná-los, mas isso pode se tornar irracional rapidamente se plotar vários conjuntos de dados diferentes.
Steven C. Howell
@ stvn66, estava traçando até cinco conjuntos de dados em um gráfico com essas soluções e funcionou bem para mim.
1
Isso funciona perfeitamente. Para aqueles que desejam isso na forma de função, que pega um objeto de eixo e executa as operações acima, eu os incentivo a verificar a resposta @karlo abaixo. É uma solução ligeiramente mais limpa.
spurra
@ user1329187 - Descobri que isso não funcionou para mim sem a equaldeclaração.
supergra
58

Gosto das soluções acima, mas elas têm a desvantagem de que você precisa controlar os intervalos e os meios de todos os seus dados. Isso pode ser complicado se você tiver vários conjuntos de dados que serão plotados juntos. Para corrigir isso, usei os métodos ax.get_ [xyz] lim3d () e coloquei tudo em uma função autônoma que pode ser chamada apenas uma vez antes de você chamar plt.show (). Aqui está a nova versão:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np

def set_axes_equal(ax):
    '''Make axes of 3D plot have equal scale so that spheres appear as spheres,
    cubes as cubes, etc..  This is one possible solution to Matplotlib's
    ax.set_aspect('equal') and ax.axis('equal') not working for 3D.

    Input
      ax: a matplotlib axis, e.g., as output from plt.gca().
    '''

    x_limits = ax.get_xlim3d()
    y_limits = ax.get_ylim3d()
    z_limits = ax.get_zlim3d()

    x_range = abs(x_limits[1] - x_limits[0])
    x_middle = np.mean(x_limits)
    y_range = abs(y_limits[1] - y_limits[0])
    y_middle = np.mean(y_limits)
    z_range = abs(z_limits[1] - z_limits[0])
    z_middle = np.mean(z_limits)

    # The plot bounding box is a sphere in the sense of the infinity
    # norm, hence I call half the max range the plot radius.
    plot_radius = 0.5*max([x_range, y_range, z_range])

    ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
    ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
    ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_aspect('equal')

X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25

scat = ax.scatter(X, Y, Z)

set_axes_equal(ax)
plt.show()
Karlo
fonte
Esteja ciente de que usar meios como o ponto central não funcionará em todos os casos, você deve usar pontos médios. Veja meu comentário sobre a resposta de tauran.
Rainman Noodles
1
Meu código acima não pega a média dos dados, mas sim a média dos limites existentes do gráfico. Minha função tem, portanto, a garantia de manter em vista todos os pontos que estavam à vista de acordo com os limites do gráfico definidos antes de ser chamada. Se o usuário já definiu limites de plotagem muito restritivos para ver todos os pontos de dados, esse é um problema separado. Minha função permite mais flexibilidade porque você pode querer visualizar apenas um subconjunto dos dados. Tudo o que faço é expandir os limites do eixo para que a proporção seja 1: 1: 1.
karlo
Outra forma de colocar isso: se você pegar uma média de apenas 2 pontos, ou seja, os limites de um único eixo, então isso significa que é o ponto médio. Então, pelo que eu posso dizer, a função de Dalum abaixo deveria ser matematicamente equivalente à minha e não havia nada para `` consertar ''.
karlo de
11
Muito superior à solução aceita atualmente que é uma bagunça quando você começa a ter muitos objetos de natureza diferente.
P-Gn
1
Eu realmente gosto da solução, mas depois de atualizar o anaconda, ax.set_aspect ("igual") relatou o erro: NotImplementedError: Atualmente não é possível definir manualmente o aspecto em eixos 3D
Ewan
51

Simplifiquei a solução de Remy F usando as set_x/y/zlim funções .

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_aspect('equal')

X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25

scat = ax.scatter(X, Y, Z)

max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max() / 2.0

mid_x = (X.max()+X.min()) * 0.5
mid_y = (Y.max()+Y.min()) * 0.5
mid_z = (Z.max()+Z.min()) * 0.5
ax.set_xlim(mid_x - max_range, mid_x + max_range)
ax.set_ylim(mid_y - max_range, mid_y + max_range)
ax.set_zlim(mid_z - max_range, mid_z + max_range)

plt.show()

insira a descrição da imagem aqui

taurano
fonte
1
Gosto do código simplificado. Esteja ciente de que alguns (muito poucos) pontos de dados podem não ser plotados. Por exemplo, suponha que X = [0, 0, 0, 100] de modo que X.mean () = 25. Se max_range for 100 (de X), então seu intervalo x será 25 + - 50, então [-25, 75] e você perderá o ponto de dados X [3]. A ideia é muito boa e fácil de modificar para garantir que você obtenha todos os pontos.
TravisJ
1
Esteja ciente de que usar meios como o centro não é correto. Você deve usar algo como midpoint_x = np.mean([X.max(),X.min()])e, em seguida, definir os limites para midpoint_x+/- max_range. Usar a média só funciona se a média estiver localizada no ponto médio do conjunto de dados, o que nem sempre é verdade. Além disso, uma dica: você pode dimensionar max_range para tornar o gráfico mais bonito se houver pontos próximos ou nos limites.
Rainman Noodles
Depois de atualizar o anaconda, ax.set_aspect ("equal") relatou o erro: NotImplementedError: Atualmente não é possível definir manualmente o aspecto nos eixos 3D
Ewan
Em vez de ligar set_aspect('equal'), use set_box_aspect([1,1,1]), conforme descrito na minha resposta abaixo. Está funcionando para mim no matplotlib versão 3.3.1!
AndrewCox
17

Adaptado da resposta de @karlo para tornar as coisas ainda mais limpas:

def set_axes_equal(ax: plt.Axes):
    """Set 3D plot axes to equal scale.

    Make axes of 3D plot have equal scale so that spheres appear as
    spheres and cubes as cubes.  Required since `ax.axis('equal')`
    and `ax.set_aspect('equal')` don't work on 3D.
    """
    limits = np.array([
        ax.get_xlim3d(),
        ax.get_ylim3d(),
        ax.get_zlim3d(),
    ])
    origin = np.mean(limits, axis=1)
    radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
    _set_axes_radius(ax, origin, radius)

def _set_axes_radius(ax, origin, radius):
    x, y, z = origin
    ax.set_xlim3d([x - radius, x + radius])
    ax.set_ylim3d([y - radius, y + radius])
    ax.set_zlim3d([z - radius, z + radius])

Uso:

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.set_aspect('equal')         # important!

# ...draw here...

set_axes_equal(ax)             # important!
plt.show()

EDIT: Esta resposta não funciona em versões mais recentes do Matplotlib devido às alterações mescladas em pull-request #13474, que é rastreado em issue #17172e issue #1077. Como uma solução temporária para isso, é possível remover as linhas recém-adicionadas em lib/matplotlib/axes/_base.py:

  class _AxesBase(martist.Artist):
      ...

      def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
          ...

+         if (not cbook._str_equal(aspect, 'auto')) and self.name == '3d':
+             raise NotImplementedError(
+                 'It is not currently possible to manually set the aspect '
+                 'on 3D axes')
Mateen Ulhaq
fonte
Amei isso, mas depois de atualizar o anaconda, ax.set_aspect ("igual") relatou o erro: NotImplementedError: Atualmente não é possível definir manualmente o aspecto nos eixos 3D
Ewan
@Ewan Eu adicionei alguns links no final da minha resposta para ajudar na investigação. Parece que o pessoal da MPL está tentando solucionar o problema sem corrigir o problema adequadamente por algum motivo. ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq
Acho que encontrei uma solução alternativa (que não requer modificação do código-fonte) para o NotImplementedError (descrição completa em minha resposta abaixo); basicamente adicione ax.set_box_aspect([1,1,1])antes de ligarset_axes_equal
AndrewCox
11

Correção simples!

Consegui fazer isso funcionar na versão 3.3.1.

Parece que esse problema foi resolvido no PR # 17172 ; Você pode usar a ax.set_box_aspect([1,1,1])função para garantir que o aspecto está correto (consulte as notas para a função set_aspect ). Quando usados ​​em conjunto com a (s) função (ões) da caixa delimitadora fornecida (s) por @karlo e / ou @Matee Ulhaq, os gráficos agora parecem corretos em 3D!

Matplotlib gráfico 3D com eixos iguais

Exemplo de trabalho mínimo

import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d
import numpy as np

# Functions from @Mateen Ulhaq and @karlo
def set_axes_equal(ax: plt.Axes):
    """Set 3D plot axes to equal scale.

    Make axes of 3D plot have equal scale so that spheres appear as
    spheres and cubes as cubes.  Required since `ax.axis('equal')`
    and `ax.set_aspect('equal')` don't work on 3D.
    """
    limits = np.array([
        ax.get_xlim3d(),
        ax.get_ylim3d(),
        ax.get_zlim3d(),
    ])
    origin = np.mean(limits, axis=1)
    radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
    _set_axes_radius(ax, origin, radius)

def _set_axes_radius(ax, origin, radius):
    x, y, z = origin
    ax.set_xlim3d([x - radius, x + radius])
    ax.set_ylim3d([y - radius, y + radius])
    ax.set_zlim3d([z - radius, z + radius])

# Generate and plot a unit sphere
u = np.linspace(0, 2*np.pi, 100)
v = np.linspace(0, np.pi, 100)
x = np.outer(np.cos(u), np.sin(v)) # np.outer() -> outer vector product
y = np.outer(np.sin(u), np.sin(v))
z = np.outer(np.ones(np.size(u)), np.cos(v))

fig = plt.figure()
ax = fig.gca(projection='3d')
ax.plot_surface(x, y, z)

ax.set_box_aspect([1,1,1]) # IMPORTANT - this is the new, key line
# ax.set_proj_type('ortho') # OPTIONAL - default is perspective (shown in image above)
set_axes_equal(ax) # IMPORTANT - this is also required
plt.show()
AndrewCox
fonte
Sim, finalmente! Obrigado - se eu pudesse votar em você para o topo :)
N. Jonas Figge
7

EDITAR: o código do user2525140 deve funcionar perfeitamente bem, embora esta resposta supostamente tentou corrigir um erro inexistente. A resposta abaixo é apenas uma implementação duplicada (alternativa):

def set_aspect_equal_3d(ax):
    """Fix equal aspect bug for 3D plots."""

    xlim = ax.get_xlim3d()
    ylim = ax.get_ylim3d()
    zlim = ax.get_zlim3d()

    from numpy import mean
    xmean = mean(xlim)
    ymean = mean(ylim)
    zmean = mean(zlim)

    plot_radius = max([abs(lim - mean_)
                       for lims, mean_ in ((xlim, xmean),
                                           (ylim, ymean),
                                           (zlim, zmean))
                       for lim in lims])

    ax.set_xlim3d([xmean - plot_radius, xmean + plot_radius])
    ax.set_ylim3d([ymean - plot_radius, ymean + plot_radius])
    ax.set_zlim3d([zmean - plot_radius, zmean + plot_radius])
Dalum
fonte
Você ainda precisa fazer: ax.set_aspect('equal')ou os valores do tique podem estar confusos. Caso contrário, boa solução. Obrigado,
Tony Power
1

A partir de matplotlib 3.3.0, Axes3D.set_box_aspect parece ser a abordagem recomendada.

import numpy as np

xs, ys, zs = <your data>
ax = <your axes>

# Option 1: aspect ratio is 1:1:1 in data space
ax.set_box_aspect((np.ptp(xs), np.ptp(ys), np.ptp(zs)))

# Option 2: aspect ratio 1:1:1 in view space
ax.set_box_aspect((1, 1, 1))
Matt Panzer
fonte