Plotting with Bokeh#
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 Bokeh 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:
img = hv.Image(np.random.rand(10, 10))
fig = hv.render(img)
print('Figure: ', fig)
print('Renderers: ', fig.renderers[-1].glyph)
Figure: figure(id='p1010', ...)
Renderers: Image(id='p1050', ...)
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')
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:
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))
Line properties: ['line_color', 'line_alpha', 'color', 'alpha', 'line_width', 'line_join', 'line_cap', 'line_dash', 'line_dash_offset', 'selection_line_color', 'nonselection_line_color', 'muted_line_color', 'hover_line_color', 'selection_line_alpha', 'nonselection_line_alpha', 'muted_line_alpha', 'hover_line_alpha', 'selection_color', 'nonselection_color', 'muted_color', 'hover_color', 'selection_alpha', 'nonselection_alpha', 'muted_alpha', 'hover_alpha', 'selection_line_width', 'nonselection_line_width', 'muted_line_width', 'hover_line_width', 'selection_line_join', 'nonselection_line_join', 'muted_line_join', 'hover_line_join', 'selection_line_cap', 'nonselection_line_cap', 'muted_line_cap', 'hover_line_cap', 'selection_line_dash', 'nonselection_line_dash', 'muted_line_dash', 'hover_line_dash', 'selection_line_dash_offset', 'nonselection_line_dash_offset', 'muted_line_dash_offset', 'hover_line_dash_offset']
Fill properties: ['fill_color', 'fill_alpha', 'selection_fill_color', 'nonselection_fill_color', 'muted_fill_color', 'hover_fill_color', 'selection_fill_alpha', 'nonselection_fill_alpha', 'muted_fill_alpha', 'hover_fill_alpha']
Text properties: ['text_font', 'text_font_size', 'text_font_style', 'text_color', 'text_alpha', 'text_align', 'text_baseline']
Note 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:
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.
Setting Backend Opts#
HoloViews does not expose every single option from Bokeh.
Instead, HoloViews allow users to attach hooks to modify the plot object directly–but writing these hooks could be cumbersome, especially if it’s only used for a single line of update.
Fortunately, HoloViews allows backend_opts
for the Bokeh backend to configure options by declaring a dictionary with accessor specification for updating the plot components.
For example, here’s how to make the toolbar auto-hide.
hv.Curve(data).opts(
backend_opts={"plot.toolbar.autohide": True}
)
The following is the equivalent, as a hook.
def hook(hv_plot, element):
toolbar = hv_plot.handles['plot'].toolbar
toolbar.autohide = True
hv.Curve(data).opts(hooks=[hook])
Notice how much more concise it is with backend_opts
!
With knowledge of the attributes of Bokeh, it’s possible to configure many other plot components besides toolbar
. Some examples include legend
, colorbar
, xaxis
, yaxis
, and much, much more.
If you’re unsure, simply input your best guess and it’ll try to provide a list of suggestions if there’s an issue.
Sizing Elements#
In the bokeh backend the sizing of plots and specifically layouts of plots is determined in an inside-out or compositional manner. Each subplot can be sized independently and it will fill the allocated space. The sizing is determined by the combination of width, height, aspect and responsive options. In this section we will discover the different approaches to setting plot dimensions and aspects.
Width and height#
The most straightforward approach to specifying the size of a plot is using a width and height specified in pixels. This is the default behavior and makes it easy to achieve precise alignment between plots but does not allow for keeping a constant aspect or responsive resizing. In particular, the specified size includes the axes and any legends or colorbars.
points_a = hv.Points(data)
points_b = hv.Points(data)
points_a.opts(width=300, height=300) + points_b.opts(width=600, height=300)
Frame width and height#
The frame width on the other hand provides precise control over the inner dimensions of a plot, it ensures the actual plot frame matches the specified dimensions exactly. This makes it possible to achieve a precise aspect ratio between the axis scales, without worrying about the size of axes, colorbars, titles and legends, e.g. below we can see two plots defined using explicit frame_width
and frame_height
share the same dimensions despite the fact that one has a colorbar.
xs = ys = np.arange(10)
yy, xx = np.meshgrid(xs, ys)
zz = xx*yy
img = hv.Image(np.random.rand(100, 100))
points_a.opts(frame_width=200, frame_height=200) +\
img.opts(frame_width=200, frame_height=200, colorbar=True, axiswise=True)
Aspect#
The aspect
and data_aspect
options provide control over the scaling of the plot dimensions and the axis limits.
aspect
:#
The aspect
specifies the ratio between the width and height dimensions of the plot. If specified this options takes absolute precedence over the dimensions of the plot but has no effect on the plot’s axis limits. It supports the following options:
float
: A numeric value will scale the ratio of plot width to plot height"equal"
: Sets aspect of the axis scaling to be equal, equivalent todata_aspect=1
"square"
: Ensures the plot dimensions are square.
data_aspect
:#
The data_aspect
specifies the scaling between the x- and y-axis ranges. If specified this option will scale both the plot ranges and dimensions unless explicit aspect
, width
or height
value overrides the plot dimensions:
float
: Sets ratio between the units on the x-scale and the y-scale
xs = np.linspace(0, 10)
ys = np.linspace(0, 5)
img = hv.Image((xs, ys, xs[:, np.newaxis]*np.sin(ys*4)))
(img.options(aspect='equal').relabel('aspect=\'equal\'') +
img.options(aspect='square', colorbar=True, frame_width=300).relabel('aspect=\'square\'') +
img.options(aspect=2).relabel('aspect=2') +
img.options(data_aspect=2, frame_width=300).relabel('data_aspect=2')).cols(2)
Responsive#
Since bokeh plots are rendered within a browser window which can be resized dynamically it supports responsive sizing modes allowing the plot to rescale when the window it is placed in is changed. If enabled, the behavior of responsive
modes depends on whether an aspect or width/height option is set. Specifically responsive mode will only work if at least one dimension of the plot is left undefined, e.g. when width and height or width and aspect are set the plot is set to a fixed size, ignoring any responsive
option. This leaves four different responsive
modes:
scale_both
: If neither a width or a height are defined but a fixed aspect is defined both axes will be scaled up to the maximum size of the container. Scaling ensures that the aspect ratio of the plot is maintained.stretch_both
: If neither a width, height or aspect are defined the plot will stretch to fill all available space.stretch_width
: If a height but neither a width or aspect are defined the plot will stretch to fill all available horizontal space.stretch_height
: If a width but neither a height or aspect are defined the plot will stretch to fill all available vertical space.
Note: In the notebook stretching and scaling the height does not increase the size of a cell.
As a simple example let us declare a plot that has a fixed height but stretches to fit all available horizontal space:
hv.Points(data).opts(height=200, responsive=True, title='stretch width')
Similarly if we declare a fixed aspect
or data_aspect
responsive modes will try to fill all available space but avoid distorting the specified aspect:
img.opts(data_aspect=0.5, responsive=True, title='scale both')
Alignment#
The alignment of a plot in a row or column can be controlled using the align
option. It controls both the vertical alignment in a row and the horizontal alignment in a column and can be set to one of 'start'
, 'center'
or 'end'
(where 'start'
is the default).
Vertical#
points = hv.Points(data).opts(axiswise=True)
img = hv.Image((xs, ys, xs[:, np.newaxis]*np.sin(ys*4)))
img + points.opts(height=200, align='end')
Horizontal#
(img.opts(axiswise=True, width=200, align='center') + points).cols(1)
Grid lines#
Grid lines can be controlled through 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:
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.
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:
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.
If you have multiple plots in a Layout
with the same legend label, muting one of them will automatically mute all of them.
overlay1 = hv.Curve([0, 0], label="A") * hv.Curve([1, 1], label="B")
overlay2 = hv.Curve([2, 2], label="A") * hv.Curve([3, 3], label="B")
layout = overlay1 + overlay2
layout
If you want to turn off this behavior, use .opts(sync_legends=False)
layout.opts(sync_legends=False)
If you want to control the number of legend shown in the Layout
or the position of them show_legends
and legend_position
can be used.
layout.opts(sync_legends=True, show_legends=0, legend_position="top_left")
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.
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.
img.hist(num_bins=100, dimension=['x', 'y'], weight_dimension='z', mean_weighted=True) +\
img.hist(dimension='z')