1
|
|
"""
|
2
|
|
Defines the PaneBase class defining the API for panes which convert
|
3
|
|
objects to a visual representation expressed as a bokeh model.
|
4
|
|
"""
|
5
|
6
|
from __future__ import absolute_import, division, unicode_literals
|
6
|
|
|
7
|
6
|
from functools import partial
|
8
|
|
|
9
|
6
|
import param
|
10
|
|
|
11
|
6
|
from bokeh.models.layouts import GridBox as _BkGridBox
|
12
|
|
|
13
|
6
|
from ..io import init_doc, push, state, unlocked
|
14
|
6
|
from ..layout import Panel, Row
|
15
|
6
|
from ..links import Link
|
16
|
6
|
from ..reactive import Reactive
|
17
|
6
|
from ..viewable import Layoutable, Viewable
|
18
|
6
|
from ..util import param_reprs
|
19
|
|
|
20
|
|
|
21
|
6
|
def Pane(obj, **kwargs):
|
22
|
|
"""
|
23
|
|
Converts any object to a Pane if a matching Pane class exists.
|
24
|
|
"""
|
25
|
6
|
if isinstance(obj, Viewable):
|
26
|
6
|
return obj
|
27
|
6
|
return PaneBase.get_pane_type(obj, **kwargs)(obj, **kwargs)
|
28
|
|
|
29
|
|
|
30
|
6
|
def panel(obj, **kwargs):
|
31
|
|
"""
|
32
|
|
Creates a panel from any supplied object by wrapping it in a pane
|
33
|
|
and returning a corresponding Panel.
|
34
|
|
|
35
|
|
Arguments
|
36
|
|
---------
|
37
|
|
obj: object
|
38
|
|
Any object to be turned into a Panel
|
39
|
|
**kwargs: dict
|
40
|
|
Any keyword arguments to be passed to the applicable Pane
|
41
|
|
|
42
|
|
Returns
|
43
|
|
-------
|
44
|
|
layout: Viewable
|
45
|
|
A Viewable representation of the input object
|
46
|
|
"""
|
47
|
6
|
if isinstance(obj, Viewable):
|
48
|
6
|
return obj
|
49
|
6
|
if kwargs.get('name', False) is None:
|
50
|
6
|
kwargs.pop('name')
|
51
|
6
|
pane = PaneBase.get_pane_type(obj, **kwargs)(obj, **kwargs)
|
52
|
6
|
if len(pane.layout) == 1 and pane._unpack:
|
53
|
6
|
return pane.layout[0]
|
54
|
6
|
return pane.layout
|
55
|
|
|
56
|
|
|
57
|
6
|
class RerenderError(RuntimeError):
|
58
|
|
"""
|
59
|
|
Error raised when a pane requests re-rendering during initial render.
|
60
|
|
"""
|
61
|
|
|
62
|
|
|
63
|
6
|
class PaneBase(Reactive):
|
64
|
|
"""
|
65
|
|
PaneBase is the abstract baseclass for all atomic displayable units
|
66
|
|
in the panel library. Pane defines an extensible interface for
|
67
|
|
wrapping arbitrary objects and transforming them into Bokeh models.
|
68
|
|
|
69
|
|
Panes are reactive in the sense that when the object they are
|
70
|
|
wrapping is changed any dashboard containing the pane will update
|
71
|
|
in response.
|
72
|
|
|
73
|
|
To define a concrete Pane type subclass this class and implement
|
74
|
|
the applies classmethod and the _get_model private method.
|
75
|
|
"""
|
76
|
|
|
77
|
6
|
default_layout = param.ClassSelector(default=Row, class_=(Panel),
|
78
|
|
is_instance=False, doc="""
|
79
|
|
Defines the layout the model(s) returned by the pane will
|
80
|
|
be placed in.""")
|
81
|
|
|
82
|
6
|
object = param.Parameter(default=None, doc="""
|
83
|
|
The object being wrapped, which will be converted to a
|
84
|
|
Bokeh model.""")
|
85
|
|
|
86
|
|
# When multiple Panes apply to an object, the one with the highest
|
87
|
|
# numerical priority is selected. The default is an intermediate value.
|
88
|
|
# If set to None, applies method will be called to get a priority
|
89
|
|
# value for a specific object type.
|
90
|
6
|
priority = 0.5
|
91
|
|
|
92
|
|
# Whether applies requires full set of keywords
|
93
|
6
|
_applies_kw = False
|
94
|
|
|
95
|
|
# Whether the Pane layout can be safely unpacked
|
96
|
6
|
_unpack = True
|
97
|
|
|
98
|
|
# Declares whether Pane supports updates to the Bokeh model
|
99
|
6
|
_updates = False
|
100
|
|
|
101
|
|
# List of parameters that trigger a rerender of the Bokeh model
|
102
|
6
|
_rerender_params = ['object']
|
103
|
|
|
104
|
6
|
__abstract = True
|
105
|
|
|
106
|
6
|
def __init__(self, object=None, **params):
|
107
|
6
|
applies = self.applies(object, **(params if self._applies_kw else {}))
|
108
|
6
|
if (isinstance(applies, bool) and not applies) and object is not None :
|
109
|
0
|
self._type_error(object)
|
110
|
|
|
111
|
6
|
super(PaneBase, self).__init__(object=object, **params)
|
112
|
6
|
kwargs = {k: v for k, v in params.items() if k in Layoutable.param}
|
113
|
6
|
self.layout = self.default_layout(self, **kwargs)
|
114
|
6
|
watcher = self.param.watch(self._update_pane, self._rerender_params)
|
115
|
6
|
self._callbacks.append(watcher)
|
116
|
|
|
117
|
6
|
def _type_error(self, object):
|
118
|
0
|
raise ValueError("%s pane does not support objects of type '%s'." %
|
119
|
|
(type(self).__name__, type(object).__name__))
|
120
|
|
|
121
|
6
|
def __repr__(self, depth=0):
|
122
|
6
|
cls = type(self).__name__
|
123
|
6
|
params = param_reprs(self, ['object'])
|
124
|
6
|
obj = 'None' if self.object is None else type(self.object).__name__
|
125
|
6
|
template = '{cls}({obj}, {params})' if params else '{cls}({obj})'
|
126
|
6
|
return template.format(cls=cls, params=', '.join(params), obj=obj)
|
127
|
|
|
128
|
6
|
def __getitem__(self, index):
|
129
|
|
"""
|
130
|
|
Allows pane objects to behave like the underlying layout
|
131
|
|
"""
|
132
|
6
|
return self.layout[index]
|
133
|
|
|
134
|
|
#----------------------------------------------------------------
|
135
|
|
# Callback API
|
136
|
|
#----------------------------------------------------------------
|
137
|
|
|
138
|
6
|
@property
|
139
|
1
|
def _linkable_params(self):
|
140
|
0
|
return [p for p in self._synced_params() if self._rename.get(p, False) is not None]
|
141
|
|
|
142
|
6
|
def _synced_params(self):
|
143
|
6
|
ignored_params = ['name', 'default_layout']+self._rerender_params
|
144
|
6
|
return [p for p in self.param if p not in ignored_params]
|
145
|
|
|
146
|
6
|
def _init_properties(self):
|
147
|
6
|
return {k: v for k, v in self.param.get_param_values()
|
148
|
|
if v is not None and k not in ['default_layout', 'object']}
|
149
|
|
|
150
|
6
|
def _update_object(self, ref, doc, root, parent, comm):
|
151
|
6
|
old_model = self._models[ref][0]
|
152
|
6
|
if self._updates:
|
153
|
6
|
self._update(ref, old_model)
|
154
|
|
else:
|
155
|
6
|
new_model = self._get_model(doc, root, parent, comm)
|
156
|
6
|
try:
|
157
|
6
|
if isinstance(parent, _BkGridBox):
|
158
|
0
|
indexes = [i for i, child in enumerate(parent.children)
|
159
|
|
if child[0] is old_model]
|
160
|
0
|
if indexes:
|
161
|
0
|
index = indexes[0]
|
162
|
|
else:
|
163
|
0
|
raise ValueError
|
164
|
0
|
new_model = (new_model,) + parent.children[index][1:]
|
165
|
|
else:
|
166
|
6
|
index = parent.children.index(old_model)
|
167
|
0
|
except ValueError:
|
168
|
0
|
self.warning('%s pane model %s could not be replaced '
|
169
|
|
'with new model %s, ensure that the '
|
170
|
|
'parent is not modified at the same '
|
171
|
|
'time the panel is being updated.' %
|
172
|
|
(type(self).__name__, old_model, new_model))
|
173
|
|
else:
|
174
|
6
|
parent.children[index] = new_model
|
175
|
|
|
176
|
6
|
from ..io import state
|
177
|
6
|
ref = root.ref['id']
|
178
|
6
|
if ref in state._views:
|
179
|
6
|
state._views[ref][0]._preprocess(root)
|
180
|
|
|
181
|
6
|
def _update_pane(self, *events):
|
182
|
6
|
for ref, (_, parent) in self._models.items():
|
183
|
6
|
if ref not in state._views or ref in state._fake_roots:
|
184
|
0
|
continue
|
185
|
6
|
viewable, root, doc, comm = state._views[ref]
|
186
|
6
|
if comm or state._unblocked(doc):
|
187
|
6
|
with unlocked():
|
188
|
6
|
self._update_object(ref, doc, root, parent, comm)
|
189
|
6
|
if comm and 'embedded' not in root.tags:
|
190
|
6
|
push(doc, comm)
|
191
|
|
else:
|
192
|
6
|
cb = partial(self._update_object, ref, doc, root, parent, comm)
|
193
|
6
|
if doc.session_context:
|
194
|
6
|
doc.add_next_tick_callback(cb)
|
195
|
|
else:
|
196
|
0
|
cb()
|
197
|
|
|
198
|
6
|
def _update(self, ref=None, model=None):
|
199
|
|
"""
|
200
|
|
If _updates=True this method is used to update an existing
|
201
|
|
Bokeh model instead of replacing the model entirely. The
|
202
|
|
supplied model should be updated with the current state.
|
203
|
|
"""
|
204
|
0
|
raise NotImplementedError
|
205
|
|
|
206
|
|
#----------------------------------------------------------------
|
207
|
|
# Public API
|
208
|
|
#----------------------------------------------------------------
|
209
|
|
|
210
|
6
|
@classmethod
|
211
|
1
|
def applies(cls, obj):
|
212
|
|
"""
|
213
|
|
Given the object return a boolean indicating whether the Pane
|
214
|
|
can render the object. If the priority of the pane is set to
|
215
|
|
None, this method may also be used to define a priority
|
216
|
|
depending on the object being rendered.
|
217
|
|
"""
|
218
|
0
|
return None
|
219
|
|
|
220
|
6
|
def clone(self, object=None, **params):
|
221
|
|
"""
|
222
|
|
Makes a copy of the Pane sharing the same parameters.
|
223
|
|
|
224
|
|
Arguments
|
225
|
|
---------
|
226
|
|
params: Keyword arguments override the parameters on the clone.
|
227
|
|
|
228
|
|
Returns
|
229
|
|
-------
|
230
|
|
Cloned Pane object
|
231
|
|
"""
|
232
|
6
|
params = dict(self.param.get_param_values(), **params)
|
233
|
6
|
old_object = params.pop('object')
|
234
|
6
|
if object is None:
|
235
|
6
|
object = old_object
|
236
|
6
|
return type(self)(object, **params)
|
237
|
|
|
238
|
6
|
def get_root(self, doc=None, comm=None, preprocess=True):
|
239
|
|
"""
|
240
|
|
Returns the root model and applies pre-processing hooks
|
241
|
|
|
242
|
|
Arguments
|
243
|
|
---------
|
244
|
|
doc: bokeh.Document
|
245
|
|
Bokeh document the bokeh model will be attached to.
|
246
|
|
comm: pyviz_comms.Comm
|
247
|
|
Optional pyviz_comms when working in notebook
|
248
|
|
preprocess: boolean (default=True)
|
249
|
|
Whether to run preprocessing hooks
|
250
|
|
|
251
|
|
Returns
|
252
|
|
-------
|
253
|
|
Returns the bokeh model corresponding to this panel object
|
254
|
|
"""
|
255
|
6
|
doc = init_doc(doc)
|
256
|
6
|
if self._updates:
|
257
|
6
|
root = self._get_model(doc, comm=comm)
|
258
|
|
else:
|
259
|
6
|
root = self.layout._get_model(doc, comm=comm)
|
260
|
6
|
if preprocess:
|
261
|
6
|
self._preprocess(root)
|
262
|
6
|
ref = root.ref['id']
|
263
|
6
|
state._views[ref] = (self, root, doc, comm)
|
264
|
6
|
return root
|
265
|
|
|
266
|
6
|
@classmethod
|
267
|
1
|
def get_pane_type(cls, obj, **kwargs):
|
268
|
|
"""
|
269
|
|
Returns the applicable Pane type given an object by resolving
|
270
|
|
the precedence of all types whose applies method declares that
|
271
|
|
the object is supported.
|
272
|
|
|
273
|
|
Arguments
|
274
|
|
---------
|
275
|
|
obj (object): The object type to return a Pane for
|
276
|
|
|
277
|
|
Returns
|
278
|
|
-------
|
279
|
|
The applicable Pane type with the highest precedence.
|
280
|
|
"""
|
281
|
6
|
if isinstance(obj, Viewable):
|
282
|
6
|
return type(obj)
|
283
|
6
|
descendents = []
|
284
|
6
|
for p in param.concrete_descendents(PaneBase).values():
|
285
|
6
|
if p.priority is None:
|
286
|
6
|
applies = True
|
287
|
6
|
try:
|
288
|
6
|
priority = p.applies(obj, **(kwargs if p._applies_kw else {}))
|
289
|
0
|
except Exception:
|
290
|
0
|
priority = False
|
291
|
|
else:
|
292
|
6
|
applies = None
|
293
|
6
|
priority = p.priority
|
294
|
6
|
if isinstance(priority, bool) and priority:
|
295
|
0
|
raise ValueError('If a Pane declares no priority '
|
296
|
|
'the applies method should return a '
|
297
|
|
'priority value specific to the '
|
298
|
|
'object type or False, but the %s pane '
|
299
|
|
'declares no priority.' % p.__name__)
|
300
|
6
|
elif priority is None or priority is False:
|
301
|
6
|
continue
|
302
|
6
|
descendents.append((priority, applies, p))
|
303
|
6
|
pane_types = reversed(sorted(descendents, key=lambda x: x[0]))
|
304
|
6
|
for _, applies, pane_type in pane_types:
|
305
|
6
|
if applies is None:
|
306
|
6
|
try:
|
307
|
6
|
applies = pane_type.applies(obj, **(kwargs if pane_type._applies_kw else {}))
|
308
|
0
|
except Exception:
|
309
|
0
|
applies = False
|
310
|
6
|
if not applies:
|
311
|
6
|
continue
|
312
|
6
|
return pane_type
|
313
|
0
|
raise TypeError('%s type could not be rendered.' % type(obj).__name__)
|
314
|
|
|
315
|
|
|
316
|
|
|
317
|
6
|
class ReplacementPane(PaneBase):
|
318
|
|
"""
|
319
|
|
A Pane type which allows for complete replacement of the underlying
|
320
|
|
bokeh model by creating an internal layout to replace the children
|
321
|
|
on.
|
322
|
|
"""
|
323
|
|
|
324
|
6
|
_updates = True
|
325
|
|
|
326
|
6
|
__abstract = True
|
327
|
|
|
328
|
6
|
def __init__(self, object=None, **params):
|
329
|
6
|
self._kwargs = {p: params.pop(p) for p in list(params)
|
330
|
|
if p not in self.param}
|
331
|
6
|
super(ReplacementPane, self).__init__(object, **params)
|
332
|
6
|
self._pane = Pane(None)
|
333
|
6
|
self._internal = True
|
334
|
6
|
self._inner_layout = Row(self._pane, **{k: v for k, v in params.items() if k in Row.param})
|
335
|
6
|
self.param.watch(self._update_inner_layout, list(Layoutable.param))
|
336
|
|
|
337
|
6
|
def _update_inner_layout(self, *events):
|
338
|
6
|
for event in events:
|
339
|
6
|
setattr(self._pane, event.name, event.new)
|
340
|
6
|
if event.name in ['sizing_mode', 'width_policy', 'height_policy']:
|
341
|
6
|
setattr(self._inner_layout, event.name, event.new)
|
342
|
|
|
343
|
6
|
def _update_pane(self, *events):
|
344
|
|
"""
|
345
|
|
Updating of the object should be handled manually.
|
346
|
|
"""
|
347
|
|
|
348
|
6
|
@classmethod
|
349
|
1
|
def _update_from_object(cls, object, old_object, was_internal, **kwargs):
|
350
|
6
|
pane_type = cls.get_pane_type(object)
|
351
|
6
|
try:
|
352
|
6
|
links = Link.registry.get(object)
|
353
|
6
|
except TypeError:
|
354
|
6
|
links = []
|
355
|
6
|
custom_watchers = False
|
356
|
6
|
if isinstance(object, Reactive):
|
357
|
6
|
watchers = [
|
358
|
|
w for pwatchers in object._param_watchers.values()
|
359
|
|
for awatchers in pwatchers.values() for w in awatchers
|
360
|
|
]
|
361
|
6
|
custom_watchers = [wfn for wfn in watchers if wfn not in object._callbacks]
|
362
|
|
|
363
|
6
|
pane, internal = None, was_internal
|
364
|
6
|
if type(old_object) is pane_type and not links and not custom_watchers and was_internal:
|
365
|
|
# If the object has not external referrers we can update
|
366
|
|
# it inplace instead of replacing it
|
367
|
6
|
if isinstance(object, Reactive):
|
368
|
6
|
pvals = dict(old_object.param.get_param_values())
|
369
|
6
|
new_params = {k: v for k, v in object.param.get_param_values()
|
370
|
|
if k != 'name' and v is not pvals[k]}
|
371
|
6
|
old_object.param.set_param(**new_params)
|
372
|
|
else:
|
373
|
6
|
old_object.object = object
|
374
|
|
else:
|
375
|
|
# Replace pane entirely
|
376
|
6
|
pane = panel(object, **{k: v for k, v in kwargs.items()
|
377
|
|
if k in pane_type.param})
|
378
|
6
|
if pane is object:
|
379
|
|
# If all watchers on the object are internal watchers
|
380
|
|
# we can make a clone of the object and update this
|
381
|
|
# clone going forward, otherwise we have replace the
|
382
|
|
# model entirely which is more expensive.
|
383
|
6
|
if not (custom_watchers or links):
|
384
|
6
|
pane = object.clone()
|
385
|
6
|
internal = True
|
386
|
|
else:
|
387
|
6
|
internal = False
|
388
|
|
else:
|
389
|
6
|
internal = object is not old_object
|
390
|
6
|
return pane, internal
|
391
|
|
|
392
|
6
|
def _update_inner(self, new_object):
|
393
|
6
|
kwargs = dict(self.param.get_param_values(), **self._kwargs)
|
394
|
6
|
del kwargs['object']
|
395
|
6
|
new_pane, internal = self._update_from_object(
|
396
|
|
new_object, self._pane, self._internal, **kwargs
|
397
|
|
)
|
398
|
6
|
if new_pane is None:
|
399
|
6
|
return
|
400
|
|
|
401
|
6
|
self._pane = new_pane
|
402
|
6
|
self._inner_layout[0] = self._pane
|
403
|
6
|
self._internal = internal
|
404
|
|
|
405
|
6
|
def _get_model(self, doc, root=None, parent=None, comm=None):
|
406
|
6
|
if root:
|
407
|
6
|
ref = root.ref['id']
|
408
|
6
|
if ref in self._models:
|
409
|
0
|
self._cleanup(root)
|
410
|
6
|
model = self._inner_layout._get_model(doc, root, parent, comm)
|
411
|
6
|
if root is None:
|
412
|
6
|
ref = model.ref['id']
|
413
|
6
|
self._models[ref] = (model, parent)
|
414
|
6
|
return model
|
415
|
|
|
416
|
6
|
def _cleanup(self, root=None):
|
417
|
6
|
self._inner_layout._cleanup(root)
|
418
|
6
|
super(ReplacementPane, self)._cleanup(root)
|
419
|
|
|
420
|
6
|
def select(self, selector=None):
|
421
|
|
"""
|
422
|
|
Iterates over the Viewable and any potential children in the
|
423
|
|
applying the Selector.
|
424
|
|
|
425
|
|
Arguments
|
426
|
|
---------
|
427
|
|
selector: type or callable or None
|
428
|
|
The selector allows selecting a subset of Viewables by
|
429
|
|
declaring a type or callable function to filter by.
|
430
|
|
|
431
|
|
Returns
|
432
|
|
-------
|
433
|
|
viewables: list(Viewable)
|
434
|
|
"""
|
435
|
6
|
selected = super(ReplacementPane, self).select(selector)
|
436
|
6
|
selected += self._pane.select(selector)
|
437
|
6
|
return selected
|