Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
import param
|
2 |
import panel as pn
|
3 |
import numpy as np
|
@@ -5,9 +7,11 @@ import pandas as pd
|
|
5 |
import hvplot.pandas
|
6 |
import geoviews as gv
|
7 |
import holoviews as hv
|
8 |
-
from holoviews.streams import Tap
|
9 |
from bokeh.themes import Theme
|
10 |
|
|
|
|
|
11 |
VAR_OPTIONS = {
|
12 |
"Maximum Air Temperature [F]": "max_temp_f",
|
13 |
"Minimum Air Temperature [F]": "min_temp_f",
|
@@ -31,11 +35,27 @@ VAR_OPTIONS = {
|
|
31 |
"Maximum wind gust [knots]": "max_wind_gust_kts",
|
32 |
"Daily Solar Radiation MJ/m2": "srad_mj",
|
33 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
VAR_OPTIONS_R = {v: k for k, v in VAR_OPTIONS.items()}
|
35 |
NETWORKS_URL = "https://mesonet.agron.iastate.edu/sites/networks.php?network=_ALL_&format=csv&nohtml=on"
|
36 |
STATION_URL_FMT = (
|
37 |
"https://mesonet.agron.iastate.edu/cgi-bin/request/daily.py?network={network}&stations={station}"
|
38 |
-
"&year1=1928&month1=1&day1=1&year2=
|
39 |
)
|
40 |
DARK_RED = "#FF5555"
|
41 |
DARK_BLUE = "#5588FF"
|
@@ -117,24 +137,63 @@ THEME_JSON = {
|
|
117 |
}
|
118 |
}
|
119 |
theme = Theme(json=THEME_JSON)
|
|
|
120 |
|
121 |
hv.renderer("bokeh").theme = theme
|
122 |
-
|
123 |
|
124 |
class ClimateApp(pn.viewable.Viewer):
|
125 |
-
network = param.Selector(default="WA_ASOS")
|
126 |
-
station = param.Selector(default="SEA")
|
127 |
-
year = param.Integer(default=
|
128 |
-
year_range = param.Range(
|
|
|
|
|
129 |
var = param.Selector(default="max_temp_f", objects=sorted(VAR_OPTIONS.values()))
|
130 |
stat = param.Selector(default="Mean", objects=["Mean", "Median"])
|
131 |
|
132 |
def __init__(self, **params):
|
133 |
super().__init__(**params)
|
134 |
-
self.
|
135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
self.param["network"].objects = networks
|
137 |
|
|
|
|
|
|
|
|
|
|
|
|
|
138 |
network_select = pn.widgets.AutocompleteInput.from_param(
|
139 |
self.param.network, min_characters=0, case_sensitive=False
|
140 |
)
|
@@ -144,62 +203,76 @@ class ClimateApp(pn.viewable.Viewer):
|
|
144 |
var_select = pn.widgets.Select.from_param(self.param.var, options=VAR_OPTIONS)
|
145 |
year_slider = pn.widgets.IntSlider.from_param(self.param.year)
|
146 |
year_range_slider = pn.widgets.RangeSlider.from_param(self.param.year_range)
|
147 |
-
stat_select = pn.widgets.RadioButtonGroup.from_param(
|
148 |
-
|
|
|
|
|
|
|
|
|
149 |
network_select,
|
150 |
station_select,
|
151 |
var_select,
|
152 |
year_slider,
|
153 |
year_range_slider,
|
154 |
stat_select,
|
|
|
155 |
]
|
156 |
-
# network_points = self._networks_df.hvplot.points(
|
157 |
-
# "lon",
|
158 |
-
# "lat",
|
159 |
-
# legend=False,
|
160 |
-
# cmap="category10",
|
161 |
-
# color="iem_network",
|
162 |
-
# hover_cols=["stid", "station_name", "iem_network"],
|
163 |
-
# size=10,
|
164 |
-
# geo=True,
|
165 |
-
# ).opts(
|
166 |
-
# "Points",
|
167 |
-
# fill_alpha=0,
|
168 |
-
# responsive=True,
|
169 |
-
# tools=["tap", "hover"],
|
170 |
-
# active_tools=["wheel_zoom"],
|
171 |
-
# )
|
172 |
-
# tap = Tap(source=network_points)
|
173 |
-
# pn.bind(self._update_station, x=tap.param.x, y=tap.param.y, watch=True)
|
174 |
-
# network_pane = pn.pane.HoloViews(
|
175 |
-
# network_points * gv.tile_sources.CartoDark(),
|
176 |
-
# sizing_mode="stretch_both",
|
177 |
-
# max_height=625,
|
178 |
-
# )
|
179 |
-
self._station_pane = pn.pane.HoloViews(sizing_mode="stretch_width", min_width=800, height=450)
|
180 |
-
# main_tabs = pn.Tabs(
|
181 |
-
# ("Climatology Plot", self._station_pane), ("Map Select", network_pane)
|
182 |
-
# )
|
183 |
-
self._main = [self._station_pane]
|
184 |
|
185 |
-
|
|
|
|
|
|
|
186 |
self._update_stations()
|
|
|
187 |
self._update_station_pane()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
188 |
|
189 |
@pn.cache
|
190 |
-
def
|
191 |
-
|
192 |
-
return
|
193 |
|
194 |
@pn.depends("network", watch=True)
|
195 |
def _update_stations(self):
|
196 |
-
network_df_subset = self.
|
197 |
-
self.
|
198 |
["stid", "station_name"],
|
199 |
]
|
200 |
names = sorted(network_df_subset["station_name"].unique())
|
201 |
stids = sorted(network_df_subset["stid"].unique())
|
202 |
self.param["station"].objects = names + stids
|
|
|
203 |
|
204 |
def _update_station(self, x, y):
|
205 |
if x is None or y is None:
|
@@ -219,21 +292,21 @@ class ClimateApp(pn.viewable.Viewer):
|
|
219 |
return R * c
|
220 |
|
221 |
distances = haversine_vectorized(
|
222 |
-
self.
|
223 |
)
|
224 |
|
225 |
min_distance_index = np.argmin(distances)
|
226 |
|
227 |
-
closest_row = self.
|
228 |
with param.parameterized.batch_call_watchers(self):
|
229 |
self.network = closest_row["iem_network"]
|
230 |
self.station = closest_row["stid"]
|
231 |
|
232 |
@pn.cache
|
233 |
def _get_station_df(self, station, var):
|
234 |
-
if station in self.
|
235 |
-
station = self.
|
236 |
-
self.
|
237 |
].iloc[0]
|
238 |
if station.startswith("K"):
|
239 |
station = station.lstrip("K")
|
@@ -288,27 +361,15 @@ class ClimateApp(pn.viewable.Viewer):
|
|
288 |
|
289 |
try:
|
290 |
self._station_pane.loading = True
|
291 |
-
df = self._station_df
|
292 |
-
if self.station not in self._networks_df["station_name"].unique():
|
293 |
-
station_name = self._networks_df.loc[
|
294 |
-
self._networks_df["stid"] == self.station, "station_name"
|
295 |
-
].iloc[0]
|
296 |
-
else:
|
297 |
-
station_name = self.station
|
298 |
|
299 |
-
#
|
|
|
300 |
df_avg = (
|
301 |
df.loc[df["year"].between(*self.year_range)].groupby("dayofyear").mean()
|
302 |
)
|
303 |
df_year = df[df.year == self.year]
|
304 |
-
if self.stat == "Mean":
|
305 |
-
df_year_avg = df_year[self.var].mean()
|
306 |
-
else:
|
307 |
-
df_year_avg = df_year[self.var].median()
|
308 |
-
df_year_max = df_year[self.var].max()
|
309 |
-
df_year_min = df_year[self.var].min()
|
310 |
|
311 |
-
#
|
312 |
df_above = df_year[["dayofyear", self.var]].merge(
|
313 |
df_avg.reset_index()[["dayofyear", self.var]],
|
314 |
on="dayofyear",
|
@@ -331,147 +392,157 @@ class ClimateApp(pn.viewable.Viewer):
|
|
331 |
f"{self.var}_year",
|
332 |
]
|
333 |
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
].
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
alpha=0.02,
|
352 |
-
hover=False,
|
353 |
-
**plot_kwargs,
|
354 |
-
)
|
355 |
-
plot_year = (
|
356 |
-
df_year.hvplot(color="black", hover="vline", **plot_kwargs)
|
357 |
-
.opts(alpha=0.2)
|
358 |
-
.redim.label(**{"dayofyear": "Julian Day", self.var: str(self.year)})
|
359 |
-
)
|
360 |
-
plot_avg = df_avg.hvplot(color="grey", **plot_kwargs).redim.label(
|
361 |
-
**{"dayofyear": "Julian Day", self.var: "Average"}
|
362 |
-
)
|
363 |
-
|
364 |
-
plot_year_avg = hv.HLine(df_year_avg).opts(
|
365 |
-
line_color="lightgrey", line_dash="dashed", line_width=0.5
|
366 |
-
)
|
367 |
-
plot_year_max = hv.HLine(df_year_max).opts(
|
368 |
-
line_color=DARK_RED, line_dash="dashed", line_width=0.5
|
369 |
-
)
|
370 |
-
plot_year_min = hv.HLine(df_year_min).opts(
|
371 |
-
line_color=DARK_BLUE, line_dash="dashed", line_width=0.5
|
372 |
-
)
|
373 |
-
|
374 |
-
text_year_opts = {
|
375 |
-
"text_align": "right",
|
376 |
-
"text_baseline": "bottom",
|
377 |
-
"text_alpha": 0.8,
|
378 |
-
}
|
379 |
-
text_year_label = "AVERAGE" if self.stat == "Mean" else "MEDIAN"
|
380 |
-
text_year_avg = hv.Text(
|
381 |
-
360, df_year_avg + 3, f"{text_year_label} {df_year_avg:.0f}", fontsize=8
|
382 |
-
).opts(
|
383 |
-
text_color="lightgrey",
|
384 |
-
**text_year_opts,
|
385 |
-
)
|
386 |
-
text_year_max = hv.Text(
|
387 |
-
360, df_year_max + 3, f"MAX {df_year_max:.0f}", fontsize=8
|
388 |
-
).opts(
|
389 |
-
text_color=DARK_RED,
|
390 |
-
**text_year_opts,
|
391 |
-
)
|
392 |
-
text_year_min = hv.Text(
|
393 |
-
360, df_year_min + 3, f"MIN {df_year_min:.0f}", fontsize=8
|
394 |
-
).opts(
|
395 |
-
text_color=DARK_BLUE,
|
396 |
-
**text_year_opts,
|
397 |
-
)
|
398 |
-
|
399 |
-
area_kwargs = {"fill_alpha": 0.2, "line_alpha": 0.8}
|
400 |
-
plot_above = df_above.hvplot.area(
|
401 |
-
x="dayofyear", y=f"{self.var}_avg", y2=self.var, hover=False
|
402 |
-
).opts(line_color=DARK_RED, fill_color=DARK_RED, **area_kwargs)
|
403 |
-
plot_below = df_below.hvplot.area(
|
404 |
-
x="dayofyear", y=f"{self.var}_avg", y2=self.var, hover=False
|
405 |
-
).opts(line_color=DARK_BLUE, fill_color=DARK_BLUE, **area_kwargs)
|
406 |
-
|
407 |
-
text_x = 25
|
408 |
-
text_y = df_year[self.var].max() + 10
|
409 |
-
text_days_above = hv.Text(text_x, text_y, f"{days_above}", fontsize=14).opts(
|
410 |
-
text_align="right",
|
411 |
-
text_baseline="bottom",
|
412 |
-
text_color=DARK_RED,
|
413 |
-
text_alpha=0.8,
|
414 |
-
)
|
415 |
-
text_days_below = hv.Text(text_x, text_y, f"{days_below}", fontsize=14).opts(
|
416 |
-
text_align="right",
|
417 |
-
text_baseline="top",
|
418 |
-
text_color=DARK_BLUE,
|
419 |
-
text_alpha=0.8,
|
420 |
-
)
|
421 |
-
text_above = hv.Text(text_x + 3, text_y, "DAYS ABOVE", fontsize=7).opts(
|
422 |
-
text_align="left",
|
423 |
-
text_baseline="bottom",
|
424 |
-
text_color="lightgrey",
|
425 |
-
text_alpha=0.8,
|
426 |
-
)
|
427 |
-
text_below = hv.Text(text_x + 3, text_y, "DAYS BELOW", fontsize=7).opts(
|
428 |
-
text_align="left",
|
429 |
-
text_baseline="top",
|
430 |
-
text_color="lightgrey",
|
431 |
-
text_alpha=0.8,
|
432 |
-
)
|
433 |
-
|
434 |
-
# overlay everything and save
|
435 |
-
station_overlay = (
|
436 |
-
plot
|
437 |
-
* plot_year
|
438 |
-
* plot_avg
|
439 |
-
* plot_year_avg
|
440 |
-
* plot_year_max
|
441 |
-
* plot_year_min
|
442 |
-
* text_year_avg
|
443 |
-
* text_year_max
|
444 |
-
* text_year_min
|
445 |
-
* plot_above
|
446 |
-
* plot_below
|
447 |
-
* text_days_above
|
448 |
-
* text_days_below
|
449 |
-
* text_above
|
450 |
-
* text_below
|
451 |
-
).opts(
|
452 |
xlabel="TIME OF YEAR",
|
453 |
ylabel=VAR_OPTIONS_R[self.var],
|
454 |
-
title=
|
455 |
gridstyle={"ygrid_line_alpha": 0},
|
456 |
xticks=XTICKS,
|
457 |
show_grid=True,
|
458 |
fontscale=1.18,
|
459 |
-
padding=(0, (0, 0.3))
|
460 |
)
|
461 |
self._station_pane.object = station_overlay
|
462 |
finally:
|
463 |
self._station_pane.loading = False
|
464 |
|
465 |
-
def
|
466 |
-
|
467 |
-
|
468 |
-
|
469 |
-
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
474 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
475 |
|
476 |
|
477 |
-
ClimateApp().servable()
|
|
|
1 |
+
from datetime import datetime
|
2 |
+
|
3 |
import param
|
4 |
import panel as pn
|
5 |
import numpy as np
|
|
|
7 |
import hvplot.pandas
|
8 |
import geoviews as gv
|
9 |
import holoviews as hv
|
10 |
+
from holoviews.streams import Tap, PointerX
|
11 |
from bokeh.themes import Theme
|
12 |
|
13 |
+
pn.extension(throttled=True, notifications=True)
|
14 |
+
|
15 |
VAR_OPTIONS = {
|
16 |
"Maximum Air Temperature [F]": "max_temp_f",
|
17 |
"Minimum Air Temperature [F]": "min_temp_f",
|
|
|
35 |
"Maximum wind gust [knots]": "max_wind_gust_kts",
|
36 |
"Daily Solar Radiation MJ/m2": "srad_mj",
|
37 |
}
|
38 |
+
WELCOME_MESSAGE = """
|
39 |
+
### Welcome to the Climaviewer!
|
40 |
+
|
41 |
+
This app allows you to compare a single year of data from a weather station to the average of a range of years.
|
42 |
+
|
43 |
+
1. Select a network station from the dropdowns; alternatively you can click on the map to select the nearest station.
|
44 |
+
2. Choose the desired variable and year to plot, and change the average range if desired.
|
45 |
+
3. Hover over the plot to see the value for each day; use the mouse wheel to zoom in/out.
|
46 |
+
"""
|
47 |
+
|
48 |
+
FOOTER_MESSAGE = """
|
49 |
+
Made entirely with OSS packages: [Panel](https://panel.holoviz.org/), [Holoviews](https://holoviews.org/), [GeoViews](https://geoviews.org/), [Bokeh](https://bokeh.org/), [Pandas](https://pandas.pydata.org/), [Numpy](https://numpy.org/)
|
50 |
+
|
51 |
+
Data sourced from the [Iowa Environmental Mesonet](https://mesonet.agron.iastate.edu/).
|
52 |
+
"""
|
53 |
+
|
54 |
VAR_OPTIONS_R = {v: k for k, v in VAR_OPTIONS.items()}
|
55 |
NETWORKS_URL = "https://mesonet.agron.iastate.edu/sites/networks.php?network=_ALL_&format=csv&nohtml=on"
|
56 |
STATION_URL_FMT = (
|
57 |
"https://mesonet.agron.iastate.edu/cgi-bin/request/daily.py?network={network}&stations={station}"
|
58 |
+
"&year1=1928&month1=1&day1=1&year2=2024&month2=12&day2=31&var={var}&na=blank&format=csv"
|
59 |
)
|
60 |
DARK_RED = "#FF5555"
|
61 |
DARK_BLUE = "#5588FF"
|
|
|
137 |
}
|
138 |
}
|
139 |
theme = Theme(json=THEME_JSON)
|
140 |
+
this_year = datetime.now().year
|
141 |
|
142 |
hv.renderer("bokeh").theme = theme
|
143 |
+
|
144 |
|
145 |
class ClimateApp(pn.viewable.Viewer):
|
146 |
+
network = param.Selector(default="WA_ASOS", label="Network (type to search)")
|
147 |
+
station = param.Selector(default="SEA", label="Station (type to search)")
|
148 |
+
year = param.Integer(default=this_year, bounds=(1928, this_year))
|
149 |
+
year_range = param.Range(
|
150 |
+
default=(1990, 2020), bounds=(1928, this_year), label="Average Range"
|
151 |
+
)
|
152 |
var = param.Selector(default="max_temp_f", objects=sorted(VAR_OPTIONS.values()))
|
153 |
stat = param.Selector(default="Mean", objects=["Mean", "Median"])
|
154 |
|
155 |
def __init__(self, **params):
|
156 |
super().__init__(**params)
|
157 |
+
self._sidebar = pn.Column(sizing_mode="stretch_both", loading=True)
|
158 |
+
self._main = pn.Column(
|
159 |
+
pn.indicators.LoadingSpinner(
|
160 |
+
value=True, width=25, height=25, name="Loading, please wait a moment..."
|
161 |
+
),
|
162 |
+
sizing_mode="stretch_both",
|
163 |
+
)
|
164 |
+
self._modal = pn.Column(width=850, height=500, align="center")
|
165 |
+
pn.state.onload(self._onload)
|
166 |
+
|
167 |
+
self._template = pn.template.FastListTemplate(
|
168 |
+
sidebar=[self._sidebar],
|
169 |
+
main=[self._main],
|
170 |
+
modal=[self._modal],
|
171 |
+
theme="dark",
|
172 |
+
theme_toggle=False,
|
173 |
+
main_layout=None,
|
174 |
+
title="Select Year vs Average Comparison",
|
175 |
+
accent="grey",
|
176 |
+
)
|
177 |
+
|
178 |
+
def _onload(self):
|
179 |
+
try:
|
180 |
+
self._populate_sidebar()
|
181 |
+
self._populate_main()
|
182 |
+
self._populate_modal()
|
183 |
+
finally:
|
184 |
+
self._sidebar.loading = False
|
185 |
+
|
186 |
+
def _populate_sidebar(self):
|
187 |
+
self._network_df = self._get_network_df()
|
188 |
+
networks = sorted(self._network_df["iem_network"].unique())
|
189 |
self.param["network"].objects = networks
|
190 |
|
191 |
+
open_button = pn.widgets.Button(
|
192 |
+
name="Select station from map",
|
193 |
+
button_type="primary",
|
194 |
+
sizing_mode="stretch_width",
|
195 |
+
)
|
196 |
+
open_button.on_click(self._open_modal)
|
197 |
network_select = pn.widgets.AutocompleteInput.from_param(
|
198 |
self.param.network, min_characters=0, case_sensitive=False
|
199 |
)
|
|
|
203 |
var_select = pn.widgets.Select.from_param(self.param.var, options=VAR_OPTIONS)
|
204 |
year_slider = pn.widgets.IntSlider.from_param(self.param.year)
|
205 |
year_range_slider = pn.widgets.RangeSlider.from_param(self.param.year_range)
|
206 |
+
stat_select = pn.widgets.RadioButtonGroup.from_param(
|
207 |
+
self.param.stat, sizing_mode="stretch_width"
|
208 |
+
)
|
209 |
+
self._sidebar.objects = [
|
210 |
+
WELCOME_MESSAGE,
|
211 |
+
open_button,
|
212 |
network_select,
|
213 |
station_select,
|
214 |
var_select,
|
215 |
year_slider,
|
216 |
year_range_slider,
|
217 |
stat_select,
|
218 |
+
FOOTER_MESSAGE,
|
219 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
220 |
|
221 |
+
def _populate_main(self):
|
222 |
+
self._station_pane = pn.pane.HoloViews(
|
223 |
+
sizing_mode="stretch_both", min_width=800, min_height=400
|
224 |
+
)
|
225 |
self._update_stations()
|
226 |
+
self._update_var_station_dependents()
|
227 |
self._update_station_pane()
|
228 |
+
self._main.objects = [self._station_pane]
|
229 |
+
|
230 |
+
def _populate_modal(self):
|
231 |
+
network_points = self._network_df.hvplot.points(
|
232 |
+
"lon",
|
233 |
+
"lat",
|
234 |
+
legend=False,
|
235 |
+
cmap="category10",
|
236 |
+
color="iem_network",
|
237 |
+
hover_cols=["stid", "station_name", "iem_network"],
|
238 |
+
size=10,
|
239 |
+
geo=True,
|
240 |
+
responsive=True,
|
241 |
+
xlabel="Longitude",
|
242 |
+
ylabel="Latitude",
|
243 |
+
).opts(
|
244 |
+
"Points",
|
245 |
+
fill_alpha=0,
|
246 |
+
tools=["tap", "hover"],
|
247 |
+
active_tools=["wheel_zoom"],
|
248 |
+
)
|
249 |
+
|
250 |
+
tap = Tap(source=network_points)
|
251 |
+
pn.bind(self._update_station, x=tap.param.x, y=tap.param.y, watch=True)
|
252 |
+
instructions = "#### The nearest station will be selected when you click on the map."
|
253 |
+
network_pane = pn.pane.HoloViews(
|
254 |
+
network_points * gv.tile_sources.CartoDark(),
|
255 |
+
)
|
256 |
+
self._modal.objects = [instructions, network_pane]
|
257 |
+
|
258 |
+
def _open_modal(self, event):
|
259 |
+
self._template.open_modal()
|
260 |
|
261 |
@pn.cache
|
262 |
+
def _get_network_df(self):
|
263 |
+
network_df = pd.read_csv(NETWORKS_URL)
|
264 |
+
return network_df.loc[network_df["iem_network"].str.contains("ASOS")]
|
265 |
|
266 |
@pn.depends("network", watch=True)
|
267 |
def _update_stations(self):
|
268 |
+
network_df_subset = self._network_df.loc[
|
269 |
+
self._network_df["iem_network"] == self.network,
|
270 |
["stid", "station_name"],
|
271 |
]
|
272 |
names = sorted(network_df_subset["station_name"].unique())
|
273 |
stids = sorted(network_df_subset["stid"].unique())
|
274 |
self.param["station"].objects = names + stids
|
275 |
+
self._template.close_modal()
|
276 |
|
277 |
def _update_station(self, x, y):
|
278 |
if x is None or y is None:
|
|
|
292 |
return R * c
|
293 |
|
294 |
distances = haversine_vectorized(
|
295 |
+
self._network_df["lon"].values, self._network_df["lat"].values, x, y
|
296 |
)
|
297 |
|
298 |
min_distance_index = np.argmin(distances)
|
299 |
|
300 |
+
closest_row = self._network_df.iloc[min_distance_index]
|
301 |
with param.parameterized.batch_call_watchers(self):
|
302 |
self.network = closest_row["iem_network"]
|
303 |
self.station = closest_row["stid"]
|
304 |
|
305 |
@pn.cache
|
306 |
def _get_station_df(self, station, var):
|
307 |
+
if station in self._network_df["station_name"].unique():
|
308 |
+
station = self._network_df.loc[
|
309 |
+
self._network_df["station_name"] == station, "stid"
|
310 |
].iloc[0]
|
311 |
if station.startswith("K"):
|
312 |
station = station.lstrip("K")
|
|
|
361 |
|
362 |
try:
|
363 |
self._station_pane.loading = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
364 |
|
365 |
+
# base dataframes
|
366 |
+
df = self._station_df
|
367 |
df_avg = (
|
368 |
df.loc[df["year"].between(*self.year_range)].groupby("dayofyear").mean()
|
369 |
)
|
370 |
df_year = df[df.year == self.year]
|
|
|
|
|
|
|
|
|
|
|
|
|
371 |
|
372 |
+
# above/below
|
373 |
df_above = df_year[["dayofyear", self.var]].merge(
|
374 |
df_avg.reset_index()[["dayofyear", self.var]],
|
375 |
on="dayofyear",
|
|
|
392 |
f"{self.var}_year",
|
393 |
]
|
394 |
|
395 |
+
# stats
|
396 |
+
if self.stat == "Mean":
|
397 |
+
year_avg = df_year[self.var].mean()
|
398 |
+
else:
|
399 |
+
year_avg = df_year[self.var].median()
|
400 |
+
year_max = df_year[self.var].max()
|
401 |
+
year_min = df_year[self.var].min()
|
402 |
+
|
403 |
+
plots = self._create_line_plots(df, df_year, df_avg)
|
404 |
+
lines = self._create_hlines(year_avg, year_max, year_min)
|
405 |
+
texts = self._create_text_labels(year_avg, year_max, year_min)
|
406 |
+
areas = self._create_areas(df_above, df_below)
|
407 |
+
text_days = self._create_text_days_labels(df, df_above, df_below)
|
408 |
+
|
409 |
+
# Overlay all elements
|
410 |
+
title = f"{self._get_station_name()} {self.year} vs AVERAGE ({self.year_range[0]}-{self.year_range[1]})"
|
411 |
+
station_overlay = (plots * lines * texts * areas * text_days).opts(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
412 |
xlabel="TIME OF YEAR",
|
413 |
ylabel=VAR_OPTIONS_R[self.var],
|
414 |
+
title=title,
|
415 |
gridstyle={"ygrid_line_alpha": 0},
|
416 |
xticks=XTICKS,
|
417 |
show_grid=True,
|
418 |
fontscale=1.18,
|
419 |
+
padding=(0, (0, 0.3)),
|
420 |
)
|
421 |
self._station_pane.object = station_overlay
|
422 |
finally:
|
423 |
self._station_pane.loading = False
|
424 |
|
425 |
+
def _create_line_plots(self, df, df_year, df_avg):
|
426 |
+
plot_kwargs = {
|
427 |
+
"x": "dayofyear",
|
428 |
+
"y": self.var,
|
429 |
+
"legend": False,
|
430 |
+
}
|
431 |
+
plot = df.hvplot(
|
432 |
+
by="year",
|
433 |
+
color="grey",
|
434 |
+
alpha=0.02,
|
435 |
+
hover=False,
|
436 |
+
**plot_kwargs,
|
437 |
+
)
|
438 |
+
plot_year = (
|
439 |
+
df_year.hvplot(color="black", hover="vline", **plot_kwargs)
|
440 |
+
.opts(alpha=0.2)
|
441 |
+
.redim.label(**{"dayofyear": "Julian Day", self.var: str(self.year)})
|
442 |
+
)
|
443 |
+
plot_avg = df_avg.hvplot(
|
444 |
+
color="grey", hover="vline", **plot_kwargs
|
445 |
+
).redim.label(**{"dayofyear": "Julian Day", self.var: "Average"})
|
446 |
+
return plot * plot_year * plot_avg
|
447 |
+
|
448 |
+
def _create_hlines(self, year_avg, year_max, year_min):
|
449 |
+
# Create horizontal lines
|
450 |
+
plot_year_avg = hv.HLine(year_avg).opts(
|
451 |
+
line_color="lightgrey", line_dash="dashed", line_width=0.5
|
452 |
+
)
|
453 |
+
plot_year_max = hv.HLine(year_max).opts(
|
454 |
+
line_color=DARK_RED, line_dash="dashed", line_width=0.5
|
455 |
+
)
|
456 |
+
plot_year_min = hv.HLine(year_min).opts(
|
457 |
+
line_color=DARK_BLUE, line_dash="dashed", line_width=0.5
|
458 |
+
)
|
459 |
+
return plot_year_avg * plot_year_max * plot_year_min
|
460 |
+
|
461 |
+
def _create_text_labels(self, year_avg, year_max, year_min):
|
462 |
+
text_year_opts = {
|
463 |
+
"text_align": "right",
|
464 |
+
"text_baseline": "bottom",
|
465 |
+
"text_alpha": 0.8,
|
466 |
+
}
|
467 |
+
text_year_label = "AVERAGE" if self.stat == "Mean" else "MEDIAN"
|
468 |
+
text_year_avg = hv.Text(
|
469 |
+
360, year_avg + 3, f"{text_year_label} {year_avg:.1f}", fontsize=8
|
470 |
+
).opts(
|
471 |
+
text_color="lightgrey",
|
472 |
+
**text_year_opts,
|
473 |
+
)
|
474 |
+
text_year_max = hv.Text(
|
475 |
+
360, year_max + 3, f"MAX {year_max:.1f}", fontsize=8
|
476 |
+
).opts(
|
477 |
+
text_color=DARK_RED,
|
478 |
+
**text_year_opts,
|
479 |
+
)
|
480 |
+
text_year_min = hv.Text(
|
481 |
+
360, year_min + 3, f"MIN {year_min:.1f}", fontsize=8
|
482 |
+
).opts(
|
483 |
+
text_color=DARK_BLUE,
|
484 |
+
**text_year_opts,
|
485 |
)
|
486 |
+
return text_year_avg * text_year_max * text_year_min
|
487 |
+
|
488 |
+
def _create_areas(self, df_above, df_below):
|
489 |
+
area_kwargs = {"fill_alpha": 0.2, "line_alpha": 0.8}
|
490 |
+
plot_above = df_above.hvplot.area(
|
491 |
+
x="dayofyear", y=f"{self.var}_avg", y2=self.var, hover=False
|
492 |
+
).opts(line_color=DARK_RED, fill_color=DARK_RED, **area_kwargs)
|
493 |
+
plot_below = df_below.hvplot.area(
|
494 |
+
x="dayofyear", y=f"{self.var}_avg", y2=self.var, hover=False
|
495 |
+
).opts(line_color=DARK_BLUE, fill_color=DARK_BLUE, **area_kwargs)
|
496 |
+
return plot_above * plot_below
|
497 |
+
|
498 |
+
def _create_text_days_labels(self, df, df_above, df_below):
|
499 |
+
days_above = df_above.loc[
|
500 |
+
df_above[f"{self.var}_year"] >= df_above[f"{self.var}_avg"]
|
501 |
+
].shape[0]
|
502 |
+
days_below = df_below.loc[
|
503 |
+
df_below[f"{self.var}_year"] < df_below[f"{self.var}_avg"]
|
504 |
+
].shape[0]
|
505 |
+
|
506 |
+
text_x = 30
|
507 |
+
text_y = df[self.var].max() + 10
|
508 |
+
text_days_above = hv.Text(text_x, text_y, f"{days_above}", fontsize=14).opts(
|
509 |
+
text_align="right",
|
510 |
+
text_baseline="bottom",
|
511 |
+
text_color=DARK_RED,
|
512 |
+
text_alpha=0.8,
|
513 |
+
)
|
514 |
+
text_days_below = hv.Text(text_x, text_y, f"{days_below}", fontsize=14).opts(
|
515 |
+
text_align="right",
|
516 |
+
text_baseline="top",
|
517 |
+
text_color=DARK_BLUE,
|
518 |
+
text_alpha=0.8,
|
519 |
+
)
|
520 |
+
text_above = hv.Text(text_x + 3, text_y, "DAYS ABOVE", fontsize=7).opts(
|
521 |
+
text_align="left",
|
522 |
+
text_baseline="bottom",
|
523 |
+
text_color="lightgrey",
|
524 |
+
text_alpha=0.8,
|
525 |
+
)
|
526 |
+
text_below = hv.Text(text_x + 3, text_y, "DAYS BELOW", fontsize=7).opts(
|
527 |
+
text_align="left",
|
528 |
+
text_baseline="top",
|
529 |
+
text_color="lightgrey",
|
530 |
+
text_alpha=0.8,
|
531 |
+
)
|
532 |
+
|
533 |
+
return text_days_above * text_days_below * text_above * text_below
|
534 |
+
|
535 |
+
def _get_station_name(self):
|
536 |
+
if self.station not in self._network_df["station_name"].unique():
|
537 |
+
station_name = self._network_df.loc[
|
538 |
+
self._network_df["stid"] == self.station, "station_name"
|
539 |
+
].iloc[0]
|
540 |
+
else:
|
541 |
+
station_name = self.station
|
542 |
+
return station_name
|
543 |
+
|
544 |
+
def __panel__(self):
|
545 |
+
return self._template
|
546 |
|
547 |
|
548 |
+
ClimateApp().servable()
|