0

I am trying to create a 3D surface plot similar to the one shown below. view of 3D surface plot with defined z-bounds and added contour lines I have been able to have some luck with the basic pyplot plot_surface function, but I have not found a way to cleanly clip the top of the surface, despite trying several different approaches suggested elsewhere online. The code to generate each of the plots I have tried is below, as well as a picture of the corresponding output. If it is important, I am also running all code below on a Jupyter notebook within PyCharm.

My first approach was to just use the basic plot_surface function. The specific surface z = f(x, y) that I am plotting rises very sharply (see the z-axis scale), but I am mainly trying to plot the region of the surface near the local minima, which is difficult to see in the graph.

import matplotlib
import matplotlib.pyplot as plt
import jax.numpy as jnp

def f(x, y, a, b):
    return x**4 + y**4 + y**3 - (4 * x**2 * y) + y**2 - (a * x) + (b * y)

X, Y = jnp.meshgrid(jnp.arange(-3, 3, 0.01), jnp.arange(-3, 3, 0.01))
Z = f(X, Y, 0, 0)  #Generate the z-values at each (x, y) for a single choice of parameters a and b

matplotlib.use("Qt5Agg")

fig, ax = plt.subplots(subplot_kw={"projection": "3d"}, figsize=(16, 12))

surf = ax.plot_surface(X, Y, Z, linewidth=0, antialiased=False, cmap="viridis")
ax.view_init(elev=10, azim=15, roll=0)

plt.tight_layout()
plt.show()

Output 1: (https://i.sstatic.net/6HWe3RaB.png)

My second approach was then to define the minimum and maximum z values that I wanted to visualize and pass the argument axlim_clip=True into the plotting function. This seems to have zoomed in on the region of interest, but apparently kept the previous color scaling (i.e. this region has virtually all the same color), and produced a jagged top edge where entire surface patches were removed.

import matplotlib
import matplotlib.pyplot as plt
import jax.numpy as jnp

def f(x, y, a, b):
    return x**4 + y**4 + y**3 - (4 * x**2 * y) + y**2 - (a * x) + (b * y)

X, Y = jnp.meshgrid(jnp.arange(-3, 3, 0.01), jnp.arange(-3, 3, 0.01))
Z = f(X, Y, 0, 0)  #Generate the z-values at each (x, y) for a single choice of parameters a and b
Zmin, Zmax = jnp.min(Z), jnp.percentile(Z, 15.)

matplotlib.use("Qt5Agg")

fig, ax = plt.subplots(subplot_kw={"projection": "3d"}, figsize=(16, 12))

surf = ax.plot_surface(X, Y, Z, linewidth=0, antialiased=False, cmap="viridis", axlim_clip=True)
ax.view_init(elev=10, azim=15, roll=0)
ax.set_zlim(Zmin, Zmax)

plt.tight_layout()
plt.show()

Output 2: (https://i.sstatic.net/rULdNyak.png)

My third approach was then to try a mask where I set all z-values above a defined threshold to NaN values. This seems to have fixed the color scaling issue, but it still has the jagged edge on the top of the surface instead of a clean clip.

import matplotlib
import matplotlib.pyplot as plt
import jax.numpy as jnp

def f(x, y, a, b):
    return x**4 + y**4 + y**3 - (4 * x**2 * y) + y**2 - (a * x) + (b * y)

X, Y = jnp.meshgrid(jnp.arange(-3, 3, 0.01), jnp.arange(-3, 3, 0.01))
Z = f(X, Y, 0, 0)  #Generate the z-values at each (x, y) for a single choice of parameters a and b

Zmax = jnp.percentile(Z, 15.)
Zplateau = jnp.minimum(Z, Zmax)
Zmasked = jnp.where(Z > Zmax, jnp.nan, Zplateau)



matplotlib.use("Qt5Agg")

fig, ax = plt.subplots(subplot_kw={"projection": "3d"}, figsize=(16, 12))

surf = ax.plot_surface(X, Y, Zmasked, linewidth=0, antialiased=False, cmap="viridis")
ax.view_init(elev=10, azim=15, roll=0)
ax.set_zlim(jnp.min(Z), Zmax)

plt.tight_layout()
plt.show()

Output 3: (https://i.sstatic.net/LR6MZFFd.png)

The last approach I have tried is similar to #3, but I set the z-values above the threshold equal to the threshold value instead of a NaN value. This makes the top of the graph clean, but creates an unwanted plateau at the top of the surface.

import matplotlib
import matplotlib.pyplot as plt
import jax.numpy as jnp


def f(x, y, a, b):
    return x ** 4 + y ** 4 + y ** 3 - (4 * x ** 2 * y) + y ** 2 - (a * x) + (b * y)


X, Y = jnp.meshgrid(jnp.arange(-3, 3, 0.01), jnp.arange(-3, 3, 0.01))
Z = f(X, Y, 0, 0)  #Generate the z-values at each (x, y) for a single choice of parameters a and b

Zmax = jnp.percentile(Z, 15.)
Zclipped = jnp.where(Z > Zmax, Zmax, Z)


matplotlib.use("Qt5Agg")

fig, ax = plt.subplots(subplot_kw={"projection": "3d"}, figsize=(16, 12))

surf = ax.plot_surface(X, Y, Zclipped, linewidth=0, antialiased=False, cmap="viridis")
contours = ax.contour(X, Y, Z, levels=[Zmax], zdir="z", offset=Zmax, colors="k", linewidths=2)
ax.view_init(elev=10, azim=15, roll=0)
ax.set_zlim(jnp.min(Z), Zmax)

plt.tight_layout()
plt.show()

Output 4: (https://i.sstatic.net/KxE9yvGy.png)

As some final notes, I have tried (for each approach) using a finer grid of xy-points to evaluate the surface over, as well as changing the rstride and cstride values with the plot_surface call. Both result in the surface of the graph below the top edge being smoother, but the top edge itself is still jagged. This is my first stackoverflow post, so thank you in advance for your help, and please let me know if there is anything else I can provide! :)

3
  • 2
    This post is very related. Commented Sep 13 at 7:33
  • Do you really need to use Matplotlib? Plotly allows to achieve this effect very very easily... Commented Sep 14 at 16:44
  • I am starting to look into Plotly as a possible alternative - I just wanted to see if there was a simple matplotlib fix that I wasn't familiar with. Do you know of any examples online that use Plotly to create this kind of plot? @Davide_sd Commented Sep 15 at 17:50

1 Answer 1

1

Plotly make it relatively simple to achieve this kind of visualizations. You can start by looking at the examples in this documentation page. If you want to change the colorscale, look at this page.

The core concepts are:

  • set the range property in the zaxis dictionary in order to create clipping planes at the desired levels.
  • set the contours dictionary when adding the surface.
import numpy as np
import plotly.graph_objects as go

def f(x, y, a, b):
    return x**4 + y**4 + y**3 - (4 * x**2 * y) + y**2 - (a * x) + (b * y)

z_max = 1.5
n = 200j
xx, yy = np.mgrid[-3:3:n, -3:3:n]
zz = f(xx, yy, 0, 0)

fig = go.Figure()
fig.add_surface(
    x=xx, y=yy, z=zz, 
    colorscale="Aggrnyl", 
    showscale=True,
    cmin=zz.min(), 
    cmax=z_max,
    contours=dict(
        z={"show": True, "start": yy.min(), "end": yy.max(), "size": 0.25, "width": 1, "project_z": True, "usecolormap": True}
    ), 
    colorbar={'len': 1, 'title': {'side': 'right', 'text': 'z'}},
    # this enables contour lines to be color mapped
    surfacecolor=zz
)
fig.update_layout(
    scene=dict(
        xaxis=dict(title="x"),
        yaxis=dict(title="y"),
        # the 'range' property allows to set z-limits
        zaxis=dict(title="z", range=(-1.5, z_max)),
        aspectratio=dict(x=1.5, y=1.5, z=1)
    )
)
fig.show()

enter image description here

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you so much @Davide_sd !! This was super helpful :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.