allenfrostline

Bokeh Plots in a Blog Post

2019-10-28


Markdown is awesome. It is awesome ‘cause it provides a universal interface to write anything and store ‘em up. However, it’s been a frustration for me being unable to embed bokeh plots in markdown. Compared with static images, interactive plots is way more condensed in information and can sometimes save a real lot of time. Apparently, by the time I wrote this post, I’ve already figured out a way to embed this kind of plots in my blog, but the question is: how does it work?

In fact, bokeh has already provided a function to prepare everything you need. The function components in the bokeh.embed library returns the two parts that you’ll need to copy and paste into the makrdown file (aka. the post). Step by step:

  1. Plot with bokeh, scale layout with the gridplot function specifying sizing_mode as scale_width. Say our final layout object is called layout.
  2. Pass layout to the components function, parse returned values into script and div.
  3. Copy (you may use packages like pyperclip) and paste the head scripts (see below), script and div into the markdown file.

Head scripts (they are actually not always required, but considering ease of use I suggest copying them anyway):

<script src="https://cdn.pydata.org/bokeh/release/bokeh-1.3.4.min.js"></script>
<script src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-1.3.4.min.js"></script>
<script src="https://cdn.pydata.org/bokeh/release/bokeh-tables-1.3.4.min.js"></script>

Whole script that produced the plot at the bottom:

import pyperclip
from bokeh.plotting import figure
from bokeh.layouts import gridplot
from bokeh.embed import components
from bokeh.io import output_file, show
from bokeh.transform import dodge, factor_cmap
from bokeh.sampledata.periodic_table import elements


output_file('periodic.html')

periods = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII']
groups = [str(x) for x in range(1, 19)]

df = elements.copy()
df['atomic mass'] = df['atomic mass'].astype(str)
df['group'] = df['group'].astype(str)
df['period'] = [periods[x-1] for x in df.period]
df = df[df.group!='-']
df = df[df.symbol!='Lr']
df = df[df.symbol!='Lu']

cmap = {
    'alkali metal'         : '#a6cee3',
    'alkaline earth metal' : '#1f78b4',
    'metal'                : '#d93b43',
    'halogen'              : '#999d9a',
    'metalloid'            : '#e08d49',
    'noble gas'            : '#eaeaea',
    'nonmetal'             : '#f1d4Af',
    'transition metal'     : '#599d7A',
}

TOOLTIPS = [
    ('Name', '@name'),
    ('Atomic number', '@{atomic number}'),
    ('Atomic mass', '@{atomic mass}'),
    ('Type', '@metal'),
    ('CPK color', '$color[hex, swatch]:CPK'),
    ('Electronic configuration', '@{electronic configuration}'),
]

p = figure(plot_width=1000, plot_height=450, x_range=groups,
           y_range=list(reversed(periods)), tools='hover',
           toolbar_location=None, tooltips=TOOLTIPS)
r = p.rect('group', 'period', 0.95, 0.95, source=df, fill_alpha=0.6,
           color=factor_cmap('metal',
                             palette=list(cmap.values()),
                             factors=list(cmap.keys())))
text_props = {'source': df, 'text_align': 'left', 'text_baseline': 'middle'}
x = dodge('group', -0.4, range=p.x_range)
p.text(x=x, y='period', text='symbol', text_font_style='bold', **text_props)
p.text(x=x, y=dodge('period', 0.3, range=p.y_range), text='atomic number',
       text_font_size='8pt', **text_props)
p.text(x=x, y=dodge('period', -0.35, range=p.y_range), text='name',
       text_font_size='5pt', **text_props)
p.text(x=x, y=dodge('period', -0.2, range=p.y_range), text='atomic mass',
       text_font_size='5pt', **text_props)
p.text(x=['3', '3'], y=['VI', 'VII'], text=['LA', 'AC'],
       text_align='center', text_baseline='middle')

p.outline_line_color = None
p.grid.grid_line_color = None
p.axis.axis_line_color = None
p.axis.major_tick_line_color = None
p.axis.major_label_standoff = 0
p.hover.renderers = [r] # only hover element boxes

layout = gridplot([[p]], sizing_mode='scale_width', toolbar_location=None)
script, div = components(layout)
script = '<script src="https://cdn.pydata.org/bokeh/release/bokeh-1.3.4.min.js"></script>' + \
         '<script src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-1.3.4.min.js"></script>' + \
         '<script src="https://cdn.pydata.org/bokeh/release/bokeh-tables-1.3.4.min.js"></script>' + \
         script + '\n' + div
pyperclip.copy(script)

(The plot may look ugly on small screens. Try with a computer.)