Preparing and exploring spatial data in Spatial Studio
Last updated: July 31, 2026
Written by Caitlin Winkler, Ph.D.
Spatial Studio is where you visualize and interrogate your spatial transcriptomics data in Pluto. This guide covers what to upload, what Pluto does to your data, and how to read every statistic in the viewer.

Spatial Studio demo: viewing gene expression and using the lasso feature to perform differential expression analysis on two custom groups
Before you upload
Spatial Studio is a visualization and exploration tool. You bring a fully processed dataset and Pluto displays it faithfully, adds standard exploratory statistics, and lets you annotate your clusters. Pluto does not re-process your data or make analytical decisions for you.
Your spatial dataset must adhere to the following format requirements:
Requirement | |
|---|---|
Format | A single AnnData |
Expression | Filtered, normalized, and log-transformed values in |
Clustering | A cluster assignment column in |
Coordinates | Cell or spot coordinates in |
Samples | One sample per experiment. Split a multi-sample dataset into separate experiments. |
NOTE: Gene symbols in var are recommended so genes display by name. If your index holds Ensembl IDs and var has a symbol column, Pluto will swap symbols into the index and keep the original IDs.
Supported platforms. 10x Genomics Visium, 10x Genomics Xenium, Vizgen MERFISH, Slide-seq, and generic H5AD. You confirm the platform during the mapping step.
Not accepted. FASTQ or other raw sequencing data, and raw count matrices such as a 10x .h5 feature-barcode matrix. Spatial Studio needs processed data.
Tissue images. If your object embeds its image, or carries coordinates only, an image upload is optional and your data renders as a coordinate plot. If your object references an image file that is not present, you will be prompted to upload one. Accepted formats are OME-ZARR, OME-TIFF (.ome.tif / .ome.tiff), and SpatialData ZARR.
If a region’s image is missing when you expected one, Pluto verifies each image pyramid is complete before publishing it and discards any that fails, so the region falls back to a coordinate plot rather than showing a broken image.
Re-running the pipeline usually resolves it. If it does not, contact us with the dataset ID rather than re-uploading.
Three things worth checking before you save
These are the most common reasons an upload behaves differently than expected. All three are avoidable in one line of your own code.
Clustering has to be in the object
Spatial Studio requires a cluster column and will not generate one. This is deliberate: clusters Pluto invented would look like yours and could be misread. Cluster your data in your own workflow first.
TIP: Your clusters do not need labels before upload. Numbered clusters are fine, and you can annotate them in Pluto later.
Save un-scaled values in X
Many scanpy workflows end with sc.pp.scale(), which centers expression and leaves negative values in X. Negative values cannot be log-normalized, so Pluto will not use that matrix. Instead it looks for your pre-scaling values in this order:
adata.rawadata.layers['counts']a single layer that reads as log-normalized
If it finds one, your upload succeeds and Spatial Studio tells you which matrix your displayed values came from. If it finds nothing usable, the upload fails with a clear message rather than showing you broken numbers.
To avoid the ambiguity, save with un-scaled log-normalized values in X, keeping the scaled matrix in a layer if you want it. Setting adata.raw before you scale also works, and the standard scanpy tutorial already does this.
Decide whether you want your full gene panel
The standard highly variable gene workflow does adata.raw = adata and then subsets to the top few thousand genes. If you save after that step, your uploaded object holds a fraction of your panel, and a gene missing from the truncated list looks exactly like a gene that was not detected.
Spatial Studio flags this in the header when it can tell. If you want your whole panel searchable, save without the HVG subset. Your clustering and embeddings are unaffected either way.
What Pluto does to your data
The ingestion pipeline
Validates your object: coordinates present, a cluster column present, readable expression.
Guarantees a normalized layer. It detects whether your expression is raw counts, normalized, or log-normalized, and produces - if needed - one consistent log-normalized layer that every plot and statistic reads from.
Records what it did, so the viewer can report it back to you.
Computes marker genes for each of your clusters, using a standard one-versus-rest Wilcoxon test.
Computes spatial statistics (Moran’s I) and builds the per-section spatial neighbor graph.
Packages a bundle so the viewer loads quickly.
What Pluto does not do
Does not cluster your data. Clusters come from you.
Does not assign cell types. You label your clusters on the Annotations page, with CellKB assistance if you want it.
Does not re-normalize data that is already normalized, and does not choose a normalization strategy for you beyond a standard default.
Does not recompute clusters, markers, or normalization inside the viewer. Spatial Studio reads what the pipeline produced.
Normalization, precisely
The pipeline acts on the state of your data, not on the platform label:
Raw counts are normalized to 10,000 counts per cell (CP10K) and log-transformed.
Already normalized data is only log-transformed.
Already log-normalized data is passed through unchanged.
Detection does not assume a particular scale. If you normalized to counts-per-million, or to your own dataset’s median library size, your data is recognized as already normalized rather than being normalized a second time.
What the viewer tells you about your data
The top bar of Spatial Studio carries three disclosures, so you never have to guess what happened to your values.
The gene count
The header states how many genes are in your dataset. If Pluto can tell that your object was a subset of a wider panel, it flags that and states the source panel width.
The absence of a flag is not confirmation that your panel is complete.
If you subset your genes without setting adata.raw, Pluto has no way to know, so it says nothing rather than implying otherwise. If your panel size looks wrong to you, trust your own expectation and check your save step.
The normalization label
What you see | What it means |
|---|---|
Normalized before upload | These are your own log-normalized values, passed through untouched. Pluto did not re-normalize them. |
log1p applied by Pluto | Your object was library-size normalized but not log-transformed, so Pluto applied |
CP10K + log1p applied by Pluto | Your object contained raw counts, so Pluto normalized to 10,000 counts per cell and log-transformed. |
Normalization not recorded | This dataset was processed before Pluto tracked this. Re-running the pipeline will record it. |

TIP: Hover over the label for the full explanation. It turns amber only when there is something worth a second look.
When your values came from another matrix
If your uploaded X was scaled, the viewer states which matrix your displayed values came from. Two cases are distinguished:
Recovered from
adata.raworlayers['counts']. You put your data where the AnnData convention says it goes, so Pluto used it. Stated plainly.Recovered from an inferred layer. Neither conventional location existed, so Pluto identified the one layer holding log-normalized values and used it. No convention names that layer, so this is flagged and asks you to confirm it is the matrix you meant to view.
The statistics in Spatial Studio
Every statistic uses a standard, published method, computed on your normalized data, with multiple-testing correction wherever p-values are reported.
Feature | Method | Computed by |
|---|---|---|
Marker genes | Wilcoxon rank-sum, one cluster versus the rest (scanpy | Pipeline |
Differential expression | Wilcoxon rank-sum on the normalized layer, BH q-values, volcano plot. Only genes detected in at least 10 percent of cells in one of the two groups are tested, and the panel reports how many were | On demand |
Pathway enrichment | Enrichr (GO Biological Process, KEGG, MSigDB Hallmark), species-aware, using your dataset’s own gene panel as the background | On demand |
Spatial autocorrelation | Moran’s I on the highly variable genes, using a per-section spatial neighbor graph | Pipeline |
Neighborhood enrichment | Permutation-tested z-scores of cluster-pair spatial adjacency, on the pipeline’s per-section graph | On demand |
Co-expression | Pearson correlation of two genes across spots, with a dual-gene overlay | On demand |
Reading Moran’s I
Moran’s I measures whether a gene’s expression is spatially structured. The graph is built per tissue section, so cells in two physically separate sections are never treated as neighbors even if their coordinates overlap.
Near +1: expression is spatially clustered, nearby locations are similar.
Near 0: no spatial structure, essentially random.
Negative: a dispersed, checkerboard-like pattern. Rarer.
Differential expression: one behavior to know about
Differential expression tests a gene only if it is detected in at least 10 percent of the cells in one of your two groups. This is the Seurat FindMarkers default, and it keeps the false-discovery correction over genes that were genuinely tested.
The trade-off is worth knowing before you hit it:
a gene expressed strongly in only a small subpopulation inside a larger selection will not appear in your results at all.
It is absent, not listed as non-significant. If you lasso a large region containing a rare immune infiltrate, that infiltrate’s markers can be filtered out before testing.
The volcano panel tells you how much of your panel this affected. Its header reads, for example, 1,203 of 5,000 genes tested, so you can see at a glance whether most of your genes were tested or most were filtered. If a selection produces nothing testable at all, the plot says “No genes passed the detection floor” rather than appearing broken. The method tooltip on the same panel states the rule.
If you are looking for markers of a rare population, select that population directly rather than the region containing it. Its markers will then be broadly detected within your selection and will come through.
Differential expression and enrichment are exploratory
Both are built for fast hypothesis generation while you explore. The methods are standard and the results are sound for interrogating your data. For a publication or a regulatory filing, confirm key findings in a dedicated, fully specified analysis, as you would with any exploratory tool.
A note on imaging platforms
When Pluto normalizes raw counts itself, it uses total-count normalization. On imaging-based assays such as Xenium and MERFISH, total-count normalization can be confounded by cell size, because a larger cell yields more transcripts regardless of biology. Atta et al., Genome Biology 2024, “Gene count normalization in single-cell imaging-based spatially resolved transcriptomics”, documents how this bias propagates into differential expression calls, fold changes, and spatially variable gene detection.
Spatial Studio surfaces this note in the viewer when it applies. Pluto does not choose a non-count-based normalization on your behalf. If you want one, apply it before upload and Pluto will respect your values.
Where your data goes
Your expression data and samples stay in Pluto. The one exception is pathway enrichment, which sends the gene symbol list from a differential expression result to Enrichr, an external web service. Only gene symbols are sent. Expression values and sample data never leave Pluto. If your organization has strict data-handling requirements, this is the one feature to review.
Naming and coloring your clusters
Whatever clustering you upload becomes an annotation set in Pluto: either numbered clusters or your existing annotations, depending on what was in your object and what you selected during mapping.
On the Annotations page you can:
Label or relabel every cluster and set its color.
Use the CellKB integration to suggest cell-type labels from each cluster’s top marker genes.
Any change you make on the Annotations page flows into Spatial Studio automatically, so the viewer always reflects your current curation. Spatial Studio displays your labels and never runs its own annotation.
FAQs
Do you re-normalize or re-cluster my data?
Never re-cluster; clustering must come from you. For normalization, Pluto guarantees one consistent log-normalized layer: already-normalized data is only log-transformed, already log-normalized data is passed through, and only raw counts get CP10K plus log1p. Whichever happened is stated in the viewer’s top bar.
What does the normalization label mean?
It reports what the pipeline did to your values. See the table above. “Normalization not recorded” means the dataset predates Pluto tracking this, and re-running the pipeline will fill it in.
Why does a gene from my rare cell population not appear in my DE results?
Differential expression only tests genes detected in at least 10 percent of the cells in one of the two groups. A marker of a small population inside a large selection can fall below that and be filtered out before testing, so it is absent rather than non-significant. Check the tested-gene count in the volcano header to see how much of your panel was filtered, then select the rare population directly and its markers will come through.
Why does it say my object is a gene subset?
Because adata.raw in your object is wider than the gene list Pluto served, which is the signature of a highly variable gene subset saved after adata.raw = adata. A gene you cannot find may have been dropped at save time rather than undetected. Re-save without the subset to make your full panel searchable.
I uploaded scaled data. What happened?
Scaled expression contains negative values and cannot be log-normalized, so Pluto looked for your pre-scaling values in adata.raw, then layers['counts'], then a single log-normalized layer, and used the first it found. The viewer states which. To avoid the ambiguity, save with un-scaled log-normalized values in X.
Why do my Xenium or MERFISH markers show very small p-values and large fold-changes?
Two things combine on imaging platforms: a small targeted gene panel and a very large number of cells. With that many cells, standard tests return extremely small p-values, and fold-changes on a targeted panel can look large. This is expected. Rank by effect size and biological plausibility, and treat the exact p-value as a ranking signal rather than a literal probability.
Can I upload multiple samples in one file?
Not today. Upload one sample per experiment and split multi-sample datasets into separate experiments.
What normalization should I use for Xenium or MERFISH?
If you have a preference, apply it before upload and Pluto will respect it. See the note on imaging platforms above for why this matters.
Why is neighborhood enrichment computed per section?
Coordinates are normalized within each tissue region, so two separate sections can overlap in coordinate space. Building the graph per section prevents cells in different physical sections from being treated as neighbors. The graph is built once by the pipeline and read by the viewer, so the numbers cannot drift between the two.
Is the AI assistant a source of results?
No. Spatial Studio includes an optional assistant for navigation and quick questions. Treat it as a convenience helper. The authoritative numbers come from the documented widgets described above.
✍ Quick checklist
Before you upload:
[ ] Single
.h5adfile[ ] Filtered, normalized, log-transformed values in
X[ ] No negative values in
X(not scaled or z-scored)[ ] Cluster column present in
obs[ ] Coordinates in
obsm['spatial'][ ] One sample only
[ ] Full gene panel, if you want every gene searchable
[ ] Gene symbols in
var(recommended)[ ] Tissue image ready, if your object references one
🚀 Ready to explore your spatial data?
We hope that the guide above helps you feel confident in uploading your data for exploration. Hope you enjoy generating new hypotheses! For more resources, we encourage you to take a look at our Blog and Knowledge Base.
Please reach out to support@pluto.bio if you have additional questions.
As always, our scientific support team is here to help! 🧬