Matplotlib - Forcing a 3D plot to use the available figsize

Viewed 286

I'd like to create a plot with a specified figure size in which the 3D axes try to use the entire available space while maintaining an equal aspect ratio on all axis.

My current attempt shows a clipping rectangle that hides part of the mesh.

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

fig = plt.figure(figsize=(5, 2.5))
ax = fig.add_subplot(projection='3d')
ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([1, 1, 1, 0.5]))

u = v = np.linspace(0, 2 * np.pi, 50)
u, v = np.meshgrid(u, v)
X = np.cos(v) * (6 - (5/4 + np.sin(3 * u)) * np.sin(u - 3 * v))
Y = (6 - (5/4 + np.sin(3 * u)) * np.sin(u - 3 * v)) * np.sin(v)
Z = -np.cos(u - 3 * v) * (5/4 + np.sin(3 * u))

ax.plot_surface(X, Y, Z, rstride=1, cstride=1, color=[0.7] * 3, linewidth=0.25, edgecolor="k")
ax.set_box_aspect([ub - lb for lb, ub in (getattr(ax, f'get_{a}lim')() for a in 'xyz')])

plt.show()

current plot

What can I do?

1 Answers

I copied the code you provided and just add 4 lines at the end.

from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
import numpy as np
plt.close()
fig = plt.figure(figsize=(5, 2.5))
ax = fig.add_subplot(projection='3d')
ax.get_proj = lambda: np.dot(Axes3D.get_proj(ax), np.diag([1, 1, 1, 0.5]))

u = v = np.linspace(0, 2 * np.pi, 50)
u, v = np.meshgrid(u, v)
X = np.cos(v) * (6 - (5/4 + np.sin(3 * u)) * np.sin(u - 3 * v))
Y = (6 - (5/4 + np.sin(3 * u)) * np.sin(u - 3 * v)) * np.sin(v)
Z = -np.cos(u - 3 * v) * (5/4 + np.sin(3 * u))

ax.plot_surface(X, Y, Z, rstride=1, cstride=1, color=[0.7] * 3, linewidth=0.25, edgecolor="k")
# ax.set_box_aspect([ub - lb for lb, ub in (getattr(ax, f'get_{a}lim')() for a in 'xyz')])


left, right = plt.xlim()
ax.set_zlim(left, right)
ax.set_ylim(left, right)
plt.tight_layout()

I comment out the line ax.set_box_aspect since it gives me an error. The output of the above is:

enter image description here

--- edit ---

I have an idea for a workaround to make it works in matplotlib v3.4.2:

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

plt.close()

# your code starts here, with a little modification
fig = plt.figure(figsize=(5,5))
ax = fig.add_subplot(projection='3d')

u = v = np.linspace(0, 2 * np.pi, 50)
u, v = np.meshgrid(u, v)
X = np.cos(v) * (6 - (5/4 + np.sin(3 * u)) * np.sin(u - 3 * v))
Y = (6 - (5/4 + np.sin(3 * u)) * np.sin(u - 3 * v)) * np.sin(v)
Z = -np.cos(u - 3 * v) * (5/4 + np.sin(3 * u))
ax.plot_surface(X, Y, Z, rstride=1, cstride=1, color=[0.7] * 3, linewidth=0.25, edgecolor="k")

# set the axes limits
left, right = plt.xlim()
ax.set_zlim(left, right)
ax.set_ylim(left, right)

# zoom in to the plot
ax.dist = 6

# make everything other than the plot itself transparent
fig.patch.set_alpha(0)
ax.patch.set_alpha(0)
ax.axis('off')
plt.tight_layout()

# save plot as image
plt.savefig('plotted.png')

# remove the axes where the image was plotted
ax.remove()

# resize figsize
fig = matplotlib.pyplot.gcf()
fig.set_size_inches(5, 2.5)

# add fake axes for gridlines as in 3d plot to make it look like a real plot
# skip this part if the gridlines are unnecessary
ax_bg = fig.add_subplot(111, projection='3d')
ax_bg.dist = 3

# add axes in cartesian coordinates (xy-plane) for the image
ax = fig.add_subplot(111)
fig.patch.set_alpha(1)
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
im = plt.imread('plotted.png')

h, w, dc = im.shape # (height=500, width=500, depth/color=4)
im_cropped = im[120:390, :, :] # this is manually adjusted
ax.axis('off')
ax.imshow(im_cropped)

# delete the saved image
os.remove('plotted.png')

Output is:

enter image description here

I don't know if it will work in your particular context, but it works for just fulfilling your question.

Let me know if something is unclear.

Related