Plots in Hugo

2017-11-01

I want to put interactive plots in my posts, ideally from Python source, with minimal development overhead. How can I do that in Hugo?

It seems that the “right” solution for “putting dynamic plots on a webpage” would be to use D3.js. For “sharing dynamic plots created with Python” I should probably just use Jupyter notebooks. But I don’t want to do either of those things, dammit! I want to create plots in my Python workflow, and present them here, in the same place I put other web stuff, while retaining some amount of interactivity. Fortunately there are some slick options, but I’ll need to evaluate them.

Bokeh

Bokeh embedding looks promising. And indeed, it works painlessly:

$$y = \frac{x^{2n+1}-x}{x^{2n}+1}$$

Similarly to getting equation support working, I just added a few includes to the page layout, toggled by a frontmatter variable bokeh:

<!-- Bokeh -->
{{ if .Params.bokeh }}
<link rel="stylesheet" href="https://cdn.pydata.org/bokeh/release/bokeh-0.12.10.min.css" type="text/css" />
<script type="text/javascript" src="https://cdn.pydata.org/bokeh/release/bokeh-0.12.10.min.js"></script>
<script type="text/javascript">
  Bokeh.set_log_level("info");
</script>
{{ end }}

Update 2021/01/24: this code now goes into layouts/partials/bokeh.html (custom), and it is called in layouts/partials/header.html (supplements the existing theme’s header)

Then added some raw html to the markdown:

<div class="bk-root">
    <div class="bk-plotdiv" id="801d6204-c8f6-4fe9-9b10-8f2ae43ed9d1"></div>
</div>
<script type="text/javascript" src="/js/plots-in-hugo-bokeh.js"></script>

The script and the div snippet are both generated by the bokeh.embed.components function described in the above Bokeh link. Multiple plots work with no trouble, just place the div wherever it needs to be in the post.

Note that the bk-plotdiv id must match what’s in the script, and it’s UUIDish, so every time you make some tweak to the source that generates the plot, you need to update both the html and the script.

The rest is just whatever Python nonsense you need to generate the plots, for example:

import numpy as np
from bokeh.plotting import figure, output_file, show
from bokeh.palettes import Category10 as palette
from bokehtools import embed_hugo

# define some curves
x = np.linspace(-3, 3, 200)
N = 20
y = [(x**(2*n+1) - x)/(x**(2*n) + 1) for n in range(1, N+1)]

# create a new plot with a title and axis labels
p = figure(title="meaningless curve example", x_axis_label='x', y_axis_label='y')

# add some line renderers
for n in range(N):
    p.line(x, y[n], legend='curve %d' % n, color=palette[N][n])

embed_hugo(p, 'plots-in-hugo-bokeh.js')

embed_hugo is a small convenience function I wrote. It just creates the script file, and prints out some hints about how to manually include it. I might make this easier to use, with tighter integration with the hugo directory structure, but for now I think this is good enough.

from bokeh.embed import components

def embed_hugo(plot, script_name):
    script, div = components(plot)

    # strip tags for use as external script
    with open(script_name, 'w') as f:
        f.write(script.replace('<script type="text/javascript">', '').replace('</script>', ''))

    print('copy to post body:')
    print('%s' % div)
    print('<script type="text/javascript" src="%s"></script>' % script_name)
    print('copy script to static directory')    

Now I’ll just have to learn how to use Bokeh. The main drawback I see is that animation and interactivity (i.e. custom widgets) are not supported by this usage. This can be done with “Bokeh server”, but I’m not willing to figure out how to set that up here. Other options?

Plotly

I’ve been using Plotly elsewhere recently, it’s a nice library. Unfortunately, it doesn’t look like it’s intended for html embdedding without using the web service, but there are ways.

Example:

The approach is essentially the same. I included one script in the <head> of the theme:

{{ if .Params.plotly }}
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
{{ end }}

Update 2021/01/24: this code now goes into layouts/partials/bokeh.html (custom), and it is called in layouts/partials/header.html (supplements the existing theme’s header)

One div and one script in the post:

<div id="0706912e-e498-4019-80bf-f58ea17a3f54" style="height: 100%; width: 100%;" class="plotly-graph-div"></div>
<script type="text/javascript" src="/js/plots-in-hugo-plotly.js"></script>

And I wrote a similar embed_hugo function for plotly:

from plotly.offline import plot

default_plot_args = dict(
    include_plotlyjs=False,
    output_type='div',
    show_link=False,
    config={'displayModeBar': False}
)

def embed_hugo(fig, script_name, plot_args=None):
    if plot_args is None:
        plot_args = default_plot_args

    html = plot(fig, **plot_args)
    # this contains a div and a script, need to separate them

    div, script = html.split('</div>')  # hack hack

    with open(script_name, 'w') as f:
        f.write(script.replace('<script type="text/javascript">', '').replace('</script>', ''))

    print('copy to post body:')
    print('%s</div>' % div)
    print('<script type="text/javascript" src="%s"></script>' % script_name)
    print('copy script to static directory')

Update 2018/10/07: This works well with org-mode posts too! The div and script tags are identical, just add to the front matter with org syntax:

#+PLOTLY: true

Update 2021/01/24: No longer works in org-mode - not sure what’s different here, but raw html doesn’t get rendered

Also, almost a year later, my embed_hugo function still works, score!

Widgets

No problem!

Animation

It’s equally easy to embed animated plots, but the support for creating animations with Plotly in Python is not great. I’ll probably avoid this for now.

Shortcodes

This was a good opportunity to start learning about Hugo shortcodes. The div and script tags used here aren’t too unwieldy, but a shortcode is… shorter.

Usage:

{{< plotly id="b40a4256-3c82-4f01-8605-85a524e3d112" script="/js/plots-in-hugo-plotly-slider-shortcode.js" >}}

Template:

<div id="{{ .Get "id" }}" style="height: 100%; width: 100%;" class="plotly-graph-div"></div>
<script type="text/javascript" src="{{ .Get "script" }}"></script>

I’ll use a similar template for Bokeh, if I end up using it for anything.

mpld3

D3 is the gold standard, and Matlab/Matplotlib plotting abstractions are second nature to me, but Matplotlib has become a pain to install in OSX. I may look into this at some point, but not now.

pygal

I’ll look into this one if none of the above work out…

Comparison

Plan: expand this as new desirable features become apparent.

Feature Bokeh Plotly mpld3 pygal
easy to embed yes yes ? ?
animation no eh
widgets no yes
depends on a service no no no no

TODO: table styles!

update 2018/10/08: tables styles updated to barebones compact! I was trying to wait to force myself to pick a new theme that I actually like, but now I just want usable tables for the Kilimanjaro posts, so screw it.

hugoweb

A webgl shader experiment

Setting up LaTeX in Hugo