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()
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))