Plotting with Bokeh


In [ ]:
import numpy as np
import pandas as pd
import holoviews as hv
from holoviews import dim, opts

hv.extension('bokeh')

One of the major design principles of HoloViews is that the declaration of data is completely independent from the plotting implementation. Bokeh provides a powerful platform to generate interactive plots using HTML5 canvas and WebGL, and is ideally suited towards interactive exploration of data. By combining the ease of generating interactive, high-dimensional visualizations with the interactive widgets and fast rendering provided by Bokeh, HoloViews becomes even more powerful.

This user guide will cover various interactive features that bokeh provides which is not covered by the more general user guides, including interactive tools, linked axes and brushing and more. The general principles behind customizing plots and styling the visual elements of a plot are covered in the Style Mapping and Customizing Plots user guides.

Working with bokeh directly

When HoloViews outputs bokeh plots it creates and manipulates bokeh models in the background. If at any time you need access to the underlying plotly representation of an object you can use the hv.render function to convert it. For example let us convert a HoloViews Image to a bokeh Figure, which will let us access and modify every aspect of the plot:


In [ ]:
img = hv.Image(np.random.rand(10, 10))

fig = hv.render(img)

print('Figure: ', fig)
print('Renderers: ', fig.renderers[-1].glyph)

Exporting static files

Bokeh supports static export to png's using Selenium, PhantomJS and pillow, to install the required dependencies run:

conda install selenium phantomjs pillow

alternatively install PhantomJS from npm using:

npm install -g phantomjs-prebuilt

To switch to png output persistently you can run:

hv.output(fig='png')

In [ ]:
violin = hv.Violin(np.random.randn(100))

hv.output(violin, fig='png')

The exported png can also be saved to disk using the save function by changing the file extension from .html, which exports an interactive plot, .png:

hv.save(violin, 'violin.png')

Element Style options

One of the major benefits of bokeh is that it was designed from the ground up with consistency in mind, therefore most style options are a combination of the fill, line, and text style options listed below:


In [ ]:
from holoviews.plotting.bokeh.styles import (line_properties, fill_properties, text_properties)
print("""
Line properties: %s\n
Fill properties: %s\n
Text properties: %s
""" % (line_properties, fill_properties, text_properties))

Not also that most of these options support vectorized style mapping as described in the Style Mapping user guide. Here's an example of HoloViews Elements using a Bokeh backend, with bokeh's style options:


In [ ]:
curve_opts = opts.Curve(line_width=10, line_color='indianred', line_dash='dotted', line_alpha=0.5)
point_opts = opts.Points(fill_color='#00AA00', fill_alpha=0.5, line_width=1, line_color='black', size=5)
text_opts  = opts.Text(text_align='center', text_baseline='middle', text_color='gray', text_font='Arial')

xs = np.linspace(0, np.pi*4, 100)
data = (xs, np.sin(xs))

(hv.Curve(data) + hv.Points(data) + hv.Text(6, 0, 'Here is some text')).opts(
    curve_opts, point_opts, text_opts)

Notice that because the first two plots use the same underlying data, they become linked, such that zooming or panning one of the plots makes the corresponding change on the other.

Sizing Elements

Sizing and aspect of Elements in bokeh are always computed in absolute pixels. To change the size or aspect of an Element set the width and height plot options.


In [ ]:
points_a = hv.Points(data, label='A')
points_b = hv.Points(data, label='B')

points_a.opts(width=300, height=300) + points_b.opts(width=600, height=300)

Grid lines

Grid lines can be controlled throught the combination of show_grid and gridstyle parameters. The gridstyle allows specifying a number of options including:

  • grid_line_color
  • grid_line_alpha
  • grid_line_dash
  • grid_line_width
  • grid_bounds
  • grid_band

These options may also be applied to minor grid lines by prepending the 'minor_' prefix and may be applied to a specific axis by replacing 'grid_ with 'xgrid_' or 'ygrid_'. Here we combine some of these options to generate a complex grid pattern:


In [ ]:
grid_style = {'grid_line_color': 'black', 'grid_line_width': 1.5, 'ygrid_bounds': (0.3, 0.7),
              'minor_xgrid_line_color': 'lightgray', 'xgrid_line_dash': [4, 4]}

hv.Points(np.random.rand(10, 2)).opts(gridstyle=grid_style, show_grid=True, size=5, width=600)

Containers

The bokeh plotting extension also supports a number of additional features relating to container components.

Tabs

Using bokeh, both (Nd)Overlay and (Nd)Layout types may be displayed inside a tabs widget. This may be enabled via a plot option tabs, and may even be nested inside a Layout.


In [ ]:
x,y = np.mgrid[-50:51, -50:51] * 0.1

img = hv.Image(np.sin(x**2+y**2), bounds=(-1,-1,1,1))
(img.relabel('Image') * img.sample(x=0).relabel('Cross-section')).opts(tabs=True)

Another reason to use tabs is that some Layout combinations may not be able to be displayed directly using HoloViews. For example, it is not currently possible to display a GridSpace as part of a Layout in any backend, and this combination will automatically switch to a tab representation for the bokeh backend.

Interactive Legends

When using NdOverlay and Overlay containers each element will get a legend entry, which can be used to interactively toggle the visibility of the element. In this example we will create a number of Histogram elements each with a different mean. By setting a muted_fill_alpha we can define the style of the element when it is de-selected using the legend, simply try tapping on each legend entry to see the effect:


In [ ]:
hv.NdOverlay({i: hv.Histogram(np.histogram(np.random.randn(100)+i*2)) for i in range(5)}).opts(
    'Histogram', width=600, alpha=0.8, muted_fill_alpha=0.1)

The other muted_ options can be used to define other aspects of the Histogram style when it is unselected.

Marginals

The Bokeh backend also supports marginal plots to generate adjoined plots. The most convenient way to build an AdjointLayout is with the .hist() method.


In [ ]:
points = hv.Points(np.random.randn(500,2))
points.hist(num_bins=51, dimension=['x','y'])

When the histogram represents a quantity that is mapped to a value dimension with a corresponding colormap, it will automatically share the colormap, making it useful as a colorbar for that dimension as well as a histogram.


In [ ]:
img.hist(num_bins=100, dimension=['x', 'y'], weight_dimension='z', mean_weighted=True) +\
img.hist(dimension='z')

Tools

Bokeh provides a range of tools to interact with a plot and HoloViews adds a number of tools by default but also makes it easy to add additional tools. The default_tools define the list of tools that are added automatically and usually a user would override only the tools option to add additional tools. By default the default_tools include:

['save', 'pan', 'wheel_zoom', 'box_zoom', 'reset']

Toolbar

The bokeh toolbar is added automatically and will be placed to the right for a single plot and on the top for a layout. Additionally for layouts of plots it will automatically merge the toolbars to avoid crowding the plot. However both behaviors can be customized, the toolbar can be hidden or moved to a different location on a plot by setting the toolbar option to one of:

['above', 'below', 'left', 'right', 'disable', None]

Secondly a layout or grid plot supports the merge_tools option which can be used to maintain one toolbar per plot:


In [ ]:
(hv.Curve([1, 2, 3]).opts(toolbar='above') + hv.Curve([1, 2, 3]).opts(toolbar=None)).opts(merge_tools=False)

Hover tools

Some Elements allow revealing additional data by hovering over the data. To enable the hover tool, simply supply 'hover' as a list to the tools plot option. By default the tool will display information for all the dimensions specified on the element:


In [ ]:
error = np.random.rand(100, 3)
heatmap_data = {(chr(65+i), chr(97+j)):i*j for i in range(5) for j in range(5) if i!=j}
data = [np.random.normal() for i in range(10000)]
hist = np.histogram(data, 20)

points = hv.Points(error)
heatmap = hv.HeatMap(heatmap_data).sort()
histogram = hv.Histogram(hist)
image = hv.Image(np.random.rand(50,50))

(points + heatmap + histogram + image).opts(
    opts.Points(tools=['hover'], size=5), opts.HeatMap(tools=['hover']),
    opts.Image(tools=['hover']), opts.Histogram(tools=['hover']),
    opts.Layout(shared_axes=False)).cols(2)

It is also possible to explicitly declare the columns to display by manually constructing a HoverTool and declaring the tooltips as a list of tuples of the labels and a specification of the dimension name and how to display it (for a complete reference see the bokeh user guide).


In [ ]:
from bokeh.models import HoverTool
from bokeh.sampledata.periodic_table import elements

points = hv.Points(
    elements, ['electronegativity', 'density'],
    ['name', 'symbol', 'metal', 'CPK', 'atomic radius']
).sort('metal')

tooltips = [
    ('Name', '@name'),
    ('Symbol', '@symbol'),
    ('CPK', '$color[hex, swatch]:CPK')
]
hover = HoverTool(tooltips=tooltips)

points.opts(
    tools=[hover], color='metal', cmap='Category20',
    line_color='black', size=dim('atomic radius')/10,
    padding=0.1, width=600, height=400, show_grid=True,
    title='Chemical Elements by Type (scaled by atomic radius)')

Selection tools

Bokeh provides a number of tools for selecting data points including tap, box_select, lasso_select and poly_select. To distinguish between selected and unselected data points we can also control the color and alpha of the selection and nonselection points. You can try out any of these selection tools and see how the plot is affected:


In [ ]:
hv.Points(error).opts(
    color='blue', nonselection_color='red', size=10, tools=['box_select', 'lasso_select', 'tap'])

Selection tool with shared axes and linked brushing

When dealing with complex multi-variate data it is often useful to explore interactions between variables across plots. HoloViews will automatically link the data sources of plots in a Layout if they draw from the same data, allowing for both linked axes and brushing.

We'll see what this looks like in practice using a small dataset of macro-economic data:


In [ ]:
macro_df = pd.read_csv('http://assets.holoviews.org/macro.csv', '\t')

By creating two Points Elements, which both draw their data from the same pandas DataFrame, the two plots become automatically linked. Note that the Linking Plots user guide provides a more explicit way to declare two elements as being linked without having to share the same underlying datastructure. The automated linking behavior can be toggled with the shared_datasource plot option on a Layout or GridSpace. You can try selecting data in one plot, and see how the corresponding data (those on the same rows of the DataFrame, even if the plots show different data, will be highlighted in each.


In [ ]:
(hv.Scatter(macro_df, 'year', 'gdp') + hv.Scatter(macro_df, 'gdp',  'unem')).opts(
    opts.Scatter(tools=['box_select', 'lasso_select']), opts.Layout(shared_axes=True, shared_datasource=True))

A gridmatrix is a clear use case for linked plotting. This operation plots any combination of numeric variables against each other, in a grid, and selecting datapoints in any plot will highlight them in all of them. Such linking can thus reveal how values in a particular range (e.g. very large outliers along one dimension) relate to each of the other dimensions.


In [ ]:
table = hv.Dataset(macro_df, kdims=['year', 'country'])
matrix = hv.operation.gridmatrix(table.groupby('country'))

matrix.select(country=['West Germany', 'United Kingdom', 'United States']).opts(
    opts.Scatter(tools=['box_select', 'lasso_select', 'hover'], border=0, padding=0.1))

Drawing Tools

Another commonly useful set of tools are the drawing tools which are integrated with the linked streams introduced in the Custom Interactivity guide. These tools allow drawing and annotating a plot and accessing the annotation back in Python. The available drawing tools include:

  • PointDraw: The PointDraw stream adds a bokeh tool to the source plot, which allows drawing, dragging and deleting points.
  • BoxEdit: The BoxEdit stream adds a bokeh tool to the source plot, which allows drawing, dragging and deleting boxes.
  • FreehandDraw: The FreehandDraw stream adds a bokeh tool to the source plot, which allows freehand drawing on the plot canvas
  • PolyDraw: The PolyDraw stream adds a bokeh tool to the source plot, which allows drawing, dragging and deleting polygons and paths.
  • PolyEdit: The PolyEdit stream adds a bokeh tool to the source plot, which allows drawing, dragging and deleting vertices on polygons and paths.

Each of the reference notebooks explains the tools in more detail but to get an of how these tools work see the FreehandDraw example below, which allows drawing on the canvas and accessing the drawn data from Python:


In [ ]:
path = hv.Path([])
freehand = hv.streams.FreehandDraw(source=path, num_objects=3)

path.opts(
    opts.Path(active_tools=['freehand_draw'], height=400, line_width=10, width=400))

To access the data from Python, you can access the element property on the stream, which lets us access each drawn line drawn as a separate Path element:


In [ ]:
freehand.element.split()

The Reference Gallery shows examples of all the Elements supported for Bokeh, in a format that can be compared with the corresponding matplotlib versions.

Theming

Bokeh supports theming via the Theme object object which can also be using in HoloViews. Applying a Bokeh theme is useful when you need to set detailed aesthetic options not directly exposed via the HoloViews style options.

To apply a Bokeh theme, you will need to create a Theme object:


In [ ]:
from bokeh.themes.theme import Theme

theme = Theme(
    json={
    'attrs' : {
        'Figure' : {
            'background_fill_color': '#2F2F2F',
            'border_fill_color': '#2F2F2F',
            'outline_line_color': '#444444',
        },
        'Grid': {
            'grid_line_dash': [6, 4],
            'grid_line_alpha': .3,
        },

        'Axis': {
            'major_label_text_color': 'white',
            'axis_label_text_color': 'white',
            'major_tick_line_color': 'white',
            'minor_tick_line_color': 'white',
            'axis_line_color': "white"
        }
    }
})

Instead of supplying a JSON object, you can also create a Bokeh Theme object from a YAML file. Once the Theme object is created, you can apply it by setting it on the theme parameter of the current Bokeh renderer:


In [ ]:
hv.renderer('bokeh').theme = theme

The theme will then be applied to subsequent plots:


In [ ]:
xs = np.linspace(0, np.pi*4, 100)
hv.Curve((xs, np.sin(xs)), label='foo').opts(bgcolor='grey')

You may also supply a name from Bokeh's built-in-themes:


In [ ]:
hv.renderer('bokeh').theme = 'light_minimal'
xs = np.linspace(0, np.pi*4, 100)
hv.Curve((xs, np.sin(xs)), label='foo')

To disable theming, you can set the theme parameter on the Bokeh renderer to None.