matplotlib (longueur unitaire égale): avec un rapport d'aspect 'égal', l'axe z n'est pas égal à x- et y-

87

Lorsque je configure le rapport hauteur / largeur égal pour le graphique 3D, l'axe z ne passe pas à «égal». Donc ça:

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 donne ce qui suit: entrez la description de l'image ici

où évidemment la longueur unitaire de l'axe z n'est pas égale aux unités x et y.

Comment puis-je rendre la longueur unitaire des trois axes égale? Toutes les solutions que j'ai pu trouver n'ont pas fonctionné. Merci.


la source

Réponses:

69

Je crois que matplotlib ne définit pas encore correctement l'axe égal en 3D ... Mais j'ai trouvé il y a quelques temps une astuce (je ne me souviens pas où) que j'ai adaptée en l'utilisant. Le concept est de créer une fausse boîte englobante cubique autour de vos données. Vous pouvez le tester avec le code suivant:

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()

Les données z sont d'un ordre de grandeur supérieur à x et y, mais même avec l'option d'axe égal, axe z autoscale matplotlib:

mal

Mais si vous ajoutez la boîte englobante, vous obtenez une mise à l'échelle correcte:

entrez la description de l'image ici

Remy F
la source
Dans ce cas, vous n'avez même pas besoin de la equaldéclaration - elle sera toujours égale.
1
Cela fonctionne bien si vous tracez un seul ensemble de données, mais qu'en est-il quand il y a plus d'ensembles de données sur le même tracé 3D? En question, il y avait 2 ensembles de données, c'est donc une chose simple de les combiner, mais cela pourrait devenir déraisonnable rapidement si l'on traçait plusieurs ensembles de données différents.
Steven C. Howell
@ stvn66, je traçais jusqu'à cinq ensembles de données dans un graphique avec ces solutions et cela a bien fonctionné pour moi.
1
Cela fonctionne parfaitement. Pour ceux qui le souhaitent sous forme de fonction, qui prend un objet axe et effectue les opérations ci-dessus, je les encourage à consulter la réponse @karlo ci-dessous. C'est une solution légèrement plus propre.
spurra
@ user1329187 - J'ai trouvé que cela ne fonctionnait pas pour moi sans la equaldéclaration.
supergra
58

J'aime les solutions ci-dessus, mais elles présentent l'inconvénient dont vous avez besoin pour garder une trace des plages et des moyens sur toutes vos données. Cela peut être fastidieux si vous disposez de plusieurs ensembles de données qui seront tracés ensemble. Pour résoudre ce problème, j'ai utilisé les méthodes ax.get_ [xyz] lim3d () et mis le tout dans une fonction autonome qui peut être appelée une seule fois avant d'appeler plt.show (). Voici la nouvelle version:

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
la source
Sachez que l'utilisation de moyennes comme point central ne fonctionnera pas dans tous les cas, vous devez utiliser des points médians. Voir mon commentaire sur la réponse de tauran.
Rainman Noodles
1
Mon code ci-dessus ne prend pas la moyenne des données, il prend la moyenne des limites de l'intrigue existantes. Ma fonction est ainsi garantie de garder en vue tous les points qui étaient en vue selon les limites de l'intrigue définies avant son appel. Si l'utilisateur a déjà défini des limites de tracé de manière trop restrictive pour voir tous les points de données, il s'agit d'un problème distinct. Ma fonction offre plus de flexibilité car vous souhaiterez peut-être n'afficher qu'un sous-ensemble des données. Tout ce que je fais, c'est élargir les limites de l'axe pour que le rapport hauteur / largeur soit de 1: 1: 1.
karlo
Une autre façon de le dire: si vous prenez une moyenne de seulement 2 points, à savoir les limites sur un seul axe, alors cela signifie EST le point médian. Donc, pour autant que je sache, la fonction de Dalum ci-dessous devrait être mathématiquement équivalente à la mienne et il n'y avait rien à `` réparer ''.
karlo
11
Vraiment supérieur à la solution actuellement acceptée qui est un gâchis lorsque vous commencez à avoir beaucoup d'objets de nature différente.
P-Gn le
1
J'aime vraiment la solution, mais après avoir mis à jour anaconda, ax.set_aspect ("equal") a signalé une erreur: NotImplementedError: Il n'est actuellement pas possible de définir manuellement l'aspect sur les axes 3D
Ewan
51

J'ai simplifié la solution de Remy F en utilisant les set_x/y/zlim fonctions .

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()

entrez la description de l'image ici

tauran
la source
1
J'aime le code simplifié. Sachez simplement que certains (très peu) points de données peuvent ne pas être tracés. Par exemple, supposons que X = [0, 0, 0, 100] de sorte que X.mean () = 25. Si max_range est égal à 100 (à partir de X), alors votre plage x sera 25 + - 50, donc [-25, 75] et vous manquerez le point de données X [3]. L'idée est cependant très agréable et facile à modifier pour vous assurer d'obtenir tous les points.
TravisJ
1
Attention, l'utilisation des moyens comme centre n'est pas correcte. Vous devez utiliser quelque chose comme midpoint_x = np.mean([X.max(),X.min()]), puis définir les limites sur midpoint_x+/- max_range. L'utilisation de la moyenne ne fonctionne que si la moyenne est située au milieu de l'ensemble de données, ce qui n'est pas toujours vrai. Aussi, un conseil: vous pouvez mettre à l'échelle max_range pour rendre le graphique plus joli s'il y a des points près ou sur les limites.
Rainman Noodles
Après avoir mis à jour anaconda, ax.set_aspect ("equal") a signalé une erreur: NotImplementedError: Il n'est actuellement pas possible de définir manuellement l'aspect sur les axes 3D
Ewan
Plutôt que d'appeler set_aspect('equal'), utilisez set_box_aspect([1,1,1]), comme décrit dans ma réponse ci-dessous. Cela fonctionne pour moi dans la version 3.3.1 de matplotlib!
AndrewCox
17

Adapté de la réponse de @ karlo pour rendre les choses encore plus propres:

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])

Usage:

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

# ...draw here...

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

EDIT: Cette réponse ne fonctionne pas sur les versions plus récentes de Matplotlib en raison des modifications fusionnées dans pull-request #13474, qui sont suivies dans issue #17172et issue #1077. Pour contourner ce problème, vous pouvez supprimer les lignes nouvellement ajoutées dans 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
la source
J'adore ça, mais après avoir mis à jour anaconda, ax.set_aspect ("equal") a signalé une erreur: NotImplementedError: Il n'est actuellement pas possible de définir manuellement l'aspect sur les axes 3D
Ewan
@Ewan J'ai ajouté quelques liens au bas de ma réponse pour aider à l'enquête. Il semble que les gens de MPL rompent les solutions de contournement sans résoudre correctement le problème pour une raison quelconque. ¯ \\ _ (ツ) _ / ¯
Mateen Ulhaq
Je pense avoir trouvé une solution de contournement (qui ne nécessite pas de modifier le code source) pour le NotImplementedError (description complète dans ma réponse ci-dessous); essentiellement ajouter ax.set_box_aspect([1,1,1])avant d'appelerset_axes_equal
AndrewCox
11

Solution simple!

J'ai réussi à faire fonctionner cela dans la version 3.3.1.

Il semble que ce problème ait peut-être été résolu dans le PR # 17172 ; Vous pouvez utiliser la ax.set_box_aspect([1,1,1])fonction pour vous assurer que l'aspect est correct (voir les notes de la fonction set_aspect ). Lorsqu'elles sont utilisées en conjonction avec les fonctions de boîte englobante fournies par @karlo et / ou @Matee Ulhaq, les tracés semblent maintenant corrects en 3D!

tracé 3D matplotlib à axes égaux

Exemple de travail minimum

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
la source
Oui enfin! Merci - si seulement je pouvais vous voter pour le haut :)
Figge
7

EDIT: le code de user2525140 devrait fonctionner parfaitement bien, même si cette réponse aurait tenté de corriger une erreur inexistante. La réponse ci-dessous est juste une implémentation en double (alternative):

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
la source
Vous devez encore faire: ax.set_aspect('equal')ou les valeurs de graduation peuvent être foirées. Sinon bonne solution. Merci,
Tony Power
1

Depuis matplotlib 3.3.0, Axes3D.set_box_aspect semble être l'approche recommandée.

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
la source