Bokeh is an open-source Python library for creating interactive visualisations that can be embedded into web pages. Here’s a Jupyter Notebook featuring an interactive map I made using Bokeh, showing Properties in Care in Scotland. Hovering over each point on the map will show you the property’s metadata, including its name, height and coordinates. On the right, there are controls to pan, zoom and reset the view, and also the option to save the current view as a PNG image.

Static view of the Bokeh plot.
Static view of the Bokeh plot.

Installing dependencies

The requirements are GeoPandas, JupyterLab, and Bokeh, as well as their dependencies and the jupyter_bokeh extension.

Create and activate a virtual environment, and install all dependencies:

  • Option 1 - using venv

    Make sure Python 3 is installed, then run the following:

    on Linux:

    python3 -m venv env
    source env/bin/activate
    python -m pip install geopandas jupyterlab jupyter_bokeh
    

    on Windows:

    py -m venv env
    .\env\Scripts\activate
    py -m pip install geopandas jupyterlab jupyter_bokeh
    
  • Option 2 - using Anaconda

    Install Anaconda (I recommend the lightweight Miniconda), then run the following:

    conda create --name bokehplot python=3 geopandas jupyterlab jupyter_bokeh
    conda activate bokehplot
    

Data

The Properties in Care spatial data is available for download on Historic Environment Scotland’s portal. This is an open-source point Shapefile in the Ordnance Survey British National Grid coordinate reference system (EPSG:27700)1.

Info
The data is available under the terms of the Open Government Licence (OGL) v3.0. The terms of this licence are compatible with a Creative Commons Attribution 4.0 International Public License (CC-BY-4.0). The following attribution statement is required: “Contains Historic Environment Scotland data. (C) Historic Environment Scotland - Scottish Charity No. SC045925 2021.”

Download the dataset from the link above and unzip the contents to a convenient location. Alternatively, use the Requests Python library to download it automatically.

Install Requests as follows:

python -m pip install requests  # using pip on Linux
py -m pip install requests  # using pip on Windows
conda install requests  # using conda

Then, use the following snippet to download the data to the data/pic/ directory:

# import libraries
import os
from io import BytesIO
from zipfile import ZipFile, ZipFile
import requests

# define download URL
url = "https://inspire.hes.scot/AtomService/DATA/pic.zip"

# create directory to store files
os.makedirs("data/pic/", exist_ok=True)

# download
r = requests.get(url)
if r.status_code == 200:
    try:
        z = ZipFile(BytesIO(r.content))
        z.extractall("data/pic/")
        print("Data successfully downloaded!")
    except BadZipFile:
        print("Data not extracted! Bad zip file.")
else:
    print("Error! Data not downloaded. Status code:", r.status_code)

JupyterLab set-up

Launch JupyterLab by running jupyter lab in your virtual environment. Then, create a new blank Python 3 notebook and import the following libraries:

import geopandas as gpd
from bokeh.io import output_notebook
from bokeh.models import CategoricalColorMapper, GeoJSONDataSource
from bokeh.palettes import viridis
from bokeh.plotting import figure, show
from bokeh.tile_providers import CARTODBPOSITRON_RETINA, get_provider

To generate inline plots, use the following:

output_notebook()

Additionally, configure the background map tile provider for the plot:

tile_provider = get_provider(CARTODBPOSITRON_RETINA)

Reading data

Import the Properties in Care data using GeoPandas:

data = gpd.read_file("data/pic/properties_in_care.shp")

Since this data is in EPSG:27700, the coordinates must be transformed into the Web Mercator projection (EPSG:3857).

data = data.to_crs(3857)

Note
The coordinates must be transformed into Web Mercator projection, as it is the projection used by map providers, such as OpenStreetMap and Google Maps, and if any of these providers are used as tiles for the plot without transforming the data, the mappings made by Bokeh will be inaccurate. See Wikipedia for more information about EPSG codes.

The data must now be converted into GeoJSON to be used with Bokeh:

geo_source = GeoJSONDataSource(geojson=data.to_json())

Creating the plot

The first step in plotting the data is to configure the plot colours. Here, a unique colour will be used for each local authority. I first got a list of local authorities in the dataset, and then applied the Viridis colour palette on it:

const = list(set(data["LOCAL_AUTH"]))
palette = viridis(len(const))
color_map = CategoricalColorMapper(factors=const, palette=palette)

I defined the plot title as well as tooltips to appear when hovering over a data point:

TITLE = (
    "Properties in Care in Scotland. © Historic Environment Scotland 2021."
)
TOOLTIPS = [
    ("NAME", "@PIC_NAME"),
    ("LOCAL_AUTH", "@LOCAL_AUTH"),
    ("COORDINATES", "(@X, @Y)"),
    ("ID", "@PIC_ID")
]

Then, I defined the plot figure. The axes types are defined as mercator, so that the axes use latitudes and longitudes instead2:

p = figure(
    title=TITLE,
    tools="wheel_zoom, pan, reset, hover, save",
    x_axis_location=None,
    y_axis_location=None,
    tooltips=TOOLTIPS,
    x_axis_type="mercator",
    y_axis_type="mercator"
)
p.grid.grid_line_color = None
p.hover.point_policy = "follow_mouse"

Add the data points using circle, and add the tile provider. I’m using CARTODBPOSITRON_RETINA, which uses OpenStreetMap and CartoDB Tile Service at retina resolution.

p.circle(
    "x",
    "y",
    source=geo_source,
    size=5,
    line_width=0,
    fill_color={"field": "LOCAL_AUTH", "transform": color_map}
)
p.add_tile(tile_provider)

Finally, display the plot:

show(p)

That’s it! 😄 Also check out this notebook, where I’ve mapped simple polygons of Constituencies in Greater London using a similar procedure.

Footnotes

Leave a comment

Your email address will not be published. Required fields are marked *.

Loading...