Skip to content

Replace scv.pl.scatter and scvelo paga with scanpy equivalents#1302

Open
Marius1311 wants to merge 19 commits intomainfrom
scvelo/replace-scatter-paga
Open

Replace scv.pl.scatter and scvelo paga with scanpy equivalents#1302
Marius1311 wants to merge 19 commits intomainfrom
scvelo/replace-scatter-paga

Conversation

@Marius1311
Copy link
Collaborator

@Marius1311 Marius1311 commented Feb 24, 2026

Remove scv.pl.scatter and scvelo.plotting.paga usage (scVelo removal PR 2)

Part of the scVelo dependency removal effort — see plan in .github/prompts/PLAN_scvelo_removal.md.
Builds on PR #1301 (now merged).

Changes

_lineage_drivers.pyplot_lineage_drivers()

  • scv.pl.scattersc.pl.embedding for gene-on-embedding scatter
  • Adds basis kwarg (default "umap") extracted from **kwargs

_circular_projection.pycircular_projection()

  • scv.pl.scattersc.pl.embedding for custom-basis scatter
  • colorbar=colorbar_loc="right" | None

_aggregate_fate_probs.py — PAGA and PAGA_PIE modes

  • from scvelo.plotting import pagasc.pl.paga
  • PAGA mode: colors=color= (node colors); scatter background drawn via sc.pl.embedding when basis is set
  • PAGA_PIE mode: node_colors=color= (pie chart dict); scatter_flag/basis handled separately; legend_loc popped (scanpy doesn't accept it)
  • transitions_confidence only used when available in adata.uns["paga"]

_term_states_estimator.py_plot_discrete() and _plot_continuous()

  • _plot_discrete: scv.pl.scattersc.pl.embedding; group-specific add_outline reimplemented via new _add_outline_to_groups helper (three-layer scatter: background → gap → foreground)
  • _plot_continuous EMBEDDING mode:
    • color_gradients reimplemented as alpha-blended matplotlib scatter (new _plot_color_gradients helper)
    • Multi-panel mode uses temporary obs columns via RandomKeys (scanpy requires column names, not raw arrays)
    • perc dropped; dpi, color_map cleaned from kwargs before sc.pl.embedding calls
  • _plot_continuous TIME mode: reimplemented as multi-panel matplotlib scatter (new _plot_time_scatter helper)

New helpers (in pl/_utils.py):

  • _add_outline_to_groups — group-specific outline via double-scatter, replacing scVelo's add_outline=["Alpha", ...]
  • _plot_color_gradients — alpha-blended lineage overlay, replacing scVelo's color_gradients parameter
  • _plot_time_scatter — fate probability vs pseudotime scatter panels

Tests

  • Removed import scvelo as scv from test_plotting.py; moved figdir and transparent settings to conftest.py::pytest_sessionstart
  • Renamed 27 test methods and ground truth files to drop scvelo_ prefix (5 genuine scVelo projection tests keep their names)
  • Added scvelo>=0.3 to the test dependency group (needed for projection tests)
  • Regenerated all ground truth images
  • Removed from __future__ import annotations from PR 1 files

Behavioral changes

  • perc kwarg is no longer passed through; use vmin/vmax instead
  • PAGA directed edges (transitions_confidence) only shown when scVelo's PAGA was used
  • save/show are handled by CellRank (popped from kwargs before calling scanpy), matching the pattern used elsewhere in the codebase

Test results

  • 295 plotting tests pass, 9 skipped
  • 121 GPCCA/CFLARE tests pass

@WeilerP
Copy link
Collaborator

WeilerP commented Feb 24, 2026

@Marius1311, we cannot simply replace scvelo with scanpy for plotting macro- and terminal states. Highlighting them is one of the essential features of the function and the reason why we implemented them as such.

- _lineage_drivers.py: scv.pl.scatter -> sc.pl.embedding (gene-on-embedding)
- _circular_projection.py: scv.pl.scatter -> sc.pl.embedding (custom basis)
- _aggregate_fate_probs.py: scvelo.plotting.paga -> sc.pl.paga for both
  PAGA and PAGA_PIE modes; handle scatter_flag/basis by drawing embedding
  separately; transitions_confidence only used when available
- _term_states_estimator.py:
  - _plot_discrete: scv.pl.scatter -> sc.pl.embedding (categorical states)
  - _plot_continuous EMBEDDING mode: color_gradients reimplemented as
    alpha-blended matplotlib scatter; multi-panel uses temp obs columns
  - _plot_continuous TIME mode: reimplemented as matplotlib scatter
  - singleton perc -> vmin/vmax percentile conversion
  - add_outline (group-specific) dropped (scanpy only supports boolean)
  - dpi/perc/color_map cleaned from kwargs before sc.pl.embedding calls
…nsitions_confidence

- Move _plot_time_scatter, _plot_color_gradients from inline in
  _term_states_estimator.py to pl/_utils.py with proper docstrings
- Add _add_outline_to_groups helper to pl/_utils.py that replicates
  scVelo's group-specific add_outline (3-layer scatter: bg/gap/dot)
- Wire up _add_outline_to_groups in _plot_discrete for same_plot mode
- Drop all perc handling (backwards-breaking; users should use
  vmin/vmax directly)
- Keep dpi pop (sc.pl.embedding doesn't accept it)
- Document transitions_confidence in aggregate_fate_probabilities
  docstring (directed edges require scVelo's PAGA extension)
sc.pl.embedding saves to sc.settings.figdir with a basis prefix (e.g.
"umap"), which doesn't match the test infrastructure expectations.
Pop save/show from kwargs and handle them via CellRank's save_fig
instead.

- _plot_discrete: pop save/show, call save_fig + plt.show after outline
- _plot_continuous: pop save/show, handle in each branch (time/embedding)
- _plot_time_scatter, _plot_color_gradients: add save support via save_fig
- show defaults to None (auto: show when save is None, matching scanpy
  convention)
- Simplify _prepare_fname: stop stripping "scvelo_" prefix (no longer
  needed since CellRank saves without adding any prefix)
- For 5 plot_projection tests that still use scVelo's save (which
  re-adds "scvelo_" prefix): strip prefix + add ".png" explicitly
- Add default-groups = ["dev", "test"] to [tool.uv] so `uv sync`
  installs test deps (adjusttext, pytest, etc.) in the local venv
- Move figure settings (figdir, transparent) from test_plotting.py
  module-level to conftest.py pytest_sessionstart
- Remove `import scvelo as scv` from test_plotting.py; set
  scv.settings.figdir via try/except in conftest instead
- Drop `scvelo_` prefix from 27 test methods and ground truth files
  (keep prefix for 5 genuine scVelo projection tests)
- Rename test_proj_scvelo_kwargs -> test_proj_legend_loc
- Regenerate all ground truth images (removing
  scv.set_figure_params changes matplotlib defaults globally)
@Marius1311 Marius1311 force-pushed the scvelo/replace-scatter-paga branch from 0d030f1 to 10705c7 Compare February 24, 2026 21:33
@Marius1311 Marius1311 requested a review from WeilerP February 24, 2026 22:14
@Marius1311
Copy link
Collaborator Author

This should be ready for your review @WeilerP, would be great if you could test some of the plotting functions on some real data if you have some suitable examples.

@codecov
Copy link

codecov bot commented Feb 24, 2026

Codecov Report

❌ Patch coverage is 76.81159% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.77%. Comparing base (8f10405) to head (10705c7).

Files with missing lines Patch % Lines
src/cellrank/pl/_utils.py 69.87% 15 Missing and 10 partials ⚠️
src/cellrank/pl/_aggregate_fate_probs.py 60.00% 3 Missing and 3 partials ⚠️
...timators/terminal_states/_term_states_estimator.py 97.22% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1302      +/-   ##
==========================================
- Coverage   80.83%   80.77%   -0.07%     
==========================================
  Files          53       53              
  Lines        8703     8799      +96     
  Branches     1490     1512      +22     
==========================================
+ Hits         7035     7107      +72     
- Misses       1090     1102      +12     
- Partials      578      590      +12     
Files with missing lines Coverage Δ
src/cellrank/estimators/mixins/_lineage_drivers.py 77.02% <100.00%> (ø)
src/cellrank/kernels/utils/_moments.py 100.00% <ø> (ø)
src/cellrank/kernels/utils/_plot_utils.py 85.71% <ø> (-0.50%) ⬇️
src/cellrank/pl/_circular_projection.py 77.70% <100.00%> (ø)
...timators/terminal_states/_term_states_estimator.py 79.61% <97.22%> (+2.14%) ⬆️
src/cellrank/pl/_aggregate_fate_probs.py 86.66% <60.00%> (-0.74%) ⬇️
src/cellrank/pl/_utils.py 75.18% <69.87%> (-1.00%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines +468 to +486
axes = sc.pl.embedding(
self.adata,
basis=basis,
color=color + keys,
title=color + title,
add_outline=outline,
show=False,
return_fig=False,
**kwargs,
)

# Draw group-specific outlines (scVelo's add_outline with group names)
if outline is not None:
coords = self.adata.obsm[f"X_{basis}"]
axes_list = [axes] if not isinstance(axes, list | np.ndarray) else list(np.ravel(axes))
for ax, key in zip(axes_list[len(color):], keys):
if key in self.adata.obs:
_add_outline_to_groups(
ax, coords, outline, self.adata.obs[key], size=size,
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The states are plotted in the background, and all cells not assigned to a state on top and included in the legend as nan. Using the CellRank pseudotime protocol:

Correct (previously)
Image

Incorrect (now)
Image

I suggest we try not using _add_outline_to_groups and call sc.pl.embedding first with the entire dataset, but no coloring, followed by calling sc.pl.embedding with the data subsetted to the states and plotting them with an outline.

@Zethson
Copy link
Member

Zethson commented Feb 26, 2026

We were discussing at scverse whether some of scvelo's plottung functionality should be implemented into scanpy. Would this be of interest and a good idea?

@Marius1311
Copy link
Collaborator Author

We were discussing at scverse whether some of scvelo's plottung functionality should be implemented into scanpy. Would this be of interest and a good idea?

It would definitively be of interest - we're currently re-implementing them here, based on scanpy + some custom tweaks.

Maybe some velocity plotting could be useful in scanpy, given that there are now plenty of velocity approaches?

@Marius1311
Copy link
Collaborator Author

We'll keep having on optional scVelo dependency just for the velocity plotting functions.

Marius1311 and others added 3 commits February 26, 2026 14:55
The previous approach added "nan" as an explicit category and relied on
a custom `_add_outline_to_groups` helper, but scanpy drew NaN cells on
top of the state cells, making states invisible.

Now:
1. Leave unassigned cells as actual NaN (drawn by scanpy as na_color).
2. Overlay state-assigned cells via a second `sc.pl.embedding` call
   with `add_outline=True`, so they always appear on top.
3. Remove the custom `_add_outline_to_groups` helper (no longer needed).
`_plot_outline` used simple linear size multipliers and the default "o"
marker, producing oversized waypoint dots compared to the old scVelo
scatter.  Restore scVelo's quadratic outline-width formula and use
`marker="."` so waypoints are correctly sized again.

Closes #1303
@Zethson
Copy link
Member

Zethson commented Feb 26, 2026

Yes exactly, that's the motivation. CC @flying-sheep

- Add `basis` as a named parameter to `plot_macrostates`,
  `plot_fate_probabilities`, `_plot_discrete`, and `_plot_continuous`
  (previously hidden in **kwargs, defaulting to "umap")
- Set `propagate = False` on the cellrank logger to prevent messages
  from bubbling to the root logger (which caused duplicate output with
  a red background in Jupyter notebooks)
- Fix stale docstring in `plot_fate_probabilities` referencing
  `scvelo.pl.scatter` instead of `scanpy.pl.embedding`
When plotting macrostates/terminal states in discrete mode, cells not
belonging to any state (NaN) produced an "NA" entry in the legend.
Set na_in_legend=False by default so only actual states appear.
@Marius1311 Marius1311 force-pushed the scvelo/replace-scatter-paga branch from 5eb0e88 to dc27682 Compare March 2, 2026 19:56
- Respect legend_loc parameter in _plot_color_gradients (was hard-coded)
- Support "right", "right margin", "on data", and matplotlib loc strings
- Default point size to 120_000/n_obs (scanpy convention) instead of 1
- Preserve dpi kwarg for same_plot path (only pop for sc.pl.embedding)
@Marius1311 Marius1311 force-pushed the scvelo/replace-scatter-paga branch from 1e064d7 to 8f9375f Compare March 2, 2026 20:15
Reimplement _plot_color_gradients using scvelo's technique: for each
pair of lineages, create a diverging colormap (color_A → transparent →
color_B).  Uncertain cells become transparent, letting the grey
background show through.  Only cells whose top-2 lineages match each
pair are drawn, so colors don't stack up and produce dark overlaps.
@Marius1311 Marius1311 force-pushed the scvelo/replace-scatter-paga branch from 8f9375f to 64382c4 Compare March 2, 2026 20:28
- circular_projection: widen figure when legend_loc contains "right"
  so the scanpy legend placed via bbox_to_anchor is not clipped
- _plot_time_scatter: pop legend_loc from kwargs and pass it to
  ax.legend(), supporting "right", "right margin", "none", and any
  matplotlib loc string (was hard-coded to "best")
@Marius1311 Marius1311 force-pushed the scvelo/replace-scatter-paga branch from 67622cb to 3ad5381 Compare March 2, 2026 20:57
@Marius1311
Copy link
Collaborator Author

Marius1311 commented Mar 2, 2026

Hi @WeilerP, the issues you raised should be fixed now - could you take another look please? I checked using the getting started tutorial and things seemed to work there.

There's a separate notebooks PR here: scverse/cellrank_notebooks#77

It's unfinished, only contains changes for the getting started tutorial.

Rename local `colors` to `lin_colors` to avoid shadowing the
`matplotlib.colors` module import, which caused an AttributeError
when calling `colors.LinearSegmentedColormap`.
pyGAM triggers harmless RuntimeWarnings (divide by zero in log,
invalid value in reduce/divide) for degenerate genes. Show each
unique warning once instead of flooding notebook output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants