-
-
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
Introduction of Streams API #832
Changes from 31 commits
90a4260
95f13ac
91cae3e
b473a83
80ab335
047b5a2
adec6db
f4c5558
b061224
e01436c
f0a26bd
88b4637
97b0f9b
eff7029
5e62cda
5c5c05b
10ba516
d726f00
c0b95ed
c5d1d56
91198d7
1230a46
5d318a3
3806f7f
12ef5c3
252c4dd
4ce5f12
8d202b7
db3150f
3e3964c
8053ecd
20e6013
a1d2b87
d6a7dec
846c9d7
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 |
---|---|---|
@@ -0,0 +1,220 @@ | ||
""" | ||
The streams module defines the streams API that allows visualizations to | ||
generate and respond to events, originating either in Python on the | ||
server-side or in Javascript in the Jupyter notebook (client-side). | ||
""" | ||
|
||
import param | ||
import uuid | ||
from collections import OrderedDict | ||
|
||
|
||
|
||
class Preprocessor(param.Parameterized): | ||
""" | ||
A Preprocessor is a callable that takes a dictionary as an argument | ||
and returns a dictionary. Where possible, Preprocessors should have | ||
valid reprs that can be evaluated. | ||
|
||
Preprocessors are used to set the value of a stream based on the | ||
parameter values. They may be used for debugging purposes or to | ||
remap or repack parameter values before they are passed onto to the | ||
subscribers. | ||
""" | ||
|
||
def __call__(self, params): | ||
return params | ||
|
||
|
||
|
||
class Rename(Preprocessor): | ||
""" | ||
A preprocessor used to rename parameter values. | ||
""" | ||
|
||
mapping = param.Dict(default={}, doc=""" | ||
The mapping from the parameter names to the designated names""") | ||
|
||
def __init__(self, **mapping): | ||
super(Rename, self).__init__(mapping=mapping) | ||
|
||
def __call__(self, params): | ||
return {self.mapping.get(k,k):v for (k,v) in params.items()} | ||
|
||
def __repr__(self): | ||
keywords = ','.join('%s=%r' % (k,v) for (k,v) in sorted(self.mapping.items())) | ||
return 'Rename(%s)' % keywords | ||
|
||
|
||
|
||
class Group(Preprocessor): | ||
""" | ||
A preprocessor that keeps the parameter dictionary together, | ||
supplying it as a value associated with the given key. | ||
""" | ||
|
||
def __init__(self, key): | ||
super(Group, self).__init__(key=key) | ||
|
||
def __call__(self, params): | ||
return {self.key:params} | ||
|
||
def __repr__(self): | ||
return 'Group(%r)' % self.key | ||
|
||
|
||
|
||
class Stream(param.Parameterized): | ||
""" | ||
A Stream is simply a parameterized object with parameters that | ||
change over time in response to update events. Parameters are | ||
updated via the update method. | ||
|
||
Streams may have one or more subscribers which are callables passed | ||
the parameter dictionary when the trigger classmethod is called. | ||
""" | ||
|
||
# Mapping from uuid to stream instance | ||
registry = OrderedDict() | ||
|
||
@classmethod | ||
def trigger(cls, streams): | ||
""" | ||
Given a list of streams, collect all the stream parameters into | ||
a dictionary and pass it to the union set of subscribers. | ||
|
||
Passing multiple streams at once to trigger can be useful when a | ||
subscriber may be set multiple times across streams but only | ||
needs to be called once. | ||
""" | ||
# Union of stream values | ||
items = [stream.value.items() for stream in streams] | ||
union = [kv for kvs in items for kv in kvs] | ||
klist = [k for k,_ in union] | ||
clashes = set([k for k in klist if klist.count(k) > 1]) | ||
if clashes: | ||
param.main.warning('Parameter name clashes for keys: %r' % clashes) | ||
|
||
# Currently building a simple set of subscribers | ||
groups = [stream.subscribers + stream._hidden_subscribers for stream in streams] | ||
subscribers = set(s for subscribers in groups for s in subscribers) | ||
for subscriber in subscribers: | ||
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, shouldn't some order be maintained, so that the subscribers defined on an individual stream are at least executed in sequence and the _hidden_subscribers are executed after all others? Something like: from .core import util
groups = [sub for stream in streams for sub in stream.subscribers]
hidden = [sub for stream in streams for sub in stream._hidden_subscribers]
for subscriber in util.unique_iterator(groups+hidden):
... 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. Yes, that makes sense mainly because we know that 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, currently it is not well defined when the plot redraw would happen. |
||
subscriber(dict(union)) | ||
|
||
|
||
@classmethod | ||
def find(cls, obj): | ||
""" | ||
Return a set of streams from the registry with a given source. | ||
""" | ||
return set(v for v in cls.registry.values() if v.source is obj) | ||
|
||
|
||
def __init__(self, preprocessors=[], source=None, subscribers=[], **params): | ||
""" | ||
Mapping allows multiple streams with similar event state to be | ||
used by remapping parameter names. | ||
|
||
Source is an optional argument specifying the HoloViews | ||
datastructure that the stream receives events from, as supported | ||
by the plotting backend. | ||
""" | ||
self.source = source | ||
self.subscribers = subscribers | ||
self.preprocessors = preprocessors | ||
self._hidden_subscribers = [] | ||
|
||
self.uuid = uuid.uuid4().hex | ||
super(Stream, self).__init__(**params) | ||
self.registry[self.uuid] = self | ||
|
||
|
||
@property | ||
def value(self): | ||
remapped = {k:v for k,v in self.get_param_values() if k!= 'name' } | ||
for preprocessor in self.preprocessors: | ||
remapped = preprocessor(remapped) | ||
return remapped | ||
|
||
|
||
def update(self, trigger=True, **kwargs): | ||
""" | ||
The update method updates the stream parameters in response to | ||
some event. | ||
|
||
If trigger is enabled, the trigger classmethod is invoked on | ||
this particular Stream instance. | ||
""" | ||
params = self.params().values() | ||
constants = [p.constant for p in params] | ||
for param in params: | ||
param.constant = False | ||
self.set_param(**kwargs) | ||
for (param, const) in zip(params, constants): | ||
param.constant = const | ||
|
||
if trigger: | ||
self.trigger([self]) | ||
|
||
|
||
def __repr__(self): | ||
cls_name = self.__class__.__name__ | ||
kwargs = ','.join('%s=%r' % (k,v) | ||
for (k,v) in self.get_param_values() if k != 'name') | ||
if not self.preprocessors: | ||
return '%s(%s)' % (cls_name, kwargs) | ||
else: | ||
return '%s(%r, %s)' % (cls_name, self.preprocessors, kwargs) | ||
|
||
|
||
def __str__(self): | ||
return repr(self) | ||
|
||
|
||
class PositionX(Stream): | ||
""" | ||
A position along the x-axis in data coordinates. | ||
|
||
With the appropriate plotting backend, this may correspond to the | ||
position of the mouse/trackpad cursor. | ||
""" | ||
|
||
x = param.Number(default=0, doc=""" | ||
Position along the x-axis in data coordinates""", constant=True) | ||
|
||
def __init__(self, mapping=None, **params): | ||
super(PositionX, self).__init__(mapping=mapping, **params) | ||
|
||
|
||
class PositionY(Stream): | ||
""" | ||
A position along the y-axis in data coordinates. | ||
|
||
With the appropriate plotting backend, this may correspond to the | ||
position of the mouse/trackpad cursor. | ||
""" | ||
|
||
y = param.Number(default=0, doc=""" | ||
Position along the y-axis in data coordinates""", constant=True) | ||
|
||
def __init__(self, mapping=None, **params): | ||
super(PositionY, self).__init__(mapping=mapping, **params) | ||
|
||
|
||
class PositionXY(Stream): | ||
""" | ||
A position along the x- and y-axes in data coordinates. | ||
|
||
With the appropriate plotting backend, this may correspond to the | ||
position of the mouse/trackpad cursor. | ||
""" | ||
|
||
|
||
x = param.Number(default=0, doc=""" | ||
Position along the x-axis in data coordinates""", constant=True) | ||
|
||
y = param.Number(default=0, doc=""" | ||
Position along the y-axis in data coordinates""", constant=True) | ||
|
||
def __init__(self, mapping=None, **params): | ||
super(PositionXY, self).__init__(mapping=mapping, **params) |
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.
Same as below, no warning for clashing kwargs?