Skip to content
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

Merged
merged 23 commits into from Oct 5, 2016
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dfa037d
Added support for DataArrays in xarray interface
philippjfr Oct 4, 2016
c6422b7
Added Datashader Aggregate and Shade operations
philippjfr Oct 4, 2016
e0874c0
Refactored and added docstrings to datashader operations
philippjfr Oct 4, 2016
ba30243
Added support for attaching streams to ElementOperations
philippjfr Oct 4, 2016
40f1153
Range streams suppress updating of ranges
philippjfr Oct 4, 2016
345c413
Renamed DynamicFunction to Dynamic
philippjfr Oct 4, 2016
08ef8e0
Set Range stream defaults to None
philippjfr Oct 4, 2016
d5fc734
Added Datashade operation
philippjfr Oct 4, 2016
b4d3fdf
Renamed DynamicFunction and moved it to hv.util
philippjfr Oct 4, 2016
0488c52
Reverted change to ElementOperation.process_element
philippjfr Oct 4, 2016
5ce5bab
Cleaned up datashader operations
philippjfr Oct 4, 2016
83acee2
Fixed datashader sampling options
philippjfr Oct 5, 2016
189a231
Expanded on Shade docstring
philippjfr Oct 5, 2016
4dee22f
Added datashader to travis
philippjfr Oct 5, 2016
97381b2
Tweaked datashader operation parameters
philippjfr Oct 5, 2016
6046f9e
Expanded on dynamic ElementOperation features
philippjfr Oct 5, 2016
088a1b7
Initialize stream parameters in Dynamic
philippjfr Oct 5, 2016
d23d873
Fixed dynamic operation tests
philippjfr Oct 5, 2016
ac04550
Fixed docstring
philippjfr Oct 5, 2016
7b8a92a
Expanded on datashader operation docstrings
philippjfr Oct 5, 2016
10ea44e
Exposed Aliases in hv.util
philippjfr Oct 5, 2016
d602fde
Small docstring fix
philippjfr Oct 5, 2016
e151dce
Small Dynamic docstring fix
philippjfr Oct 5, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -24,7 +24,7 @@ install:
- conda update -q conda
# Useful for debugging any issues with conda
- conda info -a
- conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION scipy numpy freetype nose bokeh pandas jupyter ipython=4.2.0 param matplotlib=1.5.1 xarray
- conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION scipy numpy freetype nose bokeh pandas jupyter ipython=4.2.0 param matplotlib=1.5.1 xarray datashader
- source activate test-environment
- conda install -c conda-forge -c scitools iris sip=4.18 plotly
- if [[ "$TRAVIS_PYTHON_VERSION" == "3.4" ]]; then
Expand Down
86 changes: 9 additions & 77 deletions holoviews/core/operation.py
Expand Up @@ -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):
"""
Expand All @@ -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.
Copy link
Contributor

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'...

"""

dynamic = param.ObjectSelector(default='default',
Expand Down Expand Up @@ -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.
Expand All @@ -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', [])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature probably needs mentioning in the docstring wherever dynamic is mentioned.

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):
Expand Down
4 changes: 2 additions & 2 deletions holoviews/core/overlay.py
Expand Up @@ -24,10 +24,10 @@ class Overlayable(object):

def __mul__(self, other):
if type(other).__name__ == 'DynamicMap':
from .operation import Dynamic
from ..util import Dynamic
def dynamic_mul(element):
return self * element
return Dynamic(other, callable=dynamic_mul)
return Dynamic(other, operation=dynamic_mul)
if isinstance(other, UniformNdMapping) and not isinstance(other, CompositeOverlay):
items = [(k, self * v) for (k, v) in other.items()]
return other.clone(items)
Expand Down
4 changes: 2 additions & 2 deletions holoviews/core/spaces.py
Expand Up @@ -204,10 +204,10 @@ def __mul__(self, other):
return self.clone(items, kdims=dimensions, label=self._label, group=self._group)
elif isinstance(other, self.data_type):
if isinstance(self, DynamicMap):
from .operation import Dynamic
from ..util import Dynamic
def dynamic_mul(element):
return element * other
return Dynamic(self, callable=dynamic_mul)
return Dynamic(self, operation=dynamic_mul)
items = [(k, v * other) for (k, v) in self.data.items()]
return self.clone(items, label=self._label, group=self._group)
else:
Expand Down
98 changes: 39 additions & 59 deletions holoviews/operation/datashader.py
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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.
Copy link
Member

@jbednar jbednar Oct 4, 2016

Choose a reason for hiding this comment

The 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.
"""
Copy link
Member

Choose a reason for hiding this comment

The 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.""")

Expand Down Expand Up @@ -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]))

Expand All @@ -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.""")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

acces -> has?


Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actual how attribute also supports 'cbrt', so that should probably be listed here. But it also supports any callable; is it possible to have that allowed here as well without preventing tab completion?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

@jlstevens jlstevens Oct 4, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So how is an existing parameter name in datashader then? I suspected as much...

Edit: More discussion about naming above.

@classmethod
def concatenate(cls, overlay):
Expand Down Expand Up @@ -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