Source code for Simple App Without Login

Source code for Simple App Without Login#

app.py#

# Simple Dash app for displaying Virginia weather data

# By Kenneth Burchfiel
# Released under the MIT License

# This app features a graph of recent weather data,
# along with a menu that allows users to specify the metric being
# displayed; the weather stations for which to display it; and 
# the number of days to plot within the graph. When viewers change
# the selections within the menu, the weather graph will update
# automatically. 

# Note: the Cloud Run-hosted copy of this app can be found 
# at https://simpleappwithoutlogin-470317599391.us-central1.run.app/ .

# (Seethe  readme for more information on the code used to connect
# to a Google Sheets workbook with recent weather data)

# Parts of the following code were based on the Dash app tutorial
# at https://dash.plotly.com/tutorial ; the Dash callbacks documentation at
# https://dash.plotly.com/basic-callbacks ; the online Dash deployment
# guide at https://dash.plotly.com/deployment ; the Dash Bootstrap intro at
# https://dash-bootstrap-components.opensource.faculty.ai/docs/quickstart/ 
# ; and the Dash Bootstrap Layout documentation at
# https://dash-bootstrap-components.opensource
# .faculty.ai/docs/components/layout/ .

import gspread

# From https://pypi.org/project/gspread-dataframe/
from gspread_dataframe import get_as_dataframe

from dash import Dash, html, dcc, dash_table, callback, Input, Output
import plotly.express as px
import pandas as pd
import dash_bootstrap_components as dbc
from datetime import datetime, timedelta

print("Initializing gspread using service account key stored within \
Cloud Run secrets volume:")


local_data_import = False # This allows data to get imported from local
# .csv files, which can help save time when debugging. It must be 
# set to False prior to exporting the app to Cloud Run, however.

if local_data_import == False:
# Guillaume Blaquiere's post at 
# https://stackoverflow.com/a/68536068/13097194 
# was helpful in drafting the following line. 

    gc = gspread.service_account(
    filename = '/svcacctsecret/kjb3server_service_account')
    wb = gc.open_by_key('17aDJ3mg49-n0IEnDgN7ZB85pO87fiUpkZPULYDB8dmo')

# Note: the code above will only run successfully when the app is
# deployed to Cloud Run. That's because the file path is actually a volume
# within my Cloud Run container.
# (See the readme for further details on successfully running
# this code on your end.)

# Importing data from each spreadsheet:
wx_df_list = []

for station in ['KCHO', 'KIAD', 'KOKV']:
    if local_data_import == True:
        wx_df_list.append(pd.read_csv(f'../../Updating_Online_\
Spreadsheets/weather_data/{station}_historical_hourly_data_\
updated.csv')[-960:])
    else:
        ws = wb.worksheet(station)
        wx_df_list.append(get_as_dataframe(ws)[-960:]) # Importing up to
        # 40 days of data for each station (in order to keep the charts
        # readable) . The sheets may already only show 40 days' worth
        # of data, but it won't hurt to keep the [-960:] filter in place
        # in case their format changes in the future.

# Combining these station-specific tables into a single DataFrame:

df_wx = pd.concat([df for df in wx_df_list])

df_wx['Date/Time'] = pd.to_datetime(df_wx['Date/Time'])
print(df_wx.tail())


current_date = datetime.today()

app=Dash(external_stylesheets = [dbc.themes.BOOTSTRAP])
server = app.server

# Specifying the layout of our webpage:
# This layout will include various row and column elements within
# an overarching dbc.Container element. It also features a 
# viewer-customizable graph that will get created later within this script.

# The dcc.Markdown() code below was based on
# https://dash.plotly.com/dash-core-components/markdown .

app.layout = dbc.Container(
    [dbc.Row(dcc.Markdown('''
## Recent Weather Data for Three Virginia Airports
This Dash app is part of [Python for Nonprofits]\
(https://github.com/kburchfiel/pfn) and has been 
released under the MIT license. The source code for this app can 
be viewed at [this page](https://github.com/kburchfiel/pfn/blob/main/\
Online_Visualizations/Simple_App_Without_Login/app.py). 

The NWS data displayed within this notebook was accessed via
[this Google Sheets file](https://docs.google.com/spreadsheets/d/\
17aDJ3mg49-n0IEnDgN7ZB85pO87fiUpkZPULYDB8dmo). 
This file gets updated with new NWS data
on an hourly basis via [this script](https://github.com/kburchfiel/\
pfn/blob/main/Updating_Online_Spreadsheets/\
updating_online_spreadsheets.py), which in turn calls 
[this script](https://github.com/kburchfiel/pfn/blob/main/\
Updating_Online_Spreadsheets/weather_import.py).

A more complex \
Dash app can be found within [this part of PFN]\
(https://github.com/kburchfiel/pfn/tree/main/\
Online_Visualizations/PFN_Dash_App_Demo).''')),

# Note that many of the following dbc.Row() objects contain multiple
# dbc.Col() objects. This allows for a more concise set of headers
# and options. The 'md=' elements specify how wide certain elements 
# should be within larger screens; when the app is accessed via smaller
# screens (such as cell phones), these elements will get rearranged
# for easier viewing.
     
dbc.Row([
    dbc.Col(
        dcc.Markdown('**Metric**:'), md=2),
    dbc.Col(
        dcc.Dropdown(
    options=['Temp', 'Dew Point', 
'1-Hour Precip', 'Rolling 3-Hour Precip', 'Rolling 6-Hour Precip',
'Rolling 12-Hour Precip', 'Rolling 24-Hour Precip',
'Altimeter (in.)', 'Windspeed'], value='Temp', id='metric'), 
        md=3)]),
 dbc.Row([
     dbc.Col(dcc.Markdown('**Stations:**'), md=2),
     dbc.Col(dcc.Dropdown(['KCHO', 'KIAD', 'KOKV'], 
                      ['KCHO', 'KIAD', 'KOKV'], 
                          multi=True,
                     id='station_list'),  md=4)]),
dbc.Row([dbc.Col(dcc.Markdown('**Days to Include:**'), 
                md=2),
        dbc.Col(dcc.Slider(1, 40, 3, value=10, 
        # I had originally used one-day increments for the slider,
       # but this resulted in overlapping text on mobile displays.
                   id='days_to_include'))]),
# The following Graph object will get created via the plot_graph()
     # function shown below. This function allows users' choices
     # within the menu defined above to shape the appearance of the 
     # chart.
dbc.Row(dcc.Graph(id='fig')),                                   
dbc.Row(dcc.Markdown("By Kenneth Burchfiel"))])


# Defining a callback function that, given the inputs specified
# above, plots and returns a graph of weather data:
@callback(
    Output('fig', 'figure'),
    Input('metric', 'value'),
    Input('station_list', 'value'),
    Input('days_to_include', 'value'))

def plot_graph(metric, station_list, days_to_include):
    # Determining the earliest point at which data should be displayed:
    data_cutoff = str(current_date - timedelta(
        days=days_to_include))

    # Modifying the title so that it's grammatically correct when
    # only one day of data is being displayed:
    if days_to_include == 1:
        day_title_component = 'Past Day'
    else:
        day_title_component = f'Last {days_to_include} Days'
    
    fig = px.line(
        df_wx.query("(Station in @station_list) \
    & (`Date/Time` >= @data_cutoff)"), 
        x='Date/Time', y=metric,
        color='Station',
        title=f"{metric} Over {day_title_component}")
    
    # Updating y axis title based on the selected metric:
    fig.update_layout(xaxis_title='Date')
    if metric in ['Temp', 'Dew Point']:
        fig.update_layout(yaxis_title='Degrees (F)')
    if metric == 'Windspeed':
        fig.update_layout(yaxis_title='Windspeed (mph)')
    if 'Precip' in metric:
        fig.update_layout(yaxis_title='Precipitation (in.)')
    return fig


if __name__ == '__main__':
    app.run(debug=True)

Procfile#

web: gunicorn app:server

requirements.txt#

gspread
gspread-dataframe
dash
plotly
pandas
gunicorn
dash-bootstrap-components