22
33import math
44import os
5+ import warnings
56from collections import OrderedDict
6- from collections .abc import Iterable , Mapping , Sequence
7+ from collections .abc import Callable , Iterable , Mapping , Sequence
78from copy import copy
89from functools import partial
910from pathlib import Path
@@ -2820,7 +2821,17 @@ def _validate_points_render_params(
28202821 colorbar : bool | str | None ,
28212822 colorbar_params : dict [str , object ] | None ,
28222823 gene_symbols : str | None = None ,
2824+ density : bool = False ,
2825+ density_how : Literal ["linear" , "log" , "cbrt" , "eq_hist" ] = "linear" ,
2826+ transfunc : Callable [[float ], float ] | None = None ,
2827+ method : str | None = None ,
28232828) -> dict [str , dict [str , Any ]]:
2829+ if not isinstance (density , bool ):
2830+ raise TypeError ("Parameter 'density' must be a bool." )
2831+ allowed_how = ("linear" , "log" , "cbrt" , "eq_hist" )
2832+ if density_how not in allowed_how :
2833+ raise ValueError (f"Parameter 'density_how' must be one of { allowed_how } ; got { density_how !r} ." )
2834+
28242835 param_dict : dict [str , Any ] = {
28252836 "sdata" : sdata ,
28262837 "element" : element ,
@@ -2840,6 +2851,47 @@ def _validate_points_render_params(
28402851 }
28412852 param_dict = _type_check_params (param_dict , "points" )
28422853
2854+ if density :
2855+ if method == "matplotlib" :
2856+ raise ValueError (
2857+ "density=True requires the datashader backend; got method='matplotlib'. "
2858+ "Either drop method= or set method='datashader'."
2859+ )
2860+ # Literal color (resolved into param_dict["color"] as a Color instance, with
2861+ # col_for_color set to None) is ambiguous with density: it could mean a
2862+ # single-hue cmap or a one-entry palette. Force the user to choose.
2863+ if param_dict ["color" ] is not None and param_dict ["col_for_color" ] is None :
2864+ raise ValueError (
2865+ "density=True with a literal color is ambiguous. Pass cmap= to recolor the "
2866+ "density, or palette= to assign a categorical color, but not color=<literal>."
2867+ )
2868+ # Warn-and-ignore: these parameters do not interact meaningfully with a
2869+ # count-based density and are silently dropped to keep the API consistent.
2870+ if size != 1.0 :
2871+ warnings .warn (
2872+ "size is ignored when density=True; spreading would distort the count signal." ,
2873+ UserWarning ,
2874+ stacklevel = 3 ,
2875+ )
2876+ if transfunc is not None :
2877+ warnings .warn (
2878+ "transfunc is ignored when density=True (no continuous color vector to transform)." ,
2879+ UserWarning ,
2880+ stacklevel = 3 ,
2881+ )
2882+ if isinstance (norm , Normalize ) and (norm .vmin is not None or norm .vmax is not None ):
2883+ warnings .warn (
2884+ "norm.vmin/vmax are ignored when density=True; use density_how= to control intensity mapping." ,
2885+ UserWarning ,
2886+ stacklevel = 3 ,
2887+ )
2888+ if ds_reduction is not None :
2889+ warnings .warn (
2890+ "datashader_reduction is ignored when density=True; counts are forced." ,
2891+ UserWarning ,
2892+ stacklevel = 3 ,
2893+ )
2894+
28432895 element_params : dict [str , dict [str , Any ]] = {}
28442896 for el in param_dict ["element" ]:
28452897 # ensure that the element exists in the SpatialData object
@@ -3715,11 +3767,17 @@ def _datashader_map_aggregate_to_color(
37153767 min_alpha : float = 40 ,
37163768 span : None | list [float ] = None ,
37173769 clip : bool = True ,
3770+ how : str = "linear" ,
37183771) -> ds .tf .Image | np .ndarray [Any , np .dtype [np .uint8 ]]:
37193772 """ds.tf.shade() part, ensuring correct clipping behavior.
37203773
37213774 If necessary (norm.clip=False), split shading in 3 parts and in the end, stack results.
37223775 This ensures the correct clipping behavior, because else datashader would always automatically clip.
3776+
3777+ ``how`` controls the count-to-color mapping passed to :func:`datashader.transfer_functions.shade`
3778+ (``"linear"`` by default; ``"log"``/``"cbrt"``/``"eq_hist"`` compress dynamic range). The split-shade
3779+ branch used for ``norm.clip=False`` always uses ``"linear"`` since per-segment shading would otherwise
3780+ interact poorly with rank-based mappings.
37233781 """
37243782 if not clip and isinstance (cmap , Colormap ) and span is not None :
37253783 # in case we use datashader together with a Normalize object where clip=False
@@ -3768,7 +3826,7 @@ def _datashader_map_aggregate_to_color(
37683826 color_key = color_key ,
37693827 min_alpha = min_alpha ,
37703828 span = span ,
3771- how = "linear" ,
3829+ how = how ,
37723830 )
37733831 return _apply_cmap_alpha_to_datashader_result (result , agg , cmap , span )
37743832
0 commit comments