├── Procfile ├── update_fig.sh ├── assets ├── favicon.ico ├── github.png ├── medium.png ├── database.png └── styles.css ├── data └── pickle │ └── world_info.p ├── config.example.ini ├── requirements.txt ├── .gitignore ├── update_data.sh ├── README.md ├── scripts ├── utils_covid.py └── create_world_fig.py └── app.py /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:server -------------------------------------------------------------------------------- /update_fig.sh: -------------------------------------------------------------------------------- 1 | cd scripts && echo ls && python create_world_fig.py 2 | cd .. 3 | python app.py -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThibaudLamothe/dash-mapbox/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /assets/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThibaudLamothe/dash-mapbox/HEAD/assets/github.png -------------------------------------------------------------------------------- /assets/medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThibaudLamothe/dash-mapbox/HEAD/assets/medium.png -------------------------------------------------------------------------------- /assets/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThibaudLamothe/dash-mapbox/HEAD/assets/database.png -------------------------------------------------------------------------------- /data/pickle/world_info.p: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThibaudLamothe/dash-mapbox/HEAD/data/pickle/world_info.p -------------------------------------------------------------------------------- /config.example.ini: -------------------------------------------------------------------------------- 1 | [mapbox] 2 | token=your_token 3 | 4 | [path] 5 | data=./data/ 6 | name=covid-19-pandemic-worldwide-data.csv -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | dash==1.17.0 2 | dash-daq==0.5.0 3 | gunicorn==20.0.4 4 | logzero==1.6.3 5 | numpy==1.19.4 6 | pandas==1.1.4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Unuseful files 2 | __pycache__ 3 | .DS_Store 4 | .ipynb_checkpoints 5 | .vscode 6 | .idea 7 | *.pyc 8 | 9 | 10 | # Data 11 | *.jl 12 | *.json 13 | *.token 14 | *.ipynb 15 | config.ini 16 | notebook/*/ 17 | activate_utils.sh 18 | 19 | DockerFile 20 | docker-compose.yml 21 | 22 | gist -------------------------------------------------------------------------------- /update_data.sh: -------------------------------------------------------------------------------- 1 | rm data/raw/covid-19-pandemic-worldwide-data.csv 2 | rm data/pickle/world_info.p 3 | 4 | wget -O data/raw/covid-19-pandemic-worldwide-data.csv "https://public.opendatasoft.com/explore/dataset/covid-19-pandemic-worldwide-data/download/?format=csv&timezone=Europe/Berlin&lang=fr&use_labels_for_header=true&csv_separator=%3B" 5 | 6 | cd scripts && echo ls && python create_world_fig.py 7 | cd .. 8 | python app.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dash-mapbox 2 | 3 | App available on Heroku [here](https://covid-19-worldmap.herokuapp.com/) 4 | 5 | ## Data 6 | 7 | - [worldwide dataset](https://public.opendatasoft.com/explore/dataset/covid-19-pandemic-worldwide-data/table/?disjunctive.zone&disjunctive.category) 8 | - [LEADER (CSSEGIS)](https://github.com/CSSEGISandData/COVID-19) 9 | 10 | ## Other resources 11 | 12 | https://plotly.com/python/animations/ => from animation to dash 13 | 14 | - Geo coding monde : https://geojson-maps.ash.ms/ 15 | - https://plotly.com/python/hover-text-and-formatting/ 16 | - https://community.plotly.com/t/remove-trace-0-next-to-hover/33731 17 | - http://geojson.io/#map=1/45/-210 18 | - https://en.wikipedia.org/wiki/GeoJSON 19 | - https://plotly.com/python/mapbox-county-choropleth/ 20 | - https://public.opendatasoft.com/explore/?sort=modified&q=covid 21 | - https://plotly.com/python/maps/ 22 | 23 | ## Notes 24 | 25 | To run update data script, need to have wget installed -------------------------------------------------------------------------------- /scripts/utils_covid.py: -------------------------------------------------------------------------------- 1 | # Import packages 2 | import os 3 | import pickle 4 | import configparser 5 | 6 | # Load config 7 | config = configparser.ConfigParser() 8 | 9 | 10 | if 'config.example.ini' in os.listdir(): 11 | config.read('config.example.ini') 12 | else: 13 | config.read('../config.example.ini') 14 | 15 | if 'config.ini' in os.listdir(): 16 | config.read('config.ini') 17 | else: 18 | config.read('../config.ini') 19 | 20 | # Create path for project use 21 | DATA_PATH = config['path']['data'] 22 | RAW_PATH = DATA_PATH + 'raw/' 23 | PICKLE_PATH = DATA_PATH + 'pickle/' 24 | 25 | 26 | def load_pickle(file_name): 27 | file_path = PICKLE_PATH + file_name 28 | with open(file_path, 'rb') as pfile: 29 | my_pickle = pickle.load(pfile) 30 | return my_pickle 31 | 32 | 33 | def save_pickle(object_, file_name): 34 | file_path = PICKLE_PATH + file_name 35 | with open(file_path, 'wb') as pfile: 36 | # pickle.dump(object_, pfile, protocol=pickle.HIGHEST_PROTOCOL) 37 | pickle.dump(object_, pfile, protocol=2) 38 | 39 | 40 | def list_pickle(): 41 | file_list = os.listdir(PICKLE_PATH) 42 | pickle_list = [i for i in file_list if '.p' in i] 43 | print(pickle_list) 44 | 45 | 46 | def spacify_number(number): 47 | nb_rev = str(number)[::-1] 48 | new_chain = '' 49 | for val, letter in enumerate(nb_rev): 50 | if val%3==0: 51 | new_chain += ' ' 52 | new_chain += letter 53 | final_chain = new_chain[::-1] 54 | return final_chain -------------------------------------------------------------------------------- /assets/styles.css: -------------------------------------------------------------------------------- 1 | 2 | /* Base Styles 3 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 4 | /* NOTE 5 | html is set to 62.5% so that all the REM measurements throughout Skeleton 6 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 7 | html { 8 | font-size: 62.5%; 9 | } 10 | body { 11 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 12 | line-height: 1.6; 13 | font-weight: 400; 14 | font-family: "Open Sans", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 15 | color: rgb(50, 50, 50); 16 | } 17 | 18 | /* Typography 19 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 20 | h1, 21 | h2, 22 | h3, 23 | h4, 24 | h5, 25 | h6 { 26 | margin-top: 0; 27 | margin-bottom: 0; 28 | font-weight: 300; 29 | } 30 | h1 { 31 | font-size: 4.5rem; 32 | line-height: 1.2; 33 | letter-spacing: -0.1rem; 34 | margin-bottom: 2rem; 35 | } 36 | h2 { 37 | font-size: 3.6rem; 38 | line-height: 1.25; 39 | letter-spacing: -0.1rem; 40 | margin-bottom: 1.8rem; 41 | margin-top: 1.8rem; 42 | } 43 | 44 | p { 45 | margin-top: 0; 46 | } 47 | 48 | /* Links 49 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 50 | a { 51 | color: #1eaedb; 52 | text-decoration: underline; 53 | cursor: pointer; 54 | padding-left: 20px; 55 | padding-right: 20px; 56 | padding-top: 10px; 57 | padding-bottom: 10px; 58 | } 59 | a:hover { 60 | color: #0fa0ce; 61 | } 62 | 63 | /* Custom 64 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 65 | 66 | body { 67 | background-color: #f2f2f2; 68 | } 69 | 70 | .pretty_container { 71 | border-radius: 5px; 72 | background-color: #f9f9f9; 73 | margin: 10px; 74 | padding: 15px; 75 | position: relative; 76 | box-shadow: 2px 2px 2px lightgrey; 77 | } 78 | 79 | .bare_container { 80 | margin: 0 0 0 0; 81 | padding: 0 0 0 0; 82 | } 83 | 84 | .dcc_control { 85 | margin: 0; 86 | padding: 5px; 87 | width: calc(100%-40px); 88 | } 89 | 90 | .control_label { 91 | margin: 0; 92 | padding: 10px; 93 | padding-bottom: 0px; 94 | margin-bottom: 0px; 95 | width: calc(100%-40px); 96 | } 97 | 98 | 99 | 100 | /* ____________________________________________________________*/ 101 | /* _____________________________ WORLD_________________________*/ 102 | /* ____________________________________________________________*/ 103 | 104 | 105 | section { 106 | width:100%; 107 | max-width:100%; 108 | display: flex; 109 | flex-direction: column; 110 | } 111 | 112 | .header { 113 | display: flex; 114 | flex-direction: column; 115 | justify-content: center; 116 | align-items: center; 117 | margin: auto; 118 | 119 | } 120 | 121 | #world_line_1{ 122 | display: flex; 123 | 124 | justify-content: center; 125 | } 126 | 127 | #world_map{ 128 | width:100%; 129 | } 130 | 131 | #world_line_2{ 132 | display: flex; 133 | width: 100%; 134 | justify-content: center; 135 | align-items: center; 136 | 137 | } 138 | 139 | .mini_container { 140 | border-radius: 5px; 141 | background-color: #f9f9f9; 142 | margin: 10px; 143 | padding: 15px; 144 | position: relative; 145 | box-shadow: 2px 2px 2px lightgrey; 146 | min-width:25%; 147 | justify-content: center; 148 | text-align: center; 149 | } 150 | 151 | #platforms_links{ 152 | display: flex; 153 | align-items: space-around; 154 | justify-content: center; 155 | } 156 | a { 157 | display: flex; 158 | justify-content: center; 159 | align-items: center; 160 | flex-direction: column; 161 | font-size: 10px; 162 | color: black; 163 | text-decoration: none; 164 | } -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | ############################################################################################ 2 | ########################################## IMPORTS ######################################### 3 | ############################################################################################ 4 | 5 | # Classic libraries 6 | import os 7 | import numpy as np 8 | import pandas as pd 9 | 10 | # Logging information 11 | import logging 12 | import logzero 13 | from logzero import logger 14 | 15 | # Dash imports 16 | import dash 17 | import dash_core_components as dcc 18 | import dash_html_components as html 19 | 20 | # Custom function 21 | import scripts.utils_covid as f 22 | 23 | ############################################################################################ 24 | ############################## PARAMETERS and PRE-COMPUTATION ############################## 25 | ############################################################################################ 26 | 27 | # Load pre computed data 28 | world = f.load_pickle('world_info.p') 29 | 30 | # Deployment inforamtion 31 | PORT = 8050 32 | 33 | ############################################################################################ 34 | ########################################## APP ############################################# 35 | ############################################################################################ 36 | 37 | # Creating app 38 | app = dash.Dash( 39 | __name__, meta_tags=[{"name": "viewport", "content": "width=device-width"}] 40 | ) 41 | 42 | # Associating server 43 | server = app.server 44 | app.title = 'COVID 19 - World cases' 45 | app.config.suppress_callback_exceptions = True 46 | 47 | ############################################################################################ 48 | ######################################### LAYOUT ########################################### 49 | ############################################################################################ 50 | 51 | links = html.Div( 52 | id='platforms_links', 53 | children=[ 54 | html.A( 55 | href='https://towardsdatascience.com/how-to-create-animated-scatter-maps-with-plotly-and-dash-f10bb82d357a', 56 | children=[ 57 | html.Img(src=app.get_asset_url('medium.png'), width=20, height=20), 58 | html.Span("Map") 59 | ] 60 | ), 61 | html.A( 62 | href='https://medium.com/@thibaud.lamothe2/deploying-dash-or-flask-web-application-on-heroku-easy-ci-cd-4111da3170b8', 63 | children=[ 64 | html.Img(src=app.get_asset_url('medium.png'), width=20, height=20), 65 | html.Span("Deploy") 66 | ] 67 | ), 68 | html.A( 69 | href='https://github.com/ThibaudLamothe/dash-mapbox', 70 | children=[ 71 | html.Img(src=app.get_asset_url('github.png'), width=20, height=20), 72 | # "Application code" 73 | html.Span("Code") 74 | ] 75 | ), 76 | html.A( 77 | href='https://public.opendatasoft.com/explore/dataset/covid-19-pandemic-worldwide-data/information/?disjunctive.zone&disjunctive.category&sort=date', 78 | children=[ 79 | html.Img(src=app.get_asset_url('database.png'), width=20, height=20), 80 | # "Original COVID dataset" 81 | html.Span("Data") 82 | ], 83 | ), 84 | ], 85 | ) 86 | 87 | 88 | 89 | app.layout = html.Div( 90 | children=[ 91 | 92 | # HEADER 93 | html.Div( 94 | className="header", 95 | children=[ 96 | html.H1("COVID 19 🦠 - Day to day evolution all over the world", className="header__text"), 97 | html.Span('(Last update: {})'.format(world['last_date'])), 98 | # html.Hr(), 99 | ], 100 | ), 101 | 102 | # CONTENT 103 | html.Section([ 104 | 105 | # Line 1 : KPIS - World 106 | html.Div( 107 | id='world_line_1', 108 | children = [ 109 | html.Div(children = ['🚨 Confirmed', html.Br(), world['total_confirmed']], id='confirmed_world_total', className='mini_container'), 110 | html.Div(children = ['🏡 Recovered', html.Br(), world['total_recovered']], id='recovered_world_total', className='mini_container'), 111 | html.Div(children = [' ⚰️ Victims', html.Br(), world['total_deaths']], id='deaths_world_total', className='mini_container'), 112 | ], 113 | ), 114 | # html.Br(), 115 | links, 116 | 117 | # Line 2 : MAP - WORLD 118 | html.Div( 119 | id='world_line_2', 120 | children = [ 121 | dcc.Graph(id='world_map', figure=world['figure'], config={'scrollZoom': False}), 122 | ], 123 | ), 124 | # html.Br(), 125 | ]), 126 | ], 127 | ) 128 | 129 | ############################################################################################ 130 | ######################################### RUNNING ########################################## 131 | ############################################################################################ 132 | 133 | if __name__ == "__main__": 134 | 135 | # Display app start 136 | logger.error('*' * 80) 137 | logger.error('App initialisation') 138 | logger.error('*' * 80) 139 | 140 | # Starting flask server 141 | app.run_server(debug=True, port=PORT) 142 | -------------------------------------------------------------------------------- /scripts/create_world_fig.py: -------------------------------------------------------------------------------- 1 | import json 2 | import numpy as np 3 | import pandas as pd 4 | from logzero import logger 5 | from plotly import graph_objs as go 6 | import plotly.express as px 7 | import utils_covid as f 8 | pd.set_option('chained_assignment', None) 9 | 10 | ################################################################################################ 11 | ################################################################################################ 12 | ################################################################################################ 13 | 14 | def process_pandemic_data(df, startdate = '2020-03-01'): 15 | 16 | # Columns renaming 17 | df.columns = [col.lower() for col in df.columns] 18 | 19 | # Keep only from a starting date 20 | df = df[df['date'] > startdate] 21 | 22 | # Create a zone per zone/subzone 23 | df['zone'] = df['zone'].apply(str) + ' ' + df['sub zone'].apply(lambda x: str(x).replace('nan', '')) 24 | 25 | # Extracting latitute and longitude 26 | df['lat'] = df['location'].apply(lambda x: x.split(',')[0]) 27 | df['lon'] = df['location'].apply(lambda x: x.split(',')[1]) 28 | 29 | # Saving countries positions (latitude and longitude per subzones) 30 | country_position = df[['zone', 'lat', 'lon']].drop_duplicates(['zone']).set_index(['zone']) 31 | 32 | # Pivoting per category 33 | df = pd.pivot_table(df, values='count', index=['date', 'zone'], columns=['category']) 34 | df.columns = ['confirmed', 'deaths', 'recovered'] 35 | 36 | # Merging locations after pivoting 37 | df = df.join(country_position) 38 | 39 | # Filling nan values with 0 40 | df = df.fillna(0) 41 | 42 | # Compute bubble sizes 43 | df['size'] = df['confirmed'].apply(lambda x: (np.sqrt(x/100) + 1) if x > 500 else (np.log(x) / 2 + 1)).replace(np.NINF, 0) * 0.5 44 | df['color'] = (df['recovered']/df['confirmed']).fillna(0).replace(np.inf , 0) * 100 45 | 46 | # Prepare display values for bubble hover 47 | df['confirmed_display'] = df['confirmed'].apply(int).apply(f.spacify_number) 48 | df['recovered_display'] = df['recovered'].apply(int).apply(f.spacify_number) 49 | df['deaths_display'] = df['deaths'].apply(int).apply(f.spacify_number) 50 | 51 | 52 | return df 53 | 54 | 55 | def create_world_fig(df, mapbox_access_token): 56 | 57 | days = df.index.levels[0].tolist() 58 | # day = min(days) 59 | 60 | # Defining each Frame 61 | frames = [{ 62 | # 'traces':[0], 63 | 'name':'frame_{}'.format(day), 64 | 'data':[{ 65 | 'type':'scattermapbox', 66 | 'lat':df.xs(day)['lat'], 67 | 'lon':df.xs(day)['lon'], 68 | 'marker':go.scattermapbox.Marker( 69 | size=df.xs(day)['size'], 70 | color=df.xs(day)['color'], 71 | showscale=True, 72 | colorbar={'title':'Recovered', 'titleside':'top', 'thickness':4, 'ticksuffix':' %'}, 73 | # color_continuous_scale=px.colors.cyclical.IceFire, 74 | ), 75 | 'customdata':np.stack((df.xs(day)['confirmed_display'], df.xs(day)['recovered_display'], df.xs(day)['deaths_display'], pd.Series(df.xs(day).index)), axis=-1), 76 | 'hovertemplate': "%{customdata[3]}
🚨 %{customdata[0]}
🏡 %{customdata[1]}
⚰️ %{customdata[2]}", 77 | }], 78 | } for day in days] 79 | 80 | 81 | # Prepare the frame to display 82 | data = frames[-1]['data'] 83 | 84 | # And specify the adequate button postion 85 | active_frame=len(days) - 1 86 | 87 | # Defining the slider to navigate between frames 88 | sliders = [{ 89 | 'active':active_frame, 90 | 'transition':{'duration': 0}, 91 | 'x':0.08, #slider starting position 92 | 'len':0.88, 93 | 'currentvalue':{ 94 | 'font':{'size':15}, 95 | 'prefix':'📅 ', # Day: 96 | 'visible':True, 97 | 'xanchor':'center' 98 | }, 99 | 'steps':[{ 100 | 'method':'animate', 101 | 'args':[ 102 | ['frame_{}'.format(day)], 103 | { 104 | 'mode':'immediate', 105 | 'frame':{'duration':250, 'redraw': True}, #100 106 | 'transition':{'duration':100} #50 107 | } 108 | ], 109 | 'label':day 110 | } for day in days] 111 | }] 112 | 113 | play_button = [{ 114 | 'type':'buttons', 115 | 'showactive':True, 116 | 'y':-0.08, 117 | 'x':0.045, 118 | 'buttons':[{ 119 | 'label':'🎬', # Play 120 | 'method':'animate', 121 | 'args':[ 122 | None, 123 | { 124 | 'frame':{'duration':250, 'redraw':True}, #100 125 | 'transition':{'duration':100}, #50 126 | 'fromcurrent':True, 127 | 'mode':'immediate', 128 | } 129 | ] 130 | }] 131 | }] 132 | 133 | # Global Layout 134 | layout = go.Layout( 135 | height=600, 136 | autosize=True, 137 | hovermode='closest', 138 | paper_bgcolor='rgba(0,0,0,0)', 139 | mapbox={ 140 | 'accesstoken':mapbox_access_token, 141 | 'bearing':0, 142 | 'center':{"lat": 37.86, "lon": 2.15}, 143 | 'pitch':0, 144 | 'zoom':1.7, 145 | 'style':'light', 146 | }, 147 | updatemenus=play_button, 148 | sliders=sliders, 149 | margin={"r":0,"t":0,"l":0,"b":0} 150 | ) 151 | 152 | 153 | return go.Figure(data=data, layout=layout, frames=frames) 154 | 155 | ################################################################################################ 156 | ################################################################################################ 157 | ################################################################################################ 158 | 159 | 160 | if __name__ =="__main__": 161 | 162 | # Loading necessary information 163 | mapbox_access_token = f.config['mapbox']['token'] 164 | raw_dataset_path = f.RAW_PATH + f.config['path']['name'] 165 | 166 | # Creating dataFrames 167 | df_raw = pd.read_csv(raw_dataset_path, sep=';') 168 | df_world = process_pandemic_data(df_raw) 169 | df_total_kpi = df_world.groupby('date').sum().sort_index().iloc[-1] 170 | 171 | # Preparing figure 172 | fig_world = create_world_fig(df_world, mapbox_access_token=mapbox_access_token) 173 | 174 | # Storing all necessay information for app 175 | save = { 176 | 'figure':fig_world, 177 | 'last_date':df_world.index[-1][0], 178 | 'total_confirmed': f.spacify_number(int(df_total_kpi['confirmed'])), 179 | 'total_deaths': f.spacify_number(int(df_total_kpi['deaths'])), 180 | 'total_recovered': f.spacify_number(int(df_total_kpi['recovered'])) 181 | } 182 | f.save_pickle(save, 'world_info.p') 183 | 184 | # Display information 185 | logger.info('World map updated.') 186 | logger.info('Data sotred for dash application.') 187 | logger.info('Last date in new dataset is {}'.format(df_world.index[-1][0])) 188 | 189 | 190 | --------------------------------------------------------------------------------