"""In this example we show how to construct an interactive
[Choropleth](ps://en.wikipedia.org/wiki/Choropleth_map) map.
The example is inspired by the article
[Choropleth maps with geopandas, Bokeh and Panel]\
(https://dmnfarrell.github.io/bioinformatics/bokeh-maps)
by [Damian Farrell](https://github.com/dmnfarrell). See also his
[Notebook](https://github.com/dmnfarrell/teaching/blob/master/geo/maps_python.ipynb)
The data is provided by [Our World in Data](https://ourworldindata.org/). Take a look at their web
site if you would like some inspiration for designing beautifull, interactive dashboards.
You can find the source
data on [GitHub](https://github.com/owid/owid-datasets/tree/master/datasets).
"""
import json
import pathlib
from typing import Optional
import geopandas as gpd
import pandas as pd
import streamlit as st
from bokeh.models import ColorBar, GeoJSONDataSource, LinearColorMapper
from bokeh.palettes import brewer # pylint: disable=no-name-in-module
from bokeh.plotting import figure
FILE_DIR = pathlib.Path.cwd() / "gallery/owid_dashboard"
SHAPEFILE = FILE_DIR / "data/ne_110m_admin_0_countries.shp"
OWIDDATASETS_FILE = FILE_DIR / "data/owid_datasets.csv"
INFO = """
#### Ideas for improvement
- Add tooltips to the map
- Speed up the dashboard by not downloading each dataset from the source.
- Add a seperate *tab* with a line or bar chart of the data
- Add functionality to select between maps generated by Bokeh, HoloViews, HvPlot, Plotly and Vega
in order to compare how they work.
- Add a button to maximize the interactive part of the dashboard.
- Change the year selection to a [player widget](https://panel.pyviz.org/reference/widgets/Player.html#gallery-player).
- Add a button to download an interactive embedding of the plot.
This example uses [geopandas](http://geopandas.org/).
On Windows the easiest is to `conda install geopandas`. If you wan't to `pip install geopandas` on
Windows then please follow the
[using-geopandas-windows](https://geoffboeing.com/2014/09/using-geopandas-windows/) article.
"""
class OwidDashboard:
"""A Dashboard showing the Owid World Data like 'Annual CO2 Emissions'
Args:
shape_data (Optional[gpd.geodataframe.GeoDataFrame], optional): The Map shape data.
Defaults to None.
owid_data_sets (Optional[pd.DataFrame], optional): A DataFrame listing the available
datasets. Defaults to None.
"""
def __init__(
self,
shape_data: Optional[gpd.geodataframe.GeoDataFrame] = None,
owid_data_sets: Optional[pd.DataFrame] = None,
):
if not shape_data:
self.shape_data = self.get_shape_data()
else:
self.shape_data = shape_data
if not owid_data_sets:
self.owid_data_sets = self.get_owid_data_sets()
else:
self.owid_data_sets = owid_data_sets
self.dataset_names = list(self.owid_data_sets.index)
self.dataset_name = self.dataset_names[0]
self.year_range = (1950, 2018)
self.year = 2010
def map_plot(self):
"""The Bokeh Map"""
return self._map_plot(self.dataset_name, self.year)
def _map_plot(self, name: str, year: int):
if name and year:
shape_data, key = self.get_owid_data(
self.owid_data_sets, self.shape_data, name=name, year=year
)
return self.get_map_plot(shape_data, key, key)
return None
@st.cache
def download_link(self) -> str:
"""A HTML string to enable download of the data
Returns:
str: A HTML link
"""
download_icon = (
''
)
if self.dataset_name:
download_link = self.owid_data_sets.loc[self.dataset_name].url
else:
download_link = ""
return f'{download_icon}'
@staticmethod
@st.cache
def get_shape_data() -> gpd.geodataframe.GeoDataFrame:
"""Loads the shape data of the map"""
shape_data = gpd.read_file(SHAPEFILE)[["ADMIN", "ADM0_A3", "geometry"]]
shape_data.columns = ["country", "country_code", "geometry"]
shape_data = shape_data.drop(shape_data.index[159])
return shape_data
@staticmethod
@st.cache
def get_owid_data_sets() -> pd.DataFrame:
"""The list of Owid data sets
Returns:
pd.DataFrame: A DataFrame with columns=["name", "url"] and index=["name"]
"""
return pd.read_csv(OWIDDATASETS_FILE).set_index("name")
@staticmethod
@st.cache
def get_owid_df(url) -> pd.DataFrame:
"""The DataFrame of data from Owid"""
return pd.read_csv(url)
@classmethod
def get_owid_data( # pylint: disable=too-many-arguments
cls,
owid_data_sets: pd.DataFrame,
shape_data: gpd.geodataframe.GeoDataFrame,
name: str,
year: Optional[int] = None,
key: Optional[str] = None,
) -> gpd.geodataframe.GeoDataFrame:
"""An Owid Data Set combined with the shape_data
Args:
owid_data_sets (pd.DataFrame): The list of Owid Data Sets
shape_data (gpd.geodataframe.GeoDataFrame): The shape data for the map
name (str): The name of the Owid Data Set to look up.
year (Optional[int], optional): A year to filter to. Defaults to None.
key (Optional[str], optional): The name of column containing the values.
Defaults to None.
Returns:
gpd.geodataframe.GeoDataFrame: The Owid Data Sets merged with the shape data
"""
url = owid_data_sets.loc[name].url
owid_data = cls.get_owid_df(url)
if year is not None:
owid_data = owid_data[owid_data["Year"] == year]
merged = shape_data.merge(owid_data, left_on="country", right_on="Entity", how="left")
if key is None:
key = owid_data.columns[2]
merged[key] = merged[key].fillna(0)
return merged, key
@staticmethod
def to_geo_json_data_source(data: gpd.geodataframe.GeoDataFrame) -> GeoJSONDataSource:
"""Convert the data to a GeoJSONDataSource
Args:
data (gpd.geodataframe.GeoDataFrame): The data
Returns:
GeoJSONDataSource: The resulting GeoJson Data
"""
json_data = json.dumps(json.loads(data.to_json()))
return GeoJSONDataSource(geojson=json_data)
@classmethod
def get_map_plot(
cls,
shape_data: gpd.geodataframe.GeoDataFrame,
value_column: Optional[str] = None,
title: str = "",
):
"""Plot GeoDataFrame as a map
"""
geosource = cls.to_geo_json_data_source(shape_data)
palette = brewer["OrRd"][8]
palette = palette[::-1]
vals = shape_data[value_column]
color_mapper = LinearColorMapper(palette=palette, low=vals.min(), high=vals.max())
color_bar = ColorBar(
color_mapper=color_mapper,
label_standoff=8,
height=20,
location=(0, 0),
orientation="horizontal",
)
plot = figure(title=title, plot_height=500, tools="", sizing_mode="stretch_width")
plot.xgrid.grid_line_color = None
plot.ygrid.grid_line_color = None
plot.patches(
"xs",
"ys",
source=geosource,
fill_alpha=1,
line_width=0.5,
line_color="black",
fill_color={"field": value_column, "transform": color_mapper},
)
plot.add_layout(color_bar, "below")
plot.toolbar.logo = None
return plot
def view(self):
"""Map dashboard"""
st.markdown(__doc__)
self.dataset_name = st.selectbox("Select Data Set", options=self.dataset_names, index=0)
self.year = st.slider(
"Select Year",
min_value=self.year_range[0],
max_value=self.year_range[1],
value=self.year,
)
st.bokeh_chart(self.map_plot())
st.markdown(INFO)
st.markdown(self.download_link(), unsafe_allow_html=True)
OwidDashboard().view()