From 75c1e2bbae0cce7c5e818cc073607a3e2eb1b559 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Wed, 25 May 2022 09:27:43 -0400 Subject: [PATCH 1/5] refactoring --- apps/dash-object-detection/requirements.txt | 8 +- apps/dash-opioid-epidemic/README.md | 2 +- apps/dash-opioid-epidemic/app.py | 294 ++---------------- .../assets/{ => css}/demo-button.css | 0 .../assets/css/header.css | 59 ++++ .../assets/{ => css}/opioid.css | 0 .../dash-opioid-epidemic/assets/dash-logo.png | Bin 18588 -> 0 bytes .../assets/{ => images}/app_screencast.gif | Bin .../assets/images/plotly-logo.png | Bin 0 -> 23021 bytes apps/dash-opioid-epidemic/constants.py | 71 +++++ apps/dash-opioid-epidemic/gitignore | 191 ++++++++++++ apps/dash-opioid-epidemic/requirements.txt | 11 +- apps/dash-opioid-epidemic/runtime.txt | 1 + .../utils/helper_functions.py | 220 +++++++++++++ 14 files changed, 571 insertions(+), 286 deletions(-) rename apps/dash-opioid-epidemic/assets/{ => css}/demo-button.css (100%) create mode 100644 apps/dash-opioid-epidemic/assets/css/header.css rename apps/dash-opioid-epidemic/assets/{ => css}/opioid.css (100%) delete mode 100644 apps/dash-opioid-epidemic/assets/dash-logo.png rename apps/dash-opioid-epidemic/assets/{ => images}/app_screencast.gif (100%) create mode 100644 apps/dash-opioid-epidemic/assets/images/plotly-logo.png create mode 100644 apps/dash-opioid-epidemic/constants.py create mode 100644 apps/dash-opioid-epidemic/gitignore create mode 100644 apps/dash-opioid-epidemic/runtime.txt create mode 100644 apps/dash-opioid-epidemic/utils/helper_functions.py 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/README.md b/apps/dash-opioid-epidemic/README.md index 203145788..3717f57d1 100644 --- a/apps/dash-opioid-epidemic/README.md +++ b/apps/dash-opioid-epidemic/README.md @@ -1,6 +1,6 @@ # US opioid epidemic dataset and Dash app -![plotly-dash-screencast](assets/app_screencast.gif) +![plotly-dash-screencast](assets/images/app_screencast.gif) 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). diff --git a/apps/dash-opioid-epidemic/app.py b/apps/dash-opioid-epidemic/app.py index 32d66ef11..91c3bb2f0 100644 --- a/apps/dash-opioid-epidemic/app.py +++ b/apps/dash-opioid-epidemic/app.py @@ -1,14 +1,23 @@ -import os -import pathlib -import re - import dash -import dash_core_components as dcc -import dash_html_components as html +from dash import dcc, html import pandas as pd from dash.dependencies import Input, Output, State import cufflinks as cf +from utils.helper_functions import display_map, display_selected_data + +from constants import ( + df_lat_lon, + df_full_data, + YEARS, + BINS, + DEFAULT_COLORSCALE, + DEFAULT_OPACITY, + mapbox_access_token, + mapbox_style, +) + + # Initialize app app = dash.Dash( @@ -20,74 +29,8 @@ app.title = "US Opioid Epidemic" 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=[ @@ -227,87 +170,9 @@ [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, - ) - ] +def return_display_map(year, figure): - 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 = 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 + return display_map(year, figure) @app.callback(Output("heatmap-title", "children"), [Input("years-slider", "value")]) @@ -326,129 +191,8 @@ def update_map_title(year): 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 display_selected_data(selectedData, chart_dropdown, year) if __name__ == "__main__": diff --git a/apps/dash-opioid-epidemic/assets/demo-button.css b/apps/dash-opioid-epidemic/assets/css/demo-button.css similarity index 100% rename from apps/dash-opioid-epidemic/assets/demo-button.css rename to apps/dash-opioid-epidemic/assets/css/demo-button.css diff --git a/apps/dash-opioid-epidemic/assets/css/header.css b/apps/dash-opioid-epidemic/assets/css/header.css new file mode 100644 index 000000000..f97cc1621 --- /dev/null +++ b/apps/dash-opioid-epidemic/assets/css/header.css @@ -0,0 +1,59 @@ +/* Header */ +.header { + height: 10vh; + display: flex; + background-color: #2f3445; + padding-left: 2%; + padding-right: 2%; + font-family: playfair display,sans-serif; +} +.header .header-title { + color: #9EA7C0 !important; + font-size: 5vh; +} +.header-logos { + margin-left: auto; + align-self: center; +} +.header-logos img { + margin-left: 3vh !important; + max-height: 5vh; +} +.subheader-title { + font-size: 1.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; +} +.demo-button:hover { + color: #7A76FF; + background-position: 0%; +} \ No newline at end of file diff --git a/apps/dash-opioid-epidemic/assets/opioid.css b/apps/dash-opioid-epidemic/assets/css/opioid.css similarity index 100% rename from apps/dash-opioid-epidemic/assets/opioid.css rename to apps/dash-opioid-epidemic/assets/css/opioid.css 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 040cde17424f9bd58cec6360eb2d1ace958f0634..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18588 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", +] + +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/gitignore b/apps/dash-opioid-epidemic/gitignore new file mode 100644 index 000000000..d8e187da3 --- /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/requirements.txt b/apps/dash-opioid-epidemic/requirements.txt index 2564660a0..d11545f6c 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 -cufflinks==0.17.3 -gunicorn==20.0.4 -numpy==1.18.4 -pandas==1.0.3 \ No newline at end of file +dash==2.4.1 +pandas==1.4.2 +gunicorn==20.1.0 +plotly==5.8.0 +cufflinks==0.17.3 \ 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/helper_functions.py b/apps/dash-opioid-epidemic/utils/helper_functions.py new file mode 100644 index 000000000..f3f5b07ed --- /dev/null +++ b/apps/dash-opioid-epidemic/utils/helper_functions.py @@ -0,0 +1,220 @@ +import re +import pandas as pd + +from constants import ( + df_full_data, + df_lat_lon, + BINS, + DEFAULT_COLORSCALE, + DEFAULT_OPACITY, + mapbox_access_token, + mapbox_style, +) + + +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 = 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 + + +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 From 8d62284b35227d933bd81811deecb551fee781ef Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Thu, 26 May 2022 14:18:42 -0400 Subject: [PATCH 2/5] refactoring --- apps/dash-opioid-epidemic/app.py | 160 +++--------------- .../assets/css/header.css | 11 +- apps/dash-opioid-epidemic/constants.py | 27 --- apps/dash-opioid-epidemic/utils/components.py | 129 ++++++++++++++ .../utils/{helper_functions.py => figures.py} | 4 +- apps/dash-opioid-epidemic/utils/load_data.py | 24 +++ 6 files changed, 182 insertions(+), 173 deletions(-) create mode 100644 apps/dash-opioid-epidemic/utils/components.py rename apps/dash-opioid-epidemic/utils/{helper_functions.py => figures.py} (99%) create mode 100644 apps/dash-opioid-epidemic/utils/load_data.py diff --git a/apps/dash-opioid-epidemic/app.py b/apps/dash-opioid-epidemic/app.py index 91c3bb2f0..6cafb2f4d 100644 --- a/apps/dash-opioid-epidemic/app.py +++ b/apps/dash-opioid-epidemic/app.py @@ -1,23 +1,17 @@ -import dash -from dash import dcc, html import pandas as pd -from dash.dependencies import Input, Output, State +from dash import dash, html, dcc, Input, Output, State, callback, callback_context import cufflinks as cf -from utils.helper_functions import display_map, display_selected_data +from utils.figures import display_map, display_selected_data + +from utils.components import Header, choropleth_card, slider_graph_card from constants import ( - df_lat_lon, - df_full_data, YEARS, - BINS, - DEFAULT_COLORSCALE, - DEFAULT_OPACITY, mapbox_access_token, mapbox_style, ) - # Initialize app app = dash.Dash( @@ -32,150 +26,36 @@ # 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, \ + [ + Header( + app, + "Rate of US Poison-Induced Deaths", + "† 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 \ (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), - ), - ), - ), - ], - ), + choropleth_card("county-choropleth"), + slider_graph_card("selected-data"), ], ), ], ) -@app.callback( +@callback( Output("county-choropleth", "figure"), - [Input("years-slider", "value")], - [State("county-choropleth", "figure")], + Input("years-slider", "value"), + State("county-choropleth", "figure"), ) def return_display_map(year, figure): - return display_map(year, figure) -@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( @@ -183,13 +63,11 @@ def update_map_title(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 return_display_selected_data(selectedData, chart_dropdown, year): return display_selected_data(selectedData, chart_dropdown, year) diff --git a/apps/dash-opioid-epidemic/assets/css/header.css b/apps/dash-opioid-epidemic/assets/css/header.css index f97cc1621..440c84da3 100644 --- a/apps/dash-opioid-epidemic/assets/css/header.css +++ b/apps/dash-opioid-epidemic/assets/css/header.css @@ -5,20 +5,24 @@ background-color: #2f3445; padding-left: 2%; padding-right: 2%; - font-family: playfair display,sans-serif; + font-family: playfair display, sans-serif; } + .header .header-title { color: #9EA7C0 !important; font-size: 5vh; } + .header-logos { margin-left: auto; align-self: center; } + .header-logos img { margin-left: 3vh !important; max-height: 5vh; } + .subheader-title { font-size: 1.5vh; } @@ -27,7 +31,7 @@ /* Demo button css */ .demo-button { font-size: 1.5vh; - font-family: Open Sans,sans-serif; + font-family: Open Sans, sans-serif; text-decoration: none; -webkit-align-items: center; -webkit-box-align: center; @@ -53,7 +57,8 @@ padding-top: 1vh; padding-bottom: 1vh; } + .demo-button:hover { color: #7A76FF; background-position: 0%; -} \ No newline at end of file +} \ No newline at end of file diff --git a/apps/dash-opioid-epidemic/constants.py b/apps/dash-opioid-epidemic/constants.py index 898f1f3d0..5fca3364e 100644 --- a/apps/dash-opioid-epidemic/constants.py +++ b/apps/dash-opioid-epidemic/constants.py @@ -1,30 +1,3 @@ -import os -import pathlib -import pandas as pd - - -# 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 = [ diff --git a/apps/dash-opioid-epidemic/utils/components.py b/apps/dash-opioid-epidemic/utils/components.py new file mode 100644 index 000000000..a0a66ff1a --- /dev/null +++ b/apps/dash-opioid-epidemic/utils/components.py @@ -0,0 +1,129 @@ +from dash import html, dcc + +from constants import ( + YEARS, + mapbox_access_token, + mapbox_style, +) + + +def Header(app, header, subheader=None): + left_headers = html.Div( + [ + html.Div(header, className="header-title"), + html.Div(subheader, className="subheader-title"), + ] + ) + + logo = html.Img(src=app.get_asset_url("images/plotly-logo.png")) + link = html.A(logo, href="https://plotly.com/dash/", target="_blank") + demo_link = html.A( + "LEARN MORE", + href="https://plotly.com/dash/", + target="_blank", + className="demo-button", + ) + right_logos = html.Div([demo_link, link], className="header-logos") + + return html.Div([left_headers, right_logos], className="header") + + +def choropleth_card(county_choropleth_id): + return 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_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 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_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), + ), + ), + ), + ], + ) diff --git a/apps/dash-opioid-epidemic/utils/helper_functions.py b/apps/dash-opioid-epidemic/utils/figures.py similarity index 99% rename from apps/dash-opioid-epidemic/utils/helper_functions.py rename to apps/dash-opioid-epidemic/utils/figures.py index f3f5b07ed..53d5bba76 100644 --- a/apps/dash-opioid-epidemic/utils/helper_functions.py +++ b/apps/dash-opioid-epidemic/utils/figures.py @@ -2,8 +2,6 @@ import pandas as pd from constants import ( - df_full_data, - df_lat_lon, BINS, DEFAULT_COLORSCALE, DEFAULT_OPACITY, @@ -11,6 +9,8 @@ mapbox_style, ) +from utils.load_data import df_full_data, df_lat_lon + def display_map(year, figure): cm = dict(zip(BINS, DEFAULT_COLORSCALE)) 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) +) From 8905f0ea8115540266cce17a52027b792c677d81 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Thu, 26 May 2022 14:27:45 -0400 Subject: [PATCH 3/5] fixes --- apps/dash-opioid-epidemic/app.py | 11 ++- apps/dash-opioid-epidemic/assets/css/app.css | 63 ++++++++++++++++++ ...ly-logo.png => plotly-logo-dark-theme.png} | Bin apps/dash-opioid-epidemic/utils/components.py | 19 ++++-- 4 files changed, 84 insertions(+), 9 deletions(-) create mode 100644 apps/dash-opioid-epidemic/assets/css/app.css rename apps/dash-opioid-epidemic/assets/images/{plotly-logo.png => plotly-logo-dark-theme.png} (100%) diff --git a/apps/dash-opioid-epidemic/app.py b/apps/dash-opioid-epidemic/app.py index 6cafb2f4d..34e1dabdb 100644 --- a/apps/dash-opioid-epidemic/app.py +++ b/apps/dash-opioid-epidemic/app.py @@ -4,7 +4,7 @@ from utils.figures import display_map, display_selected_data -from utils.components import Header, choropleth_card, slider_graph_card +from utils.components import header, choropleth_card, slider_graph_card from constants import ( YEARS, @@ -23,14 +23,19 @@ app.title = "US Opioid Epidemic" server = app.server +# def header( +# app, header_color, header, subheader=None, header_background_color="transparent" +# ) + # App layout app.layout = html.Div( [ - Header( + header( app, + "#1f2630", "Rate of US Poison-Induced Deaths", - "† Deaths are classified using the International Classification of Diseases, \ + subheader="† 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 \ (undetermined intent).", 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..b7a3adfd4 --- /dev/null +++ b/apps/dash-opioid-epidemic/assets/css/app.css @@ -0,0 +1,63 @@ +/* Header */ +.header { + height: 10vh; + display: flex; + padding-left: 2%; + padding-right: 2%; + font-family: playfair display, sans-serif; + font-weight: bold; +} + +.header .header-title { + font-size: 5vh; +} + +.subheader-title { + font-size: 1.5vh; +} + +.header-logos { + margin-left: auto; +} + +.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/images/plotly-logo.png b/apps/dash-opioid-epidemic/assets/images/plotly-logo-dark-theme.png similarity index 100% rename from apps/dash-opioid-epidemic/assets/images/plotly-logo.png rename to apps/dash-opioid-epidemic/assets/images/plotly-logo-dark-theme.png diff --git a/apps/dash-opioid-epidemic/utils/components.py b/apps/dash-opioid-epidemic/utils/components.py index a0a66ff1a..4b5c06b6a 100644 --- a/apps/dash-opioid-epidemic/utils/components.py +++ b/apps/dash-opioid-epidemic/utils/components.py @@ -7,25 +7,32 @@ ) -def Header(app, header, subheader=None): +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.png")) - link = html.A(logo, href="https://plotly.com/dash/", target="_blank") + 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, link], className="header-logos") + right_logos = html.Div([demo_link, logo_link], className="header-logos") - return html.Div([left_headers, right_logos], className="header") + return html.Div( + [left_headers, right_logos], + className="header", + style={"background-color": header_background_color}, + ) def choropleth_card(county_choropleth_id): From 541f495e41f63bc15faa13f8655c9349bd1b8f92 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Fri, 27 May 2022 09:20:18 -0400 Subject: [PATCH 4/5] fix header, map --- apps/dash-opioid-epidemic/README.md | 4 ++-- .../assets/{images => github}/app_screencast.gif | Bin apps/dash-opioid-epidemic/utils/components.py | 1 + apps/dash-opioid-epidemic/utils/figures.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename apps/dash-opioid-epidemic/assets/{images => github}/app_screencast.gif (100%) diff --git a/apps/dash-opioid-epidemic/README.md b/apps/dash-opioid-epidemic/README.md index 3717f57d1..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 -![plotly-dash-screencast](assets/images/app_screencast.gif) +![plotly-dash-screencast](assets/github/app_screencast.gif) -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/assets/images/app_screencast.gif b/apps/dash-opioid-epidemic/assets/github/app_screencast.gif similarity index 100% rename from apps/dash-opioid-epidemic/assets/images/app_screencast.gif rename to apps/dash-opioid-epidemic/assets/github/app_screencast.gif diff --git a/apps/dash-opioid-epidemic/utils/components.py b/apps/dash-opioid-epidemic/utils/components.py index 4b5c06b6a..5a15548ed 100644 --- a/apps/dash-opioid-epidemic/utils/components.py +++ b/apps/dash-opioid-epidemic/utils/components.py @@ -51,6 +51,7 @@ def choropleth_card(county_choropleth_id): min=min(YEARS), max=max(YEARS), value=min(YEARS), + step=1, marks={ str(year): { "label": str(year), diff --git a/apps/dash-opioid-epidemic/utils/figures.py b/apps/dash-opioid-epidemic/utils/figures.py index 53d5bba76..322fa9d10 100644 --- a/apps/dash-opioid-epidemic/utils/figures.py +++ b/apps/dash-opioid-epidemic/utils/figures.py @@ -62,7 +62,7 @@ def display_map(year, figure): else: lat = 38.72490 lon = -95.61446 - zoom = 3.5 + zoom = 2.5 layout = dict( mapbox=dict( From 5eb8246335f4edc927da4290dd7ff434281aa805 Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Thu, 1 Dec 2022 17:05:16 +0000 Subject: [PATCH 5/5] final changes --- .../{gitignore => .gitignore} | 380 ++++---- apps/dash-opioid-epidemic/app.py | 60 +- apps/dash-opioid-epidemic/assets/css/app.css | 69 +- .../assets/css/demo-button.css | 13 - .../assets/css/header.css | 64 -- .../assets/css/opioid.css | 860 ------------------ apps/dash-opioid-epidemic/requirements.txt | 6 +- apps/dash-opioid-epidemic/utils/components.py | 143 ++- apps/dash-opioid-epidemic/utils/figures.py | 5 +- 9 files changed, 338 insertions(+), 1262 deletions(-) rename apps/dash-opioid-epidemic/{gitignore => .gitignore} (94%) delete mode 100644 apps/dash-opioid-epidemic/assets/css/demo-button.css delete mode 100644 apps/dash-opioid-epidemic/assets/css/header.css delete mode 100644 apps/dash-opioid-epidemic/assets/css/opioid.css diff --git a/apps/dash-opioid-epidemic/gitignore b/apps/dash-opioid-epidemic/.gitignore similarity index 94% rename from apps/dash-opioid-epidemic/gitignore rename to apps/dash-opioid-epidemic/.gitignore index d8e187da3..90ecc9b06 100644 --- a/apps/dash-opioid-epidemic/gitignore +++ b/apps/dash-opioid-epidemic/.gitignore @@ -1,191 +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 +# .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/app.py b/apps/dash-opioid-epidemic/app.py index 34e1dabdb..dc4ecf1f5 100644 --- a/apps/dash-opioid-epidemic/app.py +++ b/apps/dash-opioid-epidemic/app.py @@ -1,53 +1,31 @@ -import pandas as pd -from dash import dash, html, dcc, Input, Output, State, callback, callback_context -import cufflinks as cf - -from utils.figures import display_map, display_selected_data +from dash import dash, html, Input, Output, State, callback +import dash_bootstrap_components as dbc +import utils.figures as figs from utils.components import header, choropleth_card, slider_graph_card -from constants import ( - YEARS, - mapbox_access_token, - mapbox_style, -) - -# 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 -# def header( -# app, header_color, header, subheader=None, header_background_color="transparent" -# ) - - # App layout -app.layout = html.Div( +app.layout = dbc.Container( [ header( app, - "#1f2630", + "inherit", "Rate of US Poison-Induced Deaths", subheader="† 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 \ + 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=[ - choropleth_card("county-choropleth"), - slider_graph_card("selected-data"), + dbc.Row([ + dbc.Col(choropleth_card("county-choropleth"), width=7), + dbc.Col(slider_graph_card("selected-data"),width=5) ], ), ], + fluid=True ) @@ -57,15 +35,15 @@ State("county-choropleth", "figure"), ) def return_display_map(year, figure): - return display_map(year, figure) + return figs.display_map(year, figure) -@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}" @callback( @@ -75,7 +53,7 @@ def update_map_title(year): Input("years-slider", "value"), ) def return_display_selected_data(selectedData, chart_dropdown, year): - 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 index b7a3adfd4..8aee67e8d 100644 --- a/apps/dash-opioid-epidemic/assets/css/app.css +++ b/apps/dash-opioid-epidemic/assets/css/app.css @@ -1,9 +1,67 @@ + +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 { - height: 10vh; display: flex; padding-left: 2%; padding-right: 2%; + padding-bottom: 1%; font-family: playfair display, sans-serif; font-weight: bold; } @@ -14,18 +72,21 @@ .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; @@ -60,4 +121,4 @@ .demo-button:hover { color: #7A76FF; background-position: 0%; -} \ No newline at end of file +} \ No newline at end of file diff --git a/apps/dash-opioid-epidemic/assets/css/demo-button.css b/apps/dash-opioid-epidemic/assets/css/demo-button.css deleted file mode 100644 index 513c949c6..000000000 --- a/apps/dash-opioid-epidemic/assets/css/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/css/header.css b/apps/dash-opioid-epidemic/assets/css/header.css deleted file mode 100644 index 440c84da3..000000000 --- a/apps/dash-opioid-epidemic/assets/css/header.css +++ /dev/null @@ -1,64 +0,0 @@ -/* Header */ -.header { - height: 10vh; - display: flex; - background-color: #2f3445; - padding-left: 2%; - padding-right: 2%; - font-family: playfair display, sans-serif; -} - -.header .header-title { - color: #9EA7C0 !important; - font-size: 5vh; -} - -.header-logos { - margin-left: auto; - align-self: center; -} - -.header-logos img { - margin-left: 3vh !important; - max-height: 5vh; -} - -.subheader-title { - font-size: 1.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; -} - -.demo-button:hover { - color: #7A76FF; - background-position: 0%; -} \ No newline at end of file diff --git a/apps/dash-opioid-epidemic/assets/css/opioid.css b/apps/dash-opioid-epidemic/assets/css/opioid.css deleted file mode 100644 index ee03bf467..000000000 --- a/apps/dash-opioid-epidemic/assets/css/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/requirements.txt b/apps/dash-opioid-epidemic/requirements.txt index d11545f6c..e4032113b 100644 --- a/apps/dash-opioid-epidemic/requirements.txt +++ b/apps/dash-opioid-epidemic/requirements.txt @@ -1,5 +1,5 @@ dash==2.4.1 +dash_bootstrap_components==1.1.0 pandas==1.4.2 -gunicorn==20.1.0 -plotly==5.8.0 -cufflinks==0.17.3 \ No newline at end of file +cufflinks==0.17.3 +gunicorn==20.1.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 index 5a15548ed..fabc512ba 100644 --- a/apps/dash-opioid-epidemic/utils/components.py +++ b/apps/dash-opioid-epidemic/utils/components.py @@ -1,21 +1,16 @@ from dash import html, dcc +import dash_bootstrap_components as dbc -from constants import ( - YEARS, - mapbox_access_token, - mapbox_style, -) +from constants import YEARS, mapbox_access_token, mapbox_style -def header( - app, header_color, header, subheader=None, header_background_color="transparent" -): +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}, + style={"color": header_color} ) logo = html.Img(src=app.get_asset_url("images/plotly-logo-dark-theme.png")) @@ -28,77 +23,54 @@ def header( ) 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}, - ) + return html.Div([left_headers, right_logos], className="header", style={"background-color": header_background_color}) def choropleth_card(county_choropleth_id): - return 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), - step=1, - 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_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, + 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 html.Div( - id="graph-container", - children=[ - html.P(id="chart-selector", children="Select chart:"), + return dbc.Card(dbc.CardBody([ + html.P(children="Select chart:"), dcc.Dropdown( options=[ { @@ -113,25 +85,28 @@ def slider_graph_card(selected_data_id): "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", - }, + # { # 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.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), + 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 index 322fa9d10..728156c08 100644 --- a/apps/dash-opioid-epidemic/utils/figures.py +++ b/apps/dash-opioid-epidemic/utils/figures.py @@ -1,5 +1,6 @@ import re import pandas as pd +import plotly.express as px from constants import ( BINS, @@ -134,9 +135,7 @@ def display_selected_data(selectedData, chart_dropdown, year): 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 = px.bar(deaths_or_rate_by_fips, y=AGGREGATE_BY, title=title) fig_layout = fig["layout"] fig_data = fig["data"]