Skip to content

Note

Click here to download the full example code

Cortical Neurons#

This tutorial demonstrates how to plot cortical neurons.

In this exercise we will visualize morphological data from "Integrated Morphoelectric and Transcriptomic Classification of Cortical GABAergic Cells" by Gouwens, Sorensen et al., Cell (2020). Specifically, we will re-create a plot similar to their Figure 4A.

For brevity, we will use some fixed cell IDs and properties from the dataset. These were taken from the 20200711_patchseq_metadata_mouse.csv file provided alongside the supplementary material of the paper:

# The cell IDs we will use (it's the first 5 in the meta data file)
ids = [601506507, 601790961, 601803754, 601808698, 601810307]

# The normalized soma depths for these cells (also from the meta data file)
soma_depths = [0.36101451, 0.62182935, 0.16423996, 0.48303029, 0.2956563]

Part I: Loading and Aligning Neurons#

First we need to load the neurons. Here, we will take them straight from their FTP server but you can of course download them first and then load from disk!

import navis

nl = navis.read_swc(
    "ftp://download.brainlib.org:8811/biccn/zeng/pseq/morph/200526/",
    limit=[f"{i}_transformed.swc" for i in ids],  #  Load only the files we need
    fmt="{name,id:int}_transformed.swc",  # Parse the name and id from the file name
)

To make our lives a bit easier, we will attach the soma depth to the neurons as metadata:

nl.set_neuron_attributes(
    soma_depths,
    name="cell_soma_normalized_depth",
    register=True
    )

nl
<class 'navis.core.neuronlist.NeuronList'> containing 5 neurons (1.5MiB)
type name id n_nodes n_connectors n_branches n_leafs cable_length soma units created_at origin file cell_soma_normalized_depth
0 navis.TreeNeuron 601506507 601506507 3680 None 30 32 6012.066895 1 1 dimensionless 2024-10-24 11:23:54.325164 download.brainlib.org:8811/bil/data/biccn/zeng... 601506507_transformed.swc 0.361015
1 navis.TreeNeuron 601790961 601790961 12333 None 163 169 19280.503906 1 1 dimensionless 2024-10-24 11:23:54.532835 download.brainlib.org:8811/bil/data/biccn/zeng... 601790961_transformed.swc 0.621829
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
3 navis.TreeNeuron 601808698 601808698 13565 None 252 265 17969.759766 1 1 dimensionless 2024-10-24 11:23:54.997560 download.brainlib.org:8811/bil/data/biccn/zeng... 601808698_transformed.swc 0.483030
4 navis.TreeNeuron 601810307 601810307 13583 None 277 285 23013.343750 1 1 dimensionless 2024-10-24 11:23:55.232458 download.brainlib.org:8811/bil/data/biccn/zeng... 601810307_transformed.swc 0.295656

Next, we need to align the neurons according to their soma depth! The normalized cell_soma_normalized_depth should map to a physical range of 0 to 922.5861720311 microns.

Let's demo with one neuron before we run this for all neurons:

# Grab one of the neurons
n = nl[0]

# This is the normalized soma depth:
print(f"Normalized soma depth: {n.cell_soma_normalized_depth}")

Out:

Normalized soma depth: 0.36101451

The physical soma depth is simply the normalized depth multiplied by the total depth of the cortex. Note that we're positioning from the bottom - i.e. 922.586 will be at the surface and 0 at the bottom! This is to make our lifes easier when it comes to plotting since the origin in matplotlib figures is in the bottom left corner.

phys_y = (1 - n.cell_soma_normalized_depth) * 922.5861720311
print(f"Physical soma depth: {phys_y}")

# Current soma
print(f"Current soma coordinates: {n.soma_pos[0]}")

Out:

Physical soma depth: 589.5191772025167
Current soma coordinates: [382.803   433.69104 118.07384]

We will now offset the neuron such that the soma is at (0, 589.519, 0):

offset = [0, phys_y, 0] - n.soma_pos[0]
offset

Out:

array([-382.80300903,  155.82813716, -118.07383728])

Moving or scaling neurons in NAVis is super straight forward: adding, subtracting, dividing or multiplying neurons by a number or an [x, y, z] vector will change their coordinates:

# Move the neuron to the new centered position
n += offset

# Check the that the soma is now in the correct position
n.soma_pos[0]

Out:

array([  0.       , 589.5191772,   0.       ])

That looks good! Let's do it for all neurons:

for n in nl:
    phys_y = (1 - n.cell_soma_normalized_depth) * 922.5861720311
    offset = [0, phys_y, 0] - n.soma_pos[0]
    n += offset

Check that all soma positions are correct:

nl.soma_pos.reshape(-1, 3)

Out:

array([[  0.        , 589.5191772 ,   0.        ],
       [  0.        , 348.89501236,   0.        ],
       [  0.        , 771.06065604,   0.        ],
       [  0.        , 476.9491058 ,   0.        ],
       [  0.        , 649.81775798,   0.        ]])

Part II: Plotting#

Now that we have loaded and aligned the neurons, let's recreate a plot similar to those in Figure 4A:

def plot_neurons(to_plot, color="purple", axon_color="magenta", offset=500):
    """Plot all neurons of a given transcriptomic type.

    Parameters
    ----------
    neurons : NeuronList
        The aligned neurons to plot.
    color : str
        The color of the dendrites.
    axon_color : str
        The color of the axon.
    offset : int
        The offset between neurons along the x-axis.

    Returns
    -------
    fig, ax
        The matplotlib figure and axis.

    """
    # Offset the neurons along the x-axis so that they don't overlap
    to_plot = [n + [offset * i, 0, 0] for i, n in enumerate(to_plot)]

    # The SWC files for this dataset include a `label` column which
    # indicates the compartment type:
    # 1 = soma
    # 2 = axon
    # 3 = dendrites
    # We will use this `label` to color the neurons' compartments.

    # Here we define a color palette for the compartments:
    compartment_palette = {1: color, 2: axon_color, 3: color}

    # Plot the neuron
    fig, ax = navis.plot2d(
        to_plot,
        radius=False,
        lw=1.5,
        soma=dict(
            fc="black",  # soma fill color
            ec="white",  # highlight the soma with a white outline
            radius=10,   # override the default soma radius
        ),
        color_by="label",  # color by `label` column in node table
        palette=compartment_palette,
        figsize=(
            len(to_plot) * 2,
            10,
        ),  # scale the figure size with the number of neurons
        method="2d",
    )

    # Add the layer boundaries (top bound for each layer in microns)
    layer_bounds = {
        "L1": 0,
        "L2/3": 115.1112491335,
        "L4": 333.4658190171,
        "L5": 453.6227158132,
        "L6": 687.6482650269,
        "L6b": 883.1308910545,
    }

    for layer, y in layer_bounds.items():
        y = 922.5861720311 - y  # flip the y-axis
        # Add a dashed line
        ax.axhline(y, color="gray", ls="--", lw=1)
        # Add the layer name
        ax.text(-300, y - 25, layer, color="gray", va="center", size=10)
    # Add the bottom bound
    ax.axhline(0, color="gray", ls="--", lw=1)

    # Set the axis y limits according to the layers
    ax.set_ylim(-10, 930)

    # Hide axes
    ax.axis("off")

    return fig, ax


fig, ax = plot_neurons(nl)

tutorial plotting 06 cortex

That looks close enough. The last bit is to add the little KDE plots for the depth-distribution of cable length!

We're going to be cheap here and simply generate a histogram over the node positions. To make this representative, we should make sure that the number of nodes per unit of cable is homogeneous across neurons. For that we will resample the neurons:

print(
    f"Sampling rate (nodes per micron of cable) before resampling: {nl.sampling_resolution.mean():.2f}"
)

# Resample to 2 nodes per micron
resampled = navis.resample_skeleton(
    nl,
    resample_to=0.5,
    map_columns="label",  # make sure label column is carried over
)

print(
    f"Sampling rate (nodes per micron of cable) after resampling: {resampled.sampling_resolution.mean():.2f}"
)

Out:

Sampling rate (nodes per micron of cable) before resampling: 1.57
Sampling rate (nodes per micron of cable) after resampling: 0.50

Get the combined nodes table:

nodes = resampled.nodes
nodes.head()
label node_id parent_id radius type x y z neuron
0 3 23 3681 0.360400 branch -5.994568 614.076123 19.583313 601506507
1 3 3681 3682 0.373750 slab -5.870639 613.586572 19.553024 601506507
2 3 3682 3683 0.387101 slab -5.746710 613.097021 19.522736 601506507
3 3 3683 3684 0.400267 slab -5.525498 612.648052 19.540078 601506507
4 3 3684 3685 0.413362 slab -5.267001 612.214638 19.575677 601506507

Now we can plot the distribution of cable lengths for our neurons:

import seaborn as sns
from mpl_toolkits.axes_grid1 import make_axes_locatable

# Plot the neurons again, re-using the function we defined above
fig, ax = plot_neurons(nl)

# Add a new axis to the right of the main plot
divider = make_axes_locatable(ax)
ax_hist = divider.append_axes("right", size=0.75, pad=0.05)

# Add histograms
# For axon:
sns.kdeplot(
    data=nodes[nodes.label == 2], y="y", ax=ax_hist, color="magenta", linewidth=1.5
)
# For the rest:
sns.kdeplot(
    data=nodes[nodes.label != 2], y="y", ax=ax_hist, color="purple", linewidth=1.5
)

# Add soma positions
soma_pos = nl.soma_pos.reshape(-1, 3)
ax_hist.scatter([0] * len(soma_pos), soma_pos[:, 1], color="black", s=10, clip_on=False)

# Set same axis limits as the main plot
ax_hist.set_ylim(-10, 930)

# Hide axes
ax_hist.set_axis_off()

tutorial plotting 06 cortex

Acknowledgements#

We thank Staci Sorensen and Casey Schneider-Mizell from the Allen Institute for Brain Science for helping with extra information and data for this tutorial!

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

Download Python source code: tutorial_plotting_06_cortex.py

Download Jupyter notebook: tutorial_plotting_06_cortex.ipynb

Gallery generated by mkdocs-gallery