-
Notifications
You must be signed in to change notification settings - Fork 67
/
Copy pathapp.py
224 lines (183 loc) · 9.31 KB
/
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import dash
import dash_core_components as dcc
import dash_html_components as html
import numpy as np
import plotly.graph_objects as go
from database import fetch_all_bpa_as_df
# Definitions of constants. This projects uses extra CSS stylesheet at `./assets/style.css`
COLORS = ['rgb(67,67,67)', 'rgb(115,115,115)', 'rgb(49,130,189)', 'rgb(189,189,189)']
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css', '/assets/style.css']
# Define the dash app first
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
# Define component functions
def page_header():
"""
Returns the page header as a dash `html.Div`
"""
return html.Div(id='header', children=[
html.Div([html.H3('Visualization with datashader and Plotly')],
className="ten columns"),
html.A([html.Img(id='logo', src=app.get_asset_url('github.png'),
style={'height': '35px', 'paddingTop': '7%'}),
html.Span('Blownhither', style={'fontSize': '2rem', 'height': '35px', 'bottom': 0,
'paddingLeft': '4px', 'color': '#a3a7b0',
'textDecoration': 'none'})],
className="two columns row",
href='https://github.com/blownhither/'),
], className="row")
def description():
"""
Returns overall project description in markdown
"""
return html.Div(children=[dcc.Markdown('''
# Energy Planner
As of today, 138 cities in the U.S. have formally announced 100% renewable energy goals or
targets, while others are actively considering similar goals. Despite ambition and progress,
conversion towards renewable energy remains challenging.
Wind and solar power are becoming more cost effective, but they will always be unreliable
and intermittent sources of energy. They follow weather patterns with potential for lots of
variability. Solar power starts to die away right at sunset, when one of the two daily peaks
arrives (see orange curve for load).
**Energy Planner is a "What-If" tool to assist making power conversion plans.**
It can be used to explore load satisfiability under different power contribution with
near-real-time energy production & consumption data.
### Data Source
Energy Planner utilizes near-real-time energy production & consumption data from [BPA
Balancing Authority](https://www.bpa.gov/news/AboutUs/Pages/default.aspx).
The [data source](https://transmission.bpa.gov/business/operations/Wind/baltwg.aspx)
**updates every 5 minutes**.
''', className='eleven columns', style={'paddingLeft': '5%'})], className="row")
def static_stacked_trend_graph(stack=False):
"""
Returns scatter line plot of all power sources and power load.
If `stack` is `True`, the 4 power sources are stacked together to show the overall power
production.
"""
df = fetch_all_bpa_as_df()
if df is None:
return go.Figure()
sources = ['Wind', 'Hydro', 'Fossil/Biomass', 'Nuclear']
x = df['Datetime']
fig = go.Figure()
for i, s in enumerate(sources):
fig.add_trace(go.Scatter(x=x, y=df[s], mode='lines', name=s,
line={'width': 2, 'color': COLORS[i]},
stackgroup='stack' if stack else None))
fig.add_trace(go.Scatter(x=x, y=df['Load'], mode='lines', name='Load',
line={'width': 2, 'color': 'orange'}))
title = 'Energy Production & Consumption under BPA Balancing Authority'
if stack:
title += ' [Stacked]'
fig.update_layout(template='plotly_dark',
title=title,
plot_bgcolor='#23272c',
paper_bgcolor='#23272c',
yaxis_title='MW',
xaxis_title='Date/Time')
return fig
def what_if_description():
"""
Returns description of "What-If" - the interactive component
"""
return html.Div(children=[
dcc.Markdown('''
# " What If "
So far, BPA has been relying on hydro power to balance the demand and supply of power.
Could our city survive an outage of hydro power and use up-scaled wind power as an
alternative? Find below **what would happen with 2.5x wind power and no hydro power at
all**.
Feel free to try out more combinations with the sliders. For the clarity of demo code,
only two sliders are included here. A fully-functioning What-If tool should support
playing with other interesting aspects of the problem (e.g. instability of load).
''', className='eleven columns', style={'paddingLeft': '5%'})
], className="row")
def what_if_tool():
"""
Returns the What-If tool as a dash `html.Div`. The view is a 8:3 division between
demand-supply plot and rescale sliders.
"""
return html.Div(children=[
html.Div(children=[dcc.Graph(id='what-if-figure')], className='nine columns'),
html.Div(children=[
html.H5("Rescale Power Supply", style={'marginTop': '2rem'}),
html.Div(children=[
dcc.Slider(id='wind-scale-slider', min=0, max=4, step=0.1, value=2.5, className='row',
marks={x: str(x) for x in np.arange(0, 4.1, 1)})
], style={'marginTop': '5rem'}),
html.Div(id='wind-scale-text', style={'marginTop': '1rem'}),
html.Div(children=[
dcc.Slider(id='hydro-scale-slider', min=0, max=4, step=0.1, value=0,
className='row', marks={x: str(x) for x in np.arange(0, 4.1, 1)})
], style={'marginTop': '3rem'}),
html.Div(id='hydro-scale-text', style={'marginTop': '1rem'}),
], className='three columns', style={'marginLeft': 5, 'marginTop': '10%'}),
], className='row eleven columns')
def architecture_summary():
"""
Returns the text and image of architecture summary of the project.
"""
return html.Div(children=[
dcc.Markdown('''
# Project Architecture
This project uses MongoDB as the database. All data acquired are stored in raw form to the
database (with de-duplication). An abstract layer is built in `database.py` so all queries
can be done via function call. For a more complicated app, the layer will also be
responsible for schema consistency. A `plot.ly` & `dash` app is serving this web page
through. Actions on responsive components on the page is redirected to `app.py` which will
then update certain components on the page.
''', className='row eleven columns', style={'paddingLeft': '5%'}),
html.Div(children=[
html.Img(src="https://docs.google.com/drawings/d/e/2PACX-1vQNerIIsLZU2zMdRhIl3ZZkDMIt7jhE_fjZ6ZxhnJ9bKe1emPcjI92lT5L7aZRYVhJgPZ7EURN0AqRh/pub?w=670&h=457",
className='row'),
], className='row', style={'textAlign': 'center'}),
dcc.Markdown('''
''')
], className='row')
# Sequentially add page components to the app's layout
def dynamic_layout():
return html.Div([
page_header(),
html.Hr(),
description(),
# dcc.Graph(id='trend-graph', figure=static_stacked_trend_graph(stack=False)),
dcc.Graph(id='stacked-trend-graph', figure=static_stacked_trend_graph(stack=True)),
what_if_description(),
what_if_tool(),
architecture_summary(),
], className='row', id='content')
# set layout to a function which updates upon reloading
app.layout = dynamic_layout
# Defines the dependencies of interactive components
@app.callback(
dash.dependencies.Output('wind-scale-text', 'children'),
[dash.dependencies.Input('wind-scale-slider', 'value')])
def update_wind_sacle_text(value):
"""Changes the display text of the wind slider"""
return "Wind Power Scale {:.2f}x".format(value)
@app.callback(
dash.dependencies.Output('hydro-scale-text', 'children'),
[dash.dependencies.Input('hydro-scale-slider', 'value')])
def update_hydro_sacle_text(value):
"""Changes the display text of the hydro slider"""
return "Hydro Power Scale {:.2f}x".format(value)
@app.callback(
dash.dependencies.Output('what-if-figure', 'figure'),
[dash.dependencies.Input('wind-scale-slider', 'value'),
dash.dependencies.Input('hydro-scale-slider', 'value')])
def what_if_handler(wind, hydro):
"""Changes the display graph of supply-demand"""
df = fetch_all_bpa_as_df(allow_cached=True)
x = df['Datetime']
supply = df['Wind'] * wind + df['Hydro'] * hydro + df['Fossil/Biomass'] + df['Nuclear']
load = df['Load']
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=supply, mode='none', name='supply', line={'width': 2, 'color': 'pink'},
fill='tozeroy'))
fig.add_trace(go.Scatter(x=x, y=load, mode='none', name='demand', line={'width': 2, 'color': 'orange'},
fill='tonexty'))
fig.update_layout(template='plotly_dark', title='Supply/Demand after Power Scaling',
plot_bgcolor='#23272c', paper_bgcolor='#23272c', yaxis_title='MW',
xaxis_title='Date/Time')
return fig
if __name__ == '__main__':
app.run_server(debug=True, port=1050, host='0.0.0.0')