New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add Datashader operations #894
Changes from 10 commits
dfa037d
c6422b7
e0874c0
ba30243
40f1153
345c413
08ef8e0
d5fc734
b4d3fdf
0488c52
5ce5bab
83acee2
189a231
4dee22f
97381b2
6046f9e
088a1b7
d23d873
ac04550
7b8a92a
10ea44e
d602fde
e151dce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -70,81 +70,6 @@ def get_overlay_bounds(cls, overlay): | |
raise ValueError("Extents across the overlay are inconsistent") | ||
|
||
|
||
class Dynamic(Operation): | ||
""" | ||
Dynamically applies a callable to the Elements in any HoloViews | ||
object. Will return a DynamicMap wrapping the original map object, | ||
which will lazily evaluate when a key is requested. By default | ||
Dynamic applies a no-op, making it useful for converting HoloMaps | ||
to a DynamicMap. | ||
|
||
Any supplied kwargs will be passed to the callable and any streams | ||
will be instantiated on the returned DynamicMap. | ||
""" | ||
|
||
callable = param.Callable(default=lambda x: x, doc=""" | ||
Function or ElementOperation to apply to DynamicMap items | ||
dynamically.""") | ||
|
||
kwargs = param.Dict(default={}, doc=""" | ||
Keyword arguments passed to the function.""") | ||
|
||
streams = param.List(default=[], doc=""" | ||
List of streams to attach to the returned DynamicMap""") | ||
|
||
def __call__(self, map_obj, **params): | ||
self.p = param.ParamOverrides(self, params) | ||
callback = self._dynamic_operation(map_obj) | ||
if isinstance(map_obj, DynamicMap): | ||
dmap = map_obj.clone(callback=callback, shared_data=False) | ||
else: | ||
dmap = self._make_dynamic(map_obj, callback) | ||
if isinstance(self.p.callable, ElementOperation): | ||
return dmap.clone(streams=[s() for s in self.p.streams]) | ||
return dmap | ||
|
||
|
||
def _process(self, element): | ||
if isinstance(self.p.callable, Operation): | ||
return self.p.callable.process_element(element, **self.p.kwargs) | ||
else: | ||
return self.p.callable(element, **self.p.kwargs) | ||
|
||
|
||
def _dynamic_operation(self, map_obj): | ||
""" | ||
Generate function to dynamically apply the operation. | ||
Wraps an existing HoloMap or DynamicMap. | ||
""" | ||
if not isinstance(map_obj, DynamicMap): | ||
def dynamic_operation(*key, **kwargs): | ||
self.p.kwargs.update(kwargs) | ||
return self._process(map_obj[key]) | ||
return dynamic_operation | ||
|
||
def dynamic_operation(*key, **kwargs): | ||
key = key[0] if map_obj.mode == 'open' else key | ||
self.p.kwargs.update(kwargs) | ||
_, el = util.get_dynamic_item(map_obj, map_obj.kdims, key) | ||
return self._process(el) | ||
|
||
return dynamic_operation | ||
|
||
|
||
def _make_dynamic(self, hmap, dynamic_fn): | ||
""" | ||
Accepts a HoloMap and a dynamic callback function creating | ||
an equivalent DynamicMap from the HoloMap. | ||
""" | ||
if isinstance(hmap, ViewableElement): | ||
return DynamicMap(dynamic_fn, kdims=[]) | ||
dim_values = zip(*hmap.data.keys()) | ||
params = util.get_param_values(hmap) | ||
kdims = [d(values=list(set(values))) for d, values in | ||
zip(hmap.kdims, dim_values)] | ||
return DynamicMap(dynamic_fn, **dict(params, kdims=kdims)) | ||
|
||
|
||
|
||
class ElementOperation(Operation): | ||
""" | ||
|
@@ -153,6 +78,12 @@ class ElementOperation(Operation): | |
input, a processed holomap is returned as output where the | ||
individual elements have been transformed accordingly. An | ||
ElementOperation may turn overlays in new elements or vice versa. | ||
|
||
An ElementOperation can be set to be dynamic, which will return a | ||
DynamicMap with a callback that will apply the operation | ||
dynamically. An ElementOperation may also supply a list of Stream | ||
classes on the streams attribute, which can allow dynamic control | ||
over the parameters on the operation. | ||
""" | ||
|
||
dynamic = param.ObjectSelector(default='default', | ||
|
@@ -182,7 +113,7 @@ def _process(self, view, key=None): | |
raise NotImplementedError | ||
|
||
|
||
def process_element(self, element, key=None, **params): | ||
def process_element(self, element, key, **params): | ||
""" | ||
The process_element method allows a single element to be | ||
operated on given an externally supplied key. | ||
|
@@ -204,9 +135,10 @@ def __call__(self, element, **params): | |
processed = GridSpace(grid_data, label=element.label, | ||
kdims=element.kdims) | ||
elif dynamic: | ||
from ..util import Dynamic | ||
streams = getattr(self, 'streams', []) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feature probably needs mentioning in the docstring wherever |
||
processed = Dynamic(element, streams=streams, | ||
callable=self, kwargs=params) | ||
operation=self, kwargs=params) | ||
elif isinstance(element, ViewableElement): | ||
processed = self._process(element) | ||
elif isinstance(element, DynamicMap): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,15 +9,15 @@ | |
import datashader as ds | ||
import datashader.transfer_functions as tf | ||
|
||
import datashader.core | ||
from datashader.core import bypixel | ||
from datashader.pandas import pandas_pipeline | ||
from datashape.dispatch import dispatch | ||
from datashape import discover as dsdiscover | ||
|
||
from ..core import (ElementOperation, Element, Dimension, NdOverlay, | ||
Overlay, CompositeOverlay, Dataset) | ||
from ..core.util import get_param_values | ||
from ..core.data import ArrayInterface, PandasInterface | ||
from ..core.util import get_param_values, basestring | ||
from ..element import GridImage, Path, Curve, Contours, RGB | ||
from ..streams import RangeXY | ||
|
||
|
@@ -28,7 +28,10 @@ def discover(dataset): | |
Allows datashader to correctly discover the dtypes of the data | ||
in a holoviews Element. | ||
""" | ||
return dsdiscover(dataset.dframe().head()) | ||
if isinstance(dataset.interface, (PandasInterface, ArrayInterface)): | ||
return dsdiscover(dataset.data) | ||
else: | ||
return dsdiscover(dataset.dframe()) | ||
|
||
|
||
@bypixel.pipeline.register(Element) | ||
|
@@ -72,18 +75,22 @@ class Aggregate(ElementOperation): | |
""" | ||
Aggregate implements 2D binning for any valid HoloViews Element | ||
type using datashader. By default it will simply count the number | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe add a sentence saying "I.e., this operation turns a HoloViews Element or overlay of Elements into an hv.Image or an overlay of hv.Images by rasterizing it, which provides a fixed-sized representation independent of the original dataset size. |
||
of values in each bin but custom aggregators can be supplied | ||
of values in each bin but other aggregators can be supplied | ||
implementing mean, max, min and other reduction operations. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wouldn't call them "custom"; they are just other aggregators. So maybe "but other aggregators can be supplied explicitly". |
||
|
||
The bins of the aggregate are defined by the width and height and | ||
the x_range and y_range. If x_sampling or y_sampling are supplied | ||
the operation will ensure that a bin is no smaller than the | ||
minimum sampling distance. | ||
minimum sampling distance by reducing the width and height when | ||
the zoomed in beyond the minimum sampling distance. | ||
""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How? |
||
|
||
aggregator = param.ClassSelector(class_=ds.reductions.Reduction, | ||
default=ds.count()) | ||
|
||
dynamic = param.Boolean(default=True, doc=""" | ||
Enables dynamic processing by default.""") | ||
|
||
height = param.Integer(default=800, doc=""" | ||
The height of the aggregated image in pixels.""") | ||
|
||
|
@@ -157,10 +164,10 @@ def _process(self, element, key=None): | |
|
||
# Compute highest allowed sampling density | ||
width, height = self.p.width, self.p.height | ||
if self.x_sampling: | ||
if self.p.x_sampling: | ||
x_range = xend - xstart | ||
width = int(min([(x_range/self.p.x_sampling), width])) | ||
if self.y_sampling: | ||
if self.p.y_sampling: | ||
y_range = yend - ystart | ||
height = int(min([(y_range/self.p.y_sampling), height])) | ||
|
||
|
@@ -172,21 +179,28 @@ def _process(self, element, key=None): | |
|
||
class Shade(ElementOperation): | ||
""" | ||
Shade applies a normalization function to the data and then | ||
applies colormapping to an Image or NdOverlay of Images, returning | ||
an RGB Element. | ||
Shade applies a normalization function followed by colormapping to | ||
an Image or NdOverlay of Images, returning an RGB Element. | ||
The data must be in the form of a 2D or 3D DataArray, but NdOverlays | ||
of 2D Images will be automatically converted to a 3D array. | ||
|
||
In the 2D case data is normalized and colormapped, while a 3D | ||
array representing categorical aggregates will be supplied a color | ||
key for each category. The colormap (cmap) may be supplied as an | ||
Iterable or a Callable. | ||
""" | ||
|
||
cmap = param.ClassSelector(class_=(Iterable, Callable), doc=""" | ||
Iterable or callable which returns colors as hex colors. | ||
Callable type must allow mapping colors between 0 and 1.""") | ||
|
||
normalization = param.ObjectSelector(default='eq_hist', | ||
objects=['linear', 'log', | ||
'eq_hist', 'cbrt'], | ||
doc=""" | ||
The normalization operation applied before colormapping.""") | ||
|
||
normalization = param.ClassSelector(default='eq_hist', | ||
class_=(basestring, Callable), | ||
doc=""" | ||
The normalization operation applied before colormapping. | ||
Valid options include 'linear', 'log', 'eq_hist', 'cbrt', | ||
and any valid transfer function that acces data, mask, nbins | ||
arguments.""") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. acces -> has? |
||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The actual There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, does it even support tab-completion? Either way cbrt and any other callable should be allowed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So Edit: More discussion about naming above. |
||
@classmethod | ||
def concatenate(cls, overlay): | ||
|
@@ -251,50 +265,16 @@ def _process(self, element, key=None): | |
|
||
|
||
|
||
class Datashade(Aggregate, Shade): | ||
""" | ||
Applies the Aggregate and Shade operations, aggregating all | ||
elements in the supplied object and then applying normalization | ||
and colormapping the aggregated data returning RGB elements. | ||
|
||
class Datashade(ElementOperation): | ||
|
||
aggregator = param.ClassSelector(class_=ds.reductions.Reduction, | ||
default=ds.count()) | ||
|
||
cmap = param.ClassSelector(class_=(Iterable, Callable), doc=""" | ||
Iterable or callable which returns colors as hex colors. | ||
Callable type must allow mapping colors between 0 and 1.""") | ||
|
||
height = param.Integer(default=800, doc=""" | ||
The height of the aggregated image in pixels.""") | ||
|
||
normalization = param.ObjectSelector(default='eq_hist', | ||
objects=['linear', 'log', | ||
'eq_hist', 'cbrt'], | ||
doc=""" | ||
The normalization operation applied before colormapping.""") | ||
|
||
streams = param.List(default=[RangeXY], doc=""" | ||
List of streams that are applied if dynamic=True, allowing | ||
for dynamic interaction with the plot.""") | ||
|
||
width = param.Integer(default=600, doc=""" | ||
The width of the aggregated image in pixels.""") | ||
|
||
x_range = param.NumericTuple(default=None, length=2, doc=""" | ||
The x_range as a tuple of min and max x-value. Auto-ranges | ||
if set to None.""") | ||
|
||
y_range = param.NumericTuple(default=None, length=2, doc=""" | ||
The x_range as a tuple of min and max y-value. Auto-ranges | ||
if set to None.""") | ||
|
||
x_sampling = param.Number(default=None, doc=""" | ||
Specifies the smallest allowed sampling interval along the y-axis.""") | ||
|
||
y_sampling = param.Number(default=None, doc=""" | ||
Specifies the smallest allowed sampling interval along the y-axis.""") | ||
See Aggregate and Shade operations for more details. | ||
""" | ||
|
||
def _process(self, element, key=None): | ||
params = self.p.items() | ||
agg_kwargs = {p: v for p, v in params if p in Aggregate.params()} | ||
shade_kwargs = {p: v for p, v in params if p in Shade.params()} | ||
aggregate = Aggregate.instance(**agg_kwargs).process_element(element) | ||
shaded = Shade.instance(**shade_kwargs).process_element(aggregate) | ||
aggregate = Aggregate._process(self, element, key) | ||
shaded = Shade._process(self, aggregate, key) | ||
return shaded |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have said 'on a streams parameter' instead of 'on the streams attribute'...