From 5256abe3e6a92060517baef6c690b9e104198678 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Tue, 24 May 2022 10:10:30 -0400 Subject: [PATCH 1/7] Updated requirements --- apps/dash-wind-streaming/app.py | 7 +++---- apps/dash-wind-streaming/requirements.txt | 9 ++++----- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/apps/dash-wind-streaming/app.py b/apps/dash-wind-streaming/app.py index 37e9d481b..839a71288 100644 --- a/apps/dash-wind-streaming/app.py +++ b/apps/dash-wind-streaming/app.py @@ -11,7 +11,6 @@ from scipy.stats import rayleigh from db.api import get_wind_data, get_wind_data_by_id - GRAPH_INTERVAL = os.environ.get("GRAPH_INTERVAL", 5000) app = dash.Dash( @@ -183,7 +182,7 @@ def get_current_time(): - """ Helper function to get the current time in seconds. """ + """Helper function to get the current time in seconds.""" now = dt.datetime.now() total_time = (now.hour * 3600) + (now.minute * 60) + (now.second) @@ -443,7 +442,7 @@ def gen_wind_histogram(interval, wind_speed_figure, slider_value, auto_state): [State("wind-speed", "figure")], ) def deselect_auto(slider_value, wind_speed_figure): - """ Toggle the auto checkbox. """ + """Toggle the auto checkbox.""" # prevent update if graph has no data if "data" not in wind_speed_figure: @@ -462,7 +461,7 @@ def deselect_auto(slider_value, wind_speed_figure): [State("bin-slider", "value")], ) def show_num_bins(autoValue, slider_value): - """ Display the number of bins. """ + """Display the number of bins.""" if "Auto" in autoValue: return "# of Bins: Auto" diff --git a/apps/dash-wind-streaming/requirements.txt b/apps/dash-wind-streaming/requirements.txt index c34ae2487..da7dcd54e 100644 --- a/apps/dash-wind-streaming/requirements.txt +++ b/apps/dash-wind-streaming/requirements.txt @@ -1,5 +1,4 @@ -dash==1.0.0 -numpy==1.16.4 -pandas==0.24.2 -scipy==1.3.0 -gunicorn==19.9.0 \ No newline at end of file +dash==2.4.1 +pandas==1.4.2 +gunicorn==20.1.0 +scipy==1.8.1 \ No newline at end of file From 8d3c33fb1aa362ab2d1a7754704a903f37c5c5bc Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Tue, 24 May 2022 14:35:25 -0400 Subject: [PATCH 2/7] refactored components --- apps/dash-wind-streaming/README.md | 2 +- apps/dash-wind-streaming/app.py | 319 ++---------------- .../{ => assets/github}/demo.gif | Bin apps/dash-wind-streaming/constants.py | 0 apps/dash-wind-streaming/utils/components.py | 72 ++++ .../utils/helper_functions.py | 250 ++++++++++++++ 6 files changed, 356 insertions(+), 287 deletions(-) rename apps/dash-wind-streaming/{ => assets/github}/demo.gif (100%) create mode 100644 apps/dash-wind-streaming/constants.py create mode 100644 apps/dash-wind-streaming/utils/components.py create mode 100644 apps/dash-wind-streaming/utils/helper_functions.py diff --git a/apps/dash-wind-streaming/README.md b/apps/dash-wind-streaming/README.md index 58a450925..7dfb6d6ac 100644 --- a/apps/dash-wind-streaming/README.md +++ b/apps/dash-wind-streaming/README.md @@ -37,7 +37,7 @@ Open a browser at http://127.0.0.1:8050 ## Screenshots -![demo.gif](demo.gif) +![demo.gif](assets/github/demo.gif) ## Resources diff --git a/apps/dash-wind-streaming/app.py b/apps/dash-wind-streaming/app.py index 839a71288..50db07960 100644 --- a/apps/dash-wind-streaming/app.py +++ b/apps/dash-wind-streaming/app.py @@ -1,17 +1,31 @@ import os import pathlib import numpy as np -import datetime as dt + import dash -import dash_core_components as dcc -import dash_html_components as html +from dash import html -from dash.exceptions import PreventUpdate from dash.dependencies import Input, Output, State -from scipy.stats import rayleigh from db.api import get_wind_data, get_wind_data_by_id -GRAPH_INTERVAL = os.environ.get("GRAPH_INTERVAL", 5000) +from dash.exceptions import PreventUpdate + +from utils.components import ( + slider, + checklist, + left_graph, + interval, + right_graph_one, + right_graph_two, +) + +from utils.helper_functions import ( + gen_wind_speed, + gen_wind_direction, + gen_wind_histogram, + app_color, +) + app = dash.Dash( __name__, @@ -21,8 +35,6 @@ server = app.server -app_color = {"graph_bg": "#082255", "graph_line": "#007ACE"} - app.layout = html.Div( [ # header @@ -69,20 +81,8 @@ html.Div( [html.H6("WIND SPEED (MPH)", className="graph__title")] ), - dcc.Graph( - id="wind-speed", - figure=dict( - layout=dict( - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - ) - ), - ), - dcc.Interval( - id="wind-speed-update", - interval=int(GRAPH_INTERVAL), - n_intervals=0, - ), + left_graph, + interval, ], className="two-thirds column wind__speed__container", ), @@ -100,51 +100,14 @@ ] ), html.Div( - [ - dcc.Slider( - id="bin-slider", - min=1, - max=60, - step=1, - value=20, - updatemode="drag", - marks={ - 20: {"label": "20"}, - 40: {"label": "40"}, - 60: {"label": "60"}, - }, - ) - ], + slider, className="slider", ), html.Div( - [ - dcc.Checklist( - id="bin-auto", - options=[ - {"label": "Auto", "value": "Auto"} - ], - value=["Auto"], - inputClassName="auto__checkbox", - labelClassName="auto__label", - ), - html.P( - "# of Bins: Auto", - id="bin-size", - className="auto__p", - ), - ], + checklist, className="auto__container", ), - dcc.Graph( - id="wind-histogram", - figure=dict( - layout=dict( - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - ) - ), - ), + right_graph_one, ], className="graph__container first", ), @@ -158,15 +121,7 @@ ) ] ), - dcc.Graph( - id="wind-direction", - figure=dict( - layout=dict( - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - ) - ), - ), + right_graph_two, ], className="graph__container second", ), @@ -181,122 +136,30 @@ ) -def get_current_time(): - """Helper function to get the current time in seconds.""" - - now = dt.datetime.now() - total_time = (now.hour * 3600) + (now.minute * 60) + (now.second) - return total_time - - @app.callback( Output("wind-speed", "figure"), [Input("wind-speed-update", "n_intervals")] ) -def gen_wind_speed(interval): +def return_gen_wind_speed(interval): """ Generate the wind speed graph. :params interval: update the graph based on an interval """ - total_time = get_current_time() - df = get_wind_data(total_time - 200, total_time) - - trace = dict( - type="scatter", - y=df["Speed"], - line={"color": "#42C4F7"}, - hoverinfo="skip", - error_y={ - "type": "data", - "array": df["SpeedError"], - "thickness": 1.5, - "width": 2, - "color": "#B4E8FC", - }, - mode="lines", - ) - - layout = dict( - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - font={"color": "#fff"}, - height=700, - xaxis={ - "range": [0, 200], - "showline": True, - "zeroline": False, - "fixedrange": True, - "tickvals": [0, 50, 100, 150, 200], - "ticktext": ["200", "150", "100", "50", "0"], - "title": "Time Elapsed (sec)", - }, - yaxis={ - "range": [ - min(0, min(df["Speed"])), - max(45, max(df["Speed"]) + max(df["SpeedError"])), - ], - "showgrid": True, - "showline": True, - "fixedrange": True, - "zeroline": False, - "gridcolor": app_color["graph_line"], - "nticks": max(6, round(df["Speed"].iloc[-1] / 10)), - }, - ) - - return dict(data=[trace], layout=layout) + return gen_wind_speed() @app.callback( - Output("wind-direction", "figure"), [Input("wind-speed-update", "n_intervals")] + Output("wind-direction", "figure"), Input("wind-speed-update", "n_intervals") ) -def gen_wind_direction(interval): +def return_gen_wind_direction(interval): """ Generate the wind direction graph. :params interval: update the graph based on an interval """ - total_time = get_current_time() - df = get_wind_data_by_id(total_time) - val = df["Speed"].iloc[-1] - direction = [0, (df["Direction"][0] - 20), (df["Direction"][0] + 20), 0] - - traces_scatterpolar = [ - {"r": [0, val, val, 0], "fillcolor": "#084E8A"}, - {"r": [0, val * 0.65, val * 0.65, 0], "fillcolor": "#B4E1FA"}, - {"r": [0, val * 0.3, val * 0.3, 0], "fillcolor": "#EBF5FA"}, - ] - - data = [ - dict( - type="scatterpolar", - r=traces["r"], - theta=direction, - mode="lines", - fill="toself", - fillcolor=traces["fillcolor"], - line={"color": "rgba(32, 32, 32, .6)", "width": 1}, - ) - for traces in traces_scatterpolar - ] - - layout = dict( - height=350, - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - font={"color": "#fff"}, - autosize=False, - polar={ - "bgcolor": app_color["graph_line"], - "radialaxis": {"range": [0, 45], "angle": 45, "dtick": 10}, - "angularaxis": {"showline": False, "tickcolor": "white"}, - }, - showlegend=False, - ) - - return dict(data=data, layout=layout) + return gen_wind_direction() @app.callback( @@ -308,7 +171,7 @@ def gen_wind_direction(interval): State("bin-auto", "value"), ], ) -def gen_wind_histogram(interval, wind_speed_figure, slider_value, auto_state): +def return_gen_wind_histogram(interval, wind_speed_figure, slider_value, auto_state): """ Genererate wind histogram graph. @@ -317,123 +180,7 @@ def gen_wind_histogram(interval, wind_speed_figure, slider_value, auto_state): :params slider_value: current slider value :params auto_state: current auto state """ - - wind_val = [] - - try: - # Check to see whether wind-speed has been plotted yet - if wind_speed_figure is not None: - wind_val = wind_speed_figure["data"][0]["y"] - if "Auto" in auto_state: - bin_val = np.histogram( - wind_val, - bins=range(int(round(min(wind_val))), int(round(max(wind_val)))), - ) - else: - bin_val = np.histogram(wind_val, bins=slider_value) - except Exception as error: - raise PreventUpdate - - avg_val = float(sum(wind_val)) / len(wind_val) - median_val = np.median(wind_val) - - pdf_fitted = rayleigh.pdf( - bin_val[1], loc=(avg_val) * 0.55, scale=(bin_val[1][-1] - bin_val[1][0]) / 3 - ) - - y_val = (pdf_fitted * max(bin_val[0]) * 20,) - y_val_max = max(y_val[0]) - bin_val_max = max(bin_val[0]) - - trace = dict( - type="bar", - x=bin_val[1], - y=bin_val[0], - marker={"color": app_color["graph_line"]}, - showlegend=False, - hoverinfo="x+y", - ) - - traces_scatter = [ - {"line_dash": "dash", "line_color": "#2E5266", "name": "Average"}, - {"line_dash": "dot", "line_color": "#BD9391", "name": "Median"}, - ] - - scatter_data = [ - dict( - type="scatter", - x=[bin_val[int(len(bin_val) / 2)]], - y=[0], - mode="lines", - line={"dash": traces["line_dash"], "color": traces["line_color"]}, - marker={"opacity": 0}, - visible=True, - name=traces["name"], - ) - for traces in traces_scatter - ] - - trace3 = dict( - type="scatter", - mode="lines", - line={"color": "#42C4F7"}, - y=y_val[0], - x=bin_val[1][: len(bin_val[1])], - name="Rayleigh Fit", - ) - layout = dict( - height=350, - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - font={"color": "#fff"}, - xaxis={ - "title": "Wind Speed (mph)", - "showgrid": False, - "showline": False, - "fixedrange": True, - }, - yaxis={ - "showgrid": False, - "showline": False, - "zeroline": False, - "title": "Number of Samples", - "fixedrange": True, - }, - autosize=True, - bargap=0.01, - bargroupgap=0, - hovermode="closest", - legend={ - "orientation": "h", - "yanchor": "bottom", - "xanchor": "center", - "y": 1, - "x": 0.5, - }, - shapes=[ - { - "xref": "x", - "yref": "y", - "y1": int(max(bin_val_max, y_val_max)) + 0.5, - "y0": 0, - "x0": avg_val, - "x1": avg_val, - "type": "line", - "line": {"dash": "dash", "color": "#2E5266", "width": 5}, - }, - { - "xref": "x", - "yref": "y", - "y1": int(max(bin_val_max, y_val_max)) + 0.5, - "y0": 0, - "x0": median_val, - "x1": median_val, - "type": "line", - "line": {"dash": "dot", "color": "#BD9391", "width": 5}, - }, - ], - ) - return dict(data=[trace, scatter_data[0], scatter_data[1], trace3], layout=layout) + return gen_wind_histogram(wind_speed_figure, slider_value, auto_state) @app.callback( diff --git a/apps/dash-wind-streaming/demo.gif b/apps/dash-wind-streaming/assets/github/demo.gif similarity index 100% rename from apps/dash-wind-streaming/demo.gif rename to apps/dash-wind-streaming/assets/github/demo.gif diff --git a/apps/dash-wind-streaming/constants.py b/apps/dash-wind-streaming/constants.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/dash-wind-streaming/utils/components.py b/apps/dash-wind-streaming/utils/components.py new file mode 100644 index 000000000..23a69e5dd --- /dev/null +++ b/apps/dash-wind-streaming/utils/components.py @@ -0,0 +1,72 @@ +from dash import html, dcc +from utils.helper_functions import app_color +import os + +GRAPH_INTERVAL = os.environ.get("GRAPH_INTERVAL", 5000) + + +slider = [ + dcc.Slider( + id="bin-slider", + min=1, + max=60, + step=1, + value=20, + updatemode="drag", + marks={ + 20: {"label": "20"}, + 40: {"label": "40"}, + 60: {"label": "60"}, + }, + ) +] +checklist = [ + dcc.Checklist( + id="bin-auto", + options=[{"label": "Auto", "value": "Auto"}], + value=["Auto"], + inputClassName="auto__checkbox", + labelClassName="auto__label", + ), + html.P( + "# of Bins: Auto", + id="bin-size", + className="auto__p", + ), +] + +left_graph = dcc.Graph( + id="wind-speed", + figure=dict( + layout=dict( + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + ) + ), +) + +interval = dcc.Interval( + id="wind-speed-update", + interval=int(GRAPH_INTERVAL), + n_intervals=0, +) + +right_graph_one = dcc.Graph( + id="wind-histogram", + figure=dict( + layout=dict( + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + ) + ), +) + +right_graph_two = dcc.Graph( + id="wind-direction", + figure=dict( + layout=dict( + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + ) + ), +) diff --git a/apps/dash-wind-streaming/utils/helper_functions.py b/apps/dash-wind-streaming/utils/helper_functions.py new file mode 100644 index 000000000..d100baaf3 --- /dev/null +++ b/apps/dash-wind-streaming/utils/helper_functions.py @@ -0,0 +1,250 @@ +import datetime as dt +from db.api import get_wind_data, get_wind_data_by_id +import numpy as np +from scipy.stats import rayleigh + + +from dash.exceptions import PreventUpdate + + +app_color = {"graph_bg": "#082255", "graph_line": "#007ACE"} + + +def get_current_time(): + """Helper function to get the current time in seconds.""" + + now = dt.datetime.now() + total_time = (now.hour * 3600) + (now.minute * 60) + (now.second) + return total_time + + +def gen_wind_speed(): + """ + Generate the wind speed graph. + + :params interval: update the graph based on an interval + """ + + total_time = get_current_time() + df = get_wind_data(total_time - 200, total_time) + + trace = dict( + type="scatter", + y=df["Speed"], + line={"color": "#42C4F7"}, + hoverinfo="skip", + error_y={ + "type": "data", + "array": df["SpeedError"], + "thickness": 1.5, + "width": 2, + "color": "#B4E8FC", + }, + mode="lines", + ) + + layout = dict( + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + font={"color": "#fff"}, + height=700, + xaxis={ + "range": [0, 200], + "showline": True, + "zeroline": False, + "fixedrange": True, + "tickvals": [0, 50, 100, 150, 200], + "ticktext": ["200", "150", "100", "50", "0"], + "title": "Time Elapsed (sec)", + }, + yaxis={ + "range": [ + min(0, min(df["Speed"])), + max(45, max(df["Speed"]) + max(df["SpeedError"])), + ], + "showgrid": True, + "showline": True, + "fixedrange": True, + "zeroline": False, + "gridcolor": app_color["graph_line"], + "nticks": max(6, round(df["Speed"].iloc[-1] / 10)), + }, + ) + + return dict(data=[trace], layout=layout) + + +def gen_wind_direction(): + """ + Generate the wind direction graph. + + :params interval: update the graph based on an interval + """ + + total_time = get_current_time() + df = get_wind_data_by_id(total_time) + val = df["Speed"].iloc[-1] + direction = [0, (df["Direction"][0] - 20), (df["Direction"][0] + 20), 0] + + traces_scatterpolar = [ + {"r": [0, val, val, 0], "fillcolor": "#084E8A"}, + {"r": [0, val * 0.65, val * 0.65, 0], "fillcolor": "#B4E1FA"}, + {"r": [0, val * 0.3, val * 0.3, 0], "fillcolor": "#EBF5FA"}, + ] + + data = [ + dict( + type="scatterpolar", + r=traces["r"], + theta=direction, + mode="lines", + fill="toself", + fillcolor=traces["fillcolor"], + line={"color": "rgba(32, 32, 32, .6)", "width": 1}, + ) + for traces in traces_scatterpolar + ] + + layout = dict( + height=350, + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + font={"color": "#fff"}, + autosize=False, + polar={ + "bgcolor": app_color["graph_line"], + "radialaxis": {"range": [0, 45], "angle": 45, "dtick": 10}, + "angularaxis": {"showline": False, "tickcolor": "white"}, + }, + showlegend=False, + ) + + return dict(data=data, layout=layout) + + +def gen_wind_histogram(wind_speed_figure, slider_value, auto_state): + """ + Genererate wind histogram graph. + + :params interval: upadte the graph based on an interval + :params wind_speed_figure: current wind speed graph + :params slider_value: current slider value + :params auto_state: current auto state + """ + + wind_val = [] + + try: + # Check to see whether wind-speed has been plotted yet + if wind_speed_figure is not None: + wind_val = wind_speed_figure["data"][0]["y"] + if "Auto" in auto_state: + bin_val = np.histogram( + wind_val, + bins=range(int(round(min(wind_val))), int(round(max(wind_val)))), + ) + else: + bin_val = np.histogram(wind_val, bins=slider_value) + except Exception as error: + raise PreventUpdate + + avg_val = float(sum(wind_val)) / len(wind_val) + median_val = np.median(wind_val) + + pdf_fitted = rayleigh.pdf( + bin_val[1], loc=(avg_val) * 0.55, scale=(bin_val[1][-1] - bin_val[1][0]) / 3 + ) + + y_val = (pdf_fitted * max(bin_val[0]) * 20,) + y_val_max = max(y_val[0]) + bin_val_max = max(bin_val[0]) + + trace = dict( + type="bar", + x=bin_val[1], + y=bin_val[0], + marker={"color": app_color["graph_line"]}, + showlegend=False, + hoverinfo="x+y", + ) + + traces_scatter = [ + {"line_dash": "dash", "line_color": "#2E5266", "name": "Average"}, + {"line_dash": "dot", "line_color": "#BD9391", "name": "Median"}, + ] + + scatter_data = [ + dict( + type="scatter", + x=[bin_val[int(len(bin_val) / 2)]], + y=[0], + mode="lines", + line={"dash": traces["line_dash"], "color": traces["line_color"]}, + marker={"opacity": 0}, + visible=True, + name=traces["name"], + ) + for traces in traces_scatter + ] + + trace3 = dict( + type="scatter", + mode="lines", + line={"color": "#42C4F7"}, + y=y_val[0], + x=bin_val[1][: len(bin_val[1])], + name="Rayleigh Fit", + ) + layout = dict( + height=350, + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + font={"color": "#fff"}, + xaxis={ + "title": "Wind Speed (mph)", + "showgrid": False, + "showline": False, + "fixedrange": True, + }, + yaxis={ + "showgrid": False, + "showline": False, + "zeroline": False, + "title": "Number of Samples", + "fixedrange": True, + }, + autosize=True, + bargap=0.01, + bargroupgap=0, + hovermode="closest", + legend={ + "orientation": "h", + "yanchor": "bottom", + "xanchor": "center", + "y": 1, + "x": 0.5, + }, + shapes=[ + { + "xref": "x", + "yref": "y", + "y1": int(max(bin_val_max, y_val_max)) + 0.5, + "y0": 0, + "x0": avg_val, + "x1": avg_val, + "type": "line", + "line": {"dash": "dash", "color": "#2E5266", "width": 5}, + }, + { + "xref": "x", + "yref": "y", + "y1": int(max(bin_val_max, y_val_max)) + 0.5, + "y0": 0, + "x0": median_val, + "x1": median_val, + "type": "line", + "line": {"dash": "dot", "color": "#BD9391", "width": 5}, + }, + ], + ) + return dict(data=[trace, scatter_data[0], scatter_data[1], trace3], layout=layout) From eb99ce2358ef1c3482005ec09de4904e6ba4f273 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Tue, 24 May 2022 14:55:32 -0400 Subject: [PATCH 3/7] Updated header --- apps/dash-wind-streaming/app.py | 41 ++--------- .../assets/{ => css}/app.css | 0 .../assets/{ => css}/demo-button.css | 0 .../dash-wind-streaming/assets/css/header.css | 65 ++++++++++++++++++ .../assets/{ => css}/style.css | 0 .../assets/dash-new-logo.png | Bin 18588 -> 0 bytes .../assets/images/plotly-logo.png | Bin 0 -> 23021 bytes apps/dash-wind-streaming/constants.py | 0 apps/dash-wind-streaming/utils/components.py | 21 ++++++ 9 files changed, 91 insertions(+), 36 deletions(-) rename apps/dash-wind-streaming/assets/{ => css}/app.css (100%) rename apps/dash-wind-streaming/assets/{ => css}/demo-button.css (100%) create mode 100644 apps/dash-wind-streaming/assets/css/header.css rename apps/dash-wind-streaming/assets/{ => css}/style.css (100%) delete mode 100644 apps/dash-wind-streaming/assets/dash-new-logo.png create mode 100644 apps/dash-wind-streaming/assets/images/plotly-logo.png delete mode 100644 apps/dash-wind-streaming/constants.py diff --git a/apps/dash-wind-streaming/app.py b/apps/dash-wind-streaming/app.py index 50db07960..a4a9d5102 100644 --- a/apps/dash-wind-streaming/app.py +++ b/apps/dash-wind-streaming/app.py @@ -11,6 +11,7 @@ from dash.exceptions import PreventUpdate from utils.components import ( + Header, slider, checklist, left_graph, @@ -23,7 +24,6 @@ gen_wind_speed, gen_wind_direction, gen_wind_histogram, - app_color, ) @@ -37,41 +37,10 @@ app.layout = html.Div( [ - # header - html.Div( - [ - html.Div( - [ - html.H4("WIND SPEED STREAMING", className="app__header__title"), - html.P( - "This app continually queries a SQL database and displays live charts of wind speed and wind direction.", - className="app__header__title--grey", - ), - ], - className="app__header__desc", - ), - html.Div( - [ - html.A( - html.Button("SOURCE CODE", className="link-button"), - href="https://github.com/plotly/dash-sample-apps/tree/main/apps/dash-wind-streaming", - ), - html.A( - html.Button("ENTERPRISE DEMO", className="link-button"), - href="https://plotly.com/get-demo/", - ), - html.A( - html.Img( - src=app.get_asset_url("dash-new-logo.png"), - className="app__menu__img", - ), - href="https://plotly.com/dash/", - ), - ], - className="app__header__logo", - ), - ], - className="app__header", + Header( + app, + "WIND SPEED STREAMING", + "This app continually queries a SQL database and displays live charts of wind speed and wind direction.", ), html.Div( [ diff --git a/apps/dash-wind-streaming/assets/app.css b/apps/dash-wind-streaming/assets/css/app.css similarity index 100% rename from apps/dash-wind-streaming/assets/app.css rename to apps/dash-wind-streaming/assets/css/app.css diff --git a/apps/dash-wind-streaming/assets/demo-button.css b/apps/dash-wind-streaming/assets/css/demo-button.css similarity index 100% rename from apps/dash-wind-streaming/assets/demo-button.css rename to apps/dash-wind-streaming/assets/css/demo-button.css diff --git a/apps/dash-wind-streaming/assets/css/header.css b/apps/dash-wind-streaming/assets/css/header.css new file mode 100644 index 000000000..6acac321d --- /dev/null +++ b/apps/dash-wind-streaming/assets/css/header.css @@ -0,0 +1,65 @@ +/* Header */ +.header { + height: 10vh; + display: flex; + background-color: #0d1c41; + padding-left: 2%; + padding-right: 2%; + font-family: playfair display, sans-serif; +} + +.header .header-title { + color: #A2ACB8 !important; + font-size: 5vh; +} + +.header-logos { + margin-left: auto; + align-self: center; +} + +.header-logos img { + margin-left: 3vh !important; + max-height: 5vh; +} + +.subheader-title { + font-size: 1.5vh; + color: #A2ACB8 !important; +} + + +/* Demo button css */ +.demo-button { + font-size: 1.5vh; + font-family: Open Sans, sans-serif; + text-decoration: none; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 8px; + font-weight: 700; + -webkit-padding-start: 1rem; + padding-inline-start: 1rem; + -webkit-padding-end: 1rem; + padding-inline-end: 1rem; + color: #ffffff; + letter-spacing: 1.5px; + border: solid 1.5px transparent; + box-shadow: 2px 1000px 1px #0c0c0c inset; + background-image: linear-gradient(135deg, #7A76FF, #7A76FF, #7FE4FF); + -webkit-background-size: 200% 100%; + background-size: 200% 100%; + -webkit-background-position: 99%; + background-position: 99%; + background-origin: border-box; + transition: all .4s ease-in-out; + padding-top: 1vh; + padding-bottom: 1vh; +} + +.demo-button:hover { + color: #7A76FF; + background-position: 0%; +} \ No newline at end of file diff --git a/apps/dash-wind-streaming/assets/style.css b/apps/dash-wind-streaming/assets/css/style.css similarity index 100% rename from apps/dash-wind-streaming/assets/style.css rename to apps/dash-wind-streaming/assets/css/style.css diff --git a/apps/dash-wind-streaming/assets/dash-new-logo.png b/apps/dash-wind-streaming/assets/dash-new-logo.png deleted file mode 100644 index 040cde17424f9bd58cec6360eb2d1ace958f0634..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18588 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 Date: Tue, 24 May 2022 20:03:30 +0100 Subject: [PATCH 4/7] changes on app --- apps/dash-wind-streaming/app.py | 47 ++++++++++++--------------------- 1 file changed, 17 insertions(+), 30 deletions(-) diff --git a/apps/dash-wind-streaming/app.py b/apps/dash-wind-streaming/app.py index a4a9d5102..daf181337 100644 --- a/apps/dash-wind-streaming/app.py +++ b/apps/dash-wind-streaming/app.py @@ -1,14 +1,4 @@ -import os -import pathlib -import numpy as np - -import dash -from dash import html - -from dash.dependencies import Input, Output, State -from db.api import get_wind_data, get_wind_data_by_id - -from dash.exceptions import PreventUpdate +from dash import Dash, html, Input, Output, State, PreventUpdate from utils.components import ( Header, @@ -19,7 +9,6 @@ right_graph_one, right_graph_two, ) - from utils.helper_functions import ( gen_wind_speed, gen_wind_direction, @@ -27,12 +16,10 @@ ) -app = dash.Dash( +app = Dash( __name__, - meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}], + title = "Wind Speed Dashboard" ) -app.title = "Wind Speed Dashboard" - server = app.server app.layout = html.Div( @@ -106,7 +93,8 @@ @app.callback( - Output("wind-speed", "figure"), [Input("wind-speed-update", "n_intervals")] + Output("wind-speed", "figure"), + Input("wind-speed-update", "n_intervals") ) def return_gen_wind_speed(interval): """ @@ -119,7 +107,8 @@ def return_gen_wind_speed(interval): @app.callback( - Output("wind-direction", "figure"), Input("wind-speed-update", "n_intervals") + Output("wind-direction", "figure"), + Input("wind-speed-update", "n_intervals") ) def return_gen_wind_direction(interval): """ @@ -133,12 +122,10 @@ def return_gen_wind_direction(interval): @app.callback( Output("wind-histogram", "figure"), - [Input("wind-speed-update", "n_intervals")], - [ - State("wind-speed", "figure"), - State("bin-slider", "value"), - State("bin-auto", "value"), - ], + Input("wind-speed-update", "n_intervals"), + State("wind-speed", "figure"), + State("bin-slider", "value"), + State("bin-auto", "value"), ) def return_gen_wind_histogram(interval, wind_speed_figure, slider_value, auto_state): """ @@ -154,8 +141,8 @@ def return_gen_wind_histogram(interval, wind_speed_figure, slider_value, auto_st @app.callback( Output("bin-auto", "value"), - [Input("bin-slider", "value")], - [State("wind-speed", "figure")], + Input("bin-slider", "value"), + State("wind-speed", "figure"), ) def deselect_auto(slider_value, wind_speed_figure): """Toggle the auto checkbox.""" @@ -167,14 +154,14 @@ def deselect_auto(slider_value, wind_speed_figure): raise PreventUpdate if wind_speed_figure is not None and len(wind_speed_figure["data"][0]["y"]) > 5: - return [""] - return ["Auto"] + return "" + return "Auto" @app.callback( Output("bin-size", "children"), - [Input("bin-auto", "value")], - [State("bin-slider", "value")], + Input("bin-auto", "value"), + State("bin-slider", "value"), ) def show_num_bins(autoValue, slider_value): """Display the number of bins.""" From 24d487b023e5693aa0379579a495f25b8837b332 Mon Sep 17 00:00:00 2001 From: elliotgunn Date: Tue, 24 May 2022 17:29:03 -0400 Subject: [PATCH 5/7] more refactoring --- apps/dash-wind-streaming/__init__.py | 0 apps/dash-wind-streaming/app.py | 19 +++++++------------ apps/dash-wind-streaming/constants.py | 1 + apps/dash-wind-streaming/runtime.txt | 1 + .../utils/helper_functions.py | 5 +---- 5 files changed, 10 insertions(+), 16 deletions(-) delete mode 100644 apps/dash-wind-streaming/__init__.py create mode 100644 apps/dash-wind-streaming/constants.py create mode 100644 apps/dash-wind-streaming/runtime.txt diff --git a/apps/dash-wind-streaming/__init__.py b/apps/dash-wind-streaming/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/dash-wind-streaming/app.py b/apps/dash-wind-streaming/app.py index daf181337..1c7029dbc 100644 --- a/apps/dash-wind-streaming/app.py +++ b/apps/dash-wind-streaming/app.py @@ -1,4 +1,7 @@ -from dash import Dash, html, Input, Output, State, PreventUpdate +from dash import Dash, html, Input, Output, State + +from dash.exceptions import PreventUpdate + from utils.components import ( Header, @@ -15,11 +18,7 @@ gen_wind_histogram, ) - -app = Dash( - __name__, - title = "Wind Speed Dashboard" -) +app = Dash(__name__, title="Wind Speed Dashboard") server = app.server app.layout = html.Div( @@ -92,10 +91,7 @@ ) -@app.callback( - Output("wind-speed", "figure"), - Input("wind-speed-update", "n_intervals") -) +@app.callback(Output("wind-speed", "figure"), Input("wind-speed-update", "n_intervals")) def return_gen_wind_speed(interval): """ Generate the wind speed graph. @@ -107,8 +103,7 @@ def return_gen_wind_speed(interval): @app.callback( - Output("wind-direction", "figure"), - Input("wind-speed-update", "n_intervals") + Output("wind-direction", "figure"), Input("wind-speed-update", "n_intervals") ) def return_gen_wind_direction(interval): """ diff --git a/apps/dash-wind-streaming/constants.py b/apps/dash-wind-streaming/constants.py new file mode 100644 index 000000000..443eb0521 --- /dev/null +++ b/apps/dash-wind-streaming/constants.py @@ -0,0 +1 @@ +app_color = {"graph_bg": "#082255", "graph_line": "#007ACE"} diff --git a/apps/dash-wind-streaming/runtime.txt b/apps/dash-wind-streaming/runtime.txt new file mode 100644 index 000000000..cfa660c42 --- /dev/null +++ b/apps/dash-wind-streaming/runtime.txt @@ -0,0 +1 @@ +python-3.8.0 \ No newline at end of file diff --git a/apps/dash-wind-streaming/utils/helper_functions.py b/apps/dash-wind-streaming/utils/helper_functions.py index d100baaf3..dc9cde1f7 100644 --- a/apps/dash-wind-streaming/utils/helper_functions.py +++ b/apps/dash-wind-streaming/utils/helper_functions.py @@ -3,13 +3,10 @@ import numpy as np from scipy.stats import rayleigh - +from constants import app_color from dash.exceptions import PreventUpdate -app_color = {"graph_bg": "#082255", "graph_line": "#007ACE"} - - def get_current_time(): """Helper function to get the current time in seconds.""" From 5c828aae2a1f29b02de0c75c8a88f0dc9a28cc75 Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Wed, 25 May 2022 14:23:03 +0100 Subject: [PATCH 6/7] updates --- apps/dash-wind-streaming/.gitignore | 148 +++- .../Notebook/wind-speed-generation.ipynb | 664 ------------------ apps/dash-wind-streaming/app.py | 153 +--- .../assets/css/demo-button.css | 6 - .../dash-wind-streaming/assets/css/header.css | 4 +- apps/dash-wind-streaming/constants.py | 7 + .../{db => data}/wind-data.db | Bin apps/dash-wind-streaming/db/__init__.py | 0 apps/dash-wind-streaming/utils/components.py | 140 ++-- .../api.py => utils/database_connection.py} | 5 +- apps/dash-wind-streaming/utils/figures.py | 243 +++++++ .../utils/helper_functions.py | 241 +------ 12 files changed, 496 insertions(+), 1115 deletions(-) delete mode 100644 apps/dash-wind-streaming/Notebook/wind-speed-generation.ipynb delete mode 100644 apps/dash-wind-streaming/assets/css/demo-button.css rename apps/dash-wind-streaming/{db => data}/wind-data.db (100%) delete mode 100644 apps/dash-wind-streaming/db/__init__.py rename apps/dash-wind-streaming/{db/api.py => utils/database_connection.py} (87%) create mode 100644 apps/dash-wind-streaming/utils/figures.py diff --git a/apps/dash-wind-streaming/.gitignore b/apps/dash-wind-streaming/.gitignore index f465a23fa..7cabaa742 100644 --- a/apps/dash-wind-streaming/.gitignore +++ b/apps/dash-wind-streaming/.gitignore @@ -1,5 +1,145 @@ -venv -*.pyc -.DS_Store +# .gitignore specifies the files that shouldn't be included +# in version control and therefore shouldn't be included when +# deploying an application to Dash Enterprise +# This is a very exhaustive list! +# This list was based off of https://github.com/github/gitignore +# Ignore data that is generated during the runtime of an application +# This folder is used by the "Large Data" sample applications +runtime_data/ + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +# Jupyter Notebook +.ipynb_checkpoints +*/.ipynb_checkpoints/* +# IPython +profile_default/ +ipython_config.py +# Environments .env -settings.json \ No newline at end of file +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +# Spyder project settings +.spyderproject +.spyproject +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +# macOS General +.DS_Store +.AppleDouble +.LSOverride +# Icon must end with two \r +Icon +# Thumbnails +._* +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +# Dump file +*.stackdump +# Folder config file +[Dd]esktop.ini +# Recycle Bin used on file shares +$RECYCLE.BIN/ +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp +# Windows shortcuts +*.lnk +# History files +.Rhistory +.Rapp.history +# Session Data files +.RData +# User-specific files +.Ruserdata +# Example code in package build process +*-Ex.R +# Output files from R CMD check +/*.Rcheck/ +# RStudio files +.Rproj.user/ +# produced vignettes +vignettes/*.html +vignettes/*.pdf +# OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3 +.httr-oauth +# knitr and R markdown default cache directories +*_cache/ +/cache/ +# Temporary files created by R markdown +*.utf8.md +*.knit.md +# R Environment Variables +.Renviron +# Linux +*~ +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* +# KDE directory preferences +.directory +# Linux trash folder which might appear on any partition or disk +.Trash-* +# .nfs files are created when an open file is removed but is still being accessed +.nfs* +# VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +# SublineText +# Cache files for Sublime Text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache +# Workspace files are user-specific +*.sublime-workspace +# Project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using Sublime Text +# *.sublime-project +# SFTP configuration file +sftp-config.json +# Package control specific files +Package Control.last-run +Package Control.ca-list +Package Control.ca-bundle +Package Control.system-ca-bundle +Package Control.cache/ +Package Control.ca-certs/ +Package Control.merged-ca-bundle +Package Control.user-ca-bundle +oscrypto-ca-bundle.crt +bh_unicode_properties.cache +# Sublime-github package stores a github token in this file +# https://packagecontrol.io/packages/sublime-github +GitHub.sublime-settings \ No newline at end of file diff --git a/apps/dash-wind-streaming/Notebook/wind-speed-generation.ipynb b/apps/dash-wind-streaming/Notebook/wind-speed-generation.ipynb deleted file mode 100644 index 1976288ed..000000000 --- a/apps/dash-wind-streaming/Notebook/wind-speed-generation.ipynb +++ /dev/null @@ -1,664 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "import plotly.plotly as py\n", - "from plotly.graph_objs import *\n", - "from scipy.optimize import curve_fit\n", - "import numpy as np\n", - "import pandas as pd\n", - "from scipy.stats import norm\n", - "import plotly.figure_factory as ff\n", - "import math\n", - "from scipy.stats import skewnorm\n", - "import datetime as dt" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "windVal = []\n", - "windError = []\n", - "windOrientation = []\n", - "prevVal = 20\n", - "prevOrientation = np.random.uniform(0, 360)\n", - "for i in range(0, 86400):\n", - " windVal.append(abs(np.random.normal(prevVal, 2, 1)[0]))\n", - " windError.append(abs(np.random.normal(round(prevVal/10), 1)))\n", - " if(i % 100 == 0):\n", - " windOrientation.append(np.random.uniform(prevOrientation-50,\n", - " prevOrientation+50))\n", - " else:\n", - " windOrientation.append(np.random.uniform(prevOrientation-5,\n", - " prevOrientation+5))\n", - " if(round(windVal[-1]) > 45):\n", - " prevVal = int(math.floor(windVal[-1]))\n", - " elif(round(windVal[-1]) < 10):\n", - " prevVal = int(math.ceil(windVal[-1]))\n", - " else:\n", - " prevVal = int(round(windVal[-1]))\n", - " prevOrientation = windOrientation[-1]" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "df = pd.DataFrame.from_dict({\n", - " 'Speed': windVal,\n", - " 'SpeedError': windError,\n", - " 'Direction': windOrientation\n", - " })" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "now = dt.datetime.now()\n", - "sec = now.second\n", - "minute = now.minute\n", - "hour = now.hour\n", - "\n", - "totalTime = (hour * 3600) + (minute * 60) + (sec)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "import sqlite3\n", - "from datetime import *\n", - "connex = sqlite3.connect(\"wind-data.db\") # Opens file if exists, else creates file\n", - "cur = connex.cursor() " - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "df.to_sql(name='Wind', con=connex)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
indexDirectionSpeedSpeedError
051818388.46711323.4556032.098050
151819385.24623022.8227481.073134
251820388.45413721.5370131.982093
351821386.65364225.4226322.680533
451822387.92431226.4440593.757128
551823392.38467824.8497311.705354
651824388.82520127.2553391.626143
751825384.11482025.5042711.971804
851826384.64613723.1418192.417021
951827387.90954723.2087111.300878
1051828388.01344419.4688761.846851
1151829391.43136316.0965081.756832
1251830395.27629813.4340310.085637
1351831391.45361614.4723170.229909
1451832394.9331619.3550900.369801
1551833395.1968926.2853181.750981
1651834396.4108484.8118511.339742
1751835399.5577622.8578930.961948
1851836394.9457873.8048831.296398
1951837396.6976273.6378590.559455
2051838397.1164282.7254070.465070
2151839395.5539733.2544781.126300
2251840395.3574686.2782690.242901
2351841399.3091596.2179411.110519
2451842396.16715411.5749180.472902
2551843392.94636912.5467612.696686
2651844388.65189011.5677950.000096
2751845389.26229812.5732601.361332
2851846389.44056315.2195820.180667
2951847390.46085813.7649022.724583
...............
16951987423.50499811.7255220.794980
17051988423.84520313.9053360.421338
17151989423.17882312.3249261.188691
17251990425.20652711.2511390.113524
17351991423.73781712.6404402.880048
17451992418.89845616.5504661.918532
17551993417.23887217.7937840.551114
17651994415.24113218.0972230.654216
17751995417.11002016.4295102.152254
17851996421.54961612.3424330.132018
17951997422.6924048.8556901.373699
18051998419.3232634.1367340.133812
18151999420.6514887.7816541.317008
18252000383.2832488.4082711.286599
18352001378.4552399.0317570.560556
18452002376.46420011.9434320.222717
18552003376.3903069.9693830.134329
18652004380.20182714.7367791.031438
18752005382.74616314.6924221.896312
18852006386.20154114.6106902.840782
18952007385.19131216.2168571.175926
19052008385.68991316.2727500.249718
19152009383.08084516.0211070.289782
19252010385.67501013.6302530.994068
19352011382.75925618.1450362.438731
19452012383.77383517.2613192.498423
19552013385.41400122.5965762.913657
19652014383.93096625.3886471.465414
19752015386.89887623.4120012.190445
19852016384.85767723.8839221.601534
\n", - "

199 rows × 4 columns

\n", - "
" - ], - "text/plain": [ - " index Direction Speed SpeedError\n", - "0 51818 388.467113 23.455603 2.098050\n", - "1 51819 385.246230 22.822748 1.073134\n", - "2 51820 388.454137 21.537013 1.982093\n", - "3 51821 386.653642 25.422632 2.680533\n", - "4 51822 387.924312 26.444059 3.757128\n", - "5 51823 392.384678 24.849731 1.705354\n", - "6 51824 388.825201 27.255339 1.626143\n", - "7 51825 384.114820 25.504271 1.971804\n", - "8 51826 384.646137 23.141819 2.417021\n", - "9 51827 387.909547 23.208711 1.300878\n", - "10 51828 388.013444 19.468876 1.846851\n", - "11 51829 391.431363 16.096508 1.756832\n", - "12 51830 395.276298 13.434031 0.085637\n", - "13 51831 391.453616 14.472317 0.229909\n", - "14 51832 394.933161 9.355090 0.369801\n", - "15 51833 395.196892 6.285318 1.750981\n", - "16 51834 396.410848 4.811851 1.339742\n", - "17 51835 399.557762 2.857893 0.961948\n", - "18 51836 394.945787 3.804883 1.296398\n", - "19 51837 396.697627 3.637859 0.559455\n", - "20 51838 397.116428 2.725407 0.465070\n", - "21 51839 395.553973 3.254478 1.126300\n", - "22 51840 395.357468 6.278269 0.242901\n", - "23 51841 399.309159 6.217941 1.110519\n", - "24 51842 396.167154 11.574918 0.472902\n", - "25 51843 392.946369 12.546761 2.696686\n", - "26 51844 388.651890 11.567795 0.000096\n", - "27 51845 389.262298 12.573260 1.361332\n", - "28 51846 389.440563 15.219582 0.180667\n", - "29 51847 390.460858 13.764902 2.724583\n", - ".. ... ... ... ...\n", - "169 51987 423.504998 11.725522 0.794980\n", - "170 51988 423.845203 13.905336 0.421338\n", - "171 51989 423.178823 12.324926 1.188691\n", - "172 51990 425.206527 11.251139 0.113524\n", - "173 51991 423.737817 12.640440 2.880048\n", - "174 51992 418.898456 16.550466 1.918532\n", - "175 51993 417.238872 17.793784 0.551114\n", - "176 51994 415.241132 18.097223 0.654216\n", - "177 51995 417.110020 16.429510 2.152254\n", - "178 51996 421.549616 12.342433 0.132018\n", - "179 51997 422.692404 8.855690 1.373699\n", - "180 51998 419.323263 4.136734 0.133812\n", - "181 51999 420.651488 7.781654 1.317008\n", - "182 52000 383.283248 8.408271 1.286599\n", - "183 52001 378.455239 9.031757 0.560556\n", - "184 52002 376.464200 11.943432 0.222717\n", - "185 52003 376.390306 9.969383 0.134329\n", - "186 52004 380.201827 14.736779 1.031438\n", - "187 52005 382.746163 14.692422 1.896312\n", - "188 52006 386.201541 14.610690 2.840782\n", - "189 52007 385.191312 16.216857 1.175926\n", - "190 52008 385.689913 16.272750 0.249718\n", - "191 52009 383.080845 16.021107 0.289782\n", - "192 52010 385.675010 13.630253 0.994068\n", - "193 52011 382.759256 18.145036 2.438731\n", - "194 52012 383.773835 17.261319 2.498423\n", - "195 52013 385.414001 22.596576 2.913657\n", - "196 52014 383.930966 25.388647 1.465414\n", - "197 52015 386.898876 23.412001 2.190445\n", - "198 52016 384.857677 23.883922 1.601534\n", - "\n", - "[199 rows x 4 columns]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "con = sqlite3.connect(\"wind-data.db\")\n", - "df = pd.read_sql_query(\"SELECT * from Wind where rowid > \"+ str(totalTime-200) + \" AND rowid < \" + str(totalTime) + \";\" , con)\n", - "df" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 2", - "language": "python", - "name": "python2" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 2 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/apps/dash-wind-streaming/app.py b/apps/dash-wind-streaming/app.py index 1c7029dbc..e7ff083e1 100644 --- a/apps/dash-wind-streaming/app.py +++ b/apps/dash-wind-streaming/app.py @@ -1,22 +1,8 @@ -from dash import Dash, html, Input, Output, State +from dash import Dash, html, dcc, Input, Output, State, callback, callback_context -from dash.exceptions import PreventUpdate - - -from utils.components import ( - Header, - slider, - checklist, - left_graph, - interval, - right_graph_one, - right_graph_two, -) -from utils.helper_functions import ( - gen_wind_speed, - gen_wind_direction, - gen_wind_histogram, -) +from constants import GRAPH_INTERVAL +from utils.components import Header, wind_speed_card, histogram_card, wind_direction_card +import utils.figures as figs app = Dash(__name__, title="Wind Speed Dashboard") server = app.server @@ -30,140 +16,51 @@ ), html.Div( [ - # wind speed - html.Div( - [ - html.Div( - [html.H6("WIND SPEED (MPH)", className="graph__title")] - ), - left_graph, - interval, - ], - className="two-thirds column wind__speed__container", - ), + wind_speed_card("wind-speed"), html.Div( [ - # histogram - html.Div( - [ - html.Div( - [ - html.H6( - "WIND SPEED HISTOGRAM", - className="graph__title", - ) - ] - ), - html.Div( - slider, - className="slider", - ), - html.Div( - checklist, - className="auto__container", - ), - right_graph_one, - ], - className="graph__container first", - ), - # wind direction - html.Div( - [ - html.Div( - [ - html.H6( - "WIND DIRECTION", className="graph__title" - ) - ] - ), - right_graph_two, - ], - className="graph__container second", - ), + histogram_card("wind-histogram"), + wind_direction_card("wind-direction") ], className="one-third column histogram__direction", ), ], className="app__content", ), + dcc.Interval(id="wind-speed-update", interval=GRAPH_INTERVAL) ], className="app__container", ) -@app.callback(Output("wind-speed", "figure"), Input("wind-speed-update", "n_intervals")) -def return_gen_wind_speed(interval): - """ - Generate the wind speed graph. - - :params interval: update the graph based on an interval - """ - - return gen_wind_speed() - - -@app.callback( - Output("wind-direction", "figure"), Input("wind-speed-update", "n_intervals") -) -def return_gen_wind_direction(interval): - """ - Generate the wind direction graph. - - :params interval: update the graph based on an interval - """ - - return gen_wind_direction() - - -@app.callback( +@callback( Output("wind-histogram", "figure"), + Output("wind-speed", "figure"), + Output("wind-direction", "figure"), Input("wind-speed-update", "n_intervals"), - State("wind-speed", "figure"), State("bin-slider", "value"), State("bin-auto", "value"), ) -def return_gen_wind_histogram(interval, wind_speed_figure, slider_value, auto_state): - """ - Genererate wind histogram graph. - - :params interval: upadte the graph based on an interval - :params wind_speed_figure: current wind speed graph - :params slider_value: current slider value - :params auto_state: current auto state - """ - return gen_wind_histogram(wind_speed_figure, slider_value, auto_state) +def return_gen_wind_histogram(interval, slider_value, auto_state): + fig_wind_speed = figs.gen_wind_speed() + fig_wind_direction =figs.gen_wind_direction() + fig_wind_histogram = figs.gen_wind_histogram(fig_wind_speed, slider_value, auto_state) + return fig_wind_histogram, fig_wind_speed, fig_wind_direction -@app.callback( +@callback( Output("bin-auto", "value"), - Input("bin-slider", "value"), - State("wind-speed", "figure"), -) -def deselect_auto(slider_value, wind_speed_figure): - """Toggle the auto checkbox.""" - - # prevent update if graph has no data - if "data" not in wind_speed_figure: - raise PreventUpdate - if not len(wind_speed_figure["data"]): - raise PreventUpdate - - if wind_speed_figure is not None and len(wind_speed_figure["data"][0]["y"]) > 5: - return "" - return "Auto" - - -@app.callback( Output("bin-size", "children"), + Input("bin-slider", "value"), Input("bin-auto", "value"), - State("bin-slider", "value"), + prevent_initial_call=True, ) -def show_num_bins(autoValue, slider_value): - """Display the number of bins.""" - - if "Auto" in autoValue: - return "# of Bins: Auto" - return "# of Bins: " + str(int(slider_value)) +def deselect_auto(slider_value, bin_auto): + triggered_id = callback_context.triggered[0]["prop_id"].split(".")[0] + if triggered_id == "bin-slider": + return [], "# of Bins: " + str(int(slider_value)) + else: + return ["Auto"], "# of Bins: Auto" if __name__ == "__main__": diff --git a/apps/dash-wind-streaming/assets/css/demo-button.css b/apps/dash-wind-streaming/assets/css/demo-button.css deleted file mode 100644 index f030e1b9e..000000000 --- a/apps/dash-wind-streaming/assets/css/demo-button.css +++ /dev/null @@ -1,6 +0,0 @@ -.link-button { - margin-top: 10px; - margin-right: 10px; - vertical-align: top; - color: white; -} \ No newline at end of file diff --git a/apps/dash-wind-streaming/assets/css/header.css b/apps/dash-wind-streaming/assets/css/header.css index 6acac321d..ec39e0172 100644 --- a/apps/dash-wind-streaming/assets/css/header.css +++ b/apps/dash-wind-streaming/assets/css/header.css @@ -2,14 +2,14 @@ .header { height: 10vh; display: flex; - background-color: #0d1c41; + background-color: transparent; padding-left: 2%; padding-right: 2%; font-family: playfair display, sans-serif; } .header .header-title { - color: #A2ACB8 !important; + color: #FFFFFF !important; font-size: 5vh; } diff --git a/apps/dash-wind-streaming/constants.py b/apps/dash-wind-streaming/constants.py index 443eb0521..16deaaa8b 100644 --- a/apps/dash-wind-streaming/constants.py +++ b/apps/dash-wind-streaming/constants.py @@ -1 +1,8 @@ +import os + app_color = {"graph_bg": "#082255", "graph_line": "#007ACE"} + +# DB_FILE = pathlib.Path(__file__).resolve().parent.joinpath("./data/wind-data.db").resolve() +DB_FILE = "data/wind-data.db" + +GRAPH_INTERVAL = int(os.environ.get("GRAPH_INTERVAL", 5000)) \ No newline at end of file diff --git a/apps/dash-wind-streaming/db/wind-data.db b/apps/dash-wind-streaming/data/wind-data.db similarity index 100% rename from apps/dash-wind-streaming/db/wind-data.db rename to apps/dash-wind-streaming/data/wind-data.db diff --git a/apps/dash-wind-streaming/db/__init__.py b/apps/dash-wind-streaming/db/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/dash-wind-streaming/utils/components.py b/apps/dash-wind-streaming/utils/components.py index fb72307c5..cc20688a8 100644 --- a/apps/dash-wind-streaming/utils/components.py +++ b/apps/dash-wind-streaming/utils/components.py @@ -1,9 +1,6 @@ from dash import html, dcc -from utils.helper_functions import app_color -import os - -GRAPH_INTERVAL = os.environ.get("GRAPH_INTERVAL", 5000) +from constants import app_color def Header(app, header, subheader=None): left_headers = html.Div( @@ -16,7 +13,7 @@ def Header(app, header, subheader=None): logo = html.Img(src=app.get_asset_url("images/plotly-logo.png")) link = html.A(logo, href="https://plotly.com/dash/", target="_blank") demo_link = html.A( - "ENTERPRISE DEMO", + "LEARN MORE", href="https://plotly.com/dash/", target="_blank", className="demo-button", @@ -26,68 +23,77 @@ def Header(app, header, subheader=None): return html.Div([left_headers, right_logos], className="header") -slider = [ - dcc.Slider( - id="bin-slider", - min=1, - max=60, - step=1, - value=20, - updatemode="drag", - marks={ - 20: {"label": "20"}, - 40: {"label": "40"}, - 60: {"label": "60"}, - }, +def wind_speed_card(graph_id): + return html.Div([ + html.Div(html.H6("WIND SPEED (MPH)", className="graph__title")), + dcc.Graph( + id=graph_id, + figure=dict( + layout=dict( + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + ) + ), + ), + ], className="two-thirds column wind__speed__container", ) -] -checklist = [ - dcc.Checklist( - id="bin-auto", - options=[{"label": "Auto", "value": "Auto"}], - value=["Auto"], - inputClassName="auto__checkbox", - labelClassName="auto__label", - ), - html.P( - "# of Bins: Auto", - id="bin-size", - className="auto__p", - ), -] - -left_graph = dcc.Graph( - id="wind-speed", - figure=dict( - layout=dict( - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - ) - ), -) -interval = dcc.Interval( - id="wind-speed-update", - interval=int(GRAPH_INTERVAL), - n_intervals=0, -) - -right_graph_one = dcc.Graph( - id="wind-histogram", - figure=dict( - layout=dict( - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - ) - ), -) +def histogram_card(graph_id): + return html.Div([ + html.Div(html.H6("WIND SPEED HISTOGRAM", className="graph__title")), + html.Div( + dcc.Slider( + id="bin-slider", + min=1, + max=60, + step=1, + value=20, + marks={ + 20: {"label": "20"}, + 40: {"label": "40"}, + 60: {"label": "60"}, + }, + ), + className="slider", + ), + html.Div([ + dcc.Checklist( + id="bin-auto", + options=["Auto"], + value=["Auto"], + inputClassName="auto__checkbox", + labelClassName="auto__label", + ), + html.P( + "# of Bins: Auto", + id="bin-size", + className="auto__p", + )], + className="auto__container", + ), + dcc.Graph( + id=graph_id, + figure=dict( + layout=dict( + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + ) + ), + ), + ], className="graph__container first", + ) -right_graph_two = dcc.Graph( - id="wind-direction", - figure=dict( - layout=dict( - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - ) - ), -) +def wind_direction_card(graph_id): + return html.Div([ + html.Div(html.H6("WIND DIRECTION", className="graph__title")), + dcc.Graph( + id=graph_id, + figure=dict( + layout=dict( + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + ) + ), + ), + ], className="graph__container second", + ) \ No newline at end of file diff --git a/apps/dash-wind-streaming/db/api.py b/apps/dash-wind-streaming/utils/database_connection.py similarity index 87% rename from apps/dash-wind-streaming/db/api.py rename to apps/dash-wind-streaming/utils/database_connection.py index 5a21df66a..fea16418f 100644 --- a/apps/dash-wind-streaming/db/api.py +++ b/apps/dash-wind-streaming/utils/database_connection.py @@ -1,10 +1,7 @@ -import pathlib import sqlite3 import pandas as pd - -DB_FILE = pathlib.Path(__file__).resolve().parent.joinpath("wind-data.db").resolve() - +from constants import DB_FILE def get_wind_data(start, end): """ diff --git a/apps/dash-wind-streaming/utils/figures.py b/apps/dash-wind-streaming/utils/figures.py new file mode 100644 index 000000000..b209e47e5 --- /dev/null +++ b/apps/dash-wind-streaming/utils/figures.py @@ -0,0 +1,243 @@ + +import numpy as np +from scipy.stats import rayleigh +from dash.exceptions import PreventUpdate + +from constants import app_color +from utils.helper_functions import get_current_time +from utils.database_connection import get_wind_data, get_wind_data_by_id + + +def gen_wind_speed(): + """ + Generate the wind speed graph. + + :params interval: update the graph based on an interval + """ + + total_time = get_current_time() + df = get_wind_data(total_time - 200, total_time) + + trace = dict( + type="scatter", + y=df["Speed"], + line={"color": "#42C4F7"}, + hoverinfo="skip", + error_y={ + "type": "data", + "array": df["SpeedError"], + "thickness": 1.5, + "width": 2, + "color": "#B4E8FC", + }, + mode="lines", + ) + + layout = dict( + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + font={"color": "#fff"}, + height=700, + xaxis={ + "range": [0, 200], + "showline": True, + "zeroline": False, + "fixedrange": True, + "tickvals": [0, 50, 100, 150, 200], + "ticktext": ["200", "150", "100", "50", "0"], + "title": "Time Elapsed (sec)", + }, + yaxis={ + "range": [ + min(0, min(df["Speed"])), + max(45, max(df["Speed"]) + max(df["SpeedError"])), + ], + "showgrid": True, + "showline": True, + "fixedrange": True, + "zeroline": False, + "gridcolor": app_color["graph_line"], + "nticks": max(6, round(df["Speed"].iloc[-1] / 10)), + }, + ) + + return dict(data=[trace], layout=layout) + + + + +def gen_wind_direction(): + """ + Generate the wind direction graph. + + :params interval: update the graph based on an interval + """ + + total_time = get_current_time() + df = get_wind_data_by_id(total_time) + val = df["Speed"].iloc[-1] + direction = [0, (df["Direction"][0] - 20), (df["Direction"][0] + 20), 0] + + traces_scatterpolar = [ + {"r": [0, val, val, 0], "fillcolor": "#084E8A"}, + {"r": [0, val * 0.65, val * 0.65, 0], "fillcolor": "#B4E1FA"}, + {"r": [0, val * 0.3, val * 0.3, 0], "fillcolor": "#EBF5FA"}, + ] + + data = [ + dict( + type="scatterpolar", + r=traces["r"], + theta=direction, + mode="lines", + fill="toself", + fillcolor=traces["fillcolor"], + line={"color": "rgba(32, 32, 32, .6)", "width": 1}, + ) + for traces in traces_scatterpolar + ] + + layout = dict( + height=350, + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + font={"color": "#fff"}, + autosize=False, + polar={ + "bgcolor": app_color["graph_line"], + "radialaxis": {"range": [0, 45], "angle": 45, "dtick": 10}, + "angularaxis": {"showline": False, "tickcolor": "white"}, + }, + showlegend=False, + ) + + return dict(data=data, layout=layout) + + + +def gen_wind_histogram(wind_speed_figure, slider_value, auto_state): + """ + Genererate wind histogram graph. + + :params interval: upadte the graph based on an interval + :params wind_speed_figure: current wind speed graph + :params slider_value: current slider value + :params auto_state: current auto state + """ + + wind_val = [] + + try: + # Check to see whether wind-speed has been plotted yet + if wind_speed_figure is not None: + wind_val = wind_speed_figure["data"][0]["y"] + if "Auto" in auto_state: + bin_val = np.histogram( + wind_val, + bins=range(int(round(min(wind_val))), int(round(max(wind_val)))), + ) + else: + bin_val = np.histogram(wind_val, bins=slider_value) + except Exception as error: + raise PreventUpdate + + avg_val = float(sum(wind_val)) / len(wind_val) + median_val = np.median(wind_val) + + pdf_fitted = rayleigh.pdf( + bin_val[1], loc=(avg_val) * 0.55, scale=(bin_val[1][-1] - bin_val[1][0]) / 3 + ) + + y_val = (pdf_fitted * max(bin_val[0]) * 20,) + y_val_max = max(y_val[0]) + bin_val_max = max(bin_val[0]) + + trace = dict( + type="bar", + x=bin_val[1], + y=bin_val[0], + marker={"color": app_color["graph_line"]}, + showlegend=False, + hoverinfo="x+y", + ) + + traces_scatter = [ + {"line_dash": "dash", "line_color": "#2E5266", "name": "Average"}, + {"line_dash": "dot", "line_color": "#BD9391", "name": "Median"}, + ] + + scatter_data = [ + dict( + type="scatter", + x=[bin_val[int(len(bin_val) / 2)]], + y=[0], + mode="lines", + line={"dash": traces["line_dash"], "color": traces["line_color"]}, + marker={"opacity": 0}, + visible=True, + name=traces["name"], + ) + for traces in traces_scatter + ] + + trace3 = dict( + type="scatter", + mode="lines", + line={"color": "#42C4F7"}, + y=y_val[0], + x=bin_val[1][: len(bin_val[1])], + name="Rayleigh Fit", + ) + layout = dict( + height=350, + plot_bgcolor=app_color["graph_bg"], + paper_bgcolor=app_color["graph_bg"], + font={"color": "#fff"}, + xaxis={ + "title": "Wind Speed (mph)", + "showgrid": False, + "showline": False, + "fixedrange": True, + }, + yaxis={ + "showgrid": False, + "showline": False, + "zeroline": False, + "title": "Number of Samples", + "fixedrange": True, + }, + autosize=True, + bargap=0.01, + bargroupgap=0, + hovermode="closest", + legend={ + "orientation": "h", + "yanchor": "bottom", + "xanchor": "center", + "y": 1, + "x": 0.5, + }, + shapes=[ + { + "xref": "x", + "yref": "y", + "y1": int(max(bin_val_max, y_val_max)) + 0.5, + "y0": 0, + "x0": avg_val, + "x1": avg_val, + "type": "line", + "line": {"dash": "dash", "color": "#2E5266", "width": 5}, + }, + { + "xref": "x", + "yref": "y", + "y1": int(max(bin_val_max, y_val_max)) + 0.5, + "y0": 0, + "x0": median_val, + "x1": median_val, + "type": "line", + "line": {"dash": "dot", "color": "#BD9391", "width": 5}, + }, + ], + ) + return dict(data=[trace, scatter_data[0], scatter_data[1], trace3], layout=layout) diff --git a/apps/dash-wind-streaming/utils/helper_functions.py b/apps/dash-wind-streaming/utils/helper_functions.py index dc9cde1f7..b7efd5039 100644 --- a/apps/dash-wind-streaming/utils/helper_functions.py +++ b/apps/dash-wind-streaming/utils/helper_functions.py @@ -1,247 +1,8 @@ import datetime as dt -from db.api import get_wind_data, get_wind_data_by_id -import numpy as np -from scipy.stats import rayleigh - -from constants import app_color -from dash.exceptions import PreventUpdate - def get_current_time(): """Helper function to get the current time in seconds.""" now = dt.datetime.now() total_time = (now.hour * 3600) + (now.minute * 60) + (now.second) - return total_time - - -def gen_wind_speed(): - """ - Generate the wind speed graph. - - :params interval: update the graph based on an interval - """ - - total_time = get_current_time() - df = get_wind_data(total_time - 200, total_time) - - trace = dict( - type="scatter", - y=df["Speed"], - line={"color": "#42C4F7"}, - hoverinfo="skip", - error_y={ - "type": "data", - "array": df["SpeedError"], - "thickness": 1.5, - "width": 2, - "color": "#B4E8FC", - }, - mode="lines", - ) - - layout = dict( - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - font={"color": "#fff"}, - height=700, - xaxis={ - "range": [0, 200], - "showline": True, - "zeroline": False, - "fixedrange": True, - "tickvals": [0, 50, 100, 150, 200], - "ticktext": ["200", "150", "100", "50", "0"], - "title": "Time Elapsed (sec)", - }, - yaxis={ - "range": [ - min(0, min(df["Speed"])), - max(45, max(df["Speed"]) + max(df["SpeedError"])), - ], - "showgrid": True, - "showline": True, - "fixedrange": True, - "zeroline": False, - "gridcolor": app_color["graph_line"], - "nticks": max(6, round(df["Speed"].iloc[-1] / 10)), - }, - ) - - return dict(data=[trace], layout=layout) - - -def gen_wind_direction(): - """ - Generate the wind direction graph. - - :params interval: update the graph based on an interval - """ - - total_time = get_current_time() - df = get_wind_data_by_id(total_time) - val = df["Speed"].iloc[-1] - direction = [0, (df["Direction"][0] - 20), (df["Direction"][0] + 20), 0] - - traces_scatterpolar = [ - {"r": [0, val, val, 0], "fillcolor": "#084E8A"}, - {"r": [0, val * 0.65, val * 0.65, 0], "fillcolor": "#B4E1FA"}, - {"r": [0, val * 0.3, val * 0.3, 0], "fillcolor": "#EBF5FA"}, - ] - - data = [ - dict( - type="scatterpolar", - r=traces["r"], - theta=direction, - mode="lines", - fill="toself", - fillcolor=traces["fillcolor"], - line={"color": "rgba(32, 32, 32, .6)", "width": 1}, - ) - for traces in traces_scatterpolar - ] - - layout = dict( - height=350, - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - font={"color": "#fff"}, - autosize=False, - polar={ - "bgcolor": app_color["graph_line"], - "radialaxis": {"range": [0, 45], "angle": 45, "dtick": 10}, - "angularaxis": {"showline": False, "tickcolor": "white"}, - }, - showlegend=False, - ) - - return dict(data=data, layout=layout) - - -def gen_wind_histogram(wind_speed_figure, slider_value, auto_state): - """ - Genererate wind histogram graph. - - :params interval: upadte the graph based on an interval - :params wind_speed_figure: current wind speed graph - :params slider_value: current slider value - :params auto_state: current auto state - """ - - wind_val = [] - - try: - # Check to see whether wind-speed has been plotted yet - if wind_speed_figure is not None: - wind_val = wind_speed_figure["data"][0]["y"] - if "Auto" in auto_state: - bin_val = np.histogram( - wind_val, - bins=range(int(round(min(wind_val))), int(round(max(wind_val)))), - ) - else: - bin_val = np.histogram(wind_val, bins=slider_value) - except Exception as error: - raise PreventUpdate - - avg_val = float(sum(wind_val)) / len(wind_val) - median_val = np.median(wind_val) - - pdf_fitted = rayleigh.pdf( - bin_val[1], loc=(avg_val) * 0.55, scale=(bin_val[1][-1] - bin_val[1][0]) / 3 - ) - - y_val = (pdf_fitted * max(bin_val[0]) * 20,) - y_val_max = max(y_val[0]) - bin_val_max = max(bin_val[0]) - - trace = dict( - type="bar", - x=bin_val[1], - y=bin_val[0], - marker={"color": app_color["graph_line"]}, - showlegend=False, - hoverinfo="x+y", - ) - - traces_scatter = [ - {"line_dash": "dash", "line_color": "#2E5266", "name": "Average"}, - {"line_dash": "dot", "line_color": "#BD9391", "name": "Median"}, - ] - - scatter_data = [ - dict( - type="scatter", - x=[bin_val[int(len(bin_val) / 2)]], - y=[0], - mode="lines", - line={"dash": traces["line_dash"], "color": traces["line_color"]}, - marker={"opacity": 0}, - visible=True, - name=traces["name"], - ) - for traces in traces_scatter - ] - - trace3 = dict( - type="scatter", - mode="lines", - line={"color": "#42C4F7"}, - y=y_val[0], - x=bin_val[1][: len(bin_val[1])], - name="Rayleigh Fit", - ) - layout = dict( - height=350, - plot_bgcolor=app_color["graph_bg"], - paper_bgcolor=app_color["graph_bg"], - font={"color": "#fff"}, - xaxis={ - "title": "Wind Speed (mph)", - "showgrid": False, - "showline": False, - "fixedrange": True, - }, - yaxis={ - "showgrid": False, - "showline": False, - "zeroline": False, - "title": "Number of Samples", - "fixedrange": True, - }, - autosize=True, - bargap=0.01, - bargroupgap=0, - hovermode="closest", - legend={ - "orientation": "h", - "yanchor": "bottom", - "xanchor": "center", - "y": 1, - "x": 0.5, - }, - shapes=[ - { - "xref": "x", - "yref": "y", - "y1": int(max(bin_val_max, y_val_max)) + 0.5, - "y0": 0, - "x0": avg_val, - "x1": avg_val, - "type": "line", - "line": {"dash": "dash", "color": "#2E5266", "width": 5}, - }, - { - "xref": "x", - "yref": "y", - "y1": int(max(bin_val_max, y_val_max)) + 0.5, - "y0": 0, - "x0": median_val, - "x1": median_val, - "type": "line", - "line": {"dash": "dot", "color": "#BD9391", "width": 5}, - }, - ], - ) - return dict(data=[trace, scatter_data[0], scatter_data[1], trace3], layout=layout) + return total_time \ No newline at end of file From 4ab7c8b277b447a291b90e3dbc78f2e12259463b Mon Sep 17 00:00:00 2001 From: admin <51248046+danton267@users.noreply.github.com> Date: Wed, 25 May 2022 14:46:53 +0100 Subject: [PATCH 7/7] final --- apps/dash-wind-streaming/constants.py | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/dash-wind-streaming/constants.py b/apps/dash-wind-streaming/constants.py index 16deaaa8b..608ffde50 100644 --- a/apps/dash-wind-streaming/constants.py +++ b/apps/dash-wind-streaming/constants.py @@ -2,7 +2,6 @@ app_color = {"graph_bg": "#082255", "graph_line": "#007ACE"} -# DB_FILE = pathlib.Path(__file__).resolve().parent.joinpath("./data/wind-data.db").resolve() DB_FILE = "data/wind-data.db" GRAPH_INTERVAL = int(os.environ.get("GRAPH_INTERVAL", 5000)) \ No newline at end of file