@@ -90,6 +90,25 @@ def __getattr__(attr):
9090 raise AttributeError (f"module { __name__ !r} has no attribute { attr !r} " )
9191
9292
93+ class AggState (enum .Enum ):
94+ AGG_ONLY = 0 # Only aggregator
95+ AGG_BY = 1 # Aggregator where the aggregator is ds.by
96+ AGG_SEL = 2 # Selector and aggregator
97+ AGG_SEL_BY = 3 # Selector and aggregator, where the aggregator is ds.by
98+
99+ def get_state (agg_fn , sel_fn ):
100+ if isinstance (agg_fn , ds .by ):
101+ return AggState .AGG_SEL_BY if sel_fn else AggState .AGG_BY
102+ else :
103+ return AggState .AGG_SEL if sel_fn else AggState .AGG_ONLY
104+
105+ def has_sel (state ):
106+ return state in (AggState .AGG_SEL , AggState .AGG_SEL_BY )
107+
108+ def has_by (state ):
109+ return state in (AggState .AGG_BY , AggState .AGG_SEL_BY )
110+
111+
93112class AggregationOperation (ResampleOperation2D ):
94113 """AggregationOperation extends the ResampleOperation2D defining an
95114 aggregator parameter used to define a datashader Reduction.
@@ -228,6 +247,77 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds):
228247 params ['vdims' ] = vdims
229248 return params
230249
250+ def _get_agg_state (self , element ):
251+ agg_fn = self ._get_aggregator (element , self .p .aggregator )
252+ sel_fn = getattr (self .p , "selector" , None )
253+ agg_state = AggState .get_state (agg_fn , sel_fn )
254+
255+ if DATASHADER_GE_0_15_1 and sel_fn and sel_fn .column is None :
256+ sel_fn = type (sel_fn )(column = rd .SpecialColumn .RowIndex )
257+ if AggState .has_by (agg_state ) and self .p .element_type is Image :
258+ self .p .element_type = ImageStack
259+
260+ return agg_fn , sel_fn , agg_state
261+
262+ def _apply_aggregate_with_agg_state (self , dfdata , cvs_fn , agg_fn , x , y , agg_state , sel_fn , params ):
263+ if AggState .has_sel (agg_state ):
264+ if isinstance (params ["vdims" ], (Dimension , str )):
265+ params ["vdims" ] = [params ["vdims" ]]
266+ sum_agg = ds .summary (** {str (params ["vdims" ][0 ]): agg_fn , "__index__" : ds .where (sel_fn )})
267+ agg = self ._apply_datashader (dfdata , cvs_fn , sum_agg , x , y , agg_state )
268+ agg .attrs ["selector" ] = (
269+ str (sel_fn )
270+ if DATASHADER_GE_0_18_1
271+ else f"{ type (sel_fn ).__name__ } ({ getattr (sel_fn , 'column' , '...' )!r} )"
272+ ).replace (repr (rd .SpecialColumn .RowIndex ), "" )
273+ else :
274+ agg = self ._apply_datashader (dfdata , cvs_fn , agg_fn , x , y , agg_state )
275+
276+ return agg
277+
278+ def _apply_where_summary (self , dfdata , x_name , y_name , agg_fn , agg , agg_state ):
279+ is_where_index = DATASHADER_GE_0_15_1 and isinstance (agg_fn , ds .where ) and isinstance (agg_fn .column , rd .SpecialColumn )
280+ is_summary_index = AggState .has_sel (agg_state )
281+ if is_where_index or is_summary_index :
282+ if is_where_index :
283+ index = agg .data
284+ agg = agg .to_dataset (name = "__index__" )
285+ else : # summary index
286+ index = agg ["__index__" ].data
287+ if agg_state == AggState .AGG_SEL_BY :
288+ main_dim = next (k for k in agg if k != "__index__" )
289+ # Taking values from the main dimension expanding it to
290+ # a new dataset
291+ agg = agg [main_dim ].to_dataset (dim = list (agg .sizes )[2 ])
292+ agg ["__index__" ] = ((y_name , x_name ), index )
293+
294+ neg1 = index == - 1
295+ agg .attrs ["selector_columns" ] = sel_cols = ["__index__" ]
296+ for col in dfdata .columns :
297+ if col in agg .coords :
298+ continue
299+ val = dfdata [col ].values [index ]
300+ if dtype_kind (val ) == 'f' :
301+ val [neg1 ] = np .nan
302+ elif isinstance (val .dtype , pd .CategoricalDtype ):
303+ val = val .to_numpy ()
304+ val [neg1 ] = "-"
305+ elif dtype_kind (val ) == "O" :
306+ val [neg1 ] = "-"
307+ elif dtype_kind (val ) == "M" :
308+ val [neg1 ] = np .datetime64 ("NaT" )
309+ else :
310+ val = val .astype (np .float64 )
311+ val [neg1 ] = np .nan
312+ agg [col ] = ((y_name , x_name ), val )
313+ sel_cols .append (col )
314+
315+ if agg_state == AggState .AGG_BY :
316+ col = agg_fn .column
317+ if '' in agg .coords [col ]:
318+ agg = agg .drop_sel (** {col : '' })
319+
320+ return agg
231321
232322
233323class LineAggregationOperation (AggregationOperation ):
@@ -246,26 +336,6 @@ class LineAggregationOperation(AggregationOperation):
246336 appearance of a subpixel line width.""" )
247337
248338
249-
250- class AggState (enum .Enum ):
251- AGG_ONLY = 0 # Only aggregator
252- AGG_BY = 1 # Aggregator where the aggregator is ds.by
253- AGG_SEL = 2 # Selector and aggregator
254- AGG_SEL_BY = 3 # Selector and aggregator, where the aggregator is ds.by
255-
256- def get_state (agg_fn , sel_fn ):
257- if isinstance (agg_fn , ds .by ):
258- return AggState .AGG_SEL_BY if sel_fn else AggState .AGG_BY
259- else :
260- return AggState .AGG_SEL if sel_fn else AggState .AGG_ONLY
261-
262- def has_sel (state ):
263- return state in (AggState .AGG_SEL , AggState .AGG_SEL_BY )
264-
265- def has_by (state ):
266- return state in (AggState .AGG_BY , AggState .AGG_SEL_BY )
267-
268-
269339class aggregate (LineAggregationOperation ):
270340 """aggregate implements 2D binning for any valid HoloViews Element
271341 type using datashader. I.e., this operation turns a HoloViews
@@ -381,20 +451,12 @@ def get_agg_data(cls, obj, category=None):
381451 df [d .name ] = cast_array_to_int64 (vals )
382452 return x , y , Dataset (df , kdims = kdims , vdims = vdims ), glyph
383453
384-
385454 def _process (self , element , key = None ):
386- agg_fn = self ._get_aggregator (element , self .p .aggregator )
387- sel_fn = getattr (self .p , "selector" , None )
455+ agg_fn , sel_fn , agg_state = self ._get_agg_state (element )
388456 if hasattr (agg_fn , 'cat_column' ):
389457 category = agg_fn .cat_column
390458 else :
391459 category = agg_fn .column if isinstance (agg_fn , ds .count_cat ) else None
392- if DATASHADER_GE_0_15_1 and sel_fn and sel_fn .column is None :
393- sel_fn = type (sel_fn )(column = rd .SpecialColumn .RowIndex )
394- agg_state = AggState .get_state (agg_fn , sel_fn )
395-
396- if AggState .has_by (agg_state ) and self .p .element_type is Image :
397- self .p .element_type = ImageStack
398460
399461 if overlay_aggregate .applies (element , agg_fn , line_width = self .p .line_width , sel_fn = sel_fn ):
400462 params = dict (
@@ -426,25 +488,9 @@ def _process(self, element, key=None):
426488 cvs = ds .Canvas (plot_width = width , plot_height = height ,
427489 x_range = x_range , y_range = y_range )
428490
429- agg_kwargs = {}
430- if self .p .line_width and glyph == 'line' and DATASHADER_GE_0_14_0 :
431- agg_kwargs ['line_width' ] = self .p .line_width
432-
433491 dfdata = PandasInterface .as_dframe (data )
434492 cvs_fn = getattr (cvs , glyph )
435-
436- if AggState .has_sel (agg_state ):
437- if isinstance (params ["vdims" ], (Dimension , str )):
438- params ["vdims" ] = [params ["vdims" ]]
439- sum_agg = ds .summary (** {str (params ["vdims" ][0 ]): agg_fn , "__index__" : ds .where (sel_fn )})
440- agg = self ._apply_datashader (dfdata , cvs_fn , sum_agg , agg_kwargs , x , y , agg_state )
441- agg .attrs ["selector" ] = (
442- str (sel_fn )
443- if DATASHADER_GE_0_18_1
444- else f"{ type (sel_fn ).__name__ } ({ getattr (sel_fn , 'column' , '...' )!r} )"
445- ).replace (repr (rd .SpecialColumn .RowIndex ), "" )
446- else :
447- agg = self ._apply_datashader (dfdata , cvs_fn , agg_fn , agg_kwargs , x , y , agg_state )
493+ agg = self ._apply_aggregate_with_agg_state (dfdata , cvs_fn , agg_fn , x , y , agg_state , sel_fn , params )
448494
449495 if 'x_axis' in agg .coords and 'y_axis' in agg .coords :
450496 agg = agg .rename ({'x_axis' : x , 'y_axis' : y })
@@ -459,7 +505,11 @@ def _process(self, element, key=None):
459505 params ['vdims' ] = [d for d in agg .data_vars if d not in agg .attrs ["selector_columns" ]]
460506 return self .p .element_type (agg , ** params )
461507
462- def _apply_datashader (self , dfdata , cvs_fn , agg_fn , agg_kwargs , x , y , agg_state : AggState ):
508+ def _apply_datashader (self , dfdata , cvs_fn , agg_fn , x , y , agg_state : AggState ):
509+ agg_kwargs = {}
510+ if cvs_fn .__name__ == "line" and DATASHADER_GE_0_14_0 :
511+ agg_kwargs ['line_width' ] = self .p .line_width
512+
463513 # Suppress numpy warning emitted by dask:
464514 # https://github.com/dask/dask/issues/8439
465515 with warnings .catch_warnings ():
@@ -468,49 +518,8 @@ def _apply_datashader(self, dfdata, cvs_fn, agg_fn, agg_kwargs, x, y, agg_state:
468518 category = FutureWarning
469519 )
470520 agg = cvs_fn (dfdata , x .name , y .name , agg_fn , ** agg_kwargs )
521+ return self ._apply_where_summary (dfdata , x .name , y .name , agg_fn , agg , agg_state )
471522
472- is_where_index = DATASHADER_GE_0_15_1 and isinstance (agg_fn , ds .where ) and isinstance (agg_fn .column , rd .SpecialColumn )
473- is_summary_index = AggState .has_sel (agg_state )
474- if is_where_index or is_summary_index :
475- if is_where_index :
476- index = agg .data
477- agg = agg .to_dataset (name = "__index__" )
478- else : # summary index
479- index = agg ["__index__" ].data
480- if agg_state == AggState .AGG_SEL_BY :
481- main_dim = next (k for k in agg if k != "__index__" )
482- # Taking values from the main dimension expanding it to
483- # a new dataset
484- agg = agg [main_dim ].to_dataset (dim = list (agg .sizes )[2 ])
485- agg ["__index__" ] = ((y .name , x .name ), index )
486-
487- neg1 = index == - 1
488- agg .attrs ["selector_columns" ] = sel_cols = ["__index__" ]
489- for col in dfdata .columns :
490- if col in agg .coords :
491- continue
492- val = dfdata [col ].values [index ]
493- if dtype_kind (val ) == 'f' :
494- val [neg1 ] = np .nan
495- elif isinstance (val .dtype , pd .CategoricalDtype ):
496- val = val .to_numpy ()
497- val [neg1 ] = "-"
498- elif dtype_kind (val ) == "O" :
499- val [neg1 ] = "-"
500- elif dtype_kind (val ) == "M" :
501- val [neg1 ] = np .datetime64 ("NaT" )
502- else :
503- val = val .astype (np .float64 )
504- val [neg1 ] = np .nan
505- agg [col ] = ((y .name , x .name ), val )
506- sel_cols .append (col )
507-
508- if agg_state == AggState .AGG_BY :
509- col = agg_fn .column
510- if '' in agg .coords [col ]:
511- agg = agg .drop_sel (** {col : '' })
512-
513- return agg
514523
515524class curve_aggregate (aggregate ):
516525 """Optimized aggregation for Curve objects by setting the default
@@ -778,8 +787,16 @@ class geom_aggregate(AggregationOperation):
778787 def _aggregate (self , cvs , df , x0 , y0 , x1 , y1 , agg ):
779788 raise NotImplementedError
780789
790+ def _apply_datashader (self , df , cvs , agg_fn , x , y , agg_state : AggState ):
791+ (x0 , x1 ), (y0 , y1 ) = x , y
792+ agg = self ._aggregate (cvs , df , x0 , y0 , x1 , y1 , agg_fn )
793+ if isinstance (agg , xr .DataArray ):
794+ agg = agg .transpose ('x' , 'y' , ...)
795+ x_name , y_name = list (agg .dims )[:2 ]
796+ return self ._apply_where_summary (df , x_name , y_name , agg_fn , agg , agg_state )
797+
781798 def _process (self , element , key = None ):
782- agg_fn = self ._get_aggregator (element , self . p . aggregator )
799+ agg_fn , sel_fn , agg_state = self ._get_agg_state (element )
783800 x0d , y0d , x1d , y1d = element .kdims
784801 info = self ._get_sampling (element , [x0d , x1d ], [y0d , y1d ], ndim = 1 )
785802 (x_range , y_range ), (xs , ys ), (width , height ), (xtype , ytype ) = info
@@ -806,25 +823,30 @@ def _process(self, element, key=None):
806823 cvs = ds .Canvas (plot_width = width , plot_height = height ,
807824 x_range = x_range , y_range = y_range )
808825
809- agg = self ._aggregate (cvs , df , x0d .name , y0d .name , x1d .name , y1d .name , agg_fn )
826+ agg = self ._apply_aggregate_with_agg_state (
827+ df ,
828+ cvs ,
829+ agg_fn ,
830+ (x0d .name , x1d .name ),
831+ (y0d .name , y1d .name ),
832+ agg_state ,
833+ sel_fn ,
834+ params
835+ )
810836
811- xdim , ydim = list (agg .dims )[:2 ][:: - 1 ]
837+ xdim , ydim = list (agg .dims )[:2 ]
812838 if xtype == "datetime" :
813839 agg [xdim ] = agg [xdim ].astype ('datetime64[ns]' )
814840 if ytype == "datetime" :
815841 agg [ydim ] = agg [ydim ].astype ('datetime64[ns]' )
816842
817843 params ['kdims' ] = [xdim , ydim ]
818844
819- if agg .ndim == 2 :
820- # Replacing x and y coordinates to avoid numerical precision issues
821- return self .p .element_type (agg , ** params )
822- else :
823- layers = {}
824- for c in agg .coords [agg_fn .column ].data :
825- cagg = agg .sel (** {agg_fn .column : c })
826- layers [c ] = self .p .element_type (cagg , ** params )
827- return NdOverlay (layers , kdims = [element .get_dimension (agg_fn .column )])
845+ if agg_state == AggState .AGG_BY :
846+ params ['vdims' ] = list (map (str , agg .coords [agg_fn .column ].data ))
847+ elif agg_state == AggState .AGG_SEL_BY :
848+ params ['vdims' ] = [d for d in agg .data_vars if d not in agg .attrs ["selector_columns" ]]
849+ return self .p .element_type (agg , ** params )
828850
829851
830852class segments_aggregate (geom_aggregate , LineAggregationOperation ):
0 commit comments