-
-
Notifications
You must be signed in to change notification settings - Fork 404
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
Dynamic Callable API #951
Dynamic Callable API #951
Changes from all commits
5558dbe
4a47dff
52d716c
67e5a39
628c19e
e3b2b88
2361036
f710fa2
ca945bf
e019eab
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 |
---|---|---|
|
@@ -120,12 +120,13 @@ def _dynamic_mul(self, dimensions, other, keys): | |
map_obj = self if isinstance(self, DynamicMap) else other | ||
mode = map_obj.mode | ||
|
||
def dynamic_mul(*key): | ||
def dynamic_mul(*key, **kwargs): | ||
key = key[0] if mode == 'open' else key | ||
layers = [] | ||
try: | ||
if isinstance(self, DynamicMap): | ||
_, self_el = util.get_dynamic_item(self, dimensions, key) | ||
safe_key = () if not self.kdims else key | ||
_, self_el = util.get_dynamic_item(self, dimensions, safe_key) | ||
if self_el is not None: | ||
layers.append(self_el) | ||
else: | ||
|
@@ -134,19 +135,21 @@ def dynamic_mul(*key): | |
pass | ||
try: | ||
if isinstance(other, DynamicMap): | ||
_, other_el = util.get_dynamic_item(other, dimensions, key) | ||
safe_key = () if not other.kdims else key | ||
_, other_el = util.get_dynamic_item(other, dimensions, safe_key) | ||
if other_el is not None: | ||
layers.append(other_el) | ||
else: | ||
layers.append(other[key]) | ||
except KeyError: | ||
pass | ||
return Overlay(layers) | ||
callback = Callable(callable_function=dynamic_mul, inputs=[self, other]) | ||
if map_obj: | ||
return map_obj.clone(callback=dynamic_mul, shared_data=False, | ||
kdims=dimensions) | ||
return map_obj.clone(callback=callback, shared_data=False, | ||
kdims=dimensions, streams=[]) | ||
else: | ||
return DynamicMap(callback=dynamic_mul, kdims=dimensions) | ||
return DynamicMap(callback=callback, kdims=dimensions) | ||
|
||
|
||
def __mul__(self, other): | ||
|
@@ -204,10 +207,13 @@ 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 ..util import Dynamic | ||
def dynamic_mul(element): | ||
def dynamic_mul(*args, **kwargs): | ||
element = self[args] | ||
return element * other | ||
return Dynamic(self, operation=dynamic_mul) | ||
callback = Callable(callable_function=dynamic_mul, | ||
inputs=[self, other]) | ||
return self.clone(shared_data=False, callback=callback, | ||
streams=[]) | ||
items = [(k, v * other) for (k, v) in self.data.items()] | ||
return self.clone(items, label=self._label, group=self._group) | ||
else: | ||
|
@@ -393,6 +399,38 @@ def hist(self, num_bins=20, bin_range=None, adjoin=True, individually=True, **kw | |
return histmaps[0] | ||
|
||
|
||
class Callable(param.Parameterized): | ||
""" | ||
Callable allows wrapping callbacks on one or more DynamicMaps | ||
allowing their inputs (and in future outputs) to be defined. | ||
This makes it possible to wrap DynamicMaps with streams and | ||
makes it possible to traverse the graph of operations applied | ||
to a DynamicMap. | ||
""" | ||
|
||
callable_function = param.Callable(default=lambda x: x, doc=""" | ||
The callable function being wrapped.""") | ||
|
||
inputs = param.List(default=[], doc=""" | ||
The list of inputs the callable function is wrapping.""") | ||
|
||
def __call__(self, *args, **kwargs): | ||
return self.callable_function(*args, **kwargs) | ||
|
||
|
||
def get_nested_streams(dmap): | ||
""" | ||
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. Would 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. Sure. |
||
Get all (potentially nested) streams from DynamicMap with Callable | ||
callback. | ||
""" | ||
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. Might say something like 'Get all (potentially nested) 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. Sure. |
||
layer_streams = list(dmap.streams) | ||
if not isinstance(dmap.callback, Callable): | ||
return layer_streams | ||
for o in dmap.callback.inputs: | ||
if isinstance(o, DynamicMap): | ||
layer_streams += get_nested_streams(o) | ||
return layer_streams | ||
|
||
|
||
class DynamicMap(HoloMap): | ||
""" | ||
|
@@ -689,7 +727,8 @@ def __getitem__(self, key): | |
|
||
# Cache lookup | ||
try: | ||
dimensionless = util.dimensionless_contents(self.streams, self.kdims) | ||
dimensionless = util.dimensionless_contents(get_nested_streams(self), | ||
self.kdims, no_duplicates=False) | ||
if (dimensionless and not self._dimensionless_cache): | ||
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. Using 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. Sure. |
||
raise KeyError('Using dimensionless streams disables DynamicMap cache') | ||
cache = super(DynamicMap,self).__getitem__(key) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -31,7 +31,7 @@ | |
from ...element import RGB | ||
from ...streams import Stream, RangeXY, RangeX, RangeY | ||
from ..plot import GenericElementPlot, GenericOverlayPlot | ||
from ..util import dynamic_update | ||
from ..util import dynamic_update, get_sources | ||
from .plot import BokehPlot | ||
from .util import (mpl_to_bokeh, convert_datetime, update_plot, | ||
bokeh_version, mplcmap_to_palette) | ||
|
@@ -177,15 +177,18 @@ def _construct_callbacks(self): | |
the plotted object as a source. | ||
""" | ||
if not self.static or isinstance(self.hmap, DynamicMap): | ||
source = self.hmap | ||
sources = [(i, o) for i, o in get_sources(self.hmap) | ||
if i in [None, self.zorder]] | ||
else: | ||
source = self.hmap.last | ||
streams = Stream.registry.get(id(source), []) | ||
registry = Stream._callbacks['bokeh'] | ||
callbacks = {(registry[type(stream)], stream) for stream in streams | ||
if type(stream) in registry and streams} | ||
sources = [(self.zorder, self.hmap.last)] | ||
cb_classes = set() | ||
for _, source in sources: | ||
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 the key difference seems to be that you can now have multiple sources whereas before there was only one. And the sources are now retrieved recursively via |
||
streams = Stream.registry.get(id(source), []) | ||
registry = Stream._callbacks['bokeh'] | ||
cb_classes |= {(registry[type(stream)], stream) for stream in streams | ||
if type(stream) in registry and streams} | ||
cbs = [] | ||
sorted_cbs = sorted(callbacks, key=lambda x: id(x[0])) | ||
sorted_cbs = sorted(cb_classes, key=lambda x: id(x[0])) | ||
for cb, group in groupby(sorted_cbs, lambda x: x[0]): | ||
cb_streams = [s for _, s in group] | ||
cbs.append(cb(self, cb_streams, source)) | ||
|
@@ -560,6 +563,11 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): | |
if plot is None: | ||
plot = self._init_plot(key, style_element, ranges=ranges, plots=plots) | ||
self._init_axes(plot) | ||
else: | ||
self.handles['xaxis'] = plot.xaxis[0] | ||
self.handles['x_range'] = plot.x_range | ||
self.handles['y_axis'] = plot.yaxis[0] | ||
self.handles['y_range'] = plot.y_range | ||
self.handles['plot'] = plot | ||
|
||
# Get data and initialize data source | ||
|
@@ -675,7 +683,10 @@ def current_handles(self): | |
rangex, rangey = True, True | ||
elif isinstance(self.hmap, DynamicMap): | ||
rangex, rangey = True, True | ||
for stream in self.hmap.streams: | ||
subplots = list(self.subplots.values()) if self.subplots else [] | ||
callbacks = [cb for p in [self]+subplots for cb in p.callbacks] | ||
streams = [s for cb in callbacks for s in cb.streams] | ||
for stream in streams: | ||
if isinstance(stream, RangeXY): | ||
rangex, rangey = False, False | ||
break | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,7 +4,8 @@ | |
import param | ||
|
||
from ..core import (HoloMap, DynamicMap, CompositeOverlay, Layout, | ||
GridSpace, NdLayout, Store, Overlay) | ||
GridSpace, NdLayout, Store, Callable, Overlay) | ||
from ..core.spaces import get_nested_streams | ||
from ..core.util import (match_spec, is_number, wrap_tuple, basestring, | ||
get_overlay_spec, unique_iterator, safe_unicode) | ||
|
||
|
@@ -295,11 +296,36 @@ def attach_streams(plot, obj): | |
Attaches plot refresh to all streams on the object. | ||
""" | ||
def append_refresh(dmap): | ||
for stream in dmap.streams: | ||
for stream in get_nested_streams(dmap): | ||
stream._hidden_subscribers.append(plot.refresh) | ||
return obj.traverse(append_refresh, [DynamicMap]) | ||
|
||
|
||
def get_sources(obj, index=None): | ||
""" | ||
Traverses Callable graph to resolve sources on | ||
DynamicMap objects, returning a list of sources | ||
indexed by the Overlay layer. | ||
""" | ||
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. Can't this be simplified to: if isinstance(obj, DynamicMap) and isinstance(obj.callback, Callable):
return [(index, obj)] leaving the rest of the function to handle 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. Not quite but it could definitely be refactored more nicely. 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. Ah...given it is: + if isinstance(obj, DynamicMap):
+ if isinstance(obj.callback, Callable):
....
+ else:
+ return [(index, obj)]
+ else:
+ return [(index, obj)] I think I meant: if not isinstance(obj, DynamicMap) or not isinstance(obj.callback, Callable):
return [(index, obj)] |
||
layers = [(index, obj)] | ||
if not isinstance(obj, DynamicMap) or not isinstance(obj.callback, Callable): | ||
return layers | ||
index = 0 if index is None else int(index) | ||
for o in obj.callback.inputs: | ||
if isinstance(o, Overlay): | ||
layers.append((None, o)) | ||
for i, o in enumerate(overlay): | ||
layers.append((index+i, o)) | ||
index += len(o) | ||
elif isinstance(o, DynamicMap): | ||
layers += get_sources(o, index) | ||
index = layers[-1][0]+1 | ||
else: | ||
layers.append((index, o)) | ||
index += 1 | ||
return layers | ||
|
||
|
||
def traverse_setter(obj, attribute, value): | ||
""" | ||
Traverses the object and sets the supplied attribute on the | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -108,7 +108,11 @@ def __init__(self, plot, renderer=None, **params): | |
super(NdWidget, self).__init__(**params) | ||
self.id = plot.comm.target if plot.comm else uuid.uuid4().hex | ||
self.plot = plot | ||
self.dimensions, self.keys = drop_streams(plot.streams, | ||
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. I assume this bit is simply a bug fix and otherwise unrelated? 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. Before this wasn't an issue, so not unrelated. 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. Ok...seems to be a result of 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. Right in these cases it just needs to know if a kdim has a corresponding stream or not, there is no actual clash at the DynamicMap level because each level of wrapping, i.e. all operations you apply, resolve their own streams. |
||
for stream in plot.streams: | ||
if any(k in plot.dimensions for k in stream.contents): | ||
streams.append(stream) | ||
self.dimensions, self.keys = drop_streams(streams, | ||
plot.dimensions, | ||
plot.keys) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,8 @@ | |
from .core import DynamicMap, ViewableElement | ||
from .core.operation import ElementOperation | ||
from .core.util import Aliases | ||
from .core.operation import OperationCallable | ||
from .core.spaces import Callable | ||
from .core import util | ||
from .streams import Stream | ||
|
||
|
@@ -33,7 +35,8 @@ 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) | ||
dmap = map_obj.clone(callback=callback, shared_data=False, | ||
streams=[]) | ||
else: | ||
dmap = self._make_dynamic(map_obj, callback) | ||
if isinstance(self.p.operation, ElementOperation): | ||
|
@@ -44,7 +47,7 @@ def __call__(self, map_obj, **params): | |
elif not isinstance(stream, Stream): | ||
raise ValueError('Stream must only contain Stream ' | ||
'classes or instances') | ||
stream.update(**{k: self.p.operation.p.get(k) for k, v in | ||
stream.update(**{k: self.p.operation.p.get(k, v) for k, v in | ||
stream.contents.items()}) | ||
streams.append(stream) | ||
return dmap.clone(streams=streams) | ||
|
@@ -69,15 +72,17 @@ def _dynamic_operation(self, map_obj): | |
def dynamic_operation(*key, **kwargs): | ||
self.p.kwargs.update(kwargs) | ||
return self._process(map_obj[key], 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, key) | ||
|
||
return dynamic_operation | ||
else: | ||
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, key) | ||
if isinstance(self.p.operation, ElementOperation): | ||
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. Couldn't 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. No because 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'm a little confused. 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. It is never used, but is useful information and will in future be used to look up the 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. Ah sorry, I'm confused different bit of code. 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. if isinstance(self.p.operation, ElementOperation):
kwargs = {k: v for k, v in self.p.kwargs.items()
if k in self.p.operation.params()}
return self.p.operation.process_element(element, key, **kwargs) |
||
return OperationCallable(callable_function=dynamic_operation, | ||
inputs=[map_obj], operation=self.p.operation) | ||
else: | ||
return Callable(callable_function=dynamic_operation, inputs=[map_obj]) | ||
|
||
|
||
def _make_dynamic(self, hmap, dynamic_fn): | ||
|
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.
Wondering whether it should be
self.clone
, orother.clone
or maybe a newDynamicMap
declaration entirely. I see this is in the condition whereother
is aDynamicMap
but is this definitely right in terms ofkdims
? I need to think about it more...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.
Can't this stuff relying on other being
DynamicMap
be moved to_dynamic_mul
? There is already this condition in__mul__
:If all the logic regarding dynamic could move to
_dynamic_mul
, that would be cleaner...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.
Yes, this is the condition where self is a single Element or Overlay.
This is the
__mul__
implementation on Overlayable, it doesn't have_dynamic_mul
, because I'd like to avoid inline imports.