Using mascarade package

knitr::opts_chunk$set(echo = TRUE, fig.width = 7, fig.height=5)

This is a vignette describing usage of mascarade to generate masks for clusters on 2D dimensional reduction plots like UMAP or t-SNE.

Package installation

The package stable version can be installed from CRAN:

install.packages("mascarade")

The most recent development version of the package can be installed from GitHub:

remotes::install_github("alserglab/mascarade")

Loading necessary libraries

library(mascarade)
library(data.table)
library(ggplot2)
library(ggforce)

Example run

Loading example data from PBMC 3K processed with Seurat (see below for more details).

data("exampleMascarade")

UMAP coordinates:

head(exampleMascarade$dims)

Cluster annotations:

head(exampleMascarade$clusters)

Expression table for several genes:

head(exampleMascarade$features)

Let's plot these data:

data <- data.table(exampleMascarade$dims, 
                   cluster=exampleMascarade$clusters,
                   exampleMascarade$features)
ggplot(data, aes(x=UMAP_1, y=UMAP_2)) + 
    geom_point(aes(color=cluster)) + 
    coord_fixed() + 
    theme_classic()

Now let's generate cluster masks:

maskTable <- generateMask(dims=exampleMascarade$dims, 
                          clusters=exampleMascarade$clusters)

The maskTable is actually a table of cluster borders. A single cluster can have multiple connected parts, and one a single part can contain multiple border lines (groups).

head(maskTable)

Now we can use this table to draw the borders with geom_path (group column should be used as the group aesthetics):

ggplot(data, aes(x=UMAP_1, y=UMAP_2)) + 
    geom_point(aes(color=cluster)) + 
    geom_path(data=maskTable, aes(group=group)) +
    coord_fixed() + 
    theme_classic()

Or we can color the borders instead of points:

ggplot(data, aes(x=UMAP_1, y=UMAP_2)) + 
    geom_point(color="grey") + 
    geom_path(data=maskTable, aes(group=group, color=cluster), linewidth=1) +
    coord_fixed() + 
    theme_classic()

We can use ggforce package to make the borders touch instead of overlap:

ggplot(data, aes(x=UMAP_1, y=UMAP_2)) + 
    geom_point(color="grey") + 
    ggforce::geom_shape(data=maskTable, aes(group=group, color=cluster),
                        linewidth=1, fill=NA, expand=unit(-1, "pt")) +
    coord_fixed() + 
    theme_classic()

In the presence of small clusters it can help to expand the borders a bit further away from the points.

maskTable <- generateMask(dims=exampleMascarade$dims, 
                          clusters=exampleMascarade$clusters,
                          expand=0.02)
ggplot(data, aes(x=UMAP_1, y=UMAP_2)) + 
    geom_point(color="grey") + 
    ggforce::geom_shape(data=maskTable, aes(group=group, color=cluster),
                        linewidth=1, fill=NA, expand=unit(-1, "pt")) +
    coord_fixed() + 
    theme_classic()

With the help of ggforce-based function geom_mark_shape we can also put the labels within the plot itself.

myMask <- list(
    geom_mark_shape(data=maskTable, aes(group=cluster, color=cluster, label = cluster),
                    fill = NA,
                   linewidth=1, expand=unit(-1, "pt"),
                   con.cap=0, con.type = "straight",
                   label.fontsize = 10, label.buffer = unit(0, "cm"),
                   label.fontface = "plain",
                   label.minwidth = 0,
                   label.margin = margin(2, 2, 2, 2, "pt"),
                   label.lineheight = 0,
                   con.colour = "inherit",
                   show.legend = FALSE),
    # expanding to give a bit more space for labels
    scale_x_continuous(expand = expansion(mult = 0.1)),
    scale_y_continuous(expand = expansion(mult = 0.1))
)

ggplot(data, aes(x=UMAP_1, y=UMAP_2)) +
    geom_point(color="grey") +
    myMask +
    coord_fixed() +
    theme_classic()

The same can be achived with the fancyMask() helper function:

ggplot(data, aes(x=UMAP_1, y=UMAP_2)) +
    geom_point(color="grey") +
    fancyMask(maskTable, ratio=1, cols = scales::hue_pal()) +
    theme_classic()

Now we can easily show association between cell types and expression of particular genes, such as GNLY being a good marker for NK cells in this dataset.

ggplot(data, aes(x=UMAP_1, y=UMAP_2)) + 
    geom_point(aes(color=GNLY), size=0.5) +
    scale_color_gradient2(low = "#404040", high="red") + 
    fancyMask(maskTable, ratio=1, cols = scales::hue_pal()) +
    theme_classic()

We can focus on a single cluster too:

ggplot(data, aes(x=UMAP_1, y=UMAP_2)) + 
    geom_point(aes(color=GNLY), size=0.5) + 
    scale_color_gradient2(low = "#404040", high="red") + 
    geom_path(data=maskTable[cluster=="NK"], aes(group=group)) +
    coord_fixed() + 
    theme_classic()

Working with Seurat

For this part of the vignette you need Seuratpackage.

library(Seurat)

Let's get the example PBMC3K dataset:

pbmc3k <- readRDS(url("https://alserglab.wustl.edu/files/mascarade/examples/pbmc3k_seurat5.rds"))
pbmc3k <- NormalizeData(pbmc3k)
pbmc3k

The same object can be obtained using SeuratData package (can be installed with remotes::install_github('satijalab/seurat-data')):

if (requireNamespace("SeuratData")) {
    if (!AvailableData()["pbmc3k", "Installed"]) {
        InstallData("pbmc3k")    
    }
    LoadData("pbmc3k")

    pbmc3k <- UpdateSeuratObject(pbmc3k.final)    
    pbmc3k
}

Generate masks using a helper function:

maskTable <- generateMaskSeurat(pbmc3k)

We can use fancyMask() now, here it reuses colors from DimPlot automatically:

DimPlot(pbmc3k) + NoLegend() +
    fancyMask(maskTable, ratio=1)

For the DimPlot, the borders can be viewed as redundant and removed:

DimPlot(pbmc3k) + NoLegend() +
    fancyMask(maskTable, linewidth = 0, ratio=1)

Let's plot an NK cell marker:

FeaturePlot(pbmc3k, "GNLY", cols=c("grey90", "red")) +
    fancyMask(maskTable, ratio=1)

Or multiple markers (skipping the labels to save space, but adding colors):

featureList <- c("MS4A1", "GNLY", "CD3E", "CD14")
FeaturePlot(pbmc3k, features=featureList, cols=c("grey90", "red")) *
    fancyMask(maskTable, ratio=1, linewidth=0.5, label=FALSE, cols = scales::hue_pal())

Works with t-SNE too:

pbmc3k <- RunTSNE(pbmc3k)

maskTable <- generateMaskSeurat(pbmc3k, reduction = "tsne")

FeaturePlot(pbmc3k, features=featureList, reduction = "tsne", cols=c("grey90", "red")) * 
    fancyMask(maskTable, ratio=1, linewidth=0.5, label=FALSE, cols = scales::hue_pal())

Session info

sessionInfo()


Try the mascarade package in your browser

Any scripts or data that you put into this service are public.

mascarade documentation built on March 7, 2026, 1:07 a.m.