How to use NGL WebGL protein viewer in Panel?

THIS QUESTION AND ANSWER IS COPIED FROM https://github.com/holoviz/panel/issues/1307.

I’d like to incorporate a protein viewer in my panel application. To do so I’m looking at ngl which has both an ipywidget for notebooks and JS/HTML available for embedding.

How do I do this?

This code will work

import panel as pn
pn.config.js_files["ngl"]="https://cdn.jsdelivr.net/gh/arose/ngl@v2.0.0-dev.33/dist/ngl.js"
pn.extension()

html = """<div id="viewport" style="width:100%; height:100%;"></div>
<script>
stage = new ngl.Stage("viewport");
stage.loadFile("rcsb://1NKT.mmtf", {defaultRepresentation: true});
</script>"""


ngl_pane = pn.pane.HTML(html, height=500, width=500)
ngl_pane

1 Like

I was playing around with this and turned it into a ‘component’ for panel following @Marc’s guide on subclassing the HTML pane.

The code is below, should I contribute it to awesome-panel?
It is a bit hacky, for example I’m putting the contents of user-specified pdb files as a raw text in the HTML. But it works well and I’m quite happy with it.

Is there already a way of getting variables from the html to python? The stage object has some variables I’d like to access from python.
Also, at the moment when the user turns on/off the ‘spin’ option, the whole thing re-renders, which shouldn’t be necessary.

import panel as pn
import param

pn.config.js_files["ngl"]="https://unpkg.com/ngl@2.0.0-dev.37/dist/ngl.js"
pn.extension()


class NGLViewer(pn.pane.HTML):
    pdb_string = param.String()
    rcsb_id = param.String()
    representation = param.Selector(default='ribbon',
                                    objects=['ball+stick', 'backbone', 'ball+stick', 'cartoon', 'hyperball', 'licorice',
                                             'ribbon', 'rope', 'spacefill', 'surface'])
    spin = param.Boolean(default=False)
    priority = 0
    _rename = dict(pn.pane.HTML._rename, pdb_string=None, rcsb_id=None, representation=None, spin=None)

    def __init__(self, **params):
        super().__init__(**params)
        self.load_string = \
        f"""
        stage = new NGL.Stage("viewport");
        stage.loadFile()"""
        self._update_object_from_parameters()

    @param.depends('representation', 'spin', watch=True)
    def _update_object_from_parameters(self):
        html =\
            f"""
            <div id="viewport" style="width:100%; height:100%;"></div>
            <script>
            {self.load_string}.then(function(o){{
                o.addRepresentation("{self.representation}");
                o.autoView();
                }}
            );
            stage.setSpin({'true' if self.spin else 'false'});
            </script>
            """
        self.object = html

    @param.depends('pdb_string', watch=True)
    def _update_object_from_pdb_string(self):
        self.load_string = \
            f"""
            var PDBString = `{self.pdb_string}`;
            stage = new NGL.Stage("viewport");
            stage.loadFile( new Blob([PDBString], {{type: 'text/plain'}}), {{ ext:'pdb'}} )"""
        self._update_object_from_parameters()

    @param.depends('rcsb_id', watch=True)
    def _update_object_from_rcsb_id(self):
        self.load_string = \
            f"""
            stage = new NGL.Stage("viewport");
            stage.loadFile("rcsb://{self.rcsb_id}")"""
        self._update_object_from_parameters()


class ProteinViewer(param.Parameterized):

    input_option = param.Selector(objects=['Upload File', 'RCSB PDB'])
    rcsb_id = param.String()
    load_structure = param.Action(lambda self: self._load_structure())

    def __init__(self, **param):
        super(ProteinViewer, self).__init__(**param)
        self.file_widget = pn.widgets.FileInput(accept='.pdb')
        self.ngl_html = NGLViewer(height=500, width=500)

    def _load_structure(self):
        if self.input_option == 'Upload File':
            if self.file_widget.value:
                string = self.file_widget.value.decode()
                self.ngl_html.pdb_string = string
            else:
                pass

        elif self.input_option == 'RCSB PDB':
            self.ngl_html.rcsb_id = self.rcsb_id

    def view(self):
        col = pn.Column(*pn.Param(self.param))
        col.insert(2, self.file_widget)
        col.append(self.ngl_html.param.representation)
        col.append(self.ngl_html.param.spin)
        app = pn.Row(
            col,
            self.ngl_html
        )

        return app


pv = ProteinViewer()

pn.serve(pv.view())

1 Like

I would be happy to review, maybe improve it, (temporarily?) host it in awesome-panel-extensions package.

I believe i might convert it to a bokeh extension, if it there are events we would like to Catch and data we would like to transfer back to python?

But i think @philippjfr should guide whether he would already now like to take it into Panel.