SQR-022: Creating new charts with the Bokeh Models API

  • Angelo Fausti

Latest Revision: 2018-06-12

Purpose

Inspired by this post, we show how to use the Bokeh models API to create new charts, in particular we are interested in metric visualization for the LSST DM verification framework.

Setup

This technical note is available as a Jupyter notebook from its GitHub repository.

In [1]:
from bokeh.io import show, output_notebook

from bokeh.models import Plot
from bokeh.models import ColumnDataSource
from bokeh.models import Range, Range1d, DataRange1d, FactorRange
from bokeh.models import LinearAxis, CategoricalAxis
from bokeh.models import LabelSet

from bokeh.models.scales import CategoricalScale
from bokeh.models.markers import Circle
from bokeh.models.glyphs import HBar, Segment

from bokeh.palettes import OrRd, Blues
from bokeh.layouts import row

output_notebook()
Loading BokehJS ...

Sample data to feed our chart. The dataset contains metrics, metric values, and specifications.

In [2]:
source = ColumnDataSource({'metrics': ['AM1', 'AM2', 'AM3'],
                           'values': [8, 11.2, 22.14],
                           'stretch': [5, 5, 10],
                           'design': [10, 10, 15],
                           'minimum': [20, 20, 30]})

Here we follow the additive development approach as suggested in the post. The idea is to explicitly add each Bokeh model as opposed to using the Bokeh plotting API and modifying the plot defaults.

Let’s start by creating an empty plot. The minimum requirement for doing so is to specify the x_range and y_range ranges.

In [3]:
p1 = Plot(x_range=Range(), y_range=Range(), plot_height=400, plot_width=400)
show(p1)
WARNING:bokeh.core.validation.check:W-1000 (MISSING_RENDERERS): Plot has no renderers: Plot(id='ed1c8790-07e1-4eeb-a111-e0e928a78648', ...)

The warning message above simply means that there’s nothing to render yet.

Our new chart will have two plots, p1 that will display the metric percentage deviation from the design, and p2 that will display the actual metric values compared against the specifications.

For the percentage deviation plot we want the x-axis range to be fixed.

In [4]:
p1.x_range = Range1d(-100, 100)

In the y-axis we want to display the metric names, a categorial variable, so we use a FactorRange() for that, and initialize the factors with the categories present in our dataset. We also need to specify an appropriate scale for this axis.

In [5]:
p1.y_range = FactorRange(factors=source.data['metrics'])
p1.y_scale = CategoricalScale()

Let’s compute the percentage deviation and add the HBar() glyph to display hat. We’ll annotate the values by adding a Labelset() close to the bars instead of adding the x-axis to the plot.

In [6]:
p1.outline_line_color = 'white'
p1.title.text = '% deviation from design'
p1.title.align = 'center'
p1.title.text_font_size = '14pt'

source.data['deviation'] = [(value-design)/design*100 for value, design in
                            zip(source.data['values'], source.data['design'])]

source.data['color'] = [OrRd[3][1] if x > 0 else Blues[3][1] for x in
                        source.data['deviation']]


height = 0.5
hbar = HBar(y='metrics', left=0, right='deviation', line_color='color',
            fill_color='color', height=height)

p1.add_glyph(source, hbar)

yaxis = CategoricalAxis()
yaxis.fixed_location = 0
yaxis.major_label_standoff = 100
yaxis.major_label_text_font_size = '14pt'

p1.add_layout(yaxis, 'left')

source.data['x_offset'] = [10 if x > 0 else -35 for x in
                           source.data['deviation']]

source.data['percentage'] = ["{}%".format(int(x)) for x in
                             source.data['deviation']]

percentage = LabelSet(x='deviation', y='metrics', text='percentage',
                      x_offset='x_offset', text_font_size="10pt",
                      level='glyph', source=source)

p1.add_layout(percentage)

show(p1)

The colors indicate if the actual metric value satisfies the design specification (blue) or not (orange).

For displaying the actual metric values, we want the x-axis range to adjust automatically based on the data, so lets use DataRange1d() for that.

In [7]:
p2 = Plot(x_range=DataRange1d(),
          y_range=FactorRange(factors=source.data['metrics']),
          plot_height=400, plot_width=400)
p2.y_scale = CategoricalScale()

Now lets add the HBar() glyph to display the minimum and strech thresholds for each metric, and a Segment() glyph to indicate the design goal.

In [8]:
p2.outline_line_color = 'white'

height = 0.5
hbar = HBar(y='metrics', left='stretch', right='minimum', line_color='lightgray',
            fill_color='lightgray', height=height)

p2.add_glyph(source, hbar)

source.data['y0'] = [(x, -height/2) for x in source.data['metrics']]
source.data['y1'] = [(x, height/2) for x in source.data['metrics']]

segment = Segment(x0='design', y0="y0", x1='design', y1='y1',
                  line_color='black', line_width=4)

p2.add_glyph(source, segment)

xaxis = LinearAxis()
xaxis.axis_label = 'AMx (marcsec)'

p2.add_layout(xaxis, 'below')

show(p2)

Lets add a Circle() marker to display the metric values and a LabelSet() for quoting the actual values.

In [9]:
circle = Circle(x='values', y='metrics', size=16,
                line_color='color', fill_color='color')

p2.add_glyph(source, circle)

source.data['formated_values'] = ["{:.1f}".format(x) for x in source.data['values']]

labels = LabelSet(x='values', y='metrics', text="formated_values", text_font_size="10pt",
                  x_offset='x_offset', y_offset=-7.5, level='glyph', source=source)

p2.add_layout(labels)

show(p2)

Here’s the final layout for the plots.

In [10]:
p1.toolbar.logo = None
p2.toolbar.logo = None
show(row(p1, p2))