1
"""
2
Defines Links which allow declaring links between bokeh properties.
3
"""
4 2
from __future__ import absolute_import, division, unicode_literals
5

6 2
import param
7 2
import weakref
8 2
import sys
9

10 2
from .reactive import Reactive
11 2
from .viewable import Viewable
12

13 2
from bokeh.models import (CustomJS, Model as BkModel)
14

15

16 2
class Callback(param.Parameterized):
17
    """
18
    A Callback defines some callback to be triggered when a property
19
    changes on the source object. A Callback can execute arbitrary
20
    Javascript code and will make all objects referenced in the args
21
    available in the JS namespace.
22
    """
23

24 2
    args = param.Dict(default={}, allow_None=True, doc="""
25
        A mapping of names to Python objects. These objects are made
26
        available to the callback's code snippet as the values of
27
        named parameters to the callback.""")
28

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

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

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

40
    # Whether the link requires a target
41 2
    _requires_target = False
42

43 2
    def __init__(self, source, target=None, **params):
44 2
        if source is None:
45 0
            raise ValueError('%s must define a source' % type(self).__name__)
46
        # Source is stored as a weakref to allow it to be garbage collected
47 2
        self._source = None if source is None else weakref.ref(source)
48 2
        super(Callback, self).__init__(**params)
49 2
        self.init()
50

51 2
    def init(self):
52
        """
53
        Registers the Callback
54
        """
55 2
        if self.source in self.registry:
56 0
            links = self.registry[self.source]
57 0
            params = {
58
                k: v for k, v in self.param.get_param_values() if k != 'name'}
59 0
            for link in links:
60 0
                link_params = {
61
                    k: v for k, v in link.param.get_param_values() if k != 'name'}
62 0
                if not hasattr(link, 'target'):
63 0
                    pass
64 0
                elif (type(link) is type(self) and link.source is self.source
65
                    and link.target is self.target and params == link_params):
66 0
                    return
67 0
            self.registry[self.source].append(self)
68
        else:
69 2
            self.registry[self.source] = [self]
70

71 2
    @classmethod
72
    def register_callback(cls, callback):
73
        """
74
        Register a LinkCallback providing the implementation for
75
        the Link for a particular backend.
76
        """
77 2
        cls._callbacks[cls] = callback
78

79 2
    @property
80
    def source(self):
81 2
        return self._source() if self._source else None
82

83 2
    @classmethod
84
    def _process_callbacks(cls, root_view, root_model):
85 2
        if not root_model:
86 0
            return
87

88 2
        linkable = root_view.select(Viewable)
89 2
        linkable += root_model.select({'type' : BkModel})
90

91 2
        if not linkable:
92 0
            return
93

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

98 2
        arg_overrides = {}
99 2
        if 'holoviews' in sys.modules:
100 2
            from .pane.holoviews import HoloViews, generate_panel_bokeh_map
101

102 2
            hv_views = root_view.select(HoloViews)
103 2
            map_hve_bk = generate_panel_bokeh_map(root_model, hv_views)
104 2
            for src in linkable:
105 2
                for link in cls.registry.get(src, []):
106 2
                    if hasattr(link, 'target'):
107 2
                        for tgt in map_hve_bk.get(link.target, []):
108 2
                            found.append((link, src, tgt))
109 2
                    arg_overrides[id(link)] = {}
110 2
                    for k, v in link.args.items():
111
                        # Not all args are hashable
112 2
                        try:
113 2
                            hv_objs = map_hve_bk.get(v, [])
114 0
                        except Exception:
115 0
                            continue
116 2
                        for tgt in hv_objs:
117 0
                            arg_overrides[id(link)][k] = tgt
118

119 2
        ref = root_model.ref['id']
120 2
        callbacks = []
121 2
        for link, src, tgt in found:
122 2
            cb = cls._callbacks[type(link)]
123 2
            if ((src is None or ref not in getattr(src, '_models', [ref])) or
124
                (getattr(link, '_requires_target', False) and tgt is None) or
125
                (tgt is not None and ref not in getattr(tgt, '_models', [ref]))):
126 0
                continue
127 2
            overrides = arg_overrides.get(id(link), {})
128 2
            callbacks.append(cb(root_model, link, src, tgt,
129
                                arg_overrides=overrides))
130 2
        return callbacks
131

132

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

142
    A Link must define a source object which is what triggers events,
143
    but must not define a target. It is also possible to define bi-
144
    directional links between the source and target object.
145
    """
146

147 2
    bidirectional = param.Boolean(default=False, doc="""
148
        Whether to link source and target in both directions.""")
149

150 2
    properties = param.Dict(default={}, doc="""
151
        A dictionary mapping between source specification to target
152
        specification.""")
153

154
    # Whether the link requires a target
155 2
    _requires_target = True
156

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

164 2
    @property
165
    def target(self):
166 2
        return self._target() if self._target else None
167

168 2
    def link(self):
169
        """
170
        Registers the Link
171
        """
172 0
        self.init()
173 0
        if self.source in self.registry:
174 0
            links = self.registry[self.source]
175 0
            params = {
176
                k: v for k, v in self.param.get_param_values() if k != 'name'}
177 0
            for link in links:
178 0
                link_params = {
179
                    k: v for k, v in link.param.get_param_values() if k != 'name'}
180 0
                if (type(link) is type(self) and link.source is self.source
181
                    and link.target is self.target and params == link_params):
182 0
                    return
183 0
            self.registry[self.source].append(self)
184
        else:
185 0
            self.registry[self.source] = [self]
186

187 2
    def unlink(self):
188
        """
189
        Unregisters the Link
190
        """
191 0
        links = self.registry.get(self.source)
192 0
        if self in links:
193 0
            links.pop(links.index(self))
194

195

196

197 2
class CallbackGenerator(object):
198

199 2
    def __init__(self, root_model, link, source, target=None, arg_overrides={}):
200 2
        self.root_model = root_model
201 2
        self.link = link
202 2
        self.source = source
203 2
        self.target = target
204 2
        self.arg_overrides = arg_overrides
205 2
        self.validate()
206 2
        specs = self._get_specs(link, source, target)
207 2
        for src_spec, tgt_spec, code in specs:
208 2
            try:
209 2
                self._init_callback(root_model, link, source, src_spec, target, tgt_spec, code)
210 0
            except Exception:
211 0
                pass
212
                
213 2
    @classmethod
214
    def _resolve_model(cls, root_model, obj, model_spec):
215
        """
216
        Resolves a model given the supplied object and a model_spec.
217

218
        Arguments
219
        ----------
220
        root_model: bokeh.model.Model
221
          The root bokeh model often used to index models
222
        obj: holoviews.plotting.ElementPlot or bokeh.model.Model or panel.Viewable
223
          The object to look the model up on
224
        model_spec: string
225
          A string defining how to look up the model, can be a single
226
          string defining the handle in a HoloViews plot or a path
227
          split by periods (.) to indicate a multi-level lookup.
228

229
        Returns
230
        -------
231
        model: bokeh.model.Model
232
          The resolved bokeh model
233
        """
234 2
        from .pane.holoviews import is_bokeh_element_plot
235

236 2
        model = None
237 2
        if 'holoviews' in sys.modules and is_bokeh_element_plot(obj):
238 2
            if model_spec is None:
239 2
                return obj.state
240
            else:
241 2
                model_specs = model_spec.split('.')
242 2
                handle_spec = model_specs[0]
243 2
                if len(model_specs) > 1:
244 0
                    model_spec = '.'.join(model_specs[1:])
245
                else:
246 2
                    model_spec = None
247 2
                model = obj.handles[handle_spec]
248 2
        elif isinstance(obj, Viewable):
249 2
            model, _ = obj._models[root_model.ref['id']]
250 2
        elif isinstance(obj, BkModel):
251 2
            model = obj
252 2
        if model_spec is not None:
253 2
            for spec in model_spec.split('.'):
254 2
                model = getattr(model, spec)
255 2
        return model
256

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

261 2
        src_model = self._resolve_model(root_model, source, src_spec[0])
262 2
        ref = root_model.ref['id']
263 2
        link_id = id(link)
264 2
        if any(link_id in cb.tags for cbs in src_model.js_property_callbacks.values() for cb in cbs):
265
            # Skip registering callback if already registered
266 2
            return
267 2
        references['source'] = src_model
268

269 2
        tgt_model = None
270 2
        if link._requires_target:
271 2
            tgt_model = self._resolve_model(root_model, target, tgt_spec[0])
272 2
            if tgt_model is not None:
273 2
                references['target'] = tgt_model
274

275 2
        for k, v in dict(link.args, **self.arg_overrides).items():
276 2
            arg_model = self._resolve_model(root_model, v, None)
277 2
            if arg_model is not None:
278 2
                references[k] = arg_model
279 2
            elif not isinstance(v, param.Parameterized):
280 2
                references[k] = v
281

282 2
        if 'holoviews' in sys.modules:
283 2
            from .pane.holoviews import HoloViews, is_bokeh_element_plot
284

285 2
            if isinstance(source, HoloViews):
286 2
                src = source._plots[ref][0]
287
            else:
288 2
                src = source
289

290 2
            prefix = 'source_' if hasattr(link, 'target') else ''
291 2
            if is_bokeh_element_plot(src):
292 2
                for k, v in src.handles.items():
293 2
                    k = prefix + k
294 2
                    if isinstance(v, BkModel) and k not in references:
295 2
                        references[k] = v
296

297 2
            if isinstance(target, HoloViews):
298 0
                tgt = target._plots[ref][0]
299
            else:
300 2
                tgt = target
301

302 2
            if is_bokeh_element_plot(tgt):
303 2
                for k, v in tgt.handles.items():
304 2
                    k = 'target_' + k
305 2
                    if isinstance(v, BkModel) and k not in references:
306 2
                        references[k] = v
307

308 2
        self._initialize_models(link, source, src_model, src_spec[1], target, tgt_model, tgt_spec[1])
309 2
        self._process_references(references)
310

311 2
        if code is None:
312 2
            code = self._get_code(link, source, src_spec[1], target, tgt_spec[1])
313
        else:
314 2
            code = "try {{ {code} }} catch(err) {{ console.log(err) }}".format(code=code)
315

316 2
        src_cb = CustomJS(args=references, code=code, tags=[link_id])
317 2
        changes, events = self._get_triggers(link, src_spec)
318 2
        for ch in changes:
319 2
            src_model.js_on_change(ch, src_cb)
320 2
        for ev in events:
321 2
            src_model.js_on_event(ev, src_cb)
322

323 2
        if getattr(link, 'bidirectional', False):
324 2
            code = self._get_code(link, target, tgt_spec[1], source, src_spec[1])
325 2
            reverse_references = dict(references)
326 2
            reverse_references['source'] = tgt_model
327 2
            reverse_references['target'] = src_model
328 2
            tgt_cb = CustomJS(args=reverse_references, code=code, tags=[link_id])
329 2
            changes, events = self._get_triggers(link, tgt_spec)
330 2
            for ch in changes:
331 2
                tgt_model.js_on_change(ch, tgt_cb)
332 2
            for ev in events:
333 0
                tgt_model.js_on_event(ev, tgt_cb)
334

335 2
    def _process_references(self, references):
336
        """
337
        Method to process references in place.
338
        """
339

340 2
    def _get_specs(self, link):
341
        """
342
        Return a list of spec tuples that define source and target
343
        models.
344
        """
345 0
        return []
346

347 2
    def _get_code(self, link, source, target):
348
        """
349
        Returns the code to be executed.
350
        """
351 0
        return ''
352

353 2
    def _get_triggers(self, link, src_spec):
354
        """
355
        Returns the changes and events that trigger the callback.
356
        """
357 0
        return [], []
358

359 2
    def _initialize_models(self, link, source, src_model, src_spec, target, tgt_model, tgt_spec):
360
        """
361
        Applies any necessary initialization to the source and target
362
        models.
363
        """
364 2
        pass
365

366 2
    def validate(self):
367 2
        pass
368

369

370

371 2
class JSCallbackGenerator(CallbackGenerator):
372

373 2
    def _get_triggers(self, link, src_spec):
374 2
        if src_spec[1].startswith('event:'):
375 2
            return [], [src_spec[1].split(':')[1]]
376 2
        return [src_spec[1]], []
377

378 2
    def _get_specs(self, link, source, target):
379 2
        for src_spec, code in link.code.items():
380 2
            src_specs = src_spec.split('.')
381 2
            if src_spec.startswith('event:'):
382 2
                src_spec = (None, src_spec)
383 2
            elif len(src_specs) > 1:
384 2
                src_spec = ('.'.join(src_specs[:-1]), src_specs[-1])
385
            else:
386 2
                src_prop = src_specs[0]
387 2
                if isinstance(source, Reactive):
388 2
                    src_prop = source._rename.get(src_prop, src_prop)
389 2
                src_spec = (None, src_prop)
390 2
        return [(src_spec, (None, None), code)]
391

392

393

394 2
class JSLinkCallbackGenerator(JSCallbackGenerator):
395

396 2
    _link_template = """
397
    var value = source['{src_attr}'];
398
    value = {src_transform};
399
    value = {tgt_transform};
400
    try {{
401
      var property = target.properties['{tgt_attr}'];
402
      if (property !== undefined) {{ property.validate(value); }}
403
    }} catch(err) {{
404
      console.log('WARNING: Could not set {tgt_attr} on target, raised error: ' + err);
405
      return;
406
    }}
407
    try {{
408
      target['{tgt_attr}'] = value;
409
    }} catch(err) {{
410
      console.log(err)
411
    }}
412
    """
413

414 2
    def _get_specs(self, link, source, target):
415 2
        if link.code:
416 2
            return super(JSLinkCallbackGenerator, self)._get_specs(link, source, target)
417

418 2
        specs = []
419 2
        for src_spec, tgt_spec in link.properties.items():
420 2
            src_specs = src_spec.split('.')
421 2
            if len(src_specs) > 1:
422 2
                src_spec = ('.'.join(src_specs[:-1]), src_specs[-1])
423
            else:
424 2
                src_prop = src_specs[0]
425 2
                if isinstance(source, Reactive):
426 2
                    src_prop = source._rename.get(src_prop, src_prop)
427 2
                src_spec = (None, src_prop)
428 2
            tgt_specs = tgt_spec.split('.')
429 2
            if len(tgt_specs) > 1:
430 2
                tgt_spec = ('.'.join(tgt_specs[:-1]), tgt_specs[-1])
431
            else:
432 2
                tgt_prop = tgt_specs[0]
433 2
                if isinstance(target, Reactive):
434 2
                    tgt_prop = target._rename.get(tgt_prop, tgt_prop)
435 2
                tgt_spec = (None, tgt_prop)
436 2
            specs.append((src_spec, tgt_spec, None))
437 2
        return specs
438

439 2
    def _initialize_models(self, link, source, src_model, src_spec, target, tgt_model, tgt_spec):
440 2
        if tgt_model and src_spec and tgt_spec:
441 2
            src_reverse = {v: k for k, v in getattr(source, '_rename', {}).items()}
442 2
            src_param = src_reverse.get(src_spec, src_spec)
443 2
            if (hasattr(source, '_process_property_change') and
444
                src_param in source.param and hasattr(target, '_process_param_change')):
445 2
                tgt_reverse = {v: k for k, v in target._rename.items()}
446 2
                tgt_param = tgt_reverse.get(tgt_spec, tgt_spec)
447 2
                value = getattr(source, src_param)
448 2
                try:
449 2
                    msg = target._process_param_change({tgt_param: value})
450 0
                except Exception:
451 0
                    msg = {}
452 2
                if tgt_spec in msg:
453 2
                    value = msg[tgt_spec]
454
            else:
455 2
                value = getattr(src_model, src_spec)
456 2
            if value:
457 2
                setattr(tgt_model, tgt_spec, value)
458 2
        if tgt_model is None and not link.code:
459 0
            raise ValueError('Model could not be resolved on target '
460
                             '%s and no custom code was specified.' %
461
                             type(self.target).__name__)
462

463 2
    def _process_references(self, references):
464
        """
465
        Strips target_ prefix from references.
466
        """
467 2
        for k in list(references):
468 2
            if k == 'target' or not k.startswith('target_') or k[7:] in references:
469 2
                continue
470 2
            references[k[7:]] = references.pop(k)
471

472 2
    def _get_code(self, link, source, src_spec, target, tgt_spec):
473 2
        if isinstance(source, Reactive):
474 2
            src_reverse = {v: k for k, v in source._rename.items()}
475 2
            src_param = src_reverse.get(src_spec, src_spec)
476 2
            src_transform = source._source_transforms.get(src_param)
477 2
            if src_transform is None:
478 2
                src_transform = 'value'
479
        else:
480 2
            src_transform = 'value'
481 2
        if isinstance(target, Reactive):
482 2
            tgt_reverse = {v: k for k, v in target._rename.items()}
483 2
            tgt_param = tgt_reverse.get(tgt_spec, tgt_spec)
484 2
            tgt_transform = target._target_transforms.get(tgt_param)
485 2
            if tgt_transform is None:
486 2
                tgt_transform = 'value'
487
        else:
488 2
            tgt_transform = 'value'
489 2
        return self._link_template.format(
490
            src_attr=src_spec, tgt_attr=tgt_spec,
491
            src_transform=src_transform, tgt_transform=tgt_transform
492
        )
493

494 2
Callback.register_callback(callback=JSCallbackGenerator)
495 2
Link.register_callback(callback=JSLinkCallbackGenerator)
496

497 2
Viewable._preprocessing_hooks.append(Callback._process_callbacks)

Read our documentation on viewing source code .

Loading