diff --git a/apps/dash-object-detection/requirements.txt b/apps/dash-object-detection/requirements.txt
index 5b3ece908..411e1d032 100644
--- a/apps/dash-object-detection/requirements.txt
+++ b/apps/dash-object-detection/requirements.txt
@@ -1,9 +1,9 @@
 dash==1.0.0
 gunicorn==19.9.0
 pillow==8.2.0
-scipy==1.2.1
-numpy==1.16.1
-pandas==0.24.1
-Flask==1.0.1
+scipy==1.8.0
+numpy==1.22.3
+pandas==1.4.2
+Flask==2.1.2
 dash-player==0.0.1
 pathlib==1.0.1
diff --git a/apps/dash-opioid-epidemic/.gitignore b/apps/dash-opioid-epidemic/.gitignore
new file mode 100644
index 000000000..90ecc9b06
--- /dev/null
+++ b/apps/dash-opioid-epidemic/.gitignore
@@ -0,0 +1,191 @@
+# .gitignore specifies the files that shouldn't be included
+# in version control and therefore shouldn't be included when
+# deploying an application to Dash Enterprise
+# This is a very exhaustive list!
+# This list was based off of https://github.com/github/gitignore
+
+# Ignore data that is generated during the runtime of an application
+# This folder is used by the "Large Data" sample applications
+runtime_data/
+data/
+
+# Omit SQLite databases that may be produced by dash-snapshots in development
+*.db
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+
+# Jupyter Notebook
+
+.ipynb_checkpoints
+*/.ipynb_checkpoints/*
+
+# IPython
+profile_default/
+ipython_config.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+
+# macOS General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# History files
+.Rhistory
+.Rapp.history
+
+# Session Data files
+.RData
+
+# User-specific files
+.Ruserdata
+
+# Example code in package build process
+*-Ex.R
+
+# Output files from R CMD check
+/*.Rcheck/
+
+# RStudio files
+.Rproj.user/
+
+# produced vignettes
+vignettes/*.html
+vignettes/*.pdf
+
+# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3
+.httr-oauth
+
+# knitr and R markdown default cache directories
+*_cache/
+/cache/
+
+# Temporary files created by R markdown
+*.utf8.md
+*.knit.md
+
+# R Environment Variables
+.Renviron
+
+# Linux
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+# VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# SublineText
+# Cache files for Sublime Text
+*.tmlanguage.cache
+*.tmPreferences.cache
+*.stTheme.cache
+
+# Workspace files are user-specific
+*.sublime-workspace
+
+# Project files should be checked into the repository, unless a significant
+# proportion of contributors will probably not be using Sublime Text
+# *.sublime-project
+
+# SFTP configuration file
+sftp-config.json
+
+# Package control specific files
+Package Control.last-run
+Package Control.ca-list
+Package Control.ca-bundle
+Package Control.system-ca-bundle
+Package Control.cache/
+Package Control.ca-certs/
+Package Control.merged-ca-bundle
+Package Control.user-ca-bundle
+oscrypto-ca-bundle.crt
+bh_unicode_properties.cache
+
+# Sublime-github package stores a github token in this file
+# https://packagecontrol.io/packages/sublime-github
+GitHub.sublime-settings 
\ No newline at end of file
diff --git a/apps/dash-opioid-epidemic/README.md b/apps/dash-opioid-epidemic/README.md
index 203145788..b47a7a257 100644
--- a/apps/dash-opioid-epidemic/README.md
+++ b/apps/dash-opioid-epidemic/README.md
@@ -1,8 +1,8 @@
 # US opioid epidemic dataset and Dash app
 
-
+
 
-Poison induced death data was downloaded from [CDC Wonder](dash_app_screencast.gif), using cause-of-death codes X40–X44 (unintentional), X60–X64 (suicide), X85 (homicide), or Y10–Y14 (undetermined intent).
+Poison induced death data was downloaded from [CDC Wonder](https://wonder.cdc.gov/), using cause-of-death codes X40–X44 (unintentional), X60–X64 (suicide), X85 (homicide), or Y10–Y14 (undetermined intent).
 
 [View the Dash app](https://dash-gallery.plotly.host/dash-opioid-epidemic/)
 
diff --git a/apps/dash-opioid-epidemic/app.py b/apps/dash-opioid-epidemic/app.py
index 32d66ef11..dc4ecf1f5 100644
--- a/apps/dash-opioid-epidemic/app.py
+++ b/apps/dash-opioid-epidemic/app.py
@@ -1,454 +1,59 @@
-import os
-import pathlib
-import re
+from dash import dash, html, Input, Output, State, callback
+import dash_bootstrap_components as dbc
 
-import dash
-import dash_core_components as dcc
-import dash_html_components as html
-import pandas as pd
-from dash.dependencies import Input, Output, State
-import cufflinks as cf
+import utils.figures as figs
+from utils.components import header, choropleth_card, slider_graph_card
 
-# Initialize app
-
-app = dash.Dash(
-    __name__,
-    meta_tags=[
-        {"name": "viewport", "content": "width=device-width, initial-scale=1.0"}
-    ],
-)
-app.title = "US Opioid Epidemic"
+app = dash.Dash(__name__, title = "US Opioid Epidemic", external_stylesheets=[dbc.themes.BOOTSTRAP])
 server = app.server
 
-# Load data
-
-APP_PATH = str(pathlib.Path(__file__).parent.resolve())
-
-df_lat_lon = pd.read_csv(
-    os.path.join(APP_PATH, os.path.join("data", "lat_lon_counties.csv"))
-)
-df_lat_lon["FIPS "] = df_lat_lon["FIPS "].apply(lambda x: str(x).zfill(5))
-
-df_full_data = pd.read_csv(
-    os.path.join(
-        APP_PATH, os.path.join("data", "age_adjusted_death_rate_no_quotes.csv")
-    )
-)
-df_full_data["County Code"] = df_full_data["County Code"].apply(
-    lambda x: str(x).zfill(5)
-)
-df_full_data["County"] = (
-    df_full_data["Unnamed: 0"] + ", " + df_full_data.County.map(str)
-)
-
-YEARS = [2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015]
-
-BINS = [
-    "0-2",
-    "2.1-4",
-    "4.1-6",
-    "6.1-8",
-    "8.1-10",
-    "10.1-12",
-    "12.1-14",
-    "14.1-16",
-    "16.1-18",
-    "18.1-20",
-    "20.1-22",
-    "22.1-24",
-    "24.1-26",
-    "26.1-28",
-    "28.1-30",
-    ">30",
-]
-
-DEFAULT_COLORSCALE = [
-    "#f2fffb",
-    "#bbffeb",
-    "#98ffe0",
-    "#79ffd6",
-    "#6df0c8",
-    "#69e7c0",
-    "#59dab2",
-    "#45d0a5",
-    "#31c194",
-    "#2bb489",
-    "#25a27b",
-    "#1e906d",
-    "#188463",
-    "#157658",
-    "#11684d",
-    "#10523e",
-]
-
-DEFAULT_OPACITY = 0.8
-
-mapbox_access_token = "pk.eyJ1IjoicGxvdGx5bWFwYm94IiwiYSI6ImNrOWJqb2F4djBnMjEzbG50amg0dnJieG4ifQ.Zme1-Uzoi75IaFbieBDl3A"
-mapbox_style = "mapbox://styles/plotlymapbox/cjvprkf3t1kns1cqjxuxmwixz"
-
 # App layout
-
-app.layout = html.Div(
-    id="root",
-    children=[
-        html.Div(
-            id="header",
-            children=[
-                html.A(
-                    html.Img(id="logo", src=app.get_asset_url("dash-logo.png")),
-                    href="https://plotly.com/dash/",
-                ),
-                html.A(
-                    html.Button("Enterprise Demo", className="link-button"),
-                    href="https://plotly.com/get-demo/",
-                ),
-                html.A(
-                    html.Button("Source Code", className="link-button"),
-                    href="https://github.com/plotly/dash-sample-apps/tree/main/apps/dash-opioid-epidemic",
-                ),
-                html.H4(children="Rate of US Poison-Induced Deaths"),
-                html.P(
-                    id="description",
-                    children="† Deaths are classified using the International Classification of Diseases, \
-                    Tenth Revision (ICD–10). Drug-poisoning deaths are defined as having ICD–10 underlying \
-                    cause-of-death codes X40–X44 (unintentional), X60–X64 (suicide), X85 (homicide), or Y10–Y14 \
+app.layout = dbc.Container(
+    [
+        header(
+            app,
+            "inherit",
+            "Rate of US Poison-Induced Deaths",
+            subheader="† Deaths are classified using the International Classification of Diseases, \
+                    Tenth Revision (ICD-10).\n\nDrug-poisoning deaths are defined as having ICD-10 underlying \
+                    cause-of-death codes X40-X44 (unintentional), X60-X64 (suicide), X85 (homicide), or Y10-Y14 \
                     (undetermined intent).",
-                ),
-            ],
         ),
-        html.Div(
-            id="app-container",
-            children=[
-                html.Div(
-                    id="left-column",
-                    children=[
-                        html.Div(
-                            id="slider-container",
-                            children=[
-                                html.P(
-                                    id="slider-text",
-                                    children="Drag the slider to change the year:",
-                                ),
-                                dcc.Slider(
-                                    id="years-slider",
-                                    min=min(YEARS),
-                                    max=max(YEARS),
-                                    value=min(YEARS),
-                                    marks={
-                                        str(year): {
-                                            "label": str(year),
-                                            "style": {"color": "#7fafdf"},
-                                        }
-                                        for year in YEARS
-                                    },
-                                ),
-                            ],
-                        ),
-                        html.Div(
-                            id="heatmap-container",
-                            children=[
-                                html.P(
-                                    "Heatmap of age adjusted mortality rates \
-                            from poisonings in year {0}".format(
-                                        min(YEARS)
-                                    ),
-                                    id="heatmap-title",
-                                ),
-                                dcc.Graph(
-                                    id="county-choropleth",
-                                    figure=dict(
-                                        layout=dict(
-                                            mapbox=dict(
-                                                layers=[],
-                                                accesstoken=mapbox_access_token,
-                                                style=mapbox_style,
-                                                center=dict(
-                                                    lat=38.72490, lon=-95.61446
-                                                ),
-                                                pitch=0,
-                                                zoom=3.5,
-                                            ),
-                                            autosize=True,
-                                        ),
-                                    ),
-                                ),
-                            ],
-                        ),
-                    ],
-                ),
-                html.Div(
-                    id="graph-container",
-                    children=[
-                        html.P(id="chart-selector", children="Select chart:"),
-                        dcc.Dropdown(
-                            options=[
-                                {
-                                    "label": "Histogram of total number of deaths (single year)",
-                                    "value": "show_absolute_deaths_single_year",
-                                },
-                                {
-                                    "label": "Histogram of total number of deaths (1999-2016)",
-                                    "value": "absolute_deaths_all_time",
-                                },
-                                {
-                                    "label": "Age-adjusted death rate (single year)",
-                                    "value": "show_death_rate_single_year",
-                                },
-                                {
-                                    "label": "Trends in age-adjusted death rate (1999-2016)",
-                                    "value": "death_rate_all_time",
-                                },
-                            ],
-                            value="show_death_rate_single_year",
-                            id="chart-dropdown",
-                        ),
-                        dcc.Graph(
-                            id="selected-data",
-                            figure=dict(
-                                data=[dict(x=0, y=0)],
-                                layout=dict(
-                                    paper_bgcolor="#F4F4F8",
-                                    plot_bgcolor="#F4F4F8",
-                                    autofill=True,
-                                    margin=dict(t=75, r=50, b=100, l=50),
-                                ),
-                            ),
-                        ),
-                    ],
-                ),
+        dbc.Row([
+                dbc.Col(choropleth_card("county-choropleth"), width=7),
+                dbc.Col(slider_graph_card("selected-data"),width=5)
             ],
         ),
     ],
+    fluid=True
 )
 
 
-@app.callback(
+@callback(
     Output("county-choropleth", "figure"),
-    [Input("years-slider", "value")],
-    [State("county-choropleth", "figure")],
+    Input("years-slider", "value"),
+    State("county-choropleth", "figure"),
 )
-def display_map(year, figure):
-    cm = dict(zip(BINS, DEFAULT_COLORSCALE))
-
-    data = [
-        dict(
-            lat=df_lat_lon["Latitude "],
-            lon=df_lat_lon["Longitude"],
-            text=df_lat_lon["Hover"],
-            type="scattermapbox",
-            hoverinfo="text",
-            marker=dict(size=5, color="white", opacity=0),
-        )
-    ]
-
-    annotations = [
-        dict(
-            showarrow=False,
-            align="right",
-            text="Age-adjusted death rate
per county per year",
-            font=dict(color="#2cfec1"),
-            bgcolor="#1f2630",
-            x=0.95,
-            y=0.95,
-        )
-    ]
-
-    for i, bin in enumerate(reversed(BINS)):
-        color = cm[bin]
-        annotations.append(
-            dict(
-                arrowcolor=color,
-                text=bin,
-                x=0.95,
-                y=0.85 - (i / 20),
-                ax=-60,
-                ay=0,
-                arrowwidth=5,
-                arrowhead=0,
-                bgcolor="#1f2630",
-                font=dict(color="#2cfec1"),
-            )
-        )
+def return_display_map(year, figure):
+    return figs.display_map(year, figure)
 
-    if "layout" in figure:
-        lat = figure["layout"]["mapbox"]["center"]["lat"]
-        lon = figure["layout"]["mapbox"]["center"]["lon"]
-        zoom = figure["layout"]["mapbox"]["zoom"]
-    else:
-        lat = 38.72490
-        lon = -95.61446
-        zoom = 3.5
 
-    layout = dict(
-        mapbox=dict(
-            layers=[],
-            accesstoken=mapbox_access_token,
-            style=mapbox_style,
-            center=dict(lat=lat, lon=lon),
-            zoom=zoom,
-        ),
-        hovermode="closest",
-        margin=dict(r=0, l=0, t=0, b=0),
-        annotations=annotations,
-        dragmode="lasso",
-    )
-
-    base_url = "https://raw.githubusercontent.com/jackparmer/mapbox-counties/master/"
-    for bin in BINS:
-        geo_layer = dict(
-            sourcetype="geojson",
-            source=base_url + str(year) + "/" + bin + ".geojson",
-            type="fill",
-            color=cm[bin],
-            opacity=DEFAULT_OPACITY,
-            # CHANGE THIS
-            fill=dict(outlinecolor="#afafaf"),
-        )
-        layout["mapbox"]["layers"].append(geo_layer)
-
-    fig = dict(data=data, layout=layout)
-    return fig
-
-
-@app.callback(Output("heatmap-title", "children"), [Input("years-slider", "value")])
+@callback(
+    Output("heatmap-title", "children"), 
+    Input("years-slider", "value")
+)
 def update_map_title(year):
-    return "Heatmap of age adjusted mortality rates \
-				from poisonings in year {0}".format(
-        year
-    )
+    return f"Heatmap of age adjusted mortality rates from poisonings in year {year}"
 
 
-@app.callback(
+@callback(
     Output("selected-data", "figure"),
-    [
-        Input("county-choropleth", "selectedData"),
-        Input("chart-dropdown", "value"),
-        Input("years-slider", "value"),
-    ],
+    Input("county-choropleth", "selectedData"),
+    Input("chart-dropdown", "value"),
+    Input("years-slider", "value"),
 )
-def display_selected_data(selectedData, chart_dropdown, year):
-    if selectedData is None:
-        return dict(
-            data=[dict(x=0, y=0)],
-            layout=dict(
-                title="Click-drag on the map to select counties",
-                paper_bgcolor="#1f2630",
-                plot_bgcolor="#1f2630",
-                font=dict(color="#2cfec1"),
-                margin=dict(t=75, r=50, b=100, l=75),
-            ),
-        )
-    pts = selectedData["points"]
-    fips = [str(pt["text"].split("
")[-1]) for pt in pts]
-    for i in range(len(fips)):
-        if len(fips[i]) == 4:
-            fips[i] = "0" + fips[i]
-    dff = df_full_data[df_full_data["County Code"].isin(fips)]
-    dff = dff.sort_values("Year")
-
-    regex_pat = re.compile(r"Unreliable", flags=re.IGNORECASE)
-    dff["Age Adjusted Rate"] = dff["Age Adjusted Rate"].replace(regex_pat, 0)
-
-    if chart_dropdown != "death_rate_all_time":
-        title = "Absolute deaths per county, 1999-2016"
-        AGGREGATE_BY = "Deaths"
-        if "show_absolute_deaths_single_year" == chart_dropdown:
-            dff = dff[dff.Year == year]
-            title = "Absolute deaths per county, {0}".format(year)
-        elif "show_death_rate_single_year" == chart_dropdown:
-            dff = dff[dff.Year == year]
-            title = "Age-adjusted death rate per county, {0}".format(year)
-            AGGREGATE_BY = "Age Adjusted Rate"
-
-        dff[AGGREGATE_BY] = pd.to_numeric(dff[AGGREGATE_BY], errors="coerce")
-        deaths_or_rate_by_fips = dff.groupby("County")[AGGREGATE_BY].sum()
-        deaths_or_rate_by_fips = deaths_or_rate_by_fips.sort_values()
-        # Only look at non-zero rows:
-        deaths_or_rate_by_fips = deaths_or_rate_by_fips[deaths_or_rate_by_fips > 0]
-        fig = deaths_or_rate_by_fips.iplot(
-            kind="bar", y=AGGREGATE_BY, title=title, asFigure=True
-        )
-
-        fig_layout = fig["layout"]
-        fig_data = fig["data"]
-
-        fig_data[0]["text"] = deaths_or_rate_by_fips.values.tolist()
-        fig_data[0]["marker"]["color"] = "#2cfec1"
-        fig_data[0]["marker"]["opacity"] = 1
-        fig_data[0]["marker"]["line"]["width"] = 0
-        fig_data[0]["textposition"] = "outside"
-        fig_layout["paper_bgcolor"] = "#1f2630"
-        fig_layout["plot_bgcolor"] = "#1f2630"
-        fig_layout["font"]["color"] = "#2cfec1"
-        fig_layout["title"]["font"]["color"] = "#2cfec1"
-        fig_layout["xaxis"]["tickfont"]["color"] = "#2cfec1"
-        fig_layout["yaxis"]["tickfont"]["color"] = "#2cfec1"
-        fig_layout["xaxis"]["gridcolor"] = "#5b5b5b"
-        fig_layout["yaxis"]["gridcolor"] = "#5b5b5b"
-        fig_layout["margin"]["t"] = 75
-        fig_layout["margin"]["r"] = 50
-        fig_layout["margin"]["b"] = 100
-        fig_layout["margin"]["l"] = 50
-
-        return fig
-
-    fig = dff.iplot(
-        kind="area",
-        x="Year",
-        y="Age Adjusted Rate",
-        text="County",
-        categories="County",
-        colors=[
-            "#1b9e77",
-            "#d95f02",
-            "#7570b3",
-            "#e7298a",
-            "#66a61e",
-            "#e6ab02",
-            "#a6761d",
-            "#666666",
-            "#1b9e77",
-        ],
-        vline=[year],
-        asFigure=True,
-    )
-
-    for i, trace in enumerate(fig["data"]):
-        trace["mode"] = "lines+markers"
-        trace["marker"]["size"] = 4
-        trace["marker"]["line"]["width"] = 1
-        trace["type"] = "scatter"
-        for prop in trace:
-            fig["data"][i][prop] = trace[prop]
-
-    # Only show first 500 lines
-    fig["data"] = fig["data"][0:500]
-
-    fig_layout = fig["layout"]
-
-    # See plot.ly/python/reference
-    fig_layout["yaxis"]["title"] = "Age-adjusted death rate per county per year"
-    fig_layout["xaxis"]["title"] = ""
-    fig_layout["yaxis"]["fixedrange"] = True
-    fig_layout["xaxis"]["fixedrange"] = False
-    fig_layout["hovermode"] = "closest"
-    fig_layout["title"] = "{0} counties selected".format(len(fips))
-    fig_layout["legend"] = dict(orientation="v")
-    fig_layout["autosize"] = True
-    fig_layout["paper_bgcolor"] = "#1f2630"
-    fig_layout["plot_bgcolor"] = "#1f2630"
-    fig_layout["font"]["color"] = "#2cfec1"
-    fig_layout["xaxis"]["tickfont"]["color"] = "#2cfec1"
-    fig_layout["yaxis"]["tickfont"]["color"] = "#2cfec1"
-    fig_layout["xaxis"]["gridcolor"] = "#5b5b5b"
-    fig_layout["yaxis"]["gridcolor"] = "#5b5b5b"
-
-    if len(fips) > 500:
-        fig["layout"][
-            "title"
-        ] = "Age-adjusted death rate per county per year 
(only 1st 500 shown)"
-
-    return fig
+def return_display_selected_data(selectedData, chart_dropdown, year):
+    return figs.display_selected_data(selectedData, chart_dropdown, year)
 
 
 if __name__ == "__main__":
diff --git a/apps/dash-opioid-epidemic/assets/css/app.css b/apps/dash-opioid-epidemic/assets/css/app.css
new file mode 100644
index 000000000..8aee67e8d
--- /dev/null
+++ b/apps/dash-opioid-epidemic/assets/css/app.css
@@ -0,0 +1,124 @@
+
+body {
+    font-size: 1.5rem; /* currently ems cause chrome bug misinterpreting rems on body element */
+    line-height: 1.6;
+    font-weight: 400;
+    font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
+    color: #7fafdf;
+    margin: 0;
+    background-color: #1f2630;
+    padding: 10px;
+
+}
+
+
+.card {
+    background-color: #262D3D;
+    padding: 20px 30px;
+}
+
+
+
+/* Dropdown
+–––––––––––––––––––––––––––––––––––––––––––––––––– */
+.Select-value {
+    cursor: pointer;
+}
+
+.Select-control {
+    color: #7fafdf !important;
+}
+
+.Select {
+    color: #7fafdf !important;
+}
+
+.Select-menu-outer {
+    background-color: #252e3f !important;
+    border: 1px solid #7fafdf !important;
+}
+
+.Select div {
+    background-color: #252e3f !important;
+}
+
+.Select-menu-outer div:hover {
+    background-color: rgba(255, 255, 255, 0.01) !important;
+    cursor: pointer;
+}
+
+.Select-value-label {
+    color: #7fafdf !important;
+}
+
+.Select--single > .Select-control .Select-value, .Select-placeholder {
+    border: 1px solid #7fafdf !important;
+    border-radius: 4px !important;
+}
+
+/* Header */
+.header {
+    display: flex;
+    padding-left: 2%;
+    padding-right: 2%;
+    padding-bottom: 1%;
+    font-family: playfair display, sans-serif;
+    font-weight: bold;
+}
+
+.header .header-title {
+    font-size: 5vh;
+}
+
+.subheader-title {
+    font-size: 1.5vh;
+    border-left: #2cfec1 solid 1rem;
+    padding-left: 1rem;
+    max-width: 60rem;
+    word-wrap: break-word;
+}
+
+.header-logos {
+    margin-left: auto;
+    align-self: center !important;
+}
+.header-logos img {
+    margin-left: 3vh !important;
+    max-height: 5vh;
+}
+
+/* Demo button css */
+.demo-button {
+    font-size: 1.5vh;
+    font-family: Open Sans, sans-serif;
+    text-decoration: none;
+    -webkit-align-items: center;
+    -webkit-box-align: center;
+    -ms-flex-align: center;
+    align-items: center;
+    border-radius: 8px;
+    font-weight: 700;
+    -webkit-padding-start: 1rem;
+    padding-inline-start: 1rem;
+    -webkit-padding-end: 1rem;
+    padding-inline-end: 1rem;
+    color: #ffffff;
+    letter-spacing: 1.5px;
+    border: solid 1.5px transparent;
+    box-shadow: 2px 1000px 1px #0c0c0c inset;
+    background-image: linear-gradient(135deg, #7A76FF, #7A76FF, #7FE4FF);
+    -webkit-background-size: 200% 100%;
+    background-size: 200% 100%;
+    -webkit-background-position: 99%;
+    background-position: 99%;
+    background-origin: border-box;
+    transition: all .4s ease-in-out;
+    padding-top: 1vh;
+    padding-bottom: 1vh;
+    vertical-align: super;
+}
+
+.demo-button:hover {
+    color: #7A76FF;
+    background-position: 0%;
+} 
\ No newline at end of file
diff --git a/apps/dash-opioid-epidemic/assets/dash-logo.png b/apps/dash-opioid-epidemic/assets/dash-logo.png
deleted file mode 100644
index 040cde174..000000000
Binary files a/apps/dash-opioid-epidemic/assets/dash-logo.png and /dev/null differ
diff --git a/apps/dash-opioid-epidemic/assets/demo-button.css b/apps/dash-opioid-epidemic/assets/demo-button.css
deleted file mode 100644
index 513c949c6..000000000
--- a/apps/dash-opioid-epidemic/assets/demo-button.css
+++ /dev/null
@@ -1,13 +0,0 @@
-.link-button {
-    margin-top: 3px;
-    margin-right: 10px;
-    vertical-align: top;
-    color: #7fafdf;
-    float: right;
-    border-color: #7fafdf;
-}
-
-.link-button:hover {
-    color: white;
-    border-color: white;
-}
\ No newline at end of file
diff --git a/apps/dash-opioid-epidemic/assets/app_screencast.gif b/apps/dash-opioid-epidemic/assets/github/app_screencast.gif
similarity index 100%
rename from apps/dash-opioid-epidemic/assets/app_screencast.gif
rename to apps/dash-opioid-epidemic/assets/github/app_screencast.gif
diff --git a/apps/dash-opioid-epidemic/assets/images/plotly-logo-dark-theme.png b/apps/dash-opioid-epidemic/assets/images/plotly-logo-dark-theme.png
new file mode 100644
index 000000000..984dd57ab
Binary files /dev/null and b/apps/dash-opioid-epidemic/assets/images/plotly-logo-dark-theme.png differ
diff --git a/apps/dash-opioid-epidemic/assets/opioid.css b/apps/dash-opioid-epidemic/assets/opioid.css
deleted file mode 100644
index ee03bf467..000000000
--- a/apps/dash-opioid-epidemic/assets/opioid.css
+++ /dev/null
@@ -1,860 +0,0 @@
-@import url('https://fonts.googleapis.com/css?family=Open+Sans');
-@import url('https://fonts.googleapis.com/css?family=Playfair+Display');
-
-/* Table of contents
-––––––––––––––––––––––––––––––––––––––––––––––––––
-- Plotly.js
-- Grid
-- Base Styles
-- Typography
-- Links
-- Buttons
-- Forms
-- Lists
-- Code
-- Tables
-- Spacing
-- Utilities
-- Clearing
-- Media Queries
-*/
-
-/* PLotly.js
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-/* plotly.js's modebar's z-index is 1001 by default
- * https://github.com/plotly/plotly.js/blob/7e4d8ab164258f6bd48be56589dacd9bdd7fded2/src/css/_modebar.scss#L5
- * In case a dropdown is above the graph, the dropdown's options
- * will be rendered below the modebar
- * Increase the select option's z-index
- */
-
-/* This was actually not quite right -
-   dropdowns were overlapping each other (edited October 26)
-
-.Select {
-    z-index: 1002;
-}*/
-
-/* Grid
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-.container {
-    position: relative;
-    width: 100%;
-    max-width: 960px;
-    margin: 0 auto;
-    padding: 0 20px;
-    box-sizing: border-box;
-}
-
-.column,
-.columns {
-    width: 100%;
-    float: left;
-    box-sizing: border-box;
-}
-
-/* For devices larger than 400px */
-@media (min-width: 400px) and (max-width: 549px) {
-    .container {
-        width: 85%;
-        padding: 0;
-    }
-}
-
-/* For devices larger than 550px */
-@media (min-width: 550px) {
-    .container {
-        width: 80%;
-    }
-
-    .column,
-    .columns {
-        margin-left: 4%;
-    }
-
-    .column:first-child,
-    .columns:first-child {
-        margin-left: 0;
-    }
-
-    .one.column,
-    .one.columns {
-        width: 4.66666666667%;
-    }
-
-    .two.columns {
-        width: 13.3333333333%;
-    }
-
-    .three.columns {
-        width: 22%;
-    }
-
-    .four.columns {
-        width: 30.6666666667%;
-    }
-
-    .five.columns {
-        width: 39.3333333333%;
-    }
-
-    .six.columns {
-        width: 48%;
-    }
-
-    .seven.columns {
-        width: 56.6666666667%;
-    }
-
-    .eight.columns {
-        width: 65.3333333333%;
-    }
-
-    .nine.columns {
-        width: 74.0%;
-    }
-
-    .ten.columns {
-        width: 82.6666666667%;
-    }
-
-    .eleven.columns {
-        width: 91.3333333333%;
-    }
-
-    .twelve.columns {
-        width: 100%;
-        margin-left: 0;
-    }
-
-    .one-third.column {
-        width: 30.6666666667%;
-    }
-
-    .two-thirds.column {
-        width: 65.3333333333%;
-    }
-
-    .one-half.column {
-        width: 48%;
-    }
-
-    /* Offsets */
-    .offset-by-one.column,
-    .offset-by-one.columns {
-        margin-left: 8.66666666667%;
-    }
-
-    .offset-by-two.column,
-    .offset-by-two.columns {
-        margin-left: 17.3333333333%;
-    }
-
-    .offset-by-three.column,
-    .offset-by-three.columns {
-        margin-left: 26%;
-    }
-
-    .offset-by-four.column,
-    .offset-by-four.columns {
-        margin-left: 34.6666666667%;
-    }
-
-    .offset-by-five.column,
-    .offset-by-five.columns {
-        margin-left: 43.3333333333%;
-    }
-
-    .offset-by-six.column,
-    .offset-by-six.columns {
-        margin-left: 52%;
-    }
-
-    .offset-by-seven.column,
-    .offset-by-seven.columns {
-        margin-left: 60.6666666667%;
-    }
-
-    .offset-by-eight.column,
-    .offset-by-eight.columns {
-        margin-left: 69.3333333333%;
-    }
-
-    .offset-by-nine.column,
-    .offset-by-nine.columns {
-        margin-left: 78.0%;
-    }
-
-    .offset-by-ten.column,
-    .offset-by-ten.columns {
-        margin-left: 86.6666666667%;
-    }
-
-    .offset-by-eleven.column,
-    .offset-by-eleven.columns {
-        margin-left: 95.3333333333%;
-    }
-
-    .offset-by-one-third.column,
-    .offset-by-one-third.columns {
-        margin-left: 34.6666666667%;
-    }
-
-    .offset-by-two-thirds.column,
-    .offset-by-two-thirds.columns {
-        margin-left: 69.3333333333%;
-    }
-
-    .offset-by-one-half.column,
-    .offset-by-one-half.columns {
-        margin-left: 52%;
-    }
-
-}
-
-
-/* Base Styles
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-/* NOTE
-html is set to 62.5% so that all the REM measurements throughout Skeleton
-are based on 10px sizing. So basically 1.5rem = 15px :) */
-html {
-    font-size: 50%;
-    background-color: #1f2630;
-    max-width: 100% !important;
-    width: 100% !important;
-    margin: 0;
-}
-
-body {
-    font-size: 1.5rem; /* currently ems cause chrome bug misinterpreting rems on body element */
-    line-height: 1.6;
-    font-weight: 400;
-    font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
-    color: #7fafdf;
-    margin: 0;
-}
-
-#root {
-    margin: 0;
-}
-
-@media (max-width: 550px) {
-    #root {
-        padding: 2rem;
-    }
-}
-
-@media (min-width: 551px) {
-    #root {
-        padding: 5rem;
-    }
-}
-
-
-/* Typography
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-h1, h2, h3, h4, h5, h6 {
-    margin-top: 0;
-    margin-bottom: 0;
-    font-weight: 300;
-}
-
-h1 {
-    font-size: 4.5rem;
-    line-height: 1.2;
-    letter-spacing: -.1rem;
-    margin-bottom: 2rem;
-}
-
-h2 {
-    font-size: 3.6rem;
-    line-height: 1.25;
-    letter-spacing: -.1rem;
-    margin-bottom: 1.8rem;
-    margin-top: 1.8rem;
-}
-
-h3 {
-    font-size: 3.0rem;
-    line-height: 1.3;
-    letter-spacing: -.1rem;
-    margin-bottom: 1.5rem;
-    margin-top: 1.5rem;
-}
-
-h4 {
-    font-family: "Playfair Display", sans-serif;
-    font-size: 4rem;
-    line-height: 1.35;
-    letter-spacing: -.08rem;
-    margin-bottom: 1.2rem;
-    margin-top: 1.2rem;
-}
-
-h5 {
-    font-size: 2.2rem;
-    line-height: 1.5;
-    letter-spacing: -.05rem;
-    margin-bottom: 0.6rem;
-    margin-top: 0.6rem;
-}
-
-h6 {
-    font-size: 2.0rem;
-    line-height: 1.6;
-    letter-spacing: 0;
-    margin-bottom: 0.75rem;
-    margin-top: 0.75rem;
-}
-
-p {
-    margin-top: 0;
-}
-
-#heatmap-title {
-    font-family: "Playfair Display", sans-serif;
-    font-size: 2rem;
-}
-
-#description {
-    font-size: 1.5rem;
-    border-left: #2cfec1 solid 1rem;
-    padding-left: 1rem;
-    max-width: 100rem;
-    margin: 2rem 0 3rem 0;
-}
-
-#logo {
-    height: 5rem;
-    margin-bottom: 2rem;
-}
-
-#slider-text, #chart-selector {
-    margin-bottom: 2rem !important;
-    font-size: 2rem;
-}
-
-
-@media only screen and (max-width: 550px) {
-    .rc-slider-mark-text {
-        font-size: 50%;
-    }
-
-    #description {
-        font-size: 1rem;
-    }
-}
-
-
-/* Blockquotes
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-blockquote {
-    border-left: 4px lightgrey solid;
-    padding-left: 1rem;
-    margin-top: 2rem;
-    margin-bottom: 2rem;
-    margin-left: 0;
-}
-
-
-/* Links
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-a {
-    color: #1EAEDB;
-    text-decoration: underline;
-    cursor: pointer;
-}
-
-a:hover {
-    color: #0FA0CE;
-}
-
-
-/* Buttons
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-.button,
-button,
-input[type="submit"],
-input[type="reset"],
-input[type="button"] {
-    display: inline-block;
-    height: 38px;
-    padding: 0 30px;
-    color: #555;
-    text-align: center;
-    font-size: 11px;
-    font-weight: 600;
-    line-height: 38px;
-    letter-spacing: .1rem;
-    text-transform: uppercase;
-    text-decoration: none;
-    white-space: nowrap;
-    background-color: transparent;
-    border-radius: 4px;
-    border: 1px solid #bbb;
-    cursor: pointer;
-    box-sizing: border-box;
-}
-
-.button:hover,
-button:hover,
-input[type="submit"]:hover,
-input[type="reset"]:hover,
-input[type="button"]:hover,
-.button:focus,
-button:focus,
-input[type="submit"]:focus,
-input[type="reset"]:focus,
-input[type="button"]:focus {
-    color: #333;
-    border-color: #888;
-    outline: 0;
-}
-
-.button.button-primary,
-button.button-primary,
-input[type="submit"].button-primary,
-input[type="reset"].button-primary,
-input[type="button"].button-primary {
-    color: #FFF;
-    background-color: #33C3F0;
-    border-color: #33C3F0;
-}
-
-.button.button-primary:hover,
-button.button-primary:hover,
-input[type="submit"].button-primary:hover,
-input[type="reset"].button-primary:hover,
-input[type="button"].button-primary:hover,
-.button.button-primary:focus,
-button.button-primary:focus,
-input[type="submit"].button-primary:focus,
-input[type="reset"].button-primary:focus,
-input[type="button"].button-primary:focus {
-    color: #FFF;
-    background-color: #1EAEDB;
-    border-color: #1EAEDB;
-}
-
-
-/* Forms
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-input[type="email"],
-input[type="number"],
-input[type="search"],
-input[type="text"],
-input[type="tel"],
-input[type="url"],
-input[type="password"],
-textarea,
-select {
-    height: 38px;
-    padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
-    background-color: #fff;
-    border: 1px solid #D1D1D1;
-    border-radius: 4px;
-    box-shadow: none;
-    box-sizing: border-box;
-    font-family: inherit;
-    font-size: inherit; /*https://stackoverflow.com/questions/6080413/why-doesnt-input-inherit-the-font-from-body*/
-}
-
-/* Removes awkward default styles on some inputs for iOS */
-input[type="email"],
-input[type="number"],
-input[type="search"],
-input[type="text"],
-input[type="tel"],
-input[type="url"],
-input[type="password"],
-textarea {
-    -webkit-appearance: none;
-    -moz-appearance: none;
-    appearance: none;
-}
-
-textarea {
-    min-height: 65px;
-    padding-top: 6px;
-    padding-bottom: 6px;
-}
-
-input[type="email"]:focus,
-input[type="number"]:focus,
-input[type="search"]:focus,
-input[type="text"]:focus,
-input[type="tel"]:focus,
-input[type="url"]:focus,
-input[type="password"]:focus,
-textarea:focus,
-select:focus {
-    border: 1px solid #33C3F0;
-    outline: 0;
-}
-
-label,
-legend {
-    display: block;
-    margin-bottom: 0;
-}
-
-fieldset {
-    padding: 0;
-    border-width: 0;
-}
-
-input[type="checkbox"],
-input[type="radio"] {
-    display: inline;
-}
-
-label > .label-body {
-    display: inline-block;
-    margin-left: .5rem;
-    font-weight: normal;
-}
-
-
-/* Lists
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-ul {
-    list-style: circle inside;
-}
-
-ol {
-    list-style: decimal inside;
-}
-
-ol, ul {
-    padding-left: 0;
-    margin-top: 0;
-}
-
-ul ul,
-ul ol,
-ol ol,
-ol ul {
-    margin: 1.5rem 0 1.5rem 3rem;
-    font-size: 90%;
-}
-
-li {
-    margin-bottom: 1rem;
-}
-
-
-/* Tables
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-table {
-    border-collapse: collapse;
-}
-
-th,
-td {
-    padding: 12px 15px;
-    text-align: left;
-    border-bottom: 1px solid #E1E1E1;
-}
-
-th:first-child,
-td:first-child {
-    padding-left: 0;
-}
-
-th:last-child,
-td:last-child {
-    padding-right: 0;
-}
-
-
-/* Spacing
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-button,
-.button {
-    margin-bottom: 0;
-}
-
-input,
-textarea,
-select,
-fieldset {
-    margin-bottom: 0;
-}
-
-pre,
-dl,
-figure,
-table,
-form {
-    margin-bottom: 0;
-}
-
-p,
-ul,
-ol {
-    margin-bottom: 0.75rem;
-}
-
-/* Utilities
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-.u-full-width {
-    width: 100%;
-    box-sizing: border-box;
-}
-
-.u-max-full-width {
-    max-width: 100%;
-    box-sizing: border-box;
-}
-
-.u-pull-right {
-    float: right;
-}
-
-.u-pull-left {
-    float: left;
-}
-
-
-/* Misc
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-hr {
-    margin-top: 3rem;
-    margin-bottom: 3.5rem;
-    border-width: 0;
-    border-top: 1px solid #E1E1E1;
-}
-
-
-/* Clearing
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-
-/* Self Clearing Goodness */
-.container:after,
-.row:after,
-.u-cf {
-    content: "";
-    display: table;
-    clear: both;
-}
-
-/* Slider
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-
-#slider-container {
-    background-color: #252e3f;
-    padding: 2rem 6rem 4rem 4rem;
-    height: 8rem;
-}
-
-.rc-slider-dot-active, .rc-slider-handle {
-    border-color: #2cfec1 !important;
-}
-
-.rc-slider-track {
-    background-color: #2cfec1 !important
-}
-
-.rc-slider-rail {
-    background-color: #1d2731 !important
-}
-
-/* Heatmap
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-#heatmap-container {
-    margin: 2.5rem 0 0 0;
-    background-color: #252e3f;
-}
-
-#heatmap-title {
-    margin: 0;
-    padding: 1rem;
-}
-
-#county-choropleth {
-    margin: 0;
-    flex-grow: 1;
-}
-
-@media (min-width: 1251px) {
-    #heatmap-container {
-        flex-grow: 1;
-        display: flex;
-        flex-direction: column;
-        justify-content: flex-start;
-    }
-}
-
-@media (max-width: 550px) {
-    #county-choropleth .annotation-text{
-        font-size: 1.2rem !important;
-    }
-}
-
-/* Left column
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-@media only screen and (max-width: 1250px) {
-    /*For mobile and smaller screens*/
-    #left-column {
-        margin-right: 1.5%;
-        width: 100%;
-    }
-}
-
-@media (min-width: 1251px) {
-    /*For desktop*/
-    #left-column {
-        margin-right: 1.5%;
-        display: flex;
-        flex-direction: column;
-        justify-content: flex-start;
-        align-content: center;
-        flex: 6 60%;
-    }
-}
-
-
-/* Graph
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-
-#graph-container {
-    background-color: #252e3f;
-    padding: 5rem;
-    margin: 0;
-}
-
-@media (max-width: 1250px) {
-    /*For mobile and smaller screens*/
-    #header h4 {
-        text-align: center;
-    }
-
-    #graph-container {
-        margin-top: 5rem;
-    }
-
-    #selected-data {
-        height: 55rem;
-    }
-}
-
-@media (min-width: 1251px) {
-    /*For desktop*/
-    #logo {
-        float: right;
-        padding-right: 2rem;
-    }
-
-    #header p {
-        font-size: 1.5rem;
-    }
-
-    #graph-container {
-        flex: 4 40%;
-        margin: 0;
-        display: flex;
-        flex-direction: column;
-        align-items: stretch;
-        justify-content: flex-start;
-    }
-
-    #selected-data {
-        flex-grow: 1;
-    }
-}
-
-@media (max-width: 550px) {
-    #graph-container {
-        padding: 2.5rem;
-    }
-
-    #selected-data .xtick text{
-        font-size: 1.1rem !important;
-    }
-
-    #selected-data .gtitle{
-        font-size: 1.7rem !important;
-    }
-}
-
-
-#chart-dropdown {
-    margin-bottom: 6.6rem;
-}
-
-/* Dropdown
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-.Select-value {
-    cursor: pointer;
-}
-
-.Select-control {
-    color: #7fafdf !important;
-}
-
-.Select {
-    color: #7fafdf !important;
-}
-
-.Select-menu-outer {
-    background-color: #252e3f !important;
-    border: 1px solid #7fafdf !important;
-}
-
-.Select div {
-    background-color: #252e3f !important;
-}
-
-.Select-menu-outer div:hover {
-    background-color: rgba(255, 255, 255, 0.01) !important;
-    cursor: pointer;
-}
-
-.Select-value-label {
-    color: #7fafdf !important;
-}
-
-.Select--single > .Select-control .Select-value, .Select-placeholder {
-    border: 1px solid #7fafdf !important;
-    border-radius: 4px !important;
-}
-
-/* Placement
-–––––––––––––––––––––––––––––––––––––––––––––––––– */
-@media only screen and (max-width: 1250px) {
-    /*For mobile and smaller screens*/
-    #app-container {
-        width: 100%;
-        display: flex;
-        flex-direction: column;
-        justify-content: flex-start;
-        align-items: stretch;
-        margin-bottom: 5rem;
-    }
-}
-
-@media (min-width: 1251px) {
-    /*For desktop*/
-    #app-container {
-        display: flex;
-        flex-direction: row;
-        justify-content: flex-start;
-        align-items: stretch;
-        height: 100rem;
-        margin-bottom: 5rem;
-    }
-}
-
-#header {
-    margin-left: 1.5%;
-}
-
-
-div, svg {
-    user-select: none !important;
-}
-
-._dash-undo-redo {
-    display: none;
-}
diff --git a/apps/dash-opioid-epidemic/constants.py b/apps/dash-opioid-epidemic/constants.py
new file mode 100644
index 000000000..5fca3364e
--- /dev/null
+++ b/apps/dash-opioid-epidemic/constants.py
@@ -0,0 +1,44 @@
+YEARS = [2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015]
+
+BINS = [
+    "0-2",
+    "2.1-4",
+    "4.1-6",
+    "6.1-8",
+    "8.1-10",
+    "10.1-12",
+    "12.1-14",
+    "14.1-16",
+    "16.1-18",
+    "18.1-20",
+    "20.1-22",
+    "22.1-24",
+    "24.1-26",
+    "26.1-28",
+    "28.1-30",
+    ">30",
+]
+
+DEFAULT_COLORSCALE = [
+    "#f2fffb",
+    "#bbffeb",
+    "#98ffe0",
+    "#79ffd6",
+    "#6df0c8",
+    "#69e7c0",
+    "#59dab2",
+    "#45d0a5",
+    "#31c194",
+    "#2bb489",
+    "#25a27b",
+    "#1e906d",
+    "#188463",
+    "#157658",
+    "#11684d",
+    "#10523e",
+]
+
+DEFAULT_OPACITY = 0.8
+
+mapbox_access_token = "pk.eyJ1IjoicGxvdGx5bWFwYm94IiwiYSI6ImNrOWJqb2F4djBnMjEzbG50amg0dnJieG4ifQ.Zme1-Uzoi75IaFbieBDl3A"
+mapbox_style = "mapbox://styles/plotlymapbox/cjvprkf3t1kns1cqjxuxmwixz"
diff --git a/apps/dash-opioid-epidemic/requirements.txt b/apps/dash-opioid-epidemic/requirements.txt
index 2564660a0..e4032113b 100644
--- a/apps/dash-opioid-epidemic/requirements.txt
+++ b/apps/dash-opioid-epidemic/requirements.txt
@@ -1,6 +1,5 @@
-dash==1.12.0
-plotly==4.7.1
+dash==2.4.1
+dash_bootstrap_components==1.1.0
+pandas==1.4.2
 cufflinks==0.17.3
-gunicorn==20.0.4
-numpy==1.18.4
-pandas==1.0.3
\ No newline at end of file
+gunicorn==20.1.0
\ No newline at end of file
diff --git a/apps/dash-opioid-epidemic/runtime.txt b/apps/dash-opioid-epidemic/runtime.txt
new file mode 100644
index 000000000..cfa660c42
--- /dev/null
+++ b/apps/dash-opioid-epidemic/runtime.txt
@@ -0,0 +1 @@
+python-3.8.0
\ No newline at end of file
diff --git a/apps/dash-opioid-epidemic/utils/components.py b/apps/dash-opioid-epidemic/utils/components.py
new file mode 100644
index 000000000..fabc512ba
--- /dev/null
+++ b/apps/dash-opioid-epidemic/utils/components.py
@@ -0,0 +1,112 @@
+from dash import html, dcc
+import dash_bootstrap_components as dbc
+
+from constants import YEARS, mapbox_access_token, mapbox_style
+
+
+def header(app, header_color, header, subheader=None, header_background_color="transparent"):
+    left_headers = html.Div(
+        [
+            html.Div(header, className="header-title"),
+            html.Div(subheader, className="subheader-title"),
+        ],
+        style={"color": header_color}
+    )
+
+    logo = html.Img(src=app.get_asset_url("images/plotly-logo-dark-theme.png"))
+    logo_link = html.A(logo, href="https://plotly.com/get-demo/", target="_blank")
+    demo_link = html.A(
+        "LEARN MORE",
+        href="https://plotly.com/dash/",
+        target="_blank",
+        className="demo-button",
+    )
+    right_logos = html.Div([demo_link, logo_link], className="header-logos")
+
+    return html.Div([left_headers, right_logos], className="header", style={"background-color": header_background_color})
+
+
+def choropleth_card(county_choropleth_id):
+    return dbc.Row([
+            dbc.Card(dbc.CardBody([
+                html.P("Drag the slider to change the year:"),
+                dcc.Slider(
+                    id="years-slider",
+                    min=min(YEARS),
+                    max=max(YEARS),
+                    value=min(YEARS),
+                    step=1,
+                    marks={
+                        str(year): {
+                            "label": str(year),
+                            "style": {"color": "#7fafdf"},
+                        }
+                        for year in YEARS
+                    },
+                ),
+            ])),
+            dbc.Card(dbc.CardBody([
+                html.P(f"Heatmap of age adjusted mortality rates from poisonings in year {min(YEARS)}", id="heatmap-title"),
+                dcc.Graph(
+                    id=county_choropleth_id,
+                    figure=dict(
+                        layout=dict(
+                            mapbox=dict(
+                                layers=[],
+                                accesstoken=mapbox_access_token,
+                                style=mapbox_style,
+                                center=dict(lat=38.72490, lon=-95.61446),
+                                pitch=0,
+                                zoom=3.5,
+                            ),
+                            autosize=True,
+                        ),
+                    ),
+                ),
+            ])),
+        ],
+    )
+
+
+def slider_graph_card(selected_data_id):
+    return dbc.Card(dbc.CardBody([
+            html.P(children="Select chart:"),
+            dcc.Dropdown(
+                options=[
+                    {
+                        "label": "Histogram of total number of deaths (single year)",
+                        "value": "show_absolute_deaths_single_year",
+                    },
+                    {
+                        "label": "Histogram of total number of deaths (1999-2016)",
+                        "value": "absolute_deaths_all_time",
+                    },
+                    {
+                        "label": "Age-adjusted death rate (single year)",
+                        "value": "show_death_rate_single_year",
+                    },
+                    # { # Takes too long to compute for many counties
+                    #     "label": "Trends in age-adjusted death rate (1999-2016)",
+                    #     "value": "death_rate_all_time",
+                    # },
+                ],
+                value="show_death_rate_single_year",
+                id="chart-dropdown",
+            ),
+            dcc.Loading(
+                dcc.Graph(
+                    id=selected_data_id,
+                    figure=dict(
+                        data=[dict(x=0, y=0)],
+                        layout=dict(
+                            paper_bgcolor="#F4F4F8",
+                            plot_bgcolor="#F4F4F8",
+                            autofill=True,
+                            margin=dict(t=75, r=50, b=100, l=50),
+                        ),
+                    ),
+                ),
+                type="dot"
+            )
+        ],
+    ))
diff --git a/apps/dash-opioid-epidemic/utils/figures.py b/apps/dash-opioid-epidemic/utils/figures.py
new file mode 100644
index 000000000..728156c08
--- /dev/null
+++ b/apps/dash-opioid-epidemic/utils/figures.py
@@ -0,0 +1,219 @@
+import re
+import pandas as pd
+import plotly.express as px
+
+from constants import (
+    BINS,
+    DEFAULT_COLORSCALE,
+    DEFAULT_OPACITY,
+    mapbox_access_token,
+    mapbox_style,
+)
+
+from utils.load_data import df_full_data, df_lat_lon
+
+
+def display_map(year, figure):
+    cm = dict(zip(BINS, DEFAULT_COLORSCALE))
+
+    data = [
+        dict(
+            lat=df_lat_lon["Latitude "],
+            lon=df_lat_lon["Longitude"],
+            text=df_lat_lon["Hover"],
+            type="scattermapbox",
+            hoverinfo="text",
+            marker=dict(size=5, color="white", opacity=0),
+        )
+    ]
+
+    annotations = [
+        dict(
+            showarrow=False,
+            align="right",
+            text="Age-adjusted death rate
per county per year",
+            font=dict(color="#2cfec1"),
+            bgcolor="#1f2630",
+            x=0.95,
+            y=0.95,
+        )
+    ]
+
+    for i, bin in enumerate(reversed(BINS)):
+        color = cm[bin]
+        annotations.append(
+            dict(
+                arrowcolor=color,
+                text=bin,
+                x=0.95,
+                y=0.85 - (i / 20),
+                ax=-60,
+                ay=0,
+                arrowwidth=5,
+                arrowhead=0,
+                bgcolor="#1f2630",
+                font=dict(color="#2cfec1"),
+            )
+        )
+
+    if "layout" in figure:
+        lat = figure["layout"]["mapbox"]["center"]["lat"]
+        lon = figure["layout"]["mapbox"]["center"]["lon"]
+        zoom = figure["layout"]["mapbox"]["zoom"]
+    else:
+        lat = 38.72490
+        lon = -95.61446
+        zoom = 2.5
+
+    layout = dict(
+        mapbox=dict(
+            layers=[],
+            accesstoken=mapbox_access_token,
+            style=mapbox_style,
+            center=dict(lat=lat, lon=lon),
+            zoom=zoom,
+        ),
+        hovermode="closest",
+        margin=dict(r=0, l=0, t=0, b=0),
+        annotations=annotations,
+        dragmode="lasso",
+    )
+
+    base_url = "https://raw.githubusercontent.com/jackparmer/mapbox-counties/master/"
+    for bin in BINS:
+        geo_layer = dict(
+            sourcetype="geojson",
+            source=base_url + str(year) + "/" + bin + ".geojson",
+            type="fill",
+            color=cm[bin],
+            opacity=DEFAULT_OPACITY,
+            # CHANGE THIS
+            fill=dict(outlinecolor="#afafaf"),
+        )
+        layout["mapbox"]["layers"].append(geo_layer)
+
+    fig = dict(data=data, layout=layout)
+    return fig
+
+
+def display_selected_data(selectedData, chart_dropdown, year):
+    if selectedData is None:
+        return dict(
+            data=[dict(x=0, y=0)],
+            layout=dict(
+                title="Click-drag on the map to select counties",
+                paper_bgcolor="#1f2630",
+                plot_bgcolor="#1f2630",
+                font=dict(color="#2cfec1"),
+                margin=dict(t=75, r=50, b=100, l=75),
+            ),
+        )
+    pts = selectedData["points"]
+    fips = [str(pt["text"].split("
")[-1]) for pt in pts]
+    for i in range(len(fips)):
+        if len(fips[i]) == 4:
+            fips[i] = "0" + fips[i]
+    dff = df_full_data[df_full_data["County Code"].isin(fips)]
+    dff = dff.sort_values("Year")
+
+    regex_pat = re.compile(r"Unreliable", flags=re.IGNORECASE)
+    dff["Age Adjusted Rate"] = dff["Age Adjusted Rate"].replace(regex_pat, 0)
+
+    if chart_dropdown != "death_rate_all_time":
+        title = "Absolute deaths per county, 1999-2016"
+        AGGREGATE_BY = "Deaths"
+        if "show_absolute_deaths_single_year" == chart_dropdown:
+            dff = dff[dff.Year == year]
+            title = "Absolute deaths per county, {0}".format(year)
+        elif "show_death_rate_single_year" == chart_dropdown:
+            dff = dff[dff.Year == year]
+            title = "Age-adjusted death rate per county, {0}".format(year)
+            AGGREGATE_BY = "Age Adjusted Rate"
+
+        dff[AGGREGATE_BY] = pd.to_numeric(dff[AGGREGATE_BY], errors="coerce")
+        deaths_or_rate_by_fips = dff.groupby("County")[AGGREGATE_BY].sum()
+        deaths_or_rate_by_fips = deaths_or_rate_by_fips.sort_values()
+        # Only look at non-zero rows:
+        deaths_or_rate_by_fips = deaths_or_rate_by_fips[deaths_or_rate_by_fips > 0]
+        fig = px.bar(deaths_or_rate_by_fips, y=AGGREGATE_BY, title=title)
+
+        fig_layout = fig["layout"]
+        fig_data = fig["data"]
+
+        fig_data[0]["text"] = deaths_or_rate_by_fips.values.tolist()
+        fig_data[0]["marker"]["color"] = "#2cfec1"
+        fig_data[0]["marker"]["opacity"] = 1
+        fig_data[0]["marker"]["line"]["width"] = 0
+        fig_data[0]["textposition"] = "outside"
+        fig_layout["paper_bgcolor"] = "#1f2630"
+        fig_layout["plot_bgcolor"] = "#1f2630"
+        fig_layout["font"]["color"] = "#2cfec1"
+        fig_layout["title"]["font"]["color"] = "#2cfec1"
+        fig_layout["xaxis"]["tickfont"]["color"] = "#2cfec1"
+        fig_layout["yaxis"]["tickfont"]["color"] = "#2cfec1"
+        fig_layout["xaxis"]["gridcolor"] = "#5b5b5b"
+        fig_layout["yaxis"]["gridcolor"] = "#5b5b5b"
+        fig_layout["margin"]["t"] = 75
+        fig_layout["margin"]["r"] = 50
+        fig_layout["margin"]["b"] = 100
+        fig_layout["margin"]["l"] = 50
+
+        return fig
+
+    fig = dff.iplot(
+        kind="area",
+        x="Year",
+        y="Age Adjusted Rate",
+        text="County",
+        categories="County",
+        colors=[
+            "#1b9e77",
+            "#d95f02",
+            "#7570b3",
+            "#e7298a",
+            "#66a61e",
+            "#e6ab02",
+            "#a6761d",
+            "#666666",
+            "#1b9e77",
+        ],
+        vline=[year],
+        asFigure=True,
+    )
+
+    for i, trace in enumerate(fig["data"]):
+        trace["mode"] = "lines+markers"
+        trace["marker"]["size"] = 4
+        trace["marker"]["line"]["width"] = 1
+        trace["type"] = "scatter"
+        for prop in trace:
+            fig["data"][i][prop] = trace[prop]
+
+    # Only show first 500 lines
+    fig["data"] = fig["data"][0:500]
+
+    fig_layout = fig["layout"]
+
+    # See plot.ly/python/reference
+    fig_layout["yaxis"]["title"] = "Age-adjusted death rate per county per year"
+    fig_layout["xaxis"]["title"] = ""
+    fig_layout["yaxis"]["fixedrange"] = True
+    fig_layout["xaxis"]["fixedrange"] = False
+    fig_layout["hovermode"] = "closest"
+    fig_layout["title"] = "{0} counties selected".format(len(fips))
+    fig_layout["legend"] = dict(orientation="v")
+    fig_layout["autosize"] = True
+    fig_layout["paper_bgcolor"] = "#1f2630"
+    fig_layout["plot_bgcolor"] = "#1f2630"
+    fig_layout["font"]["color"] = "#2cfec1"
+    fig_layout["xaxis"]["tickfont"]["color"] = "#2cfec1"
+    fig_layout["yaxis"]["tickfont"]["color"] = "#2cfec1"
+    fig_layout["xaxis"]["gridcolor"] = "#5b5b5b"
+    fig_layout["yaxis"]["gridcolor"] = "#5b5b5b"
+
+    if len(fips) > 500:
+        fig["layout"][
+            "title"
+        ] = "Age-adjusted death rate per county per year 
(only 1st 500 shown)"
+
+    return fig
diff --git a/apps/dash-opioid-epidemic/utils/load_data.py b/apps/dash-opioid-epidemic/utils/load_data.py
new file mode 100644
index 000000000..0a4fd6663
--- /dev/null
+++ b/apps/dash-opioid-epidemic/utils/load_data.py
@@ -0,0 +1,24 @@
+import os
+import pandas as pd
+
+
+# Load data
+df_lat_lon = pd.read_csv(
+    os.path.join(os.path.dirname(__file__), "../data/lat_lon_counties.csv")
+)
+
+df_lat_lon["FIPS "] = df_lat_lon["FIPS "].apply(lambda x: str(x).zfill(5))
+
+df_full_data = pd.read_csv(
+    os.path.join(
+        os.path.dirname(__file__), "../data/age_adjusted_death_rate_no_quotes.csv"
+    )
+)
+
+df_full_data["County Code"] = df_full_data["County Code"].apply(
+    lambda x: str(x).zfill(5)
+)
+
+df_full_data["County"] = (
+    df_full_data["Unnamed: 0"] + ", " + df_full_data.County.map(str)
+)