Replace scv.pl.scatter and scvelo paga with scanpy equivalents#1302
Replace scv.pl.scatter and scvelo paga with scanpy equivalents#1302Marius1311 merged 33 commits intomainfrom
Conversation
|
@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)
0d030f1 to
10705c7
Compare
|
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 Report❌ Patch coverage is Additional details and impacted files@@ 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
🚀 New features to boost your workflow:
|
| 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, | ||
| ) |
There was a problem hiding this comment.
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:
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.
|
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? |
|
We'll keep having on optional scVelo dependency just for the velocity plotting functions. |
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
|
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.
5eb0e88 to
dc27682
Compare
- 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)
1e064d7 to
8f9375f
Compare
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.
8f9375f to
64382c4
Compare
- 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")
67622cb to
3ad5381
Compare
|
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.
Leave RuntimeWarning filtering (divide by zero, invalid value) to the user instead of silencing them inside CellRank. The DeprecationWarning filters for pyGAM's deprecated numpy aliases are kept. Also update CytoTRACEKernel docs and default layer from "Ms" to "imputed" (CellMapper), and pass layer explicitly in tests.
The `backend` and `show_progress_bar` keys were included in `parallel_kwargs` by all three callers (gene_trends, heatmap, cluster_trends) but only `n_jobs` was extracted and passed to `parallelize()`. The remaining keys were silently dropped, so `parallelize()` always used its defaults (`backend="loky"`, `show_progress_bar=True`) regardless of what the user requested.
pygam prints convergence failures to stdout via a bare `print()` call, which cannot be caught by `warnings.filterwarnings`. Redirect stdout during pygam fit/gridsearch calls via `contextlib.redirect_stdout` and log non-convergence at DEBUG level instead.
This reverts commit de6acd6.
The default layer was changed from 'Ms' to 'imputed' in 7fe58c1. Test adata doesn't have 'imputed' layer, so pass 'Ms' explicitly.
Accept sparse matrix directly instead of extracting from adata, so VelocityKernel can pass self.connectivities (which respects conn_key from ConnectivityMixin). Removes test skip.
The test verifies SLEPc iteration output is suppressed when verbose=False. Since CellRank's RichHandler logs to stdout, temporarily raise the log level to WARNING so only SLEPc output (if any) is captured.
CellRank's RichHandler logs to stdout. compute_transition_matrix() emits INFO lines that capsys captures before the logger is silenced, causing the subsequent `assert not len(out)` to fail. Flush the capsys buffer after setup so only SLEPc output from compute_schur() is checked.


Remove
scv.pl.scatterandscvelo.plotting.pagausage (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.py—plot_lineage_drivers()scv.pl.scatter→sc.pl.embeddingfor gene-on-embedding scatterbasiskwarg (default"umap") extracted from**kwargs_circular_projection.py—circular_projection()scv.pl.scatter→sc.pl.embeddingfor custom-basis scattercolorbar=→colorbar_loc="right" | None_aggregate_fate_probs.py— PAGA and PAGA_PIE modesfrom scvelo.plotting import paga→sc.pl.pagacolors=→color=(node colors); scatter background drawn viasc.pl.embeddingwhenbasisis setnode_colors=→color=(pie chart dict);scatter_flag/basishandled separately;legend_locpopped (scanpy doesn't accept it)transitions_confidenceonly used when available inadata.uns["paga"]_term_states_estimator.py—_plot_discrete()and_plot_continuous()_plot_discrete:scv.pl.scatter→sc.pl.embedding; group-specificadd_outlinereimplemented via new_add_outline_to_groupshelper (three-layer scatter: background → gap → foreground)_plot_continuousEMBEDDING mode:color_gradientsreimplemented as alpha-blended matplotlib scatter (new_plot_color_gradientshelper)RandomKeys(scanpy requires column names, not raw arrays)percdropped;dpi,color_mapcleaned from kwargs beforesc.pl.embeddingcalls_plot_continuousTIME mode: reimplemented as multi-panel matplotlib scatter (new_plot_time_scatterhelper)New helpers (in
pl/_utils.py):_add_outline_to_groups— group-specific outline via double-scatter, replacing scVelo'sadd_outline=["Alpha", ...]_plot_color_gradients— alpha-blended lineage overlay, replacing scVelo'scolor_gradientsparameter_plot_time_scatter— fate probability vs pseudotime scatter panelsTests
import scvelo as scvfromtest_plotting.py; movedfigdirandtransparentsettings toconftest.py::pytest_sessionstartscvelo_prefix (5 genuine scVelo projection tests keep their names)scvelo>=0.3to thetestdependency group (needed for projection tests)from __future__ import annotationsfrom PR 1 filesBehavioral changes
perckwarg is no longer passed through; usevmin/vmaxinsteadtransitions_confidence) only shown when scVelo's PAGA was usedsave/showare handled by CellRank (popped from kwargs before calling scanpy), matching the pattern used elsewhere in the codebaseTest results