Adding Google Charts Integration

Table of Contents

Yesterday I posted about Cloudflare’s cache, and if you didn’t notice (or read far enough down), there’s actual pie charts with data in them for visualization.

Yeah, so now I can add the Google Charts API and draw charts on any page that I like, and the best part is that it was surprisingly simple to do that.

Google Charts

Charts, as I’m going to call it for now, is a two-piece kit. First, you load the chart loader, and then in another script you use callbacks to define the charts, and it will draw them. The recommended example is to add something like this in the <head> section of your webpage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<script src="https://www.gstatic.com/charts/loader.js"></script>
<script>
    google.charts.load('current', { packages: ['corechart'] });
    google.charts.setOnLoadCallback(drawChart);

    function drawChart() {
        // Define the chart to be drawn.
        var data = new google.visualization.DataTable();
        data.addColumn('string', 'Element');
        data.addColumn('number', 'Percentage');
        data.addRows([
            ['Nitrogen', 0.78],
            ['Oxygen', 0.21],
            ['Other', 0.01]
        ]);

        // Instantiate and draw the chart.
        var chart = new google.visualization.PieChart(document.getElementById('myPieChart'));
        chart.draw(data, null);
    }
</script>

And then a <div id="myPieChart"></div> in the main page code, which will be replaced with the chart (as specified in line 18 at the bottom.)

And as a result, you’ll get this:

After that, well, all you need to do is fill in the data, and tell it how to draw a chart, and it will do so. Cool! You can do a lot more, many different charts, interactive charts, dynamically pulling data from other sources.. it’s still JavaScript and you can do anything because it’s just client-side JS with Charts API calls. I, however, see a few problems.

Problems

<head> Evaluation

With how they specify loading the Charts JS, what will happen is that once the browser encounters either of the <script>s, it will stop, request that script (if external), wait for it to load, execute it, wait for it to finish, and then continue on down the page. Because these scripts are in <head> before the page content, this is what would appear to cause a little stall before anything even starts appearing on your screen. And if I had to make more requests or pull data from another source, this is all time that the browser spends waiting.

A solution is to use the async attribute which will load the script in the background, and the browser will only pause to execute, the moment it’s done loading. If multiple scripts are async, then they can execute the moment they’re loaded, with no regard to original order. And in this case, that’s not going to work.

Another solution is the defer attribute which will, like async, load in the background, and then at the end push the script onto a defer queue. This queue is executed in the same order as the script tags were encountered, and only after the main page has been read in and parsed.1 As a result, you’ll see the entire page appear, and then a split-second later, all the script output will just pop into existence.

Content-Security-Policy Violations

I do not allow unsafe-inline script evaluation, which is what they’re recommending for the actual chart data and preparation script. I do, however, allow some scripts that match a certain SHA-256 hash to execute if inline. The problem is that every change, or even, every new chart, is a new SHA-256 hash that I need to add to my already gigantic CSP header, and I really don’t feel like changing my proxy’s header manipulation tables every time I post something new, so I need a workaround.

My Solution

First, I made one change to the baseof.html template, which is the base HTML that’s used for every page. Namely, I added these four lines:

{{ if eq .Params.googleCharts true }}
<script data-cfasync="false" src="https://www.gstatic.com/charts/loader.js"></script>
<script async data-cfasync="false" src="chart_data.js"></script>
{{ end }}

Explaining this, we need to talk about front-matter parameters

Front-Matter Parameters

Every post here has a front-matter block, in my case, a bit of YAML that defines some key/value pairs to be used later. For example, here’s the one for this page:

---
title: "Adding Google Charts Integration"
date: 2020-04-04T03:27:35-04:00
publishDate: 2020-04-18T18:00:00-04:00
draft: true

googleCharts: true

categories: ["Behind the scenes", "Blog improvements"]
tags: ["Google Charts", "Hugo"]
author: "Teknikal_Domain"
---

(Hey you can see when I wrote this!)

Unless that page’s googleCharts key is true, then the scripts won’t load. If it doesn’t exist, like for many pages, then the scripts won’t load. And if it does exist, well we’ll get to that.

Deferred loading

Notice that both scripts have data-cfasync="false". This means that Cloudflare’s Rocket Loader will ignore these two. Because I don’t know if it preserves order, I’m doing this one myself.

To those that know some JS, you may know that we’re not executing our chart code immediately, we’re registering a callback that the Charts API will call when it’s ready. This, by itself, is the end of the page load. However, note that I need to call my chart script after the loader script.

Taking a look, the loader takes anywhere from 100 to 200 ms to load, technically small, but this is already 1/10th to 1/5th of a second. This by itself could be async’d away, but then we need to check execution time. So let’s check execution time.

It takes about 13 ms to execute the loader, and then, for this page, 4 ms to register the callbacks. After DOMContentLoaded is called, it takes 9 ms for the loader to prepare, and another 100 ms to load the chart libraries. The libraries themselves execute for maybe 30 ms, and then the draw callback is called, taking 50 ms by itself.

On the previous post, the chart libraries take closer to 150 ms to load, and then 100 ms to load all four charts.

Since all the loading and executing is already deferred to after page load, I do not need to actually defer them. I could async them, but I need Charts to load before the chart logic. Instead, since Charts is rather small, I async just chart_data.js, meaning that it’s guaranteed to execute after the Charts loader, and if I make it large, won’t impact the rest of the page load that much.

Bypassing CSP

Notice how instead of inlining the chart script, it’s external? This solves two problems:

  1. I don’t need to insert some raw HTML or something for a chart each time, I can just load the respective file.
  2. The CSP already specifies script-src 'self', so I’m not in violation of it, either.

chart_data.js, you’ll notice, is a relative path, meaning it will get loaded from the page resource bundle. In hugo, this means that instead of having a code/adding-google-charts-integration.md, I have a code/adding-google-charts-integration/index.md and a code/adding-google-charts-integration/chart_data.js.

And with that done, then by setting one front-matter key, and including a JavaScript file, I can place charts on any post.

There’s only one remaining problem: I need to tell it where.

Chart <div>

You are supposed to place a <div> element with an id that the chart code can use to display the chart in. Actually, <div> isn’t a requirement, it can be any element with the correct id, but <div> is actually the correct element to use in this case.

Now, I cannot type that into the post markdown, because Hugo (Well, it’s Markdown parser, GoldMark) will strip it out and replace it with a <!-- raw HTML omitted --> comment, meaning it’s effectively worthless (unless I disable this globally in the site config). I did make a shortcode (I’ll talk about those later) for raw HTML, but for now let’s just say that it’s not easy. So instead, I made a shortcode for this too. By inserting a {{< chart myPieChart >}} in this document, that’s where we got the chart to appear, because the first parameter is the id that it inserts into the <div> tag.

So what is a shortcode and what does this one do?

Hugo Shortcodes

A shortcode in Hugo is essentially something that you can insert into your markdown with {{< shortcode-name params >}} or {{% shortcode-name params %}}, and it will replace it with the template that was evaluated from layouts/shortcodes/shortcode-name.html

Note that the use of % % or < > is important: < > means that the output does not need to be parsed anymore and can be included into the final page verbatim, and % % means that the output does need to be parsed before writing it out. Basically, < > tells it the shortcode (should) output plain HTML, and % % tells it that the shortcode (should) output some Markdown to process.

Here’s the code for layouts/shortcodes/chart.html:

<div id="{{ .Get 0 }}" style="{{ with .Get 2 }}height: {{ . }}; {{ end }}{{ with .Get 1 }}width: {{ . }}; {{ end }}margin-left: auto; margin-right: auto"></div>

As a template, it has more of those weird {{ }} constructs. But instead of being themselves shortcodes, these are template parameters. And the .Get parameter gets the argument of a shortcode. This weird with nonsense means that only the text between the with and the end is written if the value specified exists, which means if I don’t specify the width or height, they’re omitted from the element completely, instead of having this syntactically invalid line: style="height: ; width: ; margin-left: auto; margin-right: auto".

Put simply, the chart shortcode is just shorthand for creating a <div>, specifying it’s id, an optional width and height, and then it centers the chart with the auto margin-left and margin-right.

For example, one of the charts in the previous post is {{< chart home-data 900px 300px >}}, specifying that it’s 900 pixels wide (almost the size of the content area), and 300 pixels tall.

And with that, by setting a front-matter key, writing a chart_data.js, and inserting a {{< chart >}} somewhere in a post, I can include just about anything with Google Charts.


  1. Specifically, it starts running through the queue as the final step before firing the DOMContentLoaded event. ↩︎