Metadata-Version: 2.4
Name: reglscatterpy
Version: 0.6.36
Summary: Interactive WebGL scatterplots for single-cell data (AnnData/MuData/SpatialData) in Jupyter, VS Code and Shiny for Python
Project-URL: Homepage, https://github.com/george123ya/reglscatterpy
Project-URL: Documentation, https://george123ya.github.io/reglscatterpy/
Project-URL: Bug Tracker, https://github.com/george123ya/reglscatterpy/issues
Project-URL: R package, https://github.com/george123ya/reglScatterplotR
Author: George Muñoz
License: MIT
License-File: LICENSE
Keywords: anndata,scanpy,scatterplot,single-cell,visualization,webgl
Classifier: Framework :: Jupyter
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.9
Requires-Dist: anywidget>=0.9
Requires-Dist: jupyter-ui-poll>=0.2
Requires-Dist: numpy
Requires-Dist: pandas
Provides-Extra: all
Requires-Dist: anndata; extra == 'all'
Requires-Dist: jupyter-scatter>=0.18; extra == 'all'
Requires-Dist: mudata; extra == 'all'
Requires-Dist: spatialdata; extra == 'all'
Provides-Extra: anndata
Requires-Dist: anndata; extra == 'anndata'
Provides-Extra: dev
Requires-Dist: anndata; extra == 'dev'
Requires-Dist: anywidget>=0.9; extra == 'dev'
Requires-Dist: ipykernel; extra == 'dev'
Requires-Dist: nbconvert>=7; extra == 'dev'
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: scipy; extra == 'dev'
Provides-Extra: docs
Requires-Dist: black; extra == 'docs'
Requires-Dist: mkdocs-include-markdown-plugin>=6.0; extra == 'docs'
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.25; extra == 'docs'
Provides-Extra: gpu
Requires-Dist: cupy-cuda12x; extra == 'gpu'
Provides-Extra: mudata
Requires-Dist: mudata; extra == 'mudata'
Provides-Extra: render
Requires-Dist: jupyter-scatter>=0.18; extra == 'render'
Provides-Extra: report
Requires-Dist: ipykernel; extra == 'report'
Requires-Dist: nbconvert>=7; extra == 'report'
Requires-Dist: nbformat; extra == 'report'
Provides-Extra: spatial
Requires-Dist: spatialdata; extra == 'spatial'
Description-Content-Type: text/markdown

# reglscatterpy

[![PyPI](https://img.shields.io/pypi/v/reglscatterpy.svg)](https://pypi.org/project/reglscatterpy/)
[![Bioconda](https://img.shields.io/conda/vn/bioconda/reglscatterpy.svg?label=bioconda&color=brightgreen)](https://anaconda.org/bioconda/reglscatterpy)
[![Python versions](https://img.shields.io/pypi/pyversions/reglscatterpy.svg)](https://pypi.org/project/reglscatterpy/)
[![Docs](https://img.shields.io/badge/docs-github.io-blue.svg)](https://george123ya.github.io/reglscatterpy/)
[![Live demo](https://img.shields.io/badge/live%20demo-pbmc3k-brightgreen.svg)](https://george123ya.github.io/reglscatterpy/demo/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

**An interactive scatterplot for single-cell and spatial data that talks back to Python.**

Plot millions of cells in the notebook, then use your mouse: pan, zoom, lasso a
population, toggle cell types in the legend. The part that matters for analysis
is the round-trip: the cells you circle in the browser come straight back into
Python, so you can subset them, label them, or run differential expression on
them without leaving the plot. It reads AnnData, MuData, SpatialData, pandas and
numpy, and runs in Jupyter, JupyterLab, VS Code and Colab.

<p align="center">
  <img src="https://raw.githubusercontent.com/george123ya/reglscatterpy/main/assets/demo.gif"
       alt="Panning, lassoing and legend-filtering an interactive UMAP" width="760">
</p>

<p align="center">
  <a href="https://george123ya.github.io/reglscatterpy/demo/"><b>Live demo</b></a>
  &nbsp;|&nbsp;
  <a href="https://nbviewer.org/github/george123ya/reglscatterpy/blob/main/notebooks/reglscatterpy_examples.ipynb"><b>Example notebook</b></a>
  &nbsp;|&nbsp;
  <a href="https://george123ya.github.io/reglscatterpy/"><b>Documentation</b></a>
</p>

## Why it is useful

Static plots (scanpy, matplotlib) are one-way: you look, but you cannot point at
a group of cells and keep working with them. reglscatterpy closes that loop, and
it does three things that make it fit a real single-cell workflow:

- **Interactive at scale.** It renders with WebGL ([regl-scatterplot](https://github.com/flekschas/regl-scatterplot)),
  so a few million points stay smooth to pan and zoom. No downsampling by hand.
- **The selection returns to Python.** Lasso cells, read `w.selection`, and the
  usual tools (`adata[...]`, annotate, differential expression) take it from there.
- **Outputs are scanpy-native.** A DE call writes the same `adata.uns` entry as
  `sc.tl.rank_genes_groups`, and a hand label writes `adata.obs`. Nothing new to
  learn downstream, and the results match scanpy exactly.

The same widget also ships in R as [reglScatterplotR](https://github.com/george123ya/reglScatterplotR),
so a plot looks and behaves identically in both languages.

## Install

```bash
pip install reglscatterpy            # core: numpy, pandas, anywidget
pip install anndata                  # for AnnData (mudata / spatialdata as needed)
```

Or from bioconda:

```bash
conda install -c bioconda -c conda-forge reglscatterpy
```

## Quick start

Give it an AnnData, say which embedding to show (`basis=`) and what to color by,
which can be an `obs` column or a gene name:

```python
import scanpy as sc
import reglscatterpy as rs

adata = sc.datasets.pbmc3k_processed()
rs.scatterplot(adata, basis="umap", color_by="louvain")   # color by cell type
rs.scatterplot(adata, basis="umap", color_by="CST3")      # or by a gene
```

For a plain DataFrame, give the coordinate columns with `x=` / `y=`:

```python
import numpy as np, pandas as pd
df = pd.DataFrame({"x": np.random.rand(10_000), "y": np.random.rand(10_000),
                   "ct": np.random.choice(list("ABC"), 10_000)})
rs.scatterplot(df, x="x", y="y", color_by="ct")
```

Plots are live and kernel-linked by default, so the selection returns to Python
straight away. Keep a handle to lasso and read it back:

```python
w = rs.scatterplot(adata, basis="umap", color_by="louvain")   # interactive by default
w                          # show it, then lasso a population in the browser
w.selection                # the cells you circled, back in Python
```

For a portable figure to keep or share, pass `interactive=False` (see
[Interactive by default](#interactive-by-default-static-snapshots-on-request)).

## What you can do

### Lasso a population and pull it out

Circle cells in the plot, read them back as indices, and subset the AnnData for
anything downstream (recluster, re-embed, save).

```python
w.selection                # -> [12, 87, 134, ...] positional indices
sub = adata[w.selection]   # a normal AnnData subset
```

<p align="center">
  <img src="https://raw.githubusercontent.com/george123ya/reglscatterpy/main/assets/subset.gif"
       alt="Lasso cells in the UMAP and subset the AnnData from the selection" width="720">
</p>

### Label clusters by hand

Select a cluster, give it a name, and the label is written into `adata.obs`. Do
it a few times and re-plot by your new column.

```python
w.annotate("cell_type", "B cells")            # writes adata.obs["cell_type"]
rs.scatterplot(adata, basis="umap", color_by="cell_type")
```

<p align="center">
  <img src="https://raw.githubusercontent.com/george123ya/reglscatterpy/main/assets/annotate.gif"
       alt="Lasso a cluster, annotate it, and recolor by the new obs column" width="700">
</p>

### Markers of a selection

Lasso a population and call `diff_expression`. It returns scanpy's native result
and saves it to `adata.uns`, so the rest of scanpy just works. Results are
identical to a direct `sc.tl.rank_genes_groups` call.

```python
w.diff_expression(n=10)                         # selection vs the rest
sc.get.rank_genes_groups_df(adata, group="A")   # tidy table
sc.pl.rank_genes_groups(adata)                  # scanpy's own plot, right after
```

<p align="center">
  <img src="https://raw.githubusercontent.com/george123ya/reglscatterpy/main/assets/diff-expression.gif"
       alt="Lasso a cluster and get its differential-expression markers" width="700">
</p>

### Summarize markers with a dot plot

`w.dotplot` gives the scanpy dot plot (fraction of cells expressing as dot size,
mean expression as color) from the same object you are plotting. Pass a plain
gene list or a labelled marker dict.

```python
markers = {"B": ["MS4A1", "CD79A"], "T": ["CD3E", "IL7R", "CCL5"],
           "NK": ["NKG7", "GNLY"], "Monocyte": ["CST3", "LYZ", "FCGR3A"]}
w.dotplot(markers, groupby="louvain")           # engine="gpu" aggregates on the GPU
```

<p align="center">
  <img src="https://raw.githubusercontent.com/george123ya/reglscatterpy/main/assets/dotplot.png"
       alt="Dot plot of marker genes across clusters" width="640">
</p>

### Compare many panels, filter across all of them

Color one embedding by several genes or columns at once and you get a linked
grid, one panel per value, with camera and lasso kept in sync. Add `filter_by`
to gate on a value (for example `n_genes` or `percent_mito`) and every panel
updates together.

```python
rs.scatterplot(adata, basis="umap",
               color_by=["louvain", "NKG7", "MS4A1"],
               filter_by=["n_genes", "percent_mito"], ncols=3)
```

<p align="center">
  <img src="https://raw.githubusercontent.com/george123ya/reglscatterpy/main/assets/filter-linked.gif"
       alt="A linked 3-panel grid with distribution sliders filtering all panels at once" width="900">
</p>

### Land a set of cells you already have

You do not have to lasso. Set the selection from any list of indices or names,
for example a gene signature or a set of cells from another tool, to see where
they sit. Here, the 200 cells with the highest MS4A1 land on the B-cell cluster.

```python
ms4a1 = adata.obs_vector("MS4A1")               # 1-D gene vector, sparse-safe
w.selection = list(np.argsort(ms4a1)[-200:])
```

<p align="center">
  <img src="https://raw.githubusercontent.com/george123ya/reglscatterpy/main/assets/select-indices.gif"
       alt="Set the selection from a list of indices and see the cells light up" width="720">
</p>

### Move between a UMAP and the tissue

For spatial data both layouts live in the same object, so cells can glide from a
UMAP into their tissue coordinates and back, carrying color and selection.

```python
w = rs.scatterplot(adata, basis="umap", color_by="leiden", interactive=True)
w.morph_to("spatial")              # animate UMAP into tissue coordinates
w.morph_to("umap", duration=800)   # and back (ms)
```

<p align="center">
  <img src="https://raw.githubusercontent.com/george123ya/reglscatterpy/main/assets/morph.gif"
       alt="Morph a UMAP into spatial tissue coordinates" width="700">
</p>

## More on selections and analysis

The selection is a normal round-trip: read it, or set it from Python.

```python
w.selection                      # positional indices
sub = w.subset()                 # same as adata[w.selection]
w.selection = list(range(100))   # drive it from Python to highlight points
```

Differential expression, in a bit more detail. `diff_expression` compares a
group against the rest or against a second group; `diff_expression_by` splits a
lasso by an `obs` column (for example `condition` or `time`) and compares its
levels. Both return scanpy's native result and auto-save to `adata.uns`.

```python
a = w.selection                                # save group A
# lasso group B
w.diff_expression(a, w.selection)              # A vs B

res = w.diff_expression_by("condition")        # each level vs the rest
w.diff_expression_by("condition", group_a="D30", group_b="Y1")   # one pair
```

By default DE uses scanpy when installed and warns if it has to fall back. Pass
`engine="scanpy"` to require it, or run on the GPU with `engine="gpu"` (needs
`cupy`, about 19x faster on 120k cells), or `engine="rapids"` for GPU logreg via
[rapids-singlecell](https://rapids-singlecell.readthedocs.io/). The same
`engine=` option accelerates `dotplot` aggregation. See the
[widget API](https://george123ya.github.io/reglscatterpy/widget-api/) for the
full table.

See what a region is made of:

```python
w.composition("louvain")           # count and fraction per cluster in the selection
```

## Scales to millions of cells

By default `scatterplot()` keeps large datasets interactive without silently
hiding cells:

```python
# AUTO (default): caps at 500k with a density-preserving subsample that KEEPS
# rare cell types. The plot stays honest: an on-figure "500,000 of 3,900,000
# shown" caption, and w.selection still indexes ALL rows.
rs.scatterplot(adata, basis="umap", color_by="cell_type")

# ALL POINTS RESIDENT: every cell on the GPU, smooth up to ~4M on a decent card.
rs.scatterplot(adata, basis="umap", color_by="cell_type", max_points=None)
```

For datasets beyond roughly 4M cells, `progressive=True` renders a light density
overview and re-draws all cells inside the viewport as you zoom in, with no
preprocessing and the lasso staying complete:

```python
rs.scatterplot(adata, basis="umap", color_by="cell_type", progressive=True)
```

Rule of thumb: `max_points=None` for 2 to 4M real atlases, `progressive=True`
beyond that.

## Spatial data

Anything with coordinates in `obsm["spatial"]` plots the same way, so Visium,
Xenium, MERFISH and CosMx all work. Point `basis` at it:

```python
import squidpy as sq
adata = sq.datasets.visium_hne_adata()         # public mouse-brain Visium
rs.scatterplot(adata, basis="spatial", color_by="cluster")    # tissue map
rs.scatterplot(adata, basis="spatial", color_by="Olfm1")      # a gene, in situ
rs.scatterplot(adata, basis="spatial", color_by="leiden", point_size=3)  # sparse tissue
```

## Interactive by default, static snapshots on request

Plots are live and kernel-linked by default, so `w.selection` and the other
round-trips work with no extra flag. The trade-off is that a live widget needs a
running kernel and, like any Jupyter widget, may render blank when you reopen the
notebook later.

For a figure you want to keep or share, pass `interactive=False`. It renders a
self-contained snapshot (a sandboxed iframe with the WebGL bundle and data baked
in) that shows in JupyterLab, Notebook 7, VS Code and Colab and survives
reopening the notebook with no kernel, like a plotly figure. It stays
interactive to look at (pan, zoom, lasso, legend, tooltips, image export) but
cannot send a selection back to Python.

```python
fig = rs.scatterplot(adata, basis="umap", color_by="louvain", interactive=False)
```

Use the default while you are actively selecting, and `interactive=False` for
the plots you want to embed in a shared notebook or report.

## Save a standalone HTML

The Python equivalent of R's `htmlwidgets::saveWidget`: one self-contained
`.html` that inlines the widget and the plot data, so it opens in any browser
with no kernel and no internet.

```python
rs.save_html(w, "umap.html")      # or w.to_html("umap.html")
```

The bundle is inlined gzip-compressed (about 0.5 MB), so a one-plot file is well
under 1 MB. To turn a whole notebook into one HTML report without re-running it,
call `rs.record_html()` once near the top, run the notebook, then
`jupyter nbconvert --to html analysis.ipynb` (needs `nbconvert` and `ipykernel`,
`pip install 'reglscatterpy[report]'`).

## Theme

Plots are a white figure card by default. Pass `theme=` to follow your notebook:

```python
rs.scatterplot(adata, basis="umap", color="leiden", theme="auto")   # dark in a dark theme
rs.scatterplot(adata, basis="umap", color="leiden", theme="dark")   # always dark
```

<p align="center">
  <img src="https://raw.githubusercontent.com/george123ya/reglscatterpy/main/assets/theme-dark.png"
       alt="Dark-theme figure card" width="520">
</p>

Set it once per session with `rs.set_theme("auto")`. The theme affects only the
live widget; an exported `.html` stays portably light.

## Other options

- **Richer tooltips**: `tooltip_by=["n_genes", "sample", "CST3"]` shows extra obs
  columns or genes on hover.
- **Outlines and highlighting**: `add_outline=True` rings every point;
  `w.highlight([12, 87, 134], color="red")` marks a chosen subset that survives
  new lassoes.
- **Encode more variables**: `size_by=` and `opacity_by=` accept a numeric column
  or a gene.
- **Toolbar and legend**: `toolbar="left"` shows an in-plot toolbar;
  `zoom_on_selection=True` auto-frames a lasso; the categorical legend shows a
  live per-category count.

## Supported objects

| Input | `x` (embedding) | `color_by` / `group_by` |
|-------|-----------------|-------------------------|
| `AnnData` | `obsm` key (`"X_umap"`, `"umap"`, `"spatial"`) | `obs` column or `var_names` feature |
| `MuData` | global `obsm` or `"modality:embedding"` | `obs` column or `"modality:feature"` |
| `SpatialData` | table's `obsm` (defaults to `"spatial"`) | table's `obs` or features |
| `pandas.DataFrame` | column name | column name or vector |
| `numpy.ndarray` | column index | vector |

## Same plot in R and Python

`rs.scatterplot(...)` mirrors R's `reglScatterplot(...)`: `color_by` / `group_by`,
`point_size`, `opacity`, `point_color`, `continuous_palette` /
`categorical_palette`, `custom_colors`, `vmin` / `vmax`, `filter_by`, legend
styling, and more. Equivalence is locked down by `tests/test_payload_parity.py`,
which checks the Python payload byte-for-byte against R fixtures.

## Develop and test

```bash
pip install -e .[dev]
pytest
```

The widget itself (`src/reglscatterpy/static/widget.js`) is a built artifact; its
source lives in the reglScatterplotR repo under `js/`. To refresh it after a JS
change, build there and copy `dist/widget.js` into `src/reglscatterpy/static/`.
