Building Real Dashboards with Plotly Dash

Now that we have a good understanding of how Dash works and how to create visualizations with Plotly, we can start building real dashboards using Plotly Dash. In this section, we will go through the process of building a dashboard that interfaces with RCS PDB to grab a PDB file for the chosen ID and display information from it including a 3D molecular view, header information, and a visualization showing the amino acid distribution. We will go through this process step by step, from setting up the environment to deploying the dashboard. By the end of this section, you should be able to:

  • Create a Dash application that interfaces with an external API (RCS PDB).

  • Display a 3D molecular viewer using a dash-bio component, Molecule3dViewer.

  • Extract and display header information from PDB files using Biopython’s Bio.PDB module.

  • Visualize amino acid distribution using a histogram with Plotly.

  • Deploy the dashboard in production (with gunicorn) in a Docker container using Docker Compose.

Setting up the Environment

Before we start building our dashboard, we need to set up our environment. To get started, we will create a new directory called pdb-dashboard in our mbs-337 directory on the Linux VM.

[mbs337-vm]$ mkdir pdb-dashboard
[mbs337-vm]$ cd pdb-dashboard

Instead of using our ever-growing virtual environment, we will create a new one specifically for this project. This will help us keep our dependencies organized and avoid conflicts with other projects.

[mbs337-vm]$ python3 -m venv .venv
[mbs337-vm]$ source .venv/bin/activate
[mbs337-vm]$ ls -la
total 12
drwxrwxr-x  3 ubuntu ubuntu 4096 Mar  9 18:05 .
drwxrwxr-x 12 ubuntu ubuntu 4096 Mar  9 18:05 ..
drwxrwxr-x  5 ubuntu ubuntu 4096 Mar  9 18:05 .venv

Next, we will install the necessary dependencies for our dashboard. To start, we will need Dash, Plotly, Biopython, dash-bootstrap-components, and dash-bio. Instead of installing these packages one by one, we will create a requirements.txt file that lists all of our dependencies. This way, we can easily install them all at once and keep track of our dependencies.

(.venv) [mbs337-vm]$ touch requirements.txt
(.venv) [mbs337-vm]$ echo "dash" >> requirements.txt
(.venv) [mbs337-vm]$ echo "biopython" >> requirements.txt
(.venv) [mbs337-vm]$ echo "dash-bootstrap-components" >> requirements.txt
(.venv) [mbs337-vm]$ echo "dash-bio" >> requirements.txt
(.venv) [mbs337-vm]$ cat requirements.txt
dash
biopython
dash-bootstrap-components
dash-bio

Now that we have our requirements.txt file, we can install all of our dependencies at once using pip.

(.venv) [mbs337-vm]$ pip install -r requirements.txt
(.venv) [mbs337-vm]$ pip list
Package                   Version
------------------------- -----------
attrs                     25.4.0
biopython                 1.86
blinker                   1.9.0
certifi                   2026.2.25
charset-normalizer        3.4.5
click                     8.3.1
colour                    0.1.5
dash                      4.0.0
dash_bio                  1.0.2
dash-bootstrap-components 2.0.4
Flask                     3.1.3
GEOparse                  2.0.4
idna                      3.11
importlib_metadata        8.7.1
itsdangerous              2.2.0
Jinja2                    3.1.6
joblib                    1.5.3
jsonschema                4.26.0
jsonschema-specifications 2025.9.1
MarkupSafe                3.0.3
narwhals                  2.17.0
nest-asyncio              1.6.0
numpy                     2.4.3
packaging                 26.0
pandas                    3.0.1
ParmEd                    4.3.1
periodictable             2.1.0
pip                       24.0
plotly                    6.6.0
pyparsing                 3.3.2
python-dateutil           2.9.0.post0
referencing               0.37.0
requests                  2.32.5
retrying                  1.4.2
rpds-py                   0.30.0
scikit-learn              1.8.0
scipy                     1.17.1
setuptools                82.0.1
six                       1.17.0
threadpoolctl             3.6.0
tqdm                      4.67.3
typing_extensions         4.15.0
urllib3                   2.6.3
Werkzeug                  3.1.6
zipp                      3.23.0

Building the Basic Dashboard

Now that we have our environment set up and our dependencies installed, we can start building our dashboard. The idea for the initial version of our dashboard is to have a simple input field where the user can enter a PDB ID, and when they submit the form by clicking a button, the dashboard will fetch the corresponding PDB file from RCS PDB, and display a 3D molecular view of the structure.

../_images/basic-pdb-dashboard-app.png

Basic PDB Dashboard app layout.

OK, now that we have the look of our dashboard in mind, let’s start building it. We will start by creating a new file called app.py in our pdb-dashboard directory. This file will contain the code for our Dash application.

(.venv) [mbs337-vm]$ touch app.py
(.venv) [mbs337-vm]$ ls -l
total 44
-rw-rw-r-- 1 ubuntu ubuntu     0 Mar 10 00:02 app.py
-rw-rw-r-- 1 ubuntu ubuntu    50 Mar  9 18:13 requirements.txt

Imports

Next, we will start by importing the necessary libraries and setting up the basic structure of our Dash application. We can take a look at the documentation for the Dash Bio library, specifically the Molecule3dViewer component, to see how it functions and what kind of data it expects. We will also need to import the necessary components from Dash, Dash Bootstrap Components, and Biopython to build our dashboard. Let’s start by editing our app.py file and adding the necessary imports.

import os

import dash_bio as dashbio
import dash_bootstrap_components as dbc
from Bio.PDB import PDBList
from dash import Dash, Input, Output, State, callback, html
from dash_bio.utils import PdbParser as DashPdbParser
from dash_bio.utils import create_mol3d_style

Most of these imports should look familiar to you from previous sections, but there are a few new ones that we haven’t seen before. We import dash_bio as dashbio to access the Dash Bio library and its components. We also import the PdbParser and create_mol3d_style utilities from dash_bio.utils to help us parse PDB files and create styles for our molecular viewer. We’ll talk more about these utilities later when we get to the callback function that loads the molecule and updates the viewer.

App Initialization

The next step is to initialize the application and set up its theme. We will use the BOOTSTRAP_CERULEAN theme from Dash Bootstrap Components to give our dashboard a nice blue look.

# Initialize the Dash app
external_stylesheets = [dbc.themes.CERULEAN]
app = Dash(__name__, external_stylesheets=external_stylesheets)

Layout

Now that we have our app initialized, we can start building the layout of our dashboard. We will use a combination of Dash HTML components and Dash Bootstrap Components to create a simple and clean layout for our dashboard. We will use a dbc.Container to hold all of our components, and inside that container, we will have a dbc.Row for the title of our dashboard, another dbc.Row that will contain a dbc.Col component for the input field, submit button, and status message, and a final dbc.Col for the 3D molecular viewer. Let’s add the layout code to our app.py file.

# 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=10),
    ]),
])

Note that all of our components have ids (id=) assigned to them. This is important because we will use these ids to create a callback that will allow us to update the content of our dashboard based on user input. The components also have some additional styling classes assigned to them using the className attribute. These classes are provided by the Bootstrap theme we are using and help to give our dashboard a nice look and feel. For example, the title has the classes text-primary, text-center, and fs-3 to make it blue, centered, and larger in size. The input field has the class mb-2 to give it some margin at the bottom, and the status message has the class mt-3 to give it some margin at the top. The initial message in the molecule viewer has the classes text-center, text-muted, and mt-5 to center it, make it gray, and give it some margin at the top. You can experiment with different Bootstrap classes to customize the look of your dashboard further. For more information on available Bootstrap classes, you can refer to the Bootstrap Documentation or a site like the Bootstrap Cheat Sheet.

Callback Function

Now that we have our layout set up, we need to create a callback function that will allow us to update the content of our dashboard based on user input. Specifically, we want to update the 3D molecular viewer when the user enters a PDB ID and clicks the Load Structure button. To do this, we will create a callback function that listens for clicks on the Load Structure button, and when it detects a click, it will fetch the corresponding PDB file from RCSB Protein Data Bank. We will use Biopython’s PDBList class to download the PDB file, and then we will use Dash Bio’s PdbParser to parse the PDB file and extract the necessary information to create a 3D molecular viewer. Also, we will use a helper function called create_mol3d_style from Dash Bio to create a style for our molecular viewer.

# create styles for visualization needed by Molecule3dViewer
# atoms is a list of dictionaries obtained from parsing the PDB file with DashPdbParser
# visualization_type can be 'cartoon', 'stick', 'sphere'
# color_element can be 'residue', 'chain', 'element', 'partialCharge'
create_mol3d_style(atoms, visualization_type='cartoon', color_element='residue')

Finally, we will update the content of the molecule viewer with the new 3D visualization. Let’s add the callback function to our app.py file.

# Callback to load and display molecule
@callback(
    [Output('molecule-viewer', 'children'),
    Output('status-message', 'children')],
    Input('load-button', 'n_clicks'),
    State('pdb-input', 'value'),
    prevent_initial_call=True
)
def load_molecule(load_clicks, pdb_id):

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

    try:
        # Clean up PDB ID (remove whitespace, convert to lowercase)
        pdb_id = pdb_id.strip().lower()

        # Create PDB directory if it doesn't exist
        pdb_dir = './pdb_files'
        os.makedirs(pdb_dir, exist_ok=True)

        # Download PDB file using BioPython
        pdbl = PDBList()
        pdb_file = pdbl.retrieve_pdb_file(pdb_id, pdir=pdb_dir, file_format='pdb')

        # Read PDB file content for visualization
        dash_parser = DashPdbParser(pdb_file)
        pdb_data = dash_parser.mol3d_data()  # Get data in format suitable for Molecule3dViewer
        # create styles for visualization needed by Molecule3dViewer
        # atoms is a list of dictionaries obtained from parsing the PDB file with DashPdbParser
        # visualization_type can be 'cartoon', 'stick', 'sphere'
        # color_element can be 'residue', 'chain', 'element', 'partialCharge'
        styles = create_mol3d_style(
            pdb_data['atoms'], visualization_type='cartoon', color_element='residue'
        )

        # Create Molecule3dViewer component
        viewer = create_molecule_viewer(pdb_data, styles)

        status = dbc.Alert(
            f"Successfully loaded PDB ID: {pdb_id.upper()}",
            color="success"
        )

        return viewer, status

    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"
        )
        return empty_viewer, error_msg

OK, let’s break down what this callback function is doing. We have two outputs: one for the content of the molecule viewer and one for the status message. The input is the number of clicks on the Load Structure button, and we also have a state for the value of the PDB ID input field. We set prevent_initial_call=True to prevent the callback from being triggered when the app first loads.

Note

What is State in Dash?

In Dash, State is used to pass the current value of a component to a callback function without triggering the callback when that value changes. It allows you to access the current state of a component at the time the callback is triggered by an Input. This is useful for cases where you want to use the current value of an input field or other component without having the callback execute every time that value changes. See the Dash documentation on Basic Callbacks for more information.

The load_molecule function starts by checking if a PDB ID was entered. If not, it returns a message asking the user to enter a valid PDB ID and a warning alert. If a PDB ID is provided, it proceeds to clean up the input by stripping whitespace and converting it to lowercase. Then, it creates a directory called pdb_files if it doesn’t already exist to store the downloaded PDB files. Next, it uses Biopython’s PDBList class to download the PDB file corresponding to the entered PDB ID.

Note

Downloading PDB Files with Biopython

Biopython’s PDBList.retrieve_pdb_file method will first check if the requested PDB file already exists in the specified directory. If it does, it will return the path to the existing file without downloading it again.

After downloading the file, it uses Dash Bio’s PdbParser to parse the PDB file and extract the necessary data for visualization. It then creates styles for the molecular viewer using the create_mol3d_style helper function described above. Finally, it creates a Molecule3dViewer component with the parsed data and styles, and returns it along with a success status message. If any errors occur during this process (e.g., invalid PDB ID, network issues), it catches the exception and returns an error message and an empty viewer.

Helper Function for Molecule Viewer

To keep our code organized and modular, we will create a helper function called create_molecule_viewer that takes the parsed PDB data and styles as input and returns a Molecule3dViewer component. This will help us keep our callback function clean and focused on the logic of loading the molecule, while the helper function will handle the specifics of creating the viewer component. Let’s add this helper function to our app.py file.

def create_molecule_viewer(pdb_data, styles):
    """Create a Molecule3dViewer from PDB data"""
    return dashbio.Molecule3dViewer(
        id='molecule-3d',
        modelData=pdb_data,
        styles=styles,
        selectionType='atom',
        backgroundColor='#F0F0F0',
        height=600,
        width='100%'
    )

See the Dash Bio documentation for Molecule3dViewer for more information on the available properties and customization options for the Molecule3dViewer component.

Running the App

Now that we have our app set up with the layout and callback function, we can add the last bit of code to run the app. At the bottom of our app.py file, we will add the following code to start the Dash server.

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8050, debug=True)

This is exactly the same code we used in the Introduction to Dash section to run our app.

Finally, putting it all together, our complete 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
  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=10),
 40    ]),
 41])
 42
 43# Callback to load and display molecule
 44@callback(
 45    [Output('molecule-viewer', 'children'),
 46    Output('status-message', 'children')],
 47    Input('load-button', 'n_clicks'),
 48    State('pdb-input', 'value'),
 49    prevent_initial_call=True
 50)
 51def load_molecule(load_clicks, pdb_id):
 52
 53    if not pdb_id:
 54        return (
 55            html.Div("Please enter a valid PDB ID.", className="text-center text-muted mt-5"),
 56            dbc.Alert("Please enter a PDB ID.", color="warning")
 57        )
 58
 59    try:
 60        # Clean up PDB ID (remove whitespace, convert to lowercase)
 61        pdb_id = pdb_id.strip().lower()
 62
 63        # Create PDB directory if it doesn't exist
 64        pdb_dir = './pdb_files'
 65        os.makedirs(pdb_dir, exist_ok=True)
 66
 67        # Download PDB file using BioPython
 68        pdbl = PDBList()
 69        pdb_file = pdbl.retrieve_pdb_file(pdb_id, pdir=pdb_dir, file_format='pdb')
 70
 71        # Read PDB file content for visualization
 72        dash_parser = DashPdbParser(pdb_file)
 73        pdb_data = dash_parser.mol3d_data()  # Get data in format suitable for Molecule3dViewer
 74        # create styles for visualization needed by Molecule3dViewer
 75        # atoms is a list of dictionaries obtained from parsing the PDB file with DashPdbParser
 76        # visualization_type can be 'cartoon', 'stick', 'sphere'
 77        # color_element can be 'residue', 'chain', 'element', 'partialCharge'
 78        styles = create_mol3d_style(
 79            pdb_data['atoms'], visualization_type='cartoon', color_element='residue'
 80        )
 81
 82        # Create Molecule3dViewer component
 83        viewer = create_molecule_viewer(pdb_data, styles)
 84
 85        status = dbc.Alert(
 86            f"Successfully loaded PDB ID: {pdb_id.upper()}",
 87            color="success"
 88        )
 89
 90        return viewer, status
 91
 92    except Exception as e:
 93        error_msg = dbc.Alert(
 94            f"Error loading PDB {pdb_id.upper()}: {str(e)}",
 95            color="danger"
 96        )
 97        empty_viewer = html.Div(
 98            "Failed to load molecule. Please check the PDB ID and try again.",
 99            className="text-center text-muted mt-5"
100        )
101        return empty_viewer, error_msg
102
103def create_molecule_viewer(pdb_data, styles):
104    """Create a Molecule3dViewer from PDB data"""
105    return dashbio.Molecule3dViewer(
106        id='molecule-3d',
107        modelData=pdb_data,
108        styles=styles,
109        selectionType='atom',
110        backgroundColor='#F0F0F0',
111        height=600,
112        width='100%'
113    )
114
115# Run the app
116if __name__ == "__main__":
117    app.run(host='0.0.0.0', port=8050, debug=True)

To run the app, simply execute the following command in your VS Code terminal:

(.venv) [mbs337-vm]$ curl ip.me
129.114.38.51
(.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 open a web browser and navigate to http://<IP_ADDRESS>:8050/ (replacing <IP_ADDRESS> with the actual IP address of your Linux VM) to see our PDB dashboard application in action.

../_images/pdb-dashboard-loaded-structure.png

PDB dashboard application running in a web browser.

Additional Resources