feat(plotting): Add group_cmaps parameter to sc.pl.dotplot to specify unique colormaps for each group
- [ ] Closes #
- [x] Tests included: New tests were added for the core functionality, swap_axes compatibility, and error conditions.
- [ ] Release notes not necessary because:
Description
This PR introduces a new feature to sc.pl.dotplot: the group_cmaps parameter. This allows users to assign a unique colormap to each category in a dot plot, which is particularly useful for creating publication-ready figures that align with a paper's established color scheme for different cell types.
Key Changes
group_cmapsparameter: Added a new parameter tosc.pl.dotplotand theDotPlotclass that accepts a dictionary mapping group names to colormap names.- Stacked Colorbar Legend: Implemented a new stacked horizontal colorbar legend (via _plot_stacked_colorbars function) that appears when
group_cmapsis used. This legend clearly displays the colormap and for each group with corresponding label. - Robust Error Handling: Added a
ValueErrorthat is raised ifgroup_cmapsis used but a group in the plot data is not defined in the dictionary. - Pre-existing Layout Bug Fix: Set the width of the dot plot legend area within the dotplot wrapper function to 2.0 (instead of DEFAULT_LEGENDS_WIDTH=1.5) to fix a pre-existing issue where the circles of the size legend could overlap (especially with dendrogram=True).
- As a result, the reference images (
expected.png) for several existing dotplot tests were updated to reflect the new, corrected layout.
- As a result, the reference images (
- Code Refactoring: The
DotPlot.__init__method was refactored into a smaller helper function (_prepare_dot_data) to improve readability and pass the automated code complexity checks by pre-commit. - Comprehensive Tests: Added new tests to verify the core functionality, ensure compatibility with
swap_axes, and confirm that the correct errors and warnings are raised.
Example Plot
from testing.scanpy._helpers.data import (pbmc3k_processed)
import scanpy as sc
adata = pbmc3k_processed()
markers = ["SERPINB1", "IGFBP7", "GNLY", "IFITM1", "IMP3", "UBALD2", "LTB", "CLPP"]
group_cmaps = {
"CD4 T cells": "pink_r",
"CD14+ Monocytes": "Reds",
"B cells": "Greys",
"CD8 T cells": "Greens",
"NK cells": "Oranges",
"FCGR3A+ Monocytes": "Purples",
"Dendritic cells": "Blues",
"Megakaryocytes": "bone_r",
}
sc.pl.dotplot(
adata,
markers,
groupby="louvain",
group_cmaps=group_cmaps, # Define custom color maps for each group from 'groupby'
dendrogram=True,
standard_scale='var',
dot_max=1.0,
title="Dot Plot with Unique Per-Group Colormaps\n(pbmc3k_processed)",
)
Good idea! However I’m concerned that the color maps aren’t visually comparable.
-
Did you think about adding a color bar instead, like for the heatmap
-
alternatively, I’d replace the parameter with
group_colorsor so. We could then usecspace_convertto make colormaps on the fly, like this: https://gist.github.com/flying-sheep/a38e925d0b0f7ad5c6eb1ada8fa5c128Unfortunately colorspacious seems unmaintained, and I haven’t figured out to make this code numerically robust. https://github.com/njsmith/colorspacious/issues/18
Thanks for the feedback! The visual comparability is a good point, I didn’t think about that. I personally prefer your second suggestion of making (perceptually uniform) colormaps on the fly. Though, the heatmap-like color bar could be an additional option. I think that should be fairly straightforward to implement.
Since colorspacious is unmaintained, perhaps we can check some alternatives. After a quick search, I found this post on the Oklab color space which might be another way to implement this: Converting from linear sRGB to Oklab. I’ll look into it a bit more some other time and report back!
Hi @flying-sheep,
I finally found some time to further look into this. I started with your GitHub Gist notebook but using the colour package. I used the OKLab color space to generate sequential colormaps by varying lightness while keeping a and b channels constant. And it seems like this can create the colormaps without the out-of-gamut errors or warnings - I think that was the issue you had before, right? I do get the similar artifacts as you in some colorbars when using CAM02-UCS (see third example in the gist linked below).
I added a second example generating white-to-color gradients by interpolating all channels in OKLab. This is the implementation I would use to create colormaps on the fly for the DotPlot feature.
Here's the gist with the examples: Perceptually uniform cmaps
Is this what you had in mind? Then I'd go ahead and integrate this into DotPlot.
I’m super sorry that I didn’t write back. Yes, this is exactly what I was thinking!
:x: 3 Tests Failed:
| Tests completed | Failed | Passed | Skipped |
|---|---|---|---|
| 2263 | 3 | 2260 | 446 |
View the top 3 failed test(s) by shortest run time
src/scanpy/preprocessing/_normalization.py::scanpy.preprocessing._normalization.normalize_totalStack Traces | 0.006s run time
194 ... np.array( 195 ... [ 196 ... [3, 3, 3, 6, 6], 197 ... [1, 1, 1, 2, 2], 198 ... [1, 22, 1, 2, 2], 199 ... ], 200 ... dtype="float32", 201 ... ) 202 ... ) 203 >>> adata.X Differences (unified diff with -expected +actual): @@ -1,3 +1,3 @@ -array([[ 3., 3., 3., 6., 6.], - [ 1., 1., 1., 2., 2.], - [ 1., 22., 1., 2., 2.]], dtype=float32) +array([[ 3., 3., 3., 6., 6.], + [ 1., 1., 1., 2., 2.], + [ 1., 22., 1., 2., 2.]], dtype=float32) #x1B[1m#x1B[.../scanpy/preprocessing/_normalization.py#x1B[0m:203: DocTestFailure
src/scanpy/preprocessing/_simple.py::scanpy.preprocessing._simple.normalize_per_cellStack Traces | 0.008s run time
507 Returns `None` if `copy=False`, else returns an updated `AnnData` object. Sets the following fields: 508 509 `adata.X` : :class:`numpy.ndarray` | :class:`scipy.sparse.csr_matrix` (dtype `float`) 510 Normalized count data matrix. 511 512 Examples 513 -------- 514 >>> import scanpy as sc 515 >>> adata = AnnData(np.array([[1, 0], [3, 0], [5, 6]], dtype=np.float32)) 516 >>> print(adata.X.sum(axis=1)) Expected: [ 1. 3. 11.] Got: [ 1. 3. 11.] #x1B[1m#x1B[.../scanpy/preprocessing/_simple.py#x1B[0m:516: DocTestFailure
src/scanpy/plotting/_baseplot_class.py::scanpy.plotting._baseplot_class.BasePlot.add_dendrogramStack Traces | 0.056s run time
283 284 Examples 285 -------- 286 >>> import scanpy as sc 287 >>> adata = sc.datasets.pbmc68k_reduced() 288 >>> markers = {"T-cell": "CD3D", "B-cell": "CD79A", "myeloid": "CST3"} 289 >>> plot = sc.pl._baseplot_class.BasePlot( 290 ... adata, markers, groupby="bulk_labels" 291 ... ).add_dendrogram() 292 >>> plot.plot_group_extra # doctest: +NORMALIZE_WHITESPACE Expected: {'kind': 'dendrogram', 'width': 0.8, 'dendrogram_key': None, 'dendrogram_ticks': array([0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5])} Got: {'kind': 'dendrogram', 'width': 0.8, 'dendrogram_key': None, 'dendrogram_ticks': array([ 0.5, 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5, 9.5])} #x1B[1m#x1B[.../scanpy/plotting/_baseplot_class.py#x1B[0m:292: DocTestFailure
To view more test analytics, go to the Test Analytics Dashboard 📋 Got 3 mins? Take this short survey to help us improve Test Analytics.