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

Non-blocking periodic_events method on DynamicMap #1429

Merged
merged 18 commits into from
May 16, 2017
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 62 additions & 8 deletions holoviews/core/spaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
from .options import Store, StoreOptions
from ..streams import Stream



class HoloMap(UniformNdMapping, Overlayable):
"""
A HoloMap can hold any number of DataLayers indexed by a list of
Expand Down Expand Up @@ -557,18 +559,23 @@ def __call__(self):
raise


def get_nested_dmaps(dmap):
"""
Get all DynamicMaps referenced by the supplied DynamicMap's callback.
"""
dmaps = [dmap]
for o in dmap.callback.inputs:
if isinstance(o, DynamicMap):
dmaps.extend(get_nested_dmaps(o))
return list(set(dmaps))


def get_nested_streams(dmap):
"""
Get all (potentially nested) streams from DynamicMap with Callable
callback.
"""
layer_streams = list(dmap.streams)
if not isinstance(dmap.callback, Callable):
return list(set(layer_streams))
for o in dmap.callback.inputs:
if isinstance(o, DynamicMap):
layer_streams += get_nested_streams(o)
return list(set(layer_streams))
return list({s for dmap in get_nested_dmaps(dmap) for s in dmap.streams})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense to refactor it this way.



@contextmanager
Expand All @@ -589,6 +596,53 @@ def dynamicmap_memoization(callable_obj, streams):
callable_obj.memoize = memoization_state



class periodic(object):
"""
Implements the utility of the same name on DynamicMap.

Used to defined periodic event updates that can be started and
stopped.
"""
_periodic_util = util.periodic

def __init__(self, dmap):
self.dmap = dmap
self.instance = None

def __call__(self, period, count, param_fn=None):
"""
Run a non-blocking loop that updates the stream parameters using
the event method. Runs count times with the specified period. If
count is None, runs indefinitely.

If param_fn is not specified, the event method is called without
arguments. If it is specified, it must be a callable accepting a
single argument (the iteration count) that returns a dictionary
of the new stream values to be passed to the event method.
"""

if self.instance is not None and not self.instance.completed:
raise RuntimeError('Periodic process already running. '
'Wait until it completes or call '
'stop() before running a new periodic process')
def inner(i):
kwargs = {} if param_fn is None else param_fn(i)
self.dmap.event(**kwargs)

instance = self._periodic_util(period, count, inner)
instance.start()
self.instance= instance

def stop(self):
"Stop the periodic process."
self.instance.stop()

def __str__(self):
return "<holoviews.core.spaces.periodic method>"

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 comment explaining what this representation is meant to achieve?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you have another idea of what the repr should be, I would be happy to implement it! What sort of comment are you thinking of?

Copy link
Member

@jbednar jbednar May 16, 2017

Choose a reason for hiding this comment

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

Just something to indicate why the default representation was not suitable. I.e. why was this method necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It isn't strictly necessary, but I think it is a bit nicer than the default which is something like:

<holoviews.core.spaces.periodic at 0x10886e850>

Instead of a random memory address, I say that it is a method which reflects how this utility is used (i.e it lives on DynamicMap).

Happy to remove it if you think the default is better.

Copy link
Member

Choose a reason for hiding this comment

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

No need to remove it, just put a docstring saying what you say here (that this repr provides additional information that should be more useful).



class DynamicMap(HoloMap):
"""
A DynamicMap is a type of HoloMap where the elements are dynamically
Expand Down Expand Up @@ -661,6 +715,7 @@ def __init__(self, callback, initial_items=None, **params):
if stream.source is None:
stream.source = self
self.redim = redim(self, mode='dynamic')
self.periodic = periodic(self)

@property
def unbounded(self):
Expand Down Expand Up @@ -754,7 +809,6 @@ def event(self, **kwargs):

Stream.trigger(self.streams)


def _style(self, retval):
"""
Use any applicable OptionTree of the DynamicMap to apply options
Expand Down
44 changes: 44 additions & 0 deletions holoviews/core/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from functools import partial
from contextlib import contextmanager

from threading import Thread, Event
import numpy as np
import param

Expand Down Expand Up @@ -78,6 +79,49 @@ def default(self, obj):
return id(obj)


class periodic(Thread):
"""
Run a callback count times with a given period without blocking.
"""

def __init__(self, period, count, callback):

if isinstance(count, int):
if count < 0: raise ValueError('Count value must be positive')
elif not type(count) is type(None):
raise ValueError('Count value must be a positive integer or None')

super(periodic, self).__init__()
self.period = period
self.callback = callback
self.count = count
self.counter = 0
self._completed = Event()

@property
def completed(self):
return self._completed.is_set()

def stop(self):
self._completed.set()

def __repr__(self):
return 'periodic(%s, %s, %s)' % (self.period,
self.count,
callable_name(self.callback))
def __str__(self):
return repr(self)

def run(self):
while not self.completed:
self._completed.wait(self.period)
self.callback(self.counter)
self.counter += 1

if self.counter == self.count:
self._completed.set()



def deephash(obj):
"""
Expand Down
11 changes: 7 additions & 4 deletions holoviews/plotting/bokeh/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@

from ...core import Store, HoloMap
from ..comms import JupyterComm, Comm
from ..plot import GenericElementPlot
from ..renderer import Renderer, MIME_TYPES
from .widgets import BokehScrubberWidget, BokehSelectionWidget, BokehServerWidgets
from .util import compute_static_patch, serialize_json
from .util import compute_static_patch, serialize_json, attach_periodic



Expand Down Expand Up @@ -122,9 +123,11 @@ def server_doc(self, plot, doc=None):
if doc is None:
doc = curdoc()
if isinstance(plot, BokehServerWidgets):
plot.plot.document = doc
else:
plot.document = doc
plot = plot.plot
plot.document = doc
Copy link
Contributor Author

@jlstevens jlstevens May 15, 2017

Choose a reason for hiding this comment

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

Maybe add a comment as a reminder of what attach_periodic does here...

plot.traverse(lambda x: attach_periodic(plot),
[GenericElementPlot])

doc.add_root(plot.state)
return doc

Expand Down
70 changes: 70 additions & 0 deletions holoviews/plotting/bokeh/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from ...core.options import abbreviated_exception
from ...core.overlay import Overlay
from ...core.util import basestring, unique_array
from ...core.spaces import get_nested_dmaps, DynamicMap

from ..util import dim_axis_label, rgb2hex

Expand Down Expand Up @@ -664,3 +665,72 @@ def categorize_array(array, dim):
treats as a categorical suffix.
"""
return np.array([dim.pprint_value(x).replace(':', ';') for x in array])


class periodic(object):
"""
Mocks the API of periodic Thread in hv.core.util, allowing a smooth
API transition on bokeh server.
"""

def __init__(self, document):
self.document = document
self.callback = None
self.period = None
self.count = None
self.counter = None

@property
def completed(self):
return self.counter is None

def start(self):
if self.document is None:
raise RuntimeError('periodic was registered to be run on bokeh'
'server but no document was found.')
self.document.add_periodic_callback(self._periodic_callback, self.period)

def __call__(self, period, count, callback):
if isinstance(count, int):
if count < 0: raise ValueError('Count value must be positive')
elif not type(count) is type(None):
raise ValueError('Count value must be a positive integer or None')

self.callback = callback
self.period = period*1000.
self.count = count
self.counter = 0
return self

def _periodic_callback(self):
self.callback(self.counter)
self.counter += 1

if self.counter == self.count:
self.stop()

def stop(self):
self.counter = None
try:
self.document.remove_periodic_callback(self._periodic_callback)
except ValueError: # Already stopped
pass

def __repr__(self):
return 'periodic(%s, %s, %s)' % (self.period,
self.count,
callable_name(self.callback))
def __str__(self):
return repr(self)




def attach_periodic(plot):
"""
Attaches plot refresh to all streams on the object.
"""
def append_refresh(dmap):
for dmap in get_nested_dmaps(dmap):
dmap.periodic._periodic_util = periodic(plot.document)
return plot.hmap.traverse(append_refresh, [DynamicMap])