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)
+)