├── 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 |
--------------------------------------------------------------------------------