diff --git a/panel/io.py b/panel/io.py
index 5da9c2ec9b3..5f0cab907de 100644
--- a/panel/io.py
+++ b/panel/io.py
@@ -82,7 +82,7 @@ def _cleanup_panel(msg_id):
"""
if msg_id not in state._views:
return
- viewable, model = state._views.pop(msg_id)
+ viewable, model, _, _ = state._views.pop(msg_id)
viewable._cleanup(model)
diff --git a/panel/layout.py b/panel/layout.py
index b9419a8b083..10b48c771cf 100644
--- a/panel/layout.py
+++ b/panel/layout.py
@@ -36,34 +36,15 @@ def __init__(self, *objects, **params):
objects = [panel(pane) for pane in objects]
super(Panel, self).__init__(objects=objects, **params)
- def _link_params(self, model, params, doc, root, comm=None):
- def set_value(*events):
- msg = {event.name: event.new for event in events}
- events = {event.name: event for event in events}
-
- def update_model():
- if 'objects' in msg:
- old = events['objects'].old
- msg['objects'] = self._get_objects(model, old, doc, root, comm)
- for pane in old:
- if pane not in self.objects:
- pane._cleanup(root)
- self._preprocess(root) #preprocess links between new elements
- processed = self._process_param_change(msg)
- model.update(**processed)
-
- if comm:
- update_model()
- push(doc, comm)
- elif state.curdoc:
- update_model()
- else:
- doc.add_next_tick_callback(update_model)
-
- ref = root.ref['id']
- if ref not in self._callbacks:
- watcher = self.param.watch(set_value, params)
- self._callbacks[ref].append(watcher)
+ def _update_model(self, events, msg, root, model, doc, comm):
+ if self._rename['objects'] in msg:
+ old = events['objects'].old
+ msg[self._rename['objects']] = self._get_objects(model, old, doc, root, comm)
+ for pane in old:
+ if pane not in self.objects:
+ pane._cleanup(root)
+ model.update(**msg)
+ self._preprocess(root) #preprocess links between new elements
def _cleanup(self, root=None, final=False):
super(Panel, self)._cleanup(root, final)
@@ -71,26 +52,6 @@ def _cleanup(self, root=None, final=False):
for p in self.objects:
p._cleanup(root, final)
- def select(self, selector=None):
- """
- Iterates over the Viewable and any potential children in the
- applying the Selector.
-
- Arguments
- ---------
- selector: type or callable or None
- The selector allows selecting a subset of Viewables by
- declaring a type or callable function to filter by.
-
- Returns
- -------
- viewables: list(Viewable)
- """
- objects = super(Panel, self).select(selector)
- for obj in self.objects:
- objects += obj.select(selector)
- return objects
-
def _get_objects(self, model, old_objects, doc, root, comm=None):
"""
Returns new child models for the layout while reusing unchanged
@@ -102,7 +63,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
pane = panel(pane)
self.objects[i] = pane
if pane in old_objects:
- child = pane._models[root.ref['id']]
+ child, _ = pane._models[root.ref['id']]
else:
child = pane._get_model(doc, root, model, comm)
new_models.append(child)
@@ -115,9 +76,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
objects = self._get_objects(model, [], doc, root, comm)
props = dict(self._init_properties(), objects=objects)
model.update(**self._process_param_change(props))
- params = [p for p in self.param if p != 'name']
- self._models[root.ref['id']] = model
- self._link_params(model, params, doc, root, comm)
+ self._models[root.ref['id']] = (model, parent)
self._link_props(model, self._linked_props, doc, root, comm)
return model
@@ -190,6 +149,30 @@ def __repr__(self, depth=0, max_depth=10):
objs=('%s' % spacer).join(objs), spacer=spacer
)
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
+
+ def select(self, selector=None):
+ """
+ Iterates over the Viewable and any potential children in the
+ applying the Selector.
+
+ Arguments
+ ---------
+ selector: type or callable or None
+ The selector allows selecting a subset of Viewables by
+ declaring a type or callable function to filter by.
+
+ Returns
+ -------
+ viewables: list(Viewable)
+ """
+ objects = super(Panel, self).select(selector)
+ for obj in self.objects:
+ objects += obj.select(selector)
+ return objects
+
def append(self, pane):
from .pane import panel
new_objects = list(self)
@@ -318,7 +301,7 @@ def _get_objects(self, model, old_objects, doc, root, comm=None):
pane = panel(pane, name=name)
self.objects[i] = pane
if pane in old_objects:
- child = pane._models[root.ref['id']]
+ child, _ = pane._models[root.ref['id']]
else:
child = pane._get_model(doc, root, model, comm)
child = BkPanel(title=name, name=pane.name, child=child)
@@ -361,6 +344,10 @@ def __setitem__(self, index, panes):
new_objects[i], self._names[i] = self._to_object_and_name(pane)
self.objects = new_objects
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
+
def append(self, pane):
new_object, new_name = self._to_object_and_name(pane)
new_objects = list(self)
@@ -415,11 +402,11 @@ class Spacer(Reactive):
_bokeh_model = BkSpacer
def _get_model(self, doc, root=None, parent=None, comm=None):
- model = self._bokeh_model(**self._process_param_change(self._init_properties()))
+ properties = self._process_param_change(self._init_properties())
+ model = self._bokeh_model(**properties)
if root is None:
root = model
- self._models[root.ref['id']] = model
- self._link_params(model, ['width', 'height'], doc, root, comm)
+ self._models[root.ref['id']] = (model, parent)
return model
diff --git a/panel/pane/base.py b/panel/pane/base.py
index b7b87513180..21c1507f539 100644
--- a/panel/pane/base.py
+++ b/panel/pane/base.py
@@ -57,39 +57,6 @@ class PaneBase(Reactive):
__abstract = True
- @classmethod
- def applies(cls, obj):
- """
- Given the object return a boolean indicating whether the Pane
- can render the object. If the priority of the pane is set to
- None, this method may also be used to define a priority
- depending on the object being rendered.
- """
- return None
-
- @classmethod
- def get_pane_type(cls, obj):
- if isinstance(obj, Viewable):
- return type(obj)
- descendents = []
- for p in param.concrete_descendents(PaneBase).values():
- priority = p.applies(obj) if p.priority is None else p.priority
- if isinstance(priority, bool) and priority:
- raise ValueError('If a Pane declares no priority '
- 'the applies method should return a '
- 'priority value specific to the '
- 'object type or False, but the %s pane '
- 'declares no priority.' % p.__name__)
- elif priority is None or priority is False:
- continue
- descendents.append((priority, p))
- pane_types = reversed(sorted(descendents, key=lambda x: x[0]))
- for _, pane_type in pane_types:
- applies = pane_type.applies(obj)
- if isinstance(applies, bool) and not applies: continue
- return pane_type
- raise TypeError('%s type could not be rendered.' % type(obj).__name__)
-
def __init__(self, object, **params):
applies = self.applies(object)
if isinstance(applies, bool) and not applies:
@@ -99,6 +66,7 @@ def __init__(self, object, **params):
super(PaneBase, self).__init__(object=object, **params)
kwargs = {k: v for k, v in params.items() if k in Layoutable.param}
self.layout = self.default_layout(self, **kwargs)
+ self.param.watch(self._update_pane, 'object')
def __repr__(self, depth=0):
cls = type(self).__name__
@@ -119,57 +87,94 @@ def _get_root(self, doc, comm=None):
else:
root = self.layout._get_model(doc, comm=comm)
self._preprocess(root)
+ ref = root.ref['id']
+ state._views[ref] = (self, root, doc, comm)
return root
- def _cleanup(self, root=None, final=False):
- super(PaneBase, self)._cleanup(root, final)
- if final:
- self.object = None
-
def _update(self, model):
"""
- If _updates=True this method is used to update an existing Bokeh
- model instead of replacing the model entirely. The supplied model
- should be updated with the current state.
+ If _updates=True this method is used to update an existing
+ Bokeh model instead of replacing the model entirely. The
+ supplied model should be updated with the current state.
"""
raise NotImplementedError
- def _link_object(self, doc, root, parent, comm=None):
+ def _synced_params(self):
+ return [p for p in self.param if p not in ['object', 'name']]
+
+ def _update_object(self, old_model, doc, root, parent, comm):
+ if self._updates:
+ self._update(old_model)
+ else:
+ new_model = self._get_model(doc, root, parent, comm)
+ try:
+ index = parent.children.index(old_model)
+ except IndexError:
+ self.warning('%s pane model %s could not be replaced '
+ 'with new model %s, ensure that the '
+ 'parent is not modified at the same '
+ 'time the panel is being updated.' %
+ (type(self).__name__, old_model, new_model))
+ else:
+ parent.children[index] = new_model
+
+ def _update_pane(self, event):
+ for ref, (model, parent) in self._models.items():
+ viewable, root, doc, comm = state._views[ref]
+ if comm or state.curdoc:
+ self._update_object(model, doc, root, parent, comm)
+ if comm:
+ push(doc, comm)
+ else:
+ cb = partial(self._update_model, model, doc, root, parent, comm)
+ doc.add_next_tick_callback(cb)
+
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
+
+ @classmethod
+ def applies(cls, obj):
"""
- Links the object parameter to the rendered Bokeh model, triggering
- an update when the object changes.
+ Given the object return a boolean indicating whether the Pane
+ can render the object. If the priority of the pane is set to
+ None, this method may also be used to define a priority
+ depending on the object being rendered.
"""
- ref = root.ref['id']
+ return None
- def update_pane(change):
- old_model = self._models[ref]
+ @classmethod
+ def get_pane_type(cls, obj):
+ """
+ Returns the applicable Pane type given an object by resolving
+ the precedence of all types whose applies method declares that
+ the object is supported.
- if self._updates:
- # Pane supports model updates
- def update_models():
- self._update(old_model)
- else:
- # Otherwise replace the whole model
- new_model = self._get_model(doc, root, parent, comm)
- def update_models():
- try:
- index = parent.children.index(old_model)
- except IndexError:
- self.warning('%s pane model %s could not be replaced '
- 'with new model %s, ensure that the '
- 'parent is not modified at the same '
- 'time the panel is being updated.' %
- (type(self).__name__, old_model, new_model))
- else:
- parent.children[index] = new_model
-
- if comm:
- update_models()
- push(doc, comm)
- elif state.curdoc:
- update_models()
- else:
- doc.add_next_tick_callback(update_models)
+ Parameters
+ ----------
+ obj (object): The object type to return a Pane for
- if ref not in self._callbacks:
- self._callbacks[ref].append(self.param.watch(update_pane, 'object'))
+ Returns
+ -------
+ The applicable Pane type with the highest precedence.
+ """
+ if isinstance(obj, Viewable):
+ return type(obj)
+ descendents = []
+ for p in param.concrete_descendents(PaneBase).values():
+ priority = p.applies(obj) if p.priority is None else p.priority
+ if isinstance(priority, bool) and priority:
+ raise ValueError('If a Pane declares no priority '
+ 'the applies method should return a '
+ 'priority value specific to the '
+ 'object type or False, but the %s pane '
+ 'declares no priority.' % p.__name__)
+ elif priority is None or priority is False:
+ continue
+ descendents.append((priority, p))
+ pane_types = reversed(sorted(descendents, key=lambda x: x[0]))
+ for _, pane_type in pane_types:
+ applies = pane_type.applies(obj)
+ if isinstance(applies, bool) and not applies: continue
+ return pane_type
+ raise TypeError('%s type could not be rendered.' % type(obj).__name__)
diff --git a/panel/pane/holoviews.py b/panel/pane/holoviews.py
index 573bd9a5766..950d4e6a3dd 100644
--- a/panel/pane/holoviews.py
+++ b/panel/pane/holoviews.py
@@ -42,22 +42,27 @@ def __init__(self, object, **params):
self.widget_box = Column()
self._update_widgets()
self._plots = {}
+ self._panes = {}
@param.depends('object', 'widgets', watch=True)
def _update_widgets(self):
if self.object is None:
- widgets, values = []
+ widgets, values = [], []
else:
widgets, values = self.widgets_from_dimensions(self.object, self.widgets,
self.widget_type)
self._values = values
# Clean up anything models listening to the previous widgets
- for _, cbs in self._callbacks.items():
- for cb in list(cbs):
- if cb.inst in self.widget_box.objects:
- cb.inst.param.unwatch(cb)
- cbs.remove(cb)
+ for cb in self._callbacks['instance']:
+ if cb.inst in self.widget_box.objects:
+ cb.inst.param.unwatch(cb)
+ self._callbacks['instance'].remove(cb)
+
+ # Add new widget callbacks
+ for widget in widgets:
+ watcher = widget.param.watch(self._widget_callback, 'value')
+ self._callbacks['instance'].append(watcher)
self.widget_box.objects = widgets
if widgets and not self.widget_box in self.layout.objects:
@@ -65,12 +70,9 @@ def _update_widgets(self):
elif not widgets and self.widget_box in self.layout.objects:
self.layout.pop(self.widget_box)
- @classmethod
- def applies(cls, obj):
- if 'holoviews' not in sys.modules:
- return False
- from holoviews.core.dimension import Dimensioned
- return isinstance(obj, Dimensioned)
+ def _widget_callback(self):
+ for ref, (plot, pane) in self._plots.items():
+ self._update_plot(plot, pane)
def _cleanup(self, root=None, final=False):
"""
@@ -78,9 +80,11 @@ def _cleanup(self, root=None, final=False):
connected to existing plots.
"""
if root is not None:
- old_plot = self._plots.pop(root.ref['id'], None)
+ old_plot, old_pane = self._plots.pop(root.ref['id'], None)
if old_plot:
old_plot.cleanup()
+ if old_pane:
+ old_pane._cleanup(root)
super(HoloViews, self)._cleanup(root, final)
def _render(self, doc, comm, root):
@@ -96,14 +100,28 @@ def _render(self, doc, comm, root):
kwargs = {'doc': doc, 'root': root} if backend == 'bokeh' else {}
if comm:
kwargs['comm'] = comm
- plot = renderer.get_plot(self.object, **kwargs)
- ref = root.ref['id']
- if ref in self._plots:
- old_plot = self._plots[ref]
- old_plot.comm = None
- old_plot.cleanup()
- self._plots[root.ref['id']] = plot
- return plot
+ return renderer.get_plot(self.object, **kwargs)
+
+ def _update_plot(self, plot, pane):
+ from holoviews.core.util import cross_index
+ from holoviews.plotting.bokeh.plot import BokehPlot
+
+ widgets = self.widget_box.objects
+ if self.widget_type == 'scrubber':
+ key = cross_index([v for v in self._values.values()], widgets[0].value)
+ else:
+ key = tuple(w.value for w in widgets)
+
+ if isinstance(plot, BokehPlot):
+ if comm or state.curdoc:
+ plot.update(key)
+ if comm:
+ plot.push()
+ else:
+ plot.document.add_next_tick_callback(partial(plot.update, key))
+ else:
+ plot.update(key)
+ pane.object = plot.state
def _get_model(self, doc, root=None, parent=None, comm=None):
if root is None:
@@ -112,42 +130,23 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
plot = self._render(doc, comm, root)
child_pane = Pane(plot.state, _temporary=True)
model = child_pane._get_model(doc, root, parent, comm)
- self._models[ref] = model
- self._link_object(doc, root, parent, comm)
- if self.widget_box.objects:
- self._link_widgets(child_pane, root, comm)
+ if ref in self._plots:
+ old_plot, old_pane = self._plots[ref]
+ old_plot.cleanup()
+ self._plots[ref] = (plot, child_pane)
+ self._models[ref] = (model, parent)
return model
- def _link_widgets(self, pane, root, comm):
- def update_plot(change):
- from holoviews.core.util import cross_index
- from holoviews.plotting.bokeh.plot import BokehPlot
-
- widgets = self.widget_box.objects
- if self.widget_type == 'scrubber':
- key = cross_index([v for v in self._values.values()], widgets[0].value)
- else:
- key = tuple(w.value for w in widgets)
-
- plot = self._plots[root.ref['id']]
- if isinstance(plot, BokehPlot):
- if comm:
- plot.update(key)
- plot.push()
- elif state.curdoc:
- plot.update(key)
- else:
- def update_plot():
- plot.update(key)
- plot.document.add_next_tick_callback(update_plot)
- else:
- plot.update(key)
- pane.object = plot.state
+ #----------------------------------------------------------------
+ # Public API
+ #----------------------------------------------------------------
- ref = root.ref['id']
- for w in self.widget_box.objects:
- watcher = w.param.watch(update_plot, 'value')
- self._callbacks[ref].append(watcher)
+ @classmethod
+ def applies(cls, obj):
+ if 'holoviews' not in sys.modules:
+ return False
+ from holoviews.core.dimension import Dimensioned
+ return isinstance(obj, Dimensioned)
@classmethod
def widgets_from_dimensions(cls, object, widget_types={}, widgets_type='individual'):
@@ -224,14 +223,14 @@ def is_bokeh_element_plot(plot):
and not isinstance(plot, GenericOverlayPlot))
-def generate_panel_bokeh_map(root_model, panel_views):
+def generate_panel_bokeh_map(root_model, panel_views): # ALERT REVIEW THIS BEFORE MERGE
"""
mapping panel elements to its bokeh models
"""
map_hve_bk = defaultdict(list)
for pane in panel_views:
if root_model.ref['id'] in pane._models:
- bk_plots = pane._plots[root_model.ref['id']].traverse(lambda x: x, [is_bokeh_element_plot])
+ bk_plots = pane._plots[root_model.ref['id']][0].traverse(lambda x: x, [is_bokeh_element_plot])
for plot in bk_plots:
for hv_elem in plot.link_sources:
map_hve_bk[hv_elem].append(plot)
diff --git a/panel/pane/markup.py b/panel/pane/markup.py
index 2ed249aa114..944b6bf363f 100644
--- a/panel/pane/markup.py
+++ b/panel/pane/markup.py
@@ -43,8 +43,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
model = _BkDiv(**self._get_properties())
if root is None:
root = model
- self._models[root.ref['id']] = model
- self._link_object(doc, root, parent, comm)
+ self._models[root.ref['id']] = (model, parent)
return model
def _update(self, model):
diff --git a/panel/pane/plot.py b/panel/pane/plot.py
index e9ab9106234..d40032e4bce 100644
--- a/panel/pane/plot.py
+++ b/panel/pane/plot.py
@@ -40,8 +40,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
if model._document and doc is not model._document:
remove_root(model, doc)
- self._models[ref] = model
- self._link_object(doc, root, parent, comm)
+ self._models[ref] = (model, parent)
return model
diff --git a/panel/pane/plotly.py b/panel/pane/plotly.py
index 3ba19bc2f5f..151a19b3a79 100644
--- a/panel/pane/plotly.py
+++ b/panel/pane/plotly.py
@@ -63,8 +63,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
model = PlotlyPlot(data=json, data_sources=sources)
if root is None:
root = model
- self._models[root.ref['id']] = model
- self._link_object(doc, root, parent, comm)
+ self._models[root.ref['id']] = (model, parent)
return model
def _update(self, model):
diff --git a/panel/pane/vega.py b/panel/pane/vega.py
index 199c8b86267..fcb49e611fc 100644
--- a/panel/pane/vega.py
+++ b/panel/pane/vega.py
@@ -88,8 +88,7 @@ def _get_model(self, doc, root=None, parent=None, comm=None):
model = VegaPlot(data=json, data_sources=sources)
if root is None:
root = model
- self._models[root.ref['id']] = model
- self._link_object(doc, root, parent, comm)
+ self._models[root.ref['id']] = (model, parent)
return model
def _update(self, model):
diff --git a/panel/param.py b/panel/param.py
index 6cb2cd14ec6..2fa5adbd821 100644
--- a/panel/param.py
+++ b/panel/param.py
@@ -389,7 +389,9 @@ def _get_widgets(self):
def _get_root(self, doc, comm=None):
root = self.layout._get_root(doc, comm)
- self._models[root.ref['id']] = root
+ ref = root.ref['id']
+ self._models[ref] = root
+ state._views[ref] = (self, model, doc, comm)
return root
def _get_model(self, doc, root=None, parent=None, comm=None):
diff --git a/panel/tests/test_holoviews.py b/panel/tests/test_holoviews.py
index 56c1d7840c3..654088e8749 100644
--- a/panel/tests/test_holoviews.py
+++ b/panel/tests/test_holoviews.py
@@ -42,7 +42,7 @@ def test_holoviews_pane_mpl_renderer(document, comm):
assert len(row.children) == 1
assert len(pane._callbacks) == 1
model = row.children[0]
- assert pane._models[row.ref['id']] is model
+ assert pane._models[row.ref['id']][0] is model
div = get_div(model)
assert 'ABC
Markdown
" # Cleanup pane._cleanup(model) - assert pane._callbacks == {} assert pane._models == {} @@ -133,19 +123,16 @@ def test_html_pane(document, comm): # Create pane model = pane._get_root(document, comm=comm) - assert model.ref['id'] in pane._callbacks - assert pane._models[model.ref['id']] is model + assert pane._models[model.ref['id']][0] is model assert model.text == "<h2>Test</h2>" # Cleanup pane._cleanup(model) - assert pane._callbacks == {} assert pane._models == {} @@ -198,8 +181,7 @@ def test_svg_pane(document, comm): # Create pane model = pane._get_root(document, comm=comm) - assert model.ref['id'] in pane._callbacks - assert pane._models[model.ref['id']] is model + assert pane._models[model.ref['id']][0] is model assert model.text.startswith('