Interactive maps with Bokeh

Our ultimate goal today is to learn few concepts how we can produce nice looking interactive maps using Geopandas and Bokeh such as:

Accessibility by PT to Helsinki City center

Simple interactive point plot

First, we learn the basic logic of plotting in Bokeh by making a simple interactive plot with few points.

Import necessary functionalities from bokeh.

from bokeh.plotting import figure, save

First we need to initialize our plot by calling the figure object.

# Initialize the plot (p) and give it a title
In [1]: p = figure(title="My first interactive plot!")

# Let's see what it is
In [2]: p
Out[2]: 
bokeh.plotting.figure.Figure(
    id='e706d4d8-9cde-424b-9241-9084743210a7',
    above=[],
    aspect_scale=1,
    background_fill_alpha={'value': 1.0},
    background_fill_color={'value': '#ffffff'},
    below=[bokeh.models.axes.LinearAxis(
         id='7edb88a8-4276-48f6-97f3-84327df3e9d4',
         axis_label='',
         axis_label_standoff=5,
         axis_label_text_align='left',
         axis_label_text_alpha={'value': 1.0},
         axis_label_text_baseline='bottom',
         axis_label_text_color={'value': '#444444'},
         axis_label_text_font='helvetica',
         axis_label_text_font_size={'value': '10pt'},
         axis_label_text_font_style='italic',
         axis_label_text_line_height=1.2,
         axis_line_alpha={'value': 1.0},
         axis_line_cap='butt',
         axis_line_color={'value': 'black'},
         axis_line_dash=[],
         axis_line_dash_offset=0,
         axis_line_join='bevel',
         axis_line_width={'value': 1},
         bounds='auto',
         fixed_location=None,
         formatter=bokeh.models.formatters.BasicTickFormatter(
             id='d2caa97a-158d-4dad-842f-d91eb98d157a',
             js_event_callbacks={},
             js_property_callbacks={},
             name=None,
             power_limit_high=5,
             power_limit_low=-3,
             precision='auto',
             subscribed_events=[],
             tags=[],
             use_scientific=True),
         js_event_callbacks={},
         js_property_callbacks={},
         level='overlay',
         major_label_orientation='horizontal',
         major_label_overrides={},
         major_label_standoff=5,
         major_label_text_align='center',
         major_label_text_alpha={'value': 1.0},
         major_label_text_baseline='alphabetic',
         major_label_text_color={'value': '#444444'},
         major_label_text_font='helvetica',
         major_label_text_font_size={'value': '8pt'},
         major_label_text_font_style='normal',
         major_label_text_line_height=1.2,
         major_tick_in=2,
         major_tick_line_alpha={'value': 1.0},
         major_tick_line_cap='butt',
         major_tick_line_color={'value': 'black'},
         major_tick_line_dash=[],
         major_tick_line_dash_offset=0,
         major_tick_line_join='bevel',
         major_tick_line_width={'value': 1},
         major_tick_out=6,
         minor_tick_in=0,
         minor_tick_line_alpha={'value': 1.0},
         minor_tick_line_cap='butt',
         minor_tick_line_color={'value': 'black'},
         minor_tick_line_dash=[],
         minor_tick_line_dash_offset=0,
         minor_tick_line_join='bevel',
         minor_tick_line_width={'value': 1},
         minor_tick_out=4,
         name=None,
         plot=bokeh.plotting.figure.Figure(id='e706d4d8-9cde-424b-9241-9084743210a7', ...),
         subscribed_events=[],
         tags=[],
         ticker=bokeh.models.tickers.BasicTicker(
             id='dd847e5c-69ee-4462-a854-d9fedade805a',
             base=10.0,
             desired_num_ticks=6,
             js_event_callbacks={},
             js_property_callbacks={},
             mantissas=[1, 2, 5],
             max_interval=None,
             min_interval=0.0,
             name=None,
             num_minor_ticks=5,
             subscribed_events=[],
             tags=[]),
         visible=True,
         x_range_name='default',
         y_range_name='default')],
    border_fill_alpha={'value': 1.0},
    border_fill_color={'value': '#ffffff'},
    css_classes=[],
    disabled=False,
    extra_x_ranges={},
    extra_y_ranges={},
    h_symmetry=True,
    height=None,
    hidpi=True,
    js_event_callbacks={},
    js_property_callbacks={},
    left=[bokeh.models.axes.LinearAxis(
         id='42f393ac-579d-4af9-8b06-87b2d93f2c20',
         axis_label='',
         axis_label_standoff=5,
         axis_label_text_align='left',
         axis_label_text_alpha={'value': 1.0},
         axis_label_text_baseline='bottom',
         axis_label_text_color={'value': '#444444'},
         axis_label_text_font='helvetica',
         axis_label_text_font_size={'value': '10pt'},
         axis_label_text_font_style='italic',
         axis_label_text_line_height=1.2,
         axis_line_alpha={'value': 1.0},
         axis_line_cap='butt',
         axis_line_color={'value': 'black'},
         axis_line_dash=[],
         axis_line_dash_offset=0,
         axis_line_join='bevel',
         axis_line_width={'value': 1},
         bounds='auto',
         fixed_location=None,
         formatter=bokeh.models.formatters.BasicTickFormatter(
             id='508a6ac5-5a00-4e21-be05-6db4c97b6be6',
             js_event_callbacks={},
             js_property_callbacks={},
             name=None,
             power_limit_high=5,
             power_limit_low=-3,
             precision='auto',
             subscribed_events=[],
             tags=[],
             use_scientific=True),
         js_event_callbacks={},
         js_property_callbacks={},
         level='overlay',
         major_label_orientation='horizontal',
         major_label_overrides={},
         major_label_standoff=5,
         major_label_text_align='center',
         major_label_text_alpha={'value': 1.0},
         major_label_text_baseline='alphabetic',
         major_label_text_color={'value': '#444444'},
         major_label_text_font='helvetica',
         major_label_text_font_size={'value': '8pt'},
         major_label_text_font_style='normal',
         major_label_text_line_height=1.2,
         major_tick_in=2,
         major_tick_line_alpha={'value': 1.0},
         major_tick_line_cap='butt',
         major_tick_line_color={'value': 'black'},
         major_tick_line_dash=[],
         major_tick_line_dash_offset=0,
         major_tick_line_join='bevel',
         major_tick_line_width={'value': 1},
         major_tick_out=6,
         minor_tick_in=0,
         minor_tick_line_alpha={'value': 1.0},
         minor_tick_line_cap='butt',
         minor_tick_line_color={'value': 'black'},
         minor_tick_line_dash=[],
         minor_tick_line_dash_offset=0,
         minor_tick_line_join='bevel',
         minor_tick_line_width={'value': 1},
         minor_tick_out=4,
         name=None,
         plot=bokeh.plotting.figure.Figure(id='e706d4d8-9cde-424b-9241-9084743210a7', ...),
         subscribed_events=[],
         tags=[],
         ticker=bokeh.models.tickers.BasicTicker(
             id='e0cf74ce-08c7-493b-8176-9ae94709ab2b',
             base=10.0,
             desired_num_ticks=6,
             js_event_callbacks={},
             js_property_callbacks={},
             mantissas=[1, 2, 5],
             max_interval=None,
             min_interval=0.0,
             name=None,
             num_minor_ticks=5,
             subscribed_events=[],
             tags=[]),
         visible=True,
         x_range_name='default',
         y_range_name='default')],
    lod_factor=10,
    lod_interval=300,
    lod_threshold=2000,
    lod_timeout=500,
    match_aspect=False,
    min_border=5,
    min_border_bottom=None,
    min_border_left=None,
    min_border_right=None,
    min_border_top=None,
    name=None,
    outline_line_alpha={'value': 1.0},
    outline_line_cap='butt',
    outline_line_color={'value': '#e5e5e5'},
    outline_line_dash=[],
    outline_line_dash_offset=0,
    outline_line_join='bevel',
    outline_line_width={'value': 1},
    output_backend='canvas',
    plot_height=600,
    plot_width=600,
    renderers=[bokeh.models.axes.LinearAxis(
         id='7edb88a8-4276-48f6-97f3-84327df3e9d4',
         axis_label='',
         axis_label_standoff=5,
         axis_label_text_align='left',
         axis_label_text_alpha={'value': 1.0},
         axis_label_text_baseline='bottom',
         axis_label_text_color={'value': '#444444'},
         axis_label_text_font='helvetica',
         axis_label_text_font_size={'value': '10pt'},
         axis_label_text_font_style='italic',
         axis_label_text_line_height=1.2,
         axis_line_alpha={'value': 1.0},
         axis_line_cap='butt',
         axis_line_color={'value': 'black'},
         axis_line_dash=[],
         axis_line_dash_offset=0,
         axis_line_join='bevel',
         axis_line_width={'value': 1},
         bounds='auto',
         fixed_location=None,
         formatter=bokeh.models.formatters.BasicTickFormatter(
             id='d2caa97a-158d-4dad-842f-d91eb98d157a',
             js_event_callbacks={},
             js_property_callbacks={},
             name=None,
             power_limit_high=5,
             power_limit_low=-3,
             precision='auto',
             subscribed_events=[],
             tags=[],
             use_scientific=True),
         js_event_callbacks={},
         js_property_callbacks={},
         level='overlay',
         major_label_orientation='horizontal',
         major_label_overrides={},
         major_label_standoff=5,
         major_label_text_align='center',
         major_label_text_alpha={'value': 1.0},
         major_label_text_baseline='alphabetic',
         major_label_text_color={'value': '#444444'},
         major_label_text_font='helvetica',
         major_label_text_font_size={'value': '8pt'},
         major_label_text_font_style='normal',
         major_label_text_line_height=1.2,
         major_tick_in=2,
         major_tick_line_alpha={'value': 1.0},
         major_tick_line_cap='butt',
         major_tick_line_color={'value': 'black'},
         major_tick_line_dash=[],
         major_tick_line_dash_offset=0,
         major_tick_line_join='bevel',
         major_tick_line_width={'value': 1},
         major_tick_out=6,
         minor_tick_in=0,
         minor_tick_line_alpha={'value': 1.0},
         minor_tick_line_cap='butt',
         minor_tick_line_color={'value': 'black'},
         minor_tick_line_dash=[],
         minor_tick_line_dash_offset=0,
         minor_tick_line_join='bevel',
         minor_tick_line_width={'value': 1},
         minor_tick_out=4,
         name=None,
         plot=bokeh.plotting.figure.Figure(id='e706d4d8-9cde-424b-9241-9084743210a7', ...),
         subscribed_events=[],
         tags=[],
         ticker=bokeh.models.tickers.BasicTicker(
             id='dd847e5c-69ee-4462-a854-d9fedade805a',
             base=10.0,
             desired_num_ticks=6,
             js_event_callbacks={},
             js_property_callbacks={},
             mantissas=[1, 2, 5],
             max_interval=None,
             min_interval=0.0,
             name=None,
             num_minor_ticks=5,
             subscribed_events=[],
             tags=[]),
         visible=True,
         x_range_name='default',
         y_range_name='default'),
     bokeh.models.grids.Grid(
         id='b5b3566a-d287-4253-ba0b-0c56f1c60459',
         band_fill_alpha={'value': 0},
         band_fill_color={'value': None},
         bounds='auto',
         dimension=0,
         grid_line_alpha={'value': 1.0},
         grid_line_cap='butt',
         grid_line_color={'value': '#e5e5e5'},
         grid_line_dash=[],
         grid_line_dash_offset=0,
         grid_line_join='bevel',
         grid_line_width={'value': 1},
         js_event_callbacks={},
         js_property_callbacks={},
         level='underlay',
         minor_grid_line_alpha={'value': 1.0},
         minor_grid_line_cap='butt',
         minor_grid_line_color={'value': None},
         minor_grid_line_dash=[],
         minor_grid_line_dash_offset=0,
         minor_grid_line_join='bevel',
         minor_grid_line_width={'value': 1},
         name=None,
         plot=bokeh.plotting.figure.Figure(id='e706d4d8-9cde-424b-9241-9084743210a7', ...),
         subscribed_events=[],
         tags=[],
         ticker=bokeh.models.tickers.BasicTicker(
             id='dd847e5c-69ee-4462-a854-d9fedade805a',
             base=10.0,
             desired_num_ticks=6,
             js_event_callbacks={},
             js_property_callbacks={},
             mantissas=[1, 2, 5],
             max_interval=None,
             min_interval=0.0,
             name=None,
             num_minor_ticks=5,
             subscribed_events=[],
             tags=[]),
         visible=True,
         x_range_name='default',
         y_range_name='default'),
     bokeh.models.axes.LinearAxis(
         id='42f393ac-579d-4af9-8b06-87b2d93f2c20',
         axis_label='',
         axis_label_standoff=5,
         axis_label_text_align='left',
         axis_label_text_alpha={'value': 1.0},
         axis_label_text_baseline='bottom',
         axis_label_text_color={'value': '#444444'},
         axis_label_text_font='helvetica',
         axis_label_text_font_size={'value': '10pt'},
         axis_label_text_font_style='italic',
         axis_label_text_line_height=1.2,
         axis_line_alpha={'value': 1.0},
         axis_line_cap='butt',
         axis_line_color={'value': 'black'},
         axis_line_dash=[],
         axis_line_dash_offset=0,
         axis_line_join='bevel',
         axis_line_width={'value': 1},
         bounds='auto',
         fixed_location=None,
         formatter=bokeh.models.formatters.BasicTickFormatter(
             id='508a6ac5-5a00-4e21-be05-6db4c97b6be6',
             js_event_callbacks={},
             js_property_callbacks={},
             name=None,
             power_limit_high=5,
             power_limit_low=-3,
             precision='auto',
             subscribed_events=[],
             tags=[],
             use_scientific=True),
         js_event_callbacks={},
         js_property_callbacks={},
         level='overlay',
         major_label_orientation='horizontal',
         major_label_overrides={},
         major_label_standoff=5,
         major_label_text_align='center',
         major_label_text_alpha={'value': 1.0},
         major_label_text_baseline='alphabetic',
         major_label_text_color={'value': '#444444'},
         major_label_text_font='helvetica',
         major_label_text_font_size={'value': '8pt'},
         major_label_text_font_style='normal',
         major_label_text_line_height=1.2,
         major_tick_in=2,
         major_tick_line_alpha={'value': 1.0},
         major_tick_line_cap='butt',
         major_tick_line_color={'value': 'black'},
         major_tick_line_dash=[],
         major_tick_line_dash_offset=0,
         major_tick_line_join='bevel',
         major_tick_line_width={'value': 1},
         major_tick_out=6,
         minor_tick_in=0,
         minor_tick_line_alpha={'value': 1.0},
         minor_tick_line_cap='butt',
         minor_tick_line_color={'value': 'black'},
         minor_tick_line_dash=[],
         minor_tick_line_dash_offset=0,
         minor_tick_line_join='bevel',
         minor_tick_line_width={'value': 1},
         minor_tick_out=4,
         name=None,
         plot=bokeh.plotting.figure.Figure(id='e706d4d8-9cde-424b-9241-9084743210a7', ...),
         subscribed_events=[],
         tags=[],
         ticker=bokeh.models.tickers.BasicTicker(
             id='e0cf74ce-08c7-493b-8176-9ae94709ab2b',
             base=10.0,
             desired_num_ticks=6,
             js_event_callbacks={},
             js_property_callbacks={},
             mantissas=[1, 2, 5],
             max_interval=None,
             min_interval=0.0,
             name=None,
             num_minor_ticks=5,
             subscribed_events=[],
             tags=[]),
         visible=True,
         x_range_name='default',
         y_range_name='default'),
     bokeh.models.grids.Grid(
         id='6a7ddf8b-88b7-44d5-aee7-b5a4b241b8d4',
         band_fill_alpha={'value': 0},
         band_fill_color={'value': None},
         bounds='auto',
         dimension=1,
         grid_line_alpha={'value': 1.0},
         grid_line_cap='butt',
         grid_line_color={'value': '#e5e5e5'},
         grid_line_dash=[],
         grid_line_dash_offset=0,
         grid_line_join='bevel',
         grid_line_width={'value': 1},
         js_event_callbacks={},
         js_property_callbacks={},
         level='underlay',
         minor_grid_line_alpha={'value': 1.0},
         minor_grid_line_cap='butt',
         minor_grid_line_color={'value': None},
         minor_grid_line_dash=[],
         minor_grid_line_dash_offset=0,
         minor_grid_line_join='bevel',
         minor_grid_line_width={'value': 1},
         name=None,
         plot=bokeh.plotting.figure.Figure(id='e706d4d8-9cde-424b-9241-9084743210a7', ...),
         subscribed_events=[],
         tags=[],
         ticker=bokeh.models.tickers.BasicTicker(
             id='e0cf74ce-08c7-493b-8176-9ae94709ab2b',
             base=10.0,
             desired_num_ticks=6,
             js_event_callbacks={},
             js_property_callbacks={},
             mantissas=[1, 2, 5],
             max_interval=None,
             min_interval=0.0,
             name=None,
             num_minor_ticks=5,
             subscribed_events=[],
             tags=[]),
         visible=True,
         x_range_name='default',
         y_range_name='default'),
     bokeh.models.annotations.BoxAnnotation(
         id='8f534a09-07c4-4553-afc1-b4ebcd4cc6e9',
         bottom=None,
         bottom_units='screen',
         fill_alpha={'value': 0.5},
         fill_color={'value': 'lightgrey'},
         js_event_callbacks={},
         js_property_callbacks={},
         left=None,
         left_units='screen',
         level='overlay',
         line_alpha={'value': 1.0},
         line_cap='butt',
         line_color={'value': 'black'},
         line_dash=[4, 4],
         line_dash_offset=0,
         line_join='bevel',
         line_width={'value': 2},
         name=None,
         plot=None,
         render_mode='css',
         right=None,
         right_units='screen',
         subscribed_events=[],
         tags=[],
         top=None,
         top_units='screen',
         visible=True,
         x_range_name='default',
         y_range_name='default')],
    right=[],
    sizing_mode='fixed',
    subscribed_events=[],
    tags=[],
    title=bokeh.models.annotations.Title(
        id='191846ca-f0d4-4ecf-90b0-c24525e61bde',
        align='left',
        background_fill_alpha={'value': 1.0},
        background_fill_color={'value': None},
        border_line_alpha={'value': 1.0},
        border_line_cap='butt',
        border_line_color={'value': None},
        border_line_dash=[],
        border_line_dash_offset=0,
        border_line_join='bevel',
        border_line_width={'value': 1},
        js_event_callbacks={},
        js_property_callbacks={},
        level='annotation',
        name=None,
        offset=0,
        plot=None,
        render_mode='canvas',
        subscribed_events=[],
        tags=[],
        text='My first interactive plot!',
        text_alpha={'value': 1.0},
        text_color={'value': '#444444'},
        text_font='helvetica',
        text_font_size={'value': '10pt'},
        text_font_style='bold',
        vertical_align='bottom',
        visible=True),
    title_location='above',
    toolbar=bokeh.models.tools.Toolbar(
        id='b0dcb649-851c-4e69-9752-029fa66c6285',
        active_drag='auto',
        active_inspect='auto',
        active_multi=None,
        active_scroll='auto',
        active_tap='auto',
        js_event_callbacks={},
        js_property_callbacks={},
        logo='normal',
        name=None,
        subscribed_events=[],
        tags=[],
        tools=[bokeh.models.tools.PanTool(
             id='e2a4c9f9-f2be-42a1-8009-c6503d7db731',
             dimensions='both',
             js_event_callbacks={},
             js_property_callbacks={},
             name=None,
             subscribed_events=[],
             tags=[]),
         bokeh.models.tools.WheelZoomTool(
             id='fc8a6dbc-cc47-4b30-9b80-b6f99fcdfe9c',
             dimensions='both',
             js_event_callbacks={},
             js_property_callbacks={},
             maintain_focus=True,
             name=None,
             speed=0.0016666666666666668,
             subscribed_events=[],
             tags=[],
             zoom_on_axis=True),
         bokeh.models.tools.BoxZoomTool(
             id='39ee2ef3-acdb-4918-b1eb-ddde9dfa38d6',
             dimensions='both',
             js_event_callbacks={},
             js_property_callbacks={},
             match_aspect=False,
             name=None,
             origin='corner',
             overlay=bokeh.models.annotations.BoxAnnotation(
                 id='8f534a09-07c4-4553-afc1-b4ebcd4cc6e9',
                 bottom=None,
                 bottom_units='screen',
                 fill_alpha={'value': 0.5},
                 fill_color={'value': 'lightgrey'},
                 js_event_callbacks={},
                 js_property_callbacks={},
                 left=None,
                 left_units='screen',
                 level='overlay',
                 line_alpha={'value': 1.0},
                 line_cap='butt',
                 line_color={'value': 'black'},
                 line_dash=[4, 4],
                 line_dash_offset=0,
                 line_join='bevel',
                 line_width={'value': 2},
                 name=None,
                 plot=None,
                 render_mode='css',
                 right=None,
                 right_units='screen',
                 subscribed_events=[],
                 tags=[],
                 top=None,
                 top_units='screen',
                 visible=True,
                 x_range_name='default',
                 y_range_name='default'),
             subscribed_events=[],
             tags=[]),
         bokeh.models.tools.SaveTool(
             id='da233d58-71ad-4630-89ea-e8ec4c5a3154',
             js_event_callbacks={},
             js_property_callbacks={},
             name=None,
             subscribed_events=[],
             tags=[]),
         bokeh.models.tools.ResetTool(
             id='8f1ea3b8-6aca-4bad-afbc-0ff49f3e5a6e',
             js_event_callbacks={},
             js_property_callbacks={},
             name=None,
             subscribed_events=[],
             tags=[]),
         bokeh.models.tools.HelpTool(
             id='6a8280c5-2194-47de-8848-007a375f4237',
             help_tooltip='Click the question mark to learn more about Bokeh plot tools.',
             js_event_callbacks={},
             js_property_callbacks={},
             name=None,
             redirect='https://bokeh.pydata.org/en/latest/docs/user_guide/tools.html#built-in-tools',
             subscribed_events=[],
             tags=[])]),
    toolbar_location='right',
    toolbar_sticky=True,
    v_symmetry=False,
    width=None,
    x_range=bokeh.models.ranges.DataRange1d(
        id='feda0cf7-6a08-4613-9dc5-b4f425574d00',
        bounds=None,
        callback=None,
        default_span=2.0,
        end=None,
        flipped=False,
        follow=None,
        follow_interval=None,
        js_event_callbacks={},
        js_property_callbacks={},
        max_interval=None,
        min_interval=None,
        name=None,
        names=[],
        range_padding=0.1,
        range_padding_units='percent',
        renderers=[],
        start=None,
        subscribed_events=[],
        tags=[]),
    x_scale=bokeh.models.scales.LinearScale(
        id='742f4c04-9f4e-4b65-b4b1-d3215a6be7a0',
        js_event_callbacks={},
        js_property_callbacks={},
        name=None,
        subscribed_events=[],
        tags=[]),
    y_range=bokeh.models.ranges.DataRange1d(
        id='153907f3-d7f1-4b6a-a3d8-336f7108174e',
        bounds=None,
        callback=None,
        default_span=2.0,
        end=None,
        flipped=False,
        follow=None,
        follow_interval=None,
        js_event_callbacks={},
        js_property_callbacks={},
        max_interval=None,
        min_interval=None,
        name=None,
        names=[],
        range_padding=0.1,
        range_padding_units='percent',
        renderers=[],
        start=None,
        subscribed_events=[],
        tags=[]),
    y_scale=bokeh.models.scales.LinearScale(
        id='1206bc4c-2d26-4404-bf6d-fb1bada8c9e1',
        js_event_callbacks={},
        js_property_callbacks={},
        name=None,
        subscribed_events=[],
        tags=[]))

Next we need to create lists of x and y coordinates that we want to plot.

# Create a list of x-coordinates
In [3]: x_coords = [0,1,2,3,4]

# Create a list of y-coordinates
In [4]: y_coords = [5,4,1,2,0]

Note

In Bokeh drawing points, lines or polygons are always done using list(s) of x and y coordinates.

Now we can plot those as points using a .circle() -object. Let’s give it a red color and size of 10.

# Plot the points
In [5]: p.circle(x=x_coords, y=y_coords, size=10, color="red")
Out[5]: 
bokeh.models.renderers.GlyphRenderer(
    id='bd0d1ce9-cfa7-4215-bc45-401388270066',
    data_source=bokeh.models.sources.ColumnDataSource(
        id='b2329f16-b24b-44e7-ac51-27596ef55354',
        callback=None,
        data={'x': [0, 1, 2, 3, 4], 'y': [5, 4, 1, 2, 0]},
        js_event_callbacks={},
        js_property_callbacks={},
        name=None,
        selected=bokeh.models.selections.Selection(
            id='edb967f6-4724-4891-9d97-451345a54e65',
            indices=[],
            js_event_callbacks={},
            js_property_callbacks={},
            line_indices=[],
            multiline_indices={},
            name=None,
            subscribed_events=[],
            tags=[]),
        selection_policy=bokeh.models.selections.UnionRenderers(
            id='7db9ac9c-4cf0-4a9d-afc8-e89d803d6a39',
            js_event_callbacks={},
            js_property_callbacks={},
            name=None,
            subscribed_events=[],
            tags=[]),
        subscribed_events=[],
        tags=[]),
    glyph=bokeh.models.markers.Circle(
        id='32c645d7-76af-4903-913f-9e68517ac087',
        angle={'units': 'rad', 'value': 0.0},
        fill_alpha={'value': 1.0},
        fill_color={'value': 'red'},
        js_event_callbacks={},
        js_property_callbacks={},
        line_alpha={'value': 1.0},
        line_cap='butt',
        line_color={'value': 'red'},
        line_dash=[],
        line_dash_offset=0,
        line_join='bevel',
        line_width={'value': 1},
        name=None,
        radius=None,
        radius_dimension='x',
        size={'units': 'screen', 'value': 10},
        subscribed_events=[],
        tags=[],
        x={'field': 'x'},
        y={'field': 'y'}),
    hover_glyph=None,
    js_event_callbacks={},
    js_property_callbacks={},
    level='glyph',
    muted=False,
    muted_glyph=None,
    name=None,
    nonselection_glyph=bokeh.models.markers.Circle(
        id='6fbf2680-2b77-4d55-b899-8bd5744d9937',
        angle={'units': 'rad', 'value': 0.0},
        fill_alpha={'value': 0.1},
        fill_color={'value': '#1f77b4'},
        js_event_callbacks={},
        js_property_callbacks={},
        line_alpha={'value': 0.1},
        line_cap='butt',
        line_color={'value': '#1f77b4'},
        line_dash=[],
        line_dash_offset=0,
        line_join='bevel',
        line_width={'value': 1},
        name=None,
        radius=None,
        radius_dimension='x',
        size={'units': 'screen', 'value': 10},
        subscribed_events=[],
        tags=[],
        x={'field': 'x'},
        y={'field': 'y'}),
    selection_glyph=None,
    subscribed_events=[],
    tags=[],
    view=bokeh.models.sources.CDSView(
        id='489def54-435e-4253-8338-6aad594e8fbf',
        filters=[],
        js_event_callbacks={},
        js_property_callbacks={},
        name=None,
        source=bokeh.models.sources.ColumnDataSource(
            id='b2329f16-b24b-44e7-ac51-27596ef55354',
            callback=None,
            data={'x': [0, 1, 2, 3, 4], 'y': [5, 4, 1, 2, 0]},
            js_event_callbacks={},
            js_property_callbacks={},
            name=None,
            selected=bokeh.models.selections.Selection(
                id='edb967f6-4724-4891-9d97-451345a54e65',
                indices=[],
                js_event_callbacks={},
                js_property_callbacks={},
                line_indices=[],
                multiline_indices={},
                name=None,
                subscribed_events=[],
                tags=[]),
            selection_policy=bokeh.models.selections.UnionRenderers(
                id='7db9ac9c-4cf0-4a9d-afc8-e89d803d6a39',
                js_event_callbacks={},
                js_property_callbacks={},
                name=None,
                subscribed_events=[],
                tags=[]),
            subscribed_events=[],
            tags=[]),
        subscribed_events=[],
        tags=[]),
    visible=True,
    x_range_name='default',
    y_range_name='default')

Finally, we can save our interactive plot into the disk with save -function that we imported in the beginning. All interactive plots are typically saved as html files which you can open in a web-browser.

# Give output filepath
outfp = r"/home/geo/points.html"

# Save the plot by passing the plot -object and output path
save(obj=p, filename=outfp)

Now open your interactive points.html plot by double-clicking it which should open it in a web browser.

This is how it should look like:

Bokeh Plot

It is interactive. You can drag the plot by clicking with left mouse and dragging. There are also specific buttons on the right side of the plot by default which you can select on and off:

../../_images/Pan.png

Pan button allows you to switch the dragging possibility on and off (on by default).

../../_images/BoxZoom.png

BoxZoom button allows you to zoom into an area that you define by left dragging with mouse an area of your interest.

../../_images/Save.png

Save button allows you to save your interactive plot as a static low resolution .png file.

../../_images/WheelZoom.png

WheelZoom button allows you to use mouse wheel to zoom in and out.

../../_images/Reset.png

Reset button allows you to use reset the plot as it was in the beginning.


Creating interactive maps using Bokeh and Geopandas

Now we now khow how to make a really simple interactive point plot using Bokeh. What about creating such a map from a Shapefile of points? Of course we can do that, and we can use Geopandas for achieving that goal which is nice!

Creating an interactive Bokeh map from Shapefile(s) contains typically following steps:

  1. Read the Shapefile into GeoDataFrame
  2. Calculate the x and y coordinates of the geometries into separate columns
  3. Convert the GeoDataFrame into a Bokeh DataSource
  4. Plot the x and y coordinates as points, lines or polygons (which are in Bokeh words: circle, multi_line and patches)

Let’s practice these things and see how we can first create an interactive point map, then a map with lines, and finally a map with polygons where we also add those points and lines into our final map.

Point map

Let’s first make a map out of those addresses that we geocoded in the Lesson 3. That Shapefile is provided for you in the data folder that you downloaded.

Read the data using geopandas which is the first step.

import geopandas as gpd

# File path
points_fp = r"/home/geo/data/addresses.shp"

# Read the data
points = gpd.read_file(points_fp)

Let’s see what we have.

In [6]: points.head()
Out[6]: 
                                             address                  ...                                               geometry
0              Kampinkuja 1, 00100 Helsinki, Finland                  ...                          POINT (24.9301701 60.1683731)
1               Kaivokatu 8, 00101 Helsinki, Finland                  ...                          POINT (24.9418933 60.1698665)
2  Hermanstads strandsväg 1, 00580 Helsingfors, F...                  ...                   POINT (24.9774004 60.18735880000001)
3                  Itäväylä, 00900 Helsinki, Finland                  ...                   POINT (25.0919641 60.21448089999999)
4         Tyynenmerenkatu 9, 00220 Helsinki, Finland                  ...                          POINT (24.9214846 60.1565781)

[5 rows x 3 columns]

Okey, so we have the address and id columns plus the geometry column as attributes.

Now, as a second step, we need to calculate the x and y coordinates of those points. Unfortunately there is not a ready made function in geopandas to do that.

Thus, let’s create our own function called getPointCoords() which will return the x or y coordinate of a given geometry. It shall have two parameters: geom and coord_type where the first one should be a Shapely geometry object and coord_type should be either 'x' or 'y'.

def getPointCoords(row, geom, coord_type):
    """Calculates coordinates ('x' or 'y') of a Point geometry"""
    if coord_type == 'x':
        return row[geom].x
    elif coord_type == 'y':
        return row[geom].y

Okey great. Let’s then use our function in a similar manner as we did before when classifying data using .apply() function.

# Calculate x coordinates
In [7]: points['x'] = points.apply(getPointCoords, geom='geometry', coord_type='x', axis=1)

# Calculate y coordinates
In [8]: points['y'] = points.apply(getPointCoords, geom='geometry', coord_type='y', axis=1)

# Let's see what we have now
In [9]: points.head()
Out[9]: 
                                             address    ...              y
0              Kampinkuja 1, 00100 Helsinki, Finland    ...      60.168373
1               Kaivokatu 8, 00101 Helsinki, Finland    ...      60.169866
2  Hermanstads strandsväg 1, 00580 Helsingfors, F...    ...      60.187359
3                  Itäväylä, 00900 Helsinki, Finland    ...      60.214481
4         Tyynenmerenkatu 9, 00220 Helsinki, Finland    ...      60.156578

[5 rows x 5 columns]

Okey great! Now we have the x and y columns in our GeoDataFrame.

The third step, is to convert our DataFrame into a format that Bokeh can understand. Thus, we will convert our DataFrame into ColumnDataSource which is a Bokeh-specific way of storing the data.

Note

Bokeh ColumnDataSource do not understand Shapely geometry -objects. Thus, we need to remove the geometry -column before convert our DataFrame into a ColumnDataSouce.

Let’s make a copy of our points GeoDataFrame where we drop the geometry column.

# Make a copy and drop the geometry column
In [10]: p_df = points.drop('geometry', axis=1).copy()

# See head
In [11]: p_df.head(2)
Out[11]: 
                                 address    id          x          y
0  Kampinkuja 1, 00100 Helsinki, Finland  1001  24.930170  60.168373
1   Kaivokatu 8, 00101 Helsinki, Finland  1002  24.941893  60.169866

Now we can convert that pandas DataFrame into a ColumnDataSource.

In [12]: from bokeh.models import ColumnDataSource

# Point DataSource
In [13]: psource = ColumnDataSource(p_df)

# What is it?
In [14]: psource
Out[14]: 
bokeh.models.sources.ColumnDataSource(
    id='045a8920-ce89-4f10-9aee-3a51f614df61',
    callback=None,
    data={'address': ['Kampinkuja 1, 00100 Helsinki, Finland',
      'Kaivokatu 8, 00101 Helsinki, Finland',
      'Hermanstads strandsväg 1, 00580 Helsingfors, Finland',
      'Itäväylä, 00900 Helsinki, Finland',
      'Tyynenmerenkatu 9, 00220 Helsinki, Finland',
      'Kontulantie 18, 00940 Helsinki, Finland',
      'Itäväylä, 00800 Helsinki, Finland',
      'Tapulikaupungintie 3, 00750 Helsinki, Finland',
      'Sompionpolku 2, 00730 Helsinki, Finland',
      'Atomitie 5, 00370 Helsinki, Finland',
      'Rautatientori 1, 00100 Helsinki, Finland',
      'Kuparitie 8, 00440 Helsinki, Finland',
      'Trumstigen 8, 00420 Helsingfors, Finland',
      'Kullatorpsvägen 1, 00620 Helsingfors, Finland',
      'Malminkaari 15, 00700 Helsinki, Finland',
      'Kylätie 23, 00320 Helsinki, Finland',
      'Malminkartanontie 17, 00410 Helsinki, Finland',
      'Åggelby torg 2b, 00640 Helsingfors, Finland',
      'Bangårdsvägen 6, 00101 Helsingfors, Finland',
      'Pitäjänmäentie 15, 00370 Helsinki, Finland',
      'Eskolantie 2, 00720 Helsinki, Finland',
      'Tattariharjuntie, 00700 Helsinki, Finland',
      'Tallinnanaukio 1, 00930 Helsinki, Finland',
      'Tyynylaavantie 7, 00980 Helsinki, Finland',
      'Varvasmäenkuja, 00920 Helsinki, Finland',
      'Mellungsbackastråket 6, 00970 Helsingfors, Finland',
      'Vasastigen 2, 00101 Helsingfors, Finland',
      'Hiihtäjäntie 2, 00810 Helsinki, Finland',
      'Ukko-Pekan porras, 00570 Helsinki, Finland',
      'Siltasaarenkatu 16, 00530 Helsinki, Finland',
      'Urho Kekkosen katu 1, 00100 Helsinki, Finland',
      'Gräsviksgatan 17, 00101 Helsingfors, Finland',
      'Stillahavsgatan 3, 00220 Helsingfors, Finland',
      'Vilhelmsgatan 4, 00101 Helsingfors, Finland',
      'Torisilta, 00980 Helsinki, Finland'],
     'id': [1001,
      1002,
      1003,
      1004,
      1005,
      1006,
      1007,
      1008,
      1009,
      1010,
      1011,
      1012,
      1013,
      1014,
      1015,
      1016,
      1017,
      1018,
      1019,
      1020,
      1021,
      1022,
      1023,
      1024,
      1025,
      1026,
      1027,
      1028,
      1029,
      1030,
      1031,
      1032,
      1033,
      1034,
      1035],
     'index': [0,
      1,
      2,
      3,
      4,
      5,
      6,
      7,
      8,
      9,
      10,
      11,
      12,
      13,
      14,
      15,
      16,
      17,
      18,
      19,
      20,
      21,
      22,
      23,
      24,
      25,
      26,
      27,
      28,
      29,
      30,
      31,
      32,
      33,
      34],
     'x': {'__ndarray__': 'kTevoB/uOEBP0lfrH/E4QOoYoek2+jhA9OeR9YoXOUCExCxq5us4QMTVXMnpFDlAoZ3TLNAKOUDpZRTLLQk5QPoO2ZtzBzlAdbIvNDLfOEAoJ9pVSPE4QAYEhCBR4jhA/RAbLJzgOEDdItUcxfI4QNKOG343AzlABVPNrKXkOEAGu2Hbotw4QCf9GM6Q9zhAbGv6ovPuOEDkh0ojZtw4QAol3eBV/jhACcbBpWMKOUBGUhxR6xM5QKrj53V6JDlA6Ek+G14TOUCZyuw9phs5QNFTiVf+9ThAGPQ50nQHOUDTznpCwAE5QM32MpsR8zhAEuozsQrvOECFkJhFzew4QEti/yvh6zhA/Ct12mTyOEB5R3zd2B45QA==',
      'dtype': 'float64',
      'shape': (35,)},
     'y': {'__ndarray__': 'WAPvP40VTkDOF3svvhVOQBZPh1/7F05A7Y4xHHQbTkCJYk3AChROQKWRKuUQHk5AeXNdnQgaTkBiFjXzPyNOQD12vG62IU5AvvjsyngcTkBfPv536RVOQCf0n9qFHU5AyVpDqb0eTkDCWbZ7XhxOQPG/phgxIE5A5xJYSuAbTkABAjFk0B9OQDrJVpdTHU5A0xedh24ZTkC4LMt8rBxOQNXUDW8xH05Au+B440kfTkA4SrlW1hpOQA5LubGPGk5AUoGTbeAcTkCEXbmDfR5OQCreEsoMGE5Aue5Sj9kYTkBPsLWtHBhOQEctgyD2Fk5A+WXVjLEVTkCnZRDEHhVOQFpnfF9cFE5AI+pPLAEWTkCtVGF9TRpOQA==',
      'dtype': 'float64',
      'shape': (35,)}},
    js_event_callbacks={},
    js_property_callbacks={},
    name=None,
    selected=bokeh.models.selections.Selection(
        id='f892939a-34ef-4cab-b595-94409e8c357e',
        indices=[],
        js_event_callbacks={},
        js_property_callbacks={},
        line_indices=[],
        multiline_indices={},
        name=None,
        subscribed_events=[],
        tags=[]),
    selection_policy=bokeh.models.selections.UnionRenderers(
        id='59ee05f4-4f0f-4309-859c-c02cd9176762',
        js_event_callbacks={},
        js_property_callbacks={},
        name=None,
        subscribed_events=[],
        tags=[]),
    subscribed_events=[],
    tags=[])

Okey, so now we have a ColumnDataSource object that has our data stored in a way that Bokeh wants it.

Finally, we can make a Point map of those points in a fairly similar manner as in the first example. Now instead of passing the coordinate lists, we can pass the data as a source for the plot with column names containing those coordinates.

# Initialize our plot figure
In [15]: p = figure(title="A map of address points from a Shapefile")

# Add the points to the map from our 'psource' ColumnDataSource -object
In [16]: p.circle('x', 'y', source=psource, color='red', size=10)
Out[16]: 
bokeh.models.renderers.GlyphRenderer(
    id='a835d43b-baca-4a5f-b439-624e42f2a8d0',
    data_source=bokeh.models.sources.ColumnDataSource(
        id='045a8920-ce89-4f10-9aee-3a51f614df61',
        callback=None,
        data={'address': ['Kampinkuja 1, 00100 Helsinki, Finland',
          'Kaivokatu 8, 00101 Helsinki, Finland',
          'Hermanstads strandsväg 1, 00580 Helsingfors, Finland',
          'Itäväylä, 00900 Helsinki, Finland',
          'Tyynenmerenkatu 9, 00220 Helsinki, Finland',
          'Kontulantie 18, 00940 Helsinki, Finland',
          'Itäväylä, 00800 Helsinki, Finland',
          'Tapulikaupungintie 3, 00750 Helsinki, Finland',
          'Sompionpolku 2, 00730 Helsinki, Finland',
          'Atomitie 5, 00370 Helsinki, Finland',
          'Rautatientori 1, 00100 Helsinki, Finland',
          'Kuparitie 8, 00440 Helsinki, Finland',
          'Trumstigen 8, 00420 Helsingfors, Finland',
          'Kullatorpsvägen 1, 00620 Helsingfors, Finland',
          'Malminkaari 15, 00700 Helsinki, Finland',
          'Kylätie 23, 00320 Helsinki, Finland',
          'Malminkartanontie 17, 00410 Helsinki, Finland',
          'Åggelby torg 2b, 00640 Helsingfors, Finland',
          'Bangårdsvägen 6, 00101 Helsingfors, Finland',
          'Pitäjänmäentie 15, 00370 Helsinki, Finland',
          'Eskolantie 2, 00720 Helsinki, Finland',
          'Tattariharjuntie, 00700 Helsinki, Finland',
          'Tallinnanaukio 1, 00930 Helsinki, Finland',
          'Tyynylaavantie 7, 00980 Helsinki, Finland',
          'Varvasmäenkuja, 00920 Helsinki, Finland',
          'Mellungsbackastråket 6, 00970 Helsingfors, Finland',
          'Vasastigen 2, 00101 Helsingfors, Finland',
          'Hiihtäjäntie 2, 00810 Helsinki, Finland',
          'Ukko-Pekan porras, 00570 Helsinki, Finland',
          'Siltasaarenkatu 16, 00530 Helsinki, Finland',
          'Urho Kekkosen katu 1, 00100 Helsinki, Finland',
          'Gräsviksgatan 17, 00101 Helsingfors, Finland',
          'Stillahavsgatan 3, 00220 Helsingfors, Finland',
          'Vilhelmsgatan 4, 00101 Helsingfors, Finland',
          'Torisilta, 00980 Helsinki, Finland'],
         'id': [1001,
          1002,
          1003,
          1004,
          1005,
          1006,
          1007,
          1008,
          1009,
          1010,
          1011,
          1012,
          1013,
          1014,
          1015,
          1016,
          1017,
          1018,
          1019,
          1020,
          1021,
          1022,
          1023,
          1024,
          1025,
          1026,
          1027,
          1028,
          1029,
          1030,
          1031,
          1032,
          1033,
          1034,
          1035],
         'index': [0,
          1,
          2,
          3,
          4,
          5,
          6,
          7,
          8,
          9,
          10,
          11,
          12,
          13,
          14,
          15,
          16,
          17,
          18,
          19,
          20,
          21,
          22,
          23,
          24,
          25,
          26,
          27,
          28,
          29,
          30,
          31,
          32,
          33,
          34],
         'x': {'__ndarray__': 'kTevoB/uOEBP0lfrH/E4QOoYoek2+jhA9OeR9YoXOUCExCxq5us4QMTVXMnpFDlAoZ3TLNAKOUDpZRTLLQk5QPoO2ZtzBzlAdbIvNDLfOEAoJ9pVSPE4QAYEhCBR4jhA/RAbLJzgOEDdItUcxfI4QNKOG343AzlABVPNrKXkOEAGu2Hbotw4QCf9GM6Q9zhAbGv6ovPuOEDkh0ojZtw4QAol3eBV/jhACcbBpWMKOUBGUhxR6xM5QKrj53V6JDlA6Ek+G14TOUCZyuw9phs5QNFTiVf+9ThAGPQ50nQHOUDTznpCwAE5QM32MpsR8zhAEuozsQrvOECFkJhFzew4QEti/yvh6zhA/Ct12mTyOEB5R3zd2B45QA==',
          'dtype': 'float64',
          'shape': (35,)},
         'y': {'__ndarray__': 'WAPvP40VTkDOF3svvhVOQBZPh1/7F05A7Y4xHHQbTkCJYk3AChROQKWRKuUQHk5AeXNdnQgaTkBiFjXzPyNOQD12vG62IU5AvvjsyngcTkBfPv536RVOQCf0n9qFHU5AyVpDqb0eTkDCWbZ7XhxOQPG/phgxIE5A5xJYSuAbTkABAjFk0B9OQDrJVpdTHU5A0xedh24ZTkC4LMt8rBxOQNXUDW8xH05Au+B440kfTkA4SrlW1hpOQA5LubGPGk5AUoGTbeAcTkCEXbmDfR5OQCreEsoMGE5Aue5Sj9kYTkBPsLWtHBhOQEctgyD2Fk5A+WXVjLEVTkCnZRDEHhVOQFpnfF9cFE5AI+pPLAEWTkCtVGF9TRpOQA==',
          'dtype': 'float64',
          'shape': (35,)}},
        js_event_callbacks={},
        js_property_callbacks={},
        name=None,
        selected=bokeh.models.selections.Selection(
            id='f892939a-34ef-4cab-b595-94409e8c357e',
            indices=[],
            js_event_callbacks={},
            js_property_callbacks={},
            line_indices=[],
            multiline_indices={},
            name=None,
            subscribed_events=[],
            tags=[]),
        selection_policy=bokeh.models.selections.UnionRenderers(
            id='59ee05f4-4f0f-4309-859c-c02cd9176762',
            js_event_callbacks={},
            js_property_callbacks={},
            name=None,
            subscribed_events=[],
            tags=[]),
        subscribed_events=[],
        tags=[]),
    glyph=bokeh.models.markers.Circle(
        id='cf9b57b4-372e-481b-9621-72ef8389fcb4',
        angle={'units': 'rad', 'value': 0.0},
        fill_alpha={'value': 1.0},
        fill_color={'value': 'red'},
        js_event_callbacks={},
        js_property_callbacks={},
        line_alpha={'value': 1.0},
        line_cap='butt',
        line_color={'value': 'red'},
        line_dash=[],
        line_dash_offset=0,
        line_join='bevel',
        line_width={'value': 1},
        name=None,
        radius=None,
        radius_dimension='x',
        size={'units': 'screen', 'value': 10},
        subscribed_events=[],
        tags=[],
        x={'field': 'x'},
        y={'field': 'y'}),
    hover_glyph=None,
    js_event_callbacks={},
    js_property_callbacks={},
    level='glyph',
    muted=False,
    muted_glyph=None,
    name=None,
    nonselection_glyph=bokeh.models.markers.Circle(
        id='e5ca9245-5536-49ea-b71e-0574a38b68ee',
        angle={'units': 'rad', 'value': 0.0},
        fill_alpha={'value': 0.1},
        fill_color={'value': '#1f77b4'},
        js_event_callbacks={},
        js_property_callbacks={},
        line_alpha={'value': 0.1},
        line_cap='butt',
        line_color={'value': '#1f77b4'},
        line_dash=[],
        line_dash_offset=0,
        line_join='bevel',
        line_width={'value': 1},
        name=None,
        radius=None,
        radius_dimension='x',
        size={'units': 'screen', 'value': 10},
        subscribed_events=[],
        tags=[],
        x={'field': 'x'},
        y={'field': 'y'}),
    selection_glyph=None,
    subscribed_events=[],
    tags=[],
    view=bokeh.models.sources.CDSView(
        id='122cb770-79a7-48eb-9274-8e8aa6c069cc',
        filters=[],
        js_event_callbacks={},
        js_property_callbacks={},
        name=None,
        source=bokeh.models.sources.ColumnDataSource(
            id='045a8920-ce89-4f10-9aee-3a51f614df61',
            callback=None,
            data={'address': ['Kampinkuja 1, 00100 Helsinki, Finland',
              'Kaivokatu 8, 00101 Helsinki, Finland',
              'Hermanstads strandsväg 1, 00580 Helsingfors, Finland',
              'Itäväylä, 00900 Helsinki, Finland',
              'Tyynenmerenkatu 9, 00220 Helsinki, Finland',
              'Kontulantie 18, 00940 Helsinki, Finland',
              'Itäväylä, 00800 Helsinki, Finland',
              'Tapulikaupungintie 3, 00750 Helsinki, Finland',
              'Sompionpolku 2, 00730 Helsinki, Finland',
              'Atomitie 5, 00370 Helsinki, Finland',
              'Rautatientori 1, 00100 Helsinki, Finland',
              'Kuparitie 8, 00440 Helsinki, Finland',
              'Trumstigen 8, 00420 Helsingfors, Finland',
              'Kullatorpsvägen 1, 00620 Helsingfors, Finland',
              'Malminkaari 15, 00700 Helsinki, Finland',
              'Kylätie 23, 00320 Helsinki, Finland',
              'Malminkartanontie 17, 00410 Helsinki, Finland',
              'Åggelby torg 2b, 00640 Helsingfors, Finland',
              'Bangårdsvägen 6, 00101 Helsingfors, Finland',
              'Pitäjänmäentie 15, 00370 Helsinki, Finland',
              'Eskolantie 2, 00720 Helsinki, Finland',
              'Tattariharjuntie, 00700 Helsinki, Finland',
              'Tallinnanaukio 1, 00930 Helsinki, Finland',
              'Tyynylaavantie 7, 00980 Helsinki, Finland',
              'Varvasmäenkuja, 00920 Helsinki, Finland',
              'Mellungsbackastråket 6, 00970 Helsingfors, Finland',
              'Vasastigen 2, 00101 Helsingfors, Finland',
              'Hiihtäjäntie 2, 00810 Helsinki, Finland',
              'Ukko-Pekan porras, 00570 Helsinki, Finland',
              'Siltasaarenkatu 16, 00530 Helsinki, Finland',
              'Urho Kekkosen katu 1, 00100 Helsinki, Finland',
              'Gräsviksgatan 17, 00101 Helsingfors, Finland',
              'Stillahavsgatan 3, 00220 Helsingfors, Finland',
              'Vilhelmsgatan 4, 00101 Helsingfors, Finland',
              'Torisilta, 00980 Helsinki, Finland'],
             'id': [1001,
              1002,
              1003,
              1004,
              1005,
              1006,
              1007,
              1008,
              1009,
              1010,
              1011,
              1012,
              1013,
              1014,
              1015,
              1016,
              1017,
              1018,
              1019,
              1020,
              1021,
              1022,
              1023,
              1024,
              1025,
              1026,
              1027,
              1028,
              1029,
              1030,
              1031,
              1032,
              1033,
              1034,
              1035],
             'index': [0,
              1,
              2,
              3,
              4,
              5,
              6,
              7,
              8,
              9,
              10,
              11,
              12,
              13,
              14,
              15,
              16,
              17,
              18,
              19,
              20,
              21,
              22,
              23,
              24,
              25,
              26,
              27,
              28,
              29,
              30,
              31,
              32,
              33,
              34],
             'x': {'__ndarray__': 'kTevoB/uOEBP0lfrH/E4QOoYoek2+jhA9OeR9YoXOUCExCxq5us4QMTVXMnpFDlAoZ3TLNAKOUDpZRTLLQk5QPoO2ZtzBzlAdbIvNDLfOEAoJ9pVSPE4QAYEhCBR4jhA/RAbLJzgOEDdItUcxfI4QNKOG343AzlABVPNrKXkOEAGu2Hbotw4QCf9GM6Q9zhAbGv6ovPuOEDkh0ojZtw4QAol3eBV/jhACcbBpWMKOUBGUhxR6xM5QKrj53V6JDlA6Ek+G14TOUCZyuw9phs5QNFTiVf+9ThAGPQ50nQHOUDTznpCwAE5QM32MpsR8zhAEuozsQrvOECFkJhFzew4QEti/yvh6zhA/Ct12mTyOEB5R3zd2B45QA==',
              'dtype': 'float64',
              'shape': (35,)},
             'y': {'__ndarray__': 'WAPvP40VTkDOF3svvhVOQBZPh1/7F05A7Y4xHHQbTkCJYk3AChROQKWRKuUQHk5AeXNdnQgaTkBiFjXzPyNOQD12vG62IU5AvvjsyngcTkBfPv536RVOQCf0n9qFHU5AyVpDqb0eTkDCWbZ7XhxOQPG/phgxIE5A5xJYSuAbTkABAjFk0B9OQDrJVpdTHU5A0xedh24ZTkC4LMt8rBxOQNXUDW8xH05Au+B440kfTkA4SrlW1hpOQA5LubGPGk5AUoGTbeAcTkCEXbmDfR5OQCreEsoMGE5Aue5Sj9kYTkBPsLWtHBhOQEctgyD2Fk5A+WXVjLEVTkCnZRDEHhVOQFpnfF9cFE5AI+pPLAEWTkCtVGF9TRpOQA==',
              'dtype': 'float64',
              'shape': (35,)}},
            js_event_callbacks={},
            js_property_callbacks={},
            name=None,
            selected=bokeh.models.selections.Selection(
                id='f892939a-34ef-4cab-b595-94409e8c357e',
                indices=[],
                js_event_callbacks={},
                js_property_callbacks={},
                line_indices=[],
                multiline_indices={},
                name=None,
                subscribed_events=[],
                tags=[]),
            selection_policy=bokeh.models.selections.UnionRenderers(
                id='59ee05f4-4f0f-4309-859c-c02cd9176762',
                js_event_callbacks={},
                js_property_callbacks={},
                name=None,
                subscribed_events=[],
                tags=[]),
            subscribed_events=[],
            tags=[]),
        subscribed_events=[],
        tags=[]),
    visible=True,
    x_range_name='default',
    y_range_name='default')

Great it worked. Now the last thing is to save our map as html file into our computer.

# Output filepath
outfp = r"/home/geo/data/point_map.html"

# Save the map
save(p, outfp)

Now you can open your point map in the browser in a similar manner as in the previous example. Your map should look like following:

Bokeh Plot

Adding interactivity to the map

In Bokeh there are specific set of plot tools that you can add to the plot. Actually all the buttons that you see on the right side of the plot are exactly such tools. It is e.g. possible to interactively show information about the plot objects to the user when placing mouse over an object as you can see from the example on top of this page. The tool that shows information from the plot objects is an inspector called HoverTool that annotate or otherwise report information about the plot, based on the current cursor position.

Let’s see now how this can be done.

First we need to import the HoverTool from bokeh.models that includes .

In [17]: from bokeh.models import HoverTool

Next, we need to initialize our tool.

In [18]: my_hover = HoverTool()

Then, we need to tell to the HoverTool that what information it should show to us. These are defined with tooltips like this:

In [19]: my_hover.tooltips = [('Address of the point', '@address')]

From the above we can see that tooltip should be defined with a list of tuple(s) where the first item is the name or label for the information that will be shown, and the second item is the column-name where that information should be read in your data. The @ character in front of the column-name is important because it tells that the information should be taken from a column named as the text that comes after the character.

Lastly we need to add this new tool into our current plot.

In [20]: p.add_tools(my_hover)

Great! Let’s save this enhanced version of our map as point_map_hover.html and see the result.

# File path
outfp = r"/home/geo/data/point_map_hover.html"

save(p, outfp)
Bokeh Plot

As you can see now the plot shows information about the points and the content is the information derived from column address.

Hint

Of course, you can show information from multiple columns at the same time. This is achieved simply by adding more tooltip variables when defining the tooltips, such as:

my_hover2.tooltips = [('Label1', '@col1'), ('Label2', '@col2'), ('Label3', '@col3')]

Line map

Okey, now we have made a nice point map out of a Shapefile. Let’s see how we can make an interactive map out of a Shapefile that represents metro lines in Helsinki. We follow the same steps than before, i.e. 1) read the data, 2) calculate x and y coordinates, 3) convert the DataFrame into a ColumnDataSource and 4) make the map and save it as html.

Read the data using geopandas which is the first step.

import geopandas as gpd

# File path
metro_fp = r"/home/geo/data/metro.shp"

# Read the data
metro = gpd.read_file(metro_fp)

Let’s see what we have.

In [21]: metro.head()
Out[21]: 
   NUMERO                        ...                                                                   geometry
0   1300M                        ...                          LINESTRING (2561676.997249531 6681346.00195433...
1   1300M                        ...                          LINESTRING (2550919.001803585 6672692.00211347...
2  1300M1                        ...                          LINESTRING (2561676.997249531 6681346.00195433...
3  1300M1                        ...                          LINESTRING (2559946.003624604 6678095.99842650...
4  1300M2                        ...                          LINESTRING (2559946.003624604 6678095.99842650...

[5 rows x 3 columns]

Okey, so we have the address and id columns plus the geometry column as attributes.

Second step is where calculate the x and y coordinates of the nodes of our lines.

Let’s create our own function called getLineCoords() in a similar manner as previously but now we need to modify it a bit so that we can get coordinates out of the Shapely LineString object.

def getLineCoords(row, geom, coord_type):
    """Returns a list of coordinates ('x' or 'y') of a LineString geometry"""
    if coord_type == 'x':
        return list( row[geom].coords.xy[0] )
    elif coord_type == 'y':
        return list( row[geom].coords.xy[1] )

Note

Wondering about what happens here? Take a tour to our earlier materials about LineString attributes. By default Shapely returns the coordinates as a numpy array of the coordinates. Bokeh does not understand arrays, hence we need to convert the array into a list which is why we apply list() -function.

Let’s now apply our function in a similar manner as previously.

# Calculate x coordinates of the line
In [22]: metro['x'] = metro.apply(getLineCoords, geom='geometry', coord_type='x', axis=1)

# Calculate y coordinates of the line
In [23]: metro['y'] = metro.apply(getLineCoords, geom='geometry', coord_type='y', axis=1)

# Let's see what we have now
In [24]: metro.head()
Out[24]: 
   NUMERO                        ...                                                                          y
0   1300M                        ...                          [6681346.001954339, 6681016.996685321, 6680969...
1   1300M                        ...                          [6672692.002113477, 6672713.997145447, 6672737...
2  1300M1                        ...                          [6681346.001954339, 6681016.996685321, 6680969...
3  1300M1                        ...                          [6678095.998426503, 6678179.9976436375, 667824...
4  1300M2                        ...                          [6678095.998426503, 6678008.998522878, 6677957...

[5 rows x 5 columns]

Yep, now we have the x and y columns in our GeoDataFrame.

The third step. Convert the DataFrame (without geometry column) into a ColumnDataSource which, as you remember, is a Bokeh-specific way of storing the data.

# Make a copy and drop the geometry column
In [25]: m_df = metro.drop('geometry', axis=1).copy()

# Point DataSource
In [26]: msource = ColumnDataSource(m_df)

Finally, we can make a map of the metro line and save it in a similar manner as earlier but now instead of plotting circle we need to use a .multiline() -object. Let’s define the line_width to be 3.

# Initialize our plot figure
p = figure(title="A map of the Helsinki metro")

# Add the lines to the map from our 'msource' ColumnDataSource -object
p.multi_line('x', 'y', source=msource, color='red', line_width=3)

# Output filepath
outfp = "/home/geo/data/metro_map.html"

# Save the map
save(p, outfp)

Now you can open your point map in the browser and it should look like following:

Bokeh Plot

Todo

Task:

As you can see we didn’t apply HoverTool for the plot. Try to apply it yourself and use a column called NUMERO from our data as the information.

Polygon map with Points and Lines

It is of course possible to add different layers on top of each other. Let’s visualize a map showing accessibility in Helsinki Region and place a metro line and the address points on top of that.

1st step: Import necessary modules and read the Shapefiles.

from bokeh.plotting import figure, save
from bokeh.models import ColumnDataSource, HoverTool, LogColorMapper
import geopandas as gpd
import pysal as ps

# File paths
grid_fp = r"/home/geo/data/TravelTimes_to_5975375_RailwayStation.shp"
point_fp = r"/home/geo/data/addresses.shp"
metro_fp = r"/home/geo/data/metro.shp"

# Read files
grid = gpd.read_file(grid_fp)
points = gpd.read_file(point_fp)
metro = gpd.read_file(metro_fp)

As usual, we need to make sure that the coordinate reference system is the same in every one of the layers. Let’s use the CRS of the grid layer and apply it to our points and metro line.

# Get the CRS of our grid
In [27]: CRS = grid.crs

In [28]: print(CRS)
{'init': 'epsg:3067'}
# Convert the geometries of metro line and points into that one
points['geometry'] = points['geometry'].to_crs(crs=CRS)
metro['geometry'] = metro['geometry'].to_crs(crs=CRS)

Okey now, the geometries should have similar values:

In [29]: points['geometry'].head(1)
Out[29]: 
0    POINT (385149.4478367225 6671962.669661295)
Name: geometry, dtype: object

In [30]: metro['geometry'].head(1)
Out[30]: 
0    LINESTRING (395534.7026002127 6679490.08463068...
Name: geometry, dtype: object

In [31]: grid['geometry'].head(1)
Out[31]: 
0    POLYGON ((382000.0001358641 6697750.000038058,...
Name: geometry, dtype: object

Indeed, they do. Let’s proceed and parse the x and y values of our grid. Let’s create own function for that as well.

def getPolyCoords(row, geom, coord_type):
    """Returns the coordinates ('x' or 'y') of edges of a Polygon exterior"""

    # Parse the exterior of the coordinate
    exterior = row[geom].exterior

    if coord_type == 'x':
        # Get the x coordinates of the exterior
        return list( exterior.coords.xy[0] )
    elif coord_type == 'y':
        # Get the y coordinates of the exterior
        return list( exterior.coords.xy[1] )

2nd step: Let’s now apply the functions that we have created and parse the x and y coordinates for all of our datasets.

# Get the Polygon x and y coordinates
grid['x'] = grid.apply(getPolyCoords, geom='geometry', coord_type='x', axis=1)
grid['y'] = grid.apply(getPolyCoords, geom='geometry', coord_type='y', axis=1)

# Calculate x and y coordinates of the line
metro['x'] = metro.apply(getLineCoords, geom='geometry', coord_type='x', axis=1)
metro['y'] = metro.apply(getLineCoords, geom='geometry', coord_type='y', axis=1)

# Calculate x and y coordinates of the points
points['x'] = points.apply(getPointCoords, geom='geometry', coord_type='x', axis=1)
points['y'] = points.apply(getPointCoords, geom='geometry', coord_type='y', axis=1)

Great, now we have x and y coordinates for all of our datasets. Let’s see how our grid coordinates look like.

# Show only head of x and y columns
In [32]: grid[['x', 'y']].head(2)
Out[32]: 
                                                   x                                                  y
0  [382000.00013586413, 381750.0001359122, 381750...  [6697750.000038058, 6697750.000038066, 6698000...
1  [382250.0001358146, 382000.00013586413, 382000...  [6697750.000038053, 6697750.000038058, 6698000...

Let’s now classify the travel times of our grid int 5 minute intervals until 200 minutes using a pysal classifier called User_Defined that allows to set our own criteria for class intervals. But first we need to replace the No Data values with a large number so that they wouldn’t be seen as the “best” accessible areas.

# Replace No Data values (-1) with large number (999)
grid = grid.replace(-1, 999)

# Classify our travel times into 5 minute classes until 200 minutes
# Create a list of values where minumum value is 5, maximum value is 200 and step is 5.
breaks = [x for x in range(5, 200, 5)]

# Initialize the classifier and apply it
classifier = ps.User_Defined.make(bins=breaks)
pt_classif = data[['pt_r_tt']].apply(classifier)

# Rename the classified column
pt_classif.columns = ['pt_r_tt_ud']

# Join it back to the grid layer
grid = grid.join(pt_classif)

What do we have now?

In [33]: grid.head(2)
Out[33]: 
   car_m_d     ...      pt_r_tt_ud
0    32297     ...              27
1    32508     ...              26

[2 rows x 18 columns]

Okey, so we have many columns but the new one that we just got is the last one, i.e. pt_r_tt_ud that contains the classes that we reclassified based on the public transportation travel times on 5 minute intervals.

3rd step: Let’s now convert our GeoDataFrames into Bokeh ColumnDataSources (without geometry columns)

# Make a copy, drop the geometry column and create ColumnDataSource
m_df = metro.drop('geometry', axis=1).copy()
msource = ColumnDataSource(m_df)

# Make a copy, drop the geometry column and create ColumnDataSource
p_df = points.drop('geometry', axis=1).copy()
psource = ColumnDataSource(p_df)

# Make a copy, drop the geometry column and create ColumnDataSource
g_df = grid.drop('geometry', axis=1).copy()
gsource = ColumnDataSource(g_df)

Okey, now we are ready to roll and visualize our layers.

4th step: For visualizing the Polygons we need to define the color palette that we are going to use. There are many different ones available but we are now going to use a palette called RdYlBu and use eleven color-classes for the values (defined as RdYlBu11). Let’s prepare our color_mapper.

# Let's first do some coloring magic that converts the color palet into map numbers (it's okey not to understand)
from bokeh.palettes import RdYlBu11 as palette
from bokeh.models import LogColorMapper

# Create the color mapper
color_mapper = LogColorMapper(palette=palette)

Now we are ready to visualize our polygons and add the metro line and the points on top of that. Polygons are visualized using patches objects in Bokeh.

# Initialize our figure
p = figure(title="Travel times with Public transportation to Central Railway station")

# Plot grid
p.patches('x', 'y', source=gsource,
         fill_color={'field': 'pt_r_tt_ud', 'transform': color_mapper},
         fill_alpha=1.0, line_color="black", line_width=0.05)

# Add metro on top of the same figure
p.multi_line('x', 'y', source=msource, color="red", line_width=2)

# Add points on top (as black points)
p.circle('x', 'y', size=3, source=psource, color="black")

# Save the figure
outfp = r"/home/geo/data/travel_time_map.html"
save(p, outfp)
Bokeh Plot

Cool, now we have an interactive map with three layers! As you see, this map does not yet have the same functionalities as the map on top of this page and we won’t go into details how to do them now. If you are interested how to make such a map, you can read the docs for producing advanced bokeh map from here.

Now we move forward to see how we can share interactive maps on GitHub.