Add Plots With Hugo Shortcodes - Plotly

December 6, 2019 • edited April 10, 2020

Plotly is my favourite tool to create plots for a lot of reasons. One is that you can create plots with javascript and create interactive graphs. I’m going to show you how to create a shortcode to add plots to your hugo static site.

Introduction

Plotly is an open source library to create interactive plots. If you have been involved in dashboard creation or data analytics. probably you already know it.

I like Plotly because it is really easy to create portable and interactive plots that can be shared as a webpage. This solves the typical problem when you want to share a plot created with specific tools. For Excel, you need Excel. For Python Pandas, you need Python. For PowerBI you need a license… You can always export a PNG image, but this removes the interaction.

In this situation, I would like to display plots in my blog that allow my viewers to interact with the data.

The code

The javascript codes are available in the official documentation and are really simple. The following code will draw a line plot in the html div with id myDiv

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var trace1 = {
  x: [1, 2, 3, 4],
  y: [10, 15, 13, 17],
  type: 'scatter'
};

var trace2 = {
  x: [1, 2, 3, 4],
  y: [16, 5, 11, 9],
  type: 'scatter'
};

var data = [trace1, trace2];

Plotly.newPlot('myDiv', data);

So we need to create a div with a known id inside the post and also run the necessary js code. This is how the short code looks like.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div class="figure">
<div class="figure-plot" id="{{ .Get 0 }}">
	Code could not finish, this are some reasons why this happen.
	- Plot name not defined. The first parameter of the shortcode is the name.
	- There is a syntax error. check browser console.
</div>
<script>
  function draw(){
	test = document.getElementById("{{ .Get 0 }}");
	if (test == null){
		console.log("The plot name is not defined")
		return
	}

	fig = null
	{{ .Inner | safeJS }}

	if (!fig) {
		test.innerText = "ERROR: fig variable is not defined"
		return
	}
	test.innerText = null
	Plotly.plot(test ,fig);
  }
  draw()
</script>
</div>

The line 3-5 are to display a default message in the case the shortcode is not used properly and something fails. The function draw finds the div by id and check if it actually exist. This will print an extra log message to the console if fails. Then it defines the fig variable and evaluates the provided code. This code should assign a value to the fig variable or the shortcode will put an error message ERROR: fig variable is not defined. If it’s defined, plotly will do it’s magic.

This is how the shortcode is used.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{{/*% plot plot2 %*/}}
var trace1 = {
  x: [1, 2, 3, 4],
  y: [10, 15, 13, 17],
  type: 'scatter'
};

var trace2 = {
  x: [1, 2, 3, 4],
  y: [16, 5, 11, 9],
  type: 'scatter'
};

data = [trace1, trace2];
fig = {
  data: data
}
{{/*% /plot %*/}}

And the plot generated

Code could not finish, this are some reasons why this happen. - Plot name not defined. The first parameter of the shortcode is the name. - There is a syntax error. check browser console.

You may wonder why I’m putting the Plotly.plot in the shortcode instead of just allowing the user to put everything in the shortocde body. One reason is to give clear feedback of what is broken. But the most important reason is that this shortcode is intended just to draw plots, not to execute complex javascript. My intention is to use Python to generate the complex structure and then just copy and paste. Here is an example of the plot in the mandelbrot parallel calculation post.

1
2
3
{{/* plot figure1 >*/}}
fig = {"data":[{"x":["1024x1024x1","512x512x2","256x256x4","128x128x8","64x64x16","32x32x32","16x16x64","8x8x128","4x4x256","2x2x512","1x1x1024"],"y":[5.319377513,3.730183194,3.492960513,3.416568462,3.406160365,3.404795796,3.401629099,3.403963371,3.406568944,3.403851128,3.401489335],"name":"1 workers"},{"x":["1024x1024x1","512x512x2","256x256x4","128x128x8","64x64x16","32x32x32","16x16x64","8x8x128","4x4x256","2x2x512","1x1x1024"],"y":[5.105426336,2.403512541,1.769243872,1.720678808,1.705878765,1.702246865,1.705907853,1.731687894,1.819791773,1.954560985,3.404077505],"name":"2 workers"},{"x":["1024x1024x1","512x512x2","256x256x4","128x128x8","64x64x16","32x32x32","16x16x64","8x8x128","4x4x256","2x2x512","1x1x1024"],"y":[4.665434148,1.840141806,1.23159353,1.156602142,1.139298289,1.135465838,1.145931396,1.190932724,1.324346159,1.955651422,3.401555276],"name":"3 workers"},{"x":["1024x1024x1","512x512x2","256x256x4","128x128x8","64x64x16","32x32x32","16x16x64","8x8x128","4x4x256","2x2x512","1x1x1024"],"y":[3.277424857,1.508889857,1.000251994,0.877004851,0.855757891,0.851836168,0.859728354,0.902649372,1.012687753,1.955834209,3.404003372],"name":"4 workers"},{"x":["1024x1024x1","512x512x2","256x256x4","128x128x8","64x64x16","32x32x32","16x16x64","8x8x128","4x4x256","2x2x512","1x1x1024"],"y":[2.687810723,1.299930746,0.848919389,0.71687461,0.697146793,0.686797944,0.695021475,0.733307298,0.782258103,1.958691928,3.406197102],"name":"5 workers"},{"x":["1024x1024x1","512x512x2","256x256x4","128x128x8","64x64x16","32x32x32","16x16x64","8x8x128","4x4x256","2x2x512","1x1x1024"],"y":[2.419063112,1.172598442,0.770078822,0.636418239,0.59517132,0.58758251,0.598871324,0.655497364,0.722161112,1.955789037,3.40305645],"name":"6 workers"},{"x":["1024x1024x1","512x512x2","256x256x4","128x128x8","64x64x16","32x32x32","16x16x64","8x8x128","4x4x256","2x2x512","1x1x1024"],"y":[1.991583443,1.033091053,0.712726608,0.605582734,0.578420338,0.577574125,0.580463384,0.652894185,0.691949137,1.970116638,3.452580344],"name":"7 workers"},{"x":["1024x1024x1","512x512x2","256x256x4","128x128x8","64x64x16","32x32x32","16x16x64","8x8x128","4x4x256","2x2x512","1x1x1024"],"y":[1.942149854,0.947873093,0.683032395,0.605635427,0.575207906,0.573205018,0.580969231,0.653116053,0.72285752,1.954330764,3.407772597],"name":"8 workers"}],"layout":{"title":"Benchmark results","yaxis":{"title":"s/op","type":"log","autorange":true},"shapes":[{"type":"line","xref":"paper","yref":"y","y0":3.40394218,"x0":0,"y1":3.40394218,"x1":1,"line":{"width":1}},{"type":"line","xref":"paper","yref":"y","y0":1.70197109,"x0":0,"y1":1.70197109,"x1":1,"line":{"width":1}},{"type":"line","xref":"paper","yref":"y","y0":1.1346473933333334,"x0":0,"y1":1.1346473933333334,"x1":1,"line":{"width":1}},{"type":"line","xref":"paper","yref":"y","y0":0.850985545,"x0":0,"y1":0.850985545,"x1":1,"line":{"width":1}},{"type":"line","xref":"paper","yref":"y","y0":0.680788436,"x0":0,"y1":0.680788436,"x1":1,"line":{"width":1}},{"type":"line","xref":"paper","yref":"y","y0":0.5673236966666667,"x0":0,"y1":0.5673236966666667,"x1":1,"line":{"width":1}}],"margin":{"l":25,"r":5,"b":75,"t":50,"pad":4}},"config":{"responsive":true}}
{{/*< /plot >*/}}
Code could not finish, this are some reasons why this happen. - Plot name not defined. The first parameter of the shortcode is the name. - There is a syntax error. check browser console.

The next howto project will be of how to setup a Python Jupyter Notebook environment to easily generate this input.

Just one last thing. To properly visualize the plot it is recommended to adjust the margins and enable responsive behaviour. This can be done easily appending the blocks layout and config to the figure.

1
2
3
4
5
6
7
fig = {
  "data":[],
  "layout":{
    "margin":{"l":25,"r":5,"b":75,"t":50,"pad":4}
  },
  "config":{"responsive":true}
}

Everything else is covered by the plotly docs. Take a look to it and you will find really easy to draw any plot!

Importing plotly.js

The last important thing that must be done is to import the plotly.js file into your webpage. How to do this depends on your template. You can add the plotly.js to your custom javacript files.

In my case, I’ve modified the post/single.html template, because I only need plots in posts, and included the following snippet in the headers section.

1
2
3
{{- if .Params.Plotly }}
<script src="https://cdn.plot.ly/plotly-1.50.0.min.js"></script>
{{- end }}

Now, if the post contains plots, I have to add a parameter plotly: true and the plotly.js will be imported.

Conclusion

This is one of the reasons I’ve chosen Hugo for my blog is that I can do this sort of things. Customization!

References

howtoplotlyhugo

Python Jupyter Notebook With Plotly Support

2019-12-02 Weekend Learnings