Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

23 map timeline controls #25

Merged
merged 11 commits into from
Oct 14, 2024
5 changes: 5 additions & 0 deletions dashboard/bokeh_dash_app.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
# load relevant libraries
import logging
from bokeh.io import curdoc
from bokeh.models import TabPanel, Tabs

# load custom modules
from utilities.bokeh_line_dash import bokeh_line_dash
from utilities.bokeh_map_dash import bokeh_map_dash

# set up logging
lgr = logging.getLogger()
lgr.setLevel(logging.INFO)

# initialise and structure bokeh line dashboard
dashboard_line = bokeh_line_dash()
panel_line_tab = TabPanel(child = dashboard_line, title = 'Time Series')
Expand Down
1 change: 1 addition & 0 deletions dashboard/cons.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
linedash_year_timespan = ["2010", linedash_yearend]
linedash_yearmonth_timespan = ["2010-01", f"{linedash_yearend}-12"]
linedash_month_timespan = ["01", "12"]
linedash_year_options = [str(year) for year in range(int(linedash_year_timespan[0]), int(linedash_year_timespan[1])+1)]

# bokeh server execution commands
bat_execBokehApp = "START /MIN CMD.EXE /C exeBokehApp.bat"
Expand Down
27 changes: 12 additions & 15 deletions dashboard/utilities/bokeh_line_dash.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pickle
import logging
from beartype import beartype
from typing import Union
from bokeh.models import Select, CheckboxGroup, Div, Button, MultiSelect
Expand All @@ -8,6 +9,7 @@
import cons
from utilities.bokeh_line_data import bokeh_line_data
from utilities.bokeh_line_plot import bokeh_line_plot
from utilities.timeit import timeit

@beartype
def bokeh_line_dash():
Expand All @@ -21,21 +23,20 @@ def bokeh_line_dash():
bokeh.layouts.row
The interactive bokeh line dashboard
"""
logging.info("Initialise line plot begin")
with open(cons.preaggregate_data_fpath, "rb") as handle:
pre_agg_data_dict = pickle.load(handle)
# generate bokeh data for line plot
bokeh_line_data_dict = bokeh_line_data(pre_agg_data_dict)
bokeh_line_data_params = {"pre_agg_data_dict":pre_agg_data_dict}
bokeh_line_data_dict = timeit(func=bokeh_line_data, params=bokeh_line_data_params)
# create bokeh plot
line_plot = bokeh_line_plot(
bokeh_line_data_dict,
col=cons.col_default,
stat=cons.stat_default,
agg_level=cons.line_agg_level_default,
selection=cons.counties,
)
bokeh_line_plot_params = {"bokeh_data_dict":bokeh_line_data_dict, "col":cons.col_default, "stat":cons.stat_default, "agg_level":cons.line_agg_level_default, "selection":cons.counties}
line_plot = timeit(func=bokeh_line_plot, params=bokeh_line_plot_params)
logging.info("Initialise line plot end")

# create call back function for bokeh dashboard interaction
def callback_line_plot(attr, old, new):
logging.info("Callback line plot begin")
# extract new selector value
agg_level = line_agg_level_selector.value
col = line_col_selector.value
Expand All @@ -44,15 +45,11 @@ def callback_line_plot(attr, old, new):
for i in line_county_multiselect.value:
selection.append(cons.counties[int(i)])
# update bokeh plot
line_plot = bokeh_line_plot(
bokeh_line_data_dict,
col=col,
stat=stat,
agg_level=agg_level,
selection=selection,
)
bokeh_line_plot_params = {"bokeh_data_dict":bokeh_line_data_dict, "col":col, "stat":stat, "agg_level":agg_level, "selection":selection}
line_plot = timeit(func=bokeh_line_plot, params=bokeh_line_plot_params)
# reassign bokeh plot to bokeh dashboard
dashboard_line.children[1] = line_plot
logging.info("Callback line plot end")

def callback_multiselect_selectall():
line_county_multiselect.value = cons.counties_values
Expand Down
46 changes: 27 additions & 19 deletions dashboard/utilities/bokeh_map_dash.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import pickle
import logging
from bokeh.models import Select, Div, CheckboxButtonGroup
from bokeh.layouts import column, row
from beartype import beartype

import cons
from utilities.bokeh_map_data import bokeh_map_data
from utilities.bokeh_map_plot import bokeh_map_plot
from utilities.timeit import timeit

@beartype
def bokeh_map_dash():
Expand All @@ -19,37 +21,36 @@ def bokeh_map_dash():
bokeh.layouts.row
The interactive bokeh map dashboard
"""
logging.info("Initialise map plot begin")
with open(cons.map_data_fpath, "rb") as handle:
map_data_dict = pickle.load(handle)
map_data = pickle.load(handle)
with open(cons.points_data_fpath, "rb") as handle:
station_data = pickle.load(handle)
# generate bokeh data for map plot
bokeh_map_data_dict, pointgeosource = bokeh_map_data(map_data_dict, station_data)
bokeh_map_data_params = {"map_data":map_data,"station_data":station_data,"col":cons.col_default,"stat":cons.stat_default,"year":cons.linedash_year_timespan[1]}
bokeh_map_data_dict = timeit(func=bokeh_map_data, params=bokeh_map_data_params)
# create bokeh map plot
map_plot = bokeh_map_plot(
bokeh_map_data_dict=bokeh_map_data_dict,
pointgeosource=pointgeosource,
col=cons.col_default,
stat=cons.stat_default,
show_stations=cons.show_stations_default,
)
bokeh_map_plot_params = {"bokeh_map_data_dict":bokeh_map_data_dict,"show_stations":cons.show_stations_default}
map_plot = timeit(func=bokeh_map_plot, params=bokeh_map_plot_params)
logging.info("Initialise map plot end")

# create call back function for bokeh dashboard interaction
def callback_map_plot(attr, old, new):
logging.info("Callback map plot begin")
# extract new selector value
col = map_col_selector.value
stat = map_stat_selector.value
year = map_year_selector.value
show_stations = toggle_stations.active
# update bokeh data
bokeh_map_data_params = {"map_data":map_data,"station_data":station_data,"col":col,"stat":stat,"year":year}
bokeh_map_data_dict = timeit(func=bokeh_map_data, params=bokeh_map_data_params)
# update bokeh plot
map_plot = bokeh_map_plot(
bokeh_map_data_dict=bokeh_map_data_dict,
pointgeosource=pointgeosource,
col=col,
stat=stat,
show_stations=show_stations,
)
bokeh_map_plot_params = {"bokeh_map_data_dict":bokeh_map_data_dict,"show_stations":show_stations}
map_plot = timeit(func=bokeh_map_plot, params=bokeh_map_plot_params)
# reassign bokeh plot to bokeh dashboard
dashboard_map.children[1] = map_plot
logging.info("Callback map plot end")

# set up selectors for bokeh map plot
map_col_selector = Select(
Expand All @@ -68,6 +69,14 @@ def callback_map_plot(attr, old, new):
height=60,
aspect_ratio=10,
)
map_year_selector = Select(
title="Year:",
value=cons.linedash_year_timespan[1],
options=cons.linedash_year_options,
width=120,
height=60,
aspect_ratio=10,
)
toggle_stations = CheckboxButtonGroup(
labels=["Toggle Stations"],
active=[],
Expand All @@ -77,11 +86,10 @@ def callback_map_plot(attr, old, new):
map_col_selector.on_change("value", callback_map_plot)
map_stat_selector.on_change("value", callback_map_plot)
toggle_stations.on_change("active", callback_map_plot)
map_year_selector.on_change("value", callback_map_plot)

# structure dashboard map plot
widgets_map = column(
toggle_stations, map_col_selector, map_stat_selector
)
widgets_map = column(toggle_stations, map_col_selector, map_stat_selector, map_year_selector)
dashboard_map = row(widgets_map, map_plot)

return dashboard_map
58 changes: 37 additions & 21 deletions dashboard/utilities/bokeh_map_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,58 @@

@beartype
def bokeh_map_data(
map_data_dict:dict,
station_data:gpd.GeoDataFrame
map_data:gpd.GeoDataFrame,
station_data:gpd.GeoDataFrame,
col:str,
stat:str,
year:str
):
"""Generates the data used in the bokeh map plot.

Parameters
----------
map_data_dict : dict
map_data : gpd.GeoDataFrame
The aggregated geopandas data to be transformed into aggregated bokeh data objects for visualisation
station_data : geopandas.DataFrame
The geospatial Met Eireann station data to be transformed into a bokeh data object for visualisation
col : str
The climate measure category to plot in the interactive bokeh heatmap
stat : str
The aggregated statistic to plot in the interactive bokeh heatmap
year : str
The year of the data to filter for

Returns
-------
dict, bokeh.models.GeoJSONDataSource
dict
The aggregated and station bokeh data objects to visualise
"""
bokeh_map_data_dict = {}
for stat, map_data in map_data_dict.items():
tmp_data_dict = {}
# split data into missing and nonmissing
nonmiss_map_data = map_data[map_data[cons.col_options].notnull().any(axis=1)]
miss_map_data = map_data[~map_data[cons.col_options].notnull().any(axis=1)]
# Input GeoJSON source that contains features for plotting
missgeosource = GeoJSONDataSource(geojson=miss_map_data.to_json())
nonmissgeosource = GeoJSONDataSource(geojson=nonmiss_map_data.to_json())
# assign data to temp dict
tmp_data_dict["map_data"] = map_data
tmp_data_dict["nonmiss_map_data"] = nonmiss_map_data
tmp_data_dict["miss_map_data"] = miss_map_data
tmp_data_dict["missgeosource"] = missgeosource
tmp_data_dict["nonmissgeosource"] = nonmissgeosource
# assign temp data dict to bokeh data dict
bokeh_map_data_dict[stat] = tmp_data_dict
# filter for year
row_filter = (map_data['year'].astype(str) == year) & (map_data['stat'].astype(str) == stat)
sub_cols = ['county', 'geometry', 'year', 'stat', col]
map_data_sub = map_data.loc[row_filter, sub_cols]
# split data into missing and nonmissing
nonmiss_map_data = map_data_sub[map_data_sub.notnull().all(axis=1)]
miss_map_data = map_data_sub[map_data_sub.isnull().any(axis=1)]
# Input GeoJSON source that contains features for plotting
missgeosource = GeoJSONDataSource(geojson=miss_map_data.to_json())
nonmissgeosource = GeoJSONDataSource(geojson=nonmiss_map_data.to_json())
pointgeosource = GeoJSONDataSource(geojson=station_data.to_json())
# calculate colour bar range
color_mapper_low = nonmiss_map_data[col].min()
color_mapper_high = nonmiss_map_data[col].max()
# assign data to temp dict
bokeh_map_data_dict["col"] = col
bokeh_map_data_dict["map_data"] = map_data
bokeh_map_data_dict["nonmiss_map_data"] = nonmiss_map_data
bokeh_map_data_dict["miss_map_data"] = miss_map_data
bokeh_map_data_dict["missgeosource"] = missgeosource
bokeh_map_data_dict["nonmissgeosource"] = nonmissgeosource
bokeh_map_data_dict["pointgeosource"] = pointgeosource
bokeh_map_data_dict["color_mapper_low"] = color_mapper_low
bokeh_map_data_dict["color_mapper_high"] = color_mapper_high
# pickle the bokeh map data dictionary to disk
# with open(cons.bokeh_map_data_fpath, 'wb') as f:
# pickle.dump(bokeh_map_data_dict, f, protocol = pickle.HIGHEST_PROTOCOL)
return bokeh_map_data_dict, pointgeosource
return bokeh_map_data_dict
37 changes: 12 additions & 25 deletions dashboard/utilities/bokeh_map_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
@beartype
def bokeh_map_plot(
bokeh_map_data_dict:dict,
pointgeosource:GeoJSONDataSource,
col:str,
stat:str,
show_stations:list
) -> figure:
"""Generates the data used in the bokeh map plot.
Expand All @@ -19,12 +16,6 @@ def bokeh_map_plot(
----------
bokeh_map_data_dict : dict
A dictionary of bokeh aggregated data objects to construct the interactive bokeh heatmap with
pointgeosource : geopandas.DataFrame
The geospatial Met Eireann station data to overlay was red dots in the interactive bokeh heatmap
col : str
The climate measure category to plot in the interactive bokeh heatmap
stat : str
The aggregated statistic to plot in the interactive bokeh heatmap
show_stations : list
Whether to toggle the Met Eireann station data overlay in the interactive bokeh heatmap

Expand All @@ -37,13 +28,11 @@ def bokeh_map_plot(
lightblue = Color("lightblue")
steelblue = Color("steelblue")
palette = tuple([col.get_hex() for col in lightblue.range_to(steelblue, 100)])
# palette = ("lightblue", "steelblue")
# instantiate LinearColorMapper that linearly maps numbers in a range, into a sequence of colors.
nonmiss_map_data = bokeh_map_data_dict[stat]["nonmiss_map_data"]
# instantiate LinearColorMapper that linearly maps numbers in a range, into a sequence of colours.
color_mapper = LinearColorMapper(
palette=palette,
low=nonmiss_map_data[col].min(),
high=nonmiss_map_data[col].max(),
low=bokeh_map_data_dict["color_mapper_low"],
high=bokeh_map_data_dict["color_mapper_high"],
)
# create color bar.
color_bar = ColorBar(
Expand All @@ -57,9 +46,7 @@ def bokeh_map_plot(
major_label_text_font_size="18px",
)
# create underlying figure object
map_plot = figure(
toolbar_location="below", output_backend="webgl", **cons.FIG_SETTING
)
map_plot = figure(toolbar_location="below", output_backend="webgl", **cons.FIG_SETTING)
map_plot.axis.visible = False
map_plot.xgrid.grid_line_color = None
map_plot.ygrid.grid_line_color = None
Expand All @@ -70,9 +57,9 @@ def bokeh_map_plot(
map_plot.title.text_font_style = "bold"
map_plot.title.text_font_size = "22px"
# add patches to render states with no aggregate data
missgeosource = bokeh_map_data_dict[stat]["missgeosource"]
missgeosource = bokeh_map_data_dict['missgeosource']#[year]['miss_map_dataview']
misscounties = map_plot.patches(
"xs", "ys", source=missgeosource, fill_color="white", **cons.MAP_SETTINGS
xs="xs", ys="ys", source=missgeosource, fill_color="slategray", **cons.MAP_SETTINGS
)
map_plot.add_tools(
HoverTool(
Expand All @@ -83,26 +70,26 @@ def bokeh_map_plot(
)
)
# add patches to render states with aggregate data
nonmissgeosource = bokeh_map_data_dict[stat]["nonmissgeosource"]
nonmissgeosource = bokeh_map_data_dict['nonmissgeosource']
nonmisscounties = map_plot.patches(
"xs",
"ys",
xs="xs",
ys="ys",
source=nonmissgeosource,
fill_color={"field": col, "transform": color_mapper},
fill_color={"field": bokeh_map_data_dict['col'], "transform": color_mapper},
**cons.MAP_SETTINGS,
)
map_plot.add_tools(
HoverTool(
renderers=[nonmisscounties],
tooltips=[("County Name", "@county"), ("County Value", f"@{col}")],
tooltips=[("County Name", "@county"), ("County Value", f"@{bokeh_map_data_dict['col']}")],
attachment="left",
mode="mouse",
)
)
# add points to render stations
if show_stations == [0]:
stationpoints = map_plot.scatter(
"x", "y", source=pointgeosource, color="red", size=8, alpha=0.3
x="x", y="y", source=bokeh_map_data_dict['pointgeosource'], color="red", size=8, alpha=0.3
)
map_plot.add_tools(
HoverTool(
Expand Down
6 changes: 5 additions & 1 deletion dashboard/arch/timeit.py → dashboard/utilities/timeit.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import time
import numpy as np
import logging


def timeit(func, params, itr=1, digits=3):
Expand Down Expand Up @@ -27,5 +28,8 @@ def timeit(func, params, itr=1, digits=3):
t1 = time.time()
tres = t1 - t0
eres = round(np.mean(tres), digits)
print(f"Mean Execution Time: {eres} seconds")
logging_message=f"Execution Time for {str(func)}: {eres} seconds"
if itr>1:
logging_message=f"Mean {logging_message}"
logging.info(logging_message)
return res
Binary file modified data/gis/map_data.pickle
Binary file not shown.
Loading
Loading