Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -7,7 +7,7 @@ import pandas as pd
|
|
7 |
import hvplot.pandas
|
8 |
import geoviews as gv
|
9 |
import holoviews as hv
|
10 |
-
from holoviews.streams import Tap
|
11 |
from bokeh.themes import Theme
|
12 |
|
13 |
pn.extension(throttled=True, notifications=True)
|
@@ -52,6 +52,7 @@ Data sourced from the [Iowa Environmental Mesonet](https://mesonet.agron.iastate
|
|
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}"
|
@@ -59,6 +60,20 @@ STATION_URL_FMT = (
|
|
59 |
)
|
60 |
DARK_RED = "#FF5555"
|
61 |
DARK_BLUE = "#5588FF"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
XTICKS = [
|
63 |
(1, "JAN"),
|
64 |
(31, "FEB"),
|
@@ -73,6 +88,12 @@ XTICKS = [
|
|
73 |
(304, "NOV"),
|
74 |
(334, "DEC"),
|
75 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
76 |
|
77 |
THEME_JSON = {
|
78 |
"attrs": {
|
@@ -143,18 +164,23 @@ hv.renderer("bokeh").theme = theme
|
|
143 |
|
144 |
|
145 |
class ClimateApp(pn.viewable.Viewer):
|
146 |
-
network = param.Selector(
|
147 |
-
|
148 |
-
|
|
|
|
|
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"
|
158 |
self._main = pn.Column(
|
159 |
pn.indicators.LoadingSpinner(
|
160 |
value=True, width=25, height=25, name="Loading, please wait a moment..."
|
@@ -162,8 +188,6 @@ class ClimateApp(pn.viewable.Viewer):
|
|
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],
|
@@ -171,12 +195,14 @@ class ClimateApp(pn.viewable.Viewer):
|
|
171 |
theme="dark",
|
172 |
theme_toggle=False,
|
173 |
main_layout=None,
|
174 |
-
title="
|
175 |
accent="grey",
|
176 |
)
|
|
|
177 |
|
178 |
def _onload(self):
|
179 |
try:
|
|
|
180 |
self._populate_sidebar()
|
181 |
self._populate_main()
|
182 |
self._populate_modal()
|
@@ -185,6 +211,7 @@ class ClimateApp(pn.viewable.Viewer):
|
|
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 |
|
@@ -207,7 +234,7 @@ class ClimateApp(pn.viewable.Viewer):
|
|
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,
|
@@ -215,17 +242,63 @@ class ClimateApp(pn.viewable.Viewer):
|
|
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",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
224 |
)
|
225 |
self._update_stations()
|
226 |
self._update_var_station_dependents()
|
227 |
-
self.
|
228 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
229 |
|
230 |
def _populate_modal(self):
|
231 |
network_points = self._network_df.hvplot.points(
|
@@ -249,7 +322,9 @@ class ClimateApp(pn.viewable.Viewer):
|
|
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 =
|
|
|
|
|
253 |
network_pane = pn.pane.HoloViews(
|
254 |
network_points * gv.tile_sources.CartoDark(),
|
255 |
)
|
@@ -258,6 +333,15 @@ class ClimateApp(pn.viewable.Viewer):
|
|
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)
|
@@ -265,6 +349,7 @@ class ClimateApp(pn.viewable.Viewer):
|
|
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"],
|
@@ -272,7 +357,6 @@ class ClimateApp(pn.viewable.Viewer):
|
|
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:
|
@@ -332,101 +416,181 @@ class ClimateApp(pn.viewable.Viewer):
|
|
332 |
@pn.depends("var", "station", watch=True)
|
333 |
def _update_var_station_dependents(self):
|
334 |
try:
|
335 |
-
self.
|
336 |
self._station_df = self._get_station_df(self.station, self.var).dropna()
|
337 |
if len(self._station_df) == 0:
|
338 |
return
|
339 |
|
340 |
year_range_min = self._station_df["year"].min()
|
341 |
year_range_max = self._station_df["year"].max()
|
342 |
-
if self.year_range[0] < year_range_min:
|
343 |
-
self.year_range = (year_range_min, self.year_range[1])
|
344 |
-
if self.year_range[1] > year_range_max:
|
345 |
-
self.year_range = (self.year_range[0], year_range_max)
|
346 |
-
|
347 |
-
self.param["year_range"].bounds = (year_range_min, year_range_max)
|
348 |
-
|
349 |
self.param["year"].bounds = (year_range_min, year_range_max)
|
350 |
if self.year < year_range_min:
|
351 |
self.year = year_range_min
|
352 |
if self.year > year_range_max:
|
353 |
self.year = year_range_max
|
354 |
finally:
|
355 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
356 |
|
357 |
-
|
358 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
359 |
if len(self._station_df) == 0:
|
360 |
return
|
361 |
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
371 |
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
382 |
-
|
383 |
-
|
384 |
-
|
385 |
-
|
386 |
-
|
387 |
-
|
388 |
-
|
389 |
-
|
390 |
-
|
391 |
-
|
392 |
-
|
393 |
-
|
394 |
-
|
395 |
-
|
396 |
-
|
397 |
-
|
398 |
-
|
399 |
-
|
400 |
-
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
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",
|
@@ -435,15 +599,23 @@ class ClimateApp(pn.viewable.Viewer):
|
|
435 |
hover=False,
|
436 |
**plot_kwargs,
|
437 |
)
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
443 |
plot_avg = df_avg.hvplot(
|
444 |
-
color="
|
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
|
@@ -486,25 +658,36 @@ class ClimateApp(pn.viewable.Viewer):
|
|
486 |
return text_year_avg * text_year_max * text_year_min
|
487 |
|
488 |
def _create_areas(self, df_above, df_below):
|
489 |
-
area_kwargs = {
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
496 |
return plot_above * plot_below
|
497 |
|
498 |
-
def _create_text_days_labels(
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
days_below
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
|
|
|
|
|
|
|
|
508 |
text_days_above = hv.Text(text_x, text_y, f"{days_above}", fontsize=14).opts(
|
509 |
text_align="right",
|
510 |
text_baseline="bottom",
|
@@ -517,13 +700,17 @@ class ClimateApp(pn.viewable.Viewer):
|
|
517 |
text_color=DARK_BLUE,
|
518 |
text_alpha=0.8,
|
519 |
)
|
520 |
-
text_above = hv.Text(
|
|
|
|
|
521 |
text_align="left",
|
522 |
text_baseline="bottom",
|
523 |
text_color="lightgrey",
|
524 |
text_alpha=0.8,
|
525 |
)
|
526 |
-
text_below = hv.Text(
|
|
|
|
|
527 |
text_align="left",
|
528 |
text_baseline="top",
|
529 |
text_color="lightgrey",
|
@@ -532,14 +719,19 @@ class ClimateApp(pn.viewable.Viewer):
|
|
532 |
|
533 |
return text_days_above * text_days_below * text_above * text_below
|
534 |
|
535 |
-
def
|
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 |
-
|
|
|
543 |
|
544 |
def __panel__(self):
|
545 |
return self._template
|
|
|
7 |
import hvplot.pandas
|
8 |
import geoviews as gv
|
9 |
import holoviews as hv
|
10 |
+
from holoviews.streams import Tap
|
11 |
from bokeh.themes import Theme
|
12 |
|
13 |
pn.extension(throttled=True, notifications=True)
|
|
|
52 |
"""
|
53 |
|
54 |
VAR_OPTIONS_R = {v: k for k, v in VAR_OPTIONS.items()}
|
55 |
+
ONI_URL = "https://raw.githubusercontent.com/ahuang11/oni/master/oni.csv"
|
56 |
NETWORKS_URL = "https://mesonet.agron.iastate.edu/sites/networks.php?network=_ALL_&format=csv&nohtml=on"
|
57 |
STATION_URL_FMT = (
|
58 |
"https://mesonet.agron.iastate.edu/cgi-bin/request/daily.py?network={network}&stations={station}"
|
|
|
60 |
)
|
61 |
DARK_RED = "#FF5555"
|
62 |
DARK_BLUE = "#5588FF"
|
63 |
+
SEASON_TO_MONTH = {
|
64 |
+
"DJF": "JAN",
|
65 |
+
"JFM": "FEB",
|
66 |
+
"FMA": "MAR",
|
67 |
+
"MAM": "APR",
|
68 |
+
"AMJ": "MAY",
|
69 |
+
"MJJ": "JUN",
|
70 |
+
"JJA": "JUL",
|
71 |
+
"JAS": "AUG",
|
72 |
+
"ASO": "SEP",
|
73 |
+
"SON": "OCT",
|
74 |
+
"OND": "NOV",
|
75 |
+
"NDJ": "DEC",
|
76 |
+
}
|
77 |
XTICKS = [
|
78 |
(1, "JAN"),
|
79 |
(31, "FEB"),
|
|
|
88 |
(304, "NOV"),
|
89 |
(334, "DEC"),
|
90 |
]
|
91 |
+
MONTH_TO_JULIAN_DAY = {month: day for day, month in XTICKS}
|
92 |
+
ONI_COLORS = {
|
93 |
+
"El Nino": DARK_RED,
|
94 |
+
"Neutral": "grey",
|
95 |
+
"La Nina": DARK_BLUE,
|
96 |
+
}
|
97 |
|
98 |
THEME_JSON = {
|
99 |
"attrs": {
|
|
|
164 |
|
165 |
|
166 |
class ClimateApp(pn.viewable.Viewer):
|
167 |
+
network = param.Selector(
|
168 |
+
default="WA_ASOS", label="Network (delete & type to search)"
|
169 |
+
)
|
170 |
+
station = param.Selector(default="SEA", label="Station (delete & type to search)")
|
171 |
+
year = param.Integer(default=this_year - 1, bounds=(1928, this_year))
|
172 |
year_range = param.Range(
|
173 |
default=(1990, 2020), bounds=(1928, this_year), label="Average Range"
|
174 |
)
|
175 |
var = param.Selector(default="max_temp_f", objects=sorted(VAR_OPTIONS.values()))
|
176 |
stat = param.Selector(default="Mean", objects=["Mean", "Median"])
|
177 |
|
178 |
+
_title = param.String()
|
179 |
+
_ylabel = param.String()
|
180 |
+
|
181 |
def __init__(self, **params):
|
182 |
super().__init__(**params)
|
183 |
+
self._sidebar = pn.Column(sizing_mode="stretch_both")
|
184 |
self._main = pn.Column(
|
185 |
pn.indicators.LoadingSpinner(
|
186 |
value=True, width=25, height=25, name="Loading, please wait a moment..."
|
|
|
188 |
sizing_mode="stretch_both",
|
189 |
)
|
190 |
self._modal = pn.Column(width=850, height=500, align="center")
|
|
|
|
|
191 |
self._template = pn.template.FastListTemplate(
|
192 |
sidebar=[self._sidebar],
|
193 |
main=[self._main],
|
|
|
195 |
theme="dark",
|
196 |
theme_toggle=False,
|
197 |
main_layout=None,
|
198 |
+
title="Year vs Climatology",
|
199 |
accent="grey",
|
200 |
)
|
201 |
+
pn.state.onload(self._onload)
|
202 |
|
203 |
def _onload(self):
|
204 |
try:
|
205 |
+
self._sidebar.loading = True
|
206 |
self._populate_sidebar()
|
207 |
self._populate_main()
|
208 |
self._populate_modal()
|
|
|
211 |
|
212 |
def _populate_sidebar(self):
|
213 |
self._network_df = self._get_network_df()
|
214 |
+
self._oni_df = self._get_oni_df()
|
215 |
networks = sorted(self._network_df["iem_network"].unique())
|
216 |
self.param["network"].objects = networks
|
217 |
|
|
|
234 |
self.param.stat, sizing_mode="stretch_width"
|
235 |
)
|
236 |
self._sidebar.objects = [
|
237 |
+
pn.pane.Markdown(WELCOME_MESSAGE),
|
238 |
open_button,
|
239 |
network_select,
|
240 |
station_select,
|
|
|
242 |
year_slider,
|
243 |
year_range_slider,
|
244 |
stat_select,
|
245 |
+
pn.pane.Markdown(FOOTER_MESSAGE),
|
246 |
]
|
247 |
|
248 |
def _populate_main(self):
|
249 |
+
self._tap_x = Tap()
|
250 |
self._station_pane = pn.pane.HoloViews(
|
251 |
+
sizing_mode="stretch_both",
|
252 |
+
min_width=800,
|
253 |
+
min_height=350,
|
254 |
+
)
|
255 |
+
self._day_pane = pn.pane.HoloViews(
|
256 |
+
sizing_mode="stretch_both",
|
257 |
+
min_width=800,
|
258 |
+
min_height=350,
|
259 |
)
|
260 |
self._update_stations()
|
261 |
self._update_var_station_dependents()
|
262 |
+
self._day_pane.object = pn.bind(
|
263 |
+
self._update_day_plot,
|
264 |
+
self.param.var,
|
265 |
+
self.param.station,
|
266 |
+
self.param.year,
|
267 |
+
self.param.year_range,
|
268 |
+
self._tap_x.param.x,
|
269 |
+
)
|
270 |
+
pointer_vline = hv.DynamicMap(self._update_vline, streams=[self._tap_x])
|
271 |
+
oni_plot = hv.DynamicMap(
|
272 |
+
self._update_oni_plot
|
273 |
+
)
|
274 |
+
station_plot = hv.DynamicMap(
|
275 |
+
pn.bind(
|
276 |
+
self._update_station_plot,
|
277 |
+
self.param.var,
|
278 |
+
self.param.station,
|
279 |
+
self.param.year,
|
280 |
+
self.param.year_range,
|
281 |
+
self.param.stat,
|
282 |
+
),
|
283 |
+
)
|
284 |
+
self._station_pane.object = (
|
285 |
+
(oni_plot * station_plot * pointer_vline)
|
286 |
+
.opts(
|
287 |
+
xlabel="Time of Year",
|
288 |
+
gridstyle={"ygrid_line_alpha": 0},
|
289 |
+
xticks=XTICKS,
|
290 |
+
show_grid=True,
|
291 |
+
padding=(0, (0, 0.45)),
|
292 |
+
responsive=True,
|
293 |
+
shared_axes=False,
|
294 |
+
legend_position="top_right"
|
295 |
+
)
|
296 |
+
.apply.opts(title=self.param._title, ylabel=self.param._ylabel)
|
297 |
+
)
|
298 |
+
self._update_ylabel()
|
299 |
+
self._update_title()
|
300 |
+
|
301 |
+
self._main.objects = [self._station_pane, self._day_pane]
|
302 |
|
303 |
def _populate_modal(self):
|
304 |
network_points = self._network_df.hvplot.points(
|
|
|
322 |
|
323 |
tap = Tap(source=network_points)
|
324 |
pn.bind(self._update_station, x=tap.param.x, y=tap.param.y, watch=True)
|
325 |
+
instructions = pn.pane.Markdown(
|
326 |
+
"#### The nearest station will be selected when you click on the map."
|
327 |
+
)
|
328 |
network_pane = pn.pane.HoloViews(
|
329 |
network_points * gv.tile_sources.CartoDark(),
|
330 |
)
|
|
|
333 |
def _open_modal(self, event):
|
334 |
self._template.open_modal()
|
335 |
|
336 |
+
@pn.cache
|
337 |
+
def _get_oni_df(self):
|
338 |
+
df = pd.read_csv(ONI_URL)
|
339 |
+
df["month"] = df["season"].map(SEASON_TO_MONTH)
|
340 |
+
df["julian_day"] = df["month"].map(MONTH_TO_JULIAN_DAY)
|
341 |
+
df["julian_day_end"] = df["julian_day"].shift(-1).fillna(365)
|
342 |
+
df["oni"] = df["oni"].str.replace("_", " ").str.title()
|
343 |
+
return df
|
344 |
+
|
345 |
@pn.cache
|
346 |
def _get_network_df(self):
|
347 |
network_df = pd.read_csv(NETWORKS_URL)
|
|
|
349 |
|
350 |
@pn.depends("network", watch=True)
|
351 |
def _update_stations(self):
|
352 |
+
self._template.close_modal()
|
353 |
network_df_subset = self._network_df.loc[
|
354 |
self._network_df["iem_network"] == self.network,
|
355 |
["stid", "station_name"],
|
|
|
357 |
names = sorted(network_df_subset["station_name"].unique())
|
358 |
stids = sorted(network_df_subset["stid"].unique())
|
359 |
self.param["station"].objects = names + stids
|
|
|
360 |
|
361 |
def _update_station(self, x, y):
|
362 |
if x is None or y is None:
|
|
|
416 |
@pn.depends("var", "station", watch=True)
|
417 |
def _update_var_station_dependents(self):
|
418 |
try:
|
419 |
+
self._main.loading = True
|
420 |
self._station_df = self._get_station_df(self.station, self.var).dropna()
|
421 |
if len(self._station_df) == 0:
|
422 |
return
|
423 |
|
424 |
year_range_min = self._station_df["year"].min()
|
425 |
year_range_max = self._station_df["year"].max()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
426 |
self.param["year"].bounds = (year_range_min, year_range_max)
|
427 |
if self.year < year_range_min:
|
428 |
self.year = year_range_min
|
429 |
if self.year > year_range_max:
|
430 |
self.year = year_range_max
|
431 |
finally:
|
432 |
+
self._main.loading = False
|
433 |
+
|
434 |
+
def _update_vline(self, x, y):
|
435 |
+
if x is None:
|
436 |
+
x = 0
|
437 |
+
if y is None:
|
438 |
+
y = 0
|
439 |
+
vline = hv.VLine(x).opts(line_width=0.9, color="lightgrey")
|
440 |
+
text = hv.Text(
|
441 |
+
x,
|
442 |
+
y,
|
443 |
+
f"Julian Day {int(x)}",
|
444 |
+
).opts(
|
445 |
+
text_color="lightgrey",
|
446 |
+
text_align="left",
|
447 |
+
text_baseline="bottom",
|
448 |
+
text_alpha=0.8,
|
449 |
+
)
|
450 |
+
return vline * text
|
451 |
+
|
452 |
+
@param.depends("year", watch=True)
|
453 |
+
def _update_oni_plot(self):
|
454 |
+
df = self._oni_df
|
455 |
+
df_year = df.loc[df["year"] == 1998]
|
456 |
+
df_year.iloc[-1, -1] = 365
|
457 |
+
|
458 |
+
overlay = hv.Overlay([])
|
459 |
+
for oni in df_year["oni"].unique():
|
460 |
+
df_subset = df_year.loc[df_year["oni"] == oni, ["julian_day", "julian_day_end"]]
|
461 |
+
overlay *= hv.VSpans(
|
462 |
+
df_subset,
|
463 |
+
["julian_day", "julian_day_end"],
|
464 |
+
label=oni,
|
465 |
+
).opts(color=ONI_COLORS[oni], alpha=0.18, line_alpha=0)
|
466 |
+
return overlay
|
467 |
+
|
468 |
+
def _update_day_plot(self, var, station, year, year_range, x):
|
469 |
+
df = self._station_df
|
470 |
+
if not x:
|
471 |
+
x = 1
|
472 |
+
x = int(x)
|
473 |
+
df_day_year = df.query(f"year == {year}")
|
474 |
+
if x > df_day_year["dayofyear"].max():
|
475 |
+
x = df_day_year["dayofyear"].max()
|
476 |
+
day_year = df_day_year.loc[df_day_year["dayofyear"] == x, self.var].iloc[0]
|
477 |
+
df_subset = df.loc[df["year"].between(*year_range)]
|
478 |
+
df_day_climo = df_subset.loc[df_subset["dayofyear"] == x]
|
479 |
+
df_day_climo = df_day_climo.assign(
|
480 |
+
above_or_below=df_day_climo[self.var] >= day_year
|
481 |
+
)
|
482 |
+
title = (
|
483 |
+
f"{VAR_OPTIONS_R[self.var]} across {year_range[0]}-{year_range[1]} on "
|
484 |
+
+ df_day_climo.index.strftime("%B %d")[0]
|
485 |
+
+ f" (Julian Day {x}) "
|
486 |
+
)
|
487 |
|
488 |
+
days_above = df_day_climo.loc[df_day_climo["above_or_below"] == True].shape[0]
|
489 |
+
days_below = df_day_climo.loc[df_day_climo["above_or_below"] == False].shape[0]
|
490 |
+
|
491 |
+
min_x = df[self.var].min()
|
492 |
+
|
493 |
+
plot = hv.Overlay([])
|
494 |
+
plot *= df_day_climo.hvplot.hist(
|
495 |
+
self.var,
|
496 |
+
responsive=True,
|
497 |
+
by="above_or_below",
|
498 |
+
bins=11,
|
499 |
+
legend=False,
|
500 |
+
color=hv.Cycle([DARK_BLUE, DARK_RED]),
|
501 |
+
).opts("Histogram", fill_alpha=0.7, line_alpha=0)
|
502 |
+
plot *= hv.VLine(day_year).opts(line_width=0.9, color="lightgrey")
|
503 |
+
plot *= hv.Text(
|
504 |
+
day_year,
|
505 |
+
0.1,
|
506 |
+
f"{year}",
|
507 |
+
).opts(
|
508 |
+
text_color="lightgrey",
|
509 |
+
text_align="left",
|
510 |
+
text_baseline="bottom",
|
511 |
+
text_alpha=0.8,
|
512 |
+
)
|
513 |
+
plot *= self._create_text_days_labels(
|
514 |
+
df,
|
515 |
+
days_above,
|
516 |
+
days_below,
|
517 |
+
text_x=min_x + 5,
|
518 |
+
text_y=4,
|
519 |
+
spacing=1,
|
520 |
+
suffix=f"YEARS",
|
521 |
+
)
|
522 |
+
|
523 |
+
return plot.opts(
|
524 |
+
xlabel=VAR_OPTIONS_R[self.var],
|
525 |
+
ylabel="Number of Days",
|
526 |
+
title=title,
|
527 |
+
shared_axes=False,
|
528 |
+
show_grid=True,
|
529 |
+
gridstyle={"xgrid_line_alpha": 0},
|
530 |
+
xlim=(min_x, df[self.var].max()),
|
531 |
+
)
|
532 |
+
|
533 |
+
def _update_station_plot(self, var, station, year, year_range, stat):
|
534 |
if len(self._station_df) == 0:
|
535 |
return
|
536 |
|
537 |
+
# base dataframes
|
538 |
+
df = self._station_df
|
539 |
+
df_subset = df.loc[df["year"].between(*year_range)]
|
540 |
+
df_avg = df_subset.groupby("dayofyear").mean()
|
541 |
+
df_year = df[df.year == year]
|
542 |
+
|
543 |
+
# above/below
|
544 |
+
df_year = df_year[["dayofyear", self.var]].merge(
|
545 |
+
df_avg.reset_index()[["dayofyear", self.var]],
|
546 |
+
on="dayofyear",
|
547 |
+
suffixes=("", "_avg"),
|
548 |
+
)
|
549 |
+
df_year["above_or_below"] = df_year[self.var] >= df_year[f"{self.var}_avg"]
|
550 |
+
days_above = df_year.loc[df_year["above_or_below"] == True].shape[0]
|
551 |
+
days_below = df_year.loc[df_year["above_or_below"] == False].shape[0]
|
552 |
|
553 |
+
# stats
|
554 |
+
if stat == "Mean":
|
555 |
+
year_avg = df_year[self.var].mean()
|
556 |
+
else:
|
557 |
+
year_avg = df_year[self.var].median()
|
558 |
+
year_max = df_year[self.var].max()
|
559 |
+
year_min = df_year[self.var].min()
|
560 |
+
|
561 |
+
plots = self._create_line_plots(df, df_year, df_avg)
|
562 |
+
lines = self._create_hlines(year_avg, year_max, year_min)
|
563 |
+
texts = self._create_text_labels(year_avg, year_max, year_min)
|
564 |
+
text_days = self._create_text_days_labels(df, days_above, days_below)
|
565 |
+
|
566 |
+
# Overlay all elements
|
567 |
+
station_overlay = plots * lines * texts * text_days
|
568 |
+
return station_overlay
|
569 |
+
|
570 |
+
@pn.depends("var", watch=True)
|
571 |
+
def _update_ylabel(self):
|
572 |
+
self._ylabel = VAR_OPTIONS_R[self.var]
|
573 |
+
|
574 |
+
@pn.depends("station", "year", "year_range", watch=True)
|
575 |
+
def _update_title(self):
|
576 |
+
df = self._station_df
|
577 |
+
df_subset = df.loc[df["year"].between(*self.year_range)]
|
578 |
+
# hack to get the title and ylabel to update
|
579 |
+
year_min = df_subset["year"].min()
|
580 |
+
if self.year_range[0] > year_min:
|
581 |
+
year_min = self.year_range[0]
|
582 |
+
year_max = df_subset["year"].max()
|
583 |
+
if self.year_range[1] < year_max:
|
584 |
+
year_max = self.year_range[1]
|
585 |
+
year_range_label = f"{year_min}-{year_max}"
|
586 |
+
self._title = f"{self._get_station_label()} - {self.year} vs Average ({year_range_label})"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
587 |
|
588 |
def _create_line_plots(self, df, df_year, df_avg):
|
589 |
plot_kwargs = {
|
590 |
"x": "dayofyear",
|
591 |
"y": self.var,
|
592 |
"legend": False,
|
593 |
+
"responsive": True,
|
594 |
}
|
595 |
plot = df.hvplot(
|
596 |
by="year",
|
|
|
599 |
hover=False,
|
600 |
**plot_kwargs,
|
601 |
)
|
602 |
+
df_above = df_year.copy()
|
603 |
+
df_above.loc[df_above["above_or_below"]] = np.nan
|
604 |
+
df_below = df_year.copy()
|
605 |
+
df_below.loc[~df_below["above_or_below"]] = np.nan
|
606 |
+
plot_year = df_year.hvplot(
|
607 |
+
color="lightgrey", hover="vline", alpha=0.5, **plot_kwargs
|
608 |
+
).redim.label(**{"dayofyear": "Julian Day", self.var: str(self.year)})
|
609 |
+
plot_above = df_above.hvplot(
|
610 |
+
hover="vline", color=DARK_BLUE, **plot_kwargs
|
611 |
+
).redim.label(**{"dayofyear": "Julian Day", self.var: str(self.year)})
|
612 |
+
plot_below = df_below.hvplot(
|
613 |
+
hover="vline", color=DARK_RED, **plot_kwargs
|
614 |
+
).redim.label(**{"dayofyear": "Julian Day", self.var: str(self.year)})
|
615 |
plot_avg = df_avg.hvplot(
|
616 |
+
color="lightgrey", hover="vline", **plot_kwargs
|
617 |
).redim.label(**{"dayofyear": "Julian Day", self.var: "Average"})
|
618 |
+
return plot * plot_year * plot_above * plot_below * plot_avg
|
619 |
|
620 |
def _create_hlines(self, year_avg, year_max, year_min):
|
621 |
# Create horizontal lines
|
|
|
658 |
return text_year_avg * text_year_max * text_year_min
|
659 |
|
660 |
def _create_areas(self, df_above, df_below):
|
661 |
+
area_kwargs = {
|
662 |
+
"x": "dayofyear",
|
663 |
+
"y": f"{self.var}_avg",
|
664 |
+
"y2": self.var,
|
665 |
+
"hover": False,
|
666 |
+
"responsive": True,
|
667 |
+
}
|
668 |
+
area_opts = {"fill_alpha": 0.2, "line_alpha": 0.8}
|
669 |
+
plot_above = df_above.hvplot.area(**area_kwargs).opts(
|
670 |
+
line_color=DARK_RED, fill_color=DARK_RED, **area_opts
|
671 |
+
)
|
672 |
+
plot_below = df_below.hvplot.area(**area_kwargs).opts(
|
673 |
+
line_color=DARK_BLUE, fill_color=DARK_BLUE, **area_opts
|
674 |
+
)
|
675 |
return plot_above * plot_below
|
676 |
|
677 |
+
def _create_text_days_labels(
|
678 |
+
self,
|
679 |
+
df,
|
680 |
+
days_above,
|
681 |
+
days_below,
|
682 |
+
text_x=None,
|
683 |
+
text_y=None,
|
684 |
+
spacing=None,
|
685 |
+
suffix=None,
|
686 |
+
):
|
687 |
+
text_x = text_x or 30
|
688 |
+
text_y = text_y or df[self.var].max() + 3
|
689 |
+
spacing = spacing or 2
|
690 |
+
suffix = suffix or "DAYS"
|
691 |
text_days_above = hv.Text(text_x, text_y, f"{days_above}", fontsize=14).opts(
|
692 |
text_align="right",
|
693 |
text_baseline="bottom",
|
|
|
700 |
text_color=DARK_BLUE,
|
701 |
text_alpha=0.8,
|
702 |
)
|
703 |
+
text_above = hv.Text(
|
704 |
+
text_x + spacing, text_y, f"{suffix} ABOVE", fontsize=7
|
705 |
+
).opts(
|
706 |
text_align="left",
|
707 |
text_baseline="bottom",
|
708 |
text_color="lightgrey",
|
709 |
text_alpha=0.8,
|
710 |
)
|
711 |
+
text_below = hv.Text(
|
712 |
+
text_x + spacing, text_y, f"{suffix} BELOW", fontsize=7
|
713 |
+
).opts(
|
714 |
text_align="left",
|
715 |
text_baseline="top",
|
716 |
text_color="lightgrey",
|
|
|
719 |
|
720 |
return text_days_above * text_days_below * text_above * text_below
|
721 |
|
722 |
+
def _get_station_label(self):
|
723 |
if self.station not in self._network_df["station_name"].unique():
|
724 |
+
stid = self.station
|
725 |
station_name = self._network_df.loc[
|
726 |
self._network_df["stid"] == self.station, "station_name"
|
727 |
].iloc[0]
|
728 |
else:
|
729 |
+
stid = self._network_df.loc[
|
730 |
+
self._network_df["station_name"] == self.station, "stid"
|
731 |
+
].iloc[0]
|
732 |
station_name = self.station
|
733 |
+
station_label = f"{station_name.title()} ({stid})"
|
734 |
+
return station_label
|
735 |
|
736 |
def __panel__(self):
|
737 |
return self._template
|