Spaces:
Running
Running
from datetime import datetime | |
import param | |
import panel as pn | |
import numpy as np | |
import pandas as pd | |
import hvplot.pandas | |
import geoviews as gv | |
import holoviews as hv | |
from holoviews.streams import Tap | |
from bokeh.themes import Theme | |
pn.extension(throttled=True, notifications=True) | |
VAR_OPTIONS = { | |
"Maximum Air Temperature [F]": "max_temp_f", | |
"Minimum Air Temperature [F]": "min_temp_f", | |
"Maximum Dew Point [F]": "max_dewpoint_f", | |
"Minimum Dew Point [F]": "min_dewpoint_f", | |
"Daily Precipitation [inch]": "precip_in", | |
"Average Wind Speed [knots]": "avg_wind_speed_kts", | |
"Average Wind Direction [deg]": "avg_wind_drct", | |
"Minimum Relative Humidity [%]": "min_rh", | |
"Average Relative Humidity [%]": "avg_rh", | |
"Maximum Relative Humidity [%]": "max_rh", | |
"NCEI 1991-2020 Daily High Temperature Climatology [F]": "climo_high_f", | |
"NCEI 1991-2020 Daily Low Temperature Climatology [F]": "climo_low_f", | |
"NCEI 1991-2020 Daily Precipitation Climatology [inch]": "climo_precip_in", | |
"Reported Snowfall [inch]": "snow_in", | |
"Reported Snow Depth [inch]": "snowd_in", | |
"Minimum 'Feels Like' Temperature [F]": "min_feel", | |
"Average 'Feels Like' Temperature [F]": "avg_feel", | |
"Maximum 'Feels Like' Temperature [F]": "max_feel", | |
"Maximum sustained wind speed [knots]": "max_wind_speed_kts", | |
"Maximum wind gust [knots]": "max_wind_gust_kts", | |
"Daily Solar Radiation MJ/m2": "srad_mj", | |
} | |
WELCOME_MESSAGE = """ | |
### Welcome to the Climaviewer! | |
This app allows you to compare a single year of data from a weather station to the average of a range of years. | |
1. Select a network station from the dropdowns; alternatively you can click on the map to select the nearest station. | |
2. Choose the desired variable and year to plot, and change the average range if desired. | |
3. Hover over the plot to see the value for each day; use the mouse wheel to zoom in/out. | |
4. Tap on the top plot to see the value for a specific day, compared to all other years in the average range. | |
""" | |
FOOTER_MESSAGE = """ | |
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/) | |
Data sourced from the [Iowa Environmental Mesonet](https://mesonet.agron.iastate.edu/). | |
""" | |
VAR_OPTIONS_R = {v: k for k, v in VAR_OPTIONS.items()} | |
ONI_URL = "https://raw.githubusercontent.com/ahuang11/oni/master/oni.csv" | |
NETWORKS_URL = "https://mesonet.agron.iastate.edu/sites/networks.php?network=_ALL_&format=csv&nohtml=on" | |
STATION_URL_FMT = ( | |
"https://mesonet.agron.iastate.edu/cgi-bin/request/daily.py?network={network}&stations={station}" | |
"&year1=1928&month1=1&day1=1&year2=2024&month2=12&day2=31&var={var}&na=blank&format=csv" | |
) | |
DARK_RED = "#FF5555" | |
DARK_BLUE = "#5588FF" | |
SEASON_TO_MONTH = { | |
"DJF": "JAN", | |
"JFM": "FEB", | |
"FMA": "MAR", | |
"MAM": "APR", | |
"AMJ": "MAY", | |
"MJJ": "JUN", | |
"JJA": "JUL", | |
"JAS": "AUG", | |
"ASO": "SEP", | |
"SON": "OCT", | |
"OND": "NOV", | |
"NDJ": "DEC", | |
} | |
XTICKS = [ | |
(1, "JAN"), | |
(31, "FEB"), | |
(59, "MAR"), | |
(90, "APR"), | |
(120, "MAY"), | |
(151, "JUN"), | |
(181, "JUL"), | |
(212, "AUG"), | |
(243, "SEP"), | |
(273, "OCT"), | |
(304, "NOV"), | |
(334, "DEC"), | |
] | |
MONTH_TO_JULIAN_DAY = {month: day for day, month in XTICKS} | |
ONI_COLORS = { | |
"El Nino": DARK_RED, | |
"Neutral": "grey", | |
"La Nina": DARK_BLUE, | |
} | |
THEME_JSON = { | |
"attrs": { | |
"figure": { | |
"background_fill_color": "#1b1e23", | |
"border_fill_color": "#1b1e23", | |
"outline_line_alpha": 0, | |
}, | |
"Grid": { | |
"grid_line_color": "#808080", | |
"grid_line_alpha": 0.1, | |
}, | |
"Axis": { | |
# tick color and alpha | |
"major_tick_line_color": "#4d4f51", | |
"minor_tick_line_alpha": 0, | |
# tick labels | |
"major_label_text_font": "Courier New", | |
"major_label_text_color": "#808080", | |
"major_label_text_align": "left", | |
"major_label_text_font_size": "0.95em", | |
"major_label_text_font_style": "normal", | |
# axis labels | |
"axis_label_text_font": "Courier New", | |
"axis_label_text_font_style": "normal", | |
"axis_label_text_font_size": "1.15em", | |
"axis_label_text_color": "lightgrey", | |
"axis_line_color": "#4d4f51", | |
}, | |
"Legend": { | |
"spacing": 8, | |
"glyph_width": 15, | |
"label_standoff": 8, | |
"label_text_color": "#808080", | |
"label_text_font": "Courier New", | |
"label_text_font_size": "0.95em", | |
"label_text_font_style": "bold", | |
"border_line_alpha": 0, | |
"background_fill_alpha": 0.25, | |
"background_fill_color": "#1b1e23", | |
}, | |
"BaseColorBar": { | |
# axis labels | |
"title_text_color": "lightgrey", | |
"title_text_font": "Courier New", | |
"title_text_font_size": "0.95em", | |
"title_text_font_style": "normal", | |
# tick labels | |
"major_label_text_color": "#808080", | |
"major_label_text_font": "Courier New", | |
"major_label_text_font_size": "0.95em", | |
"major_label_text_font_style": "normal", | |
"background_fill_color": "#1b1e23", | |
"major_tick_line_alpha": 0, | |
"bar_line_alpha": 0, | |
}, | |
"Title": { | |
"text_font": "Courier New", | |
"text_font_style": "normal", | |
"text_color": "lightgrey", | |
}, | |
} | |
} | |
theme = Theme(json=THEME_JSON) | |
this_year = datetime.now().year | |
hv.renderer("bokeh").theme = theme | |
class ClimateApp(pn.viewable.Viewer): | |
network = param.Selector( | |
default="WA_ASOS", label="Network (delete & type to search)" | |
) | |
station = param.Selector(default="SEA", label="Station (delete & type to search)") | |
year = param.Integer(default=this_year - 1, bounds=(1928, this_year)) | |
year_range = param.Range( | |
default=(1990, 2020), bounds=(1928, this_year), label="Average Range" | |
) | |
var = param.Selector(default="max_temp_f", objects=sorted(VAR_OPTIONS.values())) | |
stat = param.Selector(default="Mean", objects=["Mean", "Median"]) | |
_title = param.String() | |
_ylabel = param.String() | |
def __init__(self, **params): | |
super().__init__(**params) | |
self._sidebar = pn.Column(sizing_mode="stretch_both") | |
self._main = pn.Column( | |
pn.indicators.LoadingSpinner( | |
value=True, width=25, height=25, name="Loading, please wait a moment..." | |
), | |
sizing_mode="stretch_both", | |
) | |
self._modal = pn.Column(width=850, height=500, align="center") | |
self._template = pn.template.FastListTemplate( | |
sidebar=[self._sidebar], | |
main=[self._main], | |
modal=[self._modal], | |
theme="dark", | |
theme_toggle=False, | |
main_layout=None, | |
title="Weather Station Year vs Climatology", | |
accent="grey", | |
header_background="#1b1e23", | |
) | |
pn.state.onload(self._onload) | |
def _onload(self): | |
try: | |
self._sidebar.loading = True | |
self._populate_sidebar() | |
self._populate_main() | |
self._populate_modal() | |
finally: | |
self._sidebar.loading = False | |
def _populate_sidebar(self): | |
self._network_df = self._get_network_df() | |
self._oni_df = self._get_oni_df() | |
networks = sorted(self._network_df["iem_network"].unique()) | |
self.param["network"].objects = networks | |
open_button = pn.widgets.Button( | |
name="Select station from map", | |
button_type="primary", | |
sizing_mode="stretch_width", | |
) | |
open_button.on_click(self._open_modal) | |
network_select = pn.widgets.AutocompleteInput.from_param( | |
self.param.network, min_characters=0, case_sensitive=False | |
) | |
station_select = pn.widgets.AutocompleteInput.from_param( | |
self.param.station, min_characters=0, case_sensitive=False | |
) | |
var_select = pn.widgets.Select.from_param(self.param.var, options=VAR_OPTIONS) | |
year_slider = pn.widgets.IntSlider.from_param(self.param.year) | |
year_range_slider = pn.widgets.RangeSlider.from_param(self.param.year_range) | |
stat_select = pn.widgets.RadioButtonGroup.from_param( | |
self.param.stat, sizing_mode="stretch_width" | |
) | |
self._sidebar.objects = [ | |
pn.pane.Markdown(WELCOME_MESSAGE), | |
open_button, | |
network_select, | |
station_select, | |
var_select, | |
year_slider, | |
year_range_slider, | |
stat_select, | |
pn.pane.Markdown(FOOTER_MESSAGE), | |
] | |
def _populate_main(self): | |
self._tap_x = Tap() | |
self._station_pane = pn.pane.HoloViews( | |
sizing_mode="stretch_both", | |
min_width=800, | |
min_height=350, | |
) | |
self._day_pane = pn.pane.HoloViews( | |
sizing_mode="stretch_both", | |
min_width=800, | |
min_height=350, | |
) | |
self._update_stations() | |
self._update_var_station_dependents() | |
self._day_pane.object = pn.bind( | |
self._update_day_plot, | |
self.param.var, | |
self.param.station, | |
self.param.year, | |
self.param.year_range, | |
self._tap_x.param.x, | |
) | |
pointer_vline = hv.DynamicMap(self._update_vline, streams=[self._tap_x]) | |
oni_plot = hv.DynamicMap( | |
self._update_oni_plot | |
) | |
station_plot = hv.DynamicMap( | |
pn.bind( | |
self._update_station_plot, | |
self.param.var, | |
self.param.station, | |
self.param.year, | |
self.param.year_range, | |
self.param.stat, | |
), | |
) | |
self._station_pane.object = ( | |
(oni_plot * station_plot * pointer_vline) | |
.opts( | |
xlabel="Time of Year", | |
gridstyle={"ygrid_line_alpha": 0}, | |
xticks=XTICKS, | |
show_grid=True, | |
padding=(0, (0, 0.45)), | |
responsive=True, | |
shared_axes=False, | |
legend_position="top_right" | |
) | |
.apply.opts(title=self.param._title, ylabel=self.param._ylabel) | |
) | |
self._update_ylabel() | |
self._update_title() | |
self._main.objects = [self._station_pane, self._day_pane] | |
def _populate_modal(self): | |
network_points = self._network_df.hvplot.points( | |
"lon", | |
"lat", | |
legend=False, | |
cmap="category10", | |
color="iem_network", | |
hover_cols=["stid", "station_name", "iem_network"], | |
size=10, | |
geo=True, | |
responsive=True, | |
xlabel="Longitude", | |
ylabel="Latitude", | |
).opts( | |
"Points", | |
fill_alpha=0, | |
tools=["tap", "hover"], | |
active_tools=["wheel_zoom"], | |
) | |
tap = Tap(source=network_points) | |
pn.bind(self._update_station, x=tap.param.x, y=tap.param.y, watch=True) | |
instructions = pn.pane.Markdown( | |
"#### The nearest station will be selected when you click on the map." | |
) | |
network_pane = pn.pane.HoloViews( | |
network_points * gv.tile_sources.CartoDark(), | |
) | |
self._modal.objects = [instructions, network_pane] | |
def _open_modal(self, event): | |
self._template.open_modal() | |
def _get_oni_df(self): | |
df = pd.read_csv(ONI_URL) | |
df["month"] = df["season"].map(SEASON_TO_MONTH) | |
df["julian_day"] = df["month"].map(MONTH_TO_JULIAN_DAY) | |
df["julian_day_end"] = df["julian_day"].shift(-1).fillna(365) | |
df["oni"] = df["oni"].str.replace("_", " ").str.title() | |
return df | |
def _get_network_df(self): | |
network_df = pd.read_csv(NETWORKS_URL) | |
return network_df.loc[network_df["iem_network"].str.contains("ASOS")] | |
def _update_stations(self): | |
self._template.close_modal() | |
network_df_subset = self._network_df.loc[ | |
self._network_df["iem_network"] == self.network, | |
["stid", "station_name"], | |
] | |
names = sorted(network_df_subset["station_name"].unique()) | |
stids = sorted(network_df_subset["stid"].unique()) | |
self.param["station"].objects = names + stids | |
def _update_station(self, x, y): | |
if x is None or y is None: | |
return | |
def haversine_vectorized(lon1, lat1, lon2, lat2): | |
R = 6371 # Radius of the Earth in kilometers | |
dlat = np.radians(lat2 - lat1) | |
dlon = np.radians(lon2 - lon1) | |
a = ( | |
np.sin(dlat / 2.0) ** 2 | |
+ np.cos(np.radians(lat1)) | |
* np.cos(np.radians(lat2)) | |
* np.sin(dlon / 2.0) ** 2 | |
) | |
c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a)) | |
return R * c | |
distances = haversine_vectorized( | |
self._network_df["lon"].values, self._network_df["lat"].values, x, y | |
) | |
min_distance_index = np.argmin(distances) | |
closest_row = self._network_df.iloc[min_distance_index] | |
with param.parameterized.batch_call_watchers(self): | |
self.network = closest_row["iem_network"] | |
self.station = closest_row["stid"] | |
def _get_station_df(self, station, var): | |
if station in self._network_df["station_name"].unique(): | |
station = self._network_df.loc[ | |
self._network_df["station_name"] == station, "stid" | |
].iloc[0] | |
if station.startswith("K"): | |
station = station.lstrip("K") | |
station_url = STATION_URL_FMT.format( | |
network=self.network, station=station, var=var | |
) | |
station_df = ( | |
pd.read_csv( | |
station_url, | |
parse_dates=True, | |
index_col="day", | |
) | |
.drop(columns=["station"]) | |
.astype("float16") | |
.assign( | |
dayofyear=lambda df: df.index.dayofyear, | |
year=lambda df: df.index.year, | |
) | |
.dropna() | |
) | |
return station_df | |
def _update_var_station_dependents(self): | |
try: | |
self._main.loading = True | |
self._station_df = self._get_station_df(self.station, self.var).dropna() | |
if len(self._station_df) == 0: | |
return | |
year_range_min = self._station_df["year"].min() | |
year_range_max = self._station_df["year"].max() | |
self.param["year"].bounds = (year_range_min, year_range_max) | |
if self.year < year_range_min: | |
self.year = year_range_min | |
if self.year > year_range_max: | |
self.year = year_range_max | |
finally: | |
self._main.loading = False | |
def _update_vline(self, x, y): | |
if x is None: | |
x = 0 | |
if y is None: | |
y = 0 | |
vline = hv.VLine(x).opts(line_width=0.9, color="lightgrey") | |
text = hv.Text( | |
x, | |
y, | |
f"Julian Day {int(x)}", | |
).opts( | |
text_color="lightgrey", | |
text_align="left", | |
text_baseline="bottom", | |
text_alpha=0.8, | |
) | |
return vline * text | |
def _update_oni_plot(self): | |
df = self._oni_df | |
df_year = df.loc[df["year"] == self.year] | |
try: | |
df_year.iloc[-1, -1] = 365 | |
except: | |
pass | |
overlay = hv.Overlay([]) | |
for oni in df_year["oni"].unique(): | |
df_subset = df_year.loc[df_year["oni"] == oni, ["julian_day", "julian_day_end"]] | |
overlay *= hv.VSpans( | |
df_subset, | |
["julian_day", "julian_day_end"], | |
label=oni, | |
).opts(color=ONI_COLORS[oni], alpha=0.18, line_alpha=0) | |
return overlay | |
def _update_day_plot(self, var, station, year, year_range, x): | |
df = self._station_df | |
if not x: | |
x = 1 | |
x = int(x) | |
df_day_year = df.query(f"year == {year}") | |
if x > df_day_year["dayofyear"].max(): | |
x = df_day_year["dayofyear"].max() | |
day_year = df_day_year.loc[df_day_year["dayofyear"] == x, self.var].iloc[0] | |
df_subset = df.loc[df["year"].between(*year_range)] | |
df_day_climo = df_subset.loc[df_subset["dayofyear"] == x] | |
df_day_climo = df_day_climo.assign( | |
above_or_below=df_day_climo[self.var] >= day_year | |
) | |
title = ( | |
f"{VAR_OPTIONS_R[self.var]} across {year_range[0]}-{year_range[1]} on " | |
+ df_day_climo.index.strftime("%B %d")[0] | |
+ f" (Julian Day {x}) " | |
) | |
days_above = df_day_climo.loc[df_day_climo["above_or_below"] == True].shape[0] | |
days_below = df_day_climo.loc[df_day_climo["above_or_below"] == False].shape[0] | |
min_x = df[self.var].min() | |
plot = hv.Overlay([]) | |
plot *= df_day_climo.hvplot.hist( | |
self.var, | |
responsive=True, | |
by="above_or_below", | |
bins=11, | |
legend=False, | |
color=hv.Cycle([DARK_BLUE, DARK_RED]), | |
).opts("Histogram", fill_alpha=0.7, line_alpha=0) | |
plot *= hv.VLine(day_year).opts(line_width=0.9, color="lightgrey") | |
plot *= hv.Text( | |
day_year, | |
0.1, | |
f"{year}", | |
).opts( | |
text_color="lightgrey", | |
text_align="left", | |
text_baseline="bottom", | |
text_alpha=0.8, | |
) | |
plot *= self._create_text_days_labels( | |
df, | |
days_above, | |
days_below, | |
text_x=min_x + 5, | |
text_y=4, | |
spacing=1, | |
suffix=f"YEARS", | |
) | |
return plot.opts( | |
xlabel=VAR_OPTIONS_R[self.var], | |
ylabel="Number of Days", | |
title=title, | |
shared_axes=False, | |
show_grid=True, | |
gridstyle={"xgrid_line_alpha": 0}, | |
xlim=(min_x, df[self.var].max()), | |
) | |
def _update_station_plot(self, var, station, year, year_range, stat): | |
if len(self._station_df) == 0: | |
return | |
# base dataframes | |
df = self._station_df | |
df_subset = df.loc[df["year"].between(*year_range)] | |
df_avg = df_subset.groupby("dayofyear").mean() | |
df_year = df[df.year == year] | |
# above/below | |
df_year = df_year[["dayofyear", self.var]].merge( | |
df_avg.reset_index()[["dayofyear", self.var]], | |
on="dayofyear", | |
suffixes=("", "_avg"), | |
) | |
df_year["above_or_below"] = df_year[self.var] >= df_year[f"{self.var}_avg"] | |
days_above = df_year.loc[df_year["above_or_below"] == True].shape[0] | |
days_below = df_year.loc[df_year["above_or_below"] == False].shape[0] | |
# stats | |
if stat == "Mean": | |
year_avg = df_year[self.var].mean() | |
else: | |
year_avg = df_year[self.var].median() | |
year_max = df_year[self.var].max() | |
year_min = df_year[self.var].min() | |
plots = self._create_line_plots(df, df_year, df_avg) | |
lines = self._create_hlines(year_avg, year_max, year_min) | |
texts = self._create_text_labels(year_avg, year_max, year_min) | |
text_days = self._create_text_days_labels(df, days_above, days_below) | |
# Overlay all elements | |
station_overlay = plots * lines * texts * text_days | |
return station_overlay | |
def _update_ylabel(self): | |
self._ylabel = VAR_OPTIONS_R[self.var] | |
def _update_title(self): | |
df = self._station_df | |
df_subset = df.loc[df["year"].between(*self.year_range)] | |
# hack to get the title and ylabel to update | |
year_min = df_subset["year"].min() | |
if self.year_range[0] > year_min: | |
year_min = self.year_range[0] | |
year_max = df_subset["year"].max() | |
if self.year_range[1] < year_max: | |
year_max = self.year_range[1] | |
year_range_label = f"{year_min}-{year_max}" | |
self._title = f"{self._get_station_label()} - {self.year} vs Average ({year_range_label})" | |
def _create_line_plots(self, df, df_year, df_avg): | |
plot_kwargs = { | |
"x": "dayofyear", | |
"y": self.var, | |
"legend": False, | |
"responsive": True, | |
} | |
plot = df.hvplot( | |
by="year", | |
color="grey", | |
alpha=0.02, | |
hover=False, | |
**plot_kwargs, | |
) | |
df_above = df_year.copy() | |
df_above.loc[df_above["above_or_below"]] = np.nan | |
df_below = df_year.copy() | |
df_below.loc[~df_below["above_or_below"]] = np.nan | |
plot_year = df_year.hvplot( | |
color="lightgrey", hover="vline", alpha=0.5, **plot_kwargs | |
).redim.label(**{"dayofyear": "Julian Day", self.var: str(self.year)}) | |
plot_above = df_above.hvplot( | |
hover="vline", color=DARK_BLUE, **plot_kwargs | |
).redim.label(**{"dayofyear": "Julian Day", self.var: str(self.year)}) | |
plot_below = df_below.hvplot( | |
hover="vline", color=DARK_RED, **plot_kwargs | |
).redim.label(**{"dayofyear": "Julian Day", self.var: str(self.year)}) | |
plot_avg = df_avg.hvplot( | |
color="lightgrey", hover="vline", **plot_kwargs | |
).redim.label(**{"dayofyear": "Julian Day", self.var: "Average"}) | |
return plot * plot_year * plot_above * plot_below * plot_avg | |
def _create_hlines(self, year_avg, year_max, year_min): | |
# Create horizontal lines | |
plot_year_avg = hv.HLine(year_avg).opts( | |
line_color="lightgrey", line_dash="dashed", line_width=0.5 | |
) | |
plot_year_max = hv.HLine(year_max).opts( | |
line_color=DARK_RED, line_dash="dashed", line_width=0.5 | |
) | |
plot_year_min = hv.HLine(year_min).opts( | |
line_color=DARK_BLUE, line_dash="dashed", line_width=0.5 | |
) | |
return plot_year_avg * plot_year_max * plot_year_min | |
def _create_text_labels(self, year_avg, year_max, year_min): | |
text_year_opts = { | |
"text_align": "right", | |
"text_baseline": "bottom", | |
"text_alpha": 0.8, | |
} | |
text_year_label = "AVERAGE" if self.stat == "Mean" else "MEDIAN" | |
text_year_avg = hv.Text( | |
360, year_avg + 3, f"{text_year_label} {year_avg:.1f}", fontsize=8 | |
).opts( | |
text_color="lightgrey", | |
**text_year_opts, | |
) | |
text_year_max = hv.Text( | |
360, year_max + 3, f"MAX {year_max:.1f}", fontsize=8 | |
).opts( | |
text_color=DARK_RED, | |
**text_year_opts, | |
) | |
text_year_min = hv.Text( | |
360, year_min + 3, f"MIN {year_min:.1f}", fontsize=8 | |
).opts( | |
text_color=DARK_BLUE, | |
**text_year_opts, | |
) | |
return text_year_avg * text_year_max * text_year_min | |
def _create_areas(self, df_above, df_below): | |
area_kwargs = { | |
"x": "dayofyear", | |
"y": f"{self.var}_avg", | |
"y2": self.var, | |
"hover": False, | |
"responsive": True, | |
} | |
area_opts = {"fill_alpha": 0.2, "line_alpha": 0.8} | |
plot_above = df_above.hvplot.area(**area_kwargs).opts( | |
line_color=DARK_RED, fill_color=DARK_RED, **area_opts | |
) | |
plot_below = df_below.hvplot.area(**area_kwargs).opts( | |
line_color=DARK_BLUE, fill_color=DARK_BLUE, **area_opts | |
) | |
return plot_above * plot_below | |
def _create_text_days_labels( | |
self, | |
df, | |
days_above, | |
days_below, | |
text_x=None, | |
text_y=None, | |
spacing=None, | |
suffix=None, | |
): | |
text_x = text_x or 30 | |
text_y = text_y or df[self.var].max() + 3 | |
spacing = spacing or 2 | |
suffix = suffix or "DAYS" | |
text_days_above = hv.Text(text_x, text_y, f"{days_above}", fontsize=14).opts( | |
text_align="right", | |
text_baseline="bottom", | |
text_color=DARK_RED, | |
text_alpha=0.8, | |
) | |
text_days_below = hv.Text(text_x, text_y, f"{days_below}", fontsize=14).opts( | |
text_align="right", | |
text_baseline="top", | |
text_color=DARK_BLUE, | |
text_alpha=0.8, | |
) | |
text_above = hv.Text( | |
text_x + spacing, text_y, f"{suffix} ABOVE", fontsize=7 | |
).opts( | |
text_align="left", | |
text_baseline="bottom", | |
text_color="lightgrey", | |
text_alpha=0.8, | |
) | |
text_below = hv.Text( | |
text_x + spacing, text_y, f"{suffix} BELOW", fontsize=7 | |
).opts( | |
text_align="left", | |
text_baseline="top", | |
text_color="lightgrey", | |
text_alpha=0.8, | |
) | |
return text_days_above * text_days_below * text_above * text_below | |
def _get_station_label(self): | |
if self.station not in self._network_df["station_name"].unique(): | |
stid = self.station | |
station_name = self._network_df.loc[ | |
self._network_df["stid"] == self.station, "station_name" | |
].iloc[0] | |
else: | |
stid = self._network_df.loc[ | |
self._network_df["station_name"] == self.station, "stid" | |
].iloc[0] | |
station_name = self.station | |
station_label = f"{station_name.title()} ({stid})" | |
return station_label | |
def __panel__(self): | |
return self._template | |
ClimateApp().servable() | |