Skip to contents

In this tutorial, we’ll have a look at different ways of creating spatial plots with semla. Functions such as MapFeatures() and MapLabels() produce patchworks (see R package patchwork) which are easy to manipulate after they have been created. In addition to these two fundamental plotting functions, there are also MapFeaturesSummary() and MapLabelsSummary(), which works just as their parent functions but also adds a subplot with informative summary statistics attached.s

The patchwork R package is extremely versatile and makes it easy to customize your figures! Here we’ll have a look at some options that comes with MapFeatures and MapLabels a couple of tips and tricks for how to use the patchwork R package.

This tutorial is an extension of the ‘Map numeric features’ and ‘Map categorical features’ tutorials. Here you can find more in depth details about how to manipulate various components of the plots generated with MapFeatures and MapLabels as well as some additional plot functions.

Load data

First we need to load some 10x Visium data. here we’ll use a mouse brain tissue dataset and a mouse colon dataset that are shipped with semla.

# Load data
se_mbrain <- readRDS(file = system.file("extdata", 
                                        "mousebrain/se_mbrain", 
                                        package = "semla"))
se_mbrain$sample_id <- "mousebrain"
se_mcolon <- readRDS(file = system.file("extdata", 
                                        "mousecolon/se_mcolon", 
                                        package = "semla"))
se_mcolon$sample_id <- "mousecolon"
se <- MergeSTData(se_mbrain, se_mcolon)

We can use the functions MapFeatures and MapLabels to make spatial plots showing the distribution of numeric or categorical features. For those who are familiar with Seurat, these functions are similar to SpatialFeaturePlot and SpatialDimPlot in the sense that the first can be used to visualize numeric data and the latter can be used to color data points based on categorical data.

Map numeric features

Let’s get started with MapFeatures. The most basic usage is to map gene expression spatially:

cols <- viridis::rocket(11, direction = -1)
p <- MapFeatures(se, features = c("Th", "Clu"), colors = cols)
p

Summary subplot

We can also use the function MapFeaturesSummary to add a subplot next to the spatial plot summarizing the gene expression in the section using either a box plot, a violin plot, a histogram, or a density plot.

This function only takes one gene at the time, but you can patch two plots together manually if desired.

p <- MapFeaturesSummary(se, 
                        features = "Clu", 
                        subplot_type = "violin", 
                        colors = cols)
p

p1 <- MapFeaturesSummary(se,
                         section_number = 1,
                         features = "Clu", 
                         subplot_type = "box", 
                         colors = cols, 
                         fill_color = "white")
p2 <- MapFeaturesSummary(se,
                         section_number = 1,
                         features = "Clu", 
                         subplot_type = "histogram", 
                         colors = cols)
p1|p2

genes_to_plot <- c("Clu", "Apod", "Cd79a")

plot_list <- lapply(genes_to_plot, function(g){
  MapFeaturesSummary(se,
                     section_number = 1,
                     features = g, 
                     subplot_type = "density", 
                     pt_size = 0.8,
                     colors = cols)
})
wrap_plots(plot_list, nrow = 1)

Overlay maps on images

If we want to create a map with the H&E images we can do this by setting image_use = raw. But before we can do this, we need to load the images into our Seurat object:

cols_he <- viridis::viridis(11)
se <- LoadImages(se, verbose = FALSE)

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 image_use = "raw", 
                 colors = cols_he)
p

Right now it’s quite difficult to see the tissue underneath the spots. We can add some opacity to the colors which is scaled by the feature values to make spots with low expression transparent:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 image_use = "raw",
                 colors = cols_he, 
                 scale_alpha = TRUE)
p

Transpose patchwork layout

By default, MapFeatures arranges features in columns and samples in rows. We can transpose the plot by setting arrange_features to “row”:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 arrange_features = "row", 
                 color = cols)
p

Independent color bars

The color bars are now identical for each feature. MapFeatures calculates the range for each feature and uses this range to determine the limits of the color bars. If you want to change this behavior to scale the values independently, you can set scale = "free":

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 scale = "free", 
                 colors = cols)
p

Fit plot area to spots

MapFeatures used the image dimensions to define the plot area dimensions. Sometimes, if you have a very small piece of tissue, you will end up with a lot of white space. You can override this behavior by setting override_plot_dims = TRUE which will make MapFeatures compute the dimensions based on your coordinates. Notice how the tissues are expanded:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 override_plot_dims = TRUE, 
                 color = cols)
p

Controlling themes

Themes can be modified by adding a new ggplot theme using the & operator. This operator will make sure that the theme is added to each subplot in our patchwork. As an example, let’s say that we want to place the legends on the right side of our plots instead:

p <- MapFeatures(se, features = c("Th", "Clu"), color = cols) &
  theme(legend.position = "right", legend.text = element_text(angle = 0))
p

We can also remove the color legends entirely:

p <- MapFeatures(se, features = c("Th", "Clu"), colors = cols) &
  theme(legend.position = "none")
p

Or remove everything except the spatial feature map:

p <- MapFeatures(se, features = c("Th", "Clu"), colors = cols) &
  theme(legend.position = "none", 
        plot.title = element_blank(), 
        plot.subtitle = element_blank())
p

Collect color bars

If we don’t want to have the same color bar next to each tissue section, we can collect identical color bars and place them on the side. We can use plot_layout(guides = "collect") to modify this. Note that if we want to place the color bar on the right side of the plots, we need to set arrange_features = "row"` and adjust the legend position:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 arrange_features = "row", 
                 colors = cols) +
  plot_layout(guides = "collect") &
  theme(legend.position = "right", legend.text = element_text(angle = 0))
p

This already looks quite good, but the color bars are a bit misplaced. We can adjust their placement by modifying the legend margins:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 arrange_features = "row", 
                 color = cols) +
  plot_layout(guides = "collect") &
  theme(legend.position = "right", 
        legend.text = element_text(angle = 0),
        legend.margin = margin(t = 50, r = 0, b = 100, l = 0))
p

Do not do this

If we set scale = "free", the color bars are no longer unique and it doesn’t make sense to collect the color bars anymore:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 arrange_features = "row", 
                 scale = "free", 
                 colors = cols) +
  plot_layout(guides = "collect") &
  theme(legend.position = "right", 
        legend.text = element_text(angle = 0),
        legend.margin = margin(t = 50, r = 0, b = 100, l = 0))
p

Do not do this either

If we set arrange_features = "col", the placement of the color bar will not make sense in this case either. The color bars are now located on the right side, but the features are arranged by row:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 arrange_features = "col", 
                 colors = cols) +
  plot_layout(guides = "collect") &
  theme(legend.position = "right", 
        legend.text = element_text(angle = 0),
        legend.margin = margin(t = 50, r = 0, b = 100, l = 0))
p

Instead, if we want to arrange features by columns, it would make more sense to place the color bars on top of each column and adjust the legend margins accordingly:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 arrange_features = "col", 
                 color = cols) +
  plot_layout(guides = "collect") &
  theme(legend.position = "top", 
        legend.text = element_text(angle = 0),
        legend.margin = margin(t = 0, r = 100, b = 0, l = 10))
p

Remove plot margins

If you still think that there’s too much empty space around the tissues in the patchwork, you try setting override_plot_dims = TRUE which will crop the plot area to only fit the spots:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 override_plot_dims = TRUE, 
                 color = cols)
p

Crop

H&E images will also be cropped to fit the new plot dimensions if override_plot_dims = TRUE. This can be particularly useful when working with small tissue sections that only cover a small portion of the 10x Visium capture area.

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 image_use = "raw", 
                 override_plot_dims = TRUE, 
                 color = cols_he)
p

Note that the plots dimensions are calculated for the entire dataset when override_plot_dims = TRUE.

If the tissue sections are placed in very different part of the capture area, the resulting “crop” window might not be what you are looking for. If you want to avoid this behavior, it is better to make two separate plots. Notice the difference in the plot below compared to the previous plot. The previous plot had some empty space outside of the tissue.

p1 <- MapFeatures(se, 
                  features = c("Th", "Clu"), 
                  image_use = "raw", 
                  override_plot_dims = TRUE, 
                  color = cols_he, 
                  section_number = 1)
p2 <- MapFeatures(se, 
                  features = c("Th", "Clu"), 
                  image_use = "raw", 
                  override_plot_dims = TRUE, 
                  color = cols_he, 
                  section_number = 2)
p1 / p2

We can also crop the images manually by defining a crop_area. The crop_area should be a vector of length four defining the corners of a rectangle, where the x- and y-axes are defined from 0-1.

In order to decide how this rectangle should be defined, you can get some help by adding a grid to the plot:

p <- MapFeatures(se, 
                 features = c("Th", "Clu"), 
                 image_use = "raw", 
                 color = cols_he, 
                 pt_alpha = 0.5) &
  theme(panel.grid.major = element_line(linetype = "dashed"), axis.text = element_text())
p

Now if we want to crop out the GALT tissue in the mouse colon sample we can cut the image at left=0.45, bottom=0.55, right=0.65, top=0.7:

p <- MapFeatures(se, 
                 features = c("Cd79a", "Clu"), 
                 image_use = "raw", 
                 pt_size = 3, 
                 section_number = 2, 
                 color = cols_he, 
                 crop_area = c(0.45, 0.55, 0.65, 0.7))
p

And we can patch together a nice figure showing the expression both at a global level and inside the GALT:

p_global <- MapFeatures(se, 
                        features = c("Cd79a", "Clu"), 
                        image_use = "raw", 
                        scale_alpha = TRUE,
                        pt_size = 1, 
                        section_number = 2, 
                        color = cols_he, 
                        override_plot_dims = TRUE)
p_GALT <- MapFeatures(se, 
                      features = c("Cd79a", "Clu"), 
                      image_use = "raw", 
                      scale_alpha = TRUE, 
                      pt_size = 3, 
                      section_number = 2, 
                      color = cols_he, 
                      crop_area = c(0.45, 0.55, 0.65, 0.7)) &
  theme(plot.title = element_blank(), 
        plot.subtitle = element_blank(), 
        legend.position = "none")

(p_global / p_GALT)

Increase resolution

Right now, you can see that the resolution of zoomed in image is quite low. The reason for this is that the images were loaded to have a height of 400 pixels which is the default setting for LoadImages. If you want to, you can reload the images in higher resolution given that you provided higher resolution images as input. For example, if you used the “tissue_hires_image.png” files as input for ReadVisiumData, these images are roughly 2000x2000 pixels in size, so you can reload the images in higher resolution. If we run LoadImages again and set image_height = 1000 we should be able to view the H%&E images in slightly higher resolution.

NB: The “hires” images are not provided in semla so we will need to download them. We are currently using the “lowres” images which are only about 600x600 pixels large. We can use ReplaceImagePaths to replace the current images. You need to make sure that the new images are in the correct order and that they represent scaled representations of the images already loaded into the object.

# Download hires H&E images
he_imgs <- c(file.path("https://data.mendeley.com/public-files/datasets/kj3ntnt6vb",
                       "files/d97fb9ce-eb7d-4c1f-98e0-c17582024a40/file_downloaded"),
             file.path("https://data.mendeley.com/public-files/datasets/kj3ntnt6vb",
                    "files/d33e8074-8d83-4003-b04a-9a78ff465279/file_downloaded"))

# Replace images
se <- ReplaceImagePaths(se, paths = he_imgs)

# Load images in higher resolution
se_high_res <- LoadImages(se, image_height = 1000)
## 
## ── Loading H&E images ──
## 
##  Loading image from https://data.mendeley.com/public-files/datasets/kj3ntnt6vb/files/d97fb9ce-eb7d-4c1f-98e0-c17582024a40/file_downloaded
##  Scaled image from 2000x1882 to 1000x941 pixels
##  Loading image from https://data.mendeley.com/public-files/datasets/kj3ntnt6vb/files/d33e8074-8d83-4003-b04a-9a78ff465279/file_downloaded
##  Scaled image from 8929x9901 to 1000x1109 pixels
##  Saving loaded H&E images as 'rasters' in Seurat object
# Check object sizes
print(object.size(se), units = "MB")
## 16.4 Mb
print(object.size(se_high_res), units = "MB")
## 39.1 Mb

Note that these higher resolution images will take up more space and might slow down the plotting considerably.

p_global <- MapFeatures(se_high_res, 
                        features = c("Cd79a", "Clu"), 
                        image_use = "raw", 
                        scale_alpha = TRUE,
                        pt_size = 1, 
                        section_number = 2, 
                        color = cols_he, 
                        override_plot_dims = TRUE)
p_GALT <- MapFeatures(se_high_res, 
                      features = c("Cd79a", "Clu"), 
                      image_use = "raw", 
                      scale_alpha = TRUE, 
                      pt_size = 3, 
                      section_number = 2, 
                      color = cols_he, 
                      crop_area = c(0.45, 0.55, 0.65, 0.7)) &
  theme(plot.title = element_blank(), 
        plot.subtitle = element_blank(), 
        legend.position = "none")

(p_global / p_GALT)

Map categorical features

For categorical data, we use MapLabels() instead of MapFeatures(). This function allows us to color our spots based on some column of our Seurat object containing categorical data.

MapLabels(se, column_name = "sample_id", ncol = 1) &
  theme(legend.position = "right")

Order of categories

Categorical data can be represented as character vectors or factors, but with factors it’s easier to control the order of the labels as well as their colors. If we want to customize the order, we can convert our column into a factor and set the levels as we please:

se$sample_id_factor <- factor(se$sample_id, levels = c("mousecolon", "mousebrain"))
MapLabels(se, column_name = "sample_id_factor", ncol = 1) &
  theme(legend.position = "right")

Colors

If we pass a named vector of colors we can control the coloring of our labels:

MapLabels(se, column_name = "sample_id_factor", ncol = 1,
          colors = c("mousecolon" = "#4477AA", "mousebrain" = "#CC6677")) &
  theme(legend.position = "right")

Let’s run unsupervised clustering on our data to get slightly more interesting results to work with.

NB: It doesn’t make much sense to run data-driven clustering on two completely different tissue types, but here we are only interested in demonstrating how you can use MapLabels.

se <- se |>
  NormalizeData() |>
  ScaleData() |>
  FindVariableFeatures() |>
  RunPCA() |>
  FindNeighbors(reduction = "pca", dims = 1:10) |>
  FindClusters(resolution = 0.2)
## Modularity Optimizer version 1.3.0 by Ludo Waltman and Nees Jan van Eck
## 
## Number of nodes: 5164
## Number of edges: 170850
## 
## Running Louvain algorithm...
## Maximum modularity in 10 random starts: 0.9210
## Number of communities: 5
## Elapsed time: 0 seconds

Map clusters

MapLabels(se, column_name = "seurat_clusters", ncol = 1) &
  theme(legend.position = "right")

Summary subplot

Similarly to MapFeaturesSummary(), we also have MapLabelsSummary(), which adds a stacked barchart displaying the cluster proportion (percentage) in the section. Passing bar_display = "count" will instead show the actual spot count per cluster.

Let’s combine MapLabelsSummary with some custom cluster colors.

cluster_colors <- setNames(RColorBrewer::brewer.pal(length(levels(se$seurat_clusters)), "Set1"), 
                           nm = levels(se$seurat_clusters))

MapLabelsSummary(se, 
                 column_name = "seurat_clusters", 
                 ncol = 1, 
                 colors = cluster_colors)

We can adjust a few things, like width and text size, on the bar plot with the arguments bar_width and bar_label_size.

MapLabelsSummary(se, 
                 column_name = "seurat_clusters",
                 section_number = 1, 
                 label_by = "sample_id",
                 colors = cluster_colors,
                 pt_size = 2,
                 bar_display = "count",
                 bar_width = 2, 
                 bar_label_size = 4) &
  theme(plot.title = element_text(hjust=0.5, size = 14, face = "bold"))

Format legend

Point size

If you want to increase the size of the spots in the color legend, you can override the fill aesthetic that controls the appearance of the points without changing the size of the points in the plot. We can do this by using guides(fill = guide_legend(override.aes = list(size = ...))):

MapLabels(se, 
          column_name = "seurat_clusters", 
          ncol = 1,
          colors = cluster_colors) &
  guides(fill = guide_legend(override.aes = list(size = 3))) &
  theme(legend.position = "right")

Legend arrangement

Another useful option is to adjust the arrangement of the color legend. For example, if you have a lot of different categories, it might be easier to read the labels if they are arranged in multiple columns:

MapLabels(se, 
          column_name = "seurat_clusters", 
          ncol = 1,
          colors = cluster_colors) &
  guides(fill = guide_legend(override.aes = list(size = 3), 
                             ncol = 2)) &
  theme(legend.position = "right")

Collect legends

Since all sub plots share the same color legends, we can collect them and place them on the side of the plot:

MapLabels(se, 
          column_name = "seurat_clusters", 
          ncol = 1,
          colors = cluster_colors) +
  plot_layout(guides = "collect") &
  guides(fill = guide_legend(override.aes = list(size = 3), 
                             ncol = 2)) &
  theme(legend.position = "right")

Overlay maps on images

And just as with MapFeatures, we can add our H&E images to the plots:

MapLabels(se, 
          column_name = "seurat_clusters", 
          image_use = "raw", 
          override_plot_dims = TRUE, 
          pt_alpha = 0.6,
          colors = cluster_colors) +
  plot_layout(guides = "collect") &
  guides(fill = guide_legend(override.aes = list(size = 3), 
                             ncol = 2)) &
  theme(legend.position = "right")

Split labels

Sometimes it can get cluttered in the plot and difficult to see where each cluster is located in the tissue, especially when there are many clusters and some of them have very similar colors. In this case, MapLabels can split the data into separate panels, one for each label:

MapLabels(se, 
          column_name = "seurat_clusters", 
          split_labels = TRUE,
          colors = cluster_colors) +
  plot_layout(guides = "collect") &
  theme(legend.position = "right", 
        legend.title = element_blank(), 
        legend.margin = margin(-10,-10,-10, 0)) &
  guides(fill = guide_legend(override.aes = list(size = 3)))
## Warning: No section_number selected. Selecting section 1.

When you split data, you can only do it for one section which is why a warning is thrown and the first available section is selected. If you want to use a different section, you can specify which one to use with section_number:

MapLabels(se, 
          column_name = "seurat_clusters", 
          split_labels = TRUE, 
          section_number = 2, 
          ncol = 1,
          colors = cluster_colors) +
  plot_layout(guides = "collect") &
  theme(legend.position = "right")

Remove background

When mapping categorical features in split view it might be more useful to see the underlying image. If you set drop_na = TRUE, the background spots will be removed:

MapLabels(se, 
          column_name = "seurat_clusters", 
          split_labels = TRUE, 
          image_use = "raw", 
          drop_na = TRUE, 
          override_plot_dims = TRUE,
          colors = cluster_colors) +
  plot_layout(guides = "collect") &
  theme(legend.position = "right", 
        legend.title = element_blank(), 
        legend.margin = margin(-10, 10, -10, 10), 
        plot.title = element_blank(), 
        plot.margin = margin(0, 5, 5, 0)) &
  guides(fill = guide_legend(override.aes = list(size = 3)))
## Warning: No section_number selected. Selecting section 1.

Scale bars

To add a scalebar, simply set add_scalebar = TRUE and the scalebar should appear in the top right corner.

se_mbrain <- LoadImages(se_mbrain)
## 
## ── Loading H&E images ──
## 
##  Loading image from /Users/lovisafranzen/Library/Caches/org.R-project.R/R/renv/library/semla-41c099bc/R-4.4/aarch64-apple-darwin20/semla/extdata/mousebrain/spatial/tissue_lowres_image.jpg
##  Scaled image from 600x565 to 400x377 pixels
##  Saving loaded H&E images as 'rasters' in Seurat object
MapFeatures(se_mbrain, features = "nFeature_Spatial", pt_size = 1,
            override_plot_dims = TRUE, image_use = "raw", 
            add_scalebar = TRUE)
## Loading required namespace: ggfittext

And with mapLabels:

MapLabels(se_mbrain, column_name = "sample_id", pt_size = 1,
          override_plot_dims = TRUE, image_use = "raw", 
          add_scalebar = TRUE)

You can also tweak the appearance and position of the scalebar with scalebar_gg, scalebar_position and scalebar_height.

  • scalebar_gg allows you to modify the width and breaks of the scalebar. The title of the scalebar will be adjusted from micrometers to millimeters if x > 1000.
  • scalebar_position sets the position of the scalebar relative to the entire plot.
  • scalebar_height can be useful to adjust the height of the scalebar to make the text fit. If the scalebar is too small, for instance when plotting many sections at once, increasing the height will make the text appear.
MapLabels(se_mbrain, column_name = "sample_id", pt_size = 1,
          image_use = "raw", 
          add_scalebar = TRUE, 
          scalebar_gg = scalebar(x = 2000, breaks = 11, highlight_breaks = c(1, 6, 11)),
          scalebar_position = c(0.55, 0.7), 
          scalebar_height = 0.07)

Here’s another example with image masking:

# Mask image
se_mbrain <- MaskImages(se_mbrain)

MapFeatures(se_mbrain, features = VariableFeatures(se_mbrain)[1:6],
          image_use = "raw", 
          add_scalebar = TRUE, 
          scale_alpha = TRUE,
          override_plot_dims = TRUE,
          scalebar_gg = scalebar(x = 1000),
          scalebar_position = c(0.55, 0.88),
          scalebar_height = 0.14) &
  theme(plot.title = element_blank(), 
        legend.position = "right", legend.text = element_text(angle = 0))

NB: We will include a higher resolution version of the H&E image for the next example. This is only relevant for this tutorial and will not be necessary if you created the object using “tissue_hires_image.png” files.

# Download hires H&E images
he_img <- file.path("https://data.mendeley.com/public-files/datasets/kj3ntnt6vb",
                    "files/d97fb9ce-eb7d-4c1f-98e0-c17582024a40/file_downloaded")

# Replace images
se_mbrain <- ReplaceImagePaths(se_mbrain, paths = he_img)

If you want to crop the data, you might want to adjust the width scalebar to a more appropriate length.

se_mbrain <- LoadImages(se_mbrain, image_height = 1.5e3)
MapFeatures(se_mbrain, features = "Th", pt_size = 5,
            image_use = "raw", scale_alpha = TRUE,
            colors = RColorBrewer::brewer.pal(n = 11, name = "Spectral") |> rev(),
            crop_area = c(0.2, 0.6, 0.4, 0.75),
            scalebar_position = c(0.7, 0.8), scalebar_height = 0.08,
            add_scalebar = TRUE, scalebar_gg = scalebar(x = 400))

Rearrange patchworks

While MapLabels and MapFeatures do not provide arguments to sort samples in a specific order, this can be achieved by returning the data as a list of ggplot objects and define the order manually.

For instance, if we have plot two features and set return_plot_list = TRUE, we can rearrange the plots with wrap_plots:

p <- MapFeatures(se_mbrain, c("Th", "Calb2"), return_plot_list = TRUE)

wrap_plots(p$`1`$Calb2, p$`1`$Th)

If we have multiple sections, we can access the section-specific plots from the list. In the example below, the plots have been rearranged so that the columns and rows are flipped.

p <- MapFeatures(se, c("Th", "Calb2"), return_plot_list = TRUE)

wrap_plots(p$`2`$Calb2, p$`2`$Th, p$`1`$Th, p$`1`$Calb2)

Basic themes and plot annotation

semla also provides a couple of utility functions to make it easier to modify themes.

If you are plotting many features and sections at the same time, it can be useful to remove all components of the theme with ThemeClean to get a quick overview:

MapFeatures(se, c("Th", "Calb2", "Clu", "Mgp", "Acta2", "Tagln"), 
            override_plot_dims = TRUE, pt_size = 0.5) & 
  ThemeClean()

If you like to move the color legends to the right hand side of the plots, you can use ThemeLegendRight:

MapFeatures(se, c("Th", "Calb2"), 
            override_plot_dims = TRUE) & 
  ThemeLegendRight()

The titles of individual plots are determined internally by MapLabels and MapFeatures and it can be difficult to change these titles after the plot has been generated. ModifyPatchworkTitles can help with this task. All you need to do is to provide a character vector with new titles which should match the number of subplots in the patchwork. In the example below we have 2 genes and 2 sections, so we need to provide 4 titles:

p <- MapFeatures(se, c("Th", "Calb2"), override_plot_dims = TRUE)
ModifyPatchworkTitles(p, titles = c("First", "Second", "Third", "Fourth"))



Package version
  • semla: 1.1.6
Session info
## R version 4.4.0 (2024-04-24)
## Platform: aarch64-apple-darwin20
## Running under: macOS Sonoma 14.5
## 
## Matrix products: default
## BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0
## 
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
## 
## time zone: Europe/Stockholm
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices datasets  utils     methods   base     
## 
## other attached packages:
## [1] patchwork_1.3.0    tibble_3.2.1       semla_1.1.6        ggplot2_3.5.0     
## [5] dplyr_1.1.4        SeuratObject_4.1.4 Seurat_4.3.0.1    
## 
## loaded via a namespace (and not attached):
##   [1] RColorBrewer_1.1-3     rstudioapi_0.16.0      jsonlite_1.8.8        
##   [4] magrittr_2.0.3         magick_2.8.4           spatstat.utils_3.1-0  
##   [7] farver_2.1.2           rmarkdown_2.28         fs_1.6.4              
##  [10] ragg_1.3.3             vctrs_0.6.5            ROCR_1.0-11           
##  [13] spatstat.explore_3.3-2 forcats_1.0.0          htmltools_0.5.8.1     
##  [16] curl_5.2.2             sass_0.4.9             sctransform_0.4.1     
##  [19] parallelly_1.38.0      KernSmooth_2.23-24     bslib_0.8.0           
##  [22] htmlwidgets_1.6.4      desc_1.4.3             ica_1.0-3             
##  [25] plyr_1.8.9             plotly_4.10.4          zoo_1.8-12            
##  [28] cachem_1.1.0           ggfittext_0.10.2       igraph_2.0.3          
##  [31] mime_0.12              lifecycle_1.0.4        pkgconfig_2.0.3       
##  [34] Matrix_1.7-0           R6_2.5.1               fastmap_1.2.0         
##  [37] fitdistrplus_1.2-1     future_1.34.0          shiny_1.9.1           
##  [40] digest_0.6.37          colorspace_2.1-1       tensor_1.5            
##  [43] irlba_2.3.5.1          textshaping_0.4.0      labeling_0.4.3        
##  [46] progressr_0.14.0       fansi_1.0.6            spatstat.sparse_3.1-0 
##  [49] httr_1.4.7             polyclip_1.10-7        abind_1.4-8           
##  [52] compiler_4.4.0         withr_3.0.1            viridis_0.6.5         
##  [55] highr_0.11             MASS_7.3-60.2          tools_4.4.0           
##  [58] lmtest_0.9-40          httpuv_1.6.15          future.apply_1.11.2   
##  [61] goftest_1.2-3          glue_1.7.0             dbscan_1.2-0          
##  [64] nlme_3.1-164           promises_1.3.0         grid_4.4.0            
##  [67] Rtsne_0.17             cluster_2.1.6          reshape2_1.4.4        
##  [70] generics_0.1.3         gtable_0.3.5           spatstat.data_3.1-2   
##  [73] tidyr_1.3.1            data.table_1.16.0      sp_2.1-4              
##  [76] utf8_1.2.4             spatstat.geom_3.3-2    RcppAnnoy_0.0.22      
##  [79] ggrepel_0.9.6          RANN_2.6.2             pillar_1.9.0          
##  [82] stringr_1.5.1          spam_2.10-0            later_1.3.2           
##  [85] splines_4.4.0          lattice_0.22-6         renv_1.0.2            
##  [88] survival_3.6-4         deldir_2.0-4           tidyselect_1.2.1      
##  [91] miniUI_0.1.1.1         pbapply_1.7-2          knitr_1.48            
##  [94] gridExtra_2.3          scattermore_1.2        xfun_0.47             
##  [97] matrixStats_1.4.1      stringi_1.8.4          lazyeval_0.2.2        
## [100] yaml_2.3.10            evaluate_0.24.0        codetools_0.2-20      
## [103] BiocManager_1.30.25    cli_3.6.3              uwot_0.2.2            
## [106] xtable_1.8-4           reticulate_1.39.0      systemfonts_1.1.0     
## [109] munsell_0.5.1          jquerylib_0.1.4        Rcpp_1.0.13           
## [112] globals_0.16.3         spatstat.random_3.3-1  zeallot_0.1.0         
## [115] png_0.1-8              spatstat.univar_3.0-1  parallel_4.4.0        
## [118] pkgdown_2.1.0          dotCall64_1.1-1        listenv_0.9.1         
## [121] viridisLite_0.4.2      scales_1.3.0           ggridges_0.5.6        
## [124] leiden_0.4.3.1         purrr_1.0.2            rlang_1.1.4           
## [127] cowplot_1.1.3          shinyjs_2.1.0