ahuang11 commited on
Commit
2f21c6a
1 Parent(s): 79a6752

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +266 -195
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=2023&month2=12&day2=31&var={var}&na=blank&format=csv"
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
- pn.extension(throttled=True)
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=2023, bounds=(1928, 2023))
128
- year_range = param.Range(default=(1990, 2020), bounds=(1928, 2023))
 
 
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._networks_df = self._get_networks_df()
135
- networks = sorted(self._networks_df["iem_network"].unique())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(self.param.stat, sizing_mode="stretch_width")
148
- self._sidebar = [
 
 
 
 
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
- self._update_var_station_dependents()
 
 
 
186
  self._update_stations()
 
187
  self._update_station_pane()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  @pn.cache
190
- def _get_networks_df(self):
191
- networks_df = pd.read_csv(NETWORKS_URL)
192
- return networks_df
193
 
194
  @pn.depends("network", watch=True)
195
  def _update_stations(self):
196
- network_df_subset = self._networks_df.loc[
197
- self._networks_df["iem_network"] == self.network,
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._networks_df["lon"].values, self._networks_df["lat"].values, x, y
223
  )
224
 
225
  min_distance_index = np.argmin(distances)
226
 
227
- closest_row = self._networks_df.iloc[min_distance_index]
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._networks_df["station_name"].unique():
235
- station = self._networks_df.loc[
236
- self._networks_df["station_name"] == station, "stid"
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
- # get average and year
 
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
- # preprocess below/above
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
- days_above = df_above.loc[
335
- df_above[f"{self.var}_year"] >= df_above[f"{self.var}_avg"]
336
- ].shape[0]
337
- days_below = df_below.loc[
338
- df_below[f"{self.var}_year"] < df_below[f"{self.var}_avg"]
339
- ].shape[0]
340
-
341
- # create plot elements
342
- plot_kwargs = {
343
- "x": "dayofyear",
344
- "y": self.var,
345
- "responsive": True,
346
- "legend": False,
347
- }
348
- plot = df.hvplot(
349
- by="year",
350
- color="grey",
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=f"{station_name} {self.year} vs AVERAGE ({self.year_range[0]}-{self.year_range[1]})",
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 __panel__(self):
466
- return pn.template.FastListTemplate(
467
- sidebar=self._sidebar,
468
- main=self._main,
469
- theme="dark",
470
- theme_toggle=False,
471
- main_layout=None,
472
- title="Select Year vs Average Comparison",
473
- accent="#2F4F4F",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()