Skip to content

Note

Click here to download the full example code

Meshes#

Shade, outline and stack mesh neurons.

The mesh counterpart to the skeleton tutorial. If you have not read the plotting overview yet, start there; for colors and palettes see coloring.

As with skeletons we focus on plot2d with method="2d": matplotlib gives us a flat canvas we can project onto ourselves, which turns out to be both faster and better looking than handing the geometry to matplotlib's own 3D machinery.

Parameter Effect Works in plot3d?
mesh_shade Light the surface: True, "cel", "rim" or "ghost"
alpha Opacity - correct now even where the surface folds over itself
color_by Per-vertex colours; shading multiplies into them rather than replacing
halo Background-coloured outline, so crossings read as one neuron in front
depth_sort Bin mesh faces with the skeletons, or "global" to sort them exactly
style A named bundle of the above, e.g. "publication"

Neuropils are meshes too, but they behave differently enough to get their own tutorial.

Occlusion comes for free#

There is nothing to switch on here - it is worth knowing about because it changed. Faces pointing away from the viewer are dropped and the rest painted furthest-first, so a mesh now has a front and a back. Before, faces went down in whatever order the mesh happened to store them.

Culling roughly halves the polygons that reach the renderer, so this is also faster than what it replaced.

import navis
import matplotlib.pyplot as plt

m = navis.example_neurons(1, kind="mesh")

fig, ax = navis.plot2d(m, view=("x", "-z"), method="2d")
plt.tight_layout()

tutorial plotting 2d 01 meshes

Shading#

mesh_shade lights the surface. At whole-cell zoom a neurite is only a pixel or two wide, so this mostly shows on the soma and the thick primary neurite - zoom into an arbour and it does a lot more:

fig, axes = plt.subplots(2, 3, figsize=(13, 10))
for ax, shade in zip(axes.flat, [False, True, "cel", "rim", "ghost"]):
    navis.plot2d(m, ax=ax, method="2d", view=("x", "-z"), mesh_shade=shade)
    ax.set_title(f"mesh_shade={shade!r}", fontsize=11, y=0.98)
for ax in axes.flat:
    ax.set_axis_off()  # also blanks the unused sixth panel
plt.tight_layout()

mesh_shade=False, mesh_shade=True, mesh_shade='cel', mesh_shade='rim', mesh_shade='ghost'

Mode What it does
True Plain diffuse (Lambertian) shading. The one to reach for
"cel" The same, posterised into three tones - diagrammatic, and the smallest vector output
"rim" Diffuse plus a bright rim at grazing angles, which separates touching tubes
"ghost" Opacity from the grazing angle instead of brightness - see volumes

Pass a dict to tune it. light is a direction in view space (x right, y up, z towards you), so it follows the camera rather than the data; ambient sets how much light a face gets when it points straight away from the key light:

fig, axes = plt.subplots(1, 3, figsize=(13, 5))
for ax, (title, shade) in zip(
    axes,
    [
        ("default", True),
        ("ambient=0.45", {"mode": "lambert", "ambient": 0.45}),
        ("light from the right", {"mode": "lambert", "light": (1, 0.2, 0.6)}),
    ],
):
    navis.plot2d(m, ax=ax, method="2d", view=("x", "-z"), mesh_shade=shade)
    ax.set_title(title, fontsize=11, y=0.98)
    ax.set_axis_off()
plt.tight_layout()

default, ambient=0.45, light from the right

mesh_shade in 3D

plot3d and method="3d" understand mesh_shade as a plain on/off switch - the mode names are specific to method="2d", which is where we do the projection ourselves. For real lighting controls see 3D meshes.

Shading and colours#

Shading multiplies into whatever colour a face already has instead of replacing it, so it composes with everything else - color, color_by and depth_coloring alike:

fig, axes = plt.subplots(1, 2, figsize=(9, 5))
for ax, shade in zip(axes, [False, True]):
    navis.plot2d(
        m,
        ax=ax,
        method="2d",
        view=("x", "-z"),
        color_by=m.vertices[:, 1],  # colour by depth into the screen
        palette="magma",
        mesh_shade=shade,
    )
    ax.set_title(f"color_by + mesh_shade={shade}", fontsize=11, y=0.98)
    ax.set_axis_off()
plt.tight_layout()

color_by + mesh_shade=False, color_by + mesh_shade=True

Transparency#

A mesh in one colour is filled as a single path, which means alpha finally means what it says. Previously every triangle was composited on its own, so a fold in the surface came out darker than a flat stretch and the neuron looked solid where it was merely doubled over:

fig, ax = navis.plot2d(m, view=("x", "-z"), method="2d", alpha=0.4)

tutorial plotting 2d 01 meshes

Shading gives that up again

Shading needs a colour per face, which rules out the single-path fill. Where a translucent mesh overlaps itself the shaded version double-darkens instead of filling once. If that matters more to you than the lighting does, pass mesh_shade=False.

Halos#

Meshes take the same halo as skeletons: an outline in the background colour, drawn underneath, so a crossing reads as one neuron passing in front of another.

nl = navis.example_neurons(3, kind="mesh")

fig, axes = plt.subplots(1, 3, figsize=(15, 5.5))
for ax, kwargs in zip(
    axes, [dict(halo=False), dict(halo=3), dict(halo=3, depth_sort=True)]
):
    navis.plot2d(nl, ax=ax, method="2d", view=("x", "-z"), **kwargs)
    ax.set_title(", ".join(f"{k}={v!r}" for k, v in kwargs.items()), fontsize=11, y=0.98)
    ax.set_axis_off()
plt.tight_layout()

halo=False, halo=3, halo=3, depth_sort=True

As with skeletons, the halo only marks what is already in front: without depth_sort that is simply the last mesh in the input, so the third neuron carves a channel through the other two.

The right panel is where meshes part company with skeletons. Binning slices each mesh's outline into depth layers, and every layer strokes its own halo - so a neurite that crosses a bin boundary gets nicked by its own halo, and the primary neurite comes out looking dashed. For meshes, halo alone or depth_sort alone usually reads better than the two together; if you do need both, fewer bins means fewer seams.

Halos are slow on dense arbours

The outline is the neuron's own path stroked underneath its fill, and that path has one subpath per visible triangle. It rasterises slowly. Pair it with rasterize=True for big scenes.

Keylines instead of halos

The halo is drawn by stroking the mesh's own outline underneath its fill, so passing a thin dark colour instead of the background gives you an ink-on-paper keyline:

navis.plot2d(m, halo={"width": 1, "color": "#2a3f7a"}, mesh_shade=True)
You get one or the other, though - there is only one stroke.

Depth sorting#

By default a mesh is one artist drawn over the skeletons, whatever its actual depth. depth_sort puts its faces into the same stack skeleton nodes go into - either binned, or sorted exactly:

fig, axes = plt.subplots(1, 3, figsize=(15, 5.5))
for ax, ds in zip(axes, [False, 20, "global"]):
    navis.plot2d(nl, ax=ax, method="2d", view=("x", "-z"), depth_sort=ds)
    ax.set_title(f"depth_sort={ds!r}", fontsize=11, y=0.98)
    ax.set_axis_off()
plt.tight_layout()

depth_sort=False, depth_sort=20, depth_sort='global'

The difference shows where neurons pile up - in the glomerulus above, depth_sort is what stops the last neuron drawn from owning the whole thing. Bins are an approximation: faces in the same bin still fall back to draw order, whereas "global" merges every mesh into one artist and sorts all their faces together, so two neurons interleave face by face.

"global" is the expensive mode for meshes

A flat mesh is normally a single filled outline, and sorting across neurons forces it down to one polygon per face - on the example neurons that is several times the cost of either the default or the bins, and it gives up the fill-once alpha along with it. It is a finished-figure setting, not an exploration one. Skeletons get off far more lightly; see the skeleton tutorial.

All of the above at once#

style="publication" bundles the settings you would otherwise reach for one by one - for meshes that is mesh_shade and depth_sort (plus radius and soma, which only skeletons care about):

fig, axes = plt.subplots(1, 2, figsize=(11, 5.5))
for ax, style in zip(axes, [None, "publication"]):
    navis.plot2d(nl, ax=ax, method="2d", view=("x", "-z"), style=style)
    ax.set_title(f"style={style!r}", fontsize=11, y=0.98)
    ax.set_axis_off()
plt.tight_layout()

style=None, style='publication'

A style only fills in what you did not pass yourself, so you can still opt out of any part of it - style="publication", mesh_shade=False gets you the depth sorting and a flat fill.

Total running time of the script: ( 0 minutes 5.465 seconds)

Download Python source code: tutorial_plotting_2d_01_meshes.py

Download Jupyter notebook: tutorial_plotting_2d_01_meshes.ipynb

Gallery generated by mkdocs-gallery