Skip to contents

Abstract

Battlefield is a Swiss-army toolkit designed to define and extract spatial spots from specific regions in spatial transcriptomics data. It provides low-level, modular utilities to delineate spatial regions of interest, including interfaces between clusters, intra-cluster layers, and inter-cluster trajectories. These utilities are intended to be reused and composed within higher-level analytical workflows and packages. Battlefield supports sequencing-based spatial transcriptomics platforms such as 10x Genomics Visium, across multiple resolutions, including Visium HD (binned). Battlefield package version: 0.99.1

Introduction

Battlefield is a Swiss-army toolkit designed to define and extract spatial spots from specific regions in spatial transcriptomics data. It provides low-level, modular utilities to delineate spatial regions of interest,
including interfaces between clusters, intra-cluster layers, and
inter-cluster trajectories. These utilities are intended to be reused and composed within higher-level
analytical workflows and packages. Battlefield supports sequencing-based spatial transcriptomics platforms such as
10x Genomics Visium, across multiple resolutions, including
Visium HD (binned).

What is it for?

Battlefield provides four core functionalities to define and extract spatial transcriptomics spots from specific tissue regions, enabling the study of spatial organization under different biological contexts:

  • Interfaces between clusters — identify and analyze boundary regions where distinct tissue or cell populations interact.

  • Intra-cluster layers — characterize spatial layers or gradients within a single cluster.

  • Inter/Intra-cluster trajectories — model spatial transitions across multiple clusters.

  • Spatial neighborhood — report the cluster composition of a spatial neighborhood defined by k nearest spots, constrained by a distance threshold.

Finally, you can integrate Battlefield results back into a SpatialExperiment object for further analysis.

Starting point

We start from a SpatialExperiment object that contains (i) spatial coordinates for each spot and (ii) a precomputed clustering stored in colData().

Battlefield operates on a simple spot-level table, so we first export a data.frame with four columns:

  • spot_id: spot/barcode identifier (colnames(spe))
  • x, y: spatial coordinates (spatialCoords(spe))
  • cluster: cluster label provided by the user (a column in colData(spe))

Because the cluster annotation can have different names depending on the workflow (e.g., cluster, seurat_clusters, BayesSpace, etc.), the user can specify which colData() column should be used.

We load a simulated Visium dataset (hexagonal grid) to illustrate the different functionalities that Battlefield provides.

# Load Visium data 
data("visium_simulated_spe")

df <- data.frame(
spot_id = colnames(visium_simulated_spe),
x = spatialCoords(visium_simulated_spe)[, 1],
y = spatialCoords(visium_simulated_spe)[, 2],
cluster = colData(visium_simulated_spe)$cluster
)

# Plot cluster distribution
ggplot(df, aes(x, y, fill = cluster)) +
geom_point(size = 3.2,colour = "grey", shape = 21) +
coord_equal() +
theme_minimal()+
labs(x = "", y = "") +
theme(axis.text = element_blank()) 

You can have acces to some basic information about the dataset you are using (grid type, distance threshold (radius) advised…)

# Detect grid type and get parameters
res <- detect_grid_type(df, verbose = TRUE)
## ---- Grid type detection ----
## Estimated grid step: 55
## Median distance ratio: 1.732
## Detected grid type: hexagonal
## --------------------------------
params <- get_neighborhood_params(df, verbose = TRUE)
## ---- Neighborhood parameters ----
## grid_type: hexagonal
## connectivity: 6
## radius: 55.55
## k: 6
## comment: Standard Visium: 6 hexagonal neighbors
## --------------------------------

Interfaces between clusters

Battlefield allows you to identify and extract spatial interfaces between clusters. Here we demonstrate how to:

  1. Detect interfaces between two specific clusters (according to three modes of selection called inner, outer or both)
  2. Identify multi-interface spots all at once
  3. Detect a same number of control spots relative to the cluster of interest

Battlefield offers several functionalities related to interfaces (aka borders) via the following fonctions metionned below:

  • select_border_spots, between two clusters according to a specified mode
  • build_all_borders
  • select_core_spots , to select control spots within a cluster relative to its border
  • build_all_cores

You must have first defined borders before selecting core spots.

Here we show an example to detect interfaces between clusters 4 and 5. We identify border spots from cluster 4 that are adjacent to cluster 5, referenced as interface for the adjactent cluster.
We used the inner mode to select spots, meaning that only spots from cluster 4 that are located in the inside of the cluster towards cluster 5 (the interface) are selected.

Note : we used a max_dist = 60 microns to define the spatial neighborhood to remove spots that would have been too far away to be considered as neighbors.

border_in <- select_border_spots(df, 
    cluster = 4, 
    interface = 5,
    mode = "inner",
    max_dist = 60)
# A similar approach would have been 
# select_border_spots(df, 
#   cluster = 5, 
#   interface = 4,
#   mode = "outer",
#   max_dist = 60)
knitr::kable(head(border_in))
spot_id x y directed_pair undirected_pair cluster interface is_border is_border_multiple other_adjacent_borders mode
647 SPOT0647 330 762.1024 4-5 4-5 / 5-4 4 5 TRUE FALSE NA inner
648 SPOT0648 385 762.1024 4-5 4-5 / 5-4 4 5 TRUE FALSE NA inner
649 SPOT0649 440 762.1024 4-5 4-5 / 5-4 4 5 TRUE FALSE NA inner
650 SPOT0650 495 762.1024 4-5 4-5 / 5-4 4 5 TRUE FALSE NA inner
651 SPOT0651 550 762.1024 4-5 4-5 / 5-4 4 5 TRUE FALSE NA inner
652 SPOT0652 605 762.1024 4-5 4-5 / 5-4 4 5 TRUE FALSE NA inner
# Structre data 
df_in <- df |>
mutate(is_border = spot_id %in% border_in$spot_id) |>
left_join(border_in |> 
select(spot_id, is_border_multiple),by = "spot_id") |>
mutate(is_border_multiple = coalesce(is_border_multiple, FALSE))


# Plot interface 5 → 4
ggplot(df_in, aes(x, y, fill = cluster)) +
geom_point(size = 3.2, shape = 21, colour = "grey") +
geom_point(data = subset(df_in, is_border & !is_border_multiple),
          size = 3.2,  colour = "black", fill = NA) +
geom_point(data = subset(df_in, is_border & is_border_multiple),
          size = 3.2,  colour = "red", fill = NA) +
coord_equal() +
theme_minimal()+
labs(x = "", y = "") +
theme(axis.text = element_blank()) 

We can also identify all interfaces at once and visualize spots that are part of multiple interfaces. Here we used again a max_dist = 60 microns and k = 6 neighbors to define the spatial neighborhood.

is_border_multiple indicates whether a spot is part of multiple interfaces and is plotted with a different alpha in the example.

This approach needs further manual selection of the cluster pair of interest as it brings some redundacy. (for example, border spots for both directions 3→4 and 4→3 are identified twice due to sequencial call of select_border_spots with the two modes (inner/outer)).

all_borders <- build_all_borders(df,max_dist = 60 , k = 6)
knitr::kable(head(all_borders))
spot_id x y directed_pair undirected_pair cluster interface is_border is_border_multiple other_adjacent_borders mode
SPOT0299 1017.5 333.4198 1-3 1-3 / 3-1 1 3 TRUE FALSE NA inner
SPOT0300 1072.5 333.4198 1-3 1-3 / 3-1 1 3 TRUE FALSE NA inner
SPOT0301 1127.5 333.4198 1-3 1-3 / 3-1 1 3 TRUE FALSE NA inner
SPOT0302 1182.5 333.4198 1-3 1-3 / 3-1 1 3 TRUE FALSE NA inner
SPOT0338 935.0 381.0512 1-3 1-3 / 3-1 1 3 TRUE FALSE NA inner
SPOT0339 990.0 381.0512 1-3 1-3 / 3-1 1 3 TRUE FALSE NA inner
# Structure data 
df2 <- df |>
left_join(all_borders |> 
select(spot_id, undirected_pair, is_border_multiple),by = "spot_id") |>
mutate(is_border = !is.na(undirected_pair),
    is_border_multiple = coalesce(is_border_multiple, FALSE))

# Plot all interfaces
ggplot(df2, aes(x, y)) +
geom_point(data = subset(df2, !is_border),
           fill="grey" ,colour = "white", size = 3.2,shape = 21) +
geom_point(data = subset(df2, is_border & is_border_multiple),
            aes(color = undirected_pair), size = 3.2, alpha = 0.4) +
geom_point(data = subset(df2, is_border & !is_border_multiple),
            aes(color = undirected_pair), size = 3.2) +
coord_equal() +
theme_minimal()+
labs(x = "", y = "",color = "") +
theme(axis.text = element_blank()) 

Next, we plot the different types of spots identified for the interface between clusters 3 and 4. We identify border spots for both directions (3→4 and 4→3) and a relative selection of core spots within cluster 3 that can be used as control spots for further analysis.

# Get all pairs and detect grid
pairs_visium <- directed_cluster_interface_pairs(df$cluster)
params_visium <- get_neighborhood_params(df, verbose = FALSE)

# Define cluster pair of interest
cluster_A <- 3
cluster_B <- 4

# Build all borders to identify inner spots
all_borders_visium <- build_all_borders(df, k = 6, pairs = pairs_visium)

# Select border spots for both directions
border_A_to_B <- subset(all_borders_visium,
cluster == cluster_A & interface == cluster_B
)

border_B_to_A <- subset(all_borders_visium,
cluster == cluster_B & interface == cluster_A
)


# Select inner spots for cluster A
inner_A <- select_core_spots(df, all_borders_visium, 
                            cluster = cluster_A, 
                            interface = cluster_B, 
                            mode = "both")

# Create visualization dataframe with spot classification
df_final <- df |>
mutate(spot_type = "background") |>
mutate(spot_type = ifelse(spot_id %in% border_A_to_B$spot_id, 
                            "border_A_to_B", spot_type)) |>
mutate(spot_type = ifelse(spot_id %in% border_B_to_A$spot_id, 
                            "border_B_to_A", spot_type)) |>
mutate(spot_type = ifelse(spot_id %in% inner_A$spot_id, 
                            "inner_A", spot_type)) |>
as.data.frame()


# Plot: clusters with borders and core control spots
ggplot(df_final, aes(x, y)) +
# 1) Base clusters           fill="gray" ,colour = "grey", size = 3.2,shape = 21) + # nolint: line_length_linter.
geom_point(aes(color = cluster), size = 2.5) +
#  2) Draw outlines (slightly larger) so they "surround" without hiding
geom_point(data = subset(df_final, spot_type == "border_A_to_B"), 
  color = "blue", size = 2.5, shape = 1, stroke = 1.25) +
geom_point(data = subset(df_final, spot_type == "border_B_to_A"),
  color = "red", size = 2.5, shape = 1, stroke = 1.25) +
geom_point(data = subset(df_final, spot_type == "inner_A"), 
  color = "black", size = 2.5, shape = 1, stroke = 1.25) +
  # 3) Re-draw the colored points ON TOP for the highlighted subset
geom_point(
  data = subset(df_final, spot_type %in% c("border_A_to_B", "border_B_to_A", "inner_A")),
  aes(color = cluster),
  size = 2.5
) +
coord_equal() +
theme_minimal() +
labs(x = "", y = "",color = "") +
theme(axis.text = element_blank())

Intra-cluster layers

You can directly work on a specific cluster to define three layers as core, intermediate and border.
intermediate_quantile allows to control the thickness of the intermediate layer.

target_cluster <- 3
# Narrow intermediate layer
layers_df <- create_cluster_layers(df, 
              target_cluster = target_cluster, 
              intermediate_quantile = 0.33)
# Create visualization dataframe
df_viz <- df |>
mutate(layer = NA_character_) |>
as.data.frame()

# Map layers to the full dataset
idx_match <- match(layers_df$spot_id, df_viz$spot_id)
valid_idx <- !is.na(idx_match)
df_viz$layer[idx_match[valid_idx]] <- layers_df$layer[valid_idx]

# Create combined plot: clusters in background + layers overlay with circles
ggplot(df_viz, aes(x, y)) +
# base clusters
geom_point(aes(color = cluster), size = 2.5) +
# 2) Draw outlines (slightly larger) so they "surround" without hiding
geom_point(
    data = subset(df_viz, layer == "core"),
    shape = 1, color = "#DDDDDD",
    size = 3.2, stroke = 1.25
) +
geom_point(
    data = subset(df_viz, layer == "intermediate"),
    shape = 1, color = "#666666",
    size = 3.2, stroke = 1.25
) +
geom_point(
    data = subset(df_viz, layer == "border"),
    shape = 1, color ="black",
    size = 3.2, stroke = 1.25
) +
# 3) Re-draw the colored points ON TOP for the highlighted subset
geom_point(
    data = subset(df_viz, layer %in% c("core", "intermediate", "border")),
    aes(color = cluster),
    size = 2.5
)  +
coord_equal() +
theme_minimal()+  labs(
  x = "", y = "", color = ""
) +
theme(
  axis.text = element_blank()
) 

Inter/Intra-cluster trajectories

We can also identify spatial trajectories between two spots from two cluster or inside a single cluster.

Here we demonstrate how to build a trajectory between clusters 3 and 4 using build_one_trajectory().

# We retrieve expression for later visualization
expr <- as.numeric(assay(visium_simulated_spe, "counts")["FAKE_GENE", ])
test <- cbind(as.data.frame(colData(visium_simulated_spe)), df[, c("x", "y"), drop = FALSE])
test$expr <- expr

start_cluster <- "3"
end_cluster   <- "4"
top_n <- 8

centroids <- compute_centroids(df)

A <- centroids[centroids$cluster == start_cluster, c("x","y")]
B <- centroids[centroids$cluster == end_cluster,   c("x","y")]

res <- build_one_trajectory(df, A, B, top_n = top_n, max_dist = NULL)

knitr::kable(head(res))
spot_id x y cluster dist_to_seg pos_on_seg trajectory_id
SPOT0220 1072.5 238.1570 3 11.5795728 0.0027532 main
SPOT0300 1072.5 333.4198 1 8.6442774 0.1480379 main
SPOT0380 1072.5 428.6826 1 5.7089820 0.2933227 main
SPOT0460 1072.5 523.9454 1 2.7736866 0.4386074 main
SPOT0540 1072.5 619.2082 1 0.1616088 0.5838921 main
SPOT0620 1072.5 714.4710 1 3.0969042 0.7291769 main
# Plot cluster distribution
ggplot(test, aes(x, y, fill = cluster)) +
geom_point(size = 3.2,colour="grey", shape = 21) +
geom_path(data=res, aes(x=x, y=y, group=trajectory_id), 
  color="black",linewidth=2) +
geom_point(data = res, aes(x, y), 
  size = 3.2, fill="red",colour="black", shape = 21) +
coord_equal() +
theme_minimal() +
labs(x = "", y = "") +
theme(axis.text = element_blank())

But the real interest here is to identify multiple similar trajectories for a fixed number of spots and trajectories to explore possible gene expression gradients using build_similar_trajectories().

Here we build two extra trajectories on each side of the main trajectory.
lane_factor allows to control the spacing between trajectories. The number of trajectories returned is n_extra according to the side parameter setting.

out <- build_similar_trajectories(df, A, B, 
top_n = top_n, 
n_extra = 2, 
lane_width_factor = 2.5,
side = "both")
knitr::kable(head(out))
spot_id x y cluster dist_to_seg pos_on_seg trajectory_id offset
SPOT0220 1072.5 238.1570 3 11.5795728 0.0027532 main 0
SPOT0300 1072.5 333.4198 1 8.6442774 0.1480379 main 0
SPOT0380 1072.5 428.6826 1 5.7089820 0.2933227 main 0
SPOT0460 1072.5 523.9454 1 2.7736866 0.4386074 main 0
SPOT0540 1072.5 619.2082 1 0.1616088 0.5838921 main 0
SPOT0620 1072.5 714.4710 1 3.0969042 0.7291769 main 0
ggplot(test, aes(x, y)) +
geom_point(aes(color = expr),size = 1.6, alpha = 0.85) +
scale_color_gradient(low = "blue",high = "red") +
geom_path(data=out, aes(x=x, y=y, group=trajectory_id), 
inherit.aes = FALSE, color="black",linewidth=1,
arrow = grid::arrow(type = "closed", length = grid::unit(2, "mm"))) +
geom_point(data = out, aes(x, y), 
inherit.aes = FALSE, size = 2.2, fill="white",color="black") +
coord_equal() +
theme_minimal() +
labs(x = "", y = "") +
theme(axis.text = element_blank())

We can now explore quickly the expression of this fake gene via an heatmap.

meta <- out|>
transmute(
    spot_id = as.character(spot_id),
    trajectory = as.character(trajectory_id),
    progress = as.numeric(pos_on_seg)
) |>
group_by(trajectory) |>
arrange(progress, .by_group = TRUE) |>
mutate(
    index = seq_len(n())   # <- index 1..length ordered according to t
) |>
ungroup()

gene <- c("FAKE_GENE") 
expr <- assay(visium_simulated_spe, "counts")[gene, meta$spot_id]
meta$expr <- expr

mat <- meta |>
select(trajectory, index, expr) |>
tidyr::pivot_wider(names_from = index, values_from = expr) |>
as.data.frame()

rownames(mat) <- mat$trajectory
mat$trajectory <- NULL

pheatmap(
mat,
cluster_rows = FALSE,
cluster_cols = FALSE,
border_color = "white",
main = gene,angle_col=0,
col=pals::coolwarm(), scale="row",
cellwidth=25, cellheight=25 , na_col = "grey90"
)

Spatial neighborhood

Battlefield also provides a utility to report the cluster composition of a spatial neighborhood defined by k nearest spots, constrained by a distance threshold. It can used a cluster or a spot of interest.
There is three major functions :

  • get_neighborhood_spots : get the spots in the neighborhood of a cluster
  • count_neighborhood : count the composition of a neighborhood for a cluster
  • count_all_neighborhoods : count the composition of neighborhoods for all clusters

By default, it will report clusters present in the neighborhood but you can provide another column name with inlaid_col parameter to report other annotations.

# Getting neighborhoods...
neighborhood_spots_1 <- get_neighborhood_spots(df, cluster = 1, k = 200)
head(neighborhood_spots_1)
##           spot_id      x        y cluster neighborhood is_neighborhood
## SPOT0465 SPOT0465 1347.5 523.9454       1            2            TRUE
## SPOT0466 SPOT0466 1402.5 523.9454       1            2            TRUE
## SPOT0467 SPOT0467 1457.5 523.9454       1            2            TRUE
## SPOT0468 SPOT0468 1512.5 523.9454       1            2            TRUE
## SPOT0506 SPOT0506 1375.0 571.5768       1            2            TRUE
## SPOT0507 SPOT0507 1430.0 571.5768       1            2            TRUE
# Counting the clusters in the neighborhoods...
neighb_vis <- count_neighborhood(df, cluster = 1, k = 100)
head(neighb_vis)
##   cluster neighborhood count proportion
## 1       1            4    49       0.49
## 2       1            3    45       0.45
## 3       1            2     6       0.06

We simulated another another annotation to create inlaid black spots that we will count in the neighborhood of cluster 1 just after the plot.

neighb_vis <- count_all_neighborhoods(df,  k = 100)

# Add inlaid column with random values (5 categories)
df$inlaid <- sample(paste0("inlaid", 1:5), nrow(df), replace = TRUE)

df_neighborhood_viz <- df |>
  mutate(
    spot_type = "background",
    spot_type = ifelse(cluster == 1, "source", spot_type),
    spot_type = ifelse(spot_id %in% neighborhood_spots_1$spot_id,
                       "neighborhood", spot_type)
  ) |>
  as.data.frame()


# Apply the same cluster factor levels as df_vis
df_neighborhood_viz$cluster <- factor(df_neighborhood_viz$cluster,
                                      levels = sort(unique(df$cluster)))

ggplot(df_neighborhood_viz, aes(x, y)) +
  geom_point(
    data = subset(df_neighborhood_viz, spot_type == "neighborhood"),
    shape = 1,
    color = "grey",
    size = 3.2,
    stroke = 1.25
  ) +
  geom_point(
    aes(color = cluster),
    size = 2.5
  ) +
    geom_point(
    data = subset(df_neighborhood_viz, inlaid == "inlaid1"),
    fill = "black",
    size = 2.5
  ) +
  coord_equal() +
  theme_minimal() +
  labs(x = "", y = "") +
  theme(axis.text = element_blank()
  )

# Counting the black point -inlaid spots- in the neighborhood of cluster  1
neighb_vis <- count_neighborhood(df, cluster = 1, inlaid_col = "inlaid",k = 100)
head(neighb_vis)
##   cluster neighborhood count proportion
## 1       1      inlaid2    26       0.26
## 2       1      inlaid4    25       0.25
## 3       1      inlaid5    19       0.19
## 4       1      inlaid1    15       0.15
## 5       1      inlaid3    15       0.15

We previously count these point in the neighborhood of cluster 1. We can count the black spots directly inside cluster 1.

For this purpose, three major functions are available:

  • get_inlaid_spots : get the inlaid spots inside a cluster
  • count_inlaid : count the composition of inlaid spots for a cluster
  • count_all_inlaids : count the composition of inlaid spots for all clusters
# === Testing get_inlaid_spots  
inlaid_spots_1 <- get_inlaid_spots(df, cluster = 1, inlaid_col = "inlaid")

# === Testing count_inlaid ===
inlaid_1 <- count_inlaid(df, cluster = 1, inlaid_col = "inlaid")

# === Testing count_all_inlaids ===
all_inlaids <- count_all_inlaids(df, inlaid_col = "inlaid")
knitr::kable(head(all_inlaids, 15))
cluster inlaid count proportion
1 inlaid2 19 0.2345679
1 inlaid3 19 0.2345679
1 inlaid1 17 0.2098765
1 inlaid4 13 0.1604938
1 inlaid5 13 0.1604938
2 inlaid1 13 0.2765957
2 inlaid3 11 0.2340426
2 inlaid2 10 0.2127660
2 inlaid4 7 0.1489362
2 inlaid5 6 0.1276596
3 inlaid2 100 0.2336449
3 inlaid1 87 0.2032710
3 inlaid4 86 0.2009346
3 inlaid5 83 0.1939252
3 inlaid3 72 0.1682243

Ending point

You can finaly integrate Battlefield results back into a SpatialExperiment object for further analysis :

visium_simulated_spe <- add_borders_to_spe(visium_simulated_spe,
  border = all_borders)
head(colData(visium_simulated_spe))
## DataFrame with 6 rows and 7 columns
##           barcode_id  cluster   sample_id is_border   is_core   interface
##          <character> <factor> <character> <logical> <logical> <character>
## SPOT0001    SPOT0001        3    sample01     FALSE     FALSE          NA
## SPOT0002    SPOT0002        3    sample01     FALSE     FALSE          NA
## SPOT0003    SPOT0003        3    sample01     FALSE     FALSE          NA
## SPOT0004    SPOT0004        3    sample01     FALSE     FALSE          NA
## SPOT0005    SPOT0005        3    sample01     FALSE     FALSE          NA
## SPOT0006    SPOT0006        3    sample01     FALSE     FALSE          NA
##          border_mode
##          <character>
## SPOT0001          NA
## SPOT0002          NA
## SPOT0003          NA
## SPOT0004          NA
## SPOT0005          NA
## SPOT0006          NA
visium_simulated_spe <- add_layers_to_spe(visium_simulated_spe, 
  layer = layers_df)
head(colData(visium_simulated_spe))
## DataFrame with 6 rows and 8 columns
##           barcode_id  cluster   sample_id is_border   is_core   interface
##          <character> <factor> <character> <logical> <logical> <character>
## SPOT0001    SPOT0001        3    sample01     FALSE     FALSE          NA
## SPOT0002    SPOT0002        3    sample01     FALSE     FALSE          NA
## SPOT0003    SPOT0003        3    sample01     FALSE     FALSE          NA
## SPOT0004    SPOT0004        3    sample01     FALSE     FALSE          NA
## SPOT0005    SPOT0005        3    sample01     FALSE     FALSE          NA
## SPOT0006    SPOT0006        3    sample01     FALSE     FALSE          NA
##          border_mode       layer
##          <character> <character>
## SPOT0001          NA        core
## SPOT0002          NA        core
## SPOT0003          NA        core
## SPOT0004          NA        core
## SPOT0005          NA        core
## SPOT0006          NA        core
visium_simulated_spe <- add_trajectories_to_spe(visium_simulated_spe, 
  trajectory = out)
head(colData(visium_simulated_spe))
## DataFrame with 6 rows and 12 columns
##           barcode_id  cluster   sample_id is_border   is_core   interface
##          <character> <factor> <character> <logical> <logical> <character>
## SPOT0001    SPOT0001        3    sample01     FALSE     FALSE          NA
## SPOT0002    SPOT0002        3    sample01     FALSE     FALSE          NA
## SPOT0003    SPOT0003        3    sample01     FALSE     FALSE          NA
## SPOT0004    SPOT0004        3    sample01     FALSE     FALSE          NA
## SPOT0005    SPOT0005        3    sample01     FALSE     FALSE          NA
## SPOT0006    SPOT0006        3    sample01     FALSE     FALSE          NA
##          border_mode       layer trajectory_id    offset pos_on_seg dist_to_seg
##          <character> <character>   <character> <numeric>  <numeric>   <numeric>
## SPOT0001          NA        core            NA        NA         NA          NA
## SPOT0002          NA        core            NA        NA         NA          NA
## SPOT0003          NA        core            NA        NA         NA          NA
## SPOT0004          NA        core            NA        NA         NA          NA
## SPOT0005          NA        core            NA        NA         NA          NA
## SPOT0006          NA        core            NA        NA         NA          NA

Session Information

## R version 4.5.2 (2025-10-31)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.3 LTS
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
## 
## locale:
##  [1] LC_CTYPE=C.UTF-8       LC_NUMERIC=C           LC_TIME=C.UTF-8       
##  [4] LC_COLLATE=C.UTF-8     LC_MONETARY=C.UTF-8    LC_MESSAGES=C.UTF-8   
##  [7] LC_PAPER=C.UTF-8       LC_NAME=C              LC_ADDRESS=C          
## [10] LC_TELEPHONE=C         LC_MEASUREMENT=C.UTF-8 LC_IDENTIFICATION=C   
## 
## time zone: UTC
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] grid      stats4    stats     graphics  grDevices utils     datasets 
## [8] methods   base     
## 
## other attached packages:
##  [1] pals_1.10                   pheatmap_1.0.13            
##  [3] tidyr_1.3.2                 dplyr_1.1.4                
##  [5] ggplot2_4.0.1               SpatialExperiment_1.20.0   
##  [7] SingleCellExperiment_1.32.0 SummarizedExperiment_1.40.0
##  [9] Biobase_2.70.0              GenomicRanges_1.62.1       
## [11] Seqinfo_1.0.0               IRanges_2.44.0             
## [13] S4Vectors_0.48.0            BiocGenerics_0.56.0        
## [15] generics_0.1.4              MatrixGenerics_1.22.0      
## [17] matrixStats_1.5.0           Battlefield_0.99.01        
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6        rjson_0.2.23        xfun_0.55          
##  [4] bslib_0.9.0         lattice_0.22-7      vctrs_0.6.5        
##  [7] tools_4.5.2         tibble_3.3.1        pkgconfig_2.0.3    
## [10] Matrix_1.7-4        RColorBrewer_1.1-3  S7_0.2.1           
## [13] desc_1.4.3          lifecycle_1.0.5     compiler_4.5.2     
## [16] farver_2.1.2        textshaping_1.0.4   mapproj_1.2.12     
## [19] htmltools_0.5.9     maps_3.4.3          sass_0.4.10        
## [22] yaml_2.3.12         pillar_1.11.1       pkgdown_2.2.0      
## [25] jquerylib_0.1.4     DelayedArray_0.36.0 cachem_1.1.0       
## [28] magick_2.9.0        abind_1.4-8         tidyselect_1.2.1   
## [31] digest_0.6.39       purrr_1.2.1         labeling_0.4.3     
## [34] fastmap_1.2.0       colorspace_2.1-2    cli_3.6.5          
## [37] SparseArray_1.10.8  magrittr_2.0.4      S4Arrays_1.10.1    
## [40] dichromat_2.0-0.1   withr_3.0.2         scales_1.4.0       
## [43] rmarkdown_2.30      XVector_0.50.0      RANN_2.6.2         
## [46] ragg_1.5.0          evaluate_1.0.5      knitr_1.51         
## [49] rlang_1.1.7         Rcpp_1.1.1          glue_1.8.0         
## [52] jsonlite_2.0.0      R6_2.6.1            systemfonts_1.3.1  
## [55] fs_1.6.6