Skip to content

Commit

Permalink
Added jscallback method to Viewable objects
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Sep 25, 2019
1 parent 660f73d commit ce2ebf5
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 55 deletions.
176 changes: 121 additions & 55 deletions panel/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,18 @@
from bokeh.models import (CustomJS, Model as BkModel)


class Link(param.Parameterized):
"""
A Link defines some connection between a source and target model.
It allows defining callbacks in response to some change or event
on the source object. Instead a Link directly causes some action
to occur on the target, for JS based backends this usually means
that a corresponding JS callback will effect some change on the
target in response to a change on the source.
class Callback(param.Parameterized):

A Link must define a source object which is what triggers events,
but must not define a target. It is also possible to define bi-
directional links between the source and target object.
"""
args = param.Dict(default={}, allow_None=True, doc="""
A mapping of names to Python objects. These objects are made
available to the callback's code snippet as the values of
named parameters to the callback.""")

# Mapping from a source id to a Link instance
registry = weakref.WeakKeyDictionary()

# Mapping to define callbacks by backend and Link type.
# e.g. Link._callbacks[Link] = Callback
# e.g. Callback._callbacks[Link] = Callback
_callbacks = {}

# Whether the link requires a target
Expand All @@ -43,33 +36,14 @@ class Link(param.Parameterized):
def __init__(self, source, target=None, **params):
if source is None:
raise ValueError('%s must define a source' % type(self).__name__)
if self._requires_target and target is None:
raise ValueError('%s must define a target.' % type(self).__name__)
# Source is stored as a weakref to allow it to be garbage collected
self._source = None if source is None else weakref.ref(source)
self._target = None if target is None else weakref.ref(target)
super(Link, self).__init__(**params)
self.link()
super(Callback, self).__init__(**params)
self.init()

@classmethod
def register_callback(cls, callback):
def init(self):
"""
Register a LinkCallback providing the implementation for
the Link for a particular backend.
"""
cls._callbacks[cls] = callback

@property
def source(self):
return self._source() if self._source else None

@property
def target(self):
return self._target() if self._target else None

def link(self):
"""
Registers the Link
Registers the Callback
"""
if self.source in self.registry:
links = self.registry[self.source]
Expand All @@ -85,17 +59,21 @@ def link(self):
else:
self.registry[self.source] = [self]

def unlink(self):
@classmethod
def register_callback(cls, callback):
"""
Unregisters the Link
Register a LinkCallback providing the implementation for
the Link for a particular backend.
"""
links = self.registry.get(self.source)
if self in links:
links.pop(links.index(self))
cls._callbacks[cls] = callback

@property
def source(self):
return self._source() if self._source else None

@classmethod
def _process_links(cls, root_view, root_model):
if not isinstance(root_view, (Panel, CompositeWidget)) or not root_model:
def _process_callbacks(cls, root_view, root_model):
if not root_model:
return

linkable = root_view.select(Viewable)
Expand All @@ -104,26 +82,88 @@ def _process_links(cls, root_view, root_model):
if not linkable:
return

found = [(link, src, link.target) for src in linkable
found = [(link, src, getattr(link, 'target', None)) for src in linkable
for link in cls.registry.get(src, [])
if link.target in linkable or not link._requires_target]
if not link._requires_target or link.target in linkable]

arg_overrides = {}
if 'holoviews' in sys.modules:
hv_views = root_view.select(HoloViews)
map_hve_bk = generate_panel_bokeh_map(root_model, hv_views)
found += [(link, src, tgt) for src in linkable if src in cls.registry
for link in cls.registry[src]
for tgt in map_hve_bk[link.target]]
for src in linkable:
for link in cls.registry.get(src, []):
if hasattr(link, 'target'):
for tgt in map_hve_bk.get(link.target, []):
found.append((link, src, tgt))
arg_overrides[id(link)] = {}
for k, v in link.args.items():
for tgt in map_hve_bk.get(v, []):
arg_overrides[id(link)][k] = tgt

callbacks = []
for link, src, tgt in found:
cb = cls._callbacks[type(link)]
if src is None or (getattr(link, '_requires_target', False)
and tgt is None):
continue
callbacks.append(cb(root_model, link, src, tgt))
overrides = arg_overrides[id(link)]
callbacks.append(cb(root_model, link, src, tgt,
arg_overrides=overrides))
return callbacks


class Link(Callback):
"""
A Link defines some connection between a source and target model.
It allows defining callbacks in response to some change or event
on the source object. Instead a Link directly causes some action
to occur on the target, for JS based backends this usually means
that a corresponding JS callback will effect some change on the
target in response to a change on the source.
A Link must define a source object which is what triggers events,
but must not define a target. It is also possible to define bi-
directional links between the source and target object.
"""

def __init__(self, source, target=None, **params):
if self._requires_target and target is None:
raise ValueError('%s must define a target.' % type(self).__name__)
# Source is stored as a weakref to allow it to be garbage collected
self._target = None if target is None else weakref.ref(target)
super(Link, self).__init__(**params)

@property
def target(self):
return self._target() if self._target else None

def link(self):
"""
Registers the Link
"""
self.init()
if self.source in self.registry:
links = self.registry[self.source]
params = {
k: v for k, v in self.get_param_values() if k != 'name'}
for link in links:
link_params = {
k: v for k, v in link.get_param_values() if k != 'name'}
if (type(link) is type(self) and link.source is self.source
and link.target is self.target and params == link_params):
return
self.registry[self.source].append(self)
else:
self.registry[self.source] = [self]

def unlink(self):
"""
Unregisters the Link
"""
links = self.registry.get(self.source)
if self in links:
links.pop(links.index(self))


class GenericLink(Link):
"""
Expand All @@ -145,6 +185,20 @@ class GenericLink(Link):
_requires_target = True


class GenericCallback(Callback):
"""
A Callback which executes the provided code when the associated
properties change.
"""

code = param.Dict(default=None, doc="""
A dictionary mapping from a source specication to a JS code
snippet to be executed if the source property changes.""")

# Whether the link requires a target
_requires_target = False


class LinkCallback(param.Parameterized):

@classmethod
Expand Down Expand Up @@ -189,19 +243,20 @@ def _resolve_model(cls, root_model, obj, model_spec):
model = getattr(model, spec)
return model

def __init__(self, root_model, link, source, target=None):
def __init__(self, root_model, link, source, target=None, arg_overrides={}):
self.root_model = root_model
self.link = link
self.source = source
self.target = target
self.arg_overrides = arg_overrides
self.validate()
specs = self._get_specs(link, source, target)
for src_spec, tgt_spec, code in specs:
self._init_callback(root_model, link, source, src_spec, target, tgt_spec, code)

def _init_callback(self, root_model, link, source, src_spec, target, tgt_spec, code):
references = {k: v for k, v in link.get_param_values()
if k not in ('source', 'target', 'name', 'code')}
if k not in ('source', 'target', 'name', 'code', 'args')}

src_model = self._resolve_model(root_model, source, src_spec[0])
link_id = id(link)
Expand All @@ -210,9 +265,18 @@ def _init_callback(self, root_model, link, source, src_spec, target, tgt_spec, c
return
references['source'] = src_model

tgt_model = self._resolve_model(root_model, target, tgt_spec[0])
if tgt_model is not None:
references['target'] = tgt_model
tgt_mode = None
if link._requires_target:
tgt_model = self._resolve_model(root_model, target, tgt_spec[0])
if tgt_model is not None:
references['target'] = tgt_model
else:
tgt_model = None

for k, v in dict(link.args, **self.arg_overrides).items():
arg_model = self._resolve_model(root_model, v, None)
if arg_model is not None:
references[k] = arg_model

if 'holoviews' in sys.modules:
if is_bokeh_element_plot(source):
Expand Down Expand Up @@ -334,5 +398,7 @@ def _get_triggers(self, link, src_spec):


GenericLink.register_callback(callback=GenericLinkCallback)
GenericCallback.register_callback(callback=GenericLinkCallback)


Viewable._preprocessing_hooks.append(Link._process_links)
Viewable._preprocessing_hooks.append(Callback._process_callbacks)
22 changes: 22 additions & 0 deletions panel/viewable.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,28 @@ def add_periodic_callback(self, callback, period=500, count=None,
cb.start()
return cb

def jscallback(self, args={}, **callbacks):
"""
Arguments
----------
target: HoloViews object or bokeh Model or panel Viewable
The target to link the value to.
**callbacks: dict
A mapping between properties on the source model and the code
to execute when that property changes
Returns
-------
link: GenericCallback
The GenericCallback which can be used to disable the callback.
"""

from .links import GenericCallback
for k, v in list(callbacks.items()):
callbacks[k] = self._rename.get(v, v)
return GenericCallback(self, code=callbacks, args=args)

def jslink(self, target, code=None, **links):
"""
Links properties on the source object to those on the target
Expand Down

0 comments on commit ce2ebf5

Please sign in to comment.