3D parametric curve in Matplotlib does not respect zorder. Workaround? - python

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

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)

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:
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

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

Related

pyplot: adding point projections on axis

I can get a graph drawn using the plot function.
But I would like to highlight some "special" points by having the projections drawn on the axes and putting text on both the point and the axes.
Something like this:
I tried with this:
import matplotlib.pyplot as plt
[...]
plt.plot(X, Y, label='data') # draw curve, X and Y are arrays
plt.plot(Xp, Yp, c, duration), marker='o') # draw point #(Xp, Yp), Xp and Yp are scalars
plt.vlines(Xp, min(Y), Yp, linestyles='dashed')
plt.hlines(Yp, min(X), Xp, linestyles='dashed')
plt.grid(True)
plt.show()
but what I get is not satisfactory:
What is the right way to get what I want?
I've also considered annotate, but it doesn't seem to do what I need. Correct me if I'm wrong.
You can use annotate with a blended transformation:
import matplotlib.pyplot as plt
plt.plot([1,2], [2,4], label='data')
plt.plot([1.7], [3.4], marker='o')
plt.grid(True)
x,y = 1.7, 3.4
arrowprops={'arrowstyle': '-', 'ls':'--'}
plt.annotate(str(x), xy=(x,y), xytext=(x, 0),
textcoords=plt.gca().get_xaxis_transform(),
arrowprops=arrowprops,
va='top', ha='center')
plt.annotate(str(y), xy=(x,y), xytext=(0, y),
textcoords=plt.gca().get_yaxis_transform(),
arrowprops=arrowprops,
va='center', ha='right')
It's not perfect as the you'll still may want to manually adjust the axis coordinates (e.g. -0.05 instead of 0) to set the labels a bit off the axes.
You need to play around with xlim and ylim a bit.
For me this worked:
import matplotlib.pyplot as plt
import numpy as np
if __name__ == "__main__":
X = np.linspace(-.5, 3, 100)
Y = 15000 - 10 * (X - 2.2) ** 2
Xp = X[-10]
Yp = Y[-10]
plt.plot(X, Y, label='data')
plt.plot(Xp, Yp, marker='o')
plt.vlines(Xp, min(Y), Yp, linestyles='dashed')
plt.hlines(Yp, min(X), Xp, linestyles='dashed')
plt.grid(True)
plt.xlim(min(X), None)
plt.ylim(min(Y), None)
plt.show()
Something like that maybe is the answer that you are searching for. https://stackoverflow.com/a/14434334/14920085
y = [2.56422, 3.77284, 3.52623, 3.51468, 3.02199]
z = [0.15, 0.3, 0.45, 0.6, 0.75]
n = [58, 651, 393, 203, 123] #text that you want to print at the points
fig, ax = plt.subplots()
ax.scatter(z, y)
ax.set_ylabel('y')
ax.set_xlabel('x')
for i, txt in enumerate(n):
ax.annotate(txt, (z[i], y[i]))

How to add customized ticks in Heatmap?

I plotted an array in seaborn heatmap, and I want to add tick limits to the axis.
My code:
# plot
eixoz = numpy.linspace(0, Z)
eixor = numpy.linspace(ra, R, nr)
eixox = D
numpy.meshgrid(eixoz, eixor)
ax = seaborn.heatmap(eixox)
ax.invert_yaxis()
plt.xlabel("Eixo z", fontsize=20)
plt.ylabel("Eixo r", fontsize=20)
ax.get_xaxis().set_ticks([])
ax.get_yaxis().set_ticks([])
ax.collections[0].colorbar.set_label("Celsius", fontsize=20)
plt.show()
How can I add those limit ticks in blue? And also, how can I resize the color bar numbers?
The size of the colorbar tick labels can be changed via ax.collections[0].colorbar.ax.tick_params(labelsize=20).
Text at the start and end of the axes can be place using the axes transform, where 0 is the left (or bottom) and 1 is the right (or top) of the axes. Negative values (or values larger than 1) are proportionall outside the axes area. Horizontal and vertical lines can use the same transform, but unlike text need clip_on=False to be drawn outside the axes area.
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set()
len_eixoz = 20
eixox = np.repeat(np.arange(37.55, 37.66, 0.02), len_eixoz).reshape(-1, len_eixoz)
ax = sns.heatmap(eixox)
ax.invert_yaxis()
ax.set_xlabel("Eixo z", fontsize=20)
ax.set_ylabel("Eixo r", fontsize=20)
ax.get_xaxis().set_ticks([])
ax.get_yaxis().set_ticks([])
ax.collections[0].colorbar.set_label("Celsius", fontsize=20)
cbar = ax.collections[0].colorbar.ax.tick_params(labelsize=20)
x0, x1 = 1, 2
y0, y1 = 0, 1
ax.text(0, -0.07, x0, ha='center', va='top', fontsize=20, color='steelblue', transform=ax.transAxes)
ax.text(1, -0.07, x1, ha='center', va='top', fontsize=20, color='steelblue', transform=ax.transAxes)
ax.text(-0.05, 0, y0, ha='right', va='center', fontsize=20, color='steelblue', transform=ax.transAxes)
ax.text(-0.05, 1, y1, ha='right', va='center', fontsize=20, color='steelblue', transform=ax.transAxes)
ax.vlines([0, 1], [0, 0], [-0.06, -0.06], color='crimson', clip_on=False, transform=ax.transAxes)
ax.hlines([0, 1], [0, 0], [-0.04, -0.04], color='crimson', clip_on=False, transform=ax.transAxes)
plt.tight_layout()
plt.show()
Note that calling sns.set(font_scale=1.8) at the start would scale all fonts.

matplotlib: horizontal spacing of subplots with equal aspect ratio

I'm trying to draw two rows with three columns of pcolormesh plots and a combined colorbar for all plots. So far it seems to work. However, I'm sure I'm not using the most elegant way...
The only problem I have, is that I can't decrease the horizontal spacing any further. The following line should set the horizontal spacing to zero:
fig.subplots_adjust(left=0.05, right=0.98, top=0.93, bottom=0.00, wspace=0, hspace=0.03)
But this does not work in conjunction with
ax.set_aspect('equal')
I've attached a small code snippet that creates the following figure:
Example figure
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
rows = 2
columns = 3
fig = plt.figure()
gs = gridspec.GridSpec(rows+1, columns)
lines = []
x = np.linspace(1,10,100)
y = x
X, Y = np.meshgrid(x,y)
Z = np.random.rand(100,100)
lines = []
for i in range(rows):
lines.append([])
for j in range(columns):
ax = fig.add_subplot(gs[i, j])
line = ax.pcolormesh(X, Y, Z, cmap=plt.cm.Reds)
lines[i].append(line)
ax.set_aspect('equal')
for tick in ax.get_xticklabels():
tick.set_rotation(45)
if i!=rows-1:
ax.set_xticklabels([])
if j!=0:
ax.set_yticklabels([])
#title
props = dict(boxstyle='round', facecolor='white', alpha=0.7)
ax.text(0.05, 0.95, "plot (%i, %i)" % (i,j), transform=ax.transAxes, fontsize=5,
verticalalignment='top', bbox=props)
ax.tick_params(labelsize=7)
cb_ax = fig.add_subplot(gs[-1,:])
cb_ax.set_aspect(0.05)
cbar = fig.colorbar(lines[0][0], cax=cb_ax, orientation='horizontal')
cb_ax.tick_params(labelsize=7)
fig.subplots_adjust(left=0.05, right=0.98, top=0.93, bottom=0.00, wspace=0, hspace=0.03)
#fig.tight_layout()
fig.text(0.5, 0.2, "x axis", ha='center', va='center')
fig.text(0.5, 0.97, "overall title", ha='center', va='center')
fig.text(0.02, 0.5, "y axis", ha='center', va='center', rotation='vertical')
fig.text(0.5, 0.02, "quantity [unit]", ha='center', va='center',)
plt.savefig("test.png", dpi=600)

python text to second y axis

I'm trying to make my first plots in python using matplotlib, but i would like the text in the plot to be "outside" the plot i.e. next to the line instead of above it or under it.
plt.figure()
plt.scatter(mean, diff, color='k')
plt.axhline(md, color='black', linestyle='-', lw=3)
plt.axhline(md + 1.96*sd, color='black', linestyle='--')
plt.axhline(md - 1.96*sd, color='black', linestyle='--')
plt.axhline(0, color='black', linestyle='--')
plt.ylim(-max(diff)*2, max(diff)*2)
plt.xlabel('Mean')
plt.ylabel('Difference')
plt.title('Bland altman plot for ' + variable)
txt1=('+1.96 SD')
txt2=('-1.96 SD')
txt3 =('Mean')
x = max(mean)
plt.text(x, md+1.96*sd, txt1, horizontalalignment='left', verticalalignment='bottom', fontweight='bold')
plt.text(x, md-1.96*sd, txt2, horizontalalignment='left', verticalalignment='top', fontweight='bold')
plt.text(x, 0.1, txt3)
The result is:
The main ingredient you still need to position the labels at the edge of the axes, is the coordinate of the edge of the axes. You can get those via plt.gca().get_xlim(). The upper limit can then be used as x position of the text label.
import matplotlib.pyplot as plt
import numpy as np
X = np.random.normal(size=(12))
Y = np.random.normal(size=(12))
plt.figure()
plt.scatter(X, Y, color='k')
plt.axhline(Y.mean(), color='black', linestyle='-', lw=3)
plt.axhline(Y.mean() + 1.96*Y.std(), color='black', linestyle='--')
plt.axhline(Y.mean() - 1.96*Y.std(), color='black', linestyle='--')
plt.axhline(0, color='black', linestyle='--')
txt1=(' +1.96 SD')
txt2=(' -1.96 SD')
txt3 =(' Mean')
x0,x1 = plt.gca().get_xlim()
plt.text(x1, Y.mean() + 1.96*Y.std(), txt1, ha='left', va='center', fontweight='bold')
plt.text(x1, Y.mean() - 1.96*Y.std(), txt2, ha='left', va='center', fontweight='bold')
plt.text(x1, 0, txt3, ha='left', va='center',)
# make more space on right side to host the labels
plt.subplots_adjust(right=0.8)
plt.show()
Note that this is the very basic (but also quite understandable) version.
If you're comfortable with transformations, you can use the technique in this question:
Add a label to y-axis to show the value of y for a horizontal line in matplotlib

plotting single 3D point on top of plot_surface in python matplotlib

I have some code to plot 3D surfaces in Python using matplotlib:
import math
import numpy as np
import matplotlib.pyplot as plt
from pylab import meshgrid,cm,imshow,contour,clabel,colorbar,axis
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import seaborn as sns
sns.set(style="white")
def surface_map(func, xmin=0, xmax=1, ymin=0, ymax=1, step_size=0.05, maxz=25000):
X, Y = meshgrid(
np.arange(xmin, xmax, step_size),
np.arange(ymin, ymax, step_size))
Z = np.zeros(X.shape)
for i in range(X.shape[0]):
for j in range(X.shape[1]):
Z[i, j] = min(func(X[i, j], Y[i, j]), maxz)
return X, Y, Z
def plot_surface(X, Y, Z, xlabel, ylabel, zlabel, title, point=None, size=25):
fig = plt.figure()
ax = fig.gca(projection='3d')
surf = ax.plot_surface(X, Y, Z,
rstride=1, cstride=1, vmin=0, vmax=20*1000,
cmap=cm.RdBu, linewidth=0, antialiased=True)
ax.zaxis.set_major_locator(LinearLocator(10))
ax.zaxis.set_major_formatter(FormatStrFormatter('%.02f'))
ax.set_xlabel(xlabel)
ax.set_ylabel(ylabel)
ax.set_zlabel(zlabel)
ax.set_title(title)
fig.colorbar(surf, shrink=0.5, aspect=5)
if point:
ax.hold(True)
func, fpr, recall = point
ax.scatter([fpr], [recall], [
func(fpr, recall)
], s=size, c='b', marker='.', zorder=10)
plt.show()
And then I call it like so:
# create mesh
R, FPR, FuncValue = surface_map(my_function, xmin=0, xmax=1, ymin=0, ymax=1, step_size=0.05, maxz=20*1000)
# plot it
plot_surface(R, FPR, FuncValue,
xlabel="Recall",
ylabel="FPR",
zlabel="Function Value",
title="Recall Settings Payout Function",
point=(my_function, 0.5, 0.5))
I'm setting ax.scatter to use large marker sizes and a high zorder, but no point gets drawn on the surface when the plot gets rendered.
What am I missing?
The point you are looking for is there, but hidden "inside" the surface. This is a common problem in matplotlib.
I see two options here:
Make the surface plot semitransparent, i.e. use alpha=.8 or similar.
Use plot instead of scatter.

Categories

Resources