Wydaje mi się, że matplotlib nie ustawia jeszcze poprawnie równej osi w 3D ... Ale jakiś czas temu znalazłem sztuczkę (nie pamiętam gdzie), którą zaadaptowałem. Koncepcja polega na utworzeniu fałszywej sześciennej ramki ograniczającej wokół danych. Możesz to przetestować za pomocą następującego kodu:
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()
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())
for xb, yb, zb in zip(Xb, Yb, Zb):
ax.plot([xb], [yb], [zb], 'w')
plt.grid()
plt.show()
dane z są o rząd wielkości większe niż xiy, ale nawet z opcją równych osi, matplotlib autoscale oś z:
Ale jeśli dodasz obwiednię, uzyskasz poprawne skalowanie:
equal
oświadczenia - zawsze będzie równe.equal
oświadczenia.Podobają mi się powyższe rozwiązania, ale mają one tę wadę, że musisz śledzić zakresy i średnie dla wszystkich danych. Może to być kłopotliwe, jeśli masz wiele zestawów danych, które zostaną razem wykreślone. Aby to naprawić, użyłem metod ax.get_ [xyz] lim3d () i umieściłem całość w samodzielnej funkcji, którą można wywołać tylko raz przed wywołaniem plt.show (). Oto nowa wersja:
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()
źródło
Uprościłem rozwiązanie Remy F za pomocą
set_x/y/zlim
funkcji .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()
źródło
midpoint_x = np.mean([X.max(),X.min()])
a następnie ustawić limity namidpoint_x
+/-max_range
. Użycie średniej działa tylko wtedy, gdy średnia znajduje się w środku zbioru danych, co nie zawsze jest prawdą. Wskazówka: możesz skalować max_range, aby wykres wyglądał ładniej, jeśli w pobliżu lub na granicach znajdują się punkty.set_aspect('equal')
, użyjset_box_aspect([1,1,1])
, jak opisano w mojej odpowiedzi poniżej. U mnie działa w matplotlib w wersji 3.3.1!Na podstawie odpowiedzi @ karlo, aby uczynić rzeczy jeszcze czystszymi:
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])
Stosowanie:
fig = plt.figure() ax = fig.gca(projection='3d') ax.set_aspect('equal') # important! # ...draw here... set_axes_equal(ax) # important! plt.show()
EDYTUJ: Ta odpowiedź nie działa w nowszych wersjach Matplotlib z powodu scalonych zmian
pull-request #13474
, które są śledzone wissue #17172
iissue #1077
. W ramach tymczasowego obejścia tego problemu można usunąć nowo dodane wiersze wlib/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')
źródło
ax.set_box_aspect([1,1,1])
przed zadzwonieniemset_axes_equal
Prosta naprawa!
Udało mi się to uruchomić w wersji 3.3.1.
Wygląda na to, że ten problem został prawdopodobnie rozwiązany w dokumencie PR # 17172 ; Możesz użyć tej
ax.set_box_aspect([1,1,1])
funkcji, aby upewnić się, że aspekt jest poprawny (zobacz uwagi dotyczące funkcji set_aspect ). W połączeniu z funkcjami obwiedni udostępnianymi przez @karlo i / lub @Matee Ulhaq, wykresy wyglądają teraz poprawnie w 3D!Minimalny przykład roboczy
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()
źródło
EDYCJA: kod użytkownika2525140 powinien działać doskonale, chociaż ta odpowiedź rzekomo próbowała naprawić nieistniejący błąd. Poniższa odpowiedź to tylko zduplikowana (alternatywna) implementacja:
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])
źródło
ax.set_aspect('equal')
albo wartości tików mogą zostać schrzanione. W przeciwnym razie dobre rozwiązanie. Dzięki,Od wersji matplotlib 3.3.0 Axes3D.set_box_aspect wydaje się być zalecanym podejściem.
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))
źródło