Repository: holoviz/holoviews

Issue:
Empty output in Colab
#### ALL software version info
Google Colab

```
sys.version_info:	sys.version_info(major=3, minor=10, micro=12, releaselevel='final', serial=0)
ipywidgets.__version__:	8.1.1
bokeh.__version__:	3.3.2
jupyter_bokeh.__version__:	3.0.7
holoviews.__version__:	1.17.1
```
   
#### Description of expected behavior and the observed behavior
I'm trying to run the demo for the `BoxEdit` module, but when I run the demo nothing is shown


#### Complete, minimal, self-contained example code that reproduces the issue

```
!pip install holoviews --quiet
!pip install jupyter_bokeh --quiet

import holoviews
holoviews.extension('bokeh', logo=False)
from bokeh.plotting import output_notebook
output_notebook(hide_banner=True)

from google.colab import output
output.enable_custom_widget_manager()
```

This works:
```
%output size = 200
holoviews.extension('bokeh')

boxes = holoviews.Rectangles(
    [(0, 0, 1, 1), (2, 3, 4, 6), (0.5, 2, 1.5, 4), (2, 1, 3.5, 2.5)])
boxes.opts(
    holoviews.opts.Rectangles(
        fill_alpha=0.5,
        height=400,
        width=400))
```

Nothing is shown for this:
```
%output size = 200
holoviews.extension('bokeh')

boxes = holoviews.Rectangles(
    [(0, 0, 1, 1), (2, 3, 4, 6), (0.5, 2, 1.5, 4), (2, 1, 3.5, 2.5)])
box_stream = holoviews.streams.BoxEdit(source=boxes)
boxes.opts(
    holoviews.opts.Rectangles(
        fill_alpha=0.5,
        height=400,
        width=400))
```



#### Stack traceback and/or browser JavaScript console output
Looking in the Chrome develop tools console

```
Error has occurred while trying to update output.     output_binary.js:173
message: 'Widget @bokeh/jupyter_bokeh BokehModel is not supported',
Stack: CustomError: Widget @bokeh/jupyter_bokeh BokehModel is not supported
    at new Bk (https://ssl.gstatic.com/colaboratory-static/common/b124d179c25e30ffc6a1ff293e85eba8/output_binary.js:168:471)
    at wa.eval [as h] (https://ssl.gstatic.com/colaboratory-static/common/b124d179c25e30ffc6a1ff293e85eba8/output_binary.js:166:142)
    at za (https://ssl.gstatic.com/colaboratory-static/common/b124d179c25e30ffc6a1ff293e85eba8/output_binary.js:11:141)
    at Ba.next (https://ssl.gstatic.com/colaboratory-static/common/b124d179c25e30ffc6a1ff293e85eba8/output_binary.js:11:440)
    at eval (https://ssl.gstatic.com/colaboratory-static/common/b124d179c25e30ffc6a1ff293e85eba8/output_binary.js:12:319)
    at new Promise (<anonymous>)
    at Ca (https://ssl.gstatic.com/colaboratory-static/common/b124d179c25e30ffc6a1ff293e85eba8/output_binary.js:12:212)
    at y (https://ssl.gstatic.com/colaboratory-static/common/b124d179c25e30ffc6a1ff293e85eba8/output_binary.js:12:351)
    at yk (https://ssl.gstatic.com/colaboratory-static/common/b124d179c25e30ffc6a1ff293e85eba8/output_binary.js:166:51)
    at new zk (https://ssl.gstatic.com/colaboratory-static/common/b124d179c25e30ffc6a1ff293e85eba8/output_binary.js:165:1730)
```
#### Screenshots or screencasts of the bug in action
<img width="738" alt="image" src="https://github.com/holoviz/holoviews/assets/129072/36011584-cd4d-4bc4-98ba-a4360093e025">

### Other things to note
This code was working as is on google colab in August 2023, but is not now working in December 2023

- Note that running the code brings up snippets suggesting adding the code:

```
from google.colab import output
output.enable_custom_widget_manager()
```

However this has no effect

- Using the latest developer version of holoviews from github had no effect 


### Other things I've tried
I found this [issue](https://github.com/googlecolab/colabtools/issues/3020) on colabtools github, **_Default ipywidgets don't show up in notebooks_**

This issue suggests that "Downgrading ipywidgets to 7.7.1" may help

I tried 
```
!pip install ipywidgets==7.7.1
```

The current version of `jupyter-bokeh` is `3.0.7` and requires `ipywidgets==8.*`
The most current version of `jupyter-bokeh` that supports `ipywidgets==7.7.1` is version `3.0.2`. 
```
!pip install jupyter_bokeh==3.0.2
```
However in going from `3.0.2` => `3.0.7` there was a bug fixed to be compatible with the current version of bokeh `3.3.2`, namely
```
standalone_docs_json_and_render_items() takes 1 positional argument but 2 were given
```

This fix can be monkey patched back into `jupyter-bokeh` version `3.0.2`, so all together the code looks like this:


```
!pip install ipywidgets==7.7.1
!pip install jupyter_bokeh==3.0.2

import bokeh
import bokeh.model
import jupyter_bokeh
import jupyter_bokeh.widgets

# use functools.wraps to keep the @classmethod decorator 
import functools
@functools.wraps(jupyter_bokeh.widgets.BokehModel._model_to_traits)
def _model_to_traits(cls, model):
    if model.document is None:
        document = bokeh.document.Document()
        document.add_root(model)
    (docs_json, [render_item]) = bokeh.embed.util.standalone_docs_json_and_render_items(
        [model],
        suppress_callback_warning=True)
    render_bundle = dict(
        docs_json=docs_json,
        render_items=[render_item.to_json()],
        div=bokeh.embed.elements.div_for_render_item(render_item),
    )
    return render_bundle

jupyter_bokeh.widgets.BokehModel._model_to_traits = _model_to_traits

!pip install holoviews --quiet

from bokeh.plotting import output_notebook
output_notebook(hide_banner=True)

import holoviews
holoviews.extension('bokeh', logo=False)
```

However downgrading to ipywidgets did not resolve the problem






Discussion:

Patch:
diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py
index 56b348a63..fa05ac8e5 100644
--- a/holoviews/core/dimension.py
+++ b/holoviews/core/dimension.py
@@ -716,37 +716,27 @@ class LabelledData(param.Parameterized):
         "Restores options applied to this object."
         d = param_aliases(d)
 
-        # Backwards compatibility for objects before id was made a property
-        opts_id = d['_id'] if '_id' in d else d.pop('id', None)
-        try:
-            load_options = Store.load_counter_offset is not None
-            if load_options:
-                matches = [k for k in d if k.startswith('_custom_option')]
-                for match in matches:
-                    custom_id = int(match.split('_')[-1])+Store.load_counter_offset
-                    if not isinstance(d[match], dict):
-                        # Backward compatibility before multiple backends
-                        backend_info = {'matplotlib':d[match]}
-                    else:
-                        backend_info = d[match]
-                    for backend, info in  backend_info.items():
-                        if backend not in Store._custom_options:
-                            Store._custom_options[backend] = {}
-                        Store._custom_options[backend][custom_id] = info
-                    if backend_info:
-                        if custom_id not in Store._weakrefs:
-                            Store._weakrefs[custom_id] = []
-                        ref = weakref.ref(self, partial(cleanup_custom_options, custom_id))
-                        Store._weakrefs[opts_id].append(ref)
-                    d.pop(match)
-
-                if opts_id is not None:
-                    opts_id += Store.load_counter_offset
-        except Exception:
-            self.param.warning("Could not unpickle custom style information.")
-        d['_id'] = opts_id
+        load_options = Store.load_counter_offset is not None
+        if load_options:
+            matches = [k for k in d if k.startswith('_custom_option')]
+            for match in matches:
+                custom_id = int(match.split('_')[-1])+Store.load_counter_offset
+                for backend, info in d[match].items():
+                    if backend not in Store._custom_options:
+                        Store._custom_options[backend] = {}
+                    Store._custom_options[backend][custom_id] = info
+                if d[match]:
+                    if custom_id not in Store._weakrefs:
+                        Store._weakrefs[custom_id] = []
+                    ref = weakref.ref(self, partial(cleanup_custom_options, custom_id))
+                    Store._weakrefs[d["_id"]].append(ref)
+                d.pop(match)
+
+            if d["_id"] is not None:
+                d["_id"] += Store.load_counter_offset
+
         self.__dict__.update(d)
-        super().__setstate__({})
+        super().__setstate__(d)
 
 
 class Dimensioned(LabelledData):
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
index 7d8fcb97f..361de8193 100644
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -389,8 +389,9 @@ class Renderer(Exporter):
                 src is source or (src._plot_id is not None and src._plot_id == source._plot_id)
                 for src, streams in registry for s in streams
             )
+        if config.comms == 'colab':
+            load_notebook(config.inline)
         embed = (not (dynamic or streams or self.widget_mode == 'live') or config.embed)
-
         if embed or config.comms == 'default':
             return self._render_panel(plot, embed, comm)
         return self._render_ipywidget(plot)


Test patch:
diff --git a/holoviews/tests/core/test_dimensioned.py b/holoviews/tests/core/test_dimensioned.py
index f65060dea..1dc3dc7a4 100644
--- a/holoviews/tests/core/test_dimensioned.py
+++ b/holoviews/tests/core/test_dimensioned.py
@@ -1,8 +1,10 @@
 import gc
+import pickle
 
 from holoviews.core.element import Element
 from holoviews.core.options import Keywords, Options, OptionTree, Store
 from holoviews.core.spaces import HoloMap
+from holoviews.element.chart import Curve
 
 from ..utils import LoggingComparisonTestCase
 
@@ -247,3 +249,11 @@ class TestOptionsCleanup(CustomBackendTestCase):
         ExampleElement([]).opts(style_opt1='A').opts.clear()
         custom_options = Store._custom_options['backend_1']
         self.assertEqual(len(custom_options), 0)
+
+class TestGetSetState:
+
+    def test_pickle_roundtrip(self):
+        curve = Curve([0, 1, 2], kdims=["XAXIS"])
+        roundtrip_curve = pickle.loads(pickle.dumps(curve))
+        assert curve.kdims == roundtrip_curve.kdims
+        assert curve.vdims == roundtrip_curve.vdims