Widget object¶
The live, kernel-linked widget returned by scatterplot(..., interactive=True) (and by every panel inside compose). These methods need a running kernel; a static/exported plot keeps the visual interactions (pan/zoom/lasso/legend/export) but cannot round-trip to Python.
Most methods accept a selection= override; when omitted they act on the current lasso selection (w.selection). Indices are always given and returned in original data order.
Properties¶
w.selection¶
Indices of the lasso-selected points (read or assign), always in data order — translated through the draw-order permutation when the plot was z-ordered (sort_order / random_state).
Live (interactive=True) only — on a static plot this stays empty
because there is no kernel link.
w.filtered¶
Original indices of the cells currently passing the in-plot filters
(range sliders + legend categories). When no filter is active this is
all shown cells (everything passes). Live (interactive=True)
only — like selection, but for the filter instead of the lasso.
w.colors¶
The categorical colour map as {category: '#rrggbb'} (the rendered
palette, including any in-plot recolours via the legend colorpicker).
None for a continuous / single-colour plot. Save it scanpy-style with
e.g. adata.uns['louvain_colors'] = list(w.colors.values()).
Methods¶
w.subset(selection=None)¶
The source object subset to the selected cells (adata[w.selection]).
| Parameter | Type | Description | Default |
|---|---|---|---|
selection |
list[int] | list[str] | bool mask | None |
Cells to subset. None uses the current lasso selection. |
None |
Returns the same type as the source — adata[sel] for AnnData/MuData, df.iloc[sel] for a DataFrame.
w.annotate(key, label, selection=None)¶
Write label onto the lasso-selected cells in obs[key] / column key
of the source object. Useful for curating cell types interactively.
| Parameter | Type | Description | Default |
|---|---|---|---|
key |
str |
obs / DataFrame column to write into (created if missing). |
required |
label |
str |
Value to assign to the selected cells. | required |
selection |
list[int] | list[str] | bool mask | None |
Cells to label. None uses the current lasso selection. |
None |
Returns the annotated source object (the column becomes a pandas.Categorical).
w.composition(by, selection=None, normalize=True)¶
Count + fraction of the selected cells in each category of by — "what is
this region made of?".
| Parameter | Type | Description | Default |
|---|---|---|---|
by |
str |
Categorical obs / DataFrame column to break the selection down by. |
required |
selection |
list[int] | list[str] | bool mask | None |
Cells to summarise. None uses the current lasso selection. |
None |
normalize |
bool |
Also add a fraction column (counts ÷ total). |
True |
Returns a pandas.DataFrame indexed by category with a count (and, if
normalize, fraction) column.
w.dotplot(genes, groupby=None, *, selection=None, layer=None, use_raw=None, standard_scale=None, expression_cutoff=0.0, swap_axes=False, cmap='Reds', engine='auto', return_frames=False, **kwargs)¶
A scanpy-style dot plot: per groupby category (rows) and gene (columns), the
size of each dot is the fraction of cells expressing the gene and the
colour is the mean expression.
| Parameter | Type | Description | Default |
|---|---|---|---|
genes |
list[str] | dict[str, list[str]] |
Gene names, or a {label: [genes]} dict to draw labelled gene groups. |
required |
groupby |
str | None |
obs column to group rows by. None uses the plot's categorical colour variable. |
None |
selection |
list[int] | list[str] | bool mask | None |
Restrict to these cells (or the current lasso when omitted). | None |
layer / use_raw |
str / bool |
Expression matrix to read. use_raw defaults to adata.raw when present (matches scanpy), so means aren't taken over a scaled .X. |
None |
standard_scale |
'var' | 'group' | None |
Min-max scale the colour per gene or per group. | None |
expression_cutoff |
float |
A cell counts as expressing when its value is above this. | 0.0 |
swap_axes |
bool |
Put genes on rows and groups on columns. | False |
cmap |
str |
Matplotlib colormap for the mean-expression colour. | 'Reds' |
engine |
'auto' | 'scanpy' | 'gpu' | 'cupy' | 'numpy' |
Aggregation backend and renderer (see below). | 'auto' |
return_frames |
bool |
Also return the (mean_df, frac_df) DataFrames. |
False |
engine
'auto'(default) — hand off to scanpy'ssc.pl.dotplotwhen scanpy is installed (the authentic figure), else the built-in renderer.'scanpy'— forcesc.pl.dotplot(raises if scanpy is absent).'gpu'/'cupy'— aggregate the per-group mean and fraction on the GPU via cupy, then draw with the built-in renderer (the faster path on many cells).'numpy'runs the same aggregation on the CPU. The dot values match scanpy.
Returns scanpy's axes object (scanpy path) or a small result carrying
.figure / .mean_expression / .fraction (built-in path). With
return_frames=True it returns (result, mean_df, frac_df).
w.dotplot(["MS4A1", "NKG7", "CST3"], groupby="louvain")
# labelled gene groups + GPU aggregation
markers = {"B": ["MS4A1", "CD79A"], "NK": ["NKG7", "GNLY"], "Mono": ["CST3", "LYZ"]}
w.dotplot(markers, groupby="louvain", engine="gpu")
# just the lassoed cells
w.selection.dotplot(["MS4A1", "NKG7"], groupby="louvain")
w.diff_expression(group_a=None, group_b=None, n=10, layer=None, method='wilcoxon', key_added=None, use_raw=None, engine='auto')¶
Top differential genes between two cell groups (markers of a lasso vs the rest, or one lasso vs another).
| Parameter | Type | Description | Default |
|---|---|---|---|
group_a |
list[int] | list[str] | bool mask | None |
The "test" group. None uses the current lasso selection. |
None |
group_b |
list[int] | list[str] | bool mask | None |
The reference group. None means all other cells ("rest"). |
None |
n |
int |
Number of top genes to keep per group. | 10 |
layer |
str | None |
adata.layers key to test on. None follows scanpy's default (see note). |
None |
method |
str |
scanpy test: 'wilcoxon', 't-test', 't-test_overestim_var', 'logreg'. |
'wilcoxon' |
key_added |
str | False | None |
adata.uns key to save under. None → 'rank_genes_groups'; False → don't save. |
None |
use_raw |
bool | None |
Force testing on adata.raw. None uses scanpy's default: adata.raw when it exists, else .X. |
None |
engine |
str |
Compute backend / scanpy requirement — see the engine table. | 'auto' |
When scanpy is installed (and the source is an AnnData) this runs
sc.tl.rank_genes_groups on a copy; otherwise it falls back to a Welch
t-test. AnnData/MuData only.
Returns the scanpy-native result — the params + rec.array dict
(names / scores / logfoldchanges / pvals / pvals_adj), identical to
what scanpy stores in adata.uns. The return value and the saved entry are
the same object, and a tidy table is one
sc.get.rank_genes_groups_df(adata, group="A") away.
Which matrix (scanpy-identical)
With no layer / use_raw pinned, the matrix follows scanpy's own
default: adata.raw (the full log-normalised gene set) when it exists,
else .X. This is over the full raw, not raw restricted to the HVG
var_names, so non-HVG genes (e.g. ribosomal) can still rank — the result
is byte-for-byte what a direct sc.tl.rank_genes_groups call returns. The
only special case: if .X is scaled/z-scored and there is no raw,
logfoldchanges come out NaN (as they would in scanpy) and you get a
warning suggesting adata.raw = adata or a log-norm layer=.
w.diff_expression_by(by, group_a=None, group_b=None, selection=None, n=10, layer=None, method='wilcoxon', key_added=None, use_raw=None, min_cells=2)¶
Differential expression between the levels of an obs column, restricted to
the lasso selection — e.g. lasso a region, then compare condition or time
within it.
| Parameter | Type | Description | Default |
|---|---|---|---|
by |
str |
The obs column whose levels you compare (e.g. "condition", "time"). |
required |
group_a |
str | None |
A single level of by. With group_b → A-vs-B; alone → A-vs-rest. |
None |
group_b |
str | None |
A second level of by to use as the reference. |
None |
selection |
list[int] | list[str] | bool mask | None |
Cells to restrict to. None uses the lasso; empty selection → all cells. |
None |
n |
int |
Number of top genes to keep per group. | 10 |
layer |
str | None |
adata.layers key to test on. None auto-picks a finite-logFC matrix. |
None |
method |
str |
scanpy test: 'wilcoxon', 't-test', 't-test_overestim_var', 'logreg'. |
'wilcoxon' |
key_added |
str | False | None |
adata.uns key to save under. None → 'rank_genes_groups'; False → don't save. |
None |
use_raw |
bool | None |
Force testing on adata.raw. None lets the matrix-picker decide. |
None |
min_cells |
int |
In the all-levels mode, skip (with a warning) any level with fewer cells than this in the selection. | 2 |
engine |
str |
Compute backend / scanpy requirement — see the engine table. | 'auto' |
The three modes:
group_aandgroup_b→ a single A-vs-B comparison (e.g.group_a="D30", group_b="Y1").group_aonly → that level vs the pooled rest of the selection.- neither → every present level vs the rest, run as a single scanpy
rank_genes_groupscall (one-vs-rest, the scanpy idiom).
Returns the scanpy-native result in every mode — same shape as
diff_expression. The real by name and real level names appear in
params / the rec.array fields (no synthetic _rs_by / _rs_lognorm), so
sc.pl.rank_genes_groups(adata) and
sc.get.rank_genes_groups_df(adata, group=...) work straight after. The same
object is auto-saved to adata.uns (key_added controls the key). Also
exposed on a compose grid
(grid.diff_expression_by(...), proxied to a shared panel).
import scanpy as sc
w.selection = some_cells # or lasso in the UI
# every condition in the lasso vs the rest -> scanpy-native result (== adata.uns):
res = w.diff_expression_by("condition") # params + rec.arrays, one col per level
res["names"].dtype.names # ('D30', 'Y1', 'Y2')
sc.get.rank_genes_groups_df(adata, group="D30") # tidy table for any level
# one specific pair, saved under a custom uns key:
res = w.diff_expression_by("condition", group_a="D30", group_b="Y1",
key_added="D30_vs_Y1")
# one level vs the rest of the lasso:
res = w.diff_expression_by("condition", group_a="Y1")
# also available straight off the selection:
res = w.selection.diff_expression_by("time")
Which engine ran (scanpy / GPU / fallback)¶
Both DE methods take engine= to pick the backend and to control whether
scanpy is required — so a Welch-t-test fallback is never silent:
engine |
Backend | If unavailable |
|---|---|---|
'auto' (default) |
scanpy (sc.tl.rank_genes_groups) if importable |
warns, then CPU Welch t-test |
'scanpy' |
scanpy — always | raises ImportError (never falls back) |
'gpu' (alias 'cupy') |
GPU Welch t-test via cupy (per-cell reductions on the GPU) |
raises ImportError |
'rapids' |
GPU logistic-regression DE via rapids-singlecell | raises ImportError |
'ttest' |
the built-in CPU Welch t-test | — |
Two ways to be sure scanpy ran:
- Require it —
engine="scanpy"raises instead of falling back silently. - Check the result — scanpy records its correction in
params:
res = w.diff_expression(engine="scanpy")
res["params"]["method"] # -> 'wilcoxon' (your real test)
res["params"]["corr_method"] # -> 'benjamini-hochberg' (scanpy's default)
# both Welch t-test paths (CPU + GPU) instead stamp method='t-test', corr_method='none'
GPU acceleration
sc.tl.rank_genes_groups is CPU-only, so GPU DE uses a different test:
engine="gpu"(needs onlycupy,pip install cupy-cuda12x) runs a Welch t-test with the heavyO(cells × genes)mean/variance reductions on the GPU. The formula matches scipy'sttest_ind(..., equal_var=False), so GPU and CPU results agree to floating point. On an RTX 2060, DE over 120k cells × 2000 genes ran in ~1.9 s vs ~36 s on the CPU (≈19×). It pays off when a group has many cells; for a few-thousand-cell lasso the CPU path is already fast.engine="rapids"uses rapids-singlecell (full RAPIDS /cuml) for GPU logreg DE —method=is ignored. Heavier install, but returns the same scanpy-nativeadata.unsdict.
w.highlight(indices, color=None)¶
Persistently mark points with a crisp ring + size bump (the engine's selection look) — but this is not the selection, so it survives a double-click and a new lasso.
| Parameter | Type | Description | Default |
|---|---|---|---|
indices |
list[int] | [] | None |
Original data indices to mark. [] / None clears the highlight. |
required |
color |
str | None |
Ring colour (hex / CSS colour). None keeps the default. |
None |
Live (interactive=True) only — needs the kernel link.
Note
With progressive=True the highlight marks the currently-shown cells; it
doesn't yet follow new cells streamed in on zoom.
w.morph_to(basis, duration=1200)¶
Animate the points from their current layout to another embedding — e.g.
w.morph_to('spatial') morphs a UMAP into the spatial layout (and back with
w.morph_to('umap')). Positions tween; colours and sizes stay.
| Parameter | Type | Description | Default |
|---|---|---|---|
basis |
str |
Target obsm embedding to morph into ('umap', 'spatial', …). |
required |
duration |
int |
Animation length in milliseconds. | 1200 |
Live (interactive=True) only; not for progressive=True (only a subset is
resident). Returns self so calls can chain.
w.to_html(path, title='reglscatterpy plot')¶
Save this plot as a standalone, offline HTML file (like R's
htmlwidgets::saveWidget) — inlines the bundle and data, stays interactive
with no kernel.
| Parameter | Type | Description | Default |
|---|---|---|---|
path |
str | Path |
Where to write the .html file. |
required |
title |
str |
<title> of the saved page. |
'reglscatterpy plot' |
On a live (interactive=True) widget it captures the current view /
selection / filter, so the file opens exactly as you left it.
w.update(spec)¶
Push a partial spec update to a live widget (e.g. recolour, change point size) without rebuilding it.
| Parameter | Type | Description | Default |
|---|---|---|---|
spec |
dict |
Plot properties to merge into the current spec. | required |
Returns self. Live (interactive=True) only.