Skip to content

Commit a7d5486

Browse files
committed
enh: Support selector for geom_aggregator
1 parent ec851b7 commit a7d5486

2 files changed

Lines changed: 125 additions & 101 deletions

File tree

holoviews/operation/datashader.py

Lines changed: 123 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
93112
class 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

233323
class 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-
269339
class 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

515524
class 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

830852
class segments_aggregate(geom_aggregate, LineAggregationOperation):

holoviews/plotting/bokeh/raster.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ def _init_tools(self, element, callbacks=None):
9898
for vdim in map(str, element.vdims):
9999
if vdim in vars:
100100
vars.remove(vdim)
101+
if "__index__" not in vars:
102+
vars.append("__index__")
101103

102104
hover_model = HoverModel(data={})
103105
dims = (*coords, *vars)

0 commit comments

Comments
 (0)