10

I am designing a three-dimensional illustration using Matplotlib. All is working nicely, except that the (red) parametric curve gets the wrong zorder while the (green) parametric surface is drawn completely correctly.

Output generated by code below: Output generated by code below

I know that Matplotlib has limited capabilities for accurately computing the zorder of objects, but since it can do it for the parametric surface, it seems like a bug in Matplotlib.

That said, is there any way to force correct z-ordering just to get things to work quickly? It seems that all I have to be able to say is that the right transparent blue plane is on top of everything else. However, putting a zorder argument into PolyCollection does not seem to have any effect, and putting an explicit zorder argument into the plot function which draws the read line will mess up its ordering relative to the green surface.

Is there a way to force the right blue transparent surface on top of everything? Here is the code I have so far:

#!/bin/env python3

from pylab import *
from mpl_toolkits.mplot3d import *

from matplotlib.collections import PolyCollection
from matplotlib.colors import colorConverter
from matplotlib.patches import FancyArrowPatch

rc('text', usetex=True)
rc('font', size=20)

fig = figure(figsize=(11,6))
ax = fig.gca(projection='3d')

ax.set_axis_off()

def f(x,t):
    return t/2 * 0.55*(sin(2*x)+0.4*x**2-0.65)

c_plane   = colorConverter.to_rgba('b', alpha=0.15)

N = 50
y = linspace(-1,1,N)
t = linspace(0,2,N)
yy, tt = meshgrid(y, t)
zz = f(yy,tt)

ax.plot(0*ones(y.shape), y, f(y,0), '-g', linewidth=3)
ax.plot(2*ones(y.shape), y, f(y,2), '-g', linewidth=3)

yt = 0.7*y
zt = f(yt, t) + 0.2*t

ax.plot(t, yt, zt, '-r', linewidth=3)
ax.plot((0,2), (yt[0], yt[-1]), (zt[0], zt[-1]), 'or')

ax.plot([2,2,2], [-1,yt[-1],yt[-1]], [zt[-1],zt[-1],-1], 'k--')
ax.plot(2*ones(y.shape), yt, f(yt,2)+0.1*(y+1), 'g:', linewidth=2)
ax.plot((2,2),
(yt[0], yt[-1]),
(f(yt[0], 2), f(yt[-1], 2) + 0.1*(y[-1]+1)), 'og')
ax.plot((0,2,2),
(-1,-1,zt[-1]),
(0,yt[-1],-1), 'ok')

ax.text(0, -1.1, 0, r'$p(0)=0$', ha='right', va='center')
ax.text(2, -1.05, zt[-1], r'$p(T)$', ha='right', va='center')
ax.text(0, -1.0, 1, r'$p$', ha='right', va='bottom')
ax.text(0, 1, -1.1, r'$q$', ha='center', va='top')
ax.text(0, -1, -1.1, r'$t=0$', ha='right', va='top')
ax.text(2, -1, -1.1, r'$t=T$', ha='right', va='top')
ax.text(2, yt[-1]-0.05, -1.05, r'$q(T)=q^*$', ha='left', va='top')
ax.text(0, 0.5, 0.05, r'$\mathcal{M}(0)$', ha='center', va='bottom')
ax.text(2, 0.1, -0.8, r'$\mathcal{M}(T)$', ha='center', va='bottom')

arrowprops = dict(mutation_scale=20,
linewidth=2,
arrowstyle='-|>',
color='k')

# For arrows, see
# https://stackoverflow.com/questions/29188612/arrows-in-matplotlib-using-mplot3d
class Arrow3D(FancyArrowPatch):
    def __init__(self, xs, ys, zs, *args, **kwargs):
        FancyArrowPatch.__init__(self, (0,0), (0,0), *args, **kwargs)
        self._verts3d = xs, ys, zs

    def draw(self, renderer):
        xs3d, ys3d, zs3d = self._verts3d
        xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M)
        self.set_positions((xs[0],ys[0]),(xs[1],ys[1]))
        FancyArrowPatch.draw(self, renderer)

a = Arrow3D([0,2], [-1,-1], [-1,-1], **arrowprops)
ax.add_artist(a)
a = Arrow3D([0,0], [-1,-1], [-1,1], **arrowprops)
ax.add_artist(a)
a = Arrow3D([0,0], [-1,1], [-1,-1], **arrowprops)
ax.add_artist(a)

# For surface illumination, see
# http://physicalmodelingwithpython.blogspot.de/2015/08/illuminating-surface-plots.html

# Get lighting object for shading surface plots.
from matplotlib.colors import LightSource

# Get colormaps to use with lighting object.
from matplotlib import cm

# Create an instance of a LightSource and use it to illuminate the surface.
light = LightSource(70, -120)
white = np.ones((zz.shape[0], zz.shape[1], 3))
illuminated_surface = light.shade_rgb(white*(0,1,0), zz)

ax.plot_surface(tt, yy, zz,
cstride=1, rstride=1,
alpha=0.3, facecolors=illuminated_surface,
linewidth=0)

verts = [array([(-1,-1), (-1,1), (1,1), (1,-1), (-1,-1)])]

poly = PolyCollection(verts, facecolors=c_plane)
ax.add_collection3d(poly, zs=[0], zdir='x')
poly = PolyCollection(verts, facecolors=c_plane)
ax.add_collection3d(poly, zs=[2], zdir='x')

ax.set_xlim3d(0, 2)
ax.view_init(elev=18, azim=-54)

show()
gboffi
  • 22,939
  • 8
  • 54
  • 85
mjo
  • 189
  • 1
  • 7

3 Answers3

9

A way of changing the drawing order for plots using axis3d was added in Matplotlib 3.5.0. Setting the parameter 'computed_zorder' False allows manual control of the drawing order

ax = plt.axes(projection='3d',computed_zorder=False)
ax.plot_surface(X1, Y1, Z1,zorder=4.4)
ax.plot_surface(X2, Y2, Z2,zorder=4.5)

Higher 'zorders' are plotted on top. Some common artist z-orders (so you don't plot over your legend):

Artist Z-order
Images (AxesImage, FigureImage, BboxImage) 0
Patch, PatchCollection 1
Line2D, LineCollection (including minor ticks, grid lines) 2
Major ticks 2.01
Text (including axes labels and titles) 3
Legend 5

From: https://matplotlib.org/stable/gallery/misc/zorder_demo.html

Source: https://github.com/matplotlib/matplotlib/commit/2db6a0429af47102456366f8d3a4df24352b252e (from: https://github.com/matplotlib/matplotlib/pull/14508)

Whitty
  • 91
  • 1
  • 2
  • 1
    In case someone needs to define the axes as `fig, ax = plt.subplots(subplot_kw={'projection': '3d'})`, the same result can be achieved by setting afterwards `ax.computed_zorder = False`. On the other hand, if several subplots are created (`plt.subplots(nrows=i, ncols=j, subplot_kw={'projection': '3d'})`), just use: `for axi in ax: axi.computed_zorder = False`. PD: thanks a lot for the answer :) (+1 in the past) – Javier TG Oct 08 '22 at 22:56
5

Axes3D ignores zorder and draws all artists in the order it thinks they should be. However, you may set zorder=0 for red line and zorder=-1 for green surface (or vice-versa) to put they behind right blue panel.

My result:

enter image description here

You have to know:

The default drawing order for axes is patches, lines, text. This order is determined by the zorder attribute. The following defaults are set

Artist Z-order

Patch / PatchCollection 1

Line2D / LineCollection 2

Text 3

Community
  • 1
  • 1
Serenity
  • 35,289
  • 20
  • 120
  • 115
  • I had tried this, but it is not quite correct. The red curve is now below the green surface (very visible in the rightmost part where it comes up from behind on the picture you posted). In fact, plot_surface seems to ignore the zorder attribute completely. If it didn't, I suppose I'd run into trouble with the left blue plane, but presume that could be fixed by explicit zorder specifications, too. – mjo Jun 03 '16 at 13:58
  • Note there have been behavior changes in 3.0/3.1 that break this behavior, so if you're having trouble reproducing these results you might try reverting to 2.2.4. See: https://stackoverflow.com/q/52923540/188046 – Elliott Slaughter Jul 24 '19 at 19:50
3

After some more trial and error, I found a solution. If the right plane is drawn using plot_surface and I change the zorder on the red curve, matplotlib gets the overall order of objects right. Funny enough, the color of the planes changes slightly whether I draw them via PolyCollection or plot_surface, so I need to draw both planes using the same function. So the zorder handling of mplot3d is rather inconsistent, but the final result looks pretty good. I post it here for reference

with final code here:

#!/bin/env python3

from pylab import *
from mpl_toolkits.mplot3d import *

from matplotlib.collections import PolyCollection
from matplotlib.colors import colorConverter
from matplotlib.patches import FancyArrowPatch

rc('text', usetex=True)
rc('font', size=20)

fig = figure(figsize=(11,6))
ax = fig.gca(projection='3d')

ax.set_axis_off()

def f(x,t):
    return t/2 * 0.55*(sin(2*x)+0.4*x**2-0.65)

c_plane   = colorConverter.to_rgba('b', alpha=0.15)

N = 50
y = linspace(-1,1,N)
t = linspace(0,2,N)
yy, tt = meshgrid(y, t)
zz = f(yy,tt)

ax.plot(0*ones(y.shape), y, f(y,0), '-g', linewidth=3)
ax.plot(2*ones(y.shape), y, f(y,2), '-g', linewidth=3)

yt = 0.7*y
zt = f(yt, t) + 0.2*t

ax.plot(t, yt, zt, '-r', linewidth=3, zorder = 1)

ax.plot([2,2,2], [-1,yt[-1],yt[-1]], [zt[-1],zt[-1],-1], 'k--')
ax.plot(2*ones(y.shape), yt, f(yt,2)+0.1*(y+1), 'g:', linewidth=2)
ax.plot((2,2),
        (yt[0], yt[-1]),
        (f(yt[0], 2), f(yt[-1], 2) + 0.1*(y[-1]+1)), 'og')
ax.plot((0,2,2),
        (-1,-1,zt[-1]),
        (0,yt[-1],-1), 'ok')

ax.text(0, -1.1, 0, r'$p(0)=0$', ha='right', va='center')
ax.text(2, -1.05, zt[-1], r'$p(T)$', ha='right', va='center')
ax.text(0, -1.0, 1, r'$p$', ha='right', va='bottom')
ax.text(0, 1, -1.1, r'$q$', ha='center', va='top')
ax.text(0, -1, -1.1, r'$t=0$', ha='right', va='top')
ax.text(2, -1, -1.1, r'$t=T$', ha='right', va='top')
ax.text(2, yt[-1]-0.05, -1.05, r'$q(T)=q^*$', ha='left', va='top')
ax.text(0, 0.5, 0.05, r'$\mathcal{M}(0)$', ha='center', va='bottom')
ax.text(2, 0.1, -0.8, r'$\mathcal{M}(T)$', ha='center', va='bottom')

arrowprops = dict(mutation_scale=20,
                  linewidth=2,
                  arrowstyle='-|>',
                  color='k')

# For arrows, see
# https://stackoverflow.com/questions/29188612/arrows-in-matplotlib-using-mplot3d
class Arrow3D(FancyArrowPatch):
    def __init__(self, xs, ys, zs, *args, **kwargs):
        FancyArrowPatch.__init__(self, (0,0), (0,0), *args, **kwargs)
        self._verts3d = xs, ys, zs

    def draw(self, renderer):
        xs3d, ys3d, zs3d = self._verts3d
        xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M)
        self.set_positions((xs[0],ys[0]),(xs[1],ys[1]))
        FancyArrowPatch.draw(self, renderer)

a = Arrow3D([0,2], [-1,-1], [-1,-1], **arrowprops)
ax.add_artist(a)
a = Arrow3D([0,0], [-1,-1], [-1,1], **arrowprops)
ax.add_artist(a)
a = Arrow3D([0,0], [-1,1], [-1,-1], **arrowprops)
ax.add_artist(a)

# For surface illumination, see
# http://physicalmodelingwithpython.blogspot.de/2015/08/illuminating-surface-plots.html

# Get lighting object for shading surface plots.
from matplotlib.colors import LightSource

# Get colormaps to use with lighting object.
from matplotlib import cm

# Create an instance of a LightSource and use it to illuminate the surface.
light = LightSource(70, -120)
white = ones((zz.shape[0], zz.shape[1], 3))
illuminated_surface = light.shade_rgb(white*(0,1,0), zz)

ax.plot_surface(tt, yy, zz,
                cstride=1, rstride=1,
                alpha=0.3, facecolors=illuminated_surface,
                linewidth=0,
                zorder=10)

verts = [array([(-1,-1), (-1,1), (1,1), (1,-1), (-1,-1)])]

ax.plot_surface(((0,0),(0,0)), ((-1,-1),(1,1)), ((-1,1),(-1,1)),
                color=c_plane)

ax.plot_surface(((2,2),(2,2)), ((-1,-1),(1,1)), ((-1,1),(-1,1)),
                color=c_plane)

ax.plot((0,2), (yt[0], yt[-1]), (zt[0], zt[-1]), 'or')

ax.set_xlim3d(0, 2)
ax.view_init(elev=18, azim=-54)

show()
mjo
  • 189
  • 1
  • 7