Human and Mammalian Brain Atlas (HMBA) - Marmoset Subcortex Xenium Spatial Transcriptomics: Slab coordinates and annotation#

The Marmoset Subcortical Cell Atlas (MSCA), is a multiomic atlas of the common marmoset (Callithrix jacchus) subcortex that integrates single nucleus transcriptomics, chromatin accessibility, and spatial transcriptomics. Part of the Human and Mammalian Brain Atlas (HMBA) effort within the NIH’s BRAIN Initiative® Cell Atlas Network (BICAN), this dataset comprehensively samples key subcortical structures in the non-human primate, including the hippocampal formation, amygdala, olfactory areas, basal forebrain, thalamus, hypothalamus, midbrain, and major hindbrain structures (excluding cerebellar cortex).

This cellular-resolution Xenium spatial transcriptomic dataset includes 5.6 million cells profiled from 47 coronal hemisections from a single adult male marmoset donor, sampling the rostral-caudal extent of subcortical structures. The spatial cells were mapped to the 1,737 snRNA-seq Clusters defined in the MSCA reference cell type taxonomy, and the resulting spatial localization of the Clusters provided anatomical anchoring and nomenclature to the Group and Class hierarchy levels of the MSCA taxonomy.

For more information on this dataset and the associated 10X Multiome data, please refer to the publication: Dan, Turner, DeBerardine, et al. (2026). Spatial and regulatory organization of cell identity across the primate subcortex. Cell. https://doi.org/10.1016/j.cell.2026.10.007.


In this notebook, we walk through the data available in this release and show how they are joined together to enable analyses at the cell level.

You need to be connected to the internet to run this notebook and have run through the getting started notebook.

import json
import geojson
from typing import List, Tuple, Optional

import pandas as pd
from pathlib import Path
import numpy as np
import matplotlib.pyplot as plt

from abc_atlas_access.abc_atlas_cache.abc_project_cache import AbcProjectCache

We will interact with the data using the AbcProjectCache. This cache object tracks which data has been downloaded and serves the path to the requested data on disk. For metadata, the cache can also directly serve up a Pandas DataFrame. See the getting_started notebook for more details on using the cache including installing it if it has not already been.

Change the download_base variable to where you would like to download the data in your system.

download_base = Path('../../data/abc_atlas')
abc_cache = AbcProjectCache.from_cache_dir(
    cache_dir=download_base,
)

abc_cache.current_manifest
/Users/chris.morrison/src/abc_atlas_access/src/abc_atlas_access/abc_atlas_cache/cloud_cache.py:665: UserWarning: The manifest version recorded as last used for this cache -- releases/20261130/manifest.json-- is not a valid manifest for this dataset. Loading latest version -- releases/20261101/manifest.json -- instead.
  warnings.warn(msg, UserWarning)
/Users/chris.morrison/src/abc_atlas_access/src/abc_atlas_access/abc_atlas_cache/cloud_cache.py:697: OutdatedManifestWarning: You are loading
releases/20261101/manifest.json
which is newer than the most recent manifest file you have previously been working with
releases/20261130/manifest.json
It is possible that some data files have changed between these two data releases, which will force you to re-download those data files (currently downloaded files will not be overwritten). To continue using releases/20261130/manifest.json, run
type.load_manifest('releases/20261130/manifest.json')
  warnings.warn(msg, OutdatedManifestWarning)
'releases/20261130/manifest.json'

Data Overview#

Below we list the available metadata and expression matrix files available. The data are split across a set of metadata tables and h5ad files. The three h5ad files contain the raw counts, batch-normalized counts, and a natural log transformation of the batch-normalized counts.

for dataset_name in ['HMBA-Subcortex-Xenium-Marmoset']:
    print(f"Dataset: {dataset_name}")
    metadata_files = abc_cache.list_metadata_files(dataset_name)
    print(f"Metadata files: {metadata_files}")
    expression_matrix_files = abc_cache.list_expression_matrix_files(dataset_name)
    print(f"Expression matrix files: {expression_matrix_files}\n")
Dataset: HMBA-Subcortex-Xenium-Marmoset
Metadata files: ['additional_cell_metadata', 'ccf_coordinates', 'cell_anatomical_annotations', 'cell_metadata', 'cell_to_cluster_membership', 'cirro_spatial_grid', 'donor', 'donor_coordinates', 'example_gene_expression', 'gene', 'rotated_coordinates', 'slab_plane_coordinates', 'specimen_metadata', 'value_sets']
Expression matrix files: ['Xenium-CJ23.56.004-Subcortex-batch-normalized/ln', 'Xenium-CJ23.56.004-Subcortex-batch-normalized/raw', 'Xenium-CJ23.56.004-Subcortex/raw']

Cell metadata#

The Marmoset Subcortext spatial dataset has it’s own h5ad files and associated cell metadata tables. These metadata contain the basic identifiers for each cell and forms the basis by which we will join all other cell level and associated data.

cell_metadata = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='cell_metadata',
    dtype={"cell_label": str},
).set_index('cell_label')
cell_metadata.head()
cell_metadata.csv: 100%|██████████| 1.64G/1.64G [04:20<00:00, 6.28MMB/s]   
brain_section_label brain_section_barcode x_experiment y_experiment segmentation_job_id segmentation_method cell_area nucleus_area n_nuclei_in_cell total_counts total_counts_genes total_counts_genes_batch_normalized n_unique_genes qc_pass abc_atlas_visualization dataset_label feature_matrix_label
cell_label
1382042951_aaondhbe-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4459.1006 4524.2420 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 89.81579 60.735157 1 339 338 257.866893 139 True True HMBA-Subcortex-Xenium-Marmoset Xenium-CJ23.56.004-Subcortex-batch-normalized
1382042951_aaoneabm-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4668.6323 4653.2130 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 105.98172 74.327190 1 762 762 581.344889 154 True True HMBA-Subcortex-Xenium-Marmoset Xenium-CJ23.56.004-Subcortex-batch-normalized
1382042951_aaonekin-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4593.0610 4594.6304 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 129.50813 74.598130 1 406 406 309.745440 138 True True HMBA-Subcortex-Xenium-Marmoset Xenium-CJ23.56.004-Subcortex-batch-normalized
1382042951_aaoofhco-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4529.3900 4348.2266 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 136.37189 72.475784 1 948 948 723.247972 166 True True HMBA-Subcortex-Xenium-Marmoset Xenium-CJ23.56.004-Subcortex-batch-normalized
1382042951_aaopgffc-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4524.6533 4382.6323 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 129.41782 78.391250 1 941 940 717.144614 162 True True HMBA-Subcortex-Xenium-Marmoset Xenium-CJ23.56.004-Subcortex-batch-normalized

Specimen metadata#

In order to associate each cell to the associated specimen in comes from, we load the specimen table. This provides a mapping for each cell to the tissue/brain section it originates from as well as orders for how the sections are laid out in the brain.

specimen = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='specimen_metadata',
).set_index('brain_section_label')
specimen.head()
specimen_metadata.csv: 100%|██████████| 10.8k/10.8k [00:00<00:00, 80.7kMB/s]
brain_section_barcode brain_section_thickness brain_section_plane_of_section brain_section_ordinal brain_section_depth section_set_label section_set_ordinal section_set_base_ordinal brain_block_label brain_block_number ... brain_slab_ordinal brain_slab_thickness hemisphere brain_division_label brain_division_ordinal donor_label slab_plane_ordinal slab_plane_label slab_plane_order slab_z_order
brain_section_label
CJ23.56.004.CX.42.01.06 1382042951 10 coronal (rostral to caudal) 7 70 CJ23.56.004.CX.42.01 1 1 CJ23.56.004.CX.42 42 ... 42 5.0 right CJ23.56.004.CX 1 CJ23.56.004 1 CJ23.56.004.CX.42.Plane.01 14201 1
CJ23.56.004.CX.42.02.06 1382042748 10 coronal (rostral to caudal) 27 270 CJ23.56.004.CX.42.02 2 21 CJ23.56.004.CX.42 42 ... 42 5.0 right CJ23.56.004.CX 1 CJ23.56.004 2 CJ23.56.004.CX.42.Plane.02 14202 2
CJ23.56.004.CX.42.03.07 1382042618 10 coronal (rostral to caudal) 48 480 CJ23.56.004.CX.42.03 3 41 CJ23.56.004.CX.42 42 ... 42 5.0 right CJ23.56.004.CX 1 CJ23.56.004 3 CJ23.56.004.CX.42.Plane.03 14203 3
CJ23.56.004.CX.42.04.06 1382042399 10 coronal (rostral to caudal) 67 670 CJ23.56.004.CX.42.04 4 61 CJ23.56.004.CX.42 42 ... 42 5.0 right CJ23.56.004.CX 1 CJ23.56.004 4 CJ23.56.004.CX.42.Plane.04 14204 4
CJ23.56.004.CX.42.05.06 1382042114 10 coronal (rostral to caudal) 87 870 CJ23.56.004.CX.42.05 5 81 CJ23.56.004.CX.42 42 ... 42 5.0 right CJ23.56.004.CX 1 CJ23.56.004 5 CJ23.56.004.CX.42.Plane.05 14205 5

5 rows × 21 columns

Join the specimen tables with the cell metadata.

cell_metadata = cell_metadata.join(specimen, on='brain_section_label', rsuffix='_specimen')

Spatial coordinates#

We load spatial coordinates for all cells. Each cell comes from a brain section that is a 10 µm slice from a coronal slab of a hemisected brain.

In this tutorial, we use the rotated coordinate system. These are a transformation of [x, y] using a manual midline annotation to correctly orient each section (dorsal up; midline left); in millimeters (mm). These coordinates are for visualization purposes only and line up with the polygons provided with this dataset.

Additional coordinate systems are available, including slab plane coordinates (where each hemisection is oriented & aligned into its tissue slab in 2D), donor coordinates (registered to the donor’s 3D MRI space), and ccf coordinates (registered to 3D Common Coordinate Framework (CCF) MRI average template space). See Metadata Descriptions for details.

cell_rotated_coordinates = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='rotated_coordinates',
    dtype={"cell_label": str},
).set_index('cell_label')
cell_rotated_coordinates.head()
rotated_coordinates.csv: 100%|██████████| 392M/392M [00:58<00:00, 6.64MMB/s]    
x_rotated_mm y_rotated_mm x_rotated_centered_mm y_rotated_centered_mm
cell_label
1382042951_aaondhbe-1-20250227 3.651601 -5.898891 8.477476 10.492991
1382042951_aaoneabm-1-20250227 3.508436 -6.098991 8.334310 10.292891
1382042951_aaonekin-1-20250227 3.555394 -6.015698 8.381268 10.376184
1382042951_aaoofhco-1-20250227 3.518584 -5.763877 8.344459 10.628005
1382042951_aaopgffc-1-20250227 3.536286 -5.793757 8.362161 10.598125

Below we join the rotated coordinates into the cell metadata.

cell_metadata = cell_metadata.join(
    cell_rotated_coordinates[['x_rotated_mm', 'y_rotated_mm']], how='inner'
)

# clean up memory
del cell_rotated_coordinates

Geojson polygons#

The dataset includes polygons outlining the hemisphere boundary of each brain section, which can be plotted over rotated coordinates to allow us to orient the subcortical cells in their full hemisection contex. These polygons are provided in geojson format, and here we use the associated geojson python package to load the jsons for use in our visualization.

Additional, manually drawn polygons outlining select regions of the Basal Ganglia (STR, GP, GPe, GPi, VeP, STH, and SN), as well as polygons identifying the cells that are also part of the HMBA-BG dataset (basal_ganglia_and_adjacent), are included as part of the HMBA-BG-Xenium-Marmoset dataset and are described and can be accessed in that dataset’s equivalent tutorial notebook.

First, we load the term set for these data. All cells in this HMBA-Subcortex-Xenium-Marmoset dataset receive the term set label “Subcortical”.

value_sets = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='value_sets',
).set_index('label')
parcellation_terms = value_sets[value_sets['field'] == 'parcellation_term_symbol']
parcellation_terms
value_sets.csv: 100%|██████████| 4.85k/4.85k [00:00<00:00, 68.2kMB/s]
table field description order external_identifier parent_label color_hex_triplet
label
Hemisphere cell_anatomical_annotations parcellation_term_symbol Brain Hemisphere 1 hemisphere NaN #000000
Subcortical cell_anatomical_annotations parcellation_term_symbol Subcortical regions 2 subcortical Hemisphere #008080
Basal ganglia and adjacent cell_anatomical_annotations parcellation_term_symbol Basal ganglia and adjacent 3 basal_ganglia_and_adjacent Subcortical #BEBEBE
STR cell_anatomical_annotations parcellation_term_symbol striatum 4 HOMBA:10333 Basal ganglia and adjacent #cd86cc
GP cell_anatomical_annotations parcellation_term_symbol globus pallidus 5 HOMBA:10342 Basal ganglia and adjacent #7AE361
GPe cell_anatomical_annotations parcellation_term_symbol external segment of globus pallidus 6 HOMBA:10343 Basal ganglia and adjacent #53DB33
GPi cell_anatomical_annotations parcellation_term_symbol internal segment of globus pallidus 7 HOMBA:10344 Basal ganglia and adjacent #A7DB33
VeP cell_anatomical_annotations parcellation_term_symbol ventral pallidus 8 HOMBA:10345 Basal ganglia and adjacent #9b19f5
STH cell_anatomical_annotations parcellation_term_symbol subthalamic nucleus 9 HOMBA:10466 Basal ganglia and adjacent #907991
SN cell_anatomical_annotations parcellation_term_symbol substantia nigra 10 HOMBA:12251 Basal ganglia and adjacent #89522A

We define two functions that will be used in our plotting of the geojsons. The first overplots the geojson over the specified plot, handling the different kinds of geojson shapes/polygons. The second loads the data based on the appropriate slab provided.

def plot_geo_data(geo_data: dict, ax: plt.Axes, terms=parcellation_terms):
    """Plot a geojson polygon over the input axis.

    Parameters
    ----------
    geo_data: dict
        Geojson formatted dictionary containing Polygons to plot.
    ax: matplotlib.pyplot.axis
        Axis object to plot the geojson data over.
    terms: pandas.DataFrame
        Term list containing the colors and orders for the parcellation, brain region terms. 
    """
    if geo_data is None:
        return None
    # If the geo_data is a FeatureCollection, we need to iterate over its
    # features and plot each one.
    if geo_data['type'] == 'FeatureCollection':
        for sub_data in geo_data['features']:
            plot_geo_data(sub_data, ax)
    elif geo_data['type'] == 'Feature':
        geometry_data = geo_data['geometry']
        name = geo_data['properties']['label']
        # color = terms.loc[name, 'color_hex_triplet'] # Get the color for this object from the term list.
        color = 'black' # Default color for the polygons.
        # If the geometry is a GeometryCollection, we need to iterate over its
        # geometries that make up the complete feature.
        if geometry_data['type'] == 'GeometryCollection':
            for idx, geometry in enumerate(geometry_data['geometries']):
                coords = np.array(list(geojson.coords(geometry)))
                # Only label the first geometry in the collection. Others
                # will be plotted without a label to avoid duplicate legend entries.
                if idx == 0:
                    ax.fill(coords[:, 0],
                            coords[:, 1],
                            edgecolor=color,
                            fill=False,
                            linewidth=3,
                            label=name)
                else:
                    ax.fill(coords[:, 0],
                            coords[:, 1],
                            edgecolor=color,
                            fill=False,
                            linewidth=3)
        else:
            coords = np.array(list(geojson.coords(geometry_data)))
            try:
                ax.fill(coords[:, 0],
                        coords[:, 1],
                        edgecolor=color,
                        fill=False,
                        linewidth=3,
                        label=name)
            except ValueError as e:
                import pdb; pdb.set_trace()
    else:
        print("unknown data", geo_data)

def get_polygons(slab_name: str) -> dict:
    """Load the geojson polygons for the specified slab and species.

    Parameters
    ----------
    slab_name: str
        Name of the slab to load.

    Returns
    -------
    dict
        Geojson formatted dictionary containing the polygons for the specified slab and species.
    """

    base_dir = 'HMBA-Subcortex-Xenium-Marmoset'

    file_path = abc_cache.get_file_path(
        directory=base_dir,
        file_name=slab_name,
    )
    if not file_path.exists():
        return None

    with open(file_path) as jfile:
        data = json.load(jfile)
    
    return data

Manual anatomical annotations#

We’ll plot the cells using the rotated coordinates and overlay the hemisphere polygons described above. As with the HMBA-BG spatial dataset, the columns denote which manual anatomical annotations the cell could be considered a member of. All cells included in the HMBA-Subcortex-Xenium-Marmoset dataset are labeled as “Subcortical” with no further refinement. However, a subset of the cells are also present in the HMBA-BG dataset and thus labeled here as “Basal ganglia and adjacent”.

cell_anatomical_annotations = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-Xenium-Marmoset',
    file_name='cell_anatomical_annotations'
).set_index('cell_label')
cell_anatomical_annotations.head()
cell_anatomical_annotations.csv: 100%|██████████| 320M/320M [00:33<00:00, 9.69MMB/s]    
Basal ganglia and adjacent Subcortical parcellation_term_symbol
cell_label
1382042951_aaondhbe-1-20250227 False True Subcortical
1382042951_aaoneabm-1-20250227 False True Subcortical
1382042951_aaonekin-1-20250227 False True Subcortical
1382042951_aaoofhco-1-20250227 False True Subcortical
1382042951_aaopgffc-1-20250227 False True Subcortical

Below, we define a function to extract color and order information from the value_set table for each anatomical parcellation term. This will allow us to plot the color of the anatomical region we assigned each cell to using the manually drawn, geojson polygons.

def extract_value_set(
    cell_metadata_df: pd.DataFrame,
    input_value_set: pd.DataFrame,
    input_value_set_label: str,
    value_set_label: str=None
):
    """Add color and order columns to the cell metadata dataframe based on the input
    value set.

    Columns are added as {input_value_set_label}_color and {input_value_set_label}_order.

    Parameters
    ----------
    cell_metadata_df : pd.DataFrame
        DataFrame containing cell metadata.
    input_value_set : pd.DataFrame
        DataFrame containing the value set information.
    input_value_set_label : str
        The the column name to extract color and order information for. will be added to the cell metadata.
    """
    if value_set_label is None:
        value_set_label = input_value_set_label
    cell_metadata_df[f'{input_value_set_label}_color'] = input_value_set[
        input_value_set['field'] == value_set_label
    ].loc[cell_metadata_df[input_value_set_label]]['color_hex_triplet'].values
    cell_metadata_df[f'{input_value_set_label}_order'] = input_value_set[
        input_value_set['field'] == value_set_label
    ].loc[cell_metadata_df[input_value_set_label]]['order'].values

Merge the manual annotation information into the cell meatadata tables and add in the color and ordering information.

cell_metadata = cell_metadata.join(
    cell_anatomical_annotations[['parcellation_term_symbol']]
)
extract_value_set(cell_metadata, parcellation_terms, 'parcellation_term_symbol')

# clean up memory
del cell_anatomical_annotations

Plotting the Slabs#

We define a helper function plot_slabs to visualize the cells for a specified set of slabs either by colorized metadata or gene expression. The code also allows for plotting the annotation polygons over the data if requested. This function can be used to plot individual frames as well.

def plot_slabs(
    df: pd.DataFrame,
    feature: str,
    slab_list: List[str],
    cmap: Optional[plt.Colormap] = None,
    fig_width: float = 40,
    fig_height: float = 8,
    plot_polygons: bool = False,
    point_size: float = 1.0,
    fix_bounds: bool = False,
    labels: str = None,
    term_orders: str = None,
) -> Tuple[plt.Figure, np.ndarray]:
    """Plot spatial transcriptomics data.

    Parameters
    ----------
    df: pandas.DataFrame
        cell metadata data frame with x,y locations to plot.
    feature: str
        Expression value or color column to plot.
    slab_list: List[str]
        List of slab_plane ids to plot.
    cmap: matplotlib.colors.Colormap
        Colormap for expression features.
    fig_width: int
        Width of the plotted figure.
    fig_height: int
        Height of the plotted figure.
    plot_polygons: bool
        Overplot geojson polygons. Note that these are note available for human
        and this should be set to False.
    point_size: float
        Size of the points to plot.
    fix_bounds: bool
        If True, fix the x and y bounds across all slabs to be the same.
    labels: str
        Column name for labels to use in the legend.
    term_orders: str
        Column name for term orders to sort the legend entries.
    """
    if isinstance(slab_list, str):
        slab_list = [slab_list]

    fig, ax = plt.subplots(1, len(slab_list))
    fig.set_size_inches(fig_width, fig_height)

    if len(slab_list) == 1:
        ax = [ax]

    if 'x' in df.columns:
        x_col = 'x'
    elif 'x_rotated_mm' in df.columns:
        x_col = 'x_rotated_mm'
    if 'y' in df.columns:
        y_col = 'y'
    elif 'y_rotated_mm' in df.columns:
        y_col = 'y_rotated_mm'

    if fix_bounds:
        x_min = df[x_col].min() * 0.8
        x_max = df[x_col].max() * 1.2
        y_min = df[y_col].min() * 0.8
        y_max = df[y_col].max() * 1.2

    for idx, slab in enumerate(slab_list):

        if 'slab_plane_label' in df.columns:
            filtered = df[df['slab_plane_label'] == slab]
        elif 'brain_section_label' in df.columns:
            filtered = df[df['brain_section_label'] == slab]
        
        xx = filtered[x_col]
        yy = filtered[y_col]
        vv = filtered[feature]
        if labels is not None:
            ll = filtered[labels]
        if term_orders is not None:
            oo = filtered[term_orders]
        
        if cmap is not None:
            ax[idx].scatter(xx, yy, s=point_size, c=vv, marker='.', cmap=cmap)
        else:
            
            ax[idx].scatter(xx, yy, s=point_size, c=vv, marker=".")

        if plot_polygons:
            plot_geo_data(get_polygons(slab), ax[idx])
            
        ax[idx].axis('equal')
        if fix_bounds:
            ax[idx].set_ylim(y_min, y_max)
            ax[idx].set_xlim(x_min, x_max)
        ylim = ax[idx].get_ylim()
        ax[idx].set_xticks([])
        ax[idx].set_yticks([])
        
        ax[idx].set_title(slab)
        if labels is not None:
            from matplotlib.patches import Rectangle
            unique_label_colors = (ll + ',' + vv).unique()
            unique_labels = np.array([label_color.split(',')[0] for label_color in unique_label_colors])
            unique_colors = np.array([label_color.split(',')[1] for label_color in unique_label_colors])
    
            if term_orders is not None:
                unique_order = oo.unique()
                term_order = np.argsort(unique_order)
                unique_labels = unique_labels[term_order]
                unique_colors = unique_colors[term_order]
                
            rects = []
            for color in unique_colors:
                rects.append(Rectangle((0, 0), 1, 1, fc=color))
    
            legend = ax[idx].legend(rects, unique_labels, loc=1)
            # ax.add_artist(legend)
        else:
            ax[idx].legend(loc=0)
        
    plt.subplots_adjust(wspace=0.01, hspace=0.01)
    return fig, ax

Displaying the manual anatomical annotations.#

We now have a set of cells in x,y slab coordinates and metadata on the broad regions these cells have been manually annotated as members of. Below we plot the cells using the colors as defined in our term set, displaying all subcortical cells in black and the subset of cells also present in the HMBA-BG dataset in grey. The hemisphere outline polygon orients these subsets of cells in their hemisection context.

slab_list = [
    'CJ23.56.004.CX.42.Plane.05', 'CJ23.56.004.CX.42.Plane.13', 'CJ23.56.004.CX.43.Plane.05',
    'CJ23.56.004.CX.43.Plane.15', 'CJ23.56.004.CX.44.Plane.03'
]
feature = 'parcellation_term_symbol'
fig, ax = plot_slabs(
    cell_metadata,
    f'{feature}_color',
    slab_list,
    plot_polygons=True,
    labels=feature,
    term_orders=f'{feature}_order',
)
fig.suptitle(f'Manual Anatomical Annotations', size=20)
plt.show()
CJ23.56.004.CX.42.Plane.05.geojson: 100%|██████████| 937/937 [00:00<00:00, 17.4kMB/s]
CJ23.56.004.CX.42.Plane.13.geojson: 100%|██████████| 1.07k/1.07k [00:00<00:00, 19.0kMB/s]
CJ23.56.004.CX.43.Plane.05.geojson: 100%|██████████| 1.12k/1.12k [00:00<00:00, 21.4kMB/s]
CJ23.56.004.CX.43.Plane.15.geojson: 100%|██████████| 1.44k/1.44k [00:00<00:00, 34.0kMB/s]
CJ23.56.004.CX.44.Plane.03.geojson: 100%|██████████| 1.57k/1.57k [00:00<00:00, 22.8kMB/s]
../_images/bac500f85c7ddbb570ea120a3a1d4a44ae59ab75bdb0e0eee98fd7504c8abf0d.png

Taxonomy mapping#

Now that we have our base functions for plotting set up and we’ve exercised them using the parcellation colors, let’s load the taxonomy information so that we can plot the spatial locations of various cell types. Below, we load and merge all the information we need to plot cell colors and taxonomy as done in the previous HMBA-Subcortex tutorials.

Below we load the mapping of the Marmoset Subcortex cells onto the HMBA-Subcortex-taxonomy-Marmoset at the cluster level.

cell_to_cluster_membership = abc_cache.get_metadata_dataframe(
    'HMBA-Subcortex-Xenium-Marmoset', 'cell_to_cluster_membership'
).set_index('cell_label')
cell_to_cluster_membership
cell_to_cluster_membership.csv: 100%|██████████| 355M/355M [00:28<00:00, 12.5MMB/s]   
cluster_alias label
cell_label
1382042951_aaondhbe-1-20250227 cluster_1092 CS20260831_LVL2_0977
1382042951_aaoneabm-1-20250227 cluster_902 CS20260831_LVL2_1370
1382042951_aaonekin-1-20250227 cluster_1092 CS20260831_LVL2_0977
1382042951_aaoofhco-1-20250227 cluster_1113 CS20260831_LVL2_0959
1382042951_aaopgffc-1-20250227 cluster_987 CS20260831_LVL2_1404
... ... ...
1388176147_oihjgghm-1-20250227 cluster_706 CS20260831_LVL2_1716
1388176147_oihjjhde-1-20250227 cluster_795 CS20260831_LVL2_1647
1388176147_oihjnnol-1-20250227 cluster_1283 CS20260831_LVL2_0732
1388176147_oihjoahn-1-20250227 cluster_845 CS20260831_LVL2_1725
1388176147_oihkdpbp-1-20250227 cluster_826 CS20260831_LVL2_1723

5580065 rows × 2 columns

Below we load the subcortex taxonomy. For more details on this taxonomy see the notebook here.

cluster = abc_cache.get_metadata_dataframe(
    'HMBA-Subcortex-taxonomy-Marmoset', 'cluster'
).set_index('label')
cluster_annotation_term = abc_cache.get_metadata_dataframe(
    'HMBA-Subcortex-taxonomy-Marmoset', 'cluster_annotation_term'
).set_index('label')

cluster_annotation_term_set = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-taxonomy-Marmoset',
    file_name='cluster_annotation_term_set'
).set_index('label')

cluster_to_cluster_annotation_membership = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-taxonomy-Marmoset',
    file_name='cluster_to_cluster_annotation_membership'
).set_index('cluster_annotation_term_label')

membership_with_cluster_info = cluster_to_cluster_annotation_membership.join(
    cluster.reset_index().set_index('cluster_alias')[['number_of_cells']],
    on='cluster_alias'
)

membership_with_cluster_info = membership_with_cluster_info.join(cluster_annotation_term, rsuffix='_anno_term').reset_index()

membership_groupby = membership_with_cluster_info.groupby(
    ['cluster_alias', 'cluster_annotation_term_set_name']
)

cluster_details = membership_groupby['cluster_annotation_term_name'].first().unstack()
cluster_order = membership_groupby['term_order'].first().unstack()

cluster_colors = membership_groupby['color_hex_triplet'].first().unstack()
cluster_colors = cluster_colors[cluster_annotation_term_set['name']]

cluster_to_dominant_region = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-taxonomy-Marmoset',
    file_name='cluster_to_dominant_region',
).set_index('cluster_alias')
cluster_to_neurotransmitter = abc_cache.get_metadata_dataframe(
    directory='HMBA-Subcortex-taxonomy-Marmoset',
    file_name='cluster_to_neurotransmitter',
).set_index('cluster_alias')

Now we merge the cell type classification colors and order into our metadata.

cell_metadata = cell_metadata.join(cell_to_cluster_membership)
cell_metadata = cell_metadata.join(cluster_details, on='cluster_alias')
cell_metadata = cell_metadata.join(cluster_colors, on='cluster_alias', rsuffix='_color')
cell_metadata = cell_metadata.join(cluster_order, on='cluster_alias', rsuffix='_order')
cell_metadata = cell_metadata.join(cluster_to_dominant_region, on='cluster_alias', rsuffix='_region')
cell_metadata = cell_metadata.join(cluster_to_neurotransmitter, on='cluster_alias', rsuffix='_neurotransmitter')

del cell_to_cluster_membership

cell_metadata.head()
brain_section_label brain_section_barcode x_experiment y_experiment segmentation_job_id segmentation_method cell_area nucleus_area n_nuclei_in_cell total_counts ... Class_color Group_color Cluster_color Class_order Cluster_order Group_order Cluster_dominant_region Group_dominant_region Cluster_neurotransmitter Group_neurotransmitter
cell_label
1382042951_aaondhbe-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4459.1006 4524.2420 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 89.81579 60.735157 1 339 ... #88CCEE #BABA3B #69efa5 11 978 125 Cx Cx Glut Glut
1382042951_aaoneabm-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4668.6323 4653.2130 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 105.98172 74.327190 1 762 ... #CBC3E3 #708297 #bdb6cf 14 1371 166 Cx STR GABA GABA
1382042951_aaonekin-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4593.0610 4594.6304 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 129.50813 74.598130 1 406 ... #88CCEE #BABA3B #69efa5 11 978 125 Cx Cx Glut Glut
1382042951_aaoofhco-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4529.3900 4348.2266 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 136.37189 72.475784 1 948 ... #88CCEE #BA723B #cd5698 11 960 124 Cx Cx Glut Glut
1382042951_aaopgffc-1-20250227 CJ23.56.004.CX.42.01.06 1382042951 4524.6533 4382.6323 10X cell segmentation kit - default Segmented by boundary stain (ATP1A1+CD45+E-Cad... 129.41782 78.391250 1 941 ... #CBC3E3 #488edc #1213f8 14 1405 168 Cx STR GABA GABA

5 rows × 58 columns

Merging in the taxonomy allows one to select cells by the specific taxons they are associated with, as we will see in a function we create below.

Plotting the taxonomy#

Now that the taxonomy is loaded, we plot the the cells showing the spatial locations of the cell types in the Subcortex. Below we plot each dataset for each of the 3 levels in the taxonomy selection using the same set of slabs we used in the previous section.

slab_list = [
    'CJ23.56.004.CX.42.Plane.05', 'CJ23.56.004.CX.42.Plane.13', 'CJ23.56.004.CX.43.Plane.05',
    'CJ23.56.004.CX.43.Plane.15', 'CJ23.56.004.CX.44.Plane.03'
]
for level in ['Class', 'Group', 'Cluster']:
    fig, ax = plot_slabs(
        cell_metadata,
        f'{level}_color',
        slab_list,
        plot_polygons=True,
        labels=level if level == 'Class' else None,
        term_orders=f'{level}_order' if level == 'Class' else None
    )
    fig.suptitle(f'{level}')
    plt.show()
../_images/2add16ae18bb7f39959b055a9a677a69bf65da9313885bf1b6b3011a8a48d563.png ../_images/272847d5775b0db51c693c682b25f4d58712c55e06c0e47c4786379561efb647.png ../_images/4675e4724a8eaf53e61257d4a556401b30151f374c4ecf7188d6e7c9fd5e5fb0.png

Selecting cell types#

We define a slightly modified version of our slab plotting function that allows us to plot specific cell types, coloring by cell type level that is nominally lower in the taxonomy. This allows for us to investigate the spatial distribution of cell types more clearly by, for instance, plotting the spatial distributions of Groups localized to the Amygdala.

In general, cells associated with individual cell types and taxons can be selected by masking the DataFrame using the desired column and value in that column, as we showed in our examples above. For instance to select the Class, TEL Glut, we can create a mask thusly cell_metadata['Class']=='TEL Glut' and similarly for the other species. Again, this function can be used to plot individual slab planes and selections as well.

def plot_selected_cells(
    df: pd.DataFrame,
    feature: str,
    selection_list: List[Tuple[str, str]],
    slab: str,
    cmap: Optional[plt.Colormap] = None,
    fig_width: float = 40,
    fig_height: float = 8,
    plot_polygons: bool = True,
    point_size: float = 1.0,
    fix_bounds: bool = False,
    labels: str = None,
    term_orders: str = None,
):
    """Plot spatial transcriptomics data selected a cell type and colored by
    a feature of interest.

    Parameters
    ----------
    df: pandas.DataFrame
        cell metadata data frame with x,y locations to plot.
    feature: str
        Expression value or color column to plot.
    selection_list: List[str]
        Set of cell types to select by. Specify as ('taxonomy_level_name', 'cell type')
    slab: str
        Specific slab to plot.
    cmap: matplotlib.colors.Colormap
        Colormap for expression features.
    fid_width: int
        Width of the plotted figure.
    fig_height: iont
        Height of the plotted figure.
    plot_polygons: bool
        Overplot geojson polygons. Note that these are note available for human
        and this should be set to False.
    point_size: float
        Size of the points to plot.
    fix_bounds: bool
        If True, fix the x and y bounds across all slabs to be the same.
    labels : array-like, optional
        labels for the points to plot. If None, no labels will be added to the plot.
    term_orders : array-like, optional
        order of the labels for the legend. If None, the labels will be ordered by their appearance in `labels`.  
    """
    
    fig, ax = plt.subplots(1, len(selection_list))
    fig.set_size_inches(fig_width, fig_height)

    if len(selection_list) == 1:
        ax = [ax]

    if 'slab_plane_label' in df.columns:
        slab_filtered = df[df['slab_plane_label']==slab]
    elif 'brain_section_label' in df.columns:
        slab_filtered = df[df['brain_section_label']==slab]

    if 'x' in df.columns:
        x_col = 'x'
    elif 'x_rotated_mm' in df.columns:
        x_col = 'x_rotated_mm'
    if 'y' in df.columns:
        y_col = 'y'
    elif 'y_rotated_mm' in df.columns:
        y_col = 'y_rotated_mm'

    if fix_bounds:
        x_min = slab_filtered[x_col].min()
        x_max = slab_filtered[x_col].max()
        y_min = slab_filtered[y_col].min()
        y_max = slab_filtered[y_col].max()
    
    for idx, (col, name) in enumerate(selection_list):

        if 'slab_plane_label' in df.columns:
            filtered = slab_filtered[slab_filtered[col]==name]
        elif 'brain_section_label' in df.columns:
            filtered = slab_filtered[slab_filtered[col]==name]
        xx = filtered[x_col]
        yy = filtered[y_col]
        vv = filtered[feature]
        if labels is not None:
            ll = filtered[labels]
        if term_orders is not None:
            oo = filtered[term_orders]

        
        if cmap is not None:
            ax[idx].scatter(xx, yy, s=point_size, c=vv, marker='.', cmap=cmap)
        else :
            ax[idx].scatter(xx, yy, s=point_size, c=vv, marker=".")

        if plot_polygons:
            plot_geo_data(get_polygons(slab), ax[idx])
            
        ax[idx].axis('equal')
        ylim = ax[idx].get_ylim()
        if fix_bounds:
            ylim = (y_min, y_max)
            ax[idx].set_xlim(x_min, x_max)
        ax[idx].set_xticks([])
        ax[idx].set_yticks([])
        
        ax[idx].set_title(name)
        if labels is not None:
            from matplotlib.patches import Rectangle
            unique_label_colors = (ll + ',' + vv).unique()
            unique_labels = np.array([label_color.split(',')[0] for label_color in unique_label_colors])
            unique_colors = np.array([label_color.split(',')[1] for label_color in unique_label_colors])
    
            if term_orders is not None:
                unique_order = oo.unique()
                term_order = np.argsort(unique_order)
                unique_labels = unique_labels[term_order]
                unique_colors = unique_colors[term_order]
                
            rects = []
            for color in unique_colors:
                rects.append(Rectangle((0, 0), 1, 1, fc=color))
    
            legend = ax[idx].legend(rects, unique_labels, loc=1)
            # ax.add_artist(legend)
        else:
            ax[idx].legend(loc=0)
        
    plt.subplots_adjust(wspace=0.01, hspace=0.01)
    fig.suptitle(f'{slab}')
    return fig, ax

Using the above function, we plot the cells colored by their Group color and selected by specific Classes and Groups.

slab_list = [
    'CJ23.56.004.CX.42.Plane.22',
    'CJ23.56.004.CX.43.Plane.04', 
    'CJ23.56.004.CX.43.Plane.05',
    'CJ23.56.004.CX.43.Plane.06',
    'CJ23.56.004.CX.43.Plane.08',
    'CJ23.56.004.CX.43.Plane.10',
    'CJ23.56.004.CX.43.Plane.11',
]

for slab in slab_list:
    fig, ax = plot_selected_cells(
        cell_metadata,
        feature='Group_color',
        selection_list=[
            ('Class', 'TEL Glut'),
            ('Group', 'AAA-Me-BST Glut'),
            ('Group', 'Me-BST-AAA GABA'),
            ('Group', 'CEN-SLEA-BST GABA'),
            ('Group', 'PL-ACTA Glut'),
            ('Group', 'Co-ACTA-Pir-La Glut'),
            ('Group', 'BM-Co Glut'),
            ('Group', 'AHA Glut'),
            ('Group', 'La-PL-ACTA-Pir Glut'),
            ('Group', 'La Glut'),
            ('Group', 'BL Glut'),
        ],
        slab=slab,
        fig_width=30,
        fig_height=6,
        plot_polygons=True,
    )
    plt.show()
CJ23.56.004.CX.42.Plane.22.geojson: 100%|██████████| 1.39k/1.39k [00:00<00:00, 27.0kMB/s]
../_images/26655e36d4633515963c7648edbf94c9b6733fd23778066338926ce10ef60ede.png
CJ23.56.004.CX.43.Plane.04.geojson: 100%|██████████| 1.58k/1.58k [00:00<00:00, 17.7kMB/s]
../_images/5d65168780eadb3ec3d9c959f33d6b0c5dc8dea636cc0d06be239e5a49f4b6ff.png ../_images/65837a2d84e242bf8d1a72c3a7801c98e7afdf580db847ba5464130f5912ae8a.png
CJ23.56.004.CX.43.Plane.06.geojson: 100%|██████████| 1.43k/1.43k [00:00<00:00, 31.9kMB/s]
../_images/4b6de707d33aa11409c18280981fe3783858915e9e9f9cab07f82c2b7d173ded.png
CJ23.56.004.CX.43.Plane.08.geojson: 100%|██████████| 1.41k/1.41k [00:00<00:00, 22.8kMB/s]
../_images/2e8cb3c2e416d2c5ec2b226ccf1e25469a0b3a82c073fd4b5f6a09080ad9e013.png
CJ23.56.004.CX.43.Plane.10.geojson: 100%|██████████| 1.06k/1.06k [00:00<00:00, 21.8kMB/s]
../_images/40c80a3f44474f60d73ae738fb68abbd488e9f527f38c035ab8b5ddf6e320aa1.png
CJ23.56.004.CX.43.Plane.11.geojson: 100%|██████████| 1.08k/1.08k [00:00<00:00, 19.6kMB/s]
../_images/814bb7705f8add68c1fc542788812aea15a7ed41250c3475d9c53b9ad81ce285.png

We can also plot the cells colored by their Cluster level.

slab_list = [
    'CJ23.56.004.CX.43.Plane.08',
]

for slab in slab_list:
    fig, ax = plot_selected_cells(
        cell_metadata,
        feature='Cluster_color',
        selection_list=[
            ('Class', 'TEL Glut'),
            # ('Group', 'AAA-Me-BST Glut'),
            # ('Group', 'Me-BST-AAA GABA'),
            ('Group', 'CEN-SLEA-BST GABA'),
            ('Group', 'PL-ACTA Glut'),
            # ('Group', 'Co-ACTA-Pir-La Glut'),
            ('Group', 'BM-Co Glut'),
            # ('Group', 'AHA Glut'),
            ('Group', 'La-PL-ACTA-Pir Glut'),
            ('Group', 'La Glut'),
            ('Group', 'BL Glut'),
        ],
        slab=slab,
        fig_width=30,
        fig_height=6,
        plot_polygons=True,
    )
    plt.show()
../_images/57ee11b5d0bc21f9c3ab9ac9ae3a1db76de2fe2e083e015e3c24873dd3a723ee.png

Region and Neurotransmitter#

As with the tutorial for the Subcortex taxonomy, we have information for the dominant brain region and neurotransmitter type, derived from the cell type taxonomy, at both the Cluster and Group level.

e load both the Group and Cluster’s dominant region and neurotransmitter colors and orders below.”

extract_value_set(cell_metadata, value_sets, 'Cluster_neurotransmitter', 'neurotransmitter')
extract_value_set(cell_metadata, value_sets, 'Group_neurotransmitter', 'neurotransmitter')
extract_value_set(cell_metadata, value_sets, 'Cluster_dominant_region', 'dominant_region')
extract_value_set(cell_metadata, value_sets, 'Group_dominant_region', 'dominant_region')

Finally, we plot the dominant region and neurotransmitter for the subcortex data.

slab_list = [
    'CJ23.56.004.CX.42.Plane.05', 'CJ23.56.004.CX.42.Plane.13', 'CJ23.56.004.CX.43.Plane.05',
    'CJ23.56.004.CX.43.Plane.15', 'CJ23.56.004.CX.44.Plane.03'
]
for celltype in ['Group', 'Cluster']:
    fig, ax = plot_slabs(
        cell_metadata,
        f'{celltype}_dominant_region_color',
        slab_list,
        plot_polygons=True,
        labels=f'{celltype}_dominant_region',
        term_orders=f'{celltype}_dominant_region_order'
    )
    fig.suptitle(f'{celltype} Region')
    plt.show()
    fig, ax = plot_slabs(
        cell_metadata,
        f'{celltype}_neurotransmitter_color',
        slab_list,
        plot_polygons=True,
        labels=f'{celltype}_neurotransmitter',
        term_orders=f'{celltype}_neurotransmitter_order'
    )
    fig.suptitle(f'{celltype} Neurotransmitter')
    plt.show()
../_images/6f005a967eee20596aead8b3496ebb74387e5d5d47e61bb530487393c609da9c.png ../_images/b4fb53fee56d44ed0e4035f635466e9bc8c9965893936b93f3d4d672d58c1e4b.png ../_images/4ee6bba25932f9db7455c52d5fe0bbf5a1fb14e98faa6d54904780a197af6e87.png ../_images/eb813b7bb603034caac47d22fac10b6bea45458d2ab0d3a3fe452a658b987331.png

In the next tutorial, we will explore gene expression patterns across these spatial transcriptomics datasets.

To learn more about the taxonomy used in this notebook and the data it is derived from, see the HMBA-Subcortex Clustering Analysis and Taxonomy notebook.