holoviz / holoviews

With Holoviews, your data visualizes itself.
https://holoviews.org
BSD 3-Clause "New" or "Revised" License
2.69k stars 402 forks source link

Tabs cloning breaks app when containing plots with custom ticks #4995

Open rafgonsi opened 3 years ago

rafgonsi commented 3 years ago

ALL software version info

Python 3.7.6, holoviews==1.14.3, panel==0.11.3, bokeh==2.3.1 (also tested on Python 3.7.6, panel==0.10.3, holoviews==1.14.0, bokeh==2.2.3, with the same result)

Description of expected behavior and the observed behavior

In my application I have tabs which are dynamically populated. The tabs must be dynamic, because of performance issues. I also need a way to save tabs with all content. To achieve this I need to clone them with overwriting dynamic parameter to False.

The error is caused by the fact that one of the plots has a custom ticks (see the MRE below). This error is raised in a particular situation:

  1. I switch tab to plot that does not have custom ticks.
  2. I try to save plots (click button). The html is correctly generated.
  3. When I try to come back to the plot with custom ticks, the error is raised and the plot is not displayed.

Note that the error is not raised when the active tab contains the plot with custom ticks.

Complete, minimal, self-contained example code that reproduces the issue

import panel as pn
import holoviews as hv
import numpy as np
from bokeh.models import NumeralTickFormatter
hv.extension("bokeh")

def make_img(**opts):
    ls = np.linspace(0, 10, 200)
    xx, yy = np.meshgrid(ls, ls)
    bounds=(-1,-1,1,1)   # Coordinate system: (left, bottom, right, top)
    return hv.Image(np.sin(xx)*np.cos(yy), bounds=bounds).opts(**opts)

img1 = make_img(xformatter=NumeralTickFormatter())
img2 = make_img(cmap="jet")
tabs = pn.Tabs(img1, img2, dynamic=True)

def save_tabs(event):
    tabs.clone(dynamic=False).save("tmp.html", embed=True, resources="INLINE")

save_button = pn.widgets.Button(name="Save tabs")
save_button.on_click(save_tabs)

pn.Row(tabs, save_button)

Stack traceback and/or browser JavaScript console output

Traceback (most recent call last):
  File "/opt/conda/lib/python3.7/site-packages/pyviz_comms/__init__.py", line 325, in _handle_msg
    self._on_msg(msg)
  File "/opt/conda/lib/python3.7/site-packages/panel/viewable.py", line 259, in _on_msg
    doc.unhold()
  File "/opt/conda/lib/python3.7/site-packages/bokeh/document/document.py", line 668, in unhold
    self._trigger_on_change(event)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/document/document.py", line 1151, in _trigger_on_change
    self._with_self_as_curdoc(event.callback_invoker)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/document/document.py", line 1169, in _with_self_as_curdoc
    return f()
  File "/opt/conda/lib/python3.7/site-packages/bokeh/util/callback_manager.py", line 155, in invoke
    callback(attr, old, new)
  File "/opt/conda/lib/python3.7/site-packages/panel/layout/tabs.py", line 92, in _comm_change
    super(Tabs, self)._comm_change(doc, ref, comm, attr, old, new)
  File "/opt/conda/lib/python3.7/site-packages/panel/reactive.py", line 216, in _comm_change
    self._process_events({attr: new})
  File "/opt/conda/lib/python3.7/site-packages/panel/reactive.py", line 187, in _process_events
    self.param.set_param(**self._process_property_change(events))
  File "/opt/conda/lib/python3.7/site-packages/param/parameterized.py", line 1472, in set_param
    self_._batch_call_watchers()
  File "/opt/conda/lib/python3.7/site-packages/param/parameterized.py", line 1611, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "/opt/conda/lib/python3.7/site-packages/param/parameterized.py", line 1573, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "/opt/conda/lib/python3.7/site-packages/panel/layout/tabs.py", line 107, in _update_active
    self.param.trigger('objects')
  File "/opt/conda/lib/python3.7/site-packages/param/parameterized.py", line 1541, in trigger
    self_.set_param(**dict(params, **triggers))
  File "/opt/conda/lib/python3.7/site-packages/param/parameterized.py", line 1472, in set_param
    self_._batch_call_watchers()
  File "/opt/conda/lib/python3.7/site-packages/param/parameterized.py", line 1611, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "/opt/conda/lib/python3.7/site-packages/param/parameterized.py", line 1573, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "/opt/conda/lib/python3.7/site-packages/panel/reactive.py", line 175, in _param_change
    self._update_model(events, msg, root, model, doc, comm)
  File "/opt/conda/lib/python3.7/site-packages/panel/layout/tabs.py", line 120, in _update_model
    super(Tabs, self)._update_model(events, msg, root, model, doc, comm)
  File "/opt/conda/lib/python3.7/site-packages/panel/layout/base.py", line 65, in _update_model
    super(Panel, self)._update_model(events, msg, root, model, doc, comm)
  File "/opt/conda/lib/python3.7/site-packages/panel/reactive.py", line 137, in _update_model
    model.update(**msg)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/core/has_props.py", line 374, in update
    setattr(self, k, v)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/core/has_props.py", line 278, in __setattr__
    super().__setattr__(name, value)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 539, in __set__
    self._internal_set(obj, value, setter=setter)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 763, in _internal_set
    self._real_set(obj, old, value, hint=hint, setter=setter)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 832, in _real_set
    self._trigger(obj, old, value, hint=hint, setter=setter)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/core/property/descriptors.py", line 909, in _trigger
    obj.trigger(self.name, old, value, hint, setter)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/model.py", line 662, in trigger
    self._document._invalidate_all_models()
  File "/opt/conda/lib/python3.7/site-packages/bokeh/document/document.py", line 1031, in _invalidate_all_models
    self._recompute_all_models()
  File "/opt/conda/lib/python3.7/site-packages/bokeh/document/document.py", line 1098, in _recompute_all_models
    a._attach_document(self)
  File "/opt/conda/lib/python3.7/site-packages/bokeh/model.py", line 674, in _attach_document
    raise RuntimeError("Models must be owned by only a single document, %r is already in a doc" % (self))
RuntimeError: Models must be owned by only a single document, NumeralTickFormatter(id='3112', ...) is already in a doc

Screenshots or screencasts of the bug in action

a686cd63b7b7d1deb03206893259d9ef6f0de166

Possible workaround

A workaround that works when the plot with custom ticks is in only one tab:

  1. When a button is clicked, it first changes the active tab to the one with the plot with custom ticks.
  2. Then, html is saved.
  3. The tab is switched back to the initial tab. Note that this would not work if there are plots with custom ticks in at least two tabs.
philippjfr commented 3 years ago

Yeah this is a tough one. Bokeh absolutely does not like reusing models. The only thing I can think of is for HoloViews to create a new copy of each bokeh model it is given (and for Panel to do the same everywhere it accepts raw models).

vlvalenti commented 2 years ago

Has there been any other considerations or work arounds to this problem? I am experiencing the exact same problem with an app I've designed for visualizing climate model data with Holoviews, Panel and Bokeh. It creates a couple filled contour plots that require Bokeh Fixed Tickers for each contour level and arranges the plots in a panel Tabs. I can successfully save the plots when clicked but then attempting to change anything or update them leads to this error.