from dash import Dash, html, dcc, Input, Output, dash_table import plotly.graph_objects as go import dash_bootstrap_components as dbc from dash.dependencies import State import pandas as pd import plotly.express as px import json import os def load_data(): data = pd.read_csv("deer_tick_surveillance.csv", sep=",") counties_json = json.load(open("new-york-counties.geojson", "r")) counties_json["features"][0] for i in range(len(counties_json["features"])): counties_json["features"][i]['properties']['name'] = counties_json["features"][i]['properties']['name'].replace(" County", "") counties = [] for feature in counties_json["features"]: counties.append(feature["properties"]["name"]) counties = [*set(counties)] counties.sort() data.rename(columns={"Total Ticks Collected": "y", "Total Sites Visited": "n", "Total Tested": "t"}, inplace=True) data['Year'] = data['Year'].astype('category') county_df = pd.DataFrame(counties, columns =['County']) year_df = pd.DataFrame(list(range(2008, 2023)), columns =['Year']) data_df = county_df.merge(year_df, how='cross') data_df = data_df.merge(data[['County', 'Year', 'y', 'n', 't', 'B. burgdorferi (%)', 'A. phagocytophilum (%)', 'B. microti (%)', 'B. miyamotoi (%)']], on=['County', 'Year'], how='left') data_df['MLE'] = (data_df['y'] / data_df['n']).round(3) data_df['MLE'] = data_df['MLE'].fillna(0) for attr in ['y', 'n', 't', 'B. burgdorferi (%)', 'A. phagocytophilum (%)', 'B. microti (%)', 'B. miyamotoi (%)']: data_df[attr] = data_df[attr].fillna(0) return data_df, counties_json data_df, counties_json = load_data() app = Dash(__name__, external_stylesheets=[ 'https://fonts.googleapis.com/css2?family=Jura:wght@200&display=swap', dbc.themes.BOOTSTRAP ]) server = app.server #*******APP LAYOUT************** app.layout = html.Div( style={ 'backgroundColor':'#F1F6F1', 'height': '100%', 'color': '#1F6357', 'margin': 0, 'padding': 0 , }, children=[ #****************************HEADER************************************* html.Div( style={ 'fontFamily': 'Jura', 'backgroundColor': '#1e453e', 'padding': '10px', 'margin': 0, 'display': 'flex', 'flexDirection': 'column', }, children=[ html.H1( children='Tick Surveillance', style={ 'fontFamily': 'Jura', 'fontSize': '36px', 'fontWeight': '400', 'color': 'white' } ), html.H1( children='Monitoring of Black-Legged Tick Nymphs in the New York State (2008-2022)', style={ 'fontFamily': 'Jura', 'fontSize': '20px', 'fontWeight': '350', 'color': 'white' } ), ] ), #****************************Map************************************* html.Div( dcc.Graph(id='map'), style={'width': '90%', 'margin': '0 auto', 'display': 'block', 'padding':'0'} ), #****************************Slider************************************* html.Div([ 'Years', dcc.Slider(2008, 2022, step = 1, value=2022, id='slider', marks={i: f"{i}" for i in range(2008,2023,1)}, tooltip={'placement': 'bottom', 'always_visible': True}) ], style={'width': '90%', 'margin': '0 auto', 'display': 'block', 'height': '50px', 'paddingLeft': '20px', 'paddingRight':'20px' } ), #****************************Controls with Info Button************************************* html.Div([ html.Div([ 'Disease: ', dcc.RadioItems( ['All', 'A. phagocytophilum', 'B. burgdorferi', 'B. microti', 'B. miyamotoi'], 'All', id='disease', inline=True, style={ 'display': 'flex', 'gap': '10px', 'maxWidth': '100%', 'overflow': 'hidden' } ), ], style={ 'display': 'flex', 'fontFamily': 'Jura', 'height': '50px', 'paddingLeft': '18px', 'paddingTop': '20px', 'gap': '11px', 'maxWidth': '100%', } ), html.Div( children=[ dbc.Button( "ℹ️", id="open_modal", color="success", style={"padding": "1px 3px", "marginTop": "5px", "fontSize": "15px"} ), ], style={ 'display': 'flex', 'alignItems': 'center', 'marginLeft': '10px', 'paddingTop': '10px', } ), ], style={ 'display': 'flex', 'alignItems': 'center', 'width': '50%', 'paddingLeft': '18px', 'paddingTop': '28px', 'gap': '11px', } ), dbc.Modal( [ dbc.ModalHeader("Information"), dbc.ModalBody( html.Div([ html.P("A. phagocytophilum: The bacteria that causes anaplasmosis, also known as tick-borne fever."), html.P("B. burgdorferi: The bacteria that causes Lyme disease."), html.P("B. microti: A parasitic blood-borne piroplasm responsible for the disease babesiosis."), html.P("B. miyamotoi: The bacteria that causes the Borrelia miyamotoi disease."), ]) ), dbc.ModalFooter( dbc.Button("Close", id="close_modal", className="ml-auto") ), ], id="modal", is_open=False, ), #****************************Barplot************************************* html.Div( [dcc.Graph(id='barplot')], style={'width': '50%', 'display': 'inline-block'},), #****************************Timeline************************************* html.Div( [dcc.Graph(id='timeline')], style={'width': '50%', 'display': 'inline-block'},), #****************************Footer************************************* html.Footer( [ "Data provided by ", html.A( "New York State Department of Health", href="https://health.data.ny.gov/Health/Deer-Tick-Surveillance-Nymphs-May-to-Sept-excludin/kibp-u2ip/data", target="_blank", style={'color': 'black', 'textDecoration': 'none'} ), "." ], style={'textAlign': 'center', 'marginTop': '20px', 'color': 'gray', 'fontSize': '15px'} ) ] ) #**************FUNCTIONS***************************** def get_map(year, county=None): stats_map = data_df[data_df["Year"] == year] fig = px.choropleth( stats_map, geojson=counties_json, featureidkey='properties.name', locations='County', color='MLE', color_continuous_scale='OrRd', projection='equirectangular', title='', labels={ 'y': 'Total Ticks Collected', 'n': 'Total Sites Visited', 'MLE': 'Max Likelihood Estimate' }, hover_data=['County', 'MLE', 'n', 'y'] ) fig.update_geos(visible=False) fig.update_layout( margin={"r": 0, "t": 50, "l": 0, "b": 0}, geo_bgcolor="#F1F1F1", geo_showlakes=False, geo_fitbounds="locations", geo_resolution=110, geo_landcolor="#F1F6F1", geo_showcountries=True, paper_bgcolor="#F1F6F1", font_color="black", ) if county: county_geojson = { "type": "FeatureCollection", "features": [ feature for feature in counties_json["features"] if feature["properties"]["name"] == county ] } outline_coords = county_geojson["features"][0]["geometry"]["coordinates"] for polygon in outline_coords: if isinstance(polygon[0][0], list): polygons = polygon else: polygons = [polygon] for poly in polygons: lons, lats = zip(*poly) fig.add_trace( go.Scattergeo( lon=lons, lat=lats, mode="lines", line=dict(color="#32CD32", width=3), name=f"Selected County: {county}", hoverinfo="skip", showlegend=False, ) ) return fig def get_county_graph(county): timeline = data_df[data_df['County'] == county] bars = px.bar(timeline, x="Year", y='MLE', title= "Barplot of the mean number of ticks in " + county + " over the years", ) bars.update_layout(plot_bgcolor= "#F1F6F1", paper_bgcolor= "#F1F6F1", font_color="black", xaxis=dict(dtick=1)) bars.update_traces(marker_color="#7CDF7C") bars.update_layout(margin={"r":20,"t":55,"l":20,"b":0}) return bars def get_county_graph2(disease, county): timeline = data_df[data_df['County'] == county] if disease == 'All': diseases = ['A. phagocytophilum (%)', 'B. burgdorferi (%)', 'B. microti (%)', 'B. miyamotoi (%)'] stacked_data = timeline[diseases + ['t']].copy() stacked_data['Year'] = timeline['Year'] bars = px.bar( stacked_data, x="Year", y=diseases, title= f"Stacked Barplot of the percentage of positively tested ticks in {county} over the Years", labels={'t': 'Total Ticks Tested'}, hover_data=['Year', 't'] ) colors = ["#FF7F50", "#FFD700", "#ADFF2F", "#32CD32"] tested = [timeline[timeline['Year'] == i]['t'].iloc[0] for i in range(2008, 2023)] for i, trace in enumerate(bars.data): trace.marker.color = colors[i] bars.update_layout( barmode='stack', plot_bgcolor="#F1F6F1", paper_bgcolor="#F1F6F1", font_color="black", xaxis=dict(dtick=1), yaxis=dict(range=[0, 100], title="Percentage") ) else: bars = px.bar( timeline, x="Year", y=disease + ' (%)', title=f"Barplot of the percentage of positively tested ticks for {disease} in {county} over the years", labels={'t': 'Total Ticks Tested'}, hover_data=['Year', 't'] ) bars.update_layout( plot_bgcolor="#F1F6F1", paper_bgcolor="#F1F6F1", font_color="black", yaxis=dict(range=[0, 100]), xaxis=dict(dtick=1) ) bars.update_traces(marker_color="#7CDF7C") bars.update_layout(margin={"r":20,"t":60,"l":20,"b":0}) return bars #*************CALLBACKS***************************************** #slider->map @app.callback( Output('map', 'figure'), Input('slider', 'value'), Input('map', 'clickData') ) def update_map(year, clickData): county = "Washington" if clickData: county = clickData['points'][0]['location'] fig = get_map(year, county) return fig #map->bars @app.callback( Output('timeline', 'figure'), Input('map', 'clickData') ) def update_timeline(clickData): county = "Washington" if clickData is not None: county= clickData['points'][0]["location"] bars = get_county_graph(county) return bars #timeline->slider @app.callback( Output("slider", "value"), Input('timeline', 'clickData') ) def update_year(clickData): year = 2022 if clickData is not None: year = clickData['points'][0]["x"] return year #radio,map->barplot @app.callback( Output('barplot', 'figure'), Input('disease', 'value'), Input('map', 'clickData') ) def update_barplot(disease, clickData): county = "Washington" if clickData is not None: county= clickData['points'][0]["location"] bars = get_county_graph2(disease, county) return bars #data dump @app.callback( Output('click-data', 'children'), Input('map', 'clickData') ) def update_data(clickData): if clickData is not None: country= clickData['points'][0]["location"] return json.dumps(clickData, indent=2) @app.callback( Output("modal", "is_open"), [Input("open_modal", "n_clicks"), Input("close_modal", "n_clicks")], [State("modal", "is_open")] ) def toggle_modal(n1, n2, is_open): if n1 or n2: return not is_open return is_open # ********RUNNING THE APP************************************************* if __name__ == '__main__': app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)), debug=False)