awesome-panel / panel-chemistry

🧪📈 🐍. The purpose of the panel-chemistry project is to make it really easy for you to do DATA ANALYSIS and build powerful DATA AND VIZ APPLICATIONS within the domain of Chemistry using using Python and HoloViz Panel.
MIT License
118 stars 16 forks source link

Add pdbe molstar viewer #19

Closed Jhsmit closed 2 years ago

Jhsmit commented 2 years ago

Current status of the PR adds the PBDe webcomponent implementation of the molstar viewer

The webcomponent works well although there are some bugs/confusion around setting of some boolean parameters: https://github.com/PDBeurope/pdbe-molstar/issues/44

pdbe-molstar-webcomponent

I'm planning to further add the PDBe JS plugin as ReactiveHTML and perhaps also the Mol* viewer

MarcSkovMadsen commented 2 years ago

Hi @Jhsmit

Thx. This looks amazing. It seems the molstar_webcomponent.html file is not included in the PR. Could you please add?

image

MarcSkovMadsen commented 2 years ago

My main comments are

MarcSkovMadsen commented 2 years ago

I took a look. And I would suggest converting it to a ReactiveHTML implementation @Jhsmit . Then you would also be able to get events. Etc.

A start is below. It will probably be a bit rough as the web component does not seem that mature/ dynamic. An alternative is using the JS component.

"""A Panel Pane to wrap the PDBe implementation of the Mol* ('MolStar') viewer.

    The PBBe viewer is available both as a webcomponent or interactive plugin.

    Check out

    - [PDBe Mol*](https://github.com/PDBeurope/pdbe-molstar)
    - [Mol*](https://molstar.org/)
    - [Mol* GitHub](https://github.com/molstar/molstar)

    Cite Mol*:
    David Sehnal, Sebastian Bittrich, Mandar Deshpande, Radka Svobodová, Karel Berka,
    Václav Bazgier, Sameer Velankar, Stephen K Burley, Jaroslav Koča, Alexander S Rose:
    Mol* Viewer: modern web app for 3D visualization and analysis of large biomolecular structures,
    Nucleic Acids Research, 2021; https://doi.org/10.1093/nar/gkab314.

    """

import param
from panel.reactive import ReactiveHTML
from panel.pane import HTML
import panel as pn
from pathlib import Path
from string import Template

REPRESENTATIONS = [
    "cartoon",
    "ball-and-stick",
    "carbohydrate",
    "distance-restraint",
    "ellipsoid",
    "gaussian-surface",
    "molecular-surface",
    "point",
    "putty",
    "spacefill",
]

# See https://embed.plnkr.co/plunk/m3GxFYx9cBjIanBp for an example JS implementation
class PdbeMolStarWebComponent(ReactiveHTML):
    """PDBe MolStar structure viewer.

    For more information:

    - https://github.com/PDBeurope/pdbe-molstar/wiki
    - https://molstar.org/

    The implementation is based on the web component. See

    - https://github.com/PDBeurope/pdbe-molstar/wiki/2.-PDBe-Molstar-as-Web-component

    """

    molecule_id = param.String(
        default=None,
        doc="PDB id to load"
    )
    custom_data_url = param.String(
        default=None,
        doc="Data url for loading custom data. Incompatible with `molecule_id`"
    )
    custom_data_binary = param.Boolean(
        default=None, doc="Optional parameter")

    custom_data_format = param.String(
        default=None,
        doc="Format for custom data, for example 'cif'"
    )

    ligand_label_comp_id = param.String(
        default=None,
        doc=""
    )

    ligand_auth_asym_id = param.String(
        default=None,
    )

    ligand_auth_seq_id = param.Number(
        default=None,
    )

    ligand_hydrogens = param.Boolean(
        default=None
    )

    alphafold_view = param.Boolean(
        default=None,
        doc="Applies AlphaFold confidence score colouring theme for alphafold model"
    )

    assembly_id = param.Number(
        default=None,
        doc="Specify assembly"
    )

    bg_color = param.String(
        doc="Color of the background. If `None`, colors default is chosen depending on the color theme"
    )

    highlight_color = param.String(
        doc='Color for mouseover highlighting'
    )

    select_color = param.String(
        doc='Color for selections'
    )

    visual_style = param.Selector(
        objects=REPRESENTATIONS,
        doc="Visual styling"
    )

    theme = param.Selector(
        default='dark',
        objects=['light', 'dark'],
        doc="CSS theme to use"
    )

    # hide_polymer = param.Boolean(
    #     default=None,
    #     doc="Hide polymer"
    # )

    # hide_water = param.Boolean(
    #     default=None,
    #     doc="Hide water"
    # )

    # hide_het = param.Boolean(
    #     default=None,
    #     doc="Hide het"
    # )

    # hide_carbs = param.Boolean(
    #     default=None,
    #     doc="Hide carbs"
    # )

    # hide_non_standard = param.Boolean(
    #     default=None,
    #     doc="Hide non standard"
    # )

    # hide_coarse = param.Boolean(
    #     default=None,
    #     doc="Hide coarse"
    # )

    # pdbe_url = param.String(
    #     default=None,
    #     doc="Url for PDB data. Mostly used for internal testing"
    # )

    # load_maps = param.Boolean(
    #     default=None,
    #     doc="Load electron density maps from the pdb volume server"
    # )

    # validation_annotation = param.Boolean(
    #     default=None,
    #     doc="Adds 'annotation' control in the menu"
    # )

    # domain_annotations = param.Boolean(
    #     default=None,
    #     doc="Adds 'annotation' control in the menu"
    # )

    # low_precision = param.Boolean(
    #     default=None,
    #     doc="Load low precision coordinates from the model server"
    # )

    # expanded = param.Boolean(
    #     default=None,
    #     doc="Display full-screen by default on load"
    # )

    # hide_controls = param.Boolean(
    #     default=True,
    #     doc="Hide the control menu"
    # )

    # landscape = param.Boolean(
    #     default=None
    # )

    # select_interaction = param.Boolean(
    #     default=None,
    #     doc="Switch on or off the default selection interaction behaviour"
    # )

    # lighting = param.Selector(
    #     default='matte',
    #     objects=['flat', 'matte', 'glossy', 'metallic', 'plastic'],
    #     doc="Set the lighting"
    # )

    # default_preset = param.Selector(
    #     default='default',
    #     objects=['default', 'unitcell', 'all-models', 'supercell'],
    #     doc="Set the preset view"
    # )

    # pdbe_link = param.Selector(
    #     default=None,
    #     doc="Show the PDBe entry link at in the top right corner"
    # )

    # hide_expand_icon = param.Boolean(
    #     default=None,
    #     doc="Hide the expand icon"
    # )

    # hide_selection_icon = param.Boolean(
    #     default=None, # Default False, set False/True for True
    #     doc="Hide the selection icon"
    # )

    # hide_animation_icon = param.Boolean(
    #     default=None,
    #     doc="Hide the animation icon"
    # )

    # component_spec = param.String()

    _template = """
<link id="molstarTheme" rel="stylesheet" type="text/css" href="https://www.ebi.ac.uk/pdbe/pdb-component-library/css/pdbe-molstar-1.2.0.css"/>
<div id="pdbeMolStarContainer" style="width:100%; height: 100%;">
<pdbe-molstar id="pdbeMolstarComponent" 
    molecule-id=${molecule_id} 
    custom-data-url=${custom_data_url} custom-data-format=${custom_data_format} custom-data-binary=${custom_data_binary}
    ligand-label-comp-id=${ligand_label_comp_id} ligand-auth-asym-Id=${ligand_auth_asym_id} ligand-auth-seq-id=${ligand_auth_seq_id} ligand-hydrogens=${ligand_hydrogens}
    alphafold-view=${alphafold_view}
    assembly-id=${assembly_id}
></pdbe-molstar>
</div>
"""
# highlight-color-r=${highlight_color_r} highlight-color-g=${highlight_color_g} highlight-color-b=${highlight_color_b}

    __javascript__=[
        # "https://cdn.jsdelivr.net/npm/babel-polyfill/dist/polyfill.min.js",
        # "https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs/webcomponents-lite.js",
        # "https://cdn.jsdelivr.net/npm/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js",
        "https://www.ebi.ac.uk/pdbe/pdb-component-library/js/pdbe-molstar-component-1.2.1.js",
    ]

    _scripts = {
        "render": """
function standardize_color(str){
    var ctx = document.createElement("canvas").getContext("2d");
    ctx.fillStyle = str;
    return ctx.fillStyle;
}
function toRgb(color) {
  var hex = standardize_color(color)
  var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : null;
}
state.toRgb = toRgb
self.bg_color()
self.highlight_color()
self.select_color()
""",
    "bg_color": """
rgb = state.toRgb(data.bg_color)
pdbeMolstarComponent.viewerInstance.canvas.setBgColor(rgb)
""",
    # does not work
    "highlight_color": """ 
rgb = state.toRgb(data.highlight_color)
pdbeMolstarComponent.highlightColorR=rgb.r
pdbeMolstarComponent.highlightColorG=rgb.g
pdbeMolstarComponent.highlightColorB=rgb.b
""",
    # does not work
    "select_color": """ 
rgb = state.toRgb(data.select_color)
pdbeMolstarComponent.selectColorR=rgb.r
pdbeMolstarComponent.selectColorG=rgb.g
pdbeMolstarComponent.selectColorB=rgb.b
"""
    }

    def __init__(self, **params):
        super().__init__(**params)

        # skip = {
        #     'theme',
        #     'name',
        #     'bg_color'
        # }

        # # Find class attributes defined only on this subclass
        # attrs = self.__class__.__dict__.keys() - HTML.__class__.__dict__.keys() - skip
        # attrs = [a for a in attrs if not a.startswith('_')]

        # elements = []
        # for attr in attrs:
        #     val = getattr(self, attr)
        #     if val is None:
        #         continue
        #     elif isinstance(val, bool):
        #         val = 'true' if val else 'false'
        #     else:
        #         val = f'"{val}"'

        #     name = attr.replace('_', '-')
        #     elem = f'{name}={val}'
        #     elements.append(elem)

        # bg_default = 'F7F7F7' if self.theme == 'light' else '000000'
        # self.bg_color = self.bg_color or bg_default

        # # Split color options into rgb components
        # color_options = ['bg_color', 'highlight_color', 'select_color']
        # for option in color_options:
        #     hex_val = getattr(self, option)
        #     if hex_val is None:
        #         continue
        #     hex_val = hex_val.lstrip('#')
        #     name = option.replace('_', '-')
        #     for i, c in enumerate(['r', 'g', 'b']):
        #         val = int(hex_val[2*i:2*i+2], 16)
        #         elem = f'{name}-{c}="{val}"'
        #         elements.append(elem)

        # component_spec = ' '.join(elements)

        # theme = '-light' if self.theme == 'light' else ''
        # html_string = (Path(__file__).parent / 'molstar_webcomponent.html').read_text()
        # html_template = Template(html_string)

        # self.component_spec = html_template.substitute(
        #     component_spec=component_spec,
        #     theme=theme
        # )
        # print(self.component_spec)

pn.extension(sizing_mode="stretch_width")

pdbe = PdbeMolStarWebComponent(
    height=500,
    # hide_water=True,
    # theme='light',
    # lighting='metallic',
    # hide_expand_icon=True,
    # highlight_color='#d1fa07',
    # bg_color='#8ad6e6',
    visual_style="distance-restraint",
    highlight_color="blue",
    molecule_id='1qyn',
)
pn.template.FastListTemplate(
    site="Panel Chemistry", title="Pdbe Molstar Viewer",
    sidebar=[pn.Param(pdbe)],
    main=[pdbe],
).servable()
MarcSkovMadsen commented 2 years ago

I've experimented with the JS plugin as well @Jhsmit . I would recommend using this one. It seems more robust.

I have implemented below. I will continue a little bit more with it now and the create a separate PR using this one. I will try to figure out how you can become a co-contributor and push directly to branches on this repo.

"""A Panel Pane to wrap the PDBe implementation of the Mol* ('MolStar') viewer.

    The PBBe viewer is available both as a webcomponent or interactive plugin.

    Check out

    - [PDBe Mol*](https://github.com/PDBeurope/pdbe-molstar)
    - [Mol*](https://molstar.org/)
    - [Mol* GitHub](https://github.com/molstar/molstar)

    Cite Mol*:
    David Sehnal, Sebastian Bittrich, Mandar Deshpande, Radka Svobodová, Karel Berka,
    Václav Bazgier, Sameer Velankar, Stephen K Burley, Jaroslav Koča, Alexander S Rose:
    Mol* Viewer: modern web app for 3D visualization and analysis of large biomolecular structures,
    Nucleic Acids Research, 2021; https://doi.org/10.1093/nar/gkab314.

    """

import param
from panel.reactive import ReactiveHTML
from panel.pane import HTML
import panel as pn
from pathlib import Path
from string import Template

REPRESENTATIONS = [
    "cartoon",
    "ball-and-stick",
    "carbohydrate",
    "distance-restraint",
    "ellipsoid",
    "gaussian-surface",
    "molecular-surface",
    "point",
    "putty",
    "spacefill",
]

# See https://embed.plnkr.co/plunk/m3GxFYx9cBjIanBp for an example JS implementation
class PdbeMolStarWebComponent(ReactiveHTML):
    """PDBe MolStar structure viewer.

    Set one of `molecule_id`, `custom_data` and `ligand_view`.

    For more information:

    - https://github.com/PDBeurope/pdbe-molstar/wiki
    - https://molstar.org/

    The implementation is based on the js Plugin. See

    - https://github.com/PDBeurope/pdbe-molstar/wiki/1.-PDBe-Molstar-as-JS-plugin

    """

    molecule_id = param.String(
        default=None,
        doc="PDB id to load. Example: '1qyn' or '1cbs'"
    )
    custom_data = param.Dict(
        doc="""Load data from a specific data source. Example: 
        { "url": "https://www.ebi.ac.uk/pdbe/coordinates/1cbs/chains?entityId=1&asymId=A&encoding=bcif", "format": "cif", "binary": True }
        """
    )
    ligand_view = param.Dict(
        doc="""This option can be used to display the PDBe ligand page 3D view like https://www.ebi.ac.uk/pdbe/entry/pdb/1cbs/bound/REA.
        Example: {"label_comp_id": "REA"}
        """
    )

    alphafold_view = param.Boolean(
        default=False,
        doc="Applies AlphaFold confidence score colouring theme for alphafold model"
    )

    assembly_id = param.String(
        doc="Specify assembly"
    )

    # bg_color = param.String(
    #     doc="Color of the background. If `None`, colors default is chosen depending on the color theme"
    # )

    # highlight_color = param.String(
    #     doc='Color for mouseover highlighting'
    # )

    # select_color = param.String(
    #     doc='Color for selections'
    # )

    visual_style = param.Selector(
        objects=REPRESENTATIONS,
        doc="Visual styling"
    )

    # theme = param.Selector(
    #     default='dark',
    #     objects=['light', 'dark'],
    #     doc="CSS theme to use"
    # )

    # hide_polymer = param.Boolean(
    #     default=None,
    #     doc="Hide polymer"
    # )

    # hide_water = param.Boolean(
    #     default=None,
    #     doc="Hide water"
    # )

    # hide_het = param.Boolean(
    #     default=None,
    #     doc="Hide het"
    # )

    # hide_carbs = param.Boolean(
    #     default=None,
    #     doc="Hide carbs"
    # )

    # hide_non_standard = param.Boolean(
    #     default=None,
    #     doc="Hide non standard"
    # )

    # hide_coarse = param.Boolean(
    #     default=None,
    #     doc="Hide coarse"
    # )

    # pdbe_url = param.String(
    #     default=None,
    #     doc="Url for PDB data. Mostly used for internal testing"
    # )

    # load_maps = param.Boolean(
    #     default=None,
    #     doc="Load electron density maps from the pdb volume server"
    # )

    # validation_annotation = param.Boolean(
    #     default=None,
    #     doc="Adds 'annotation' control in the menu"
    # )

    # domain_annotations = param.Boolean(
    #     default=None,
    #     doc="Adds 'annotation' control in the menu"
    # )

    # low_precision = param.Boolean(
    #     default=None,
    #     doc="Load low precision coordinates from the model server"
    # )

    # expanded = param.Boolean(
    #     default=None,
    #     doc="Display full-screen by default on load"
    # )

    # hide_controls = param.Boolean(
    #     default=True,
    #     doc="Hide the control menu"
    # )

    # landscape = param.Boolean(
    #     default=None
    # )

    # select_interaction = param.Boolean(
    #     default=None,
    #     doc="Switch on or off the default selection interaction behaviour"
    # )

    # lighting = param.Selector(
    #     default='matte',
    #     objects=['flat', 'matte', 'glossy', 'metallic', 'plastic'],
    #     doc="Set the lighting"
    # )

    # default_preset = param.Selector(
    #     default='default',
    #     objects=['default', 'unitcell', 'all-models', 'supercell'],
    #     doc="Set the preset view"
    # )

    # pdbe_link = param.Selector(
    #     default=None,
    #     doc="Show the PDBe entry link at in the top right corner"
    # )

    # hide_expand_icon = param.Boolean(
    #     default=None,
    #     doc="Hide the expand icon"
    # )

    # hide_selection_icon = param.Boolean(
    #     default=None, # Default False, set False/True for True
    #     doc="Hide the selection icon"
    # )

    # hide_animation_icon = param.Boolean(
    #     default=None,
    #     doc="Hide the animation icon"
    # )

    # component_spec = param.String()

    _template = """
<link id="molstarTheme" rel="stylesheet" type="text/css" href="https://www.ebi.ac.uk/pdbe/pdb-component-library/css/pdbe-molstar-1.2.0.css"/>
<div id="container" style="width:100%; height: 100%;"><div id="pdbeViewer" style="width:100%; height: 100%;"></div></div>
"""
    __javascript__=[
        "https://www.ebi.ac.uk/pdbe/pdb-component-library/js/pdbe-molstar-plugin-1.2.0.js",
    ]

    _scripts = {
        "render": """
function standardize_color(str){
    var ctx = document.createElement("canvas").getContext("2d");
    ctx.fillStyle = str;
    return ctx.fillStyle;
}
function toRgb(color) {
  var hex = standardize_color(color)
  var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result ? {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16)
  } : null;
}
state.toRgb = toRgb
function getOptions(){
   return {
        moleculeId: data.molecule_id,
        customData: data.custom_data,
        ligandView: data.ligand_view,
        alphafoldView: data.alphafold_view,
        assemblyId: data.assembly_id,
        visualStyle: data.visual_style,
    }
}
state.getOptions=getOptions
state.viewerInstance = new PDBeMolstarPlugin();
state.viewerInstance.render(pdbeViewer, state.getOptions());    
""",
    "rerender": """
state.viewerInstance.visual.update(state.getOptions(), fullLoad=true)
""",
    "molecule_id": "state.viewerInstance.visual.update({moleculeId:data.molecule_id})",
    "custom_data": "state.viewerInstance.visual.update({customData:data.custom_data})",
    # Don't know if below works. To be tested
    "ligand_view": "state.viewerInstance.visual.update({ligandView:data.ligand_view})",
    "alphafold_view": "state.viewerInstance.visual.update({alphafoldView:data.alphafold_view})",
    "assembly_id": "state.viewerInstance.visual.update({assembly_id:data.assembly_id})",
    "visual_style": "self.rerender()",
    }

    def __init__(self, **params):
        super().__init__(**params)

        # skip = {
        #     'theme',
        #     'name',
        #     'bg_color'
        # }

        # # Find class attributes defined only on this subclass
        # attrs = self.__class__.__dict__.keys() - HTML.__class__.__dict__.keys() - skip
        # attrs = [a for a in attrs if not a.startswith('_')]

        # elements = []
        # for attr in attrs:
        #     val = getattr(self, attr)
        #     if val is None:
        #         continue
        #     elif isinstance(val, bool):
        #         val = 'true' if val else 'false'
        #     else:
        #         val = f'"{val}"'

        #     name = attr.replace('_', '-')
        #     elem = f'{name}={val}'
        #     elements.append(elem)

        # bg_default = 'F7F7F7' if self.theme == 'light' else '000000'
        # self.bg_color = self.bg_color or bg_default

        # # Split color options into rgb components
        # color_options = ['bg_color', 'highlight_color', 'select_color']
        # for option in color_options:
        #     hex_val = getattr(self, option)
        #     if hex_val is None:
        #         continue
        #     hex_val = hex_val.lstrip('#')
        #     name = option.replace('_', '-')
        #     for i, c in enumerate(['r', 'g', 'b']):
        #         val = int(hex_val[2*i:2*i+2], 16)
        #         elem = f'{name}-{c}="{val}"'
        #         elements.append(elem)

        # component_spec = ' '.join(elements)

        # theme = '-light' if self.theme == 'light' else ''
        # html_string = (Path(__file__).parent / 'molstar_webcomponent.html').read_text()
        # html_template = Template(html_string)

        # self.component_spec = html_template.substitute(
        #     component_spec=component_spec,
        #     theme=theme
        # )
        # print(self.component_spec)

pn.extension(sizing_mode="stretch_width")

pdbe = PdbeMolStarWebComponent(
    molecule_id='1qyn',
    # custom_data= { "url": "https://www.ebi.ac.uk/pdbe/coordinates/1cbs/chains?entityId=1&asymId=A&encoding=bcif", "format": "cif", "binary": True },
    # ligand_view={"label_comp_id": "REA"},
    alphafold_view=False,
    height=500,
    # hide_water=True,
    # theme='light',
    # lighting='metallic',
    # hide_expand_icon=True,
    # highlight_color='#d1fa07',
    # bg_color='#8ad6e6',
    visual_style="cartoon" # , "ball-and-stick",
    # highlight_color="blue",
)
pn.template.FastListTemplate(
    site="Panel Chemistry", title="Pdbe Molstar Viewer",
    sidebar=[pn.Param(pdbe)],
    main=[pdbe],
).servable()
Jhsmit commented 2 years ago

Ok sounds good. I've reached the same conclusion and started with the JS plugin. But I'm about at the same stage as you are. Thanks for letting me know, I was about to continue, but I'll hold off so we dont repeat the same effort.

Jhsmit commented 2 years ago

@MarcSkovMadsen did you have any problems with param.Color on a ReactiveHTML? This raises an error for me: https://github.com/holoviz/panel/issues/3058

MarcSkovMadsen commented 2 years ago

I will close this one and create a new one based on the JS plugin