@@ -321,6 +321,90 @@ def _ds_shade_categorical(
321321 return _apply_user_alpha (shaded , alpha )
322322
323323
324+ def _shade_datashader_aggregate (
325+ cvs : ds .Canvas ,
326+ frame : Any ,
327+ * ,
328+ col_for_color : str | None ,
329+ color_vector : Any ,
330+ color_by_categorical : bool ,
331+ norm : Normalize | None ,
332+ cmap : Any ,
333+ na_color : Color ,
334+ alpha : float ,
335+ ds_reduction : _DsReduction | None ,
336+ default_reduction : _DsReduction ,
337+ kind : Literal ["shapes" , "points" ],
338+ spread_px : int | None = None ,
339+ shade_how : str = "linear" ,
340+ density : bool = False ,
341+ uniform_alpha : bool = False ,
342+ strip_alpha_hex : bool = False ,
343+ ) -> tuple [Any , Any | None , tuple [Any , Any ] | None , Any ]:
344+ """Aggregate ``frame`` over ``cvs`` and shade it; the core shared by shapes and points.
345+
346+ Runs the steps every datashader caller repeats: ``_ds_aggregate`` -> ``_apply_ds_norm`` ->
347+ transparent-NA guard -> ``_build_color_key`` -> categorical/continuous shade dispatch. Returns
348+ ``(shaded, nan_shaded, reduction_bounds, color_vector)``; ``color_vector`` is returned because the
349+ points path strips hex alpha off it and the caller's legend must match. Callers keep their
350+ element-specific prep (geometry transform / point parse), outline rendering, ``_render_ds_image``
351+ and ``_build_ds_colorbar``.
352+ """
353+ agg , reduction_bounds , nan_agg = _ds_aggregate (
354+ cvs , frame , col_for_color , color_by_categorical , ds_reduction , default_reduction , kind
355+ )
356+ agg , color_span = _apply_ds_norm (agg , norm )
357+ na_color_hex = _hex_no_alpha (na_color .get_hex ())
358+ if na_color .is_fully_transparent ():
359+ nan_agg = None
360+ color_key = _build_color_key (frame , col_for_color , color_by_categorical , color_vector , na_color_hex )
361+
362+ if (
363+ strip_alpha_hex
364+ and color_vector is not None
365+ and len (color_vector ) > 0
366+ and isinstance (color_vector [0 ], str )
367+ and color_vector [0 ].startswith ("#" )
368+ ):
369+ # color_vector usually holds only a few distinct hex strings (one per category), so strip
370+ # alpha on the unique values and map back rather than parsing once per point.
371+ unique_hex , inverse = np .unique (color_vector , return_inverse = True )
372+ color_vector = np .asarray ([_hex_no_alpha (c ) for c in unique_hex ])[inverse ]
373+
374+ # density without a color column collapses to a sequential count gradient; everything else with no
375+ # explicit continuous value (categorical or no color) goes through the categorical shader.
376+ plain_density = density and col_for_color is None
377+ nan_shaded = None
378+ if not plain_density and (color_by_categorical or col_for_color is None ):
379+ shaded = _ds_shade_categorical (
380+ agg ,
381+ color_key ,
382+ color_vector ,
383+ alpha ,
384+ spread_px = spread_px ,
385+ how = shade_how ,
386+ density = density ,
387+ uniform_alpha = uniform_alpha ,
388+ )
389+ else :
390+ shaded , nan_shaded , reduction_bounds = _ds_shade_continuous (
391+ agg ,
392+ color_span ,
393+ norm ,
394+ cmap ,
395+ alpha ,
396+ reduction_bounds ,
397+ nan_agg ,
398+ na_color_hex ,
399+ spread_px = spread_px ,
400+ ds_reduction = ds_reduction ,
401+ how = shade_how ,
402+ uniform_alpha = uniform_alpha ,
403+ )
404+
405+ return shaded , nan_shaded , reduction_bounds , color_vector
406+
407+
324408# ---------------------------------------------------------------------------
325409# Image rendering
326410# ---------------------------------------------------------------------------
0 commit comments