Last active
June 12, 2020 07:23
-
-
Save tcbegley/7df557221c270486e074811b2770b9ce to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
import dash | |
import dash_core_components as dcc | |
import dash_html_components as html | |
import dash_bootstrap_components as dbc | |
from dash.dependencies import Input, Output | |
import pandas as pd | |
import datetime as dt | |
from io import StringIO | |
# Dash callbacks: https://dash.plot.ly/getting-started-part-2 | |
# wind direction polar plot with dcc: | |
# https://github.com/plotly/dash-sample-apps/blob/master/apps/dash-wind-streaming/app.py | |
external_stylesheets = ["https://codepen.io/chriddyp/pen/bWLwgP.css"] | |
app_color = {"graph_bg": "#082255", "graph_line": "#007ACE"} | |
datafile = "ex.dat" | |
dat_names = [ | |
"wdate", | |
"wind_dir", | |
"wind_speed", | |
"wind_gust", | |
"col5", | |
"humidity", | |
"col7", | |
"temp_out", | |
"col9", | |
"col10", | |
] | |
TESTDATA = StringIO( | |
""" | |
202003190115 359.000 0.000 0.000 100.000 97.000 55.500 27.700 24.986 9.120 | |
202003190235 359.000 0.000 0.000 100.000 97.000 55.100 28.100 24.986 9.120 | |
202003190350 359.000 0.000 0.000 100.000 97.000 54.800 28.500 24.986 9.120 | |
202003190515 359.000 5.000 8.000 100.000 97.000 54.500 28.400 24.986 9.120 | |
202003190635 359.000 0.000 0.000 100.000 97.000 54.000 28.000 24.986 9.120 | |
202003190750 359.000 0.000 0.000 100.000 97.000 53.100 27.700 24.986 9.120 | |
202003190915 359.000 0.000 0.000 100.000 97.000 53.300 29.000 24.986 9.120 | |
202003191035 359.000 0.000 0.000 100.000 98.000 54.600 31.300 24.986 9.120 | |
202003191150 241.000 0.000 0.000 100.000 98.000 56.100 32.600 24.986 9.120 | |
202003191315 359.000 0.000 0.000 100.000 97.000 58.700 34.600 24.986 9.120 | |
202003191435 359.000 0.000 0.000 100.000 97.000 59.700 33.500 24.986 9.120 | |
202003191550 290.000 0.000 0.000 100.000 98.000 59.700 33.200 24.986 9.130 | |
202003191715 15.000 0.000 0.000 100.000 98.000 59.600 32.600 24.986 9.160 | |
202003191835 23.000 0.000 0.000 100.000 98.000 59.600 33.200 24.986 9.160 | |
202003191950 23.000 0.000 0.000 100.000 98.000 59.100 32.800 24.986 9.160 | |
202003192115 18.000 0.000 0.000 100.000 98.000 58.400 32.100 24.986 9.160 | |
202003192235 22.000 0.000 0.000 100.000 98.000 57.700 32.500 24.986 9.160 | |
202003192350 37.000 0.000 0.000 100.000 98.000 57.100 32.100 24.986 9.170 | |
202003200115 41.000 0.000 0.000 100.000 98.000 56.700 31.600 24.986 9.170 | |
202003200235 89.000 0.000 0.000 100.000 98.000 56.300 31.200 24.986 9.170 | |
202003200350 93.000 0.000 0.000 100.000 98.000 55.700 30.100 24.986 9.170 | |
202003200515 90.000 0.000 0.000 100.000 98.000 55.400 30.600 24.986 9.170 | |
202003200635 359.000 0.000 0.000 100.000 98.000 54.800 29.400 24.986 9.170 | |
202003200750 359.000 0.000 0.000 100.000 98.000 54.200 30.500 24.986 9.170 | |
202003200915 60.000 0.000 0.000 100.000 98.000 55.200 31.600 24.986 9.170 | |
202003201035 97.000 0.000 0.000 100.000 98.000 56.500 32.800 24.986 9.170 | |
202003201150 359.000 3.000 6.000 100.000 98.000 58.800 34.600 24.986 9.170 | |
202003201315 359.000 0.000 0.000 100.000 97.000 60.300 35.900 24.986 9.170 | |
202003201435 359.000 0.000 0.000 100.000 97.000 61.300 36.900 24.986 9.170 | |
202003201550 10.000 2.000 3.000 100.000 97.000 62.100 37.700 24.986 9.170 | |
202003201715 16.000 0.000 0.000 100.000 97.000 62.400 37.100 24.986 9.170 | |
202003201835 32.000 0.000 0.000 100.000 98.000 62.200 35.900 24.986 9.170 | |
202003201950 97.000 1.000 4.000 100.000 98.000 61.400 33.300 24.986 9.190 | |
202003202115 359.000 0.000 0.000 100.000 98.000 60.500 34.800 24.986 9.190 | |
202003202235 105.000 0.000 5.000 100.000 98.000 59.700 34.800 24.986 9.190 | |
202003202350 105.000 0.000 0.000 100.000 98.000 59.300 34.400 24.986 9.200 | |
202003210115 102.000 0.000 0.000 100.000 98.000 58.800 33.400 24.986 9.220 | |
202003210235 359.000 0.000 0.000 100.000 98.000 58.200 32.500 24.986 9.250 | |
202003210350 359.000 0.000 6.000 100.000 98.000 57.700 32.800 24.986 9.260 | |
202003210515 359.000 0.000 0.000 100.000 98.000 57.300 33.200 24.986 9.270 | |
202003210635 359.000 0.000 0.000 100.000 98.000 57.000 32.800 24.986 9.280 | |
202003210750 158.000 0.000 0.000 100.000 98.000 56.700 32.600 24.986 9.290 | |
202003210915 158.000 0.000 0.000 100.000 98.000 56.700 32.000 24.986 9.320 | |
202003211035 157.000 0.000 2.000 100.000 98.000 56.800 32.300 24.986 9.380 | |
202003211150 162.000 0.000 6.000 100.000 98.000 57.100 34.200 24.986 9.410 | |
202003211315 115.000 11.000 17.000 100.000 98.000 56.800 35.600 24.986 9.410 | |
202003211435 137.000 2.000 6.000 100.000 98.000 57.400 36.000 24.986 9.410 | |
202003211550 359.000 0.000 9.000 100.000 98.000 59.400 38.700 24.986 9.410 | |
202003211715 359.000 0.000 0.000 100.000 98.000 61.100 38.000 24.986 9.410 | |
202003211835 359.000 0.000 0.000 100.000 98.000 61.300 35.900 24.986 9.410 | |
202003211950 359.000 0.000 4.000 100.000 98.000 60.700 36.200 24.986 9.410 | |
202003212115 116.000 2.000 5.000 100.000 98.000 60.000 35.600 24.986 9.410 | |
202003212235 359.000 7.000 10.000 100.000 98.000 59.400 36.000 24.986 9.410 | |
202003212350 91.000 0.000 5.000 100.000 98.000 58.500 35.800 24.986 9.410 | |
202003220115 359.000 10.000 11.000 100.000 98.000 58.100 35.900 24.986 9.410 | |
202003220235 58.000 4.000 12.000 100.000 98.000 57.600 35.900 24.986 9.410 | |
202003220350 67.000 1.000 7.000 100.000 98.000 57.300 36.700 24.986 9.410 | |
202003220515 51.000 0.000 7.000 100.000 98.000 57.100 36.900 24.986 9.410 | |
202003220635 66.000 0.000 8.000 100.000 98.000 56.800 35.900 24.986 9.410 | |
202003220750 56.000 0.000 7.000 100.000 98.000 56.700 35.700 24.986 9.410 | |
202003220915 66.000 0.000 9.000 100.000 99.000 56.400 35.800 24.986 9.420 | |
202003221035 78.000 0.000 2.000 100.000 98.000 56.700 36.700 24.986 9.420 | |
202003221150 359.000 0.000 7.000 100.000 98.000 56.800 37.400 24.986 9.440 | |
202003221315 49.000 5.000 9.000 100.000 99.000 57.300 38.800 24.986 9.450 | |
202003221435 359.000 0.000 3.000 100.000 98.000 58.100 39.700 24.986 9.450 | |
202003221550 359.000 0.000 0.000 100.000 99.000 58.500 39.400 24.986 9.450 | |
202003221705 359.000 0.000 0.000 100.000 99.000 59.000 39.800 24.986 9.460 | |
""" | |
) | |
df = pd.read_csv( | |
TESTDATA, sep=" ", names=dat_names, index_col=0, parse_dates=True | |
) | |
df = df.drop_duplicates() | |
df.index = pd.to_datetime(df.index, format="%Y%m%d%H%M") | |
# the full index range | |
date_limits = [df.index.min(), df.index.max()] | |
N_DAYS = 20 # start with range slider of last N days | |
# use datetime, set earliest to 00:00 and latest to 23:59 | |
datetime1 = dt.datetime( | |
date_limits[1].year, | |
date_limits[1].month, | |
date_limits[1].day, | |
hour=23, | |
minute=59, | |
) | |
date1 = dt.date(date_limits[1].year, date_limits[1].month, date_limits[1].day) | |
datetime0 = dt.datetime( | |
date_limits[0].year, date_limits[0].month, date_limits[0].day | |
) | |
date0 = dt.date(date_limits[0].year, date_limits[0].month, date_limits[0].day) | |
date_range = date1 - date0 # integer number of days | |
# date0 no more than N_DAYS before date1 | |
date0 = ( | |
date0 | |
if date_range <= dt.timedelta(days=N_DAYS) | |
else date1 - dt.timedelta(days=N_DAYS) | |
) | |
df30 = df.loc[datetime0:datetime1] | |
# TODO: I can select specific date like this: df.loc['2020-02-06'], even if the | |
# index contains HHMMSS. | |
# TODO: How to select a range of dates? | |
# generate a list of dates for the slider | |
def gen_date_list(date0, date1): | |
d0 = dt.date(date0.year, date0.month, date0.day) | |
d1 = dt.date(date1.year, date1.month, date1.day) | |
ndate = (d1 - d0).days + 1 | |
day_delta = dt.timedelta(days=1) | |
dlist = [d0 + i * day_delta for i in range(ndate)] | |
return dlist | |
day_list = gen_date_list(date0, date1) | |
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP]) | |
# Next dbc.Row contains two graphs side-by-side | |
row1 = dbc.Row( | |
# style for Div that contains both wind speed and wind direction graphs | |
# margin-bottom required for labels on date selector slider | |
children=[ | |
# XY graph of wind speed, time on x-axis | |
dbc.Col( | |
children=[ | |
dcc.Graph(id="wind-date-plot", style={"height": "90%"}), | |
html.Div( | |
# date slider sits underneath the wind-speed plot | |
dcc.Slider( | |
id="wind-date-slider", | |
included=False, | |
min=0, | |
max=len(day_list) - 1, | |
value=len(day_list) - 1, # start at last day | |
marks={ | |
i: { | |
"label": str(dm), | |
"style": { | |
"transform": ( | |
"rotate(-40deg) translate(-40px,-20px)" | |
) | |
}, | |
} | |
for i, dm in enumerate(day_list) | |
if i % 2 == 0 | |
}, | |
dots=True, | |
), | |
# style for Div that contains Slider | |
style={"width": "70%", "margin-left": "100px"}, | |
), | |
], | |
width=8, | |
className="h-100", | |
), | |
# polar plot of wind speed and direction with hour slider | |
dbc.Col( | |
# style for Span that contains Graph and Slider | |
# style=dict(display="inline-block", width="30%"), | |
width=4, | |
className="h-100", | |
children=[ | |
dcc.Graph(id="wind-dir-plot", style={"height": "90%"}), | |
html.Div( | |
# style for Div that contains Slider | |
style={"margin-top": "20px"}, | |
children=[ | |
dcc.Slider( | |
# style={'height':'inherit'}, | |
id="hour-slider", | |
included=False, | |
min=0, | |
max=23, | |
value=0, # start at zero hour | |
marks={ | |
str(hour): str(hour) | |
for hour in range(0, 23, 2) | |
}, | |
step=None, | |
), | |
], | |
), | |
], | |
), | |
], | |
className="h-75", | |
) | |
# Second row of graphs | |
row2 = dbc.Row( | |
children=[ | |
dbc.Col( | |
width=6, | |
children=[dcc.Graph(id="temp-out", className="h-100")], | |
className="h-100", | |
), | |
dbc.Col( | |
width=6, | |
children=[dcc.Graph(id="rel-hum", className="h-100")], | |
className="h-100", | |
), | |
], | |
className="h-25", | |
) | |
# must set fluid=True to occupy full width | |
app.layout = dbc.Container([row1, row2], className="vh-100", fluid=True) | |
@app.callback( | |
Output("wind-date-plot", "figure"), [Input("wind-date-slider", "value")] | |
) | |
def update_wind_speed_figure(selected_day): | |
f_df = df30[str(date0 + dt.timedelta(days=selected_day))] | |
traces = [] | |
traces.append( | |
dict( | |
x=f_df.index, y=f_df["wind_speed"], type="line", name=u"wind_speed" | |
) | |
) | |
traces.append( | |
dict(x=f_df.index, y=f_df["wind_gust"], type="line", name=u"wind_gust") | |
) | |
return { | |
"data": traces, | |
"layout": dict( | |
title={"text": "Wind Speed (every 5 minutes)", "y": "0.8"} | |
), | |
} | |
@app.callback( | |
Output("wind-dir-plot", "figure"), | |
[Input("wind-date-slider", "value"), Input("hour-slider", "value")], | |
) | |
def update_wind_dir_figure(selected_day, selected_hour): | |
f_df = df30[str(date0 + dt.timedelta(days=selected_day))] | |
# Max wind speed in this time | |
wmax = f_df["wind_speed"].max() | |
wmax = int(round(wmax, -1)) | |
tick_int = 10 if wmax >= 20 else 5 | |
# Get 4 hour slice for this graph | |
d0 = f_df.index[0] # it's a datetime | |
hour0 = dt.timedelta(hours=selected_hour) | |
h1 = selected_hour + 4 if selected_hour <= 20 else 24 | |
hour1 = dt.timedelta(hours=h1) | |
f_df = f_df.loc[str(d0 + hour0) : str(d0 + hour1)] | |
# Wind direction of 359 is almost certainly bogus, so remove | |
winds = f_df.loc[df["wind_dir"] != 359.0][["wind_dir", "wind_speed"]] | |
val = winds["wind_speed"] | |
direction = winds["wind_dir"] | |
data = [ | |
dict( | |
type="scatterpolar", | |
r=val, | |
theta=direction, | |
mode="markers", | |
marker=dict(color="yellow"), | |
) | |
] | |
layout = dict( | |
paper_bgcolor=app_color["graph_bg"], | |
font={"color": "#fff"}, | |
autosize=False, | |
polar=dict( | |
bgcolor=app_color["graph_line"], | |
radialaxis=dict(range=[0, wmax], angle=0, dtick=tick_int), | |
angularaxis=dict( | |
showline=False, | |
tickcolor="white", | |
rotation=90, | |
direction="clockwise", | |
), | |
), | |
showlegend=False, | |
title="Wind Direction (4 hours)", | |
) | |
return { | |
"data": data, | |
"layout": layout, | |
} | |
@app.callback( | |
Output("temp-out", "figure"), [Input("wind-date-slider", "value")] | |
) | |
def update_temp_out_figure(selected_day): | |
f_df = df30[str(date0 + dt.timedelta(days=selected_day))] | |
traces = [] | |
traces.append( | |
dict( | |
x=f_df.index, y=f_df["temp_out"], type="line", name=u"temperature" | |
) | |
) | |
return { | |
"data": traces, | |
"layout": dict(title={"text": "Temperature Outside", "y": "0.8"}), | |
} | |
@app.callback( | |
Output("rel-hum", "figure"), [Input("wind-date-slider", "value")] | |
) | |
def update_rel_hum_figure(selected_day): | |
f_df = df30[str(date0 + dt.timedelta(days=selected_day))] | |
traces = [] | |
traces.append( | |
dict(x=f_df.index, y=f_df["humidity"], type="line", name=u"humidity") | |
) | |
return { | |
"data": traces, | |
"layout": dict(title={"text": "Relative Humidity", "y": "0.8"},), | |
} | |
if __name__ == "__main__": | |
server = app.server | |
app.run_server(debug=True, port=8050) | |
else: # not __main__ | |
# when running by wsgi script | |
app.config.update( | |
{ | |
"url_base_pathname": "/OOS-data/", | |
"routes_pathname_prefix": "/OOS-data/", | |
"requests_pathname_prefix": "/OOS-data/", | |
} | |
) | |
server = app.server |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment