├── .gitignore ├── Procfile ├── requirements.txt ├── README.md └── app.py /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | __pycache__ -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: gunicorn app:server -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2017.7.27.1 2 | chardet==3.0.4 3 | click==6.7 4 | dash==0.17.7 5 | dash-core-components==0.9.0 6 | dash-html-components==0.7.0 7 | dash-renderer==0.7.4 8 | decorator==4.1.2 9 | Flask==1.0 10 | Flask-Caching==1.3.2 11 | Flask-Compress==1.4.0 12 | Flask-SeaSurf==0.2.2 13 | gunicorn==19.7.1 14 | idna==2.5 15 | ipython-genutils==0.2.0 16 | itsdangerous==0.24 17 | Jinja2>=2.10.1 18 | jsonschema==2.6.0 19 | jupyter-core==4.3.0 20 | MarkupSafe==1.0 21 | nbformat==4.3.0 22 | plotly==2.0.12 23 | python-dotenv==0.6.4 24 | pytz==2017.2 25 | requests==2.20.0 26 | six==1.10.0 27 | toolz==0.8.2 28 | traitlets==4.3.2 29 | urllib3==1.24.2 30 | Werkzeug==0.15.3 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bigfoot Sightings Dash App 2 | 3 | This is an example of an app built with Plotly's [Dash](https://plot.ly/products/dash/) framework. 4 | It's an exploratory app based on the [Bigfoot Sightings](https://data.world/timothyrenner/bfro-sightings-data) dataset I hosted on data.world. 5 | It demonstrates several plots (including a map), a grid layout built with [Bootstrap](http://getbootstrap.com/), interactions with an input field, and caching. 6 | 7 | There's also a Procfile for deploying on to Heroku. 8 | That does require a little special sauce in the code - I've tried to be clear in the comments where that is so you can ignore it if you want. 9 | 10 | ## Quickstart 11 | 12 | Create an environment with virtualenv or conda. 13 | For conda, 14 | 15 | ``` 16 | conda create --name bigfoot-sightings-dash python=3.6 17 | source activate bigfoot-sightings-dash 18 | ``` 19 | 20 | Install the stuff in `requirements.txt`. 21 | 22 | ``` 23 | pip install -r requirements.txt 24 | ``` 25 | 26 | This app requires a [mapbox](https://www.mapbox.com/) key for the map to render. 27 | It needs to be assigned to the `MAPBOX_KEY` environment variable. 28 | The code will also read from a `.env` file. 29 | 30 | Launch the app. 31 | 32 | ``` 33 | python app.py 34 | ``` -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import dash 2 | import dash_core_components as dcc 3 | import dash_html_components as html 4 | from dash.dependencies import Input, Output 5 | from flask_caching import Cache 6 | 7 | from csv import DictReader 8 | from toolz import compose, pluck, groupby, valmap, first, unique, get, countby 9 | import datetime as dt 10 | from dotenv import find_dotenv,load_dotenv 11 | 12 | import os 13 | 14 | load_dotenv(find_dotenv()) 15 | 16 | ################################################################################ 17 | # HELPERS 18 | ################################################################################ 19 | listpluck = compose(list, pluck) 20 | listfilter = compose(list, filter) 21 | listmap = compose(list, map) 22 | listunique = compose(list, unique) 23 | 24 | TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%SZ" 25 | 26 | # Datetime helpers. 27 | def sighting_year(sighting): 28 | return dt.datetime.strptime(sighting['timestamp'], TIMESTAMP_FORMAT).year 29 | 30 | def sighting_dow(sighting): 31 | return dt.datetime.strptime(sighting['timestamp'], TIMESTAMP_FORMAT)\ 32 | .strftime("%a") 33 | 34 | ################################################################################ 35 | # PLOTS 36 | ################################################################################ 37 | def bigfoot_map(sightings): 38 | classifications = groupby('classification', sightings) 39 | return { 40 | "data": [ 41 | { 42 | "type": "scattermapbox", 43 | "lat": listpluck("latitude", class_sightings), 44 | "lon": listpluck("longitude", class_sightings), 45 | "text": listpluck("title", class_sightings), 46 | "mode": "markers", 47 | "name": classification, 48 | "marker": { 49 | "size": 3, 50 | "opacity": 1.0 51 | } 52 | } 53 | for classification, class_sightings in classifications.items() 54 | ], 55 | "layout": { 56 | "autosize": True, 57 | "hovermode": "closest", 58 | "mapbox": { 59 | "accesstoken": os.environ.get("MAPBOX_KEY"), 60 | "bearing": 0, 61 | "center": { 62 | "lat": 40, 63 | "lon": -98.5 64 | }, 65 | "pitch": 0, 66 | "zoom": 2, 67 | "style": "outdoors" 68 | } 69 | } 70 | } 71 | 72 | def bigfoot_by_year(sightings): 73 | # Create a dict mapping the 74 | # classification -> [(year, count), (year, count) ... ] 75 | sightings_by_year = { 76 | classification: 77 | sorted( 78 | list( 79 | # Group by year -> count. 80 | countby(sighting_year, class_sightings).items() 81 | ), 82 | # Sort by year. 83 | key=first 84 | ) 85 | for classification, class_sightings 86 | in groupby('classification', sightings).items() 87 | } 88 | 89 | # Build the plot with a dictionary. 90 | return { 91 | "data": [ 92 | { 93 | "type": "scatter", 94 | "mode": "lines+markers", 95 | "name": classification, 96 | "x": listpluck(0, class_sightings_by_year), 97 | "y": listpluck(1, class_sightings_by_year) 98 | } 99 | for classification, class_sightings_by_year 100 | in sightings_by_year.items() 101 | ], 102 | "layout": { 103 | "title": "Sightings by Year", 104 | "showlegend": False 105 | } 106 | } 107 | 108 | def bigfoot_dow(sightings): 109 | 110 | # Produces a dict (year, dow) => count. 111 | sightings_dow = countby("dow", 112 | [ 113 | { 114 | "dow": sighting_dow(sighting) 115 | } 116 | for sighting in sightings 117 | ] 118 | ) 119 | 120 | dows = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] 121 | 122 | return { 123 | "data": [ 124 | { 125 | "type": "bar", 126 | "x": dows, 127 | "y": [get(d, sightings_dow, 0) for d in dows] 128 | } 129 | ], 130 | "layout": { 131 | "title": "Sightings by Day of Week", 132 | } 133 | } 134 | 135 | def bigfoot_class(sightings): 136 | sightings_by_class = countby("classification", sightings) 137 | 138 | return { 139 | "data": [ 140 | { 141 | "type": "pie", 142 | "labels": list(sightings_by_class.keys()), 143 | "values": list(sightings_by_class.values()), 144 | "hole": 0.4 145 | } 146 | ], 147 | "layout": { 148 | "title": "Sightings by Class" 149 | } 150 | } 151 | 152 | ################################################################################ 153 | # APP INITIALIZATION 154 | ################################################################################ 155 | # Read the data. 156 | fin = open('data/bfro_report_locations.csv','r') 157 | reader = DictReader(fin) 158 | BFRO_LOCATION_DATA = \ 159 | [ 160 | line for line in reader 161 | if (sighting_year(line) <= 2017) and (sighting_year(line) >= 1900) 162 | ] 163 | fin.close() 164 | 165 | app = dash.Dash() 166 | # For Heroku deployment. 167 | server = app.server 168 | # Don't understand this one bit, but apparently it's needed. 169 | server.secret_key = os.environ.get("SECRET_KEY", "secret") 170 | 171 | app.title = "Bigfoot Sightings" 172 | cache = Cache(app.server, config={"CACHE_TYPE": "simple"}) 173 | 174 | 175 | # This function can be memoized because it's called for each graph, so it will 176 | # only get called once per filter text. 177 | @cache.memoize(10) 178 | def filter_sightings(filter_text): 179 | return listfilter( 180 | lambda x: filter_text.lower() in x['title'].lower(), 181 | BFRO_LOCATION_DATA 182 | ) 183 | 184 | ################################################################################ 185 | # LAYOUT 186 | ################################################################################ 187 | app.css.append_css({ 188 | "external_url": "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" 189 | }) 190 | 191 | app.css.append_css({ 192 | "external_url": 'https://codepen.io/chriddyp/pen/bWLwgP.css' 193 | }) 194 | 195 | app.scripts.append_script({ 196 | "external_url": "https://code.jquery.com/jquery-3.2.1.min.js" 197 | }) 198 | 199 | app.scripts.append_script({ 200 | "external_url": "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" 201 | }) 202 | 203 | app.layout = html.Div([ 204 | # Row: Title 205 | html.Div([ 206 | # Column: Title 207 | html.Div([ 208 | html.H1("Bigfoot Sightings", className="text-center") 209 | ], className="col-md-12") 210 | ], className="row"), 211 | # Row: Filter + References 212 | html.Div([ 213 | # Column: Filter 214 | html.Div([ 215 | html.P([ 216 | html.B("Filter the titles: "), 217 | dcc.Input( 218 | placeholder="Try 'heard'", 219 | id="bigfoot-text-filter", 220 | value="") 221 | ]), 222 | ], className="col-md-6"), 223 | # Column: References. 224 | html.Div([ 225 | html.P([ 226 | "Data pulled from ", 227 | html.A("bfro.net", href="http://www.bfro.net/"), 228 | ". Grab it at ", 229 | html.A("data.world", href="https://data.world/timothyrenner/bfro-sightings-data"), 230 | "." 231 | ], style={"text-align": "right"}) 232 | ], className="col-md-6") 233 | ], className="row"), 234 | # Row: Map + Bar Chart 235 | html.Div([ 236 | # Column: Map 237 | html.Div([ 238 | dcc.Graph(id="bigfoot-map") 239 | ], className="col-md-8"), 240 | # Column: Bar Chart 241 | html.Div([ 242 | dcc.Graph(id="bigfoot-dow") 243 | ], className="col-md-4") 244 | ], className="row"), 245 | # Row: Line Chart + Donut Chart 246 | html.Div([ 247 | # Column: Line Chart 248 | html.Div([ 249 | dcc.Graph(id="bigfoot-by-year") 250 | ], className="col-md-8"), 251 | # Column: Donut Chart 252 | html.Div([ 253 | dcc.Graph(id="bigfoot-class") 254 | ], className="col-md-4") 255 | ], className="row"), 256 | # Row: Footer 257 | html.Div([ 258 | html.Hr(), 259 | html.P([ 260 | "Built with ", 261 | html.A("Dash", href="https://plot.ly/products/dash/"), 262 | ". Check out the code on ", 263 | html.A("GitHub", href="https://github.com/timothyrenner/bigfoot-dash-app"), 264 | "." 265 | ]) 266 | ], className="row", 267 | style={ 268 | "textAlign": "center", 269 | "color": "Gray" 270 | }) 271 | ], className="container-fluid") 272 | 273 | ################################################################################ 274 | # INTERACTION CALLBACKS 275 | ################################################################################ 276 | @app.callback( 277 | Output('bigfoot-map', 'figure'), 278 | [ 279 | Input('bigfoot-text-filter', 'value') 280 | ] 281 | ) 282 | def filter_bigfoot_map(filter_text): 283 | return bigfoot_map(filter_sightings(filter_text)) 284 | 285 | @app.callback( 286 | Output('bigfoot-by-year', 'figure'), 287 | [ 288 | Input('bigfoot-text-filter', 'value') 289 | ] 290 | ) 291 | def filter_bigfoot_by_year(filter_text): 292 | return bigfoot_by_year(filter_sightings(filter_text)) 293 | 294 | @app.callback( 295 | Output('bigfoot-dow', 'figure'), 296 | [ 297 | Input('bigfoot-text-filter', 'value') 298 | ] 299 | ) 300 | def filter_bigfoot_dow(filter_text): 301 | return bigfoot_dow(filter_sightings(filter_text)) 302 | 303 | @app.callback( 304 | Output('bigfoot-class', 'figure'), 305 | [ 306 | Input('bigfoot-text-filter', 'value') 307 | ] 308 | ) 309 | def filter_bigfoot_class(filter_text): 310 | return bigfoot_class(filter_sightings(filter_text)) 311 | 312 | if __name__ == "__main__": 313 | app.run_server(debug=True) --------------------------------------------------------------------------------