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 -![plotly-dash-screencast](assets/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/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) +)