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.
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.
PDB dashboard application with added header information running in a web browser.