Hi everyone, I just made my first ReactiveHTML which I’m very excited about and I wanted to share.
Its the NGL molecular viewer, which I was using before but as a custom bokeh widget.
(this can be found in the panel-chemistry repo)
As I was struggling to get custom bokeh extensions to built correctly for distribution in PyPi, the ReactiveHTML is far easier for me to use, as well as its easier to extend/update as I can just modify the python code.
I’ve added some additional shenanigans on top here just to play around with some nice colors:
(only works if you upload a .pdb file and requires BioPython/PyHDX 0.4.0b4)
Code:
from itertools import groupby, count
import param
from panel.reactive import ReactiveHTML
import panel as pn
import pandas as pd
import numpy as np
import proplot as pplt
from pyhdx.support import apply_cmap
from Bio.PDB import PDBParser
# monkey patch PBDParser
def get_structure(self, id, lines):
"""Return the structure.
Arguments:
- id - string, the id that will be used for the structure
- lines - lines of the PDB file
"""
self.header = None
self.trailer = None
# Make a StructureBuilder instance (pass id of structure as parameter)
self.structure_builder.init_structure(id)
self._parse(lines)
self.structure_builder.set_header(self.header)
# Return the Structure instance
structure = self.structure_builder.get_structure()
return structure
PDBParser.get_structure = get_structure
parser = PDBParser()
REPRESENTATIONS = [
# "base",
# "distance",
"axes",
"backbone",
"ball+stick",
"cartoon",
"helixorient",
"hyperball",
"label",
"licorice",
"line",
"point",
"ribbon",
"rocket",
"rope",
"spacefill",
"surface",
"trace",
"unitcell",
# "validation",
]
COLOR_SCHEMES = [
"atomindex",
"bfactor",
"chainid",
"chainindex",
"chainname",
"custom",
"densityfit",
"electrostatic",
"element",
"entityindex",
"entitytype",
"geoquality",
"hydrophobicity",
"modelindex",
"moleculetype",
"occupancy",
"random",
"residueindex",
"resname",
"sstruc",
"uniform",
"value",
"volume",
]
EXTENSIONS = [
"", # no extension is allowed?
"pdb",
"cif",
"csv",
"ent",
"gro",
"json",
"mcif",
"mmcif",
"mmtf",
"mol2",
"msgpack",
"netcdf",
"parm7",
"pqr",
"prmtop",
"psf",
"sd",
"sdf",
"top",
"txt",
"xml",
]
class NGL(ReactiveHTML):
object = param.String()
extension = param.Selector(
default="pdb",
objects=EXTENSIONS,
)
representation = param.Selector(
default="ball+stick",
objects=REPRESENTATIONS,
doc="""
A display representation. Default is 'ball+stick'. See
http://nglviewer.org/ngl/api/manual/coloring.html#representations
""",
)
selection = param.String(
default="not ( water or ion )",
doc="""
Selection string to apply representations to. Default is to exclude water and ions. See
http://nglviewer.org/ngl/api/manual/selection-language.html
"""
)
color_scheme = param.String('chainid')
custom_color_scheme = param.List(
default=[["white", "*"]],
doc="""
A custom color scheme. See
http://nglviewer.org/ngl/api/manual/coloring.html#custom-coloring.""",
)
spin = param.Boolean(False)
_template = """
<div id="ngl_stage" style="width:100%; height:800px;"></div>
"""
_scripts = {
'render': """
var stage = new NGL.Stage(ngl_stage)
state._stage = stage
stage.handleResize();
""",
'object': """
self.updateStage()
""",
'color_scheme': """
self.setParameters()
""",
'custom_color_scheme': """
self.setParameters()
""",
'setParameters': """
if (state._stage.compList.length !== 0) {
const parameters = self.getParameters();
state._stage.compList[0].reprList[0].setParameters( parameters );
}
self.after_layout
""",
'getParameters': """
if (data.color_scheme==="custom"){
var scheme = NGL.ColormakerRegistry.addSelectionScheme( data.custom_color_scheme, "new scheme")
var parameters = {color: scheme}
}
else {
var parameters = {colorScheme: data.color_scheme}
}
parameters["sele"] = data.selection
return parameters
""",
'representation': """
const parameters = self.getParameters();
const component = state._stage.compList[0];
component.removeAllRepresentations();
component.addRepresentation(data.representation, parameters);
""",
'spin': """
state._stage.setSpin(data.spin);
""",
'updateStage': """
parameters = self.getParameters();
state._stage.removeAllComponents()
state._stage.loadFile(new Blob([data.object], {type: 'text/plain'}), { ext: data.extension}).then(function (component) {
component.addRepresentation(data.representation, parameters);
component.autoView();
});
""",
'after_layout':"""
state._stage.handleResize();
"""
}
__javascript__ = [
"https://unpkg.com/ngl@2.0.0-dev.38/dist/ngl.js",
]
class NGLControl(param.Parameterized):
ngl = param.ClassSelector(NGL, precedence=-1)
color_scheme = param.Selector(
default="chainid",
objects=COLOR_SCHEMES,
doc="""
A predefined or 'custom' color scheme. If 'custom' is specified you need to specify the
'custom_color_scheme' parameter. Default is 'element'. See
http://nglviewer.org/ngl/api/manual/coloring.html""",
)
custom_color_scheme = param.List(precedence=-1)
representation = param.Selector(
default="ball+stick",
objects=REPRESENTATIONS,
doc="""
A display representation. Default is 'ball+stick'. See
http://nglviewer.org/ngl/api/manual/coloring.html#representations
""",
)
phase = param.Number(0., bounds=(0., 2*np.pi))
wavelength = param.Number(np.pi, bounds=(0.1*np.pi, 2.*np.pi))
spin = param.Boolean(False)
def __init__(self, **params):
super().__init__(**params)
params = self.param.params().keys() & self.ngl.param.params().keys() - {'name', 'phase', 'frequency'}
self.param.watch(self._update_params, list(params))
# file_input = param.Bytes() -> pn.Param(widgets={'file_input': pn.widgets.FileInput)
self.file_input = pn.widgets.FileInput(accept=','.join('.' + s for s in EXTENSIONS[1:]))
self.file_input.param.watch(self._file_input_updated, ['value', 'filename'])
self.c_term = 2
def _file_input_updated(self, *events):
for event in events: # Is this always length 1?
name = self.file_input.filename.split('.')[0]
ext = self.file_input.filename.split('.')[-1]
# Set extension first as it does not trigger update of the ReactiveHTML
self.extension = ext
object = self.file_input.value.decode('utf-8')
self.ngl.object = object
if ext == 'pdb':
lines = object.split('\n')
structure = parser.get_structure(name, lines)
self.c_term = max(r.get_id()[1] for r in structure.get_residues() if r.resname not in ['HOH'])
self._sin_updated()
def _update_params(self, *events):
for event in events:
setattr(self.ngl, event.name, event.new)
@param.depends('phase', 'wavelength', watch=True)
def _sin_updated(self):
t = np.linspace(0, 2 * np.pi, num=self.c_term, endpoint=True)
f = 1./self.wavelength
s = pd.Series(np.sin(2*np.pi*f*t + self.phase))
colors = apply_cmap(s, cmap, norm)
colors.index += 1
self.set_color_scheme(colors)
def set_color_scheme(self, colors):
"""Set the colors of protein residues
Parameters
----------
colors : :class:`pd.Series`
Panda series with hexadecimal colors. Index should match residue number
"""
grp = colors.groupby(colors)
color_list = []
for c, pd_series in grp:
result = [list(g) for _, g in groupby(pd_series.index, key=lambda n, c=count(): n - next(c))]
resi = ' or '.join([f'{g[0]}-{g[-1]}' for g in result])
color_list.append([c, resi])
self.custom_color_scheme = color_list
# proplot / pyhdx functions
cmap = pplt.Colormap('turbo')
norm = pplt.Norm('linear', -1, 1.)
ngl = NGL(sizing_mode='stretch_both')
ngl_control = NGLControl(ngl=ngl)
controls = pn.Column(ngl_control.file_input, *pn.Param(ngl_control))
ngl_pane = pn.Pane(ngl, sizing_mode='stretch_both')
app = pn.Row(ngl_pane, controls)
app.servable()