Real Dashboards: Adding a Component to Display Header Information

To enhance our PDB dashboard, we can add a component to display header information, such as the PDB ID, the name of the molecule, the method, and other relevant details. The idea here is just to add another column to our layout to the right of the molecule viewer that will display this information in a nice format.

../_images/dash-app-with-header-column.png

PDB dashboard layout with an additional column for header information.

Updated Imports

To implement this feature, we will need to import some additional components from Biopython. Specifically, we will need to import the parse_pdb_header function from Biopython’s Bio.PDB module to parse the PDB file and extract the header information. Let’s update our imports in the app.py file to include this new import.

from Bio.PDB import PDBList, parse_pdb_header

Updated Layout

Next, we will update our layout to include a new column for the header information. We will use a dbc.Col component to create this new column, and we will give it an id of header-info so that we can update its content with a callback function later. Since we want this column to be to the right of the molecule viewer, we will place it after the column that contains the molecule viewer in our layout and resize the columns accordingly (give the molecule viewer column and the header info column a width 0f 5). Let’s update our layout code in the app.py file to include this new column.

# App layout
app.layout = dbc.Container([
    dbc.Row([
        html.Div("Molecular Structure Viewer", className="text-primary text-center fs-3 mb-4")
    ]),

    dbc.Row([
        dbc.Col([
            dbc.Label("Enter PDB ID:", className="fw-bold"),
            dbc.Input(
                id='pdb-input',
                type='text',
                placeholder='e.g., 4HHB, 3AID, 2MRU, 4K8X',
                value='4HHB',
                className="mb-2"
            ),
            dbc.Button("Load Structure", id='load-button', color="primary"),
            html.Div(id='status-message', className="mt-3")
        ], width=2),

        dbc.Col([
            html.Div(id='molecule-viewer', children=[
                html.Div("Enter a PDB ID and click 'Load Structure' to view the molecule.",
                        className="text-center text-muted mt-5")
            ])
        ], width=5),

        dbc.Col([
            html.Div(id='header-info', children=[
                html.Div("Header information will appear here.",
                        className="text-center text-muted mt-5")
            ], style={'maxHeight': '600px', 'overflowY': 'auto'})
        ], width=5)
    ], className="mt-4"),
], fluid=True)

Updated Callback Function

Since we want to update the content of the new header information column when the user loads a new PDB structure, we will need to update our callback function to include an additional output for the header information.

@callback(
    [Output('molecule-viewer', 'children'),
    Output('header-info', 'children'),
    Output('status-message', 'children')],
    Input('load-button', 'n_clicks'),
    State('pdb-input', 'value'),
    prevent_initial_call=True
)

As we saw in the layout section, we will target the id header-info to update the content of the header information column and will replace the existing content of children.

Now we need to update the logic of our callback function, load_molecule, to parse the header information from the PDB file and create a nice format to display it in the header information column. We will add the following code to our callback function to parse the header information from the PDB file:

# Parse PDB header information
header_info = parse_pdb_header(pdb_file)

Then, we will call a helper function called create_header_display that will take the parsed header information and create a nice format using dbc.Card and dbc.CardBody to display it in the header information column.

header_display = create_header_display(header_info, pdb_id)

And, the helper function create_header_display will look something like this:

def create_header_display(header_info, pdb_id):
    """Create a formatted display of PDB header information"""
    header_sections = []

    # Title
    if 'name' in header_info:
        header_sections.append(
            html.Div([
                html.H6("Name", className="fw-bold mt-3 mb-2"),
                html.P(header_info['name'], className="text-sm")
            ])
        )

    # Structure Classification
    if 'structure_method' in header_info:
        header_sections.append(
            html.Div([
                html.H6("Method", className="fw-bold mt-3 mb-2"),
                html.P(header_info['structure_method'], className="text-sm")
            ])
        )

    # Release Date
    if 'release_date' in header_info:
        header_sections.append(
            html.Div([
                html.H6("Release Date", className="fw-bold mt-3 mb-2"),
                html.P(header_info['release_date'], className="text-sm")
            ])
        )

    # Deposition Date
    if 'deposition_date' in header_info:
        header_sections.append(
            html.Div([
                html.H6("Deposition Date", className="fw-bold mt-3 mb-2"),
                html.P(header_info['deposition_date'], className="text-sm")
            ])
        )

    # Resolution
    if 'resolution' in header_info and header_info['resolution'] is not None:
        header_sections.append(
            html.Div([
                html.H6("Resolution (Å)", className="fw-bold mt-3 mb-2"),
                html.P(f"{header_info['resolution']:.2f}", className="text-sm")
            ])
        )

    if 'journal_reference' in header_info and header_info['journal_reference']:
        journal_text = header_info['journal_reference']
        header_sections.append(
            html.Div([
                html.H6("Journal Reference", className="fw-bold mt-3 mb-2"),
                html.P(journal_text, className="text-sm", style={'wordWrap': 'break-word'})
            ])
        )

    # Keywords
    if 'keywords' in header_info and header_info['keywords']:
        keywords_text = header_info['keywords']
        header_sections.append(
            html.Div([
                html.H6("Keywords", className="fw-bold mt-3 mb-2"),
                html.P(keywords_text, className="text-sm", style={'wordWrap': 'break-word'})
            ])
        )

    if header_sections:
        return dbc.Card([
            dbc.CardBody([
                html.H5(f"PDB: {pdb_id.upper()}", className="card-title"),
                html.Hr(),
                *header_sections
            ])
        ], style={'height': '100%'})
    else:
        return html.Div("No header information available.", className="text-center text-muted mt-5")

Of course, now that we have added a third output to our callback function, we will also need to update the any return statements in the callback function to include the new output for the header information. For example, if we don’t receive a valid PDB ID, we will return:

if not pdb_id:
    return (
        html.Div("Please enter a valid PDB ID.", className="text-center text-muted mt-5"),
        html.Div("Header information will appear here.", className="text-center text-muted mt-5"),
        dbc.Alert("Please enter a PDB ID.", color="warning")
    )

Or, if there is an error loading the molecule, we will return:

except Exception as e:
    error_msg = dbc.Alert(
        f"Error loading PDB {pdb_id.upper()}: {str(e)}",
        color="danger"
    )
    empty_viewer = html.Div(
        "Failed to load molecule. Please check the PDB ID and try again.",
        className="text-center text-muted mt-5"
    )
    empty_header = html.Div(
        "Header information will appear here.",
        className="text-center text-muted mt-5"
    )
    return empty_viewer, empty_header, error_msg

And, finally, if the molecule loads successfully, we will return:

return viewer, header_display, status

Running the Updated App

Finally, putting all of these updates together, our updated app.py file should look like this:

Code
  1import os
  2
  3import dash_bio as dashbio
  4import dash_bootstrap_components as dbc
  5from Bio.PDB import PDBList, parse_pdb_header
  6from dash import Dash, Input, Output, State, callback, html
  7from dash_bio.utils import PdbParser as DashPdbParser
  8from dash_bio.utils import create_mol3d_style
  9
 10# Initialize the Dash app
 11external_stylesheets = [dbc.themes.CERULEAN]
 12app = Dash(__name__, external_stylesheets=external_stylesheets)
 13
 14# App layout
 15app.layout = dbc.Container([
 16    dbc.Row([
 17        html.Div("Molecular Structure Viewer", className="text-primary text-center fs-3 mb-4")
 18    ]),
 19
 20    dbc.Row([
 21        dbc.Col([
 22            dbc.Label("Enter PDB ID:", className="fw-bold"),
 23            dbc.Input(
 24                id='pdb-input',
 25                type='text',
 26                placeholder='e.g., 4HHB, 3AID, 2MRU, 4K8X',
 27                value='4HHB',
 28                className="mb-2"
 29            ),
 30            dbc.Button("Load Structure", id='load-button', color="primary"),
 31            html.Div(id='status-message', className="mt-3")
 32        ], width=2),
 33
 34        dbc.Col([
 35            html.Div(id='molecule-viewer', children=[
 36                html.Div("Enter a PDB ID and click 'Load Structure' to view the molecule.",
 37                        className="text-center text-muted mt-5")
 38            ])
 39        ], width=5),
 40
 41        dbc.Col([
 42            html.Div(id='header-info', children=[
 43                html.Div("Header information will appear here.",
 44                        className="text-center text-muted mt-5")
 45            ], style={'maxHeight': '600px', 'overflowY': 'auto'})
 46        ], width=5)
 47    ], className="mt-4"),
 48], fluid=True)
 49
 50# Callback to load and display molecule
 51@callback(
 52    [Output('molecule-viewer', 'children'),
 53    Output('header-info', 'children'),
 54    Output('status-message', 'children')],
 55    Input('load-button', 'n_clicks'),
 56    State('pdb-input', 'value'),
 57    prevent_initial_call=True
 58)
 59def load_molecule(load_clicks, pdb_id):
 60
 61    if not pdb_id:
 62        return (
 63            html.Div("Please enter a valid PDB ID.", className="text-center text-muted mt-5"),
 64            html.Div("Header information will appear here.", className="text-center text-muted mt-5"),
 65            dbc.Alert("Please enter a PDB ID.", color="warning")
 66        )
 67
 68    try:
 69        # Clean up PDB ID (remove whitespace, convert to lowercase)
 70        pdb_id = pdb_id.strip().lower()
 71
 72        # Create PDB directory if it doesn't exist
 73        pdb_dir = './pdb_files'
 74        os.makedirs(pdb_dir, exist_ok=True)
 75
 76        # Download PDB file using BioPython
 77        pdbl = PDBList()
 78        pdb_file = pdbl.retrieve_pdb_file(pdb_id, pdir=pdb_dir, file_format='pdb')
 79
 80        # Read PDB file content for visualization
 81        dash_parser = DashPdbParser(pdb_file)
 82        pdb_data = dash_parser.mol3d_data()  # Get data in format suitable for Molecule3dViewer
 83        # create styles for visualization needed by Molecule3dViewer
 84        # atoms is a list of dictionaries obtained from parsing the PDB file with DashPdbParser
 85        # visualization_type can be 'cartoon', 'stick', 'sphere'
 86        # color_element can be 'residue', 'chain', 'element', 'partialCharge'
 87        styles = create_mol3d_style(
 88            pdb_data['atoms'], visualization_type='cartoon', color_element='residue'
 89        )
 90
 91        # Parse PDB header information
 92        header_info = parse_pdb_header(pdb_file)
 93
 94        # Create Molecule3dViewer component
 95        viewer = create_molecule_viewer(pdb_data, styles)
 96
 97        # Create header display
 98        header_display = create_header_display(header_info, pdb_id)
 99
100        status = dbc.Alert(
101            f"Successfully loaded PDB ID: {pdb_id.upper()}",
102            color="success"
103        )
104
105        return viewer, header_display, status
106
107    except Exception as e:
108        error_msg = dbc.Alert(
109            f"Error loading PDB {pdb_id.upper()}: {str(e)}",
110            color="danger"
111        )
112        empty_viewer = html.Div(
113            "Failed to load molecule. Please check the PDB ID and try again.",
114            className="text-center text-muted mt-5"
115        )
116        empty_header = html.Div(
117            "Header information will appear here.",
118            className="text-center text-muted mt-5"
119        )
120        return empty_viewer, empty_header, error_msg
121
122def create_molecule_viewer(pdb_data, styles):
123    """Create a Molecule3dViewer from PDB data"""
124    return dashbio.Molecule3dViewer(
125        id='molecule-3d',
126        modelData=pdb_data,
127        styles=styles,
128        selectionType='atom',
129        backgroundColor='#F0F0F0',
130        height=600,
131        width='100%'
132    )
133
134def create_header_display(header_info, pdb_id):
135    """Create a formatted display of PDB header information"""
136    header_sections = []
137
138    # Title
139    if 'name' in header_info:
140        header_sections.append(
141            html.Div([
142                html.H6("Name", className="fw-bold mt-3 mb-2"),
143                html.P(header_info['name'], className="text-sm")
144            ])
145        )
146
147    # Structure Classification
148    if 'structure_method' in header_info:
149        header_sections.append(
150            html.Div([
151                html.H6("Method", className="fw-bold mt-3 mb-2"),
152                html.P(header_info['structure_method'], className="text-sm")
153            ])
154        )
155
156    # Release Date
157    if 'release_date' in header_info:
158        header_sections.append(
159            html.Div([
160                html.H6("Release Date", className="fw-bold mt-3 mb-2"),
161                html.P(header_info['release_date'], className="text-sm")
162            ])
163        )
164
165    # Deposition Date
166    if 'deposition_date' in header_info:
167        header_sections.append(
168            html.Div([
169                html.H6("Deposition Date", className="fw-bold mt-3 mb-2"),
170                html.P(header_info['deposition_date'], className="text-sm")
171            ])
172        )
173
174    # Resolution
175    if 'resolution' in header_info and header_info['resolution'] is not None:
176        header_sections.append(
177            html.Div([
178                html.H6("Resolution (Å)", className="fw-bold mt-3 mb-2"),
179                html.P(f"{header_info['resolution']:.2f}", className="text-sm")
180            ])
181        )
182
183    if 'journal_reference' in header_info and header_info['journal_reference']:
184        journal_text = header_info['journal_reference']
185        header_sections.append(
186            html.Div([
187                html.H6("Journal Reference", className="fw-bold mt-3 mb-2"),
188                html.P(journal_text, className="text-sm", style={'wordWrap': 'break-word'})
189            ])
190        )
191
192    # Keywords
193    if 'keywords' in header_info and header_info['keywords']:
194        keywords_text = header_info['keywords']
195        header_sections.append(
196            html.Div([
197                html.H6("Keywords", className="fw-bold mt-3 mb-2"),
198                html.P(keywords_text, className="text-sm", style={'wordWrap': 'break-word'})
199            ])
200        )
201
202    if header_sections:
203        return dbc.Card([
204            dbc.CardBody([
205                html.H5(f"PDB: {pdb_id.upper()}", className="card-title"),
206                html.Hr(),
207                *header_sections
208            ])
209        ], style={'height': '100%'})
210    else:
211        return html.Div("No header information available.", className="text-center text-muted mt-5")
212
213# Run the app
214if __name__ == "__main__":
215    app.run(host='0.0.0.0', port=8050, debug=True)

To run the updated app, simply execute the following command in your VS Code terminal (if it’s not already running):

(.venv) [mbs337-vm]$ python app.py
Dash is running on http://0.0.0.0:8050/

* Serving Flask app 'app'
* Debug mode: on

Now we can navigate to http://<IP_ADDRESS>:8050/ in our web browser to see the updated PDB dashboard with the new header information column.

../_images/dash-app-with-header-successful.png

PDB dashboard application with added header information running in a web browser.

Additional Resources