Skip to content

Conversation

@rwbaber
Copy link

@rwbaber rwbaber commented Aug 16, 2025

  • Closes #
  • Tests included: New tests were added for the core functionality, swap_axes compatibility, and error conditions.

Description

This PR introduces a new feature to sc.pl.dotplot: the group_colors parameter. This allows users to assign a unique color to each category in a dot plot, which automatically generates perceptually uniform white-to-color gradients using the OKLab color space. This is particularly useful for creating publication-ready figures that align with a paper's established color scheme for different cell types.

Key Changes

New Feature: group_colors parameter

  • Added a new parameter to sc.pl.dotplot and the DotPlot class that accepts a dictionary mapping group names to colors (e.g., {'T-cell': 'blue', 'B-cell': '#aa40fc'}).
  • Colors can be specified as any valid matplotlib color (named colors, hex codes, RGB tuples, etc.).
  • For each group, a perceptually uniform gradient from white to the specified color is generated using the OKLab color space via the colour-science library.

OKLab Color Gradient Function

  • Added _create_white_to_color_gradient() function in _utils.py that creates smooth, perceptually uniform colormaps from white to any target color.
  • Uses the OKLab color space for interpolation, which avoids out-of-gamut errors and visual artifacts common in standard RGB interpolation.

Optional Dependency: colour-science

  • Added colour-science as an optional dependency under [project.optional-dependencies.plotting] in pyproject.toml.
  • When group_colors is used without colour-science installed, a clear ImportError is raised with installation instructions.
  • Added colour marker in marks.py for tests that require this dependency.

Stacked Colorbar Legend

  • Implemented a stacked colorbar legend (via _plot_stacked_colorbars method) that displays individual colorbars for each group when group_colors is used.
  • The legend shows the gradient colormap for each group with corresponding labels.

Fallback Behavior & Warnings

  • Groups not specified in group_colors fall back to the default colormap (or a custom cmap if provided).
  • A UserWarning is emitted listing which groups will use the default colormap.
  • A UserWarning is raised when both cmap and group_colors are specified, informing users that cmap will only be used as a fallback.

Internal Refactoring

  • Refactored the DotPlot.__init__ method by extracting logic into a new helper function _prepare_dot_data.
  • This reduces cyclomatic complexity to meet pre-commit standards and improves the readability of the initialization process.

Layout & Testing Improvements

Dotplot Legend Spacing

  • Change: Increased the legend width allocation from 1.5 to 2.0 within the dotplot wrapper.
  • Reason: This fixes a pre-existing layout issue where size legend circles would overlap (especially when dendrogram=True).
  • Impact: Reference images (expected.png) for several existing dotplot tests have been updated to reflect this cleaner, non-overlapping layout.

Test Suite Adjustments

  • New Tests: Added 6 comprehensive tests covering dependency checks, image comparison, axis swapping, and fallback warnings.
  • Coverage & CI: Added test_dotplot_group_colors_coverage_mock to ensure full coverage without adding scanpy[plotting] to pyproject.toml, which prevents regressions in low-vers CI environments and led to failure of the patch coverage test.
  • Tolerance Update: Increased image comparison tolerance to 25 for test_rank_genes_groups to prevent flaky failures caused by minor rendering shifts in the new environment.

Files Modified

  • src/scanpy/plotting/_dotplot.py: Added group_colors logic, legend handling, and layout width update.
  • src/scanpy/plotting/_utils.py: Added _create_white_to_color_gradient() helper.
  • tests/test_plotting.py: Added group_colors test cases, a mock coverage test, and updated tolerance for rank genes.
  • pyproject.toml: Added colour-science to optional dependencies.
  • src/testing/scanpy/_pytest/marks.py - Added colour marker for optional dependency

Example Usage

import scanpy as sc

adata = sc.datasets.pbmc68k_reduced()

# Define markers and group colors
markers = ["CD3D", "CST3", "LYZ", "SERPINB1", "IGFBP7", "GNLY", "IFITM1", "IMP3", "UBALD2", "LTB", "CLPP"]

# Define colors for each group with any valid matplotlib color specification
# these will be used to generate white-to-color gradients
group_colors = {
    "CD14+ Monocyte": (0.1, 0.2, 0.5),            # RGB tuple (floats 0..1)
    "Dendritic": ("#02ecfc", 0.9),                # (color, alpha)
    "CD8+ Cytotoxic T": "0.5",                    # Grayscale string ('0'..'1')
    "CD8+/CD45RA+ Naive Cytotoxic": "m",          # Short single-letter name
    "CD4+/CD45RA+/CD25- Naive T": "#D868B2",      # Hex string
    "CD4+/CD25 T Reg": "tab:olive",               # Tableau color name ('tab:...')
  # "CD4+/CD45RO+ Memory": "xkcd:sky blue",       # Not defined: fallback to default cmap 'Reds'
    "CD19+ B": "crimson",                         # CSS4/X11 named color
    "CD56+ NK": "#c06636",                        # Hex string
    "CD34+": "C2",                                # Matplotlib color-cycle shorthand ('C0', 'C1', ...)
}

sc.pl.dotplot(
    adata,
    markers,
    groupby="bulk_labels",
    group_colors=group_colors, # 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(pbmc68k_reduced)",
)
output

@rwbaber rwbaber force-pushed the feature/dotplot-group-colors branch from b5e8003 to 6855232 Compare August 17, 2025 08:23
@flying-sheep
Copy link
Member

Good idea! However I’m concerned that the color maps aren’t visually comparable.

@rwbaber
Copy link
Author

rwbaber commented Aug 22, 2025

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!

@rwbaber
Copy link
Author

rwbaber commented Sep 29, 2025

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.

@flying-sheep
Copy link
Member

I’m super sorry that I didn’t write back. Yes, this is exactly what I was thinking!

@rwbaber rwbaber force-pushed the feature/dotplot-group-colors branch 5 times, most recently from f8e225e to 39cb5d0 Compare December 21, 2025 15:46
@rwbaber rwbaber changed the title feat(plotting): Add group_cmaps parameter to sc.pl.dotplot to specify unique colormaps for each group feat(plotting): Add group_colors parameter to sc.pl.dotplot to specify unique, perceptually uniform colormaps for each group Dec 21, 2025
@rwbaber rwbaber force-pushed the feature/dotplot-group-colors branch from a161c5b to e897d9f Compare December 21, 2025 23:36
@codecov
Copy link

codecov bot commented Dec 21, 2025

Codecov Report

❌ Patch coverage is 92.64706% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.02%. Comparing base (28a1ed4) to head (762609c).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/scanpy/plotting/_dotplot.py 91.73% 10 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3764      +/-   ##
==========================================
+ Coverage   76.89%   77.02%   +0.12%     
==========================================
  Files         117      117              
  Lines       12450    12539      +89     
==========================================
+ Hits         9574     9658      +84     
- Misses       2876     2881       +5     
Flag Coverage Δ
hatch-test.pre 77.02% <92.64%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/scanpy/plotting/_utils.py 78.32% <100.00%> (+0.75%) ⬆️
src/scanpy/plotting/_dotplot.py 93.43% <91.73%> (-0.06%) ⬇️

@rwbaber rwbaber force-pushed the feature/dotplot-group-colors branch from e897d9f to 26060d2 Compare December 21, 2025 23:46
… coloring

- Adds the group_cmaps parameter to sc.pl.dotplot, allowing users to specify a unique colormap for each group.

- Implements a corresponding stacked colorbar legend (_plot_stacked_colorbars) to display the multiple colormaps, with group labels for clarity.

- Adds robust error handling, raising a ValueError if a plotted group is not defined in the group_cmaps dictionary.

- Fixes a pre-existing layout bug (dots overlapping in size legend, especially when dendrogram=True) by setting the legends width to 2.0 (compared to DEFAULT_LEGENDS_WIDTH = 1.5) inside the dotplot wrapper function.

- Refactors data preparation logic from DotPlot.__init__ into a new _prepare_dot_data helper method. This was done to improve code quality and resolve pre-commit linter errors related to complexity.

- Includes comprehensive tests for the new functionality, swap-axis compatibility, and error conditions.
…erpolation

- Implemented group_colors parameter accepting dict of colors
- Added OKLab white-to-color gradient generation in _utils.py
- Updated DotPlot to use dynamic ListedColormaps
- Added colour-science as optional dependency
- Updated default legend width to 2.0 to fix layout bugs
- Updated reference images for dotplots and affected plots
@rwbaber rwbaber force-pushed the feature/dotplot-group-colors branch from 5155c5e to 4b15dd6 Compare December 23, 2025 01:00
@rwbaber rwbaber force-pushed the feature/dotplot-group-colors branch from 9c20b29 to 5776567 Compare December 23, 2025 01:14
@rwbaber rwbaber force-pushed the feature/dotplot-group-colors branch from 456adc8 to b20011a Compare December 23, 2025 01:26
@rwbaber
Copy link
Author

rwbaber commented Dec 23, 2025

@flying-sheep No worries! I had not found the time before anyway.

But now I integrated the colour-science package to generate perceptually uniform white-to-color gradients via the OKLab color space. This avoids the out-of-gamut issues we discussed and produces really clean gradients for the dot plots. I added it as an optional dependency in pyproject.toml.

A few additional improvements I included:

  1. Legend Layout Fix: I slightly increased the default legend width (from 1.5 to 2.0) to prevent the size legend circles from overlapping, which required updating dot plot reference images.
  2. Refactoring: I extracted data logic into _prepare_dot_data to keep init readable and satisfy complexity checks.
  3. Docs & Tests: Added full test coverage for the new parameter, including checks for the optional dependency. I initially tried adding scanpy[plotting] to pyproject.toml, but this caused the low-vers compatibility tests to fail due to dependency upgrades breaking strict DocTests. To fix coverage without destabilizing the CI, I reverted that change and added a mock test.

I’ve updated the PR description at the top with the full technical details and an example snippet. Let me know if this needs any other tweaks!

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.

2 participants