Real Dashboards: Adding a Reset Button

Finally, let’s add a reset button to our dashboard that allows users to clear the current molecule, header information, and histogram from the dashboard and return to the initial state. This will make it easier for users to start fresh without having to manually clear the input field or refresh the page. To do this, we will add a new button to our layout (to the right of the Load Structure button) and then update our callback function to handle the reset functionality.

../_images/pdb-dashboard-layout-reset.png

PDB dashboard layout with reset button.

Updated Imports

To implement the reset functionality, we will need to import the ctx object from Dash which allows us to determine which input triggered the callback. This will help us differentiate between the load button and the reset button. Let’s update our imports in the app.py file.

from dash import Dash, Input, Output, State, callback, ctx, dcc, html

Updated Layout

Next, we will update our layout to include a new reset button. We will use another dbc.Button component for this reset button and place it to the right of the existing load button. To line it up nicely, we will wrap both buttons in a dbc.Row component and each button in its own column using dbc.Col components.

dbc.Row([
    dbc.Col(dbc.Button("Load Structure", id='load-button', color="primary"), width="auto"),
    dbc.Col(dbc.Button("Reset", id='reset-button', color="danger"), width="auto"),
], className="g-2"),

Updated Callback Function

The first thing we need to do is add an additional input to our callback function for the reset button. This will allow us to trigger the same callback function when the reset button is clicked.

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

Since we now have two buttons that can trigger the same callback function, we will need to update our callback function, load_molecule, to determine which button was clicked and handle the logic accordingly. We will use the ctx object to check which input triggered the callback. If the reset button was clicked, we will return the initial state of the dashboard with empty molecule viewer, header information, and histogram. If the load button was clicked, we will proceed with loading the molecule as before.

def load_molecule(load_clicks, reset_clicks, pdb_id):

    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"),
            {},
            {'display': 'none'},
            dbc.Alert("Please enter a PDB ID.", color="warning")
        )

    if ctx.triggered_id == 'reset-button':
        return (
            html.Div("Enter a PDB ID and click 'Load Structure' to view the molecule.", className="text-center text-muted mt-5"),
            html.Div("Header information will appear here.", className="text-center text-muted mt-5"),
            {},
            {'display': 'none'},
            None
        )

Running the Updated App

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

Code
  1import os
  2from collections import Counter
  3
  4import dash_bio as dashbio
  5import dash_bootstrap_components as dbc
  6import plotly.express as px
  7from Bio.PDB import PDBList, PDBParser, parse_pdb_header
  8from dash import Dash, Input, Output, State, callback, ctx, dcc, html
  9from dash_bio.utils import PdbParser as DashPdbParser
 10from dash_bio.utils import create_mol3d_style
 11
 12# Initialize the Dash app
 13external_stylesheets = [dbc.themes.CERULEAN]
 14app = Dash(__name__, external_stylesheets=external_stylesheets)
 15
 16# App layout
 17app.layout = dbc.Container([
 18    dbc.Row([
 19        html.Div("Molecular Structure Viewer", className="text-primary text-center fs-3 mb-4")
 20    ]),
 21
 22    dbc.Row([
 23        dbc.Col([
 24            dbc.Label("Enter PDB ID:", className="fw-bold"),
 25            dbc.Input(
 26                id='pdb-input',
 27                type='text',
 28                placeholder='e.g., 4HHB, 3AID, 2MRU, 4K8X',
 29                value='4HHB',
 30                className="mb-2"
 31            ),
 32            dbc.Row([
 33                dbc.Col(dbc.Button("Load Structure", id='load-button', color="primary"), width="auto"),
 34                dbc.Col(dbc.Button("Reset", id='reset-button', color="danger"), width="auto"),
 35            ], className="g-2"),
 36            html.Div(id='status-message', className="mt-3")
 37        ], width=2),
 38
 39        dbc.Col([
 40            html.Div(id='molecule-viewer', children=[
 41                html.Div("Enter a PDB ID and click 'Load Structure' to view the molecule.",
 42                        className="text-center text-muted mt-5")
 43            ])
 44        ], width=5),
 45
 46        dbc.Col([
 47            html.Div(id='header-info', children=[
 48                html.Div("Header information will appear here.",
 49                        className="text-center text-muted mt-5")
 50            ], style={'maxHeight': '600px', 'overflowY': 'auto'})
 51        ], width=5)
 52    ], className="mt-4"),
 53
 54    dbc.Row([
 55        dbc.Col([
 56            dcc.Graph(id='amino-acid-histogram', figure={}, style={'display': 'none'})
 57        ], width=12)
 58    ], className="mt-4"),
 59], fluid=True)
 60
 61# Callback to load and display molecule
 62@callback(
 63    [Output('molecule-viewer', 'children'),
 64    Output('header-info', 'children'),
 65    Output('amino-acid-histogram', 'figure'),
 66    Output('amino-acid-histogram', 'style'),
 67    Output('status-message', 'children')],
 68    Input('load-button', 'n_clicks'),
 69    Input('reset-button', 'n_clicks'),
 70    State('pdb-input', 'value'),
 71    prevent_initial_call=True
 72)
 73def load_molecule(load_clicks, reset_clicks, pdb_id):
 74
 75    if not pdb_id:
 76        return (
 77            html.Div("Please enter a valid PDB ID.", className="text-center text-muted mt-5"),
 78            html.Div("Header information will appear here.", className="text-center text-muted mt-5"),
 79            {},
 80            {'display': 'none'},
 81            dbc.Alert("Please enter a PDB ID.", color="warning")
 82        )
 83
 84    if ctx.triggered_id == 'reset-button':
 85        return (
 86            html.Div("Enter a PDB ID and click 'Load Structure' to view the molecule.", className="text-center text-muted mt-5"),
 87            html.Div("Header information will appear here.", className="text-center text-muted mt-5"),
 88            {},
 89            {'display': 'none'},
 90            None
 91        )
 92
 93    try:
 94        # Clean up PDB ID (remove whitespace, convert to lowercase)
 95        pdb_id = pdb_id.strip().lower()
 96
 97        # Create PDB directory if it doesn't exist
 98        pdb_dir = './pdb_files'
 99        os.makedirs(pdb_dir, exist_ok=True)
100
101        # Download PDB file using BioPython
102        pdbl = PDBList()
103        pdb_file = pdbl.retrieve_pdb_file(pdb_id, pdir=pdb_dir, file_format='pdb')
104
105        # Read PDB file content for visualization
106        dash_parser = DashPdbParser(pdb_file)
107        pdb_data = dash_parser.mol3d_data()  # Get data in format suitable for Molecule3dViewer
108        # create styles for visualization needed by Molecule3dViewer
109        # atoms is a list of dictionaries obtained from parsing the PDB file with DashPdbParser
110        # visualization_type can be 'cartoon', 'stick', 'sphere'
111        # color_element can be 'residue', 'chain', 'element', 'partialCharge'
112        styles = create_mol3d_style(
113            pdb_data['atoms'], visualization_type='cartoon', color_element='residue'
114        )
115
116        # Parse PDB structure for amino acid analysis
117        bio_parser = PDBParser(QUIET=True)
118        structure = bio_parser.get_structure(pdb_id, pdb_file)
119        amino_acid_counts = count_amino_acids(structure)
120
121        # Parse PDB header information
122        header_info = parse_pdb_header(pdb_file)
123
124        # Create Molecule3dViewer component
125        viewer = create_molecule_viewer(pdb_data, styles)
126
127        # Create header display
128        header_display = create_header_display(header_info, pdb_id)
129
130        # Create amino acid histogram
131        histogram = create_amino_acid_histogram(amino_acid_counts, pdb_id)
132
133        status = dbc.Alert(
134            f"Successfully loaded PDB ID: {pdb_id.upper()}",
135            color="success"
136        )
137
138        if not histogram:
139            return viewer, header_display, histogram, {'display': 'none'}, status
140        else:
141            return viewer, header_display, histogram, {'display': 'block'}, status
142
143    except Exception as e:
144        error_msg = dbc.Alert(
145            f"Error loading PDB {pdb_id.upper()}: {str(e)}",
146            color="danger"
147        )
148        empty_viewer = html.Div(
149            "Failed to load molecule. Please check the PDB ID and try again.",
150            className="text-center text-muted mt-5"
151        )
152        empty_header = html.Div(
153            "Header information will appear here.",
154            className="text-center text-muted mt-5"
155        )
156        return empty_viewer, empty_header, {}, {'display': 'none'}, error_msg
157
158def create_molecule_viewer(pdb_data, styles):
159    """Create a Molecule3dViewer from PDB data"""
160    return dashbio.Molecule3dViewer(
161        id='molecule-3d',
162        modelData=pdb_data,
163        styles=styles,
164        selectionType='atom',
165        backgroundColor='#F0F0F0',
166        height=600,
167        width='100%'
168    )
169
170def create_header_display(header_info, pdb_id):
171    """Create a formatted display of PDB header information"""
172    header_sections = []
173
174    # Title
175    if 'name' in header_info:
176        header_sections.append(
177            html.Div([
178                html.H6("Name", className="fw-bold mt-3 mb-2"),
179                html.P(header_info['name'], className="text-sm")
180            ])
181        )
182
183    # Structure Classification
184    if 'structure_method' in header_info:
185        header_sections.append(
186            html.Div([
187                html.H6("Method", className="fw-bold mt-3 mb-2"),
188                html.P(header_info['structure_method'], className="text-sm")
189            ])
190        )
191
192    # Release Date
193    if 'release_date' in header_info:
194        header_sections.append(
195            html.Div([
196                html.H6("Release Date", className="fw-bold mt-3 mb-2"),
197                html.P(header_info['release_date'], className="text-sm")
198            ])
199        )
200
201    # Deposition Date
202    if 'deposition_date' in header_info:
203        header_sections.append(
204            html.Div([
205                html.H6("Deposition Date", className="fw-bold mt-3 mb-2"),
206                html.P(header_info['deposition_date'], className="text-sm")
207            ])
208        )
209
210    # Resolution
211    if 'resolution' in header_info and header_info['resolution'] is not None:
212        header_sections.append(
213            html.Div([
214                html.H6("Resolution (Å)", className="fw-bold mt-3 mb-2"),
215                html.P(f"{header_info['resolution']:.2f}", className="text-sm")
216            ])
217        )
218
219    if 'journal_reference' in header_info and header_info['journal_reference']:
220        journal_text = header_info['journal_reference']
221        header_sections.append(
222            html.Div([
223                html.H6("Journal Reference", className="fw-bold mt-3 mb-2"),
224                html.P(journal_text, className="text-sm", style={'wordWrap': 'break-word'})
225            ])
226        )
227
228    # Keywords
229    if 'keywords' in header_info and header_info['keywords']:
230        keywords_text = header_info['keywords']
231        header_sections.append(
232            html.Div([
233                html.H6("Keywords", className="fw-bold mt-3 mb-2"),
234                html.P(keywords_text, className="text-sm", style={'wordWrap': 'break-word'})
235            ])
236        )
237
238    if header_sections:
239        return dbc.Card([
240            dbc.CardBody([
241                html.H5(f"PDB: {pdb_id.upper()}", className="card-title"),
242                html.Hr(),
243                *header_sections
244            ])
245        ], style={'height': '100%'})
246    else:
247        return html.Div("No header information available.", className="text-center text-muted mt-5")
248
249def count_amino_acids(structure):
250    """Count amino acid frequencies in a PDB structure"""
251    # Standard amino acids (3-letter codes)
252    standard_aa = {
253        'ALA', 'CYS', 'ASP', 'GLU', 'PHE', 'GLY', 'HIS', 'ILE', 'LYS', 'LEU',
254        'MET', 'ASN', 'PRO', 'GLN', 'ARG', 'SER', 'THR', 'VAL', 'TRP', 'TYR'
255    }
256
257    amino_acids = []
258
259    # Iterate through all residues in all chains
260    for model in structure:
261        for chain in model:
262            for residue in chain:
263                # Get residue name and check if it's a standard amino acid
264                res_name = residue.get_resname().strip()
265                if res_name in standard_aa:
266                    amino_acids.append(res_name)
267
268    # Count frequencies
269    return Counter(amino_acids)
270
271def create_amino_acid_histogram(amino_acid_counts, pdb_id):
272    """Create a Plotly histogram of amino acid frequencies"""
273    if not amino_acid_counts:
274        return {}
275
276    # Convert to lists for plotting
277    amino_acids = list(amino_acid_counts.keys())
278    counts = list(amino_acid_counts.values())
279
280    # Create bar chart (histogram)
281    fig = px.bar(
282        x=amino_acids,
283        y=counts,
284        labels={'x': 'Amino Acid', 'y': 'Frequency'},
285        title=f'Amino Acid Composition - PDB: {pdb_id.upper()}',
286        color=counts,
287        color_continuous_scale='Viridis'
288    )
289
290    # Update layout
291    fig.update_layout(
292        xaxis_title='Amino Acid (3-letter code)',
293        yaxis_title='Count',
294        showlegend=False,
295        height=400,
296        hovermode='x'
297    )
298
299    # Sort by amino acid name for consistent display
300    fig.update_xaxes(categoryorder='category ascending')
301
302    return fig
303
304# Run the app
305if __name__ == "__main__":
306    app.run(host='0.0.0.0', port=8050, debug=True)

Again, 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 reset button.

../_images/pdb-dashboard-clicked-reset.png

PDB dashboard application with reset button running in a web browser.

Additional Resources