K3D-tools / K3D-jupyter

K3D lets you create 3D plots backed by WebGL with high-level API (surfaces, isosurfaces, voxels, mesh, cloud points, vtk objects, volume renderer, colormaps, etc). The primary aim of K3D-jupyter is to be easy for use as stand alone package like matplotlib, but also to allow interoperation with existing libraries as VTK.
MIT License
917 stars 123 forks source link

Saving an html file of an holoviz panel application containing k3d #387

Closed Davide-sd closed 1 year ago

Davide-sd commented 1 year ago

Description

I'm trying to generate an html file of an interactive application that uses holoviz panel for widgets and K3D for visualization. However, I'm encountering a problem.

What I Did

import numpy as np
import panel as pn
pn.extension()
import k3d

f = lambda r, d: 5 * np.cos(r) * np.exp(-r * d)
x, y = np.mgrid[-7:7:100j, -7:7:100j]
r = np.sqrt(x**2 + y**2)
z = f(r, 0.1)

fig = k3d.plot()
surface = k3d.surface(z, bounds=[-7, 7, -7, 7])
fig += surface

slider = pn.widgets.FloatSlider(start=0, end=1, value=0.1)
@pn.depends(slider)
def update(d):
    surface.heights = f(r, d)
    return pn.pane.Pane(fig, width=800)
application = pn.Column(slider, update)
application

The command pn.pane.Pane(fig, width=800) is going to create a pn.pane.IPyWidget object, wrapping the actual plot: I believe that it does that because panel sees a K3D plot as an ipywidget. The wrapping pane is necessary to render the plot on the application.

Screenshot 2022-11-20 at 22-05-50 Untitled23 - Jupyter Notebook

So far so good. Now, let say I want to save an html file of application:

application.save("test-k3d-app.html")
...
File ~/Documents/Development/envs/plot/lib/python3.10/site-packages/panel/pane/ipywidget.py:60, in IPyWidget._get_model(self, doc, root, parent, comm)
     58     return self.get_root(doc, comm)
     59 kwargs = self._process_param_change(self._init_params())
---> 60 model = self._get_ipywidget(self.object, doc, root, comm, **kwargs)
     61 self._models[root.ref['id']] = (model, parent)
     62 return model

File ~/Documents/Development/envs/plot/lib/python3.10/site-packages/panel/pane/ipywidget.py:49, in IPyWidget._get_ipywidget(self, obj, doc, root, comm, **kwargs)
     47 if not isinstance(ipykernel.kernelbase.Kernel._instance, PanelKernel):
     48     kernel = PanelKernel(document=doc, key=str(id(doc)).encode('utf-8'))
---> 49     for w in obj.widgets.values():
     50         w.comm.kernel = kernel
     51         w.comm.open()

AttributeError: 'Plot' object has no attribute 'widgets'

I believe that pn.pane.IPyWidget doesn't fully understand what a k3d plot actually is. Just for debugging purposes, I tried:

fig.widgets = dict()
application.save("test-k3d-app.html")

Which saves the actual application, but without data for k3d plot. Here is a screenshot.

Screenshot 2022-11-20 at 22-17-16 Panel

At this point I probably have to create a new pn.pane that better understands K3D plots. However, how can I retrieve the actual data? Alternatively, is there any way I can extend a K3D plot to inform pn.pane.IPyWidget that there is data to be saved?

artur-trzesiok commented 1 year ago

Hi! I try to reproduce your results. On 2.15.0 I didn't get error AttributeError: 'Plot' object has no attribute 'widgets' image

I got html fil without "hack" fig.widgets = dict() but when I'm opening a file I cannot see a blue chart there.

artur-trzesiok commented 1 year ago

Hi @Davide-sd

That's not a final solution but I somehow figure out what is happening here.

In K3D every object is in fact a widget in ipywidgets. The same with k3d.plot. When you add object to plot you add in fact only a reference. That is resonable assumption - in k3d you can add the same object to two plots and you can expect that line:

obj.color = 0x00ff00

will change color to green in two plots at the same time, right?

So in your example holoviz panel is not aware about that and treat k3d as single widget. Because you are passing a fig to it (it is instance of k3d.plot) you save state only of plot.

How to "solve" this? By adding:

application.objects.extend([surface])

image

you will inform your appplication that you will have more than plot. And thats works. Output test-k3d-app.html contain object data and size of file increased. But application is trying to plot second widget too :/ and it showing a error on bottom: image

So it is not perfect solution for sure. From technical js side:

class ObjectModel extends widgets.WidgetModel class PlotModel extends widgets.DOMWidgetModel

So if holoviz somehow recognize that not all widgets are "DOMWidgetModel" maybe can ommit them to display directly?